Skip to content

Commit f29ec53

Browse files
authored
Merge pull request #163 from diffCheckOrg/implement_pca_patch/improve_sepwise_progression
improve workflow for stepwise evaluation
2 parents 3828e8e + 2995041 commit f29ec53

File tree

11 files changed

+262
-78
lines changed

11 files changed

+262
-78
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ module = [
2020
"GH_IO.*",
2121
"clr.*",
2222
"diffcheck_bindings",
23-
"diffCheck.diffcheck_bindings"
23+
"diffCheck.diffcheck_bindings",
24+
"ghpythonlib.*"
2425
]
2526
ignore_missing_imports = true
2627

src/diffCheck/geometry/DFPointCloud.cc

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,32 @@ namespace diffCheck::geometry
256256

257257
for(size_t i = 0; i < nComponents; ++i)
258258
{
259-
principalAxes.push_back(sortedClustersBySize[i].second);
259+
if(principalAxes.size() == 0)
260+
{
261+
principalAxes.push_back(sortedClustersBySize[i].second);
262+
}
263+
else
264+
{
265+
bool isAlreadyPresent = false;
266+
for (const auto& axis : principalAxes)
267+
{
268+
double dotProduct = std::abs(axis.dot(sortedClustersBySize[i].second));
269+
if (std::abs(dotProduct) > 0.7) // Threshold to consider as similar direction
270+
{
271+
isAlreadyPresent = true;
272+
break;
273+
}
274+
}
275+
if (!isAlreadyPresent)
276+
{
277+
principalAxes.push_back(sortedClustersBySize[i].second);
278+
}
279+
}
280+
}
281+
if (principalAxes.size() < 2) // Fallback to OBB if k-means fails to provide enough distinct axes
282+
{
283+
open3d::geometry::OrientedBoundingBox obb = this->Cvt2O3DPointCloud()->GetOrientedBoundingBox();
284+
principalAxes = {obb.R_.col(0), obb.R_.col(1), obb.R_.col(2)};
260285
}
261286
return principalAxes;
262287
}

src/gh/components/DF_main_pc_axes/code.py

Lines changed: 0 additions & 65 deletions
This file was deleted.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#! python3
2+
3+
from diffCheck import df_cvt_bindings
4+
from diffCheck import df_poses
5+
6+
import Rhino
7+
from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML
8+
9+
from ghpythonlib.componentbase import executingcomponent as component
10+
import System
11+
12+
13+
class DFPoseEstimation(component):
14+
def RunScript(self,
15+
i_clouds: System.Collections.Generic.List[Rhino.Geometry.PointCloud],
16+
i_assembly,
17+
i_save: bool,
18+
i_reset: bool):
19+
20+
# ensure assembly has enough beams
21+
if len(i_assembly.beams) < len(i_clouds):
22+
ghenv.Component.AddRuntimeMessage(RML.Warning, "Assembly has fewer beams than input clouds") # noqa: F821
23+
return None, None
24+
25+
planes = []
26+
all_poses_in_time = df_poses.DFPosesAssembly()
27+
if i_reset:
28+
all_poses_in_time.reset()
29+
return None, None
30+
31+
all_poses_this_time = []
32+
for i, cloud in enumerate(i_clouds):
33+
try:
34+
df_cloud = df_cvt_bindings.cvt_rhcloud_2_dfcloud(cloud)
35+
if df_cloud is None:
36+
return None, None
37+
if not df_cloud.has_normals():
38+
ghenv.Component.AddRuntimeMessage(RML.Error, f"Point cloud {i} has no normals. Please compute the normals.") # noqa: F821
39+
40+
df_points = df_cloud.get_axis_aligned_bounding_box()
41+
df_point = (df_points[0] + df_points[1]) / 2
42+
rh_point = Rhino.Geometry.Point3d(df_point[0], df_point[1], df_point[2])
43+
44+
axes = df_cloud.get_principal_axes(3)
45+
vectors = []
46+
for axe in axes:
47+
vectors.append(Rhino.Geometry.Vector3d(axe[0], axe[1], axe[2]))
48+
49+
new_xDirection, new_yDirection = df_poses.select_vectors(vectors, i_assembly.beams[i].plane.XAxis, i_assembly.beams[i].plane.YAxis)
50+
51+
pose = df_poses.DFPose(
52+
origin = [rh_point.X, rh_point.Y, rh_point.Z],
53+
xDirection = [new_xDirection.X, new_xDirection.Y, new_xDirection.Z],
54+
yDirection = [new_yDirection.X, new_yDirection.Y, new_yDirection.Z])
55+
all_poses_this_time.append(pose)
56+
plane = Rhino.Geometry.Plane(origin = rh_point, xDirection=new_xDirection, yDirection=new_yDirection)
57+
planes.append(plane)
58+
except Exception as e:
59+
# Any unexpected error on this cloud, skip it and keep going
60+
ghenv.Component.AddRuntimeMessage(RML.Error, f"Cloud {i}: processing failed ({e}); skipping.") # noqa: F821
61+
planes.append(None)
62+
all_poses_this_time.append(None)
63+
continue
64+
65+
if i_save:
66+
all_poses_in_time.add_step(all_poses_this_time)
67+
68+
return [planes, all_poses_in_time.to_gh_tree()]
File renamed without changes.

src/gh/components/DF_main_pc_axes/metadata.json renamed to src/gh/components/DF_pose_estimation/metadata.json

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"subcategory": "PointCloud",
66
"description": "This compoment calculates the pose of a list of point clouds.",
77
"exposure": 4,
8-
"instanceGuid": "22b0c6fc-bc16-4ff5-b789-e99776277f65",
8+
"instanceGuid": "a13c4414-f5df-46e6-beae-7054bb9c3e72",
99
"ghpython": {
1010
"hideOutput": true,
1111
"hideInput": true,
@@ -16,7 +16,7 @@
1616
{
1717
"name": "i_clouds",
1818
"nickname": "i_clouds",
19-
"description": "clouds whose main axes are to be calculated",
19+
"description": "clouds whose pose is to be calculated",
2020
"optional": false,
2121
"allowTreeAccess": true,
2222
"showTypeHints": true,
@@ -25,6 +25,18 @@
2525
"sourceCount": 0,
2626
"typeHintID": "pointcloud"
2727
},
28+
{
29+
"name": "i_assembly",
30+
"nickname": "i_assembly",
31+
"description": "The DFAssembly corresponding to the list of clouds.",
32+
"optional": false,
33+
"allowTreeAccess": true,
34+
"showTypeHints": true,
35+
"scriptParamAccess": "item",
36+
"wireDisplay": "default",
37+
"sourceCount": 0,
38+
"typeHintID": "ghdoc"
39+
},
2840
{
2941
"name": "i_reset",
3042
"nickname": "i_reset",
@@ -36,6 +48,18 @@
3648
"wireDisplay": "default",
3749
"sourceCount": 0,
3850
"typeHintID": "bool"
51+
},
52+
{
53+
"name": "i_save",
54+
"nickname": "i_save",
55+
"description": "save the poses computed at this iteration",
56+
"optional": true,
57+
"allowTreeAccess": false,
58+
"showTypeHints": true,
59+
"scriptParamAccess": "item",
60+
"wireDisplay": "default",
61+
"sourceCount": 0,
62+
"typeHintID": "bool"
3963
}
4064
],
4165
"outputParameters": [
@@ -50,7 +74,7 @@
5074
{
5175
"name": "o_history",
5276
"nickname": "o_history",
53-
"description": "The history of poses of all the elements.",
77+
"description": "The history of poses per elements.",
5478
"optional": false,
5579
"sourceCount": 0,
5680
"graft": false
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from ghpythonlib.componentbase import executingcomponent as component
2+
3+
import diffCheck
4+
import diffCheck.df_geometries
5+
6+
class DFTruncateAssembly(component):
7+
def RunScript(self,
8+
i_assembly,
9+
i_truncate_index: int):
10+
beams = i_assembly.beams[:i_truncate_index]
11+
name = i_assembly.name
12+
13+
o_assembly = diffCheck.df_geometries.DFAssembly(name=name, beams=beams)
14+
ghenv.Component.Message = f"number of beams: {len(o_assembly.beams)}" # noqa: F821
15+
return o_assembly
6.92 KB
Loading
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "DFTruncateAssembly",
3+
"nickname": "TruncateAssembly",
4+
"category": "diffCheck",
5+
"subcategory": "Structure",
6+
"description": "This component truncates an assembly.",
7+
"exposure": 4,
8+
"instanceGuid": "cf8af97f-dd84-40b6-af44-bf6aca7b941b",
9+
"ghpython": {
10+
"hideOutput": true,
11+
"hideInput": true,
12+
"isAdvancedMode": true,
13+
"marshalOutGuids": true,
14+
"iconDisplay": 2,
15+
"inputParameters": [
16+
{
17+
"name": "i_assembly",
18+
"nickname": "i_assembly",
19+
"description": "The assembly to be truncated.",
20+
"optional": false,
21+
"allowTreeAccess": true,
22+
"showTypeHints": true,
23+
"scriptParamAccess": "item",
24+
"wireDisplay": "default",
25+
"sourceCount": 0,
26+
"typeHintID": "ghdoc"
27+
},
28+
{
29+
"name": "i_truncate_index",
30+
"nickname": "i_truncate_index",
31+
"description": "The index at which to truncate the assembly.",
32+
"optional": false,
33+
"allowTreeAccess": false,
34+
"showTypeHints": true,
35+
"scriptParamAccess": "item",
36+
"wireDisplay": "default",
37+
"sourceCount": 0,
38+
"typeHintID": "int"
39+
}
40+
],
41+
"outputParameters": [
42+
{
43+
"name": "o_assembly",
44+
"nickname": "o_assembly",
45+
"description": "The resulting assembly after truncation.",
46+
"optional": false,
47+
"sourceCount": 0,
48+
"graft": false
49+
}
50+
]
51+
}
52+
}

src/gh/diffCheck/diffCheck/df_geometries.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def __post_init__(self):
101101
self._center: DFVertex = None
102102
# the normal of the face
103103
self._normal: typing.List[float] = None
104+
self._area: float = None
104105

105106
def __getstate__(self):
106107
state = self.__dict__.copy()
@@ -261,6 +262,12 @@ def normal(self):
261262
self._normal = [normal_rg.X, normal_rg.Y, normal_rg.Z]
262263
return self._normal
263264

265+
@property
266+
def area(self):
267+
if self._area is None:
268+
self._area = self.to_brep_face().ToBrep().GetArea()
269+
return self._area
270+
264271
@dataclass
265272
class DFJoint:
266273
"""
@@ -375,6 +382,7 @@ def __post_init__(self):
375382

376383
self._center: rg.Point3d = None
377384
self._axis: rg.Line = self.compute_axis()
385+
self.plane: rg.Plane = self.compute_plane()
378386
self._length: float = self._axis.Length
379387

380388
self.__uuid = uuid.uuid4().int
@@ -506,6 +514,28 @@ def compute_axis(self, is_unitized: bool = True) -> rg.Line:
506514

507515
return axis_ln
508516

517+
def compute_plane(self) -> rg.Plane:
518+
"""
519+
This function computes the plane of the beam based on its axis and the first joint's center.
520+
The plane is oriented along the beam's axis.
521+
522+
:return plane: The plane of the beam
523+
"""
524+
if not self.joints:
525+
raise ValueError("The beam has no joints to compute a plane")
526+
527+
#main axis as defined above
528+
main_direction = self.compute_axis().Direction
529+
530+
#secondary axis as normal to the largest face of the beam
531+
largest_face = max(self.faces, key=lambda f: f.area)
532+
secondary_axis = largest_face.normal
533+
secondary_vector = rg.Vector3d(secondary_axis[0], secondary_axis[1], secondary_axis[2])
534+
first_vector = rg.Vector3d.CrossProduct(main_direction, secondary_vector)
535+
origin = self.center
536+
537+
return rg.Plane(origin, first_vector, secondary_vector)
538+
509539
def compute_joint_distances_to_midpoint(self) -> typing.List[float]:
510540
"""
511541
This function computes the distances from the center of the beam to each joint.

0 commit comments

Comments
 (0)