diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index fba948fed..ae1f62cd7 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -5805,6 +5805,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 not mod.IsEmpty(): + 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. @@ -6365,7 +6413,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] @@ -6375,15 +6425,27 @@ 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) - return tcast(Compound | Solid, _compound_or_shape(rvs)) + else: + rvs = [builder.SolidFromShell(sh) for sh in shells] + + if history: + _update_history(history, name, shells_faces, ctx.History()) + + rv = tcast(Compound | Solid, _compound_or_shape(rvs)) + + if history: + _polish_images(history.ops[-1], rv) + + return rv @multidispatch @@ -6401,8 +6463,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) @@ -6414,12 +6474,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 diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 6228b607c..ff0860f04 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,78 @@ 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-6 + + 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) + + # another manipulated orientation case + s3 = solid(face_outer, history=h) + + check_faces_helper((face_outer,), h[-1], s3) + + # manipulated edges case + s4 = solid(*tiny_edge_box_faces, history=h) + + 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(): s1 = box(1, 1, 1)