diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 31b125e37..2495d7c9e 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -825,20 +825,15 @@ def _mass_calc_function(obj: Shape) -> Any: # special handling of compounds - first non-compound child is assumed to define the type of the operation if type_ == ta.TopAbs_COMPOUND: - # if the compound is not empty check its children - if obj: - # first child - child = next(iter(obj)) - - # if compound, go deeper - while child.ShapeType() == "Compound": - child = next(iter(child)) - - type_ = shapetype(child.wrapped) - - # if the compound is empty assume it was meant to be a solid - else: - type_ = ta.TopAbs_SOLID + # descend to the first non-compound child, if any; the sentinel + # guards against an empty compound at the top level or nested inside + child = next(iter(obj), None) + while child is not None and child.ShapeType() == "Compound": + child = next(iter(child), None) + + # an empty or empty-nested compound has no child to inspect; + # assume it was meant to be a solid (consistent with an empty compound) + type_ = ta.TopAbs_SOLID if child is None else shapetype(child.wrapped) # get the function based on dimensionality of the object return shape_properties_LUT[type_] diff --git a/tests/test_shapes.py b/tests/test_shapes.py index f0509bb9d..42d091ecf 100644 --- a/tests/test_shapes.py +++ b/tests/test_shapes.py @@ -459,6 +459,17 @@ def test_special(): assert cs[0].Volume() == approx(3 ** 3) +def test_nested_empty_compound_mass(): + + # a bare empty compound already resolves to zero mass + assert compound().Volume() == approx(0) + + # a compound whose first child is an empty compound must not crash and + # must stay consistent with the bare empty compound (see issue #2078) + assert compound(compound()).Volume() == approx(0) + assert compound(compound()).Area() == approx(0) + + def test_center(): v = vertex(1, 1, 1)