Repository: specklesystems/speckle-sharp-connectors
Pull request #643: Rhino region conversions
Author: login: KatKatKateryna
Created: 2025-03-05T03:20:43Z | Merged: 2025-03-17T17:16:10Z

<!---

Provide a short summary in the Title above. Use the following template:

category(project): summary

Categories:

* feat: (new feature for the user, not a new feature for build script)
* fix: (bug fix for the user, not a fix to a build script)
* docs: (changes to the documentation)
* style: (formatting, missing semi colons, etc; no production code change)
* refactor: (refactoring production code, eg. renaming a variable)
* test: (adding missing tests, refactoring tests; no production code change)
* chore: (updating grunt tasks etc; no production code change)

Example:

feat(revit): added category filter to send

-->

## Description
Conversion toSpeckle and toHost for Hatches in Rhino. 

New Region geometry class in the hostApps is usually called Region or Polygon (pure geometry), or Hatch ("region" with a pattern or gradient fill). The property "hasHatchPattern" on Region objects indicates which one it is. If the host app only supports one type, conversion defaults to that 1 type. Like in case with Rhino: it only supports Hatch, so any Region from Speckle is received as Hatch in Rhino.

Currently the displayValue is a list of combined boundary and loop curves. We hope that in the future the Viewer can support this geometry and create a fill in the Viewer, without the need to generate and send an extra Mesh. 

Sent:
![image](https://github.com/user-attachments/assets/2e55d363-5a6b-4713-8408-1a26347dbf99)

In Speckle https://app.speckle.systems/projects/16ce7b208c/models/8b96943c98@86a3023149 :
![image](https://github.com/user-attachments/assets/121b6d26-ff15-4739-84b5-81c64c5cb862)

Received back in Rhino (hatch styles ignored):
![image](https://github.com/user-attachments/assets/3bb0f7f2-b90d-4039-a17d-48eb9c71705f)


<!---

Describe your changes, and why you're making them.

Link related github issues here ->
Fixes #85, Fixes #22, Connects #123

-->

## User Value

<!---

Describe in 1 sentence the user value.

This can also be a link to the relevant thread in Speckle community, or a link to the Linear issue.
-->


## Changes:

<!---

- Item 1
- Item 2

-->

## To-do before merge:

<!---

(Optional -- remove this section if not needed)

Include any notes about things that need to happen before this PR is merged, e.g.:

- [ ] Change the base branch

- [ ] Ensure PR #56 is merged

-->

## Screenshots:

<!---

Include a screenshot the before and after.  This can be a screenshot of a plugin, web frontend, or output in a terminal.

-->

## Validation of changes:

<!---

Describe what tests have been added or amended, and why these demonstrate it works and will prevent this feature being accidentally broken by future changes.

-->

## Checklist:

<!---

This checklist is a useful reminder of related tasks to uphold our repo quality. Amend this list as needed for the pr.

-->
- [ ] My commits are related to the pull request and do not amend unrelated code or documentation.
- [ ] I have added appropriate tests.
- [ ] I have updated or added relevant documentation.




Diff:
diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/Raw/RegionToHostConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/Raw/RegionToHostConverter.cs
new file mode 100644
index 000000000..00a07990b
--- /dev/null
+++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/Raw/RegionToHostConverter.cs
@@ -0,0 +1,46 @@
+using Speckle.Converters.Common;
+using Speckle.Converters.Common.Objects;
+using Speckle.Objects;
+using Speckle.Sdk.Common.Exceptions;
+
+namespace Speckle.Converters.Rhino.ToHost.Raw;
+
+public class RegionToHostConverter : ITypedConverter<SOG.Region, RG.Hatch>
+{
+  private readonly ITypedConverter<ICurve, RG.Curve> _curveConverter;
+  private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
+
+  public RegionToHostConverter(
+    IConverterSettingsStore<RhinoConversionSettings> settingsStore,
+    ITypedConverter<ICurve, RG.Curve> curveConverter
+  )
+  {
+    _settingsStore = settingsStore;
+    _curveConverter = curveConverter;
+  }
+
+  /// <summary>
+  /// Converts a Speckle Region geometry to a Rhino Hatch.
+  /// </summary>
+  /// <param name="target">The Speckle Region geometry to convert.</param>
+  /// <returns>The converted Rhino Hatch.</returns>
+  /// <remarks>⚠️ This conversion does NOT perform scaling.</remarks>
+  public RG.Hatch Convert(SOG.Region target)
+  {
+    List<RG.Curve> rhinoCurves = new() { _curveConverter.Convert(target.boundary) };
+    foreach (var loop in target.innerLoops)
+    {
+      rhinoCurves.Add(_curveConverter.Convert(loop));
+    }
+    var tolerance = _settingsStore.Current.Document.ModelAbsoluteTolerance;
+    // .Create method returns array, but in case of Speckle Region we always expect only 1 converted Hatch
+    RG.Hatch[] result = RG.Hatch.Create(rhinoCurves, 0, 0, 1, tolerance);
+    if (result.Length != 1)
+    {
+      throw new ConversionException(
+        $"Hatch conversion failed for {target}: unexpected number of shapes generated ({result.Length}). Make sure that input loops are planar, closed, non self-intersecting curves."
+      );
+    }
+    return result[0];
+  }
+}
diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/TopLevel/DataObjectConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/TopLevel/DataObjectConverter.cs
index ab5c5be52..ff2420ea9 100644
--- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/TopLevel/DataObjectConverter.cs
+++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/TopLevel/DataObjectConverter.cs
@@ -22,6 +22,7 @@ public class DataObjectConverter
   private readonly ITypedConverter<SOG.Point, RG.Point> _pointConverter;
   private readonly ITypedConverter<SOG.Polycurve, RG.PolyCurve> _polycurveConverter;
   private readonly ITypedConverter<SOG.Polyline, RG.PolylineCurve> _polylineConverter;
+  private readonly ITypedConverter<SOG.Region, RG.Hatch> _regionConverter;
   private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
 
   public DataObjectConverter(
@@ -35,6 +36,7 @@ public DataObjectConverter(
     ITypedConverter<SOG.Point, RG.Point> pointConverter,
     ITypedConverter<SOG.Polyline, RG.PolylineCurve> polylineConverter,
     ITypedConverter<SOG.Polycurve, RG.PolyCurve> polycurveConverter,
+    ITypedConverter<SOG.Region, RG.Hatch> regionConverter,
     IConverterSettingsStore<RhinoConversionSettings> settingsStore
   )
   {
@@ -48,6 +50,7 @@ IConverterSettingsStore<RhinoConversionSettings> settingsStore
     _pointConverter = pointConverter;
     _polycurveConverter = polycurveConverter;
     _polylineConverter = polylineConverter;
+    _regionConverter = regionConverter;
     _settingsStore = settingsStore;
   }
 
@@ -70,6 +73,7 @@ IConverterSettingsStore<RhinoConversionSettings> settingsStore
         SOG.Point point => _pointConverter.Convert(point),
         SOG.Polycurve polycurve => _polycurveConverter.Convert(polycurve),
         SOG.Polyline polyline => _polylineConverter.Convert(polyline),
+        SOG.Region region => _regionConverter.Convert(region),
         _ => throw new ConversionException($"Found unsupported fallback geometry: {item.GetType()}")
       };
       x.Transform(GetUnitsTransform(item));
diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/TopLevel/RegionToHostTopLevelConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/TopLevel/RegionToHostTopLevelConverter.cs
new file mode 100644
index 000000000..dfdfdcb00
--- /dev/null
+++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/TopLevel/RegionToHostTopLevelConverter.cs
@@ -0,0 +1,14 @@
+﻿using Speckle.Converters.Common;
+using Speckle.Converters.Common.Objects;
+
+namespace Speckle.Converters.Rhino.ToHost.TopLevel;
+
+[NameAndRankValue(typeof(SOG.Region), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)]
+public class RegionToHostTopLevelConverter : SpeckleToHostGeometryBaseTopLevelConverter<SOG.Region, RG.Hatch>
+{
+  public RegionToHostTopLevelConverter(
+    IConverterSettingsStore<RhinoConversionSettings> settingsStore,
+    ITypedConverter<SOG.Region, RG.Hatch> geometryBaseConverter
+  )
+    : base(settingsStore, geometryBaseConverter) { }
+}
diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/HatchToSpeckleConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/HatchToSpeckleConverter.cs
new file mode 100644
index 000000000..a4a292057
--- /dev/null
+++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/HatchToSpeckleConverter.cs
@@ -0,0 +1,43 @@
+﻿using Speckle.Converters.Common;
+using Speckle.Converters.Common.Objects;
+using Speckle.Objects;
+
+namespace Speckle.Converters.Rhino.ToSpeckle.Raw;
+
+public class HatchToSpeckleConverter : ITypedConverter<RG.Hatch, SOG.Region>
+{
+  private readonly ITypedConverter<RG.Curve, ICurve> _curveConverter;
+  private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
+
+  public HatchToSpeckleConverter(
+    ITypedConverter<RG.Curve, ICurve> curveConverter,
+    IConverterSettingsStore<RhinoConversionSettings> settingsStore
+  )
+  {
+    _curveConverter = curveConverter;
+    _settingsStore = settingsStore;
+  }
+
+  /// <summary>
+  /// Converts a Hatch geometry to a Speckle Region object.
+  /// </summary>
+  /// <param name="target">The Hatch to convert.</param>
+  /// <returns>The converted Speckle Region object.</returns>
+  public SOG.Region Convert(RG.Hatch target)
+  {
+    // get boundary and inner curves
+    RG.Curve rhinoBoundary = target.Get3dCurves(true)[0];
+    RG.Curve[] rhinoLoops = target.Get3dCurves(false);
+
+    ICurve boundary = _curveConverter.Convert(rhinoBoundary);
+    List<ICurve> innerLoops = rhinoLoops.Select(x => _curveConverter.Convert(x)).ToList();
+
+    return new SOG.Region
+    {
+      boundary = boundary,
+      innerLoops = innerLoops,
+      hasHatchPattern = true,
+      units = _settingsStore.Current.SpeckleUnits,
+    };
+  }
+}
diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/TopLevel/HatchObjectToSpeckleTopLevelConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/TopLevel/HatchObjectToSpeckleTopLevelConverter.cs
new file mode 100644
index 000000000..7fd221a3e
--- /dev/null
+++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/TopLevel/HatchObjectToSpeckleTopLevelConverter.cs
@@ -0,0 +1,15 @@
+﻿using Rhino.DocObjects;
+using Speckle.Converters.Common;
+using Speckle.Converters.Common.Objects;
+
+namespace Speckle.Converters.Rhino.ToSpeckle.TopLevel;
+
+[NameAndRankValue(typeof(HatchObject), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)]
+public class HatchObjectToSpeckleTopLevelConverter
+  : RhinoObjectToSpeckleTopLevelConverter<HatchObject, RG.Hatch, SOG.Region>
+{
+  public HatchObjectToSpeckleTopLevelConverter(ITypedConverter<RG.Hatch, SOG.Region> conversion)
+    : base(conversion) { }
+
+  protected override RG.Hatch GetTypedGeometry(HatchObject input) => input.HatchGeometry;
+}


Reviews:
user: login: clairekuang
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T18:27:04Z
user: login: clairekuang
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T19:04:01Z
user: login: KatKatKateryna
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T22:51:25Z
user: login: KatKatKateryna
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T22:53:06Z
user: login: clairekuang
body: Don't forget to update sdk nuget to include the region class, so build will pass.
Also, add some screenshots for the received hatches! Probably would be good to update the objects file in rhino with different types of hatches for testing
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T12:10:43Z
user: login: clairekuang
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T17:49:45Z
user: login: KatKatKateryna
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-16T19:55:43Z
user: login: clairekuang
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T17:16:01Z

Review comments:
diff_hunk: @@ -0,0 +1,81 @@
+﻿using Rhino.DocObjects;
+using Speckle.Converters.Common;
+using Speckle.Converters.Common.Objects;
+using Speckle.Objects;
+using Speckle.Sdk.Models;
+
+namespace Speckle.Converters.Rhino.ToSpeckle.TopLevel;
+
+[NameAndRankValue(typeof(HatchObject), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)]
+public class HatchObjectToSpeckleTopLevelConverter : IToSpeckleTopLevelConverter
+{
+  private readonly ITypedConverter<RG.Curve, ICurve> _curveConverter;
+  private readonly ITypedConverter<RG.Point3d, SOG.Point> _pointConverter;
+  private readonly ITypedConverter<RG.Vector3d, SOG.Vector> _vectorConverter;
+  private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
+
+  public HatchObjectToSpeckleTopLevelConverter(
+    IConverterSettingsStore<RhinoConversionSettings> settingsStore,
+    ITypedConverter<RG.Point3d, SOG.Point> pointConverter,
+    ITypedConverter<RG.Vector3d, SOG.Vector> vectorConverter,
+    ITypedConverter<RG.Curve, ICurve> curveConverter
+  )
+  {
+    _settingsStore = settingsStore;
+    _pointConverter = pointConverter;
+    _vectorConverter = vectorConverter;
+    _curveConverter = curveConverter;
+  }
+
+  public Base Convert(object target)
+  {
+    var hatchObject = (HatchObject)target;
+    var hatch = (RG.Hatch)hatchObject.Geometry;
+
+    // get boundary and inner curves
+    RG.Curve rhinoBoundary = (hatch).Get3dCurves(true)[0];
+    RG.Curve[] rhinoLoops = (hatch).Get3dCurves(false);
+
+    ICurve boundary = _curveConverter.Convert(rhinoBoundary);
+    List<ICurve> innerLoops = rhinoLoops.Select(x => _curveConverter.Convert(x)).ToList();
+
+    List<ICurve> allCurves = new List<ICurve> { boundary }
+      .Concat(innerLoops)
+      .ToList();
+
+    SOG.Box bbox = GetBboxFromHatch(hatch);
+
+    return new SOG.Region
+    {
+      boundary = boundary,
+      innerLoops = innerLoops,
+      hasHatchPattern = true,
+      bbox = bbox,
+      displayValue = allCurves.Cast<Base>().ToList(),
path: Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/TopLevel/HatchObjectToSpeckleTopLevelConverter.cs
user: login: clairekuang
body: should set this displayvalue to null instead, no need to duplicate curves
created_at: 2025-03-11T18:26:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+using Microsoft.Extensions.DependencyInjection;
+using Speckle.Converters.Common;
+using Speckle.Converters.Common.Objects;
+using Speckle.Objects;
+
+namespace Speckle.Converters.Rhino.ToHost.Raw;
+
+public class RegionToHostConverter : ITypedConverter<SOG.Region, RG.Hatch>
+{
+  private readonly IServiceProvider _serviceProvider;
path: Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/Raw/RegionToHostConverter.cs
user: login: clairekuang
body: This should be an injected CurveToHostConverter
created_at: 2025-03-11T18:54:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+using Microsoft.Extensions.DependencyInjection;
+using Speckle.Converters.Common;
+using Speckle.Converters.Common.Objects;
+using Speckle.Objects;
+
+namespace Speckle.Converters.Rhino.ToHost.Raw;
+
+public class RegionToHostConverter : ITypedConverter<SOG.Region, RG.Hatch>
+{
+  private readonly IServiceProvider _serviceProvider;
+  private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
+
+  public RegionToHostConverter(
+    IConverterSettingsStore<RhinoConversionSettings> settingsStore,
+    IServiceProvider serviceProvider
+  )
+  {
+    _settingsStore = settingsStore;
+    _serviceProvider = serviceProvider;
+  }
+
+  /// <summary>
+  /// Converts a Speckle Region geometry to a Rhino Hatch.
+  /// </summary>
+  /// <param name="target">The Speckle Region geometry to convert.</param>
+  /// <returns>The converted Rhino Hatch.</returns>
+  /// <remarks>⚠️ This conversion does NOT perform scaling.</remarks>
+  public RG.Hatch Convert(SOG.Region target)
+  {
+    List<RG.Curve> rhinoCurves = new() { ConvertAndValidateICurve(target.boundary) };
+    foreach (var loop in target.innerLoops)
+    {
+      rhinoCurves.Add(ConvertAndValidateICurve(loop));
+    }
+    var tolerance = _settingsStore.Current.Document.ModelAbsoluteTolerance;
+    // .Create method returns array, but in case of Speckle Region we always expect only 1 converted Hatch
+    RG.Hatch result = RG.Hatch.Create(rhinoCurves, 0, 0, 1, tolerance)[0];
+    return result;
+  }
+
+  private RG.Curve ConvertAndValidateICurve(ICurve curve)
path: Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/Raw/RegionToHostConverter.cs
user: login: clairekuang
body: no need for the method atm, just use the CurveToHostConverter in the foreach loop (line 33)
created_at: 2025-03-11T18:56:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+﻿using Rhino.DocObjects;
+using Speckle.Converters.Common;
+using Speckle.Converters.Common.Objects;
+using Speckle.Objects;
+using Speckle.Sdk.Models;
+
+namespace Speckle.Converters.Rhino.ToSpeckle.TopLevel;
+
+[NameAndRankValue(typeof(HatchObject), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)]
+public class HatchObjectToSpeckleTopLevelConverter : IToSpeckleTopLevelConverter
path: Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/TopLevel/HatchObjectToSpeckleTopLevelConverter.cs
user: login: clairekuang
body: use the RhinoObjectToSpeckleTopLevelConverter pattern here, since `Hatch` is a `HatchObject` and the RG.Hatch geometry can be accessed via `hatchobject.Geometry`.

This means this conversion logic should be in the raw converter. this will be important when we need to inject the raw converters into eg grasshopper.
created_at: 2025-03-11T19:00:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+﻿using Rhino.DocObjects;
+using Speckle.Converters.Common;
+using Speckle.Converters.Common.Objects;
+using Speckle.Objects;
+using Speckle.Sdk.Models;
+
+namespace Speckle.Converters.Rhino.ToSpeckle.TopLevel;
+
+[NameAndRankValue(typeof(HatchObject), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)]
+public class HatchObjectToSpeckleTopLevelConverter : IToSpeckleTopLevelConverter
+{
+  private readonly ITypedConverter<RG.Curve, ICurve> _curveConverter;
+  private readonly ITypedConverter<RG.Point3d, SOG.Point> _pointConverter;
+  private readonly ITypedConverter<RG.Vector3d, SOG.Vector> _vectorConverter;
+  private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
+
+  public HatchObjectToSpeckleTopLevelConverter(
+    IConverterSettingsStore<RhinoConversionSettings> settingsStore,
+    ITypedConverter<RG.Point3d, SOG.Point> pointConverter,
+    ITypedConverter<RG.Vector3d, SOG.Vector> vectorConverter,
+    ITypedConverter<RG.Curve, ICurve> curveConverter
+  )
+  {
+    _settingsStore = settingsStore;
+    _pointConverter = pointConverter;
+    _vectorConverter = vectorConverter;
+    _curveConverter = curveConverter;
+  }
+
+  public Base Convert(object target)
+  {
+    var hatchObject = (HatchObject)target;
+    var hatch = (RG.Hatch)hatchObject.Geometry;
+
+    // get boundary and inner curves
+    RG.Curve rhinoBoundary = (hatch).Get3dCurves(true)[0];
path: Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/TopLevel/HatchObjectToSpeckleTopLevelConverter.cs
user: login: clairekuang
body: the return type and method documentation implies there may be multiple boundary curves. could you validate if this is true or not?
created_at: 2025-03-11T19:02:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+﻿using Rhino.DocObjects;
+using Speckle.Converters.Common;
+using Speckle.Converters.Common.Objects;
+using Speckle.Objects;
+using Speckle.Sdk.Models;
+
+namespace Speckle.Converters.Rhino.ToSpeckle.TopLevel;
+
+[NameAndRankValue(typeof(HatchObject), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)]
+public class HatchObjectToSpeckleTopLevelConverter : IToSpeckleTopLevelConverter
+{
+  private readonly ITypedConverter<RG.Curve, ICurve> _curveConverter;
+  private readonly ITypedConverter<RG.Point3d, SOG.Point> _pointConverter;
+  private readonly ITypedConverter<RG.Vector3d, SOG.Vector> _vectorConverter;
+  private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
+
+  public HatchObjectToSpeckleTopLevelConverter(
+    IConverterSettingsStore<RhinoConversionSettings> settingsStore,
+    ITypedConverter<RG.Point3d, SOG.Point> pointConverter,
+    ITypedConverter<RG.Vector3d, SOG.Vector> vectorConverter,
+    ITypedConverter<RG.Curve, ICurve> curveConverter
+  )
+  {
+    _settingsStore = settingsStore;
+    _pointConverter = pointConverter;
+    _vectorConverter = vectorConverter;
+    _curveConverter = curveConverter;
+  }
+
+  public Base Convert(object target)
+  {
+    var hatchObject = (HatchObject)target;
+    var hatch = (RG.Hatch)hatchObject.Geometry;
+
+    // get boundary and inner curves
+    RG.Curve rhinoBoundary = (hatch).Get3dCurves(true)[0];
+    RG.Curve[] rhinoLoops = (hatch).Get3dCurves(false);
+
+    ICurve boundary = _curveConverter.Convert(rhinoBoundary);
+    List<ICurve> innerLoops = rhinoLoops.Select(x => _curveConverter.Convert(x)).ToList();
+
+    List<ICurve> allCurves = new List<ICurve> { boundary }
+      .Concat(innerLoops)
+      .ToList();
+
+    SOG.Box bbox = GetBboxFromHatch(hatch);
+
+    return new SOG.Region
+    {
+      boundary = boundary,
+      innerLoops = innerLoops,
+      hasHatchPattern = true,
+      bbox = bbox,
+      displayValue = allCurves.Cast<Base>().ToList(),
+      units = _settingsStore.Current.SpeckleUnits,
+    };
+  }
+
+  private SOG.Box GetBboxFromHatch(RG.Hatch hatch)
path: Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/TopLevel/HatchObjectToSpeckleTopLevelConverter.cs
user: login: clairekuang
body: tbh I'd delete this- we aren't using bbox for anything, this is wasted conversion so you can just pass a null
created_at: 2025-03-11T19:03:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+﻿using Rhino.DocObjects;
+using Speckle.Converters.Common;
+using Speckle.Converters.Common.Objects;
+using Speckle.Objects;
+using Speckle.Sdk.Models;
+
+namespace Speckle.Converters.Rhino.ToSpeckle.TopLevel;
+
+[NameAndRankValue(typeof(HatchObject), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)]
+public class HatchObjectToSpeckleTopLevelConverter : IToSpeckleTopLevelConverter
path: Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/TopLevel/HatchObjectToSpeckleTopLevelConverter.cs
user: login: KatKatKateryna
body: I see, I was looking at the Brep TopLevel conversion which was fully done there. But now I can see other geometries are following the pattern 👍
created_at: 2025-03-11T22:51:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+﻿using Rhino.DocObjects;
+using Speckle.Converters.Common;
+using Speckle.Converters.Common.Objects;
+using Speckle.Objects;
+using Speckle.Sdk.Models;
+
+namespace Speckle.Converters.Rhino.ToSpeckle.TopLevel;
+
+[NameAndRankValue(typeof(HatchObject), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)]
+public class HatchObjectToSpeckleTopLevelConverter : IToSpeckleTopLevelConverter
+{
+  private readonly ITypedConverter<RG.Curve, ICurve> _curveConverter;
+  private readonly ITypedConverter<RG.Point3d, SOG.Point> _pointConverter;
+  private readonly ITypedConverter<RG.Vector3d, SOG.Vector> _vectorConverter;
+  private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
+
+  public HatchObjectToSpeckleTopLevelConverter(
+    IConverterSettingsStore<RhinoConversionSettings> settingsStore,
+    ITypedConverter<RG.Point3d, SOG.Point> pointConverter,
+    ITypedConverter<RG.Vector3d, SOG.Vector> vectorConverter,
+    ITypedConverter<RG.Curve, ICurve> curveConverter
+  )
+  {
+    _settingsStore = settingsStore;
+    _pointConverter = pointConverter;
+    _vectorConverter = vectorConverter;
+    _curveConverter = curveConverter;
+  }
+
+  public Base Convert(object target)
+  {
+    var hatchObject = (HatchObject)target;
+    var hatch = (RG.Hatch)hatchObject.Geometry;
+
+    // get boundary and inner curves
+    RG.Curve rhinoBoundary = (hatch).Get3dCurves(true)[0];
path: Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/TopLevel/HatchObjectToSpeckleTopLevelConverter.cs
user: login: KatKatKateryna
body: It uses the same .Get3dCurves method for outer and inner curves (you specify the difference with bool input variable), so the return type is enumerable. But outer boundary can only be 1
created_at: 2025-03-11T22:53:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,40 @@
+using Speckle.Converters.Common;
+using Speckle.Converters.Common.Objects;
+using Speckle.Objects;
+
+namespace Speckle.Converters.Rhino.ToHost.Raw;
+
+public class RegionToHostConverter : ITypedConverter<SOG.Region, RG.Hatch>
+{
+  private readonly ITypedConverter<ICurve, RG.Curve> _curveConverter;
+  private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
+
+  public RegionToHostConverter(
+    IConverterSettingsStore<RhinoConversionSettings> settingsStore,
+    ITypedConverter<ICurve, RG.Curve> curveConverter
+  )
+  {
+    _settingsStore = settingsStore;
+    _curveConverter = curveConverter;
+  }
+
+  /// <summary>
+  /// Converts a Speckle Region geometry to a Rhino Hatch.
+  /// </summary>
+  /// <param name="target">The Speckle Region geometry to convert.</param>
+  /// <returns>The converted Rhino Hatch.</returns>
+  /// <remarks>⚠️ This conversion does NOT perform scaling.</remarks>
+  public RG.Hatch Convert(SOG.Region target)
+  {
+    List<RG.Curve> rhinoCurves = new() { _curveConverter.Convert(target.boundary) };
+    foreach (var loop in target.innerLoops)
+    {
+      // TODO: add extra checks depending on edge case Regions sent from other apps (e.g. not closed)
+      rhinoCurves.Add(_curveConverter.Convert(loop));
+    }
+    var tolerance = _settingsStore.Current.Document.ModelAbsoluteTolerance;
+    // .Create method returns array, but in case of Speckle Region we always expect only 1 converted Hatch
+    RG.Hatch result = RG.Hatch.Create(rhinoCurves, 0, 0, 1, tolerance)[0];
path: Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/Raw/RegionToHostConverter.cs
user: login: clairekuang
body: This doesn't look quite right. Use the method I linked instead, so you can set the outer and inner loops directly. The method you use can return an array of hatches, especially if the curve is self-intersecting. Since we do not prevent people from sending "bad" hatches, you shouldn't assume there is only one returned https://developer.rhino3d.com/api/rhinocommon/rhino.geometry.hatch/create#(plane,curve,ienumerable%3Ccurve%3E,int,double,double)
created_at: 2025-03-15T17:43:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,40 @@
+using Speckle.Converters.Common;
+using Speckle.Converters.Common.Objects;
+using Speckle.Objects;
+
+namespace Speckle.Converters.Rhino.ToHost.Raw;
+
+public class RegionToHostConverter : ITypedConverter<SOG.Region, RG.Hatch>
+{
+  private readonly ITypedConverter<ICurve, RG.Curve> _curveConverter;
+  private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
+
+  public RegionToHostConverter(
+    IConverterSettingsStore<RhinoConversionSettings> settingsStore,
+    ITypedConverter<ICurve, RG.Curve> curveConverter
+  )
+  {
+    _settingsStore = settingsStore;
+    _curveConverter = curveConverter;
+  }
+
+  /// <summary>
+  /// Converts a Speckle Region geometry to a Rhino Hatch.
+  /// </summary>
+  /// <param name="target">The Speckle Region geometry to convert.</param>
+  /// <returns>The converted Rhino Hatch.</returns>
+  /// <remarks>⚠️ This conversion does NOT perform scaling.</remarks>
+  public RG.Hatch Convert(SOG.Region target)
+  {
+    List<RG.Curve> rhinoCurves = new() { _curveConverter.Convert(target.boundary) };
+    foreach (var loop in target.innerLoops)
+    {
+      // TODO: add extra checks depending on edge case Regions sent from other apps (e.g. not closed)
path: Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/Raw/RegionToHostConverter.cs
user: login: clairekuang
body: remove this todo if this isn't done in the pr: I wouldn't worry about object validation atm, if there's a bad hatch then let it throw on create below
created_at: 2025-03-15T17:44:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+﻿using Speckle.Converters.Common;
+using Speckle.Converters.Common.Objects;
+using Speckle.Objects;
+
+namespace Speckle.Converters.Rhino.ToSpeckle.Raw;
+
+public class HatchToSpeckleConverter : ITypedConverter<RG.Hatch, SOG.Region>
+{
+  private readonly ITypedConverter<RG.Curve, ICurve> _curveConverter;
+  private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
+
+  public HatchToSpeckleConverter(
+    ITypedConverter<RG.Curve, ICurve> curveConverter,
+    IConverterSettingsStore<RhinoConversionSettings> settingsStore
+  )
+  {
+    _curveConverter = curveConverter;
+    _settingsStore = settingsStore;
+  }
+
+  /// <summary>
+  /// Converts a Hatch geometry to a Speckle Region object.
+  /// </summary>
+  /// <param name="target">The Hatch to convert.</param>
+  /// <returns>The converted Speckle Region object.</returns>
+  public SOG.Region Convert(RG.Hatch target)
+  {
+    // get boundary and inner curves
+    RG.Curve rhinoBoundary = target.Get3dCurves(true)[0];
+    RG.Curve[] rhinoLoops = target.Get3dCurves(false);
+
+    ICurve boundary = _curveConverter.Convert(rhinoBoundary);
+    List<ICurve> innerLoops = rhinoLoops.Select(x => _curveConverter.Convert(x)).ToList();
+
+    List<ICurve> allCurves = new List<ICurve> { boundary }
path: Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/HatchToSpeckleConverter.cs
user: login: clairekuang
body: this is a bit inefficient, since you are creating a new list with `concat` and then calling `tolist()` again. You can just use an `addrange` to modify your allcurves list.

Actually, you aren't using the allcurves list at all. Just delete lines 35-37
created_at: 2025-03-15T17:48:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,40 @@
+using Speckle.Converters.Common;
+using Speckle.Converters.Common.Objects;
+using Speckle.Objects;
+
+namespace Speckle.Converters.Rhino.ToHost.Raw;
+
+public class RegionToHostConverter : ITypedConverter<SOG.Region, RG.Hatch>
+{
+  private readonly ITypedConverter<ICurve, RG.Curve> _curveConverter;
+  private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
+
+  public RegionToHostConverter(
+    IConverterSettingsStore<RhinoConversionSettings> settingsStore,
+    ITypedConverter<ICurve, RG.Curve> curveConverter
+  )
+  {
+    _settingsStore = settingsStore;
+    _curveConverter = curveConverter;
+  }
+
+  /// <summary>
+  /// Converts a Speckle Region geometry to a Rhino Hatch.
+  /// </summary>
+  /// <param name="target">The Speckle Region geometry to convert.</param>
+  /// <returns>The converted Rhino Hatch.</returns>
+  /// <remarks>⚠️ This conversion does NOT perform scaling.</remarks>
+  public RG.Hatch Convert(SOG.Region target)
+  {
+    List<RG.Curve> rhinoCurves = new() { _curveConverter.Convert(target.boundary) };
+    foreach (var loop in target.innerLoops)
+    {
+      // TODO: add extra checks depending on edge case Regions sent from other apps (e.g. not closed)
+      rhinoCurves.Add(_curveConverter.Convert(loop));
+    }
+    var tolerance = _settingsStore.Current.Document.ModelAbsoluteTolerance;
+    // .Create method returns array, but in case of Speckle Region we always expect only 1 converted Hatch
+    RG.Hatch result = RG.Hatch.Create(rhinoCurves, 0, 0, 1, tolerance)[0];
path: Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/Raw/RegionToHostConverter.cs
user: login: KatKatKateryna
body: This method will require a Hatch Plane as an input, which we were not storing at any point (plane was stored as a part of Bbox, but was deleted at request in one of the previous commits). If Plane is something necessary, we can revise Region class to include Plane in the constructor, or simply return the Bbox conversion (possibly non-nullable), and we will get the plane as a part of it. We can quickly sync to decide what's best 🙌 
The simplest option if to keep the method which is used now, possibly adding a check that exactly one Hatch was generated, 0 or >1 means an error:
![image](https://github.com/user-attachments/assets/ad7a7cb2-6837-4705-8627-7aaa1b837045)

P.S. AutoCAD is operating under the same assumption: creation method returns an array but expect exactly 1 region per closed loop: https://help.autodesk.com/view/OARX/2025/ENU/?guid=GUID-684E602E-3555-4370-BCDC-1CE594676C43 
created_at: 2025-03-16T19:55:43Z
author_association: CONTRIBUTOR

PR comments:
user: login: KatKatKateryna
created_at: 2025-03-12T16:55:11Z
body: > Don't forget to update sdk nuget to include the region class, so build will pass. Also, add some screenshots for the received hatches! Probably would be good to update the objects file in rhino with different types of hatches for testing

Added a new ticket to update Objects model for all apps sending new Region class:
https://linear.app/speckle/issue/CNX-1405/update-sample-object-models-from-supported-send-connectors-to-include
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-03-13T17:51:52Z
body: ## [Codecov](https://app.codecov.io/gh/specklesystems/speckle-sharp-connectors/pull/643?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=specklesystems) Report
Attention: Patch coverage is `0%` with `41 lines` in your changes missing coverage. Please review.
> Project coverage is 17.46%. Comparing base [(`be63b4a`)](https://app.codecov.io/gh/specklesystems/speckle-sharp-connectors/commit/be63b4ab556487843bd1cc6c6d5811ccc472c92a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=specklesystems) to head [(`31a3afc`)](https://app.codecov.io/gh/specklesystems/speckle-sharp-connectors/commit/31a3afcbea1b2cbbeb317fb0b3630861545e41cc?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=specklesystems).
> Report is 11 commits behind head on dev.

| [Files with missing lines](https://app.codecov.io/gh/specklesystems/speckle-sharp-connectors/pull/643?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=specklesystems) | Patch % | Lines |
|---|---|---|
| [...inoShared/ToSpeckle/Raw/HatchToSpeckleConverter.cs](https://app.codecov.io/gh/specklesystems/speckle-sharp-connectors/pull/643?src=pr&el=tree&filepath=Converters%2FRhino%2FSpeckle.Converters.RhinoShared%2FToSpeckle%2FRaw%2FHatchToSpeckleConverter.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=specklesystems#diff-Q29udmVydGVycy9SaGluby9TcGVja2xlLkNvbnZlcnRlcnMuUmhpbm9TaGFyZWQvVG9TcGVja2xlL1Jhdy9IYXRjaFRvU3BlY2tsZUNvbnZlcnRlci5jcw==) | 0.00% | [18 Missing :warning: ](https://app.codecov.io/gh/specklesystems/speckle-sharp-connectors/pull/643?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=specklesystems) |
| [...rs.RhinoShared/ToHost/Raw/RegionToHostConverter.cs](https://app.codecov.io/gh/specklesystems/speckle-sharp-connectors/pull/643?src=pr&el=tree&filepath=Converters%2FRhino%2FSpeckle.Converters.RhinoShared%2FToHost%2FRaw%2FRegionToHostConverter.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=specklesystems#diff-Q29udmVydGVycy9SaGluby9TcGVja2xlLkNvbnZlcnRlcnMuUmhpbm9TaGFyZWQvVG9Ib3N0L1Jhdy9SZWdpb25Ub0hvc3RDb252ZXJ0ZXIuY3M=) | 0.00% | [17 Missing :warning: ](https://app.codecov.io/gh/specklesystems/speckle-sharp-connectors/pull/643?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=specklesystems) |
| [...RhinoShared/ToHost/TopLevel/DataObjectConverter.cs](https://app.codecov.io/gh/specklesystems/speckle-sharp-connectors/pull/643?src=pr&el=tree&filepath=Converters%2FRhino%2FSpeckle.Converters.RhinoShared%2FToHost%2FTopLevel%2FDataObjectConverter.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=specklesystems#diff-Q29udmVydGVycy9SaGluby9TcGVja2xlLkNvbnZlcnRlcnMuUmhpbm9TaGFyZWQvVG9Ib3N0L1RvcExldmVsL0RhdGFPYmplY3RDb252ZXJ0ZXIuY3M=) | 0.00% | [3 Missing :warning: ](https://app.codecov.io/gh/specklesystems/speckle-sharp-connectors/pull/643?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=specklesystems) |
| [.../TopLevel/HatchObjectToSpeckleTopLevelConverter.cs](https://app.codecov.io/gh/specklesystems/speckle-sharp-connectors/pull/643?src=pr&el=tree&filepath=Converters%2FRhino%2FSpeckle.Converters.RhinoShared%2FToSpeckle%2FTopLevel%2FHatchObjectToSpeckleTopLevelConverter.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=specklesystems#diff-Q29udmVydGVycy9SaGluby9TcGVja2xlLkNvbnZlcnRlcnMuUmhpbm9TaGFyZWQvVG9TcGVja2xlL1RvcExldmVsL0hhdGNoT2JqZWN0VG9TcGVja2xlVG9wTGV2ZWxDb252ZXJ0ZXIuY3M=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/specklesystems/speckle-sharp-connectors/pull/643?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=specklesystems) |
| [...d/ToHost/TopLevel/RegionToHostTopLevelConverter.cs](https://app.codecov.io/gh/specklesystems/speckle-sharp-connectors/pull/643?src=pr&el=tree&filepath=Converters%2FRhino%2FSpeckle.Converters.RhinoShared%2FToHost%2FTopLevel%2FRegionToHostTopLevelConverter.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=specklesystems#diff-Q29udmVydGVycy9SaGluby9TcGVja2xlLkNvbnZlcnRlcnMuUmhpbm9TaGFyZWQvVG9Ib3N0L1RvcExldmVsL1JlZ2lvblRvSG9zdFRvcExldmVsQ29udmVydGVyLmNz) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/specklesystems/speckle-sharp-connectors/pull/643?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=specklesystems) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##              dev     #643      +/-   ##
==========================================
+ Coverage   15.45%   17.46%   +2.00%     
==========================================
  Files         230      240      +10     
  Lines        4606     5055     +449     
  Branches      551      618      +67     
==========================================
+ Hits          712      883     +171     
- Misses       3866     4144     +278     
  Partials       28       28              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/specklesystems/speckle-sharp-connectors/pull/643?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=specklesystems).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=specklesystems).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: KatKatKateryna
created_at: 2025-03-14T02:10:58Z
body: Objects model updated: https://latest.speckle.systems/projects/2295cb26a0/models/bd1fd98086@fda1477843
![image](https://github.com/user-attachments/assets/e16eecd2-4c57-4fe5-a082-290e6cb6698c)

author_association: CONTRIBUTOR