From 2181ad9350e280cd63d3f64214b05795367785f6 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Mon, 11 Aug 2025 11:21:06 +0200 Subject: [PATCH 01/17] feat: add plane property to DFBeam --- src/gh/diffCheck/diffCheck/df_geometries.py | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/gh/diffCheck/diffCheck/df_geometries.py b/src/gh/diffCheck/diffCheck/df_geometries.py index 821a0849..6fba22ad 100644 --- a/src/gh/diffCheck/diffCheck/df_geometries.py +++ b/src/gh/diffCheck/diffCheck/df_geometries.py @@ -101,6 +101,7 @@ def __post_init__(self): self._center: DFVertex = None # the normal of the face self._normal: typing.List[float] = None + self._area: float = None def __getstate__(self): state = self.__dict__.copy() @@ -261,6 +262,12 @@ def normal(self): self._normal = [normal_rg.X, normal_rg.Y, normal_rg.Z] return self._normal + @property + def area(self): + if self._area is None: + self._area = self.to_brep_face().ToBrep().GetArea() + return self._area + @dataclass class DFJoint: """ @@ -506,6 +513,27 @@ def compute_axis(self, is_unitized: bool = True) -> rg.Line: return axis_ln + def compute_plane(self) -> rg.Plane: + """ + This function computes the plane of the beam based on its axis and the first joint's center. + The plane is oriented along the beam's axis. + + :return plane: The plane of the beam + """ + if not self.joints: + raise ValueError("The beam has no joints to compute a plane") + + #main axis as defined above + main_vector = self.compute_axis().Direction + + #secondary axis as normal to the largest face of the beam + largest_face = max(self.faces, key=lambda f: f.area) + secondary_axis = largest_face.normal + secondary_vector = rg.Vector3d(secondary_axis[0], secondary_axis[1], secondary_axis[2]) + origin = self.center + + return rg.Plane(origin, main_vector, secondary_vector) + def compute_joint_distances_to_midpoint(self) -> typing.List[float]: """ This function computes the distances from the center of the beam to each joint. From cedfcc10cb20c85b6c825caf8dcca6b96015c1bf Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Mon, 11 Aug 2025 11:36:30 +0200 Subject: [PATCH 02/17] feat: add max id to CAD segmentation to allow segmentation during fabrication process --- src/gh/components/DF_CAD_segmentator/code.py | 7 +++++-- src/gh/components/DF_CAD_segmentator/metadata.json | 12 ++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index f2ae9f9b..b7cb3926 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -19,7 +19,8 @@ def RunScript(self, i_clouds: System.Collections.Generic.IList[Rhino.Geometry.PointCloud], i_assembly, i_angle_threshold: float = 0.1, - i_association_threshold: float = 0.1) -> Rhino.Geometry.PointCloud: + i_association_threshold: float = 0.1, + i_stop_after_id: int = None) -> Rhino.Geometry.PointCloud: if i_clouds is None or i_assembly is None: self.AddRuntimeMessage(RML.Warning, "Please provide a cloud and an assembly to segment.") @@ -38,7 +39,9 @@ def RunScript(self, df_beams_meshes = [] rh_beams_meshes = [] - for df_b in df_beams: + stop_after_id = i_stop_after_id if i_stop_after_id is not None else len(df_beams) + + for df_b in df_beams[:stop_after_id]: rh_b_mesh_faces = [df_b_f.to_mesh() for df_b_f in df_b.side_faces] df_b_mesh_faces = [df_cvt_bindings.cvt_rhmesh_2_dfmesh(rh_b_mesh_face) for rh_b_mesh_face in rh_b_mesh_faces] df_beams_meshes.append(df_b_mesh_faces) diff --git a/src/gh/components/DF_CAD_segmentator/metadata.json b/src/gh/components/DF_CAD_segmentator/metadata.json index 415dc571..262f810f 100644 --- a/src/gh/components/DF_CAD_segmentator/metadata.json +++ b/src/gh/components/DF_CAD_segmentator/metadata.json @@ -60,6 +60,18 @@ "wireDisplay": "default", "sourceCount": 0, "typeHintID": "float" + }, + { + "name": "i_stop_after_id", + "nickname": "i_stop_after_id", + "description": "The ID of the beam to stop processing after. This is intended for evaluating an assembly before its construction is completed.", + "optional": true, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "int" } ], "outputParameters": [ From 953f5f42130fef0b85c4ed4d6ffff7117eb08398 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Tue, 2 Sep 2025 22:31:37 +0200 Subject: [PATCH 03/17] feat: remove the i_stop_after_id parameter because it is moved to a dedicated component --- src/gh/components/DF_CAD_segmentator/code.py | 4 +--- src/gh/components/DF_CAD_segmentator/metadata.json | 12 ------------ 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index b7cb3926..7ac7fb3d 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -39,9 +39,7 @@ def RunScript(self, df_beams_meshes = [] rh_beams_meshes = [] - stop_after_id = i_stop_after_id if i_stop_after_id is not None else len(df_beams) - - for df_b in df_beams[:stop_after_id]: + for df_b in df_beams: rh_b_mesh_faces = [df_b_f.to_mesh() for df_b_f in df_b.side_faces] df_b_mesh_faces = [df_cvt_bindings.cvt_rhmesh_2_dfmesh(rh_b_mesh_face) for rh_b_mesh_face in rh_b_mesh_faces] df_beams_meshes.append(df_b_mesh_faces) diff --git a/src/gh/components/DF_CAD_segmentator/metadata.json b/src/gh/components/DF_CAD_segmentator/metadata.json index 262f810f..415dc571 100644 --- a/src/gh/components/DF_CAD_segmentator/metadata.json +++ b/src/gh/components/DF_CAD_segmentator/metadata.json @@ -60,18 +60,6 @@ "wireDisplay": "default", "sourceCount": 0, "typeHintID": "float" - }, - { - "name": "i_stop_after_id", - "nickname": "i_stop_after_id", - "description": "The ID of the beam to stop processing after. This is intended for evaluating an assembly before its construction is completed.", - "optional": true, - "allowTreeAccess": true, - "showTypeHints": true, - "scriptParamAccess": "item", - "wireDisplay": "default", - "sourceCount": 0, - "typeHintID": "int" } ], "outputParameters": [ From adf1576109f7b5218455eb9e2583cda4fcecdab4 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Tue, 2 Sep 2025 22:34:01 +0200 Subject: [PATCH 04/17] feat: create new truncate_assembly component --- .../components/DF_truncate_assembly/code.py | 15 +++++ .../components/DF_truncate_assembly/icon.png | Bin 0 -> 7082 bytes .../DF_truncate_assembly/metadata.json | 52 ++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 src/gh/components/DF_truncate_assembly/code.py create mode 100644 src/gh/components/DF_truncate_assembly/icon.png create mode 100644 src/gh/components/DF_truncate_assembly/metadata.json diff --git a/src/gh/components/DF_truncate_assembly/code.py b/src/gh/components/DF_truncate_assembly/code.py new file mode 100644 index 00000000..02d0c7c2 --- /dev/null +++ b/src/gh/components/DF_truncate_assembly/code.py @@ -0,0 +1,15 @@ +from ghpythonlib.componentbase import executingcomponent as component + +import diffCheck +import diffCheck.df_geometries + +class DFTester(component): + def RunScript(self, + i_assembly, + i_truncate_index: int): + beams = i_assembly.beams[:i_truncate_index] + name = i_assembly.name + + o_assembly = diffCheck.df_geometries.DFAssembly(name=name, beams=beams) + ghenv.Component.Message = f"number of beams: {len(o_assembly.beams)}" # noqa: F821 + return o_assembly diff --git a/src/gh/components/DF_truncate_assembly/icon.png b/src/gh/components/DF_truncate_assembly/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d15d814292690cd957adc8937cf11dc61d9c55b1 GIT binary patch literal 7082 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 H^oja6M~oeV literal 0 HcmV?d00001 diff --git a/src/gh/components/DF_truncate_assembly/metadata.json b/src/gh/components/DF_truncate_assembly/metadata.json new file mode 100644 index 00000000..ec63631d --- /dev/null +++ b/src/gh/components/DF_truncate_assembly/metadata.json @@ -0,0 +1,52 @@ +{ + "name": "DFTruncateAssembly", + "nickname": "TruncateAssembly", + "category": "diffCheck", + "subcategory": "Structure", + "description": "This component truncates an assembly.", + "exposure": 4, + "instanceGuid": "cf8af97f-dd84-40b6-af44-bf6aca7b941b", + "ghpython": { + "hideOutput": true, + "hideInput": true, + "isAdvancedMode": true, + "marshalOutGuids": true, + "iconDisplay": 2, + "inputParameters": [ + { + "name": "i_assembly", + "nickname": "i_assembly", + "description": "The assembly to be truncated.", + "optional": false, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "ghdoc" + }, + { + "name": "i_truncate_index", + "nickname": "i_truncate_index", + "description": "The index at which to truncate the assembly.", + "optional": false, + "allowTreeAccess": false, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "int" + } + ], + "outputParameters": [ + { + "name": "o_assembly", + "nickname": "o_assembly", + "description": "The resulting assembly after truncation.", + "optional": false, + "sourceCount": 0, + "graft": false + } + ] + } +} \ No newline at end of file From f7016b45cacb37ca7f642a2fe35e83817f1215d9 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Tue, 2 Sep 2025 23:35:55 +0200 Subject: [PATCH 05/17] fix: main axis of DFBeam computed such that the main axes are also the ones where we will have the most normals in the point cloud --- src/gh/diffCheck/diffCheck/df_geometries.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/gh/diffCheck/diffCheck/df_geometries.py b/src/gh/diffCheck/diffCheck/df_geometries.py index 6fba22ad..541efcd5 100644 --- a/src/gh/diffCheck/diffCheck/df_geometries.py +++ b/src/gh/diffCheck/diffCheck/df_geometries.py @@ -382,6 +382,7 @@ def __post_init__(self): self._center: rg.Point3d = None self._axis: rg.Line = self.compute_axis() + self.plane: rg.Plane = self.compute_plane() self._length: float = self._axis.Length self.__uuid = uuid.uuid4().int @@ -524,15 +525,16 @@ def compute_plane(self) -> rg.Plane: raise ValueError("The beam has no joints to compute a plane") #main axis as defined above - main_vector = self.compute_axis().Direction + main_direction = self.compute_axis().Direction #secondary axis as normal to the largest face of the beam largest_face = max(self.faces, key=lambda f: f.area) secondary_axis = largest_face.normal secondary_vector = rg.Vector3d(secondary_axis[0], secondary_axis[1], secondary_axis[2]) + first_vector = rg.Vector3d.CrossProduct(main_direction, secondary_vector) origin = self.center - return rg.Plane(origin, main_vector, secondary_vector) + return rg.Plane(origin, first_vector, secondary_vector) def compute_joint_distances_to_midpoint(self) -> typing.List[float]: """ From de7b58033ec201f73fc9a521d098a433f157dff2 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Tue, 2 Sep 2025 23:42:06 +0200 Subject: [PATCH 06/17] fix: add ghpythonlib. to the overrides of mypy --- pyproject.toml | 3 ++- src/gh/diffCheck/diffCheck/df_poses.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1b7616fe..032c09f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,8 @@ module = [ "GH_IO.*", "clr.*", "diffcheck_bindings", - "diffCheck.diffcheck_bindings" + "diffCheck.diffcheck_bindings", + "ghpythonlib.*" ] ignore_missing_imports = true diff --git a/src/gh/diffCheck/diffCheck/df_poses.py b/src/gh/diffCheck/diffCheck/df_poses.py index 397a3ffb..7239d169 100644 --- a/src/gh/diffCheck/diffCheck/df_poses.py +++ b/src/gh/diffCheck/diffCheck/df_poses.py @@ -1,4 +1,7 @@ from scriptcontext import sticky as rh_sticky_dict +import ghpythonlib.treehelpers as th +import Rhino + import json from dataclasses import dataclass, field @@ -11,6 +14,15 @@ class DFPose: xDirection: list yDirection: list + def to_rh_plane(self): + """ + Convert the pose to a Rhino Plane object. + """ + origin = Rhino.Geometry.Point3d(self.origin[0], self.origin[1], self.origin[2]) + xDirection = Rhino.Geometry.Vector3d(self.xDirection[0], self.xDirection[1], self.xDirection[2]) + yDirection = Rhino.Geometry.Vector3d(self.yDirection[0], self.yDirection[1], self.yDirection[2]) + return Rhino.Geometry.Plane(origin, xDirection, yDirection) + @dataclass class DFPosesBeam: """ @@ -83,6 +95,18 @@ def save(self, file_path: str): with open(file_path, 'w') as f: json.dump(self.poses_per_element_dictionary, f, default=lambda o: o.__dict__, indent=4) + def to_gh_tree(self): + """ + Convert the assembly poses to a Grasshopper tree structure. + """ + list_of_poses = [] + for element, poses in self.poses_per_element_dictionary.items(): + list_of_pose_of_element = [] + for pose in poses.poses_dictionnary.values(): + list_of_pose_of_element.append(pose.to_rh_plane() if pose is not None else None) + list_of_poses.append(list_of_pose_of_element) + return th.list_to_tree(list_of_poses) + def compute_dot_product(v1, v2): """ From 5745db8d2ecedf26f0172bd667669674254f6a44 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Tue, 2 Sep 2025 23:48:20 +0200 Subject: [PATCH 07/17] feat: update DFMainPCAxes to save the new poses only when user triggers it --- src/gh/components/DF_main_pc_axes/code.py | 23 ++++++------------ .../components/DF_main_pc_axes/metadata.json | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/gh/components/DF_main_pc_axes/code.py b/src/gh/components/DF_main_pc_axes/code.py index ce398c2c..ef889b8e 100644 --- a/src/gh/components/DF_main_pc_axes/code.py +++ b/src/gh/components/DF_main_pc_axes/code.py @@ -1,6 +1,5 @@ #! python3 -from diffCheck import diffcheck_bindings from diffCheck import df_cvt_bindings from diffCheck import df_poses @@ -14,6 +13,8 @@ class DFMainPCAxes(component): def RunScript(self, i_clouds: System.Collections.Generic.List[Rhino.Geometry.PointCloud], + i_assembly, + i_save: bool, i_reset: bool): planes = [] @@ -22,7 +23,6 @@ def RunScript(self, all_poses_in_time.reset() return None, None - previous_poses = all_poses_in_time.get_last_poses() all_poses_this_time = [] for i, cloud in enumerate(i_clouds): df_cloud = df_cvt_bindings.cvt_rhcloud_2_dfcloud(cloud) @@ -34,23 +34,13 @@ def RunScript(self, df_points = df_cloud.get_axis_aligned_bounding_box() df_point = (df_points[0] + df_points[1]) / 2 rh_point = Rhino.Geometry.Point3d(df_point[0], df_point[1], df_point[2]) - vectors = [] - # Get the main axes of the point cloud - previous_pose = previous_poses[i] if previous_poses else None - if previous_pose: - rh_previous_xDirection = Rhino.Geometry.Vector3d(previous_pose.xDirection[0], previous_pose.xDirection[1], previous_pose.xDirection[2]) - rh_previous_yDirection = Rhino.Geometry.Vector3d(previous_pose.yDirection[0], previous_pose.yDirection[1], previous_pose.yDirection[2]) - n_faces = all_poses_in_time.poses_per_element_dictionary[f"element_{i}"].n_faces - else: - rh_previous_xDirection = None - rh_previous_yDirection = None - n_faces = len(diffcheck_bindings.dfb_segmentation.DFSegmentation.segment_by_normal(df_cloud, 12, int(len(df_cloud.points)/20), True, int(len(df_cloud.points)/200), 1)) - axes = df_cloud.get_principal_axes(n_faces) + axes = df_cloud.get_principal_axes(3) + vectors = [] for axe in axes: vectors.append(Rhino.Geometry.Vector3d(axe[0], axe[1], axe[2])) - new_xDirection, new_yDirection = df_poses.select_vectors(vectors, rh_previous_xDirection, rh_previous_yDirection) + new_xDirection, new_yDirection = df_poses.select_vectors(vectors, i_assembly.beams[i].plane.XAxis, i_assembly.beams[i].plane.YAxis) pose = df_poses.DFPose( origin = [rh_point.X, rh_point.Y, rh_point.Z], @@ -60,6 +50,7 @@ def RunScript(self, plane = Rhino.Geometry.Plane(origin = rh_point, xDirection=new_xDirection, yDirection=new_yDirection) planes.append(plane) - all_poses_in_time.add_step(all_poses_this_time) + if i_save: + all_poses_in_time.add_step(all_poses_this_time) return [planes, all_poses_in_time] diff --git a/src/gh/components/DF_main_pc_axes/metadata.json b/src/gh/components/DF_main_pc_axes/metadata.json index 056d13c6..876d1009 100644 --- a/src/gh/components/DF_main_pc_axes/metadata.json +++ b/src/gh/components/DF_main_pc_axes/metadata.json @@ -25,6 +25,18 @@ "sourceCount": 0, "typeHintID": "pointcloud" }, + { + "name": "i_assembly", + "nickname": "i_assembly", + "description": "The DFAssembly object to deconstruct.", + "optional": false, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "ghdoc" + }, { "name": "i_reset", "nickname": "i_reset", @@ -36,6 +48,18 @@ "wireDisplay": "default", "sourceCount": 0, "typeHintID": "bool" + }, + { + "name": "i_save", + "nickname": "i_save", + "description": "save the poses computed at this iteration", + "optional": true, + "allowTreeAccess": false, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "bool" } ], "outputParameters": [ From a4e54d205135ba2fa7df6d868b229267ab5235ed Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:19:20 +0200 Subject: [PATCH 08/17] fix: name of class in truncate_assembly component --- src/gh/components/DF_truncate_assembly/code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gh/components/DF_truncate_assembly/code.py b/src/gh/components/DF_truncate_assembly/code.py index 02d0c7c2..9312b74f 100644 --- a/src/gh/components/DF_truncate_assembly/code.py +++ b/src/gh/components/DF_truncate_assembly/code.py @@ -3,7 +3,7 @@ import diffCheck import diffCheck.df_geometries -class DFTester(component): +class DFTruncateAssembly(component): def RunScript(self, i_assembly, i_truncate_index: int): From a637b5dd31e7af3621c71a89d743b99bacbe4f23 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:21:54 +0200 Subject: [PATCH 09/17] fix: remove unused function parameter in DF_CAD_segmentator component --- src/gh/components/DF_CAD_segmentator/code.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index 7ac7fb3d..f2ae9f9b 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -19,8 +19,7 @@ def RunScript(self, i_clouds: System.Collections.Generic.IList[Rhino.Geometry.PointCloud], i_assembly, i_angle_threshold: float = 0.1, - i_association_threshold: float = 0.1, - i_stop_after_id: int = None) -> Rhino.Geometry.PointCloud: + i_association_threshold: float = 0.1) -> Rhino.Geometry.PointCloud: if i_clouds is None or i_assembly is None: self.AddRuntimeMessage(RML.Warning, "Please provide a cloud and an assembly to segment.") From 2513713cd0c113a8b8aa47905021aaf4aeb7a288 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Thu, 2 Oct 2025 21:25:19 +0200 Subject: [PATCH 10/17] fix: change index in TruncateAssembly component --- src/gh/components/DF_truncate_assembly/code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gh/components/DF_truncate_assembly/code.py b/src/gh/components/DF_truncate_assembly/code.py index 9312b74f..e5aa074b 100644 --- a/src/gh/components/DF_truncate_assembly/code.py +++ b/src/gh/components/DF_truncate_assembly/code.py @@ -7,7 +7,7 @@ class DFTruncateAssembly(component): def RunScript(self, i_assembly, i_truncate_index: int): - beams = i_assembly.beams[:i_truncate_index] + beams = i_assembly.beams[:i_truncate_index - 1] name = i_assembly.name o_assembly = diffCheck.df_geometries.DFAssembly(name=name, beams=beams) From a851476d3156a6b633fd100400081a2d87963061 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Thu, 2 Oct 2025 21:26:46 +0200 Subject: [PATCH 11/17] fix: change sign of index change in TruncateAssembly component --- src/gh/components/DF_truncate_assembly/code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gh/components/DF_truncate_assembly/code.py b/src/gh/components/DF_truncate_assembly/code.py index e5aa074b..4ea4569d 100644 --- a/src/gh/components/DF_truncate_assembly/code.py +++ b/src/gh/components/DF_truncate_assembly/code.py @@ -7,7 +7,7 @@ class DFTruncateAssembly(component): def RunScript(self, i_assembly, i_truncate_index: int): - beams = i_assembly.beams[:i_truncate_index - 1] + beams = i_assembly.beams[:i_truncate_index + 1] name = i_assembly.name o_assembly = diffCheck.df_geometries.DFAssembly(name=name, beams=beams) From b00e145b0dd7a9ef86b3aa8f7e75c04bfa5f17cd Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Wed, 15 Oct 2025 18:15:06 +0200 Subject: [PATCH 12/17] fix: output the history as ghtree --- src/gh/components/DF_main_pc_axes/code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gh/components/DF_main_pc_axes/code.py b/src/gh/components/DF_main_pc_axes/code.py index ef889b8e..7cc67b38 100644 --- a/src/gh/components/DF_main_pc_axes/code.py +++ b/src/gh/components/DF_main_pc_axes/code.py @@ -53,4 +53,4 @@ def RunScript(self, if i_save: all_poses_in_time.add_step(all_poses_this_time) - return [planes, all_poses_in_time] + return [planes, all_poses_in_time.to_gh_tree()] From 6b38623c8c4a6bfe54a5d627b2b0954e7e397999 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Wed, 15 Oct 2025 21:50:38 +0200 Subject: [PATCH 13/17] feat: add fallback to obb when knn on normals gives insufficiently different axes --- src/diffCheck/geometry/DFPointCloud.cc | 27 +++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/diffCheck/geometry/DFPointCloud.cc b/src/diffCheck/geometry/DFPointCloud.cc index 0277b802..eb767dd0 100644 --- a/src/diffCheck/geometry/DFPointCloud.cc +++ b/src/diffCheck/geometry/DFPointCloud.cc @@ -256,7 +256,32 @@ namespace diffCheck::geometry for(size_t i = 0; i < nComponents; ++i) { - principalAxes.push_back(sortedClustersBySize[i].second); + if(principalAxes.size() == 0) + { + principalAxes.push_back(sortedClustersBySize[i].second); + } + else + { + bool isAlreadyPresent = false; + for (const auto& axis : principalAxes) + { + double dotProduct = std::abs(axis.dot(sortedClustersBySize[i].second)); + if (std::abs(dotProduct) > 0.7) // Threshold to consider as similar direction + { + isAlreadyPresent = true; + break; + } + } + if (!isAlreadyPresent) + { + principalAxes.push_back(sortedClustersBySize[i].second); + } + } + } + if (principalAxes.size() < 2) // Fallback to OBB if k-means fails to provide enough distinct axes + { + open3d::geometry::OrientedBoundingBox obb = this->Cvt2O3DPointCloud()->GetOrientedBoundingBox(); + principalAxes = {obb.R_.col(0), obb.R_.col(1), obb.R_.col(2)}; } return principalAxes; } From daff8bb8e15e8877a644a2a93f3f5bb73e9dba38 Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Mon, 20 Oct 2025 10:31:46 +0200 Subject: [PATCH 14/17] ADD check i_assembly has enough beams and if there is an error on a cloud, put None and keep going --- src/gh/components/DF_main_pc_axes/code.py | 60 ++++++++++++++--------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/src/gh/components/DF_main_pc_axes/code.py b/src/gh/components/DF_main_pc_axes/code.py index 7cc67b38..7ab78cc3 100644 --- a/src/gh/components/DF_main_pc_axes/code.py +++ b/src/gh/components/DF_main_pc_axes/code.py @@ -17,6 +17,11 @@ def RunScript(self, i_save: bool, i_reset: bool): + # ensure assembly has enough beams + if len(i_assembly.beams) < len(i_clouds): + ghenv.Component.AddRuntimeMessage(RML.Warning, "Assembly has fewer beams than input clouds") # noqa: F821 + return None, None + planes = [] all_poses_in_time = df_poses.DFPosesAssembly() if i_reset: @@ -25,30 +30,37 @@ def RunScript(self, all_poses_this_time = [] for i, cloud in enumerate(i_clouds): - df_cloud = df_cvt_bindings.cvt_rhcloud_2_dfcloud(cloud) - if df_cloud is None: - return None, None - if not df_cloud.has_normals(): - ghenv.Component.AddRuntimeMessage(RML.Error, f"Point cloud {i} has no normals. Please compute the normals.") # noqa: F821 - - df_points = df_cloud.get_axis_aligned_bounding_box() - df_point = (df_points[0] + df_points[1]) / 2 - rh_point = Rhino.Geometry.Point3d(df_point[0], df_point[1], df_point[2]) - - axes = df_cloud.get_principal_axes(3) - vectors = [] - for axe in axes: - vectors.append(Rhino.Geometry.Vector3d(axe[0], axe[1], axe[2])) - - new_xDirection, new_yDirection = df_poses.select_vectors(vectors, i_assembly.beams[i].plane.XAxis, i_assembly.beams[i].plane.YAxis) - - pose = df_poses.DFPose( - origin = [rh_point.X, rh_point.Y, rh_point.Z], - xDirection = [new_xDirection.X, new_xDirection.Y, new_xDirection.Z], - yDirection = [new_yDirection.X, new_yDirection.Y, new_yDirection.Z]) - all_poses_this_time.append(pose) - plane = Rhino.Geometry.Plane(origin = rh_point, xDirection=new_xDirection, yDirection=new_yDirection) - planes.append(plane) + try: + df_cloud = df_cvt_bindings.cvt_rhcloud_2_dfcloud(cloud) + if df_cloud is None: + return None, None + if not df_cloud.has_normals(): + ghenv.Component.AddRuntimeMessage(RML.Error, f"Point cloud {i} has no normals. Please compute the normals.") # noqa: F821 + + df_points = df_cloud.get_axis_aligned_bounding_box() + df_point = (df_points[0] + df_points[1]) / 2 + rh_point = Rhino.Geometry.Point3d(df_point[0], df_point[1], df_point[2]) + + axes = df_cloud.get_principal_axes(3) + vectors = [] + for axe in axes: + vectors.append(Rhino.Geometry.Vector3d(axe[0], axe[1], axe[2])) + + new_xDirection, new_yDirection = df_poses.select_vectors(vectors, i_assembly.beams[i].plane.XAxis, i_assembly.beams[i].plane.YAxis) + + pose = df_poses.DFPose( + origin = [rh_point.X, rh_point.Y, rh_point.Z], + xDirection = [new_xDirection.X, new_xDirection.Y, new_xDirection.Z], + yDirection = [new_yDirection.X, new_yDirection.Y, new_yDirection.Z]) + all_poses_this_time.append(pose) + plane = Rhino.Geometry.Plane(origin = rh_point, xDirection=new_xDirection, yDirection=new_yDirection) + planes.append(plane) + except Exception as e: + # Any unexpected error on this cloud, skip it and keep going + ghenv.Component.AddRuntimeMessage(RML.Error, f"Cloud {i}: processing failed ({e}); skipping.") # noqa: F821 + planes.append(None) + all_poses_this_time.append(None) + continue if i_save: all_poses_in_time.add_step(all_poses_this_time) From 067903e514660251228fb188f9345383149fc9b9 Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Mon, 20 Oct 2025 10:43:17 +0200 Subject: [PATCH 15/17] FIX rename files assossiated with Pose Estimation component to have the same name, switched back truncate_assembly index logic --- .../{DF_main_pc_axes => DF_pose_estimation}/code.py | 4 ++-- .../icon.png | Bin .../metadata.json | 0 src/gh/components/DF_truncate_assembly/code.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/gh/components/{DF_main_pc_axes => DF_pose_estimation}/code.py (98%) rename src/gh/components/{DF_main_pc_axes => DF_pose_estimation}/icon.png (100%) rename src/gh/components/{DF_main_pc_axes => DF_pose_estimation}/metadata.json (100%) diff --git a/src/gh/components/DF_main_pc_axes/code.py b/src/gh/components/DF_pose_estimation/code.py similarity index 98% rename from src/gh/components/DF_main_pc_axes/code.py rename to src/gh/components/DF_pose_estimation/code.py index 7ab78cc3..4ab24063 100644 --- a/src/gh/components/DF_main_pc_axes/code.py +++ b/src/gh/components/DF_pose_estimation/code.py @@ -7,10 +7,10 @@ from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML from ghpythonlib.componentbase import executingcomponent as component - import System -class DFMainPCAxes(component): + +class DFPoseEstimation(component): def RunScript(self, i_clouds: System.Collections.Generic.List[Rhino.Geometry.PointCloud], i_assembly, diff --git a/src/gh/components/DF_main_pc_axes/icon.png b/src/gh/components/DF_pose_estimation/icon.png similarity index 100% rename from src/gh/components/DF_main_pc_axes/icon.png rename to src/gh/components/DF_pose_estimation/icon.png diff --git a/src/gh/components/DF_main_pc_axes/metadata.json b/src/gh/components/DF_pose_estimation/metadata.json similarity index 100% rename from src/gh/components/DF_main_pc_axes/metadata.json rename to src/gh/components/DF_pose_estimation/metadata.json diff --git a/src/gh/components/DF_truncate_assembly/code.py b/src/gh/components/DF_truncate_assembly/code.py index 4ea4569d..9312b74f 100644 --- a/src/gh/components/DF_truncate_assembly/code.py +++ b/src/gh/components/DF_truncate_assembly/code.py @@ -7,7 +7,7 @@ class DFTruncateAssembly(component): def RunScript(self, i_assembly, i_truncate_index: int): - beams = i_assembly.beams[:i_truncate_index + 1] + beams = i_assembly.beams[:i_truncate_index] name = i_assembly.name o_assembly = diffCheck.df_geometries.DFAssembly(name=name, beams=beams) From 1fad289b2b1b82f4f74a2ced693c155e1249e121 Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Mon, 20 Oct 2025 11:44:11 +0200 Subject: [PATCH 16/17] FIX typo and use not all sticky but a single namespaced key df_poses --- .../DF_pose_estimation/metadata.json | 8 +++---- src/gh/diffCheck/diffCheck/df_poses.py | 24 +++++++++++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/gh/components/DF_pose_estimation/metadata.json b/src/gh/components/DF_pose_estimation/metadata.json index 876d1009..60d1f363 100644 --- a/src/gh/components/DF_pose_estimation/metadata.json +++ b/src/gh/components/DF_pose_estimation/metadata.json @@ -5,7 +5,7 @@ "subcategory": "PointCloud", "description": "This compoment calculates the pose of a list of point clouds.", "exposure": 4, - "instanceGuid": "22b0c6fc-bc16-4ff5-b789-e99776277f65", + "instanceGuid": "a13c4414-f5df-46e6-beae-7054bb9c3e72", "ghpython": { "hideOutput": true, "hideInput": true, @@ -16,7 +16,7 @@ { "name": "i_clouds", "nickname": "i_clouds", - "description": "clouds whose main axes are to be calculated", + "description": "clouds whose pose is to be calculated", "optional": false, "allowTreeAccess": true, "showTypeHints": true, @@ -28,7 +28,7 @@ { "name": "i_assembly", "nickname": "i_assembly", - "description": "The DFAssembly object to deconstruct.", + "description": "The DFAssembly corresponding to the list of clouds.", "optional": false, "allowTreeAccess": true, "showTypeHints": true, @@ -74,7 +74,7 @@ { "name": "o_history", "nickname": "o_history", - "description": "The history of poses of all the elements.", + "description": "The history of poses per elements.", "optional": false, "sourceCount": 0, "graft": false diff --git a/src/gh/diffCheck/diffCheck/df_poses.py b/src/gh/diffCheck/diffCheck/df_poses.py index 7239d169..66341639 100644 --- a/src/gh/diffCheck/diffCheck/df_poses.py +++ b/src/gh/diffCheck/diffCheck/df_poses.py @@ -5,6 +5,13 @@ import json from dataclasses import dataclass, field +# use a key and not all the sticky +_STICKY_KEY = "df_poses" + +def _get_store(): + # returns private sub-dict inside rhino sticky + return rh_sticky_dict.setdefault(_STICKY_KEY, {}) + @dataclass class DFPose: """ @@ -29,14 +36,14 @@ class DFPosesBeam: This class contains the poses of a single beam, at different times in the assembly process. It also contains the number of faces detected for this element, based on which the poses are calculated. """ - poses_dictionnary: dict + poses_dictionary: dict n_faces: int = 3 def add_pose(self, pose: DFPose, step_number: int): """ Add a pose to the dictionary of poses. """ - self.poses_dictionnary[f"pose_{step_number}"] = pose + self.poses_dictionary[f"pose_{step_number}"] = pose def set_n_faces(self, n_faces: int): """ @@ -47,7 +54,7 @@ def set_n_faces(self, n_faces: int): @dataclass class DFPosesAssembly: n_step: int = 0 - poses_per_element_dictionary: dict = field(default_factory=lambda: rh_sticky_dict) + poses_per_element_dictionary: dict = field(default_factory=_get_store) """ This class contains the poses of the different elements of the assembly, at different times in the assembly process. @@ -58,7 +65,7 @@ def __post_init__(self): """ lengths = [] for element in self.poses_per_element_dictionary: - lengths.append(len(self.poses_per_element_dictionary[element].poses_dictionnary)) + lengths.append(len(self.poses_per_element_dictionary[element].poses_dictionary)) self.n_step = max(lengths) if lengths else 0 def add_step(self, new_poses: list[DFPose]): @@ -78,7 +85,7 @@ def get_last_poses(self): return None last_poses = [] for i in range(len(self.poses_per_element_dictionary)): - last_poses.append(self.poses_per_element_dictionary[f"element_{i}"].poses_dictionnary[f"pose_{self.n_step-1}"]) + last_poses.append(self.poses_per_element_dictionary[f"element_{i}"].poses_dictionary[f"pose_{self.n_step-1}"]) return last_poses def reset(self): @@ -86,7 +93,10 @@ def reset(self): Reset the assembly poses to the initial state. """ self.n_step = 0 - rh_sticky_dict.clear() + # clear only namespace + rh_sticky_dict[_STICKY_KEY] = {} + # refresh the local reference to the (now empty) store + self.poses_per_element_dictionary = _get_store() def save(self, file_path: str): """ @@ -102,7 +112,7 @@ def to_gh_tree(self): list_of_poses = [] for element, poses in self.poses_per_element_dictionary.items(): list_of_pose_of_element = [] - for pose in poses.poses_dictionnary.values(): + for pose in poses.poses_dictionary.values(): list_of_pose_of_element.append(pose.to_rh_plane() if pose is not None else None) list_of_poses.append(list_of_pose_of_element) return th.list_to_tree(list_of_poses) From 299504199683f949ecc4c62f9d6b49b5b239fd62 Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:10:57 +0200 Subject: [PATCH 17/17] FIX remove re-assignying new_xDirection and fix projection to use tthe best candidate and not vectors[1]] --- src/gh/diffCheck/diffCheck/df_poses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gh/diffCheck/diffCheck/df_poses.py b/src/gh/diffCheck/diffCheck/df_poses.py index 66341639..adaeee16 100644 --- a/src/gh/diffCheck/diffCheck/df_poses.py +++ b/src/gh/diffCheck/diffCheck/df_poses.py @@ -145,8 +145,8 @@ def select_vectors(vectors, previous_xDirection, previous_yDirection): new_yDirection = sorted_vectors_by_perpendicularity[0] - compute_dot_product(sorted_vectors_by_perpendicularity[0], new_xDirection) * new_xDirection new_yDirection.Unitize() else: - new_xDirection = vectors[0] + sorted_vectors = sorted(vectors[1:], key=lambda v: compute_dot_product(v, new_xDirection)**2) - new_yDirection = sorted_vectors[0] - compute_dot_product(vectors[1], new_xDirection) * new_xDirection + new_yDirection = sorted_vectors[0] - compute_dot_product(sorted_vectors[0], new_xDirection) * new_xDirection new_yDirection.Unitize() return new_xDirection, new_yDirection