Skip to content
Open
Show file tree
Hide file tree
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
88 changes: 77 additions & 11 deletions cadquery/occ_impl/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down
74 changes: 73 additions & 1 deletion tests/test_free_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down