From e42689064c836e81893abab109eab9f8a160a457 Mon Sep 17 00:00:00 2001 From: youdie006 Date: Wed, 19 Aug 2026 16:40:37 +0900 Subject: [PATCH] Fix StopIteration when computing mass of a compound containing an empty compound A bare empty compound reports a mass of 0.0, but a non-empty compound whose first child is an empty compound raised StopIteration instead. Shape._mass_calc_function picks the mass function from the first non-compound descendant of a compound. It guarded the top-level empty case with `if obj:` but then descended with a bare next(iter(child)); Shape.__iter__ yields nothing for an empty compound, so next() raised. This is self-inconsistent: a bare empty compound already resolves to 0.0, so nesting it inside another compound must not flip that into a crash. Use the two-argument next(..., None) sentinel so both the top-level-empty case (unchanged) and the nested-empty case resolve to SOLID -> 0.0. Non-empty compounds still descend to their first non-compound child exactly as before. The same helper backs Volume(), Area() and centerOfMass(), so all three are fixed. Fixes #2078. --- cadquery/occ_impl/shapes.py | 23 +++++++++-------------- tests/test_shapes.py | 11 +++++++++++ 2 files changed, 20 insertions(+), 14 deletions(-) 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)