Skip to content

Fix Calculation of Compound Center of Masses #1822

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions cadquery/occ_impl/shapes.py
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
TypeVar,
cast as tcast,
Literal,
Protocol,
Protocol, get_args,
)

from typing_extensions import Self
@@ -773,14 +773,19 @@ def computeMass(obj: "Shape") -> float:
raise NotImplementedError

@staticmethod
def centerOfMass(obj: "Shape") -> Vector:
def centerOfMass(obj: "Shape", shape_type: ta = None) -> Vector:
"""
Calculates the center of 'mass' of an object.

:param obj: Compute the center of mass of this object
:param shape_type: An optional specification of the topological type of the shape. If not provided,
the shape type is inferred automatically. This is used to determine the correct
property calculation function from the lookup table.
"""
Properties = GProp_GProps()
calc_function = shape_properties_LUT[shapetype(obj.wrapped)]
if shape_type is None:
shape_type = shapetype(obj.wrapped)
calc_function = shape_properties_LUT[shape_type]

if calc_function:
calc_function(obj.wrapped, Properties)
@@ -816,6 +821,7 @@ def Closed(self) -> bool:
def ShapeType(self) -> Shapes:
return tcast(Shapes, shape_LUT[shapetype(self.wrapped)])


def _entities(self, topo_type: Shapes) -> Iterable[TopoDS_Shape]:

shape_set = TopTools_IndexedMapOfShape()
@@ -4680,6 +4686,38 @@ def _siblings(shapes, level):

return Compound.makeCompound(_siblings(self, level))

def getHighestOrderShapeType(self) -> Shapes:
"""
Determines the highest-order topological shape within a compound shape, or returns the shape type itself.

If the shape is not a compound (`TopoDS_Compound`), it directly returns its shape type.
If the shape is a compound, it iterates through all shape types (excluding CompSolid and Compound) in descending topological order
(e.g., Solid > Shell > Face > Wire > Edge) and returns the type of the highest-level shape present within the compound.

:returns: Shapes
"""
return tcast(Shapes, shape_LUT[self._getHighestOrderShapeType()])

def _getHighestOrderShapeType(self) -> ta:
"""
:returns: TopAbs
"""
shape_OCC = self.wrapped

if shape_OCC.ShapeType() != ta.TopAbs_COMPOUND:
return shape_OCC.ShapeType()

reversed_shape_types = list(reversed(get_args(Shapes)))

for shape_type in reversed_shape_types[2:]: # Iterate over topo shapes excluding Compounds and CompSolids
if not self._entities(shape_type).IsEmpty():
return inverse_shape_LUT[shape_type]

raise Exception("Unable to find any shape types present in the given shape.")

def Center(self) -> Vector:
return Shape.centerOfMass(self, shape_type=self._getHighestOrderShapeType())


def sortWiresByBuildOrder(wireList: List[Wire]) -> List[List[Wire]]:
"""Tries to determine how wires should be combined into faces.
10 changes: 10 additions & 0 deletions tests/test_cadquery.py
Original file line number Diff line number Diff line change
@@ -5904,3 +5904,13 @@ def test_loft_to_vertex(self):
# in both cases we get a solid
assert w1.solids().size() == 1
assert w2.solids().size() == 1

def test_compound_faces_center(self):
sk = Sketch().rect(50, 50).faces()
face1 = sk.val()
face2 = face1.copy().translate(Vector(100, 0, 0))
compound = Compound.makeCompound([face1, face2])
expected_center = Shape.CombinedCenter([face1, face2])

assert compound.Center() == expected_center, "Incorrect center of mass of the compound, expected {}, got {}".format(
expected_center, compound.Center())