Skip to content

Commit 471ab6f

Browse files
Add imprinting to free func (#1781)
* Control point visualization tools * Add tests * Nicer exception' * Some docs * Coverage tweaks * Fix tests * More coverage * Update docs * Docstring fixes Co-authored-by: Jeremy Wright <[email protected]> * Typo fix * Adding per item styling to show * Refactor styling * Mypy fixes * Add some tests and fix prop handling * Fix alpha handling for non-shapes * Test camera position settings * Test camera position * Coverage tweaks * Add some docs * Add screenshots * Filename typo fix * Fix wrong kwarg name * Apply suggestions from code review Co-authored-by: Jeremy Wright <[email protected]> * Add imprinting * Add history * Add params * Small restructuring * Improve speed * Add tangents * Cut/fuse improvements * Fix imprint * Restore previous cut/fuse impl * Move to BOP * Fix failure * Test imprint * Test setThreads * More tests * Test multisolid fuse * Use more robust version always * Cleanup imports * Review fixes --------- Co-authored-by: adam-urbanczyk <[email protected]> Co-authored-by: Jeremy Wright <[email protected]>
1 parent 2698db5 commit 471ab6f

File tree

4 files changed

+299
-23
lines changed

4 files changed

+299
-23
lines changed

cadquery/func.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
fuse,
3333
cut,
3434
intersect,
35+
imprint,
3536
split,
3637
fill,
3738
clean,
@@ -45,4 +46,5 @@
4546
loft,
4647
check,
4748
closest,
49+
setThreads,
4850
)

cadquery/occ_impl/shapes.py

Lines changed: 224 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@
156156
Geom_Surface,
157157
Geom_Plane,
158158
Geom_BSplineCurve,
159+
Geom_Curve,
159160
)
160161
from OCP.Geom2d import Geom2d_Line
161162

@@ -238,7 +239,14 @@
238239
from OCP.BRepOffsetAPI import BRepOffsetAPI_MakeFilling
239240
from OCP.BRepOffset import BRepOffset_MakeOffset, BRepOffset_Mode
240241

241-
from OCP.BOPAlgo import BOPAlgo_GlueEnum
242+
from OCP.BOPAlgo import (
243+
BOPAlgo_GlueEnum,
244+
BOPAlgo_Builder,
245+
BOPAlgo_BOP,
246+
BOPAlgo_FUSE,
247+
BOPAlgo_CUT,
248+
BOPAlgo_COMMON,
249+
)
242250

243251
from OCP.IFSelect import IFSelect_ReturnStatus
244252

@@ -296,13 +304,16 @@
296304

297305
from OCP.GeomAdaptor import GeomAdaptor_Surface
298306

307+
from OCP.OSD import OSD_ThreadPool
308+
299309
from math import pi, sqrt, inf, radians, cos
300310

301311
import warnings
302312

303313
from ..utils import deprecate
304314

305315
Real = Union[float, int]
316+
GlueLiteral = Literal["partial", "full", None]
306317

307318
TOLERANCE = 1e-6
308319

@@ -1735,6 +1746,9 @@ class Mixin1DProtocol(ShapeProtocol, Protocol):
17351746
def _approxCurve(self) -> Geom_BSplineCurve:
17361747
...
17371748

1749+
def _curve(self) -> Geom_Curve:
1750+
...
1751+
17381752
def _geomAdaptor(self) -> Union[BRepAdaptor_Curve, BRepAdaptor_CompCurve]:
17391753
...
17401754

@@ -1763,6 +1777,9 @@ def curvatureAt(
17631777
) -> float:
17641778
...
17651779

1780+
def paramsLength(self, locations: Iterable[float]) -> List[float]:
1781+
...
1782+
17661783

17671784
T1D = TypeVar("T1D", bound=Mixin1DProtocol)
17681785

@@ -1811,6 +1828,20 @@ def _approxCurve(self: Mixin1DProtocol) -> Geom_BSplineCurve:
18111828

18121829
return rv
18131830

1831+
def _curve(self: Mixin1DProtocol) -> Geom_Curve:
1832+
"""
1833+
Return the underlying curve.
1834+
"""
1835+
1836+
curve = self._geomAdaptor()
1837+
1838+
if isinstance(curve, BRepAdaptor_Curve):
1839+
rv = curve.Curve().Curve() # get the underlying curve object
1840+
else:
1841+
rv = self._approxCurve() # approximate the adaptor as a real curve
1842+
1843+
return rv
1844+
18141845
def paramAt(self: Mixin1DProtocol, d: Union[Real, Vector]) -> float:
18151846
"""
18161847
Compute parameter value at the specified normalized distance or a point.
@@ -1822,11 +1853,7 @@ def paramAt(self: Mixin1DProtocol, d: Union[Real, Vector]) -> float:
18221853
curve = self._geomAdaptor()
18231854

18241855
if isinstance(d, Vector):
1825-
# handle comp curves (i.e. wire adaptors)
1826-
if isinstance(curve, BRepAdaptor_Curve):
1827-
curve_ = curve.Curve().Curve() # get the underlying curve object
1828-
else:
1829-
curve_ = self._approxCurve() # approximate the adaptor as a real curve
1856+
curve_ = self._curve()
18301857

18311858
rv = GeomAPI_ProjectPointOnCurve(
18321859
d.toPnt(), curve_, curve.FirstParameter(), curve.LastParameter(),
@@ -1838,14 +1865,67 @@ def paramAt(self: Mixin1DProtocol, d: Union[Real, Vector]) -> float:
18381865

18391866
return rv
18401867

1868+
def params(self: Mixin1DProtocol, pts: Iterable[Vector], tol=1e-6) -> List[float]:
1869+
"""
1870+
Computes u values closest to given vectors.
1871+
1872+
:param pts: the points to compute the parameters at.
1873+
:return: list of u values.
1874+
"""
1875+
1876+
us = []
1877+
1878+
curve = self._geomAdaptor()
1879+
curve_ = self._curve()
1880+
umin = curve.FirstParameter()
1881+
umax = curve.LastParameter()
1882+
1883+
# get the first point
1884+
it = iter(pts)
1885+
pt = next(it)
1886+
1887+
proj = GeomAPI_ProjectPointOnCurve(pt.toPnt(), curve_, umin, umax)
1888+
1889+
u = proj.LowerDistanceParameter()
1890+
us.append(u)
1891+
1892+
for pt in it:
1893+
proj.Perform(pt.toPnt())
1894+
u = proj.LowerDistanceParameter()
1895+
1896+
us.append(u)
1897+
1898+
return us
1899+
1900+
def paramsLength(self: Mixin1DProtocol, locations: Iterable[float]) -> List[float]:
1901+
"""
1902+
Computes u values at given relative lengths.
1903+
1904+
:param locations: list of distances.
1905+
:returns: list of u values.
1906+
:param pts: the points to compute the parameters at.
1907+
"""
1908+
1909+
us = []
1910+
1911+
curve = self._geomAdaptor()
1912+
1913+
L = GCPnts_AbscissaPoint.Length_s(curve)
1914+
1915+
for d in locations:
1916+
u = GCPnts_AbscissaPoint(curve, L * d, curve.FirstParameter()).Parameter()
1917+
us.append(u)
1918+
1919+
return us
1920+
18411921
def tangentAt(
18421922
self: Mixin1DProtocol, locationParam: float = 0.5, mode: ParamMode = "length",
18431923
) -> Vector:
18441924
"""
18451925
Compute tangent vector at the specified location.
18461926
18471927
:param locationParam: distance or parameter value (default: 0.5)
1848-
:param mode: position calculation mode (default: parameter)
1928+
:param mode: position calculation mode (default: length)
18491929
:return: tangent vector
18501930
"""
18511931

@@ -1863,6 +1943,35 @@ def tangentAt(
18631943

18641944
return Vector(gp_Dir(res))
18651945

1946+
def tangents(
1947+
self: Mixin1DProtocol, locations: Iterable[float], mode: ParamMode = "length",
1948+
) -> List[Vector]:
1949+
"""
1950+
Compute tangent vectors at the specified locations.
1951+
1952+
:param locations: list of distances or parameters.
1953+
:param mode: position calculation mode (default: length).
1954+
:return: list of tangent vectors
1955+
"""
1956+
1957+
curve = self._geomAdaptor()
1958+
params: Iterable[float]
1959+
1960+
if mode == "length":
1961+
params = self.paramsLength(locations)
1962+
else:
1963+
params = locations
1964+
1965+
rv = []
1966+
tmp = gp_Pnt()
1967+
res = gp_Vec()
1968+
1969+
for param in params:
1970+
curve.D1(param, tmp, res)
1971+
rv.append(Vector(gp_Dir(res)))
1972+
1973+
return rv
1974+
18661975
def normal(self: Mixin1DProtocol) -> Vector:
18671976
"""
18681977
Calculate the normal Vector. Only possible for planar curves.
@@ -5432,46 +5541,142 @@ def _bool_op(
54325541
builder.Build()
54335542

54345543

5435-
def fuse(s1: Shape, s2: Shape, tol: float = 0.0) -> Shape:
5544+
def _set_glue(builder: BOPAlgo_Builder, glue: GlueLiteral):
5545+
5546+
if glue:
5547+
builder.SetGlue(
5548+
BOPAlgo_GlueEnum.BOPAlgo_GlueFull
5549+
if glue == "full"
5550+
else BOPAlgo_GlueEnum.BOPAlgo_GlueShift
5551+
)
5552+
5553+
5554+
def _set_builder_options(builder: BOPAlgo_Builder, tol: float):
5555+
5556+
builder.SetRunParallel(True)
5557+
builder.SetUseOBB(True)
5558+
builder.SetNonDestructive(True)
5559+
5560+
if tol:
5561+
builder.SetFuzzyValue(tol)
5562+
5563+
5564+
def setThreads(n: int):
54365565
"""
5437-
Fuse two shapes.
5566+
Set number of threads to be used by boolean operations.
54385567
"""
54395568

5440-
builder = BRepAlgoAPI_Fuse()
5441-
_bool_op(s1, s2, builder, tol)
5569+
pool = OSD_ThreadPool.DefaultPool_s()
5570+
pool.Init(n)
5571+
5572+
5573+
def fuse(
5574+
s1: Shape, s2: Shape, *shapes: Shape, tol: float = 0.0, glue: GlueLiteral = None,
5575+
) -> Shape:
5576+
"""
5577+
Fuse at least two shapes.
5578+
"""
5579+
5580+
builder = BOPAlgo_BOP()
5581+
builder.SetOperation(BOPAlgo_FUSE)
5582+
5583+
_set_glue(builder, glue)
5584+
_set_builder_options(builder, tol)
5585+
5586+
builder.AddArgument(s1.wrapped)
5587+
builder.AddTool(s2.wrapped)
5588+
5589+
for s in shapes:
5590+
builder.AddTool(s.wrapped)
5591+
5592+
builder.Perform()
54425593

54435594
return _compound_or_shape(builder.Shape())
54445595

54455596

5446-
def cut(s1: Shape, s2: Shape, tol: float = 0.0) -> Shape:
5597+
def cut(s1: Shape, s2: Shape, tol: float = 0.0, glue: GlueLiteral = None) -> Shape:
54475598
"""
54485599
Subtract two shapes.
54495600
"""
54505601

5451-
builder = BRepAlgoAPI_Cut()
5452-
_bool_op(s1, s2, builder, tol)
5602+
builder = BOPAlgo_BOP()
5603+
builder.SetOperation(BOPAlgo_CUT)
5604+
5605+
_set_glue(builder, glue)
5606+
_set_builder_options(builder, tol)
5607+
5608+
builder.AddArgument(s1.wrapped)
5609+
builder.AddTool(s2.wrapped)
5610+
5611+
builder.Perform()
54535612

54545613
return _compound_or_shape(builder.Shape())
54555614

54565615

5457-
def intersect(s1: Shape, s2: Shape, tol: float = 0.0) -> Shape:
5616+
def intersect(
5617+
s1: Shape, s2: Shape, tol: float = 0.0, glue: GlueLiteral = None
5618+
) -> Shape:
54585619
"""
54595620
Intersect two shapes.
54605621
"""
54615622

5462-
builder = BRepAlgoAPI_Common()
5463-
_bool_op(s1, s2, builder, tol)
5623+
builder = BOPAlgo_BOP()
5624+
builder.SetOperation(BOPAlgo_COMMON)
5625+
5626+
_set_glue(builder, glue)
5627+
_set_builder_options(builder, tol)
5628+
5629+
builder.AddArgument(s1.wrapped)
5630+
builder.AddTool(s2.wrapped)
5631+
5632+
builder.Perform()
54645633

54655634
return _compound_or_shape(builder.Shape())
54665635

54675636

5468-
def split(s1: Shape, s2: Shape) -> Shape:
5637+
def split(s1: Shape, s2: Shape, tol: float = 0.0) -> Shape:
54695638
"""
54705639
Split one shape with another.
54715640
"""
54725641

54735642
builder = BRepAlgoAPI_Splitter()
5474-
_bool_op(s1, s2, builder)
5643+
_bool_op(s1, s2, builder, tol)
5644+
5645+
return _compound_or_shape(builder.Shape())
5646+
5647+
5648+
def imprint(
5649+
*shapes: Shape,
5650+
tol: float = 0.0,
5651+
glue: GlueLiteral = "full",
5652+
history: Optional[Dict[Union[Shape, str], Shape]] = None,
5653+
) -> Shape:
5654+
"""
5655+
Imprint arbitrary number of shapes.
5656+
"""
5657+
5658+
builder = BOPAlgo_Builder()
5659+
5660+
_set_glue(builder, glue)
5661+
_set_builder_options(builder, tol)
5662+
5663+
for s in shapes:
5664+
builder.AddArgument(s.wrapped)
5665+
5666+
builder.Perform()
5667+
5668+
# fill history if provided
5669+
if history is not None:
5670+
images = builder.Images()
5671+
5672+
# collect shapes present in the history dict
5673+
for k, v in history.items():
5674+
if isinstance(k, str):
5675+
history[k] = _compound_or_shape(list(images.Find(v.wrapped)))
5676+
5677+
# store all top-level shape relations
5678+
for s in shapes:
5679+
history[s] = _compound_or_shape(list(images.Find(s.wrapped)))
54755680

54765681
return _compound_or_shape(builder.Shape())
54775682

0 commit comments

Comments
 (0)