From bc42a4eab358a088e4a20faa3cdecf69ed1b54ba Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:59:05 +0200 Subject: [PATCH 1/4] Rework solid history handling --- cadquery/occ_impl/shapes.py | 82 ++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 10 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 31b125e37..f678ac702 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -5807,6 +5807,54 @@ def _combine_ops(op: Op, *ops: Op) -> Op: return op +def _apply_reshape(op: Op, ctx: ShapeBuild_ReShape) -> Op: + """ + Apply (if applicable) additional ReShape history to an exisitn Op. Used by solid. + """ + + hist = ctx.History() + + if hist.HasModified(): + + # update modified + for key, val in op._modified.items(): + + processed = [] + + for subshape in val: + modified = hist.Modified(subshape.wrapped) + + if modified: + processed.extend([Shape.cast(el) for el in modified]) + else: + processed.append(subshape) + + op._modified[key] = compound(processed) + + # update images + for key, val in op._images.items(): + + mod = hist.Modified(val.wrapped) + if mod: + op._images[key] = Shape.cast(mod.First()) + + return op + + +def _polish_images(op: Op, s: Shape) -> Op: + """ + Workaround for Reshape context not tracking (face, face) relations when fixing solids. + """ + + face_dict = {f: f for f in s.faces()} + + for k, v in op._images.items(): + if isinstance(k, Face): + op._images[k] = compound([face_dict[f] for f in v.Faces()]) + + return op + + class History: """ Operation history. @@ -6367,7 +6415,9 @@ def solid( Build solid from faces or shells. """ + ctx = ShapeBuild_ReShape() builder = ShapeFix_Solid() + builder.SetContext(ctx) # get both Shells and Faces s = [s1, *sn] @@ -6377,13 +6427,21 @@ def solid( shells = [el.wrapped for el in shells_faces if isinstance(el, Shell)] if not shells: faces = [el for el in shells_faces if isinstance(el, Face)] - shells = [ - tcast( - TopoDS_Shell, shell(*faces, tol=tol, history=history, name=name).wrapped + rvs = [ + builder.SolidFromShell( + TopoDS.Shell(shell(*faces, tol=tol, history=history, name=name).wrapped) ) ] - rvs = [builder.SolidFromShell(sh) for sh in shells] + if history: + _apply_reshape(history.ops[-1], ctx) + + else: + rvs = [builder.SolidFromShell(sh) for sh in shells] + + if history: + _update_history(history, name, shells_faces, ctx.History()) + # FIXME what about images? return tcast(Compound | Solid, _compound_or_shape(rvs)) @@ -6403,8 +6461,6 @@ def solid( builder = BRepBuilderAPI_MakeSolid() builder.Add(_get_one(shell(*s, tol=tol, history=history, name=name), Shell).wrapped) - n_inner = 0 - if inner: for sh in _get(shell(*inner, tol=tol, history=history), Shell): builder.Add(sh.wrapped) @@ -6416,12 +6472,18 @@ def solid( sf.SetContext(ctx) sf.Perform() + rv = _shape(sf.Solid(), Solid) + # combine histories of all shell operations if needed - if history and inner: - inner_op = history.pop() - _combine_ops(history.ops[-1], inner_op) + if history: + if inner: + inner_op = history.pop() + _combine_ops(history.ops[-1], inner_op) - return _shape(sf.Solid(), Solid) + _apply_reshape(history.ops[-1], ctx) + _polish_images(history.ops[-1], rv) + + return rv @multimethod From fe7a9e5bb4cf138b5bb7853e7a9821a669ec0155 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:43:17 +0200 Subject: [PATCH 2/4] Add and fix tests --- cadquery/occ_impl/shapes.py | 2 +- tests/test_free_functions.py | 64 +++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 24d6cb482..405fc4e2d 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -5833,7 +5833,7 @@ def _apply_reshape(op: Op, ctx: ShapeBuild_ReShape) -> Op: for key, val in op._images.items(): mod = hist.Modified(val.wrapped) - if mod: + if not mod.IsEmpty(): op._images[key] = Shape.cast(mod.First()) return op diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 6228b607c..8e59fde13 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -1447,7 +1447,7 @@ def test_history_offset(): assert sides.edges().size() == 4 -def test_comibine_hist_dict(): +def test_combine_hist_dict(): f = plane(1, 1) v = vertex(0, 0, 0) @@ -1562,6 +1562,68 @@ def _check(op, edges): _check(op, edges) +@fixture +def nested_spheres() -> tuple[Solid, Solid]: + + return sphere(0.5), sphere(0.25) + + +@fixture +def tiny_edge_box_faces() -> list[Face]: + """ + Faces of a box with one face manipulated to contain a tiny edge. + """ + + faces = box(1, 1, 1).Faces() + + edges = faces[0].outerWire().Edges() + edge = edges[0] + + start, end = edge.startPoint(), edge.endPoint() + sliver_end = start + (end - start) * 1e-5 + + faces[0] = face( + wire(segment(start, sliver_end), segment(sliver_end, end), *edges[1:]) + ) + + return faces + + +def test_solid_history(nested_spheres, tiny_edge_box_faces): + + # helper to check if images are subshapes and have correct orientation + def check_faces_helper(faces, op, s): + + for f in faces: + assert isSubshape(op.images(f), s) + assert any(f_res.isEqual(op.images(f)) for f_res in s.faces()) + + h = History() + + sphere_outer, sphere_inner = nested_spheres + + # regular case + face_outer = sphere_outer.face() + face_inner = sphere_inner.face() + + s1 = solid([face_outer], inner=[face_inner], history=h) + + check_faces_helper((face_outer, face_inner), h[-1], s1) + + # manipulated orientation case + face_outer = sphere_outer.face().reverse() + face_inner = sphere_inner.face() + + s2 = solid([face_outer], inner=[face_inner], history=h) + + check_faces_helper((face_outer, face_inner), h[-1], s2) + + # manipulated edges case + s3 = solid(*tiny_edge_box_faces, history=h) + + check_faces_helper(tiny_edge_box_faces, h[-1], s3) + + def test_hlr(): s1 = box(1, 1, 1) From 762cb02f6c201360c29b38a897c926f1d1a6477f Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:51:04 +0200 Subject: [PATCH 3/4] Add a test case --- cadquery/occ_impl/shapes.py | 10 +++++++--- tests/test_free_functions.py | 11 ++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 405fc4e2d..ae1f62cd7 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -5844,7 +5844,7 @@ def _polish_images(op: Op, s: Shape) -> Op: Workaround for Reshape context not tracking (face, face) relations when fixing solids. """ - face_dict = {f: f for f in s.faces()} + face_dict = {f: f for f in s.Faces()} for k, v in op._images.items(): if isinstance(k, Face): @@ -6439,9 +6439,13 @@ def solid( if history: _update_history(history, name, shells_faces, ctx.History()) - # FIXME what about images? - return tcast(Compound | Solid, _compound_or_shape(rvs)) + rv = tcast(Compound | Solid, _compound_or_shape(rvs)) + + if history: + _polish_images(history.ops[-1], rv) + + return rv @multidispatch diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 8e59fde13..04df9c061 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -1596,7 +1596,7 @@ def check_faces_helper(faces, op, s): for f in faces: assert isSubshape(op.images(f), s) - assert any(f_res.isEqual(op.images(f)) for f_res in s.faces()) + assert any(f_res.isEqual(op.images(f)) for f_res in s.Faces()) h = History() @@ -1618,10 +1618,15 @@ def check_faces_helper(faces, op, s): check_faces_helper((face_outer, face_inner), h[-1], s2) + # another manipulated orientation case + s3 = solid(face_outer, history=h) + + check_faces_helper((face_outer,), h[-1], s3) + # manipulated edges case - s3 = solid(*tiny_edge_box_faces, history=h) + s4 = solid(*tiny_edge_box_faces, history=h) - check_faces_helper(tiny_edge_box_faces, h[-1], s3) + check_faces_helper(tiny_edge_box_faces, h[-1], s4) def test_hlr(): From 6fe1677d21f8943b9c265831cd81b45207b58f43 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:26:24 +0200 Subject: [PATCH 4/4] Another case --- tests/test_free_functions.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 04df9c061..ff0860f04 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -1580,7 +1580,7 @@ def tiny_edge_box_faces() -> list[Face]: edge = edges[0] start, end = edge.startPoint(), edge.endPoint() - sliver_end = start + (end - start) * 1e-5 + sliver_end = start + (end - start) * 1e-6 faces[0] = face( wire(segment(start, sliver_end), segment(sliver_end, end), *edges[1:]) @@ -1628,6 +1628,11 @@ def check_faces_helper(faces, op, s): check_faces_helper(tiny_edge_box_faces, h[-1], s4) + # manipulated edges case + s5 = solid(tiny_edge_box_faces, history=h) + + check_faces_helper(tiny_edge_box_faces, h[-1], s5) + def test_hlr():