Skip to content
Merged
119 changes: 104 additions & 15 deletions cadquery/occ_impl/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5496,6 +5496,19 @@ def _compound_or_shape(s: TopoDS_Shape | Sequence[TopoDS_Shape]) -> Shape:
return rv


def _compound(s: TopoDS_Shape | Sequence[TopoDS_Shape]) -> Compound:
"""
Convert a list of TopoDS_Shape to a Compound.
"""

if isinstance(s, TopoDS_Shape):
rv = Compound.makeCompound([_normalize(Shape.cast(s))])
else:
rv = Compound.makeCompound([_normalize(Shape.cast(el)) for el in s])

return rv


def _shape(s: TopoDS_Shape, _: type[T]) -> T:
"""
Cast a TopoDS_Shape to a Shape of the specfied type.
Expand Down Expand Up @@ -5805,6 +5818,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.
Comment thread
adam-urbanczyk marked this conversation as resolved.
Outdated
Comment thread
adam-urbanczyk marked this conversation as resolved.
Outdated
"""

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:
Comment thread
lorenzncode marked this conversation as resolved.
Outdated
Comment thread
adam-urbanczyk marked this conversation as resolved.
Outdated
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 @@ -5940,7 +6001,7 @@ def _update_history(

if has_modifidied:
try:
mod = _compound_or_shape(list(builder.Modified(wrapped)))
mod = _compound(list(builder.Modified(wrapped)))
if mod:
if el in op._modified:
op._modified[el] |= mod
Expand Down Expand Up @@ -5983,12 +6044,12 @@ def _remap_history_values(history: History | None, aux: History,) -> None:

# handle last shape
last_op._last_shape = compound(
[last_aux._modified.get(el, el) for el in last_op._last_shape]
[_normalize(last_aux._modified.get(el, el)) for el in last_op._last_shape]
)

# handle first shape
last_op._first_shape = compound(
[last_aux._modified.get(el, el) for el in last_op._first_shape]
[_normalize(last_aux._modified.get(el, el)) for el in last_op._first_shape]
)


Expand Down Expand Up @@ -6365,25 +6426,49 @@ 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]
shells_faces = [f for el in s for f in _get(el, (Shell, Face))]
# try to collect shells
shells = [el.wrapped for el in shells_faces if isinstance(el, Shell)]

# if no shells are present, use faces to construct them
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:
# NB: reusing the history from shell()
_apply_reshape(history.ops[-1], ctx)

rv = tcast(Compound | Solid, _compound_or_shape(rvs))

return tcast(Compound | Solid, _compound_or_shape(rvs))
# otherwise construct solids with provided shells
else:
rvs = [builder.SolidFromShell(sh) for sh in shells]
rv = tcast(Compound | Solid, _compound_or_shape(rvs))

if history:
# update history - this is likely a noop for this branch
_update_history(history, name, shells_faces, ctx.History())
# update images by hand
op = history.ops[-1]
for el in op._tracked:
if isSubshape(el, rv):
op._images[el] = el

if history:
_polish_images(history.ops[-1], rv)

return rv


@multidispatch
Expand All @@ -6401,8 +6486,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 +6497,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)

_apply_reshape(history.ops[-1], ctx)
_polish_images(history.ops[-1], rv)

return _shape(sf.Solid(), Solid)
return rv


@multimethod
Expand Down
88 changes: 87 additions & 1 deletion tests/test_free_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
_combine_hist_dict,
enclose,
split,
_compound,
)

from OCP.BOPAlgo import BOPAlgo_CheckStatus
Expand Down Expand Up @@ -187,6 +188,15 @@ def test__shape_to_faces_shells():
_shape_to_faces_shells(vertex(0, 0, 0).wrapped)


def test__compound(box_shape):

res1 = _compound(box_shape.wrapped)
assert isinstance(res1, Compound)

res2 = _compound([box_shape.wrapped])
assert isinstance(res2, Compound)


# %% constructors


Expand Down Expand Up @@ -1447,7 +1457,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 +1572,82 @@ 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)

# solid from shells
s6 = solid(sphere_outer.shell(), history=h)
check_faces_helper(sphere_outer.Faces(), h[-1], s6)


def test_hlr():

s1 = box(1, 1, 1)
Expand Down