From 77127a0b668e0f1a28549f48d2b1ac260f93883b Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Sat, 17 Aug 2024 19:17:29 +0200 Subject: [PATCH 01/40] UPDATE: joint detector adapted for cylinders --- src/gh/diffCheck/diffCheck/df_geometries.py | 10 +-- .../diffCheck/diffCheck/df_joint_detector.py | 66 +++++++++++++++---- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/src/gh/diffCheck/diffCheck/df_geometries.py b/src/gh/diffCheck/diffCheck/df_geometries.py index 3f12bb26..e5f8c1d2 100644 --- a/src/gh/diffCheck/diffCheck/df_geometries.py +++ b/src/gh/diffCheck/diffCheck/df_geometries.py @@ -116,10 +116,12 @@ def from_brep_face(cls, brep_face: rg.BrepFace, joint_id: int = None): """ all_loops = [] - if brep_face.IsCylinder(): - cls.is_cylinder = True - df_face._rh_brepface = brep_face - return df_face + # if brep_face.IsCylinder(): + + # df_face = cls([[[], [], []]], joint_id) + # cls.is_cylinder = True + # df_face._rh_brepface = brep_face + # return df_face for idx, loop in enumerate(brep_face.Loops): loop_trims = loop.Trims diff --git a/src/gh/diffCheck/diffCheck/df_joint_detector.py b/src/gh/diffCheck/diffCheck/df_joint_detector.py index 9601eca7..71ba8fb1 100644 --- a/src/gh/diffCheck/diffCheck/df_joint_detector.py +++ b/src/gh/diffCheck/diffCheck/df_joint_detector.py @@ -48,6 +48,39 @@ def _assign_ids(self, joint_face_ids): return extended_ids + def is_cylinder_beam(self): + """ + Detects if the brep is a cylinder beam. + This is done by finding the largest cylinder in the brep, + and looking if all brep vertices are inside the cylinder. + + :return: True if the brep is detected as a cylinder beam, False otherwise + :return: the largest cylinder if beam detected as a cylinder, None otherwise + """ + + # extract all cylinders from the brep + open_cylinders = [] + for face in self.brep.Faces: + if face.IsCylinder(): + open_cylinder = face.ToBrep() + open_cylinders.append(open_cylinder) + + # find largest cylinder + largest_cylinder = None + largest_srf = 0 + for cylinder in open_cylinders: + if cylinder.GetArea() > largest_srf: + largest_srf = cylinder.GetArea() + print(largest_srf) + largest_cylinder = cylinder.CapPlanarHoles(sc.doc.ModelAbsoluteTolerance) + + # check if all vertices are inside the cylinder + for vertex in self.brep.Vertices: + if not largest_cylinder.IsPointInside(vertex.Location, sc.doc.ModelAbsoluteTolerance, False): + return False, None + return True, largest_cylinder + + def run(self): """ Run the joint detector. We use a dictionary to store the faces of the cuts based wethear they are cuts or holes. @@ -56,32 +89,43 @@ def run(self): :return: a list of faces from joins and faces """ + # check if the brep is a cylinder beam + is_cylinder_beam, cylinder = self.is_cylinder_beam() + # brep vertices to cloud df_cloud = diffCheck.diffcheck_bindings.dfb_geometry.DFPointCloud() df_cloud.points = [np.array([vertex.Location.X, vertex.Location.Y, vertex.Location.Z]).reshape(3, 1) for vertex in self.brep.Vertices] - rh_OBB = diffCheck.df_cvt_bindings.cvt_dfOBB_2_rhbrep(df_cloud.get_tight_bounding_box()) + if is_cylinder_beam: + Bounding_geometry = cylinder + else: + Bounding_geometry = diffCheck.df_cvt_bindings.cvt_dfOBB_2_rhbrep(df_cloud.get_tight_bounding_box()) - # scale the box in the longest edge direction by 1.5 from center on both directions - rh_OBB_center = rh_OBB.GetBoundingBox(True).Center - edges = rh_OBB.Edges + # scale the bounding geometry in the longest edge direction by 1.5 from center on both directions + rh_Bounding_geometry_center = Bounding_geometry.GetBoundingBox(True).Center + edges = Bounding_geometry.Edges edge_lengths = [edge.GetLength() for edge in edges] longest_edge = edges[edge_lengths.index(max(edge_lengths))] - rh_OBB_zaxis = rg.Vector3d(longest_edge.PointAt(1) - longest_edge.PointAt(0)) - rh_OBB_plane = rg.Plane(rh_OBB_center, rh_OBB_zaxis) - scale_factor = 0.09 + rh_Bounding_geometry_zaxis = rg.Vector3d(longest_edge.PointAt(1) - longest_edge.PointAt(0)) + rh_Bounding_geometry_plane = rg.Plane(rh_Bounding_geometry_center, rh_Bounding_geometry_zaxis) + scale_factor = 0.001 xform = rg.Transform.Scale( - rh_OBB_plane, + rh_Bounding_geometry_plane, 1 - scale_factor, 1 - scale_factor, 1 + scale_factor ) - rh_OBB.Transform(xform) + Bounding_geometry.Transform(xform) # check if face's centers are inside the OBB - faces = {idx: (face, rh_OBB.IsPointInside(rg.AreaMassProperties.Compute(face).Centroid, sc.doc.ModelAbsoluteTolerance, True)) for idx, face in enumerate(self.brep.Faces)} - + faces = {} + for idx, face in enumerate(self.brep.Faces): + face_centroid = rg.AreaMassProperties.Compute(face).Centroid + coord = face.ClosestPoint(face_centroid) + projected_centroid = face.PointAt(coord[1], coord[2]) + faces[idx] = (face, Bounding_geometry.IsPointInside(projected_centroid, sc.doc.ModelAbsoluteTolerance, True)) + # get the proximity faces of the joint faces joint_face_ids = [[key] + [adj_face for adj_face in value[0].AdjacentFaces() if faces[adj_face][1] and adj_face != key] for key, value in faces.items() if value[1]] From 106e4c4d38285d90d8228257b1ec87d2dbd772f1 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Wed, 21 Aug 2024 22:40:02 +0200 Subject: [PATCH 02/40] FIX-UPDATE: return tuple with right values --- src/gh/diffCheck/diffCheck/df_joint_detector.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/gh/diffCheck/diffCheck/df_joint_detector.py b/src/gh/diffCheck/diffCheck/df_joint_detector.py index 71ba8fb1..356886da 100644 --- a/src/gh/diffCheck/diffCheck/df_joint_detector.py +++ b/src/gh/diffCheck/diffCheck/df_joint_detector.py @@ -74,6 +74,10 @@ def is_cylinder_beam(self): print(largest_srf) largest_cylinder = cylinder.CapPlanarHoles(sc.doc.ModelAbsoluteTolerance) + # Check if the cylinder exists + if largest_cylinder is None: + return False, None + # check if all vertices are inside the cylinder for vertex in self.brep.Vertices: if not largest_cylinder.IsPointInside(vertex.Location, sc.doc.ModelAbsoluteTolerance, False): @@ -109,7 +113,7 @@ def run(self): rh_Bounding_geometry_zaxis = rg.Vector3d(longest_edge.PointAt(1) - longest_edge.PointAt(0)) rh_Bounding_geometry_plane = rg.Plane(rh_Bounding_geometry_center, rh_Bounding_geometry_zaxis) - scale_factor = 0.001 + scale_factor = 0.01 xform = rg.Transform.Scale( rh_Bounding_geometry_plane, 1 - scale_factor, @@ -133,4 +137,4 @@ def run(self): self._faces = [(face, face_ids[idx]) for idx, face in enumerate(self.brep.Faces)] - return self._faces \ No newline at end of file + return self._faces, is_cylinder_beam \ No newline at end of file From b264a3aae90cf6096f25c41e9341a886c4c90576 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Wed, 21 Aug 2024 22:41:03 +0200 Subject: [PATCH 03/40] UPDATE: add flag to DFAssembly and DFBeam for log cases --- src/gh/diffCheck/diffCheck/df_geometries.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/gh/diffCheck/diffCheck/df_geometries.py b/src/gh/diffCheck/diffCheck/df_geometries.py index e5f8c1d2..47bae818 100644 --- a/src/gh/diffCheck/diffCheck/df_geometries.py +++ b/src/gh/diffCheck/diffCheck/df_geometries.py @@ -264,6 +264,7 @@ def __post_init__(self): self._joints = [] self.__id = uuid.uuid4().int + self.is_cylinder = None @classmethod def from_brep_face(cls, brep): @@ -272,7 +273,8 @@ def from_brep_face(cls, brep): It also removes duplicates and creates a list of unique faces. """ faces : typing.List[DFFace] = [] - data_faces = diffCheck.df_joint_detector.JointDetector(brep).run() + data_faces, is_cylinder = diffCheck.df_joint_detector.JointDetector(brep).run() + cls.is_cylinder = is_cylinder for data in data_faces: face = DFFace.from_brep_face(data[0], data[1]) faces.append(face) @@ -353,6 +355,13 @@ def __post_init__(self): self._all_sidefaces: typing.List[DFFace] = [] self._all_joints: typing.List[DFJoint] = [] + + for beam in self.beams: + if beam.is_cylinder: + self.contains_cylinders = True + break + else: + self.contains_cylinders = False def __repr__(self): return f"Assembly: {self.name}, Beams: {len(self.beams)}" From c1b49d443774843598424357e8c8fcef3457ee1b Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Wed, 21 Aug 2024 22:42:34 +0200 Subject: [PATCH 04/40] FIX: comment-out if __name__ part --- src/gh/components/DF_CAD_segmentator/code.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index 9eba4e42..2c551f00 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -73,11 +73,11 @@ def RunScript(self, return o_clusters, rh_beams_meshes -if __name__ == "__main__": - com = DFCADSegmentator() - o_clusters, rh_beams_meshes = com.RunScript( - i_clouds, - i_assembly, - i_angle_threshold, - i_association_threshold - ) \ No newline at end of file +# if __name__ == "__main__": +# com = DFCADSegmentator() +# o_clusters, rh_beams_meshes = com.RunScript( +# i_clouds, +# i_assembly, +# i_angle_threshold, +# i_association_threshold +# ) \ No newline at end of file From 8d4b55977f78f2b58d574e8efc5211ea490bc5ce Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Wed, 21 Aug 2024 22:44:29 +0200 Subject: [PATCH 05/40] WIP: GetCylinderCenterAndAxis method added to DFMesh --- src/diffCheck/geometry/DFMesh.cc | 50 ++++++++++++++++++++++++++++++++ src/diffCheck/geometry/DFMesh.hh | 7 +++++ 2 files changed, 57 insertions(+) diff --git a/src/diffCheck/geometry/DFMesh.cc b/src/diffCheck/geometry/DFMesh.cc index 71b8b16f..182fed2e 100644 --- a/src/diffCheck/geometry/DFMesh.cc +++ b/src/diffCheck/geometry/DFMesh.cc @@ -137,6 +137,56 @@ namespace diffCheck::geometry return false; } + std::tuple DFMesh::GetCylinderCenterAndAxis() + { + Eigen::Vector3d center; + Eigen::Vector3d axis; + + // Check if the mesh contains normals. If not, compute them + if (this->NormalsFace.size() == 0) + { + std::shared_ptr O3DTriangleMesh = this->Cvt2O3DTriangleMesh(); + O3DTriangleMesh->ComputeTriangleNormals(); + this->NormalsFace.resize(O3DTriangleMesh->triangle_normals_.size()); + for (int i = 0; i < O3DTriangleMesh->triangle_normals_.size(); i++) + { + this->NormalsFace[i] = O3DTriangleMesh->triangle_normals_[i]; + } + } + + // retrieve 20 random mesh faces to test the cylinder hypothesis + std::vector randomFaces; + std::vector randomFacesNormals; + std::vector randomFacesIndices; + for (int i = 0; i < 20; i++) + { + int randomIndex = rand() % this->Faces.size(); + randomFaces.push_back(this->Faces[randomIndex]); + randomFacesNormals.push_back(this->NormalsFace[randomIndex].normalized()); + randomFacesIndices.push_back(randomIndex); + } + + // compute normal to the normals of the 20 vertices, and check that they are colinear + std::vector normalsToVertexNormals; + for (int i = 0; i < 20 ; i++) + { + for (int j = i + 1; j < 20; j++) + { + normalsToVertexNormals.push_back(randomFacesNormals[i].cross(randomFacesNormals[j]).normalized()); + } + } + + std::cout << "normalsToVertexNormals size: " << normalsToVertexNormals.size() << std::endl; + Eigen::Vector3d meanNormalToVertexNormals; + for (Eigen::Vector3d normal : normalsToVertexNormals) + { + meanNormalToVertexNormals += normal; + } + meanNormalToVertexNormals.normalize(); + std::cout << "meanNormalToVertexNormals : " << meanNormalToVertexNormals << std::endl; + return std::make_tuple(center, axis); + } + void DFMesh::LoadFromPLY(const std::string &path) { std::shared_ptr tempMesh_ptr = diffCheck::io::ReadPLYMeshFromFile(path); diff --git a/src/diffCheck/geometry/DFMesh.hh b/src/diffCheck/geometry/DFMesh.hh index 2e865f34..13bc4b91 100644 --- a/src/diffCheck/geometry/DFMesh.hh +++ b/src/diffCheck/geometry/DFMesh.hh @@ -86,6 +86,13 @@ namespace diffCheck::geometry */ bool IsPointOnFace(Eigen::Vector3d point, double associationThreshold = 0.1); + /** + * @brief Get the center and axis of the cylinder, assuming the mesh is a cylinder + * + * @return std::tuple the first element is the center of the cylinder, the second element is the axis of the cylinder + */ + std::tuple GetCylinderCenterAndAxis(); + public: ///< I/O loader /** * @brief Read a mesh from a file From 15312113c4f9042979f8868cffd8cf34ac7f58a9 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:53:35 +0200 Subject: [PATCH 06/40] WIP-UPDATE: add cylinder case to associateClusters methods. --- src/diffCheck/geometry/DFMesh.cc | 51 +-- src/diffCheck/geometry/DFMesh.hh | 6 +- src/diffCheck/segmentation/DFSegmentation.cc | 440 ++++++++++++------- src/diffCheck/segmentation/DFSegmentation.hh | 5 +- 4 files changed, 296 insertions(+), 206 deletions(-) diff --git a/src/diffCheck/geometry/DFMesh.cc b/src/diffCheck/geometry/DFMesh.cc index 182fed2e..afdc7f21 100644 --- a/src/diffCheck/geometry/DFMesh.cc +++ b/src/diffCheck/geometry/DFMesh.cc @@ -137,53 +137,28 @@ namespace diffCheck::geometry return false; } - std::tuple DFMesh::GetCylinderCenterAndAxis() + std::tuple DFMesh::GetCenterAndAxis() { - Eigen::Vector3d center; - Eigen::Vector3d axis; + Eigen::Vector3d center = Eigen::Vector3d::Zero(); + Eigen::Vector3d axis = Eigen::Vector3d::Zero(); - // Check if the mesh contains normals. If not, compute them - if (this->NormalsFace.size() == 0) - { - std::shared_ptr O3DTriangleMesh = this->Cvt2O3DTriangleMesh(); - O3DTriangleMesh->ComputeTriangleNormals(); - this->NormalsFace.resize(O3DTriangleMesh->triangle_normals_.size()); - for (int i = 0; i < O3DTriangleMesh->triangle_normals_.size(); i++) - { - this->NormalsFace[i] = O3DTriangleMesh->triangle_normals_[i]; - } - } + std::vector tightBoundingBox = this->GetTightBoundingBox(); - // retrieve 20 random mesh faces to test the cylinder hypothesis - std::vector randomFaces; - std::vector randomFacesNormals; - std::vector randomFacesIndices; - for (int i = 0; i < 20; i++) - { - int randomIndex = rand() % this->Faces.size(); - randomFaces.push_back(this->Faces[randomIndex]); - randomFacesNormals.push_back(this->NormalsFace[randomIndex].normalized()); - randomFacesIndices.push_back(randomIndex); - } + Eigen::Vector3d deltaFirstDir = tightBoundingBox[1] - tightBoundingBox[0]; + Eigen::Vector3d deltaSecondDir = tightBoundingBox[2] - tightBoundingBox[0]; + Eigen::Vector3d deltaThirdDir = tightBoundingBox[3] - tightBoundingBox[0]; - // compute normal to the normals of the 20 vertices, and check that they are colinear - std::vector normalsToVertexNormals; - for (int i = 0; i < 20 ; i++) + for (Eigen::Vector3d direction : {deltaFirstDir, deltaSecondDir, deltaThirdDir}) { - for (int j = i + 1; j < 20; j++) + if (direction.norm() > axis.norm()) { - normalsToVertexNormals.push_back(randomFacesNormals[i].cross(randomFacesNormals[j]).normalized()); + axis = direction; } } + axis.normalize(); - std::cout << "normalsToVertexNormals size: " << normalsToVertexNormals.size() << std::endl; - Eigen::Vector3d meanNormalToVertexNormals; - for (Eigen::Vector3d normal : normalsToVertexNormals) - { - meanNormalToVertexNormals += normal; - } - meanNormalToVertexNormals.normalize(); - std::cout << "meanNormalToVertexNormals : " << meanNormalToVertexNormals << std::endl; + center = tightBoundingBox[0] + deltaFirstDir/2 + deltaSecondDir/2 + deltaThirdDir/2; + return std::make_tuple(center, axis); } diff --git a/src/diffCheck/geometry/DFMesh.hh b/src/diffCheck/geometry/DFMesh.hh index 13bc4b91..d46448eb 100644 --- a/src/diffCheck/geometry/DFMesh.hh +++ b/src/diffCheck/geometry/DFMesh.hh @@ -87,11 +87,11 @@ namespace diffCheck::geometry bool IsPointOnFace(Eigen::Vector3d point, double associationThreshold = 0.1); /** - * @brief Get the center and axis of the cylinder, assuming the mesh is a cylinder + * @brief Get the center and main axis of oriented boundung box of the mesh. It was developped for the cylinder case, but can be used for other shapes. * - * @return std::tuple the first element is the center of the cylinder, the second element is the axis of the cylinder + * @return std::tuple the first element is the center of the obb of the mesh, the second element is the main axis of the obb of the mesh */ - std::tuple GetCylinderCenterAndAxis(); + std::tuple GetCenterAndAxis(); public: ///< I/O loader /** diff --git a/src/diffCheck/segmentation/DFSegmentation.cc b/src/diffCheck/segmentation/DFSegmentation.cc index 52c8400f..ab344f42 100644 --- a/src/diffCheck/segmentation/DFSegmentation.cc +++ b/src/diffCheck/segmentation/DFSegmentation.cc @@ -96,6 +96,7 @@ namespace diffCheck::segmentation } std::shared_ptr DFSegmentation::AssociateClustersToMeshes( + bool isCylinder, std::vector> referenceMesh, std::vector> &clusters, double angleThreshold, @@ -108,93 +109,172 @@ namespace diffCheck::segmentation { DIFFCHECK_WARN("No mesh faces to associate with the clusters. Returning an empty point cloud."); return unifiedPointCloud; - } - for (std::shared_ptr face : referenceMesh) - { - std::shared_ptr correspondingSegment; - // Getting the center of the mesh face - Eigen::Vector3d faceCenter = face->Cvt2O3DTriangleMesh()->GetCenter(); + } - // Getting the normal of the mesh face - Eigen::Vector3d faceNormal = face->GetFirstNormal(); - faceNormal.normalize(); - - double faceDistance = std::numeric_limits::max(); - if (clusters.size() == 0) + //differentiate between cylinder and other shapes + if (isCylinder) + { + for (std::shared_ptr face : referenceMesh) { - DIFFCHECK_WARN("No clusters to associate with the mesh faces. Returning an empty point cloud."); - return unifiedPointCloud; - } - for (auto segment : clusters) - { - Eigen::Vector3d segmentCenter; - Eigen::Vector3d segmentNormal; + std::tuple centerAndAxis = face->GetCenterAndAxis(); + Eigen::Vector3d cylinderCenter = std::get<0>(centerAndAxis); + Eigen::Vector3d cylinderAxis = std::get<1>(centerAndAxis); - for (auto point : segment->Points){segmentCenter += point;} - if (segment->GetNumPoints() > 0) + double faceDistance = std::numeric_limits::max(); + std::shared_ptr correspondingSegment; + + if (clusters.size() == 0) { - segmentCenter /= segment->GetNumPoints(); + DIFFCHECK_WARN("No clusters to associate with the mesh faces. Returning an empty point cloud."); + return unifiedPointCloud; } - else + for (auto segment : clusters) { - DIFFCHECK_WARN("Empty segment. Skipping the segment."); + Eigen::Vector3d segmentCenter; + Eigen::Vector3d segmentNormal; + + for (auto point : segment->Points) + { + segmentCenter += point; + } + if (segment->GetNumPoints() > 0) + { + segmentCenter /= segment->GetNumPoints(); + } + else + { + DIFFCHECK_WARN("Empty segment. Skipping the segment."); + continue; + } + + for (auto normal : segment->Normals) + { + segmentNormal += normal; + } + segmentNormal.normalize(); + + // we consider the distance to the cylinder axis, not the cylinder center + Eigen::Vector3d projectedSegmentCenter = (segmentCenter - cylinderCenter).dot(cylinderAxis) * cylinderAxis + cylinderCenter; + double currentDistance = (cylinderCenter - projectedSegmentCenter).norm(); + if (std::abs(cylinderAxis.dot(segmentNormal)) < angleThreshold && currentDistance < faceDistance) + { + correspondingSegment = segment; + faceDistance = currentDistance; + } + } + if (correspondingSegment == nullptr) + { + DIFFCHECK_WARN("No segment found for the face. Skipping the face."); continue; } + for (Eigen::Vector3d point : correspondingSegment->Points) + { + bool pointInFace = false; + if (face->IsPointOnFace(point, associationThreshold)) + { + unifiedPointCloud->Points.push_back(point); + unifiedPointCloud->Normals.push_back( + correspondingSegment->Normals[std::distance( + correspondingSegment->Points.begin(), + std::find(correspondingSegment->Points.begin(), + correspondingSegment->Points.end(), + point))]); + } + } + } + + } + else + { + for (std::shared_ptr face : referenceMesh) + { + std::shared_ptr correspondingSegment; - for (auto normal : segment->Normals){segmentNormal += normal;} - segmentNormal.normalize(); + // Getting the center of the mesh + Eigen::Vector3d faceCenter = face->Cvt2O3DTriangleMesh()->GetCenter(); - double currentDistance = (faceCenter - segmentCenter).norm(); - // if the distance is smaller than the previous one, update the distance and the corresponding segment - if (std::abs(sin(acos(faceNormal.dot(segmentNormal)))) < angleThreshold && currentDistance < faceDistance) + // Getting the normal of the mesh face + Eigen::Vector3d faceNormal = face->GetFirstNormal(); + faceNormal.normalize(); + + double faceDistance = std::numeric_limits::max(); + if (clusters.size() == 0) { - correspondingSegment = segment; - faceDistance = currentDistance; + DIFFCHECK_WARN("No clusters to associate with the mesh faces. Returning an empty point cloud."); + return unifiedPointCloud; } - - } + for (auto segment : clusters) + { + Eigen::Vector3d segmentCenter; + Eigen::Vector3d segmentNormal; - if (correspondingSegment == nullptr) - { - DIFFCHECK_WARN("No segment found for the face. Skipping the face."); - continue; - } - for (Eigen::Vector3d point : correspondingSegment->Points) - { - bool pointInFace = false; - if (face->IsPointOnFace(point, associationThreshold)) + for (auto point : segment->Points){segmentCenter += point;} + if (segment->GetNumPoints() > 0) + { + segmentCenter /= segment->GetNumPoints(); + } + else + { + DIFFCHECK_WARN("Empty segment. Skipping the segment."); + continue; + } + + for (auto normal : segment->Normals){segmentNormal += normal;} + segmentNormal.normalize(); + + double currentDistance = (faceCenter - segmentCenter).norm(); + // if the distance is smaller than the previous one, update the distance and the corresponding segment + if (std::abs(sin(acos(faceNormal.dot(segmentNormal)))) < angleThreshold && currentDistance < faceDistance) + { + correspondingSegment = segment; + faceDistance = currentDistance; + } + + } + + if (correspondingSegment == nullptr) + { + DIFFCHECK_WARN("No segment found for the face. Skipping the face."); + continue; + } + for (Eigen::Vector3d point : correspondingSegment->Points) { - unifiedPointCloud->Points.push_back(point); - unifiedPointCloud->Normals.push_back( - correspondingSegment->Normals[std::distance( + bool pointInFace = false; + if (face->IsPointOnFace(point, associationThreshold)) + { + unifiedPointCloud->Points.push_back(point); + unifiedPointCloud->Normals.push_back( + correspondingSegment->Normals[std::distance( + correspondingSegment->Points.begin(), + std::find(correspondingSegment->Points.begin(), + correspondingSegment->Points.end(), + point))] + ); + } + } + // removing points from the segment that are in the face + if (unifiedPointCloud->GetNumPoints() == 0) + { + DIFFCHECK_WARN("No point was associated to this segment. Skipping the segment."); + continue; + } + for(Eigen::Vector3d point : unifiedPointCloud->Points) + { + correspondingSegment->Points.erase( + std::remove( correspondingSegment->Points.begin(), - std::find(correspondingSegment->Points.begin(), correspondingSegment->Points.end(), - point))] - ); + point), + correspondingSegment->Points.end()); } } - // removing points from the segment that are in the face - if (unifiedPointCloud->GetNumPoints() == 0) - { - DIFFCHECK_WARN("No point was associated to this segment. Skipping the segment."); - continue; - } - for(Eigen::Vector3d point : unifiedPointCloud->Points) - { - correspondingSegment->Points.erase( - std::remove( - correspondingSegment->Points.begin(), - correspondingSegment->Points.end(), - point), - correspondingSegment->Points.end()); - } } return unifiedPointCloud; } void DFSegmentation::CleanUnassociatedClusters( + bool isCylinder, std::vector> &unassociatedClusters, std::vector> &existingPointCloudSegments, std::vector>> meshes, @@ -206,124 +286,156 @@ namespace diffCheck::segmentation DIFFCHECK_WARN("No unassociated clusters. Nothing is done"); return; } - for (std::shared_ptr cluster : unassociatedClusters) + if (isCylinder == true) { - std::shared_ptr correspondingMeshFace; - Eigen::Vector3d clusterCenter; - Eigen::Vector3d clusterNormal = Eigen::Vector3d::Zero(); - - if (cluster->GetNumPoints() == 0) - { - DIFFCHECK_WARN("Empty cluster. Skipping the cluster."); - continue; - } - for (Eigen::Vector3d point : cluster->Points) - { - clusterCenter += point; - } - clusterCenter /= cluster->GetNumPoints(); - if (cluster->GetNumNormals() == 0) - { - DIFFCHECK_WARN("Empty normals in the cluster. Skipping the cluster."); - continue; - } - for (Eigen::Vector3d normal : cluster->Normals) + DIFFCHECK_WARN("Cylinder mode not implemented yet. Nothing is done"); + return; + } + else + { + for (std::shared_ptr cluster : unassociatedClusters) { - clusterNormal += normal; - } - clusterNormal.normalize(); - - int meshIndex = 0; - int faceIndex = 0 ; - int goodMeshIndex = 0; - int goodFaceIndex = 0; - double distance = std::numeric_limits::max(); + std::shared_ptr correspondingMeshFace; + Eigen::Vector3d clusterCenter; + Eigen::Vector3d clusterNormal = Eigen::Vector3d::Zero(); - if (meshes.size() == 0) - { - DIFFCHECK_WARN("No meshes to associate with the clusters. Skipping the cluster."); - continue; - } - for (std::vector> mesh : meshes) - { - if (mesh.size() == 0) + if (cluster->GetNumPoints() == 0) { - DIFFCHECK_WARN("Empty piece in the meshes vector. Skipping the mesh face vector."); + DIFFCHECK_WARN("Empty cluster. Skipping the cluster."); continue; } - for (std::shared_ptr meshFace : mesh) + if (cluster->GetNumNormals() == 0) { - Eigen::Vector3d faceCenter = Eigen::Vector3d::Zero(); - Eigen::Vector3d faceNormal = Eigen::Vector3d::Zero(); - - std::shared_ptr o3dFace = meshFace->Cvt2O3DTriangleMesh(); - - faceNormal = meshFace->GetFirstNormal(); - faceNormal.normalize(); - faceCenter = o3dFace->GetCenter(); - /* - To make sure we select the right meshFace, we add another metric: - Indeed, from experimentation, sometimes the wrong mesh face is selected, because it is parallel to the correct one - (so the normal don't play a role) and the center of the face is closer to the cluster center than the correct face. - To prevent this, we take into the account the angle between the line linking the center of the meshFace considered - and the center of the point cloud cluster and the normal of the cluster. This value should be close to pi/2 - - The following two lines are not super optimized but more readable than the optimized version - */ - - double dotProduct = clusterNormal.dot((clusterCenter - faceCenter).normalized()); - dotProduct = std::max(-1.0, std::min(1.0, dotProduct)); - double clusterNormalToJunctionLineAngle = std::acos(dotProduct); - - double currentDistance = (clusterCenter - faceCenter).norm() * std::abs(std::cos(clusterNormalToJunctionLineAngle)) - / std::min(std::abs(clusterNormal.dot(faceNormal)), 0.05) ; - if (std::abs(sin(acos(faceNormal.dot(clusterNormal)))) < angleThreshold && currentDistance < distance) + DIFFCHECK_WARN("Empty normals in the cluster. Skipping the cluster."); + continue; + } + if (meshes.size() == 0) + { + DIFFCHECK_WARN("No meshes to associate with the clusters. Skipping the cluster."); + continue; + } + for (Eigen::Vector3d point : cluster->Points) + { + clusterCenter += point; + } + clusterCenter /= cluster->GetNumPoints(); + for (Eigen::Vector3d normal : cluster->Normals) + { + clusterNormal += normal; + } + clusterNormal.normalize(); + + int meshIndex = 0; + int faceIndex = 0 ; + int goodMeshIndex = 0; + int goodFaceIndex = 0; + double distance = std::numeric_limits::max(); + + + for (std::vector> mesh : meshes) + { + if (mesh.size() == 0) + { + DIFFCHECK_WARN("Empty piece in the meshes vector. Skipping the mesh face vector."); + continue; + } + for (std::shared_ptr meshFace : mesh) { - goodMeshIndex = meshIndex; - goodFaceIndex = faceIndex; - distance = currentDistance; - correspondingMeshFace = meshFace; + Eigen::Vector3d faceCenter = Eigen::Vector3d::Zero(); + Eigen::Vector3d faceNormal = Eigen::Vector3d::Zero(); + if (isCylinder) + { + std::tuple centerAndAxis = mesh[0]->GetCenterAndAxis(); + Eigen::Vector3d center = std::get<0>(centerAndAxis); + Eigen::Vector3d axis = std::get<1>(centerAndAxis); + double dotProduct = clusterNormal.dot(axis); + dotProduct = std::max(-1.0, std::min(1.0, dotProduct)); + + double currentDistance = (center - clusterCenter).norm() * std::abs(dotProduct); + + if (std::abs(dotProduct) < angleThreshold && currentDistance < distance) + { + goodMeshIndex = meshIndex; + goodFaceIndex = faceIndex; + distance = currentDistance; + correspondingMeshFace = meshFace; + } + + } + else + { + + + std::shared_ptr o3dFace = meshFace->Cvt2O3DTriangleMesh(); + + faceNormal = meshFace->GetFirstNormal(); + faceNormal.normalize(); + faceCenter = o3dFace->GetCenter(); + /* + To make sure we select the right meshFace, we add another metric: + Indeed, from experimentation, sometimes the wrong mesh face is selected, because it is parallel to the correct one + (so the normal don't play a role) and the center of the face is closer to the cluster center than the correct face. + To prevent this, we take into the account the angle between the line linking the center of the meshFace considered + and the center of the point cloud cluster and the normal of the cluster. This value should be close to pi/2 + + The following two lines are not super optimized but more readable than the optimized version + */ + + double dotProduct = clusterNormal.dot((clusterCenter - faceCenter).normalized()); + dotProduct = std::max(-1.0, std::min(1.0, dotProduct)); + double clusterNormalToJunctionLineAngle = std::acos(dotProduct); + + double currentDistance = (clusterCenter - faceCenter).norm() * std::abs(std::cos(clusterNormalToJunctionLineAngle)) + / std::min(std::abs(clusterNormal.dot(faceNormal)), 0.05) ; + if (std::abs(sin(acos(faceNormal.dot(clusterNormal)))) < angleThreshold && currentDistance < distance) + { + goodMeshIndex = meshIndex; + goodFaceIndex = faceIndex; + distance = currentDistance; + correspondingMeshFace = meshFace; + } + } + faceIndex++; } - faceIndex++; + meshIndex++; } - meshIndex++; - } - if (correspondingMeshFace == nullptr) - { - DIFFCHECK_WARN("No mesh face found for the cluster. Skipping the cluster."); - continue; - } - if (goodMeshIndex >= existingPointCloudSegments.size()) - { - DIFFCHECK_WARN("No segment found for the face. Skipping the face."); - continue; - } - std::shared_ptr completed_segment = existingPointCloudSegments[goodMeshIndex]; + if (correspondingMeshFace == nullptr) + { + DIFFCHECK_WARN("No mesh face found for the cluster. Skipping the cluster."); + continue; + } + if (goodMeshIndex >= existingPointCloudSegments.size()) + { + DIFFCHECK_WARN("No segment found for the face. Skipping the face."); + continue; + } + std::shared_ptr completed_segment = existingPointCloudSegments[goodMeshIndex]; - for (Eigen::Vector3d point : cluster->Points) - { - if (correspondingMeshFace->IsPointOnFace(point, associationThreshold)) + for (Eigen::Vector3d point : cluster->Points) { - completed_segment->Points.push_back(point); - completed_segment->Normals.push_back(cluster->Normals[std::distance(cluster->Points.begin(), std::find(cluster->Points.begin(), cluster->Points.end(), point))]); + if (correspondingMeshFace->IsPointOnFace(point, associationThreshold)) + { + completed_segment->Points.push_back(point); + completed_segment->Normals.push_back(cluster->Normals[std::distance(cluster->Points.begin(), std::find(cluster->Points.begin(), cluster->Points.end(), point))]); + } } - } - std::vector indicesToRemove; + std::vector indicesToRemove; - for (int i = 0; i < cluster->Points.size(); ++i) - { - if (std::find(completed_segment->Points.begin(), completed_segment->Points.end(), cluster->Points[i]) != completed_segment->Points.end()) + for (int i = 0; i < cluster->Points.size(); ++i) { - indicesToRemove.push_back(i); + if (std::find(completed_segment->Points.begin(), completed_segment->Points.end(), cluster->Points[i]) != completed_segment->Points.end()) + { + indicesToRemove.push_back(i); + } + } + for (auto it = indicesToRemove.rbegin(); it != indicesToRemove.rend(); ++it) + { + std::swap(cluster->Points[*it], cluster->Points.back()); + cluster->Points.pop_back(); + std::swap(cluster->Normals[*it], cluster->Normals.back()); + cluster->Normals.pop_back(); } - } - for (auto it = indicesToRemove.rbegin(); it != indicesToRemove.rend(); ++it) - { - std::swap(cluster->Points[*it], cluster->Points.back()); - cluster->Points.pop_back(); - std::swap(cluster->Normals[*it], cluster->Normals.back()); - cluster->Normals.pop_back(); } } }; diff --git a/src/diffCheck/segmentation/DFSegmentation.hh b/src/diffCheck/segmentation/DFSegmentation.hh index 2e325aac..ee05da3b 100644 --- a/src/diffCheck/segmentation/DFSegmentation.hh +++ b/src/diffCheck/segmentation/DFSegmentation.hh @@ -27,19 +27,21 @@ namespace diffCheck::segmentation public: ///< segmentation refinement methods /** @brief Associates point cloud segments to mesh faces and merges them. It uses the center of mass of the segments and the mesh faces to find correspondances. For each mesh face it then iteratively associate the points of the segment that are actually on the mesh face. - * @param referenceMesh the vector of mesh faces to associate with the segments. It is a representation of a beam and its faces. + * @param isCylinder a boolean to indicate if the model is a cylinder. If true, the method will use the GetCenterAndAxis method of the mesh to find the center and axis of the mesh. based on that, we only want points that have normals more or less perpendicular to the cylinder axis. * @param clusters the vector of clusters from cilantro to associate with the mesh faces of the reference mesh * @param angleThreshold the threshold to consider the a cluster as potential candidate for association. the value passed is the minimum sine of the angles. A value of 0 requires perfect alignment (angle = 0), while a value of 0.1 allows an angle of 5.7 degrees. * @param associationThreshold the threshold to consider the points of a segment and a mesh face as associable. It is the ratio between the surface of the closest mesh triangle and the sum of the areas of the three triangles that form the rest of the pyramid described by the mesh triangle and the point we want to associate or not. The lower the number, the more strict the association will be and some poinnts on the mesh face might be wrongfully excluded. * @return std::shared_ptr The unified segments */ static std::shared_ptr DFSegmentation::AssociateClustersToMeshes( + bool isCylinder, std::vector> referenceMesh, std::vector> &clusters, double angleThreshold = 0.1, double associationThreshold = 0.1); /** @brief Iterated through clusters and finds the corresponding mesh face. It then associates the points of the cluster that are on the mesh face to the segment already associated with the mesh face. + * @param isCylinder a boolean to indicate if the model is a cylinder. If true, the method will use the GetCenterAndAxis method of the mesh to find the center and axis of the mesh. based on that, we only want points that have normals more or less perpendicular to the cylinder axis. * @param unassociatedClusters the clusters from the normal-based segmentatinon that haven't been associated yet. * @param existingPointCloudSegments the already associated segments * @param meshes the mesh faces for all the model. This is used to associate the clusters to the mesh faces. @@ -47,6 +49,7 @@ namespace diffCheck::segmentation * @param associationThreshold the threshold to consider the points of a segment and a mesh face as associable. It is the ratio between the surface of the closest mesh triangle and the sum of the areas of the three triangles that form the rest of the pyramid described by the mesh triangle and the point we want to associate or not. The lower the number, the more strict the association will be and some poinnts on the mesh face might be wrongfully excluded. */ static void DFSegmentation::CleanUnassociatedClusters( + bool isCylinder, std::vector> &unassociatedClusters, std::vector> &existingPointCloudSegments, std::vector>> meshes, From 985085108855667a3c622d8d6f27e3e0fea64835 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:58:50 +0200 Subject: [PATCH 07/40] WIP-UPDATE: implement changes between cases with and without cylinder in code.py --- src/gh/components/DF_CAD_segmentator/code.py | 35 ++++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index 2c551f00..5894d13e 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -51,15 +51,38 @@ def RunScript(self, df_beams_meshes.append(df_b_mesh_faces) rh_beams_meshes.append(rh_b_mesh_faces) - df_asssociated_cluster = dfb_segmentation.DFSegmentation.associate_clusters( - reference_mesh=df_b_mesh_faces, + # different association depending on the type of beam + if df_b.is_cylinder: + df_asssociated_cluster = dfb_segmentation.DFSegmentation.associate_clusters( + is_cylinder=True, + reference_mesh=df_b_mesh_faces, + unassociated_clusters=df_clouds, + angle_threshold=i_angle_threshold, + association_threshold=i_association_threshold + ) + else: + df_asssociated_cluster = dfb_segmentation.DFSegmentation.associate_clusters( + is_cylinder=False, + reference_mesh=df_b_mesh_faces, + unassociated_clusters=df_clouds, + angle_threshold=i_angle_threshold, + association_threshold=i_association_threshold + ) + df_clusters.append(df_asssociated_cluster) + + # clean the unassociated clusters depending on the type of assembly + if i_assembly.contains_cylinders: + dfb_segmentation.DFSegmentation.clean_unassociated_clusters( + is_cylinder=True, unassociated_clusters=df_clouds, + associated_clusters=df_clusters, + reference_mesh=df_beams_meshes, angle_threshold=i_angle_threshold, association_threshold=i_association_threshold ) - df_clusters.append(df_asssociated_cluster) - - dfb_segmentation.DFSegmentation.clean_unassociated_clusters( + else: + dfb_segmentation.DFSegmentation.clean_unassociated_clusters( + is_cylinder=False, unassociated_clusters=df_clouds, associated_clusters=df_clusters, reference_mesh=df_beams_meshes, @@ -71,7 +94,7 @@ def RunScript(self, o_clusters = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_clusters] - return o_clusters, rh_beams_meshes + return o_clusters # if __name__ == "__main__": # com = DFCADSegmentator() From 7d89b06f0db4ffbccbf81c4eccdc1ec451d381ed Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:18:48 +0200 Subject: [PATCH 08/40] UPDATE: update binding to add is_cylinder parameter to segmentation methods --- src/diffCheckBindings.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/diffCheckBindings.cc b/src/diffCheckBindings.cc index 55e2db7e..ba4f5295 100644 --- a/src/diffCheckBindings.cc +++ b/src/diffCheckBindings.cc @@ -190,12 +190,14 @@ PYBIND11_MODULE(diffcheck_bindings, m) { py::arg("color_clusters") = false) .def_static("associate_clusters", &diffCheck::segmentation::DFSegmentation::AssociateClustersToMeshes, + py::arg("is_cylinder"), py::arg("reference_mesh"), py::arg("unassociated_clusters"), py::arg("angle_threshold") = 0.1, py::arg("association_threshold") = 0.1) .def_static("clean_unassociated_clusters", &diffCheck::segmentation::DFSegmentation::CleanUnassociatedClusters, + py::arg("is_cylinder"), py::arg("unassociated_clusters"), py::arg("associated_clusters"), py::arg("reference_mesh"), From f4c067d33162bfb4b2722154e28fee7fe2bcf70b Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:20:26 +0200 Subject: [PATCH 09/40] FIX: __init__.py imports the binding as well --- src/gh/diffCheck/diffCheck/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/gh/diffCheck/diffCheck/__init__.py b/src/gh/diffCheck/diffCheck/__init__.py index 9285b46c..1e91a49e 100644 --- a/src/gh/diffCheck/diffCheck/__init__.py +++ b/src/gh/diffCheck/diffCheck/__init__.py @@ -3,6 +3,10 @@ __version__ = "0.0.24" +# make the dlls available to the python interpreter PATH_TO_DLL = "dlls" extra_dll_dir = os.path.join(os.path.dirname(__file__), PATH_TO_DLL) -os.add_dll_directory(extra_dll_dir) \ No newline at end of file +os.add_dll_directory(extra_dll_dir) + +# import the bindings +from . import diffcheck_bindings \ No newline at end of file From dae8a0bba71ad78f8edfaae377ede9dc18bb97f7 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Sat, 31 Aug 2024 20:25:31 +0200 Subject: [PATCH 10/40] WIP: cylinder segmentation now without IsPointOnFace check, because not useful here --- src/diffCheck/segmentation/DFSegmentation.cc | 57 ++++++++++++++------ 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/src/diffCheck/segmentation/DFSegmentation.cc b/src/diffCheck/segmentation/DFSegmentation.cc index ab344f42..cc7231a5 100644 --- a/src/diffCheck/segmentation/DFSegmentation.cc +++ b/src/diffCheck/segmentation/DFSegmentation.cc @@ -171,16 +171,39 @@ namespace diffCheck::segmentation for (Eigen::Vector3d point : correspondingSegment->Points) { bool pointInFace = false; - if (face->IsPointOnFace(point, associationThreshold)) - { - unifiedPointCloud->Points.push_back(point); - unifiedPointCloud->Normals.push_back( - correspondingSegment->Normals[std::distance( - correspondingSegment->Points.begin(), - std::find(correspondingSegment->Points.begin(), - correspondingSegment->Points.end(), - point))]); - } + + unifiedPointCloud->Points.push_back(point); + unifiedPointCloud->Normals.push_back( + correspondingSegment->Normals[std::distance( + correspondingSegment->Points.begin(), + std::find(correspondingSegment->Points.begin(), + correspondingSegment->Points.end(), + point))]); + } + // removing points from the segment that are in the face + if (unifiedPointCloud->GetNumPoints() == 0) + { + DIFFCHECK_WARN("No point was associated to this segment. Skipping the segment."); + continue; + } + for(Eigen::Vector3d point : unifiedPointCloud->Points) + { + correspondingSegment->Points.erase( + std::remove( + correspondingSegment->Points.begin(), + correspondingSegment->Points.end(), + point), + correspondingSegment->Points.end()); + } + if (correspondingSegment->GetNumPoints() == 0) + { + DIFFCHECK_WARN("No point was left in the segment. Deleting the segment."); + clusters.erase( + std::remove( + clusters.begin(), + clusters.end(), + correspondingSegment), + clusters.end()); } } @@ -286,11 +309,6 @@ namespace diffCheck::segmentation DIFFCHECK_WARN("No unassociated clusters. Nothing is done"); return; } - if (isCylinder == true) - { - DIFFCHECK_WARN("Cylinder mode not implemented yet. Nothing is done"); - return; - } else { for (std::shared_ptr cluster : unassociatedClusters) @@ -414,11 +432,18 @@ namespace diffCheck::segmentation for (Eigen::Vector3d point : cluster->Points) { - if (correspondingMeshFace->IsPointOnFace(point, associationThreshold)) + if(isCylinder) { completed_segment->Points.push_back(point); completed_segment->Normals.push_back(cluster->Normals[std::distance(cluster->Points.begin(), std::find(cluster->Points.begin(), cluster->Points.end(), point))]); + } + else + if (correspondingMeshFace->IsPointOnFace(point, associationThreshold)) + { + completed_segment->Points.push_back(point); + completed_segment->Normals.push_back(cluster->Normals[std::distance(cluster->Points.begin(), std::find(cluster->Points.begin(), cluster->Points.end(), point))]); + } } std::vector indicesToRemove; From 21e7c6e96311ef5779a5da56449a603d64043a43 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Mon, 2 Sep 2024 19:36:12 +0200 Subject: [PATCH 11/40] UPDATE: joint detector more robust in the adjacency face calculation --- .../diffCheck/diffCheck/df_joint_detector.py | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/gh/diffCheck/diffCheck/df_joint_detector.py b/src/gh/diffCheck/diffCheck/df_joint_detector.py index 75800c7a..5bdc4166 100644 --- a/src/gh/diffCheck/diffCheck/df_joint_detector.py +++ b/src/gh/diffCheck/diffCheck/df_joint_detector.py @@ -121,6 +121,16 @@ def run(self): Bounding_geometry.Transform(xform) # check if face's centers are inside the OBB + ''' + the structure of the disctionnary is as follows: + { + face_id: (face, is_inside) + ... + } + face_id is int + face is Rhino.Geometry.BrepFace + is_inside is bool + ''' faces = {} for idx, face in enumerate(self.brep.Faces): face_centroid = rg.AreaMassProperties.Compute(face).Centroid @@ -128,10 +138,30 @@ def run(self): projected_centroid = face.PointAt(coord[1], coord[2]) faces[idx] = (face, Bounding_geometry.IsPointInside(projected_centroid, sc.doc.ModelAbsoluteTolerance, True)) + # compute the adjacency list of each face + adjacency_of_faces = {} + ''' + the structure of the disctionnary is as follows: + { + face_id: (face, [adj_face_id_1, adj_face_id_2, ...]) + ... + } + face_id is int + face is Rhino.Geometry.BrepFace + adj_face_id_1, adj_face_id_2, ... are int + ''' + for idx, face in faces.items(): + if not face[1]: + continue + adjacency_of_faces[idx] = (face[0], [adj_face for adj_face in face[0].AdjacentFaces() if faces[adj_face][1] and adj_face != idx]) + adjacency_of_faces = diffCheck.df_util.merge_shared_indexes(adjacency_of_faces) + new_joint_face_ids = [[key] + value[1] for key, value in adjacency_of_faces.items()] + print("new_joint_face_ids: ",new_joint_face_ids) + # get the proximity faces of the joint faces joint_face_ids = [[key] + [adj_face for adj_face in value[0].AdjacentFaces() if faces[adj_face][1] and adj_face != key] for key, value in faces.items() if value[1]] - - face_ids = self._assign_ids(joint_face_ids) + print("joint_face_ids: ",joint_face_ids) + face_ids = self._assign_ids(new_joint_face_ids) self._faces = [(face, face_ids[idx]) for idx, face in enumerate(self.brep.Faces)] From 7a4ee7362c0e3fb34799829d52d57f72dbf250b0 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Mon, 2 Sep 2024 19:37:46 +0200 Subject: [PATCH 12/40] UPDATE: add merge_shared_index method to df_utils to be used by df_joint_detector.py --- src/gh/diffCheck/diffCheck/df_util.py | 53 +++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/gh/diffCheck/diffCheck/df_util.py b/src/gh/diffCheck/diffCheck/df_util.py index 2a1b2717..07822862 100644 --- a/src/gh/diffCheck/diffCheck/df_util.py +++ b/src/gh/diffCheck/diffCheck/df_util.py @@ -159,3 +159,56 @@ def get_doc_2_meters_unitf(): elif RhinoDoc.ModelUnitSystem == Rhino.UnitSystem.Yards: unit_scale = 0.9144 return unit_scale + +def merge_shared_indexes(original_dict): + """ + Merge the shared indexes of a dictionary + + Assume we have a dictionary with lists of indexes as values. + We want to merge the lists that share some indexes, in order to have a dictionary with, for each key, indexes that are not present under other keys. + + :param original_dict: the dictionary to merge + :return: the merged dictionary + """ + merged_dict = {} + index_to_key = {} + + for key, (face, indexes) in original_dict.items(): + merged_indexes = set(indexes) + keys_to_merge = set() + + for index in indexes: + if index in index_to_key: + keys_to_merge.add(index_to_key[index]) + + for merge_key in keys_to_merge: + merged_indexes.update(merged_dict[merge_key][1]) + # del merged_dict[merge_key] + + for index in merged_indexes: + index_to_key[index] = key + + merged_dict[key] = (face, list(merged_indexes)) + + keys_with_duplicates = {} + + for key in merged_dict.keys(): + for other_key, (face, indexes) in merged_dict.items(): + if key in indexes: + if key not in keys_with_duplicates: + keys_with_duplicates[key] = [] + keys_with_duplicates[key].append(other_key) + print("keys with duplicates; ",keys_with_duplicates) + + # # Remove the cross-references + # for first_key, first_duplicates in keys_with_duplicates.items(): + # for second_key, second_duplicates in keys_with_duplicates.items(): + # if first_key < second_key and first_key in second_duplicates and second_key in first_duplicates: + # del merged_dict[second_key] + + # # remoev the self-references + # for key, duplicates in keys_with_duplicates.items(): + # if key in duplicates: + # merged_dict[key] = (merged_dict[key][0], [index for index in merged_dict[key][1] if index != key]) + + return merged_dict \ No newline at end of file From 46d1249c1651b6f429f4990fcc483d4aef1f28e9 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Mon, 2 Sep 2024 19:39:28 +0200 Subject: [PATCH 13/40] FIX: remove commented-out code for component testing --- src/gh/components/DF_CAD_segmentator/code.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index 9c20cf5b..21060286 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -78,12 +78,3 @@ def RunScript(self, o_clusters = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_clusters] return o_clusters - -# if __name__ == "__main__": -# com = DFCADSegmentator() -# o_clusters, rh_beams_meshes = com.RunScript( -# i_clouds, -# i_assembly, -# i_angle_threshold, -# i_association_threshold -# ) \ No newline at end of file From 4ea1e2680505152a2f0ac5afc160b414175b5ec9 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Mon, 2 Sep 2024 19:52:04 +0200 Subject: [PATCH 14/40] FIX: update diffCheckApp.cc --- src/diffCheckApp.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/diffCheckApp.cc b/src/diffCheckApp.cc index cdb7c7d7..c84800ae 100644 --- a/src/diffCheckApp.cc +++ b/src/diffCheckApp.cc @@ -57,6 +57,7 @@ int main() { std::shared_ptr unifiedSegment = std::make_shared(); unifiedSegment = diffCheck::segmentation::DFSegmentation::AssociateClustersToMeshes( + true, meshSrc[i], segments, .2, @@ -64,7 +65,8 @@ int main() unifiedSegments.push_back(unifiedSegment); } - diffCheck::segmentation::DFSegmentation::CleanUnassociatedClusters(segments, + diffCheck::segmentation::DFSegmentation::CleanUnassociatedClusters(true, + segments, unifiedSegments, meshSrc, .2, From fdd13a3a2078edf595bb411e26b270bb34df95ec Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Mon, 2 Sep 2024 22:27:38 +0200 Subject: [PATCH 15/40] FIX: applying changes from pre-commit --- src/gh/diffCheck/diffCheck/df_geometries.py | 7 ++++--- .../diffCheck/diffCheck/df_joint_detector.py | 20 +++++++++---------- src/gh/diffCheck/diffCheck/df_util.py | 6 +++--- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/gh/diffCheck/diffCheck/df_geometries.py b/src/gh/diffCheck/diffCheck/df_geometries.py index 19573e7a..1a147c92 100644 --- a/src/gh/diffCheck/diffCheck/df_geometries.py +++ b/src/gh/diffCheck/diffCheck/df_geometries.py @@ -136,7 +136,7 @@ def from_brep_face(cls, df_face: DFFace = cls([], joint_id) # if brep_face.IsCylinder(): - + # df_face = cls([[[], [], []]], joint_id) # cls.is_cylinder = True # df_face._rh_brepface = brep_face @@ -298,11 +298,12 @@ def __post_init__(self): self._index_assembly = None self._center = None + self.__id = uuid.uuid4().int + self.is_cylinder = None def deepcopy(self): return DFBeam(self.name, [face.deepcopy() for face in self.faces]) - self.__id = uuid.uuid4().int - self.is_cylinder = None + @classmethod def from_brep_face(cls, brep): diff --git a/src/gh/diffCheck/diffCheck/df_joint_detector.py b/src/gh/diffCheck/diffCheck/df_joint_detector.py index 5bdc4166..4cf54f68 100644 --- a/src/gh/diffCheck/diffCheck/df_joint_detector.py +++ b/src/gh/diffCheck/diffCheck/df_joint_detector.py @@ -50,7 +50,7 @@ def _assign_ids(self, joint_face_ids): def is_cylinder_beam(self): """ Detects if the brep is a cylinder beam. - This is done by finding the largest cylinder in the brep, + This is done by finding the largest cylinder in the brep, and looking if all brep vertices are inside the cylinder. :return: True if the brep is detected as a cylinder beam, False otherwise @@ -63,7 +63,7 @@ def is_cylinder_beam(self): if face.IsCylinder(): open_cylinder = face.ToBrep() open_cylinders.append(open_cylinder) - + # find largest cylinder largest_cylinder = None largest_srf = 0 @@ -72,17 +72,17 @@ def is_cylinder_beam(self): largest_srf = cylinder.GetArea() print(largest_srf) largest_cylinder = cylinder.CapPlanarHoles(sc.doc.ModelAbsoluteTolerance) - + # Check if the cylinder exists if largest_cylinder is None: return False, None - + # check if all vertices are inside the cylinder for vertex in self.brep.Vertices: if not largest_cylinder.IsPointInside(vertex.Location, sc.doc.ModelAbsoluteTolerance, False): return False, None return True, largest_cylinder - + def run(self): """ @@ -94,7 +94,7 @@ def run(self): """ # check if the brep is a cylinder beam is_cylinder_beam, cylinder = self.is_cylinder_beam() - + # brep vertices to cloud df_cloud = diffCheck.diffcheck_bindings.dfb_geometry.DFPointCloud() df_cloud.points = [np.array([vertex.Location.X, vertex.Location.Y, vertex.Location.Z]).reshape(3, 1) for vertex in self.brep.Vertices] @@ -126,7 +126,7 @@ def run(self): { face_id: (face, is_inside) ... - } + } face_id is int face is Rhino.Geometry.BrepFace is_inside is bool @@ -137,7 +137,7 @@ def run(self): coord = face.ClosestPoint(face_centroid) projected_centroid = face.PointAt(coord[1], coord[2]) faces[idx] = (face, Bounding_geometry.IsPointInside(projected_centroid, sc.doc.ModelAbsoluteTolerance, True)) - + # compute the adjacency list of each face adjacency_of_faces = {} ''' @@ -145,7 +145,7 @@ def run(self): { face_id: (face, [adj_face_id_1, adj_face_id_2, ...]) ... - } + } face_id is int face is Rhino.Geometry.BrepFace adj_face_id_1, adj_face_id_2, ... are int @@ -165,4 +165,4 @@ def run(self): self._faces = [(face, face_ids[idx]) for idx, face in enumerate(self.brep.Faces)] - return self._faces, is_cylinder_beam \ No newline at end of file + return self._faces, is_cylinder_beam diff --git a/src/gh/diffCheck/diffCheck/df_util.py b/src/gh/diffCheck/diffCheck/df_util.py index 07822862..2468c63b 100644 --- a/src/gh/diffCheck/diffCheck/df_util.py +++ b/src/gh/diffCheck/diffCheck/df_util.py @@ -199,7 +199,7 @@ def merge_shared_indexes(original_dict): keys_with_duplicates[key] = [] keys_with_duplicates[key].append(other_key) print("keys with duplicates; ",keys_with_duplicates) - + # # Remove the cross-references # for first_key, first_duplicates in keys_with_duplicates.items(): # for second_key, second_duplicates in keys_with_duplicates.items(): @@ -210,5 +210,5 @@ def merge_shared_indexes(original_dict): # for key, duplicates in keys_with_duplicates.items(): # if key in duplicates: # merged_dict[key] = (merged_dict[key][0], [index for index in merged_dict[key][1] if index != key]) - - return merged_dict \ No newline at end of file + + return merged_dict From 8a9be0795fb63cd7f6da7f8c2cce4aff026327ed Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Mon, 2 Sep 2024 22:32:28 +0200 Subject: [PATCH 16/40] FIX: forgot a file in pre-commit file adding in previous commit --- src/gh/components/DF_CAD_segmentator/code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index 21060286..1936189f 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -54,7 +54,7 @@ def RunScript(self, association_threshold=i_association_threshold ) df_clusters.append(df_asssociated_cluster) - + # clean the unassociated clusters depending on the type of assembly if i_assembly.contains_cylinders: dfb_segmentation.DFSegmentation.clean_unassociated_clusters( From 7540d8ba1d3aba4c54c09fe2bf80ca8709ce25e6 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Tue, 3 Sep 2024 03:12:34 +0200 Subject: [PATCH 17/40] UPDATE: IsPointOnFace method optimized --- src/diffCheck/geometry/DFMesh.cc | 39 ++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/diffCheck/geometry/DFMesh.cc b/src/diffCheck/geometry/DFMesh.cc index afdc7f21..23f59901 100644 --- a/src/diffCheck/geometry/DFMesh.cc +++ b/src/diffCheck/geometry/DFMesh.cc @@ -112,26 +112,37 @@ namespace diffCheck::geometry Eigen::Vector3d v1 = this->Vertices[triangle[1]]; Eigen::Vector3d v2 = this->Vertices[triangle[2]]; Eigen::Vector3d n = (v1 - v0).cross(v2 - v0); - double normOfNormal = n.norm(); n.normalize(); - Eigen::Vector3d projectedPoint = point - n * (n.dot(point - v0)) ; + // Project the point onto the plane of the triangle + Eigen::Vector3d projectedPoint = point - n * (n.dot(point - v0)); - double referenceTriangleArea = normOfNormal*0.5; - Eigen::Vector3d n1 = (v1 - v0).cross(projectedPoint - v0); - double area1 = n1.norm()*0.5; - Eigen::Vector3d n2 = (v2 - v1).cross(projectedPoint - v1); - double area2 = n2.norm()*0.5; - Eigen::Vector3d n3 = (v0 - v2).cross(projectedPoint - v2); - double area3 = n3.norm()*0.5; - double res = (area1 + area2 + area3 - referenceTriangleArea) / referenceTriangleArea; + // Compute vectors + Eigen::Vector3d v0v1 = v1 - v0; + Eigen::Vector3d v0v2 = v2 - v0; + Eigen::Vector3d v0p = projectedPoint - v0; - // arbitrary value to avoid false positives (points that, when projected on the triangle, are in it, but that are actually located too far from the mesh to actually belong to it) - double maxProjectionDistance = std::min({(v1 - v0).norm(), (v2 - v1).norm(), (v0 - v2).norm()}) / 2; + // Compute dot products + double dot00 = v0v2.dot(v0v2); + double dot01 = v0v2.dot(v0v1); + double dot02 = v0v2.dot(v0p); + double dot11 = v0v1.dot(v0v1); + double dot12 = v0v1.dot(v0p); - if (std::abs(res) < associationThreshold && (projectedPoint - point).norm() < maxProjectionDistance) + // Compute barycentric coordinates + double invDenom = 1.0 / (dot00 * dot11 - dot01 * dot01); + double u = (dot11 * dot02 - dot01 * dot12) * invDenom; + double v = (dot00 * dot12 - dot01 * dot02) * invDenom; + + // Check if point is in triangle + if ((u >= -associationThreshold) && (v >= -associationThreshold) && (u + v <= 1 + associationThreshold)) { - return true; + // Check if the point is close enough to the face + double maxProjectionDistance = std::min({(v1 - v0).norm(), (v2 - v1).norm(), (v0 - v2).norm()}) ; + if ((projectedPoint - point).norm() < maxProjectionDistance) + { + return true; + } } } return false; From e04fe7099728957e82ed666178ab51bb020617f1 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Tue, 3 Sep 2024 03:13:49 +0200 Subject: [PATCH 18/40] UPDATE: Small adaptation for cylinder case in DFSegmentation, integrate association threshold --- src/diffCheck/segmentation/DFSegmentation.cc | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/diffCheck/segmentation/DFSegmentation.cc b/src/diffCheck/segmentation/DFSegmentation.cc index cc7231a5..9dd08df1 100644 --- a/src/diffCheck/segmentation/DFSegmentation.cc +++ b/src/diffCheck/segmentation/DFSegmentation.cc @@ -124,6 +124,10 @@ namespace diffCheck::segmentation double faceDistance = std::numeric_limits::max(); std::shared_ptr correspondingSegment; + std::vector minmax = face->GetTightBoundingBox(); + Eigen::Vector3d min = minmax[0]; + Eigen::Vector3d max = minmax[1]; + if (clusters.size() == 0) { DIFFCHECK_WARN("No clusters to associate with the mesh faces. Returning an empty point cloud."); @@ -157,7 +161,8 @@ namespace diffCheck::segmentation // we consider the distance to the cylinder axis, not the cylinder center Eigen::Vector3d projectedSegmentCenter = (segmentCenter - cylinderCenter).dot(cylinderAxis) * cylinderAxis + cylinderCenter; double currentDistance = (cylinderCenter - projectedSegmentCenter).norm(); - if (std::abs(cylinderAxis.dot(segmentNormal)) < angleThreshold && currentDistance < faceDistance) + double absoluteDistance = (segmentCenter - cylinderCenter).norm(); + if (std::abs(cylinderAxis.dot(segmentNormal)) < angleThreshold && currentDistance < faceDistance && absoluteDistance < (max - min).norm()*associationThreshold) { correspondingSegment = segment; faceDistance = currentDistance; @@ -363,19 +368,24 @@ namespace diffCheck::segmentation Eigen::Vector3d faceNormal = Eigen::Vector3d::Zero(); if (isCylinder) { + std::vector minmax = meshFace->GetTightBoundingBox(); + Eigen::Vector3d min = minmax[0]; + Eigen::Vector3d max = minmax[1]; + std::tuple centerAndAxis = mesh[0]->GetCenterAndAxis(); Eigen::Vector3d center = std::get<0>(centerAndAxis); Eigen::Vector3d axis = std::get<1>(centerAndAxis); double dotProduct = clusterNormal.dot(axis); dotProduct = std::max(-1.0, std::min(1.0, dotProduct)); - double currentDistance = (center - clusterCenter).norm() * std::abs(dotProduct); + double currentDistance = (center - clusterCenter).norm() ; + double adaptedDistance = currentDistance * std::abs(dotProduct); - if (std::abs(dotProduct) < angleThreshold && currentDistance < distance) + if (std::abs(dotProduct) < angleThreshold && adaptedDistance < distance && currentDistance < (max - min).norm()*associationThreshold) { goodMeshIndex = meshIndex; goodFaceIndex = faceIndex; - distance = currentDistance; + distance = adaptedDistance; correspondingMeshFace = meshFace; } From 4bb9b34b819d5ab4df1ea5ca61543a7b7cfd3eac Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Tue, 3 Sep 2024 03:15:29 +0200 Subject: [PATCH 19/40] UPDATE: code.py of segmentation component cleaned --- src/gh/components/DF_CAD_segmentator/code.py | 21 ++++++------------- .../components/DF_joint_segmentator/code.py | 13 ++++++++++-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index 1936189f..c3155570 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -37,21 +37,12 @@ def RunScript(self, rh_beams_meshes.append(rh_b_mesh_faces) # different association depending on the type of beam - if df_b.is_cylinder: - df_asssociated_cluster = dfb_segmentation.DFSegmentation.associate_clusters( - is_cylinder=True, - reference_mesh=df_b_mesh_faces, - unassociated_clusters=df_clouds, - angle_threshold=i_angle_threshold, - association_threshold=i_association_threshold - ) - else: - df_asssociated_cluster = dfb_segmentation.DFSegmentation.associate_clusters( - is_cylinder=False, - reference_mesh=df_b_mesh_faces, - unassociated_clusters=df_clouds, - angle_threshold=i_angle_threshold, - association_threshold=i_association_threshold + df_asssociated_cluster = dfb_segmentation.DFSegmentation.associate_clusters( + is_cylinder=df_b.is_cylinder, + reference_mesh=df_b_mesh_faces, + unassociated_clusters=df_clouds, + angle_threshold=i_angle_threshold, + association_threshold=i_association_threshold ) df_clusters.append(df_asssociated_cluster) diff --git a/src/gh/components/DF_joint_segmentator/code.py b/src/gh/components/DF_joint_segmentator/code.py index f7baf46a..ed793db7 100644 --- a/src/gh/components/DF_joint_segmentator/code.py +++ b/src/gh/components/DF_joint_segmentator/code.py @@ -61,8 +61,17 @@ def RunScript(self, ref_rh_joint_clouds.append(df_cvt.cvt_dfcloud_2_rhcloud(ref_df_joint_cloud)) # find the corresponding clusters and merge them - df_joint_segment = diffcheck_bindings.dfb_segmentation.DFSegmentation.associate_clusters(df_joint, df_cloud_clusters, i_angle_threshold, i_distance_threshold) - diffcheck_bindings.dfb_segmentation.DFSegmentation.clean_unassociated_clusters(df_cloud_clusters, [df_joint_segment], [df_joint], i_angle_threshold, i_distance_threshold) + df_joint_segment = diffcheck_bindings.dfb_segmentation.DFSegmentation.associate_clusters(False, + df_joint, + df_cloud_clusters, + i_angle_threshold, + i_distance_threshold) + diffcheck_bindings.dfb_segmentation.DFSegmentation.clean_unassociated_clusters(False, + df_cloud_clusters, + [df_joint_segment], + [df_joint], + i_angle_threshold, + i_distance_threshold) # register the merged clusters to the reference point cloud registration = diffcheck_bindings.dfb_registrations.DFRefinedRegistration.O3DICP(df_joint_segment, ref_df_joint_cloud) From 407ce1634ff797d24a9e1b4e538d3c5722c9dcb8 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Tue, 3 Sep 2024 03:17:01 +0200 Subject: [PATCH 20/40] FIX: is_cylinder property of DFBeam properly handled --- src/gh/diffCheck/diffCheck/df_geometries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gh/diffCheck/diffCheck/df_geometries.py b/src/gh/diffCheck/diffCheck/df_geometries.py index 1a147c92..e395a835 100644 --- a/src/gh/diffCheck/diffCheck/df_geometries.py +++ b/src/gh/diffCheck/diffCheck/df_geometries.py @@ -299,7 +299,7 @@ def __post_init__(self): self._center = None self.__id = uuid.uuid4().int - self.is_cylinder = None + # self.is_cylinder = None def deepcopy(self): return DFBeam(self.name, [face.deepcopy() for face in self.faces]) From e6199c117c221c1acfd03a09382b9c9dcc59f8cd Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Tue, 3 Sep 2024 03:17:59 +0200 Subject: [PATCH 21/40] UPDATE: is_cylinder_beam function more robust --- .../diffCheck/diffCheck/df_joint_detector.py | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/gh/diffCheck/diffCheck/df_joint_detector.py b/src/gh/diffCheck/diffCheck/df_joint_detector.py index 4cf54f68..6670a9f6 100644 --- a/src/gh/diffCheck/diffCheck/df_joint_detector.py +++ b/src/gh/diffCheck/diffCheck/df_joint_detector.py @@ -68,19 +68,36 @@ def is_cylinder_beam(self): largest_cylinder = None largest_srf = 0 for cylinder in open_cylinders: - if cylinder.GetArea() > largest_srf: - largest_srf = cylinder.GetArea() - print(largest_srf) + area = cylinder.GetArea() + if area > largest_srf: + largest_srf = area largest_cylinder = cylinder.CapPlanarHoles(sc.doc.ModelAbsoluteTolerance) + print(largest_srf) # Check if the cylinder exists if largest_cylinder is None: + print("No cylinder found") return False, None + + df_cloud = diffCheck.diffcheck_bindings.dfb_geometry.DFPointCloud() + df_cloud.points = [np.array([vertex.Location.X, vertex.Location.Y, vertex.Location.Z]).reshape(3, 1) for vertex in self.brep.Vertices] + Bounding_geometry = diffCheck.df_cvt_bindings.cvt_dfOBB_2_rhbrep(df_cloud.get_tight_bounding_box()) + rh_Bounding_geometry_center = Bounding_geometry.GetBoundingBox(True).Center + + # scale the bounding geometry in the longest edge direction by 1.5 from center on both directions + scale_factor = 0.2 + xform = rg.Transform.Scale( rh_Bounding_geometry_center, 1 + scale_factor) + + largest_cylinder.Transform(xform) # check if all vertices are inside the cylinder for vertex in self.brep.Vertices: if not largest_cylinder.IsPointInside(vertex.Location, sc.doc.ModelAbsoluteTolerance, False): + print("Not all vertices are inside the cylinder !! bummer") return False, None + + largest_cylinder.Transform(xform.TryGetInverse()[1]) + print("All vertices are inside the cylinder !! Yey" ) return True, largest_cylinder @@ -156,11 +173,9 @@ def run(self): adjacency_of_faces[idx] = (face[0], [adj_face for adj_face in face[0].AdjacentFaces() if faces[adj_face][1] and adj_face != idx]) adjacency_of_faces = diffCheck.df_util.merge_shared_indexes(adjacency_of_faces) new_joint_face_ids = [[key] + value[1] for key, value in adjacency_of_faces.items()] - print("new_joint_face_ids: ",new_joint_face_ids) # get the proximity faces of the joint faces joint_face_ids = [[key] + [adj_face for adj_face in value[0].AdjacentFaces() if faces[adj_face][1] and adj_face != key] for key, value in faces.items() if value[1]] - print("joint_face_ids: ",joint_face_ids) face_ids = self._assign_ids(new_joint_face_ids) self._faces = [(face, face_ids[idx]) for idx, face in enumerate(self.brep.Faces)] From 4387e4d5ec61f9bf6c0ac0a0a790c0c6bb7ee7d6 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Tue, 3 Sep 2024 03:21:33 +0200 Subject: [PATCH 22/40] UPDATE: general cleanup from pre-commit --- src/gh/components/DF_joint_segmentator/code.py | 16 ++++++++-------- src/gh/diffCheck/diffCheck/df_joint_detector.py | 11 +++++------ src/gh/diffCheck/diffCheck/df_util.py | 12 ------------ 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/gh/components/DF_joint_segmentator/code.py b/src/gh/components/DF_joint_segmentator/code.py index ed793db7..31ad5389 100644 --- a/src/gh/components/DF_joint_segmentator/code.py +++ b/src/gh/components/DF_joint_segmentator/code.py @@ -62,15 +62,15 @@ def RunScript(self, # find the corresponding clusters and merge them df_joint_segment = diffcheck_bindings.dfb_segmentation.DFSegmentation.associate_clusters(False, - df_joint, - df_cloud_clusters, - i_angle_threshold, + df_joint, + df_cloud_clusters, + i_angle_threshold, i_distance_threshold) - diffcheck_bindings.dfb_segmentation.DFSegmentation.clean_unassociated_clusters(False, - df_cloud_clusters, - [df_joint_segment], - [df_joint], - i_angle_threshold, + diffcheck_bindings.dfb_segmentation.DFSegmentation.clean_unassociated_clusters(False, + df_cloud_clusters, + [df_joint_segment], + [df_joint], + i_angle_threshold, i_distance_threshold) # register the merged clusters to the reference point cloud diff --git a/src/gh/diffCheck/diffCheck/df_joint_detector.py b/src/gh/diffCheck/diffCheck/df_joint_detector.py index 6670a9f6..9aea2103 100644 --- a/src/gh/diffCheck/diffCheck/df_joint_detector.py +++ b/src/gh/diffCheck/diffCheck/df_joint_detector.py @@ -78,16 +78,16 @@ def is_cylinder_beam(self): if largest_cylinder is None: print("No cylinder found") return False, None - + df_cloud = diffCheck.diffcheck_bindings.dfb_geometry.DFPointCloud() df_cloud.points = [np.array([vertex.Location.X, vertex.Location.Y, vertex.Location.Z]).reshape(3, 1) for vertex in self.brep.Vertices] Bounding_geometry = diffCheck.df_cvt_bindings.cvt_dfOBB_2_rhbrep(df_cloud.get_tight_bounding_box()) rh_Bounding_geometry_center = Bounding_geometry.GetBoundingBox(True).Center - + # scale the bounding geometry in the longest edge direction by 1.5 from center on both directions scale_factor = 0.2 xform = rg.Transform.Scale( rh_Bounding_geometry_center, 1 + scale_factor) - + largest_cylinder.Transform(xform) # check if all vertices are inside the cylinder @@ -172,11 +172,10 @@ def run(self): continue adjacency_of_faces[idx] = (face[0], [adj_face for adj_face in face[0].AdjacentFaces() if faces[adj_face][1] and adj_face != idx]) adjacency_of_faces = diffCheck.df_util.merge_shared_indexes(adjacency_of_faces) - new_joint_face_ids = [[key] + value[1] for key, value in adjacency_of_faces.items()] + joint_face_ids = [[key] + value[1] for key, value in adjacency_of_faces.items()] # get the proximity faces of the joint faces - joint_face_ids = [[key] + [adj_face for adj_face in value[0].AdjacentFaces() if faces[adj_face][1] and adj_face != key] for key, value in faces.items() if value[1]] - face_ids = self._assign_ids(new_joint_face_ids) + face_ids = self._assign_ids(joint_face_ids) self._faces = [(face, face_ids[idx]) for idx, face in enumerate(self.brep.Faces)] diff --git a/src/gh/diffCheck/diffCheck/df_util.py b/src/gh/diffCheck/diffCheck/df_util.py index 2468c63b..b928984c 100644 --- a/src/gh/diffCheck/diffCheck/df_util.py +++ b/src/gh/diffCheck/diffCheck/df_util.py @@ -198,17 +198,5 @@ def merge_shared_indexes(original_dict): if key not in keys_with_duplicates: keys_with_duplicates[key] = [] keys_with_duplicates[key].append(other_key) - print("keys with duplicates; ",keys_with_duplicates) - - # # Remove the cross-references - # for first_key, first_duplicates in keys_with_duplicates.items(): - # for second_key, second_duplicates in keys_with_duplicates.items(): - # if first_key < second_key and first_key in second_duplicates and second_key in first_duplicates: - # del merged_dict[second_key] - - # # remoev the self-references - # for key, duplicates in keys_with_duplicates.items(): - # if key in duplicates: - # merged_dict[key] = (merged_dict[key][0], [index for index in merged_dict[key][1] if index != key]) return merged_dict From b436598f75b4ea63f4529cdde674e13ab86d4cc4 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Tue, 3 Sep 2024 03:50:36 +0200 Subject: [PATCH 23/40] FIX: small changes to component code to properly generate and handle 'None' cases --- src/gh/components/DF_CAD_segmentator/code.py | 8 ++++++++ src/gh/components/DF_cloud_mesh_distance/code.py | 9 +++++++-- src/gh/components/DF_joint_segmentator/code.py | 5 ++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index c3155570..3dfd5df8 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -4,6 +4,7 @@ import Rhino.Geometry as rg from ghpythonlib.componentbase import executingcomponent as component +from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML import diffCheck @@ -68,4 +69,11 @@ def RunScript(self, o_clusters = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_clusters] + #small cleanup + for o_cluster in o_clusters: + if not o_cluster.IsValid: + o_cluster = None + ghenv.Component.AddRuntimeMessage(RML.Warning, "Some beams could not be segmented and were replaced by 'None'") # noqa: F821 + + return o_clusters diff --git a/src/gh/components/DF_cloud_mesh_distance/code.py b/src/gh/components/DF_cloud_mesh_distance/code.py index fbd46531..dcceed94 100644 --- a/src/gh/components/DF_cloud_mesh_distance/code.py +++ b/src/gh/components/DF_cloud_mesh_distance/code.py @@ -36,10 +36,15 @@ def RunScript(self, return None, None, None, None, None, None # conversion - df_cloud_source_list = [df_cvt_bindings.cvt_rhcloud_2_dfcloud(i_cl_s) for i_cl_s in i_cloud_source] + siffed_df_cloud_source_list = [] + siffed_rh_mesh_target_list = [] + for i in range(len(i_cloud_source)): + if i_cloud_source[i] is not None: + siffed_df_cloud_source_list.append(df_cvt_bindings.cvt_rhcloud_2_dfcloud(i_cloud_source[i])) + siffed_rh_mesh_target_list.append(rh_mesh_target_list[i]) # calculate distances - o_result = df_error_estimation.df_cloud_2_rh_mesh_comparison(df_cloud_source_list, rh_mesh_target_list, i_signed_flag, i_swap) + o_result = df_error_estimation.df_cloud_2_rh_mesh_comparison(siffed_df_cloud_source_list, siffed_rh_mesh_target_list, i_signed_flag, i_swap) return o_result.distances, o_result.distances_rmse, o_result.distances_max_deviation, o_result.distances_min_deviation, o_result.distances_sd_deviation, o_result diff --git a/src/gh/components/DF_joint_segmentator/code.py b/src/gh/components/DF_joint_segmentator/code.py index 31ad5389..f4b2b112 100644 --- a/src/gh/components/DF_joint_segmentator/code.py +++ b/src/gh/components/DF_joint_segmentator/code.py @@ -88,6 +88,9 @@ def RunScript(self, o_transforms.append(transform) o_reference_point_clouds.append(_joint_cloud) else: - ghenv.Component.AddRuntimeMessage(RML.Warning, "Some joints could not be segmented and were ignored.") # noqa: F821 + o_joint_segments.append(None) + o_transforms.append(None) + o_reference_point_clouds.append(None) + ghenv.Component.AddRuntimeMessage(RML.Warning, "Some joints could not be segmented and were replaced by 'None'") # noqa: F821 return o_joint_segments, o_transforms, o_reference_point_clouds From 298bf10aedcf638dba1546fab78a06235ddd8a6a Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Tue, 3 Sep 2024 04:01:29 +0200 Subject: [PATCH 24/40] FIX: line in documentation accidentally deleted --- src/diffCheck/segmentation/DFSegmentation.hh | 1 + 1 file changed, 1 insertion(+) diff --git a/src/diffCheck/segmentation/DFSegmentation.hh b/src/diffCheck/segmentation/DFSegmentation.hh index ee05da3b..c41755cf 100644 --- a/src/diffCheck/segmentation/DFSegmentation.hh +++ b/src/diffCheck/segmentation/DFSegmentation.hh @@ -28,6 +28,7 @@ namespace diffCheck::segmentation public: ///< segmentation refinement methods /** @brief Associates point cloud segments to mesh faces and merges them. It uses the center of mass of the segments and the mesh faces to find correspondances. For each mesh face it then iteratively associate the points of the segment that are actually on the mesh face. * @param isCylinder a boolean to indicate if the model is a cylinder. If true, the method will use the GetCenterAndAxis method of the mesh to find the center and axis of the mesh. based on that, we only want points that have normals more or less perpendicular to the cylinder axis. + * @param referenceMesh the vector of mesh faces to associate with the segments. It is a representation of a beam and its faces. * @param clusters the vector of clusters from cilantro to associate with the mesh faces of the reference mesh * @param angleThreshold the threshold to consider the a cluster as potential candidate for association. the value passed is the minimum sine of the angles. A value of 0 requires perfect alignment (angle = 0), while a value of 0.1 allows an angle of 5.7 degrees. * @param associationThreshold the threshold to consider the points of a segment and a mesh face as associable. It is the ratio between the surface of the closest mesh triangle and the sum of the areas of the three triangles that form the rest of the pyramid described by the mesh triangle and the point we want to associate or not. The lower the number, the more strict the association will be and some poinnts on the mesh face might be wrongfully excluded. From 7a805e2011fd60f064f5e2c21f7b33882fef9f4b Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Thu, 12 Sep 2024 15:35:59 +0200 Subject: [PATCH 25/40] FIX: joint_detector now excludes 'cylindrical' faces from joints --- src/gh/diffCheck/diffCheck/df_joint_detector.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gh/diffCheck/diffCheck/df_joint_detector.py b/src/gh/diffCheck/diffCheck/df_joint_detector.py index 9aea2103..9dfa524c 100644 --- a/src/gh/diffCheck/diffCheck/df_joint_detector.py +++ b/src/gh/diffCheck/diffCheck/df_joint_detector.py @@ -158,7 +158,7 @@ def run(self): # compute the adjacency list of each face adjacency_of_faces = {} ''' - the structure of the disctionnary is as follows: + the structure of the dictionnary is as follows: { face_id: (face, [adj_face_id_1, adj_face_id_2, ...]) ... @@ -168,9 +168,9 @@ def run(self): adj_face_id_1, adj_face_id_2, ... are int ''' for idx, face in faces.items(): - if not face[1]: + if not face[1] or face[0].IsCylinder(tolerance=sc.doc.ModelAbsoluteTolerance): continue - adjacency_of_faces[idx] = (face[0], [adj_face for adj_face in face[0].AdjacentFaces() if faces[adj_face][1] and adj_face != idx]) + adjacency_of_faces[idx] = (face[0], [adj_face for adj_face in face[0].AdjacentFaces() if faces[adj_face][1] and not faces[adj_face][0].IsCylinder() and adj_face != idx]) adjacency_of_faces = diffCheck.df_util.merge_shared_indexes(adjacency_of_faces) joint_face_ids = [[key] + value[1] for key, value in adjacency_of_faces.items()] From e46a8a170d7d429970d16f1c81fcfc126d3b14fe Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:57:10 +0200 Subject: [PATCH 26/40] WIP-UPDATE: joint detector more robust for corner log case --- src/gh/diffCheck/diffCheck/df_geometries.py | 7 ++-- .../diffCheck/diffCheck/df_joint_detector.py | 34 +++++++++++-------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/gh/diffCheck/diffCheck/df_geometries.py b/src/gh/diffCheck/diffCheck/df_geometries.py index e395a835..34b5af8b 100644 --- a/src/gh/diffCheck/diffCheck/df_geometries.py +++ b/src/gh/diffCheck/diffCheck/df_geometries.py @@ -299,7 +299,7 @@ def __post_init__(self): self._center = None self.__id = uuid.uuid4().int - # self.is_cylinder = None + self.is_cylinder = None def deepcopy(self): return DFBeam(self.name, [face.deepcopy() for face in self.faces]) @@ -312,8 +312,7 @@ def from_brep_face(cls, brep): It also removes duplicates and creates a list of unique faces. """ faces : typing.List[DFFace] = [] - data_faces, is_cylinder = diffCheck.df_joint_detector.JointDetector(brep).run() - cls.is_cylinder = is_cylinder + data_faces, cls.is_cylinder = diffCheck.df_joint_detector.JointDetector(brep).run() for data in data_faces: face = DFFace.from_brep_face(data[0], data[1]) faces.append(face) @@ -353,7 +352,7 @@ def to_mesh(self, max_edge_length): return mesh def __repr__(self): - return f"Beam: {self.name}, Faces: {len(self.faces)}" + return f"Beam: {self.name}, Is cylinder:{self.is_cylinder}, Faces: {len(self.faces)}" @property def uuid(self): diff --git a/src/gh/diffCheck/diffCheck/df_joint_detector.py b/src/gh/diffCheck/diffCheck/df_joint_detector.py index 9dfa524c..c65dff82 100644 --- a/src/gh/diffCheck/diffCheck/df_joint_detector.py +++ b/src/gh/diffCheck/diffCheck/df_joint_detector.py @@ -58,20 +58,20 @@ def is_cylinder_beam(self): """ # extract all cylinders from the brep - open_cylinders = [] + candidate_open_cylinders = [] for face in self.brep.Faces: - if face.IsCylinder(): - open_cylinder = face.ToBrep() - open_cylinders.append(open_cylinder) + if not face.IsPlanar(): + candidate_open_cylinder = face.ToBrep() + candidate_open_cylinders.append(candidate_open_cylinder) # find largest cylinder largest_cylinder = None largest_srf = 0 - for cylinder in open_cylinders: - area = cylinder.GetArea() - if area > largest_srf: + for candidate_cylinder in candidate_open_cylinders: + area = candidate_cylinder.GetArea() + if area > largest_srf and candidate_cylinder.CapPlanarHoles(sc.doc.ModelAbsoluteTolerance): largest_srf = area - largest_cylinder = cylinder.CapPlanarHoles(sc.doc.ModelAbsoluteTolerance) + largest_cylinder = candidate_cylinder.CapPlanarHoles(sc.doc.ModelAbsoluteTolerance) print(largest_srf) # Check if the cylinder exists @@ -82,11 +82,15 @@ def is_cylinder_beam(self): df_cloud = diffCheck.diffcheck_bindings.dfb_geometry.DFPointCloud() df_cloud.points = [np.array([vertex.Location.X, vertex.Location.Y, vertex.Location.Z]).reshape(3, 1) for vertex in self.brep.Vertices] Bounding_geometry = diffCheck.df_cvt_bindings.cvt_dfOBB_2_rhbrep(df_cloud.get_tight_bounding_box()) - rh_Bounding_geometry_center = Bounding_geometry.GetBoundingBox(True).Center + _rh_Bounding_geometry_center = Bounding_geometry.GetBoundingBox(True).Center + mean_X = sum([vertex.Location.X for vertex in self.brep.Vertices]) / self.brep.Vertices.Count + mean_Y = sum([vertex.Location.Y for vertex in self.brep.Vertices]) / self.brep.Vertices.Count + mean_Z = sum([vertex.Location.Z for vertex in self.brep.Vertices]) / self.brep.Vertices.Count + rh_Bounding_geometry_center = rg.Point3d(mean_X, mean_Y, mean_Z) # scale the bounding geometry in the longest edge direction by 1.5 from center on both directions - scale_factor = 0.2 - xform = rg.Transform.Scale( rh_Bounding_geometry_center, 1 + scale_factor) + scale_factor = 1.2 + xform = rg.Transform.Scale( rh_Bounding_geometry_center, scale_factor) largest_cylinder.Transform(xform) @@ -110,12 +114,12 @@ def run(self): :return: a list of faces from joins and faces """ # check if the brep is a cylinder beam - is_cylinder_beam, cylinder = self.is_cylinder_beam() + self.is_cylinder_beam, cylinder = self.is_cylinder_beam() # brep vertices to cloud df_cloud = diffCheck.diffcheck_bindings.dfb_geometry.DFPointCloud() df_cloud.points = [np.array([vertex.Location.X, vertex.Location.Y, vertex.Location.Z]).reshape(3, 1) for vertex in self.brep.Vertices] - if is_cylinder_beam: + if self.is_cylinder_beam: Bounding_geometry = cylinder else: Bounding_geometry = diffCheck.df_cvt_bindings.cvt_dfOBB_2_rhbrep(df_cloud.get_tight_bounding_box()) @@ -128,7 +132,7 @@ def run(self): rh_Bounding_geometry_zaxis = rg.Vector3d(longest_edge.PointAt(1) - longest_edge.PointAt(0)) rh_Bounding_geometry_plane = rg.Plane(rh_Bounding_geometry_center, rh_Bounding_geometry_zaxis) - scale_factor = 0.01 + scale_factor = 0.1 xform = rg.Transform.Scale( rh_Bounding_geometry_plane, 1 - scale_factor, @@ -179,4 +183,4 @@ def run(self): self._faces = [(face, face_ids[idx]) for idx, face in enumerate(self.brep.Faces)] - return self._faces, is_cylinder_beam + return self._faces, self.is_cylinder_beam From 18d66b67a3d8a7510d655a5ba4f020f2cd2a4ffc Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Sat, 21 Sep 2024 15:37:24 +0200 Subject: [PATCH 27/40] ADD: DFMergeAssemblies component created --- src/gh/components/DF_merge_assemblies/code.py | 23 ++++++++ .../components/DF_merge_assemblies/icon.png | Bin 0 -> 11662 bytes .../DF_merge_assemblies/metadata.json | 52 ++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 src/gh/components/DF_merge_assemblies/code.py create mode 100644 src/gh/components/DF_merge_assemblies/icon.png create mode 100644 src/gh/components/DF_merge_assemblies/metadata.json diff --git a/src/gh/components/DF_merge_assemblies/code.py b/src/gh/components/DF_merge_assemblies/code.py new file mode 100644 index 00000000..49b4a1bd --- /dev/null +++ b/src/gh/components/DF_merge_assemblies/code.py @@ -0,0 +1,23 @@ +#! python3 + +import diffCheck +from diffCheck.df_geometries import DFBeam, DFAssembly + +from ghpythonlib.componentbase import executingcomponent as component + +import System + +class DFMergeAssemblies(component): + def RunScript(self, + i_new_name: str, + i_assemblies: System.Collections.Generic.IList[diffCheck.df_geometries.DFAssembly] + ) -> diffCheck.df_geometries.DFAssembly: + + beams = System.Collections.Generic.List[DFBeam]() + for assembly in i_assemblies: + for beam in assembly.beams: + beams.Add(beam) + + o_assembly = DFAssembly(beams, i_new_name) + + return o_assembly diff --git a/src/gh/components/DF_merge_assemblies/icon.png b/src/gh/components/DF_merge_assemblies/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f42959c9f538c42834e399fcecdc5ff42f862282 GIT binary patch literal 11662 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 literal 0 HcmV?d00001 diff --git a/src/gh/components/DF_merge_assemblies/metadata.json b/src/gh/components/DF_merge_assemblies/metadata.json new file mode 100644 index 00000000..85936081 --- /dev/null +++ b/src/gh/components/DF_merge_assemblies/metadata.json @@ -0,0 +1,52 @@ +{ + "name": "DFMergeAssemblies", + "nickname": "DFMergeAssemblies", + "category": "diffCheck", + "subcategory": "Structure", + "description": "This component parse a series of DFAssemblies into a unique DFAssembly object.", + "exposure": 4, + "instanceGuid": "f6f0f6ca-0944-4311-828f-7308321b289f", + "ghpython": { + "hideOutput": true, + "hideInput": true, + "isAdvancedMode": true, + "marshalOutGuids": true, + "iconDisplay": 2, + "inputParameters": [ + { + "name": "i_new_assembly_name", + "nickname": "i_new_assembly_name", + "description": "The new name of the assembly to export.", + "optional": false, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "str" + }, + { + "name": "i_assemblies", + "nickname": "i_assemblies", + "description": "The assemblies to merge.", + "optional": true, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "list", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "ghdoc" + } + ], + "outputParameters": [ + { + "name": "o_assembly", + "nickname": "o_assembly", + "description": "The create DFAssembly object representing the timber elements.", + "optional": false, + "sourceCount": 0, + "graft": false + } + ] + } +} \ No newline at end of file From abe3f618bbd9a4497b834edce5feee8006398e27 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Sat, 21 Sep 2024 15:38:10 +0200 Subject: [PATCH 28/40] UPDATE-WIP: add is_cylinder parameter to BuildAssembly component --- src/gh/components/DF_build_assembly/code.py | 7 ++++++- src/gh/components/DF_build_assembly/metadata.json | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/gh/components/DF_build_assembly/code.py b/src/gh/components/DF_build_assembly/code.py index 7a9af9fc..22e365f6 100644 --- a/src/gh/components/DF_build_assembly/code.py +++ b/src/gh/components/DF_build_assembly/code.py @@ -13,10 +13,15 @@ class DFBuildAssembly(component): def RunScript(self, i_assembly_name, - i_breps : System.Collections.Generic.IList[Rhino.Geometry.Brep]): + i_breps : System.Collections.Generic.IList[Rhino.Geometry.Brep], + i_is_cylinder : bool): beams: typing.List[DFBeam] = [] + if i_is_cylinder is None: + i_is_cylinder = False + for brep in i_breps: beam = DFBeam.from_brep_face(brep) + beam.is_cylinder = i_is_cylinder beams.append(beam) o_assembly = DFAssembly(beams, i_assembly_name) diff --git a/src/gh/components/DF_build_assembly/metadata.json b/src/gh/components/DF_build_assembly/metadata.json index a0355271..c024254c 100644 --- a/src/gh/components/DF_build_assembly/metadata.json +++ b/src/gh/components/DF_build_assembly/metadata.json @@ -36,6 +36,18 @@ "wireDisplay": "default", "sourceCount": 0, "typeHintID": "brep" + }, + { + "name": "i_is_cylinder", + "nickname": "i_is_cylinder", + "description": "Whether the beams are cylinders or not.", + "optional": true, + "allowTreeAccess": false, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "bool" } ], "outputParameters": [ From d40e4ede671484409494571a7b2a3b190c41ab53 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Sat, 21 Sep 2024 22:49:28 +0200 Subject: [PATCH 29/40] WIP-UPDATE: Adaptations to semantic segmentation pipeline to improve log cases --- src/diffCheck/segmentation/DFSegmentation.cc | 17 ++++-- src/gh/diffCheck/diffCheck/df_geometries.py | 6 +- .../diffCheck/diffCheck/df_joint_detector.py | 61 ++++++------------- 3 files changed, 35 insertions(+), 49 deletions(-) diff --git a/src/diffCheck/segmentation/DFSegmentation.cc b/src/diffCheck/segmentation/DFSegmentation.cc index 9dd08df1..76bf315e 100644 --- a/src/diffCheck/segmentation/DFSegmentation.cc +++ b/src/diffCheck/segmentation/DFSegmentation.cc @@ -123,10 +123,19 @@ namespace diffCheck::segmentation double faceDistance = std::numeric_limits::max(); std::shared_ptr correspondingSegment; - - std::vector minmax = face->GetTightBoundingBox(); - Eigen::Vector3d min = minmax[0]; - Eigen::Vector3d max = minmax[1]; + + Eigen::Vector3d min = face->Vertices[0]; + Eigen::Vector3d max = face->Vertices[0]; + for (auto vertex : face->Vertices) + { + if(vertex.x() < min.x()){min.x() = vertex.x();} + if(vertex.y() < min.y()){min.y() = vertex.y();} + if(vertex.z() < min.z()){min.z() = vertex.z();} + if(vertex.x() > max.x()){max.x() = vertex.x();} + if(vertex.y() > max.y()){max.y() = vertex.y();} + if(vertex.z() > max.z()){max.z() = vertex.z();} + + } if (clusters.size() == 0) { diff --git a/src/gh/diffCheck/diffCheck/df_geometries.py b/src/gh/diffCheck/diffCheck/df_geometries.py index 34b5af8b..eb7cb97b 100644 --- a/src/gh/diffCheck/diffCheck/df_geometries.py +++ b/src/gh/diffCheck/diffCheck/df_geometries.py @@ -299,24 +299,24 @@ def __post_init__(self): self._center = None self.__id = uuid.uuid4().int - self.is_cylinder = None def deepcopy(self): return DFBeam(self.name, [face.deepcopy() for face in self.faces]) @classmethod - def from_brep_face(cls, brep): + def from_brep_face(cls, brep, is_cylinder=False): """ Create a DFBeam from a RhinoBrep object. It also removes duplicates and creates a list of unique faces. """ faces : typing.List[DFFace] = [] - data_faces, cls.is_cylinder = diffCheck.df_joint_detector.JointDetector(brep).run() + data_faces = diffCheck.df_joint_detector.JointDetector(brep).run(is_cylinder) for data in data_faces: face = DFFace.from_brep_face(data[0], data[1]) faces.append(face) beam = cls("Beam", faces) + beam.is_cylinder = is_cylinder return beam def to_brep(self): diff --git a/src/gh/diffCheck/diffCheck/df_joint_detector.py b/src/gh/diffCheck/diffCheck/df_joint_detector.py index c65dff82..fabba3ab 100644 --- a/src/gh/diffCheck/diffCheck/df_joint_detector.py +++ b/src/gh/diffCheck/diffCheck/df_joint_detector.py @@ -47,13 +47,10 @@ def _assign_ids(self, joint_face_ids): return extended_ids - def is_cylinder_beam(self): + def find_largest_cylinder(self): """ - Detects if the brep is a cylinder beam. - This is done by finding the largest cylinder in the brep, - and looking if all brep vertices are inside the cylinder. + Finds and returns the largest cylinder in the brep - :return: True if the brep is detected as a cylinder beam, False otherwise :return: the largest cylinder if beam detected as a cylinder, None otherwise """ @@ -77,35 +74,12 @@ def is_cylinder_beam(self): # Check if the cylinder exists if largest_cylinder is None: print("No cylinder found") - return False, None + return None - df_cloud = diffCheck.diffcheck_bindings.dfb_geometry.DFPointCloud() - df_cloud.points = [np.array([vertex.Location.X, vertex.Location.Y, vertex.Location.Z]).reshape(3, 1) for vertex in self.brep.Vertices] - Bounding_geometry = diffCheck.df_cvt_bindings.cvt_dfOBB_2_rhbrep(df_cloud.get_tight_bounding_box()) - _rh_Bounding_geometry_center = Bounding_geometry.GetBoundingBox(True).Center - mean_X = sum([vertex.Location.X for vertex in self.brep.Vertices]) / self.brep.Vertices.Count - mean_Y = sum([vertex.Location.Y for vertex in self.brep.Vertices]) / self.brep.Vertices.Count - mean_Z = sum([vertex.Location.Z for vertex in self.brep.Vertices]) / self.brep.Vertices.Count - rh_Bounding_geometry_center = rg.Point3d(mean_X, mean_Y, mean_Z) - - # scale the bounding geometry in the longest edge direction by 1.5 from center on both directions - scale_factor = 1.2 - xform = rg.Transform.Scale( rh_Bounding_geometry_center, scale_factor) + return largest_cylinder - largest_cylinder.Transform(xform) - # check if all vertices are inside the cylinder - for vertex in self.brep.Vertices: - if not largest_cylinder.IsPointInside(vertex.Location, sc.doc.ModelAbsoluteTolerance, False): - print("Not all vertices are inside the cylinder !! bummer") - return False, None - - largest_cylinder.Transform(xform.TryGetInverse()[1]) - print("All vertices are inside the cylinder !! Yey" ) - return True, largest_cylinder - - - def run(self): + def run(self, is_cylinder_beam): """ Run the joint detector. We use a dictionary to store the faces of the cuts based wethear they are cuts or holes. - for cuts: If it is a cut we return the face, and the id of the joint the faces belongs to. @@ -113,13 +87,12 @@ def run(self): :return: a list of faces from joins and faces """ - # check if the brep is a cylinder beam - self.is_cylinder_beam, cylinder = self.is_cylinder_beam() # brep vertices to cloud df_cloud = diffCheck.diffcheck_bindings.dfb_geometry.DFPointCloud() df_cloud.points = [np.array([vertex.Location.X, vertex.Location.Y, vertex.Location.Z]).reshape(3, 1) for vertex in self.brep.Vertices] - if self.is_cylinder_beam: + if is_cylinder_beam: + cylinder = self.find_largest_cylinder() Bounding_geometry = cylinder else: Bounding_geometry = diffCheck.df_cvt_bindings.cvt_dfOBB_2_rhbrep(df_cloud.get_tight_bounding_box()) @@ -153,11 +126,15 @@ def run(self): is_inside is bool ''' faces = {} - for idx, face in enumerate(self.brep.Faces): - face_centroid = rg.AreaMassProperties.Compute(face).Centroid - coord = face.ClosestPoint(face_centroid) - projected_centroid = face.PointAt(coord[1], coord[2]) - faces[idx] = (face, Bounding_geometry.IsPointInside(projected_centroid, sc.doc.ModelAbsoluteTolerance, True)) + if is_cylinder_beam: + for idx, face in enumerate(self.brep.Faces): + faces[idx] = (face, face.IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance)) + else: + for idx, face in enumerate(self.brep.Faces): + face_centroid = rg.AreaMassProperties.Compute(face).Centroid + coord = face.ClosestPoint(face_centroid) + projected_centroid = face.PointAt(coord[1], coord[2]) + faces[idx] = (face, Bounding_geometry.IsPointInside(projected_centroid, sc.doc.ModelAbsoluteTolerance, True)) # compute the adjacency list of each face adjacency_of_faces = {} @@ -172,9 +149,9 @@ def run(self): adj_face_id_1, adj_face_id_2, ... are int ''' for idx, face in faces.items(): - if not face[1] or face[0].IsCylinder(tolerance=sc.doc.ModelAbsoluteTolerance): + if not face[1]: continue - adjacency_of_faces[idx] = (face[0], [adj_face for adj_face in face[0].AdjacentFaces() if faces[adj_face][1] and not faces[adj_face][0].IsCylinder() and adj_face != idx]) + adjacency_of_faces[idx] = (face[0], [adj_face for adj_face in face[0].AdjacentFaces() if faces[adj_face][1] and faces[adj_face][0].IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance) and adj_face != idx]) # used to be not faces[adj_face][0].IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance) adjacency_of_faces = diffCheck.df_util.merge_shared_indexes(adjacency_of_faces) joint_face_ids = [[key] + value[1] for key, value in adjacency_of_faces.items()] @@ -183,4 +160,4 @@ def run(self): self._faces = [(face, face_ids[idx]) for idx, face in enumerate(self.brep.Faces)] - return self._faces, self.is_cylinder_beam + return self._faces From 73ab8ecb9e0334923cab805b86b4664be3561d77 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Sun, 22 Sep 2024 11:34:57 +0200 Subject: [PATCH 30/40] UPDATE: cleaner CAD_segmentator component code --- src/gh/components/DF_CAD_segmentator/code.py | 26 ++++++-------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index 3dfd5df8..69e59947 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -48,24 +48,14 @@ def RunScript(self, df_clusters.append(df_asssociated_cluster) # clean the unassociated clusters depending on the type of assembly - if i_assembly.contains_cylinders: - dfb_segmentation.DFSegmentation.clean_unassociated_clusters( - is_cylinder=True, - unassociated_clusters=df_clouds, - associated_clusters=df_clusters, - reference_mesh=df_beams_meshes, - angle_threshold=i_angle_threshold, - association_threshold=i_association_threshold - ) - else: - dfb_segmentation.DFSegmentation.clean_unassociated_clusters( - is_cylinder=False, - unassociated_clusters=df_clouds, - associated_clusters=df_clusters, - reference_mesh=df_beams_meshes, - angle_threshold=i_angle_threshold, - association_threshold=i_association_threshold - ) + dfb_segmentation.DFSegmentation.clean_unassociated_clusters( + is_cylinder=i_assembly.contains_cylinders, + unassociated_clusters=df_clouds, + associated_clusters=df_clusters, + reference_mesh=df_beams_meshes, + angle_threshold=i_angle_threshold, + association_threshold=i_association_threshold + ) o_clusters = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_clusters] From ec7952472cb551d822af96c12ddd0635f73e0a02 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Sun, 22 Sep 2024 14:16:36 +0200 Subject: [PATCH 31/40] FIX: name of input parameter of DFMergeAssemblies in metadata.json [no ci] --- src/gh/components/DF_merge_assemblies/metadata.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gh/components/DF_merge_assemblies/metadata.json b/src/gh/components/DF_merge_assemblies/metadata.json index 85936081..8f6f7be7 100644 --- a/src/gh/components/DF_merge_assemblies/metadata.json +++ b/src/gh/components/DF_merge_assemblies/metadata.json @@ -14,8 +14,8 @@ "iconDisplay": 2, "inputParameters": [ { - "name": "i_new_assembly_name", - "nickname": "i_new_assembly_name", + "name": "i_new_name", + "nickname": "i_new_name", "description": "The new name of the assembly to export.", "optional": false, "allowTreeAccess": true, From 959630a1e19a3a43666c5df9a4741102e2f54d89 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Sun, 22 Sep 2024 14:35:29 +0200 Subject: [PATCH 32/40] FIX: CleanUnassociatedClusters better integrated so it daly with composite assemblies (logs + squared) --- src/gh/components/DF_CAD_segmentator/code.py | 21 ++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index f2a01f75..c2c2dca1 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -45,6 +45,7 @@ def RunScript(self, rh_beams_meshes.append(rh_b_mesh_faces) # different association depending on the type of beam + print(df_b.is_cylinder) df_asssociated_cluster_faces = dfb_segmentation.DFSegmentation.associate_clusters( is_cylinder=df_b.is_cylinder, reference_mesh=df_b_mesh_faces, @@ -56,17 +57,17 @@ def RunScript(self, df_asssociated_cluster = dfb_geometry.DFPointCloud() for df_associated_face in df_asssociated_cluster_faces: df_asssociated_cluster.add_points(df_associated_face) - df_clusters.append(df_asssociated_cluster) - # clean the unassociated clusters depending on the type of assembly - dfb_segmentation.DFSegmentation.clean_unassociated_clusters( - is_cylinder=i_assembly.contains_cylinders, - unassociated_clusters=df_clouds, - associated_clusters=df_clusters, - reference_mesh=df_beams_meshes, - angle_threshold=i_angle_threshold, - association_threshold=i_association_threshold - ) + dfb_segmentation.DFSegmentation.clean_unassociated_clusters( + is_cylinder=df_b.is_cylinder, + unassociated_clusters=df_clouds, + associated_clusters=[df_asssociated_cluster], + reference_mesh=[df_b_mesh_faces], + angle_threshold=i_angle_threshold, + association_threshold=i_association_threshold + ) + + df_clusters.append(df_asssociated_cluster) o_clusters = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_clusters] From c4ebde68056d3f22cc70778530c29f4182829681 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Sun, 22 Sep 2024 15:46:17 +0200 Subject: [PATCH 33/40] ADD Documentation for merrge assemblies --- doc/gh_DFMergeAssemblies.rst | 8 ++++++++ doc/gh_components.rst | 7 ++++++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 doc/gh_DFMergeAssemblies.rst diff --git a/doc/gh_DFMergeAssemblies.rst b/doc/gh_DFMergeAssemblies.rst new file mode 100644 index 00000000..afebef77 --- /dev/null +++ b/doc/gh_DFMergeAssemblies.rst @@ -0,0 +1,8 @@ +.. image:: ../src/gh/components/DF_merge_assemblies/icon.png + :align: left + :width: 40px + +``DFMergeAssemblies`` component +=========================== + +.. ghcomponent_to_rst:: ../src/gh/components/DF_merge_assemblies \ No newline at end of file diff --git a/doc/gh_components.rst b/doc/gh_components.rst index 0dad3459..842bc305 100644 --- a/doc/gh_components.rst +++ b/doc/gh_components.rst @@ -81,6 +81,10 @@ DF has a Grasshopper_ plugin with a set of components that allows the user to in - .. image:: ../src/gh/components/DF_remove_statistical_outliers/icon.png - `gh_DFRemoveStatisticalOutliers `_ + * - .. image:: ../src/gh/components/DF_merge_assemblies/icon.png + - `gh_DFJointSegmentator `_ + - + - .. toctree:: @@ -114,4 +118,5 @@ DF has a Grasshopper_ plugin with a set of components that allows the user to in gh_DFRemoveBeam gh_DFColorizeCloud gh_DFBrepToCloud - gh_DFRemoveStatisticalOutliers \ No newline at end of file + gh_DFRemoveStatisticalOutliers + gh_DFMergeAssemblies \ No newline at end of file From 602756d84df141d0db4a5a1369804e7ecf72e402 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Sun, 22 Sep 2024 16:02:36 +0200 Subject: [PATCH 34/40] FIX: underlining of title set to correct length [no ci] --- doc/gh_DFMergeAssemblies.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/gh_DFMergeAssemblies.rst b/doc/gh_DFMergeAssemblies.rst index afebef77..bf650361 100644 --- a/doc/gh_DFMergeAssemblies.rst +++ b/doc/gh_DFMergeAssemblies.rst @@ -3,6 +3,6 @@ :width: 40px ``DFMergeAssemblies`` component -=========================== +=============================== .. ghcomponent_to_rst:: ../src/gh/components/DF_merge_assemblies \ No newline at end of file From c259efa69531ea4f582fe2c638e61c63c775932c Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Mon, 23 Sep 2024 09:28:04 +0200 Subject: [PATCH 35/40] FIX: Typo in rst file + issues with naming convention and commented-out code --- doc/gh_components.rst | 2 +- src/gh/components/DF_CAD_segmentator/code.py | 2 -- src/gh/diffCheck/diffCheck/df_geometries.py | 7 ------- src/gh/diffCheck/diffCheck/df_joint_detector.py | 14 +++++++------- 4 files changed, 8 insertions(+), 17 deletions(-) diff --git a/doc/gh_components.rst b/doc/gh_components.rst index 842bc305..0b6781d1 100644 --- a/doc/gh_components.rst +++ b/doc/gh_components.rst @@ -82,7 +82,7 @@ DF has a Grasshopper_ plugin with a set of components that allows the user to in - `gh_DFRemoveStatisticalOutliers `_ * - .. image:: ../src/gh/components/DF_merge_assemblies/icon.png - - `gh_DFJointSegmentator `_ + - `gh_DFMergeAssemblies `_ - - diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index c2c2dca1..b2f67286 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -45,7 +45,6 @@ def RunScript(self, rh_beams_meshes.append(rh_b_mesh_faces) # different association depending on the type of beam - print(df_b.is_cylinder) df_asssociated_cluster_faces = dfb_segmentation.DFSegmentation.associate_clusters( is_cylinder=df_b.is_cylinder, reference_mesh=df_b_mesh_faces, @@ -71,7 +70,6 @@ def RunScript(self, o_clusters = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_clusters] - #small cleanup for o_cluster in o_clusters: if not o_cluster.IsValid: o_cluster = None diff --git a/src/gh/diffCheck/diffCheck/df_geometries.py b/src/gh/diffCheck/diffCheck/df_geometries.py index 1c3cd50c..cc4d6c98 100644 --- a/src/gh/diffCheck/diffCheck/df_geometries.py +++ b/src/gh/diffCheck/diffCheck/df_geometries.py @@ -135,13 +135,6 @@ def from_brep_face(cls, all_loops = [] df_face: DFFace = cls([], joint_id) - # if brep_face.IsCylinder(): - - # df_face = cls([[[], [], []]], joint_id) - # cls.is_cylinder = True - # df_face._rh_brepface = brep_face - # return df_face - for idx, loop in enumerate(brep_face.Loops): loop_curve = loop.To3dCurve() loop_curve = loop_curve.ToNurbsCurve() diff --git a/src/gh/diffCheck/diffCheck/df_joint_detector.py b/src/gh/diffCheck/diffCheck/df_joint_detector.py index fabba3ab..1e90d235 100644 --- a/src/gh/diffCheck/diffCheck/df_joint_detector.py +++ b/src/gh/diffCheck/diffCheck/df_joint_detector.py @@ -93,13 +93,13 @@ def run(self, is_cylinder_beam): df_cloud.points = [np.array([vertex.Location.X, vertex.Location.Y, vertex.Location.Z]).reshape(3, 1) for vertex in self.brep.Vertices] if is_cylinder_beam: cylinder = self.find_largest_cylinder() - Bounding_geometry = cylinder + bounding_geometry = cylinder else: - Bounding_geometry = diffCheck.df_cvt_bindings.cvt_dfOBB_2_rhbrep(df_cloud.get_tight_bounding_box()) + bounding_geometry = diffCheck.df_cvt_bindings.cvt_dfOBB_2_rhbrep(df_cloud.get_tight_bounding_box()) # scale the bounding geometry in the longest edge direction by 1.5 from center on both directions - rh_Bounding_geometry_center = Bounding_geometry.GetBoundingBox(True).Center - edges = Bounding_geometry.Edges + rh_Bounding_geometry_center = bounding_geometry.GetBoundingBox(True).Center + edges = bounding_geometry.Edges edge_lengths = [edge.GetLength() for edge in edges] longest_edge = edges[edge_lengths.index(max(edge_lengths))] @@ -112,7 +112,7 @@ def run(self, is_cylinder_beam): 1 - scale_factor, 1 + scale_factor ) - Bounding_geometry.Transform(xform) + bounding_geometry.Transform(xform) # check if face's centers are inside the OBB ''' @@ -134,7 +134,7 @@ def run(self, is_cylinder_beam): face_centroid = rg.AreaMassProperties.Compute(face).Centroid coord = face.ClosestPoint(face_centroid) projected_centroid = face.PointAt(coord[1], coord[2]) - faces[idx] = (face, Bounding_geometry.IsPointInside(projected_centroid, sc.doc.ModelAbsoluteTolerance, True)) + faces[idx] = (face, bounding_geometry.IsPointInside(projected_centroid, sc.doc.ModelAbsoluteTolerance, True)) # compute the adjacency list of each face adjacency_of_faces = {} @@ -151,7 +151,7 @@ def run(self, is_cylinder_beam): for idx, face in faces.items(): if not face[1]: continue - adjacency_of_faces[idx] = (face[0], [adj_face for adj_face in face[0].AdjacentFaces() if faces[adj_face][1] and faces[adj_face][0].IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance) and adj_face != idx]) # used to be not faces[adj_face][0].IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance) + adjacency_of_faces[idx] = (face[0], [adj_face for adj_face in face[0].AdjacentFaces() if faces[adj_face][1] and faces[adj_face][0].IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance) and adj_face != idx]) adjacency_of_faces = diffCheck.df_util.merge_shared_indexes(adjacency_of_faces) joint_face_ids = [[key] + value[1] for key, value in adjacency_of_faces.items()] From 992a96a9dbe3fa0b95ede14f5f50786d472a4fc7 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:14:22 +0200 Subject: [PATCH 36/40] FIX: naming of functions and switch from 'is_cylinder' to 'is_roundwood' --- src/diffCheck/geometry/DFMesh.cc | 2 +- src/diffCheck/geometry/DFMesh.hh | 2 +- src/diffCheckBindings.cc | 4 ++-- src/gh/components/DF_build_assembly/code.py | 9 ++++----- .../components/DF_build_assembly/metadata.json | 6 +++--- src/gh/diffCheck/diffCheck/df_geometries.py | 16 +++++++++------- 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/diffCheck/geometry/DFMesh.cc b/src/diffCheck/geometry/DFMesh.cc index 23f59901..0669b56d 100644 --- a/src/diffCheck/geometry/DFMesh.cc +++ b/src/diffCheck/geometry/DFMesh.cc @@ -148,7 +148,7 @@ namespace diffCheck::geometry return false; } - std::tuple DFMesh::GetCenterAndAxis() + std::tuple DFMesh::ComputeOBBCenterAndAxis() { Eigen::Vector3d center = Eigen::Vector3d::Zero(); Eigen::Vector3d axis = Eigen::Vector3d::Zero(); diff --git a/src/diffCheck/geometry/DFMesh.hh b/src/diffCheck/geometry/DFMesh.hh index d46448eb..19712411 100644 --- a/src/diffCheck/geometry/DFMesh.hh +++ b/src/diffCheck/geometry/DFMesh.hh @@ -91,7 +91,7 @@ namespace diffCheck::geometry * * @return std::tuple the first element is the center of the obb of the mesh, the second element is the main axis of the obb of the mesh */ - std::tuple GetCenterAndAxis(); + std::tuple ComputeOBBCenterAndAxis(); public: ///< I/O loader /** diff --git a/src/diffCheckBindings.cc b/src/diffCheckBindings.cc index f086593f..606e31d7 100644 --- a/src/diffCheckBindings.cc +++ b/src/diffCheckBindings.cc @@ -200,14 +200,14 @@ PYBIND11_MODULE(diffcheck_bindings, m) { py::arg("color_clusters") = false) .def_static("associate_clusters", &diffCheck::segmentation::DFSegmentation::AssociateClustersToMeshes, - py::arg("is_cylinder"), + py::arg("is_roundwood"), py::arg("reference_mesh"), py::arg("unassociated_clusters"), py::arg("angle_threshold") = 0.1, py::arg("association_threshold") = 0.1) .def_static("clean_unassociated_clusters", &diffCheck::segmentation::DFSegmentation::CleanUnassociatedClusters, - py::arg("is_cylinder"), + py::arg("is_roundwood"), py::arg("unassociated_clusters"), py::arg("associated_clusters"), py::arg("reference_mesh"), diff --git a/src/gh/components/DF_build_assembly/code.py b/src/gh/components/DF_build_assembly/code.py index 22e365f6..0761d778 100644 --- a/src/gh/components/DF_build_assembly/code.py +++ b/src/gh/components/DF_build_assembly/code.py @@ -14,14 +14,13 @@ class DFBuildAssembly(component): def RunScript(self, i_assembly_name, i_breps : System.Collections.Generic.IList[Rhino.Geometry.Brep], - i_is_cylinder : bool): + i_is_roundwood : bool): beams: typing.List[DFBeam] = [] - if i_is_cylinder is None: - i_is_cylinder = False + if i_is_roundwood is None: + i_is_roundwood = False for brep in i_breps: - beam = DFBeam.from_brep_face(brep) - beam.is_cylinder = i_is_cylinder + beam = DFBeam.from_brep_face(brep, i_is_roundwood) beams.append(beam) o_assembly = DFAssembly(beams, i_assembly_name) diff --git a/src/gh/components/DF_build_assembly/metadata.json b/src/gh/components/DF_build_assembly/metadata.json index c024254c..18c7a446 100644 --- a/src/gh/components/DF_build_assembly/metadata.json +++ b/src/gh/components/DF_build_assembly/metadata.json @@ -38,9 +38,9 @@ "typeHintID": "brep" }, { - "name": "i_is_cylinder", - "nickname": "i_is_cylinder", - "description": "Whether the beams are cylinders or not.", + "name": "i_is_roundwood", + "nickname": "i_is_roundwood", + "description": "Whether the beams are of roundwood type or not.", "optional": true, "allowTreeAccess": false, "showTypeHints": true, diff --git a/src/gh/diffCheck/diffCheck/df_geometries.py b/src/gh/diffCheck/diffCheck/df_geometries.py index cc4d6c98..f3fead03 100644 --- a/src/gh/diffCheck/diffCheck/df_geometries.py +++ b/src/gh/diffCheck/diffCheck/df_geometries.py @@ -93,7 +93,7 @@ def __post_init__(self): # if df_face is created from a rhino brep face, we store the rhino brep face self._rh_brepface = None - self.is_cylinder = False + self.is_roundwood = False def __repr__(self): return f"Face id: {(self.id)}, IsJoint: {self.is_joint} Loops: {len(self.all_loops)}" @@ -159,7 +159,7 @@ def to_brep_face(self): if self._rh_brepface is not None: return self._rh_brepface - if self.is_cylinder: + if self.is_roundwood: ghenv.Component.AddRuntimeMessage( # noqa: F821 RML.Warning, "The DFFace was a cylinder created from scratch \n \ , it cannot convert to brep.") @@ -280,6 +280,8 @@ class DFBeam: def __post_init__(self): self.name = self.name or "Unnamed Beam" self.faces = self.faces or [] + self.is_roundwood = False + self._joint_faces = [] self._side_faces = [] self._vertices = [] @@ -299,18 +301,18 @@ def deepcopy(self): @classmethod - def from_brep_face(cls, brep, is_cylinder=False): + def from_brep_face(cls, brep, is_roundwood=False): """ Create a DFBeam from a RhinoBrep object. It also removes duplicates and creates a list of unique faces. """ faces : typing.List[DFFace] = [] - data_faces = diffCheck.df_joint_detector.JointDetector(brep).run(is_cylinder) + data_faces = diffCheck.df_joint_detector.JointDetector(brep, is_roundwood).run() for data in data_faces: face = DFFace.from_brep_face(data[0], data[1]) faces.append(face) beam = cls("Beam", faces) - beam.is_cylinder = is_cylinder + beam.is_roundwood = is_roundwood return beam def to_brep(self): @@ -346,7 +348,7 @@ def to_mesh(self, max_edge_length): return mesh def __repr__(self): - return f"Beam: {self.name}, Is cylinder:{self.is_cylinder}, Faces: {len(self.faces)}" + return f"Beam: {self.name}, Faces: {len(self.faces)}" @property def uuid(self): @@ -421,7 +423,7 @@ def __post_init__(self): self._all_joints: typing.List[DFJoint] = [] for beam in self.beams: - if beam.is_cylinder: + if beam.is_roundwood: self.contains_cylinders = True break else: From 1f1e22e6fd72bd75b07d2fdda844ba64a0344a53 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:26:52 +0200 Subject: [PATCH 37/40] FIX: naming of functions and switch from 'is_cylinder' to 'is_roundwood' --- .../diffCheck/diffCheck/df_joint_detector.py | 102 ++++++++++-------- 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/src/gh/diffCheck/diffCheck/df_joint_detector.py b/src/gh/diffCheck/diffCheck/df_joint_detector.py index 1e90d235..1a31f65e 100644 --- a/src/gh/diffCheck/diffCheck/df_joint_detector.py +++ b/src/gh/diffCheck/diffCheck/df_joint_detector.py @@ -16,6 +16,7 @@ class JointDetector: This class is responsible for detecting joints in a brep """ brep: Rhino.Geometry.Brep + is_roundwood: bool def __post_init__(self): self._faces = [] @@ -47,7 +48,7 @@ def _assign_ids(self, joint_face_ids): return extended_ids - def find_largest_cylinder(self): + def _find_largest_cylinder(self): """ Finds and returns the largest cylinder in the brep @@ -78,8 +79,59 @@ def find_largest_cylinder(self): return largest_cylinder - - def run(self, is_cylinder_beam): +def _find_joint_faces(self, bounding_geometry): + """ + Finds the brep faces that are joint faces. + + the structure of the disctionnary is as follows: + { + face_id: (face, is_inside) + ... + } + face_id is int + face is Rhino.Geometry.BrepFace + is_inside is bool + """ + faces = {} + if self.is_roundwood: + for idx, face in enumerate(self.brep.Faces): + faces[idx] = (face, face.IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance)) + else: + for idx, face in enumerate(self.brep.Faces): + face_centroid = rg.AreaMassProperties.Compute(face).Centroid + coord = face.ClosestPoint(face_centroid) + projected_centroid = face.PointAt(coord[1], coord[2]) + faces[idx] = (face, bounding_geometry.IsPointInside(projected_centroid, sc.doc.ModelAbsoluteTolerance, True)) + + return faces + +def _compute_adjacency_of_faces(faces): + """ + Computes the adjacency list of each face + + the structure of the dictionnary is as follows: + { + face_id: (face, [adj_face_id_1, adj_face_id_2, ...]) + ... + } + face_id is int + face is Rhino.Geometry.BrepFace + adj_face_id_1, adj_face_id_2, ... are int + """ + adjacency_of_faces = {} + for idx, face in faces.items(): + if not face[1]: + continue + adjacency_of_faces[idx] = (face[0], + [adj_face + for adj_face in face[0].AdjacentFaces() + if faces[adj_face][1] + and faces[adj_face][0].IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance) + and adj_face != idx]) + + return adjacency_of_faces + +def run(self): """ Run the joint detector. We use a dictionary to store the faces of the cuts based wethear they are cuts or holes. - for cuts: If it is a cut we return the face, and the id of the joint the faces belongs to. @@ -91,8 +143,8 @@ def run(self, is_cylinder_beam): # brep vertices to cloud df_cloud = diffCheck.diffcheck_bindings.dfb_geometry.DFPointCloud() df_cloud.points = [np.array([vertex.Location.X, vertex.Location.Y, vertex.Location.Z]).reshape(3, 1) for vertex in self.brep.Vertices] - if is_cylinder_beam: - cylinder = self.find_largest_cylinder() + if self.is_roundwood: + cylinder = self._find_largest_cylinder() bounding_geometry = cylinder else: bounding_geometry = diffCheck.df_cvt_bindings.cvt_dfOBB_2_rhbrep(df_cloud.get_tight_bounding_box()) @@ -114,44 +166,8 @@ def run(self, is_cylinder_beam): ) bounding_geometry.Transform(xform) - # check if face's centers are inside the OBB - ''' - the structure of the disctionnary is as follows: - { - face_id: (face, is_inside) - ... - } - face_id is int - face is Rhino.Geometry.BrepFace - is_inside is bool - ''' - faces = {} - if is_cylinder_beam: - for idx, face in enumerate(self.brep.Faces): - faces[idx] = (face, face.IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance)) - else: - for idx, face in enumerate(self.brep.Faces): - face_centroid = rg.AreaMassProperties.Compute(face).Centroid - coord = face.ClosestPoint(face_centroid) - projected_centroid = face.PointAt(coord[1], coord[2]) - faces[idx] = (face, bounding_geometry.IsPointInside(projected_centroid, sc.doc.ModelAbsoluteTolerance, True)) - - # compute the adjacency list of each face - adjacency_of_faces = {} - ''' - the structure of the dictionnary is as follows: - { - face_id: (face, [adj_face_id_1, adj_face_id_2, ...]) - ... - } - face_id is int - face is Rhino.Geometry.BrepFace - adj_face_id_1, adj_face_id_2, ... are int - ''' - for idx, face in faces.items(): - if not face[1]: - continue - adjacency_of_faces[idx] = (face[0], [adj_face for adj_face in face[0].AdjacentFaces() if faces[adj_face][1] and faces[adj_face][0].IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance) and adj_face != idx]) + faces = _find_joint_faces(self, bounding_geometry) + adjacency_of_faces = _compute_adjacency_of_faces(self, faces) adjacency_of_faces = diffCheck.df_util.merge_shared_indexes(adjacency_of_faces) joint_face_ids = [[key] + value[1] for key, value in adjacency_of_faces.items()] From d12a1e83592763b1cfa969d1a1bad842531cda9e Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:32:01 +0200 Subject: [PATCH 38/40] FIX: df_joint_detector excludes non planar joint faces in squared beam case --- src/gh/diffCheck/diffCheck/df_joint_detector.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/gh/diffCheck/diffCheck/df_joint_detector.py b/src/gh/diffCheck/diffCheck/df_joint_detector.py index 1a31f65e..55f5f385 100644 --- a/src/gh/diffCheck/diffCheck/df_joint_detector.py +++ b/src/gh/diffCheck/diffCheck/df_joint_detector.py @@ -101,7 +101,9 @@ def _find_joint_faces(self, bounding_geometry): face_centroid = rg.AreaMassProperties.Compute(face).Centroid coord = face.ClosestPoint(face_centroid) projected_centroid = face.PointAt(coord[1], coord[2]) - faces[idx] = (face, bounding_geometry.IsPointInside(projected_centroid, sc.doc.ModelAbsoluteTolerance, True)) + faces[idx] = (face, + bounding_geometry.IsPointInside(projected_centroid, sc.doc.ModelAbsoluteTolerance, True) + * face.IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance)) return faces From 5e7d71bd426a90c98e046c68dca6d2a686cd536c Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:34:09 +0200 Subject: [PATCH 39/40] FIX: forgotten chsnge of method name --- src/diffCheck/segmentation/DFSegmentation.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/diffCheck/segmentation/DFSegmentation.cc b/src/diffCheck/segmentation/DFSegmentation.cc index 673cfeb1..3d09b8a6 100644 --- a/src/diffCheck/segmentation/DFSegmentation.cc +++ b/src/diffCheck/segmentation/DFSegmentation.cc @@ -116,7 +116,7 @@ namespace diffCheck::segmentation { for (std::shared_ptr face : referenceMesh) { - std::tuple centerAndAxis = face->GetCenterAndAxis(); + std::tuple centerAndAxis = face->ComputeOBBCenterAndAxis(); Eigen::Vector3d cylinderCenter = std::get<0>(centerAndAxis); Eigen::Vector3d cylinderAxis = std::get<1>(centerAndAxis); @@ -396,7 +396,7 @@ namespace diffCheck::segmentation Eigen::Vector3d min = minmax[0]; Eigen::Vector3d max = minmax[1]; - std::tuple centerAndAxis = mesh[0]->GetCenterAndAxis(); + std::tuple centerAndAxis = mesh[0]->ComputeOBBCenterAndAxis(); Eigen::Vector3d center = std::get<0>(centerAndAxis); Eigen::Vector3d axis = std::get<1>(centerAndAxis); double dotProduct = clusterNormal.dot(axis); From 057ee36ce0e3fac262c5196d2cd7ed744e9cfb4b Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:20:13 +0200 Subject: [PATCH 40/40] FIX: indent mistake in df_joint_detector.py --- .../diffCheck/diffCheck/df_joint_detector.py | 190 +++++++++--------- 1 file changed, 94 insertions(+), 96 deletions(-) diff --git a/src/gh/diffCheck/diffCheck/df_joint_detector.py b/src/gh/diffCheck/diffCheck/df_joint_detector.py index 55f5f385..bef18712 100644 --- a/src/gh/diffCheck/diffCheck/df_joint_detector.py +++ b/src/gh/diffCheck/diffCheck/df_joint_detector.py @@ -70,7 +70,6 @@ def _find_largest_cylinder(self): if area > largest_srf and candidate_cylinder.CapPlanarHoles(sc.doc.ModelAbsoluteTolerance): largest_srf = area largest_cylinder = candidate_cylinder.CapPlanarHoles(sc.doc.ModelAbsoluteTolerance) - print(largest_srf) # Check if the cylinder exists if largest_cylinder is None: @@ -79,103 +78,102 @@ def _find_largest_cylinder(self): return largest_cylinder -def _find_joint_faces(self, bounding_geometry): - """ - Finds the brep faces that are joint faces. - - the structure of the disctionnary is as follows: - { - face_id: (face, is_inside) - ... - } - face_id is int - face is Rhino.Geometry.BrepFace - is_inside is bool - """ - faces = {} - if self.is_roundwood: - for idx, face in enumerate(self.brep.Faces): - faces[idx] = (face, face.IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance)) - else: - for idx, face in enumerate(self.brep.Faces): - face_centroid = rg.AreaMassProperties.Compute(face).Centroid - coord = face.ClosestPoint(face_centroid) - projected_centroid = face.PointAt(coord[1], coord[2]) - faces[idx] = (face, - bounding_geometry.IsPointInside(projected_centroid, sc.doc.ModelAbsoluteTolerance, True) - * face.IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance)) + def _find_joint_faces(self, bounding_geometry): + """ + Finds the brep faces that are joint faces. + + the structure of the disctionnary is as follows: + { + face_id: (face, is_inside) + ... + } + face_id is int + face is Rhino.Geometry.BrepFace + is_inside is bool + """ + faces = {} + if self.is_roundwood: + for idx, face in enumerate(self.brep.Faces): + faces[idx] = (face, face.IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance)) + else: + for idx, face in enumerate(self.brep.Faces): + face_centroid = rg.AreaMassProperties.Compute(face).Centroid + coord = face.ClosestPoint(face_centroid) + projected_centroid = face.PointAt(coord[1], coord[2]) + faces[idx] = (face, + bounding_geometry.IsPointInside(projected_centroid, sc.doc.ModelAbsoluteTolerance, True) + * face.IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance)) return faces -def _compute_adjacency_of_faces(faces): - """ - Computes the adjacency list of each face - - the structure of the dictionnary is as follows: - { - face_id: (face, [adj_face_id_1, adj_face_id_2, ...]) - ... - } - face_id is int - face is Rhino.Geometry.BrepFace - adj_face_id_1, adj_face_id_2, ... are int - """ - adjacency_of_faces = {} - for idx, face in faces.items(): - if not face[1]: - continue - adjacency_of_faces[idx] = (face[0], - [adj_face - for adj_face in face[0].AdjacentFaces() - if faces[adj_face][1] - and faces[adj_face][0].IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance) - and adj_face != idx]) - - return adjacency_of_faces - -def run(self): + def _compute_adjacency_of_faces(self, faces): """ - Run the joint detector. We use a dictionary to store the faces of the cuts based wethear they are cuts or holes. - - for cuts: If it is a cut we return the face, and the id of the joint the faces belongs to. - - for sides: If it is a face from the sides, we return the face and None. - - :return: a list of faces from joins and faces + Computes the adjacency list of each face + + the structure of the dictionnary is as follows: + { + face_id: (face, [adj_face_id_1, adj_face_id_2, ...]) + ... + } + face_id is int + face is Rhino.Geometry.BrepFace + adj_face_id_1, adj_face_id_2, ... are int """ - - # brep vertices to cloud - df_cloud = diffCheck.diffcheck_bindings.dfb_geometry.DFPointCloud() - df_cloud.points = [np.array([vertex.Location.X, vertex.Location.Y, vertex.Location.Z]).reshape(3, 1) for vertex in self.brep.Vertices] - if self.is_roundwood: - cylinder = self._find_largest_cylinder() - bounding_geometry = cylinder - else: - bounding_geometry = diffCheck.df_cvt_bindings.cvt_dfOBB_2_rhbrep(df_cloud.get_tight_bounding_box()) - - # scale the bounding geometry in the longest edge direction by 1.5 from center on both directions - rh_Bounding_geometry_center = bounding_geometry.GetBoundingBox(True).Center - edges = bounding_geometry.Edges - edge_lengths = [edge.GetLength() for edge in edges] - longest_edge = edges[edge_lengths.index(max(edge_lengths))] - - rh_Bounding_geometry_zaxis = rg.Vector3d(longest_edge.PointAt(1) - longest_edge.PointAt(0)) - rh_Bounding_geometry_plane = rg.Plane(rh_Bounding_geometry_center, rh_Bounding_geometry_zaxis) - scale_factor = 0.1 - xform = rg.Transform.Scale( - rh_Bounding_geometry_plane, - 1 - scale_factor, - 1 - scale_factor, - 1 + scale_factor - ) - bounding_geometry.Transform(xform) - - faces = _find_joint_faces(self, bounding_geometry) - adjacency_of_faces = _compute_adjacency_of_faces(self, faces) - adjacency_of_faces = diffCheck.df_util.merge_shared_indexes(adjacency_of_faces) - joint_face_ids = [[key] + value[1] for key, value in adjacency_of_faces.items()] - - # get the proximity faces of the joint faces - face_ids = self._assign_ids(joint_face_ids) - - self._faces = [(face, face_ids[idx]) for idx, face in enumerate(self.brep.Faces)] - - return self._faces + adjacency_of_faces = {} + for idx, face in faces.items(): + if not face[1]: + continue + adjacency_of_faces[idx] = (face[0], + [adj_face + for adj_face in face[0].AdjacentFaces() + if faces[adj_face][1] + and faces[adj_face][0].IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance) + and adj_face != idx]) + + return adjacency_of_faces + + def run(self): + """ + Run the joint detector. We use a dictionary to store the faces of the cuts based wethear they are cuts or holes. + - for cuts: If it is a cut we return the face, and the id of the joint the faces belongs to. + - for sides: If it is a face from the sides, we return the face and None. + + :return: a list of faces from joins and faces + """ + + # brep vertices to cloud + df_cloud = diffCheck.diffcheck_bindings.dfb_geometry.DFPointCloud() + df_cloud.points = [np.array([vertex.Location.X, vertex.Location.Y, vertex.Location.Z]).reshape(3, 1) for vertex in self.brep.Vertices] + if self.is_roundwood: + bounding_geometry = self._find_largest_cylinder() + else: + bounding_geometry = diffCheck.df_cvt_bindings.cvt_dfOBB_2_rhbrep(df_cloud.get_tight_bounding_box()) + + # scale the bounding geometry in the longest edge direction by 1.5 from center on both directions + rh_Bounding_geometry_center = bounding_geometry.GetBoundingBox(True).Center + edges = bounding_geometry.Edges + edge_lengths = [edge.GetLength() for edge in edges] + longest_edge = edges[edge_lengths.index(max(edge_lengths))] + + rh_Bounding_geometry_zaxis = rg.Vector3d(longest_edge.PointAt(1) - longest_edge.PointAt(0)) + rh_Bounding_geometry_plane = rg.Plane(rh_Bounding_geometry_center, rh_Bounding_geometry_zaxis) + scale_factor = 0.1 + xform = rg.Transform.Scale( + rh_Bounding_geometry_plane, + 1 - scale_factor, + 1 - scale_factor, + 1 + scale_factor + ) + bounding_geometry.Transform(xform) + + faces = self._find_joint_faces(bounding_geometry) + adjacency_of_faces = self._compute_adjacency_of_faces(faces) + adjacency_of_faces = diffCheck.df_util.merge_shared_indexes(adjacency_of_faces) + joint_face_ids = [[key] + value[1] for key, value in adjacency_of_faces.items()] + + # get the proximity faces of the joint faces + face_ids = self._assign_ids(joint_face_ids) + + self._faces = [(face, face_ids[idx]) for idx, face in enumerate(self.brep.Faces)] + + return self._faces