Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
99d7b8c
Add extend and speed up distance calc
adam-urbanczyk Mar 31, 2025
0e19086
Add conversion to NURBS
adam-urbanczyk Apr 1, 2025
597d63c
Add checks to extend
adam-urbanczyk Apr 1, 2025
80e2ebf
Remove checks
adam-urbanczyk Apr 1, 2025
dc90168
Add test and defaults
adam-urbanczyk Apr 1, 2025
0dfc407
Better coverage
adam-urbanczyk Apr 3, 2025
b606d01
Ignore coverege of a failed imprint
adam-urbanczyk Apr 3, 2025
f278347
Better coverage
adam-urbanczyk Apr 7, 2025
c3906bc
Remove pragma
adam-urbanczyk Apr 7, 2025
e6a8c2d
Fixture cleanup
adam-urbanczyk Apr 7, 2025
0f3b416
Add replace
adam-urbanczyk Apr 7, 2025
dd2396e
Fix replace and add remove to Shape
adam-urbanczyk Apr 7, 2025
56629b7
Add addCavity
adam-urbanczyk Apr 8, 2025
c5f3835
Add more tests
adam-urbanczyk Apr 9, 2025
74e9b48
Coverage fix
adam-urbanczyk Apr 9, 2025
3d25e2e
Actually faster distance
adam-urbanczyk Apr 11, 2025
8f336f3
Add local and non-manifold sewing
adam-urbanczyk Apr 13, 2025
af4fab8
Add history to sewing
adam-urbanczyk Apr 14, 2025
e2d8884
Rework local sewing and add a test
adam-urbanczyk Apr 16, 2025
400b520
Simplify history handling
adam-urbanczyk Apr 17, 2025
a8c5019
Add history to solid and reorganize tests
adam-urbanczyk Apr 17, 2025
d5605a7
Mypy fix
adam-urbanczyk Apr 17, 2025
35bb5cf
add addHole
adam-urbanczyk Apr 17, 2025
547da2c
Add test for addHole
adam-urbanczyk Apr 17, 2025
9066a9d
Merge branch 'master' into shapes/sewing
adam-urbanczyk Apr 17, 2025
03ef822
black fix
adam-urbanczyk Apr 17, 2025
ff17bd7
Add project
adam-urbanczyk Apr 23, 2025
16ebc8c
Test project
adam-urbanczyk Apr 23, 2025
c52d5a1
Add some docs
adam-urbanczyk Apr 23, 2025
980ed71
Add one more example
adam-urbanczyk Apr 24, 2025
ced7442
Doc fix
adam-urbanczyk Apr 24, 2025
7d0a3aa
Fix typo
adam-urbanczyk Apr 29, 2025
5389302
Typo fix
adam-urbanczyk May 4, 2025
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
1 change: 1 addition & 0 deletions cadquery/func.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@
check,
closest,
setThreads,
project,
)
119 changes: 101 additions & 18 deletions cadquery/occ_impl/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@

from OCP.ShapeCustom import ShapeCustom, ShapeCustom_RestrictionParameters

from OCP.BRepAlgo import BRepAlgo
from OCP.BRepAlgo import BRepAlgo, BRepAlgo_NormalProjection

from OCP.ChFi2d import ChFi2d_FilletAPI # For Wire.Fillet()

Expand Down Expand Up @@ -2216,15 +2216,15 @@ def project(
self: T1D, face: "Face", d: VectorLike, closest: bool = True
) -> Union[T1D, List[T1D]]:
"""
Project onto a face along the specified direction
Project onto a face along the specified direction.
"""

bldr = BRepProj_Projection(self.wrapped, face.wrapped, Vector(d).toDir())
shapes = Compound(bldr.Shape())

# select the closest projection if requested
rv: Union[T1D, List[T1D]]

bldr = BRepProj_Projection(self.wrapped, face.wrapped, Vector(d).toDir())
shapes = Compound(bldr.Shape())

if closest:

dist_calc = BRepExtrema_DistShapeShape()
Expand Down Expand Up @@ -3555,6 +3555,20 @@ def extend(

return self.__class__(rv)

def addHole(self, *inner: Wire | Edge) -> Self:
"""
Add one or more holes.
"""

bldr = BRepBuilderAPI_MakeFace(self.wrapped)

for w in inner:
bldr.Add(
TopoDS.Wire_s(w.wrapped if isinstance(w, Wire) else wire(w).wrapped)
)

return self.__class__(bldr.Face()).fix()


class Shell(Shape):
"""
Expand Down Expand Up @@ -5082,6 +5096,8 @@ def _adaptor_curve_to_edge(crv: Adaptor3d_Curve, p1: float, p2: float) -> TopoDS

#%% alternative constructors

ShapeHistory = Dict[Union[Shape, str], Shape]


@multimethod
def wire(*s: Shape) -> Shape:
Expand Down Expand Up @@ -5131,21 +5147,54 @@ def face(s: Sequence[Shape]) -> Shape:
return face(*s)


def _process_sewing_history(
builder: BRepBuilderAPI_Sewing, faces: List[Face], history: Optional[ShapeHistory],
):
"""
Reusable helper for processing sewing history.
"""

# fill history if provided
if history is not None:
# collect shapes present in the history dict
for k, v in history.items():
if isinstance(k, str):
history[k] = Face(builder.Modified(v.wrapped))

# store all top-level shape relations
for f in faces:
history[f] = Face(builder.Modified(f.wrapped))


@multimethod
def shell(*s: Shape, tol: float = 1e-6) -> Shape:
def shell(
*s: Shape,
tol: float = 1e-6,
manifold: bool = True,
ctx: Optional[Sequence[Shape] | Shape] = None,
history: Optional[ShapeHistory] = None,
) -> Shape:
"""
Build shell from faces.
Build shell from faces. If ctx is specified, local sewing is performed.
"""

builder = BRepBuilderAPI_Sewing(tol)
builder = BRepBuilderAPI_Sewing(tol, option4=not manifold)
if ctx:
if isinstance(ctx, Shape):
builder.Load(ctx.wrapped)
else:
builder.Load(compound(ctx).wrapped)

faces: list[Face] = []

for el in s:
for f in _get(el, "Face"):
builder.Add(f.wrapped)
faces.append(f)

builder.Perform()

sewed = builder.SewedShape()
_process_sewing_history(builder, faces, history)

# for one face sewing will not produce a shell
if sewed.ShapeType() == TopAbs_ShapeEnum.TopAbs_FACE:
Expand All @@ -5162,16 +5211,24 @@ def shell(*s: Shape, tol: float = 1e-6) -> Shape:


@shell.register
def shell(s: Sequence[Shape], tol: float = 1e-6) -> Shape:
def shell(
s: Sequence[Shape],
tol: float = 1e-6,
manifold: bool = True,
ctx: Optional[Sequence[Shape] | Shape] = None,
history: Optional[ShapeHistory] = None,
) -> Shape:
"""
Build shell from a sequence of faces.
Build shell from a sequence of faces. If ctx is specified, local sewing is performed.
"""

return shell(*s, tol=tol)
return shell(*s, tol=tol, manifold=manifold, ctx=ctx, history=history)


@multimethod
def solid(s1: Shape, *sn: Shape, tol: float = 1e-6) -> Shape:
def solid(
s1: Shape, *sn: Shape, tol: float = 1e-6, history: Optional[ShapeHistory] = None,
) -> Shape:
"""
Build solid from faces or shells.
"""
Expand All @@ -5186,7 +5243,7 @@ def solid(s1: Shape, *sn: Shape, tol: float = 1e-6) -> Shape:
shells = [el.wrapped for el in shells_faces if el.ShapeType() == "Shell"]
if not shells:
faces = [el for el in shells_faces]
shells = [shell(*faces, tol=tol).wrapped]
shells = [shell(*faces, tol=tol, history=history).wrapped]

rvs = [builder.SolidFromShell(sh) for sh in shells]

Expand All @@ -5195,17 +5252,20 @@ def solid(s1: Shape, *sn: Shape, tol: float = 1e-6) -> Shape:

@solid.register
def solid(
s: Sequence[Shape], inner: Optional[Sequence[Shape]] = None, tol: float = 1e-6
s: Sequence[Shape],
inner: Optional[Sequence[Shape]] = None,
tol: float = 1e-6,
history: Optional[ShapeHistory] = None,
) -> Shape:
"""
Build solid from a sequence of faces.
"""

builder = BRepBuilderAPI_MakeSolid()
builder.Add(shell(*s, tol=tol).wrapped)
builder.Add(shell(*s, tol=tol, history=history).wrapped)

if inner:
for sh in _get(shell(*inner, tol=tol), "Shell"):
for sh in _get(shell(*inner, tol=tol, history=history), "Shell"):
builder.Add(sh.wrapped)

# fix orientations
Expand Down Expand Up @@ -5741,7 +5801,7 @@ def imprint(
*shapes: Shape,
tol: float = 0.0,
glue: GlueLiteral = "full",
history: Optional[Dict[Union[Shape, str], Shape]] = None,
history: Optional[ShapeHistory] = None,
) -> Shape:
"""
Imprint arbitrary number of shapes.
Expand Down Expand Up @@ -6199,6 +6259,29 @@ def loft(
return loft(s, cap, ruled, continuity, parametrization, degree, compat)


def project(
s: Shape,
base: Shape,
continuity: Literal["C1", "C2", "C3"] = "C2",
degree: int = 3,
maxseg: int = 30,
tol: float = 1e-4,
):
"""
Project s onto base using normal projection.
"""

bldr = BRepAlgo_NormalProjection(base.wrapped)
bldr.SetParams(tol, tol ** (2 / 3), _to_geomabshape(continuity), degree, maxseg)

for el in _get_edges(s):
bldr.Add(s.wrapped)

bldr.Build()

return _compound_or_shape(bldr.Projection())


#%% diagnotics


Expand Down
78 changes: 76 additions & 2 deletions doc/free-func.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.. _freefuncapi:


*****************
Free function API
Expand Down Expand Up @@ -241,7 +241,8 @@ Placement and creation of arrays is possible using :meth:`~cadquery.Shape.move`
Text
----

The free function API has extensive text creation capabilities including text on planar curves and text on surfaces.
The free function API has extensive text creation capabilities including text on
planar curves and text on surfaces.


.. cadquery::
Expand Down Expand Up @@ -275,3 +276,76 @@ The free function API has extensive text creation capabilities including text on
r4 = offset(r3, TH).moved(z=S)

result = compound(r1, r2, r3, r4)


Adding features manually
------------------------

In certain cases it is desirable to add features such as holes or protrusions manually.
E.g., for complicated shapes it might be beneficial performance-wise because it
avoids boolean operations. One can add or remove faces, add holes to existing faces
and last but not least reconstruct existing solids.

.. cadquery::

from cadquery.func import *

w = 1
r = 0.9*w/2

# box
b = box(w, w, w)
# bottom face
b_bot = b.faces('<Z')
# top faces
b_top = b.faces('>Z')

# inner face
inner = extrude(circle(r), (0,0,w))

# add holes to the bottom and top face
b_bot_hole = b_bot.addHole(inner.edges('<Z'))
b_top_hole = b_top.addHole(inner.edges('>Z'))

# construct the final solid
result = solid(
b.remove(b_top, b_bot).faces(), #side faces
b_bot_hole, # bottom with a hole
inner, # inner cylinder face
b_top_hole, # top with a hole
)

If the base shape is more complicated, it is possible to use local sewing that
takes into account on indicated elements of the context shape. This, however,
necessitates a two step approach - first a shell needs to be explicitly sewn
and only then the final solid can be constructed.

.. cadquery::

from cadquery.func import *

w = 1
h = 0.1
r = 0.9*w/2

# box
b = box(w, w, w)
# top face
b_top = b.faces('>Z')

# protrusion
feat_side = extrude(circle(r).moved(b_top.Center()), (0,0,h))
feat_top = face(feat_side.edges('>Z'))
feat = shell(feat_side, feat_top) # sew into a shell

# add hole to the box
b_top_hole = b_top.addHole(feat.edges('<Z'))
b = b.replace(b_top, b_top_hole)

# local sewing - only two faces are taken into account
sh = shell(b_top_hole, feat.faces('<Z'), ctx=(b, feat))

# construct the final solid
result = solid(sh)


Loading