Skip to content

Commit 5720c7d

Browse files
cq.Shape and some free-func improvements (#1672)
* Initial commit of better paramAt, solid and a new function check * mypy fix * isSolid fix * Implement outerShell, innerShells * Imports cleanup * Typo fix * Make paramAt fully compatibile with Wires * Typo fix * Add check test * Fix orientation handling * Add test * paramAt test * More tests * Add orientation fix and optimize check * Switch to miniforge * Add missing newline
1 parent e2f9e85 commit 5720c7d

File tree

4 files changed

+200
-30
lines changed

4 files changed

+200
-30
lines changed

appveyor.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ environment:
1616
secure: $(anaconda_token)
1717

1818
init:
19-
- cmd: curl -fsSLo Miniforge.exe https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-Windows-x86_64.exe
19+
- cmd: curl -fsSLo Miniforge.exe https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Windows-x86_64.exe
2020
- cmd: Miniforge.exe /InstallationType=JustMe /RegisterPython=0 /S /D=%MINICONDA_DIRNAME%
2121
- cmd: set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%"
2222
- cmd: activate
23-
- sh: curl -sL https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$OS-x86_64.sh > miniconda.sh
23+
- sh: curl -sL https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$OS-x86_64.sh > miniconda.sh
2424
- sh: bash miniconda.sh -b -p $HOME/miniconda;
2525
- sh: export PATH="$HOME/miniconda/bin:$HOME/miniconda/lib:$PATH";
26-
- sh: source $HOME/miniconda/bin/activate
26+
- sh: source $HOME/miniconda/bin/activate
2727

2828
install:
2929
- mamba env create -f environment.yml

cadquery/occ_impl/shapes.py

Lines changed: 114 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
from OCP.GeomAPI import (
133133
GeomAPI_Interpolate,
134134
GeomAPI_ProjectPointOnSurf,
135+
GeomAPI_ProjectPointOnCurve,
135136
GeomAPI_PointsToBSpline,
136137
GeomAPI_PointsToBSplineSurface,
137138
)
@@ -144,6 +145,7 @@
144145
BRepAlgoAPI_Cut,
145146
BRepAlgoAPI_BooleanOperation,
146147
BRepAlgoAPI_Splitter,
148+
BRepAlgoAPI_Check,
147149
)
148150

149151
from OCP.Geom import (
@@ -152,6 +154,7 @@
152154
Geom_CylindricalSurface,
153155
Geom_Surface,
154156
Geom_Plane,
157+
Geom_BSplineCurve,
155158
)
156159
from OCP.Geom2d import Geom2d_Line
157160

@@ -211,20 +214,11 @@
211214
Graphic3d_VTA_TOP,
212215
)
213216

214-
from OCP.Graphic3d import (
215-
Graphic3d_HTA_LEFT,
216-
Graphic3d_HTA_CENTER,
217-
Graphic3d_HTA_RIGHT,
218-
Graphic3d_VTA_BOTTOM,
219-
Graphic3d_VTA_CENTER,
220-
Graphic3d_VTA_TOP,
221-
)
222-
223217
from OCP.NCollection import NCollection_Utf8String
224218

225219
from OCP.BRepFeat import BRepFeat_MakeDPrism
226220

227-
from OCP.BRepClass3d import BRepClass3d_SolidClassifier
221+
from OCP.BRepClass3d import BRepClass3d_SolidClassifier, BRepClass3d
228222

229223
from OCP.TCollection import TCollection_AsciiString
230224

@@ -233,10 +227,8 @@
233227
from OCP.GeomAbs import (
234228
GeomAbs_Shape,
235229
GeomAbs_C0,
236-
GeomAbs_C1,
237-
GeomAbs_C2,
238230
GeomAbs_G2,
239-
GeomAbs_G1,
231+
GeomAbs_C2,
240232
GeomAbs_Intersection,
241233
GeomAbs_JoinType,
242234
)
@@ -249,7 +241,7 @@
249241

250242
from OCP.TopAbs import TopAbs_ShapeEnum, TopAbs_Orientation
251243

252-
from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds
244+
from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds, ShapeAnalysis_Wire
253245
from OCP.TopTools import TopTools_HSequenceOfShape
254246

255247
from OCP.GCPnts import GCPnts_AbscissaPoint
@@ -281,6 +273,8 @@
281273

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

276+
from OCP.GeomConvert import GeomConvert_ApproxCurve
277+
284278
from math import pi, sqrt, inf, radians, cos
285279

286280
import warnings
@@ -1643,6 +1637,9 @@ def makeVertex(cls, x: float, y: float, z: float) -> "Vertex":
16431637

16441638

16451639
class Mixin1DProtocol(ShapeProtocol, Protocol):
1640+
def _approxCurve(self) -> Geom_BSplineCurve:
1641+
...
1642+
16461643
def _geomAdaptor(self) -> Union[BRepAdaptor_Curve, BRepAdaptor_CompCurve]:
16471644
...
16481645

@@ -1699,18 +1696,44 @@ def endPoint(self: Mixin1DProtocol) -> Vector:
16991696

17001697
return Vector(curve.Value(umax))
17011698

1702-
def paramAt(self: Mixin1DProtocol, d: float) -> float:
1699+
def _approxCurve(self: Mixin1DProtocol) -> Geom_BSplineCurve:
17031700
"""
1704-
Compute parameter value at the specified normalized distance.
1701+
Approximate curve adaptor into a real b-spline. Meant for handling of
1702+
BRepAdaptor_CompCurve.
1703+
"""
1704+
1705+
rv = GeomConvert_ApproxCurve(
1706+
self._geomAdaptor(), TOLERANCE, GeomAbs_C2, MaxSegments=100, MaxDegree=3
1707+
).Curve()
17051708

1706-
:param d: normalized distance [0, 1]
1709+
return rv
1710+
1711+
def paramAt(self: Mixin1DProtocol, d: Union[Real, Vector]) -> float:
1712+
"""
1713+
Compute parameter value at the specified normalized distance or a point.
1714+
1715+
:param d: normalized distance [0, 1] or a point
17071716
:return: parameter value
17081717
"""
17091718

17101719
curve = self._geomAdaptor()
17111720

1712-
l = GCPnts_AbscissaPoint.Length_s(curve)
1713-
return GCPnts_AbscissaPoint(curve, l * d, curve.FirstParameter()).Parameter()
1721+
if isinstance(d, Vector):
1722+
# handle comp curves (i.e. wire adaptors)
1723+
if isinstance(curve, BRepAdaptor_Curve):
1724+
curve_ = curve.Curve().Curve() # get the underlying curve object
1725+
else:
1726+
curve_ = self._approxCurve() # approximate the adaptor as a real curve
1727+
1728+
rv = GeomAPI_ProjectPointOnCurve(
1729+
d.toPnt(), curve_, curve.FirstParameter(), curve.LastParameter(),
1730+
).LowerDistanceParameter()
1731+
1732+
else:
1733+
l = GCPnts_AbscissaPoint.Length_s(curve)
1734+
rv = GCPnts_AbscissaPoint(curve, l * d, curve.FirstParameter()).Parameter()
1735+
1736+
return rv
17141737

17151738
def tangentAt(
17161739
self: Mixin1DProtocol,
@@ -2253,12 +2276,29 @@ class Wire(Shape, Mixin1D):
22532276

22542277
wrapped: TopoDS_Wire
22552278

2256-
def _geomAdaptor(self) -> BRepAdaptor_CompCurve:
2279+
def _nbEdges(self) -> int:
22572280
"""
2258-
Return the underlying geometry
2281+
Number of edges.
22592282
"""
22602283

2261-
return BRepAdaptor_CompCurve(self.wrapped)
2284+
sa = ShapeAnalysis_Wire()
2285+
sa.Load(self.wrapped)
2286+
2287+
return sa.NbEdges()
2288+
2289+
def _geomAdaptor(self) -> Union[BRepAdaptor_Curve, BRepAdaptor_CompCurve]:
2290+
"""
2291+
Return the underlying geometry.
2292+
"""
2293+
2294+
rv: Union[BRepAdaptor_Curve, BRepAdaptor_CompCurve]
2295+
2296+
if self._nbEdges() == 1:
2297+
rv = self.Edges()[-1]._geomAdaptor()
2298+
else:
2299+
rv = BRepAdaptor_CompCurve(self.wrapped)
2300+
2301+
return rv
22622302

22632303
def close(self) -> "Wire":
22642304
"""
@@ -2518,6 +2558,7 @@ def fillet(
25182558
"""
25192559
Apply 2D or 3D fillet to a wire
25202560
2561+
:param wire: The input wire to fillet. Currently only open wires are supported
25212562
:param radius: the radius of the fillet, must be > zero
25222563
:param vertices: Optional list of vertices to fillet. By default all vertices are fillet.
25232564
:return: A wire with filleted corners
@@ -2948,7 +2989,7 @@ def thicken(self, thickness: float) -> "Solid":
29482989
False,
29492990
GeomAbs_Intersection,
29502991
True,
2951-
) # The last True is important to make solid
2992+
) # The last True is important to make a solid
29522993

29532994
builder.MakeOffsetShape()
29542995

@@ -3300,8 +3341,8 @@ def isSolid(obj: Shape) -> bool:
33003341
Returns true if the object is a solid, false otherwise
33013342
"""
33023343
if hasattr(obj, "ShapeType"):
3303-
if obj.ShapeType == "Solid" or (
3304-
obj.ShapeType == "Compound" and len(obj.Solids()) > 0
3344+
if obj.ShapeType() == "Solid" or (
3345+
obj.ShapeType() == "Compound" and len(obj.Solids()) > 0
33053346
):
33063347
return True
33073348
return False
@@ -3837,6 +3878,22 @@ def sweep_multi(
38373878

38383879
return cls(builder.Shape())
38393880

3881+
def outerShell(self) -> Shell:
3882+
"""
3883+
Returns outer shell.
3884+
"""
3885+
3886+
return Shell(BRepClass3d.OuterShell_s(self.wrapped))
3887+
3888+
def innerShells(self) -> List[Shell]:
3889+
"""
3890+
Returns inner shells.
3891+
"""
3892+
3893+
outer = self.outerShell()
3894+
3895+
return [s for s in self.Shells() if not s.isSame(outer)]
3896+
38403897

38413898
class CompSolid(Shape, Mixin3D):
38423899
"""
@@ -4419,9 +4476,20 @@ def solid(*s: Shape) -> Shape:
44194476

44204477

44214478
@solid.register
4422-
def solid(s: Sequence[Shape]) -> Shape:
4479+
def solid(s: Sequence[Shape], inner: Optional[Sequence[Shape]] = None) -> Shape:
44234480

4424-
return solid(*s)
4481+
builder = BRepBuilderAPI_MakeSolid()
4482+
builder.Add(shell(*s).wrapped)
4483+
4484+
if inner:
4485+
for sh in _get(shell(*inner), "Shell"):
4486+
builder.Add(sh.wrapped)
4487+
4488+
# fix orientations
4489+
sf = ShapeFix_Solid(builder.Solid())
4490+
sf.Perform()
4491+
4492+
return _compound_or_shape(sf.Solid())
44254493

44264494

44274495
@multimethod
@@ -5111,3 +5179,22 @@ def loft(s: Sequence[Shape], cap: bool = False, ruled: bool = False) -> Shape:
51115179
def loft(*s: Shape, cap: bool = False, ruled: bool = False) -> Shape:
51125180

51135181
return loft(s, cap, ruled)
5182+
5183+
5184+
#%% diagnotics
5185+
5186+
5187+
def check(s: Shape) -> bool:
5188+
"""
5189+
Check if a shape is valid.
5190+
"""
5191+
5192+
analyzer = BRepAlgoAPI_Check(s.wrapped)
5193+
analyzer.SetRunParallel(True)
5194+
analyzer.SetUseOBB(True)
5195+
5196+
analyzer.Perform()
5197+
5198+
rv = analyzer.IsValid()
5199+
5200+
return rv

tests/test_free_functions.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
_get,
4040
_get_one,
4141
_get_edges,
42+
check,
4243
)
4344

4445
from pytest import approx, raises
@@ -139,6 +140,13 @@ def test_constructors():
139140
assert s1.Volume() == approx(1)
140141
assert s2.Volume() == approx(1)
141142

143+
# solid with voids
144+
b1 = box(0.1, 0.1, 0.1)
145+
146+
s3 = solid(b.Faces(), b1.moved([(0.2, 0, 0.5), (-0.2, 0, 0.5)]).Faces())
147+
148+
assert s3.Volume() == approx(1 - 2 * 0.1 ** 3)
149+
142150
# compound
143151
c1 = compound(b.Faces())
144152
c2 = compound(*b.Faces())
@@ -558,3 +566,15 @@ def test_export():
558566
b2 = Shape.importBrep("box.brep")
559567

560568
assert (b1 - b2).Volume() == approx(0)
569+
570+
571+
# %% diagnostics
572+
def test_check():
573+
574+
s1 = box(1, 1, 1)
575+
576+
assert check(s1)
577+
578+
s2 = sweep(rect(1, 1), segment((0, 0), (1, 1)))
579+
580+
assert not check(s2)

tests/test_shapes.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from cadquery.occ_impl.shapes import (
2+
wire,
3+
segment,
4+
polyline,
5+
Vector,
6+
box,
7+
Solid,
8+
compound,
9+
)
10+
11+
from pytest import approx
12+
13+
14+
def test_paramAt():
15+
16+
# paramAt for a segment
17+
e = segment((0, 0), (0, 1))
18+
19+
p1 = e.paramAt(Vector(0, 0))
20+
p2 = e.paramAt(Vector(-1, 0))
21+
p3 = e.paramAt(Vector(0, 1))
22+
23+
assert p1 == approx(p2)
24+
assert p1 == approx(0)
25+
assert p3 == approx(e.paramAt(1))
26+
27+
# paramAt for a simple wire
28+
w1 = wire(e)
29+
30+
p4 = w1.paramAt(Vector(0, 0))
31+
p5 = w1.paramAt(Vector(0, 1))
32+
33+
assert p4 == approx(p1)
34+
assert p5 == approx(p3)
35+
36+
# paramAt for a complex wire
37+
w2 = polyline((0, 0), (0, 1), (1, 1))
38+
39+
p6 = w2.paramAt(Vector(0, 0))
40+
p7 = w2.paramAt(Vector(0, 1))
41+
p8 = w2.paramAt(Vector(0.1, 0.1))
42+
43+
assert p6 == approx(w2.paramAt(0))
44+
assert p7 == approx(w2.paramAt(0.5))
45+
assert p8 == approx(w2.paramAt(0.1 / 2))
46+
47+
48+
def test_isSolid():
49+
50+
s = box(1, 1, 1)
51+
52+
assert Solid.isSolid(s)
53+
assert Solid.isSolid(compound(s))
54+
assert not Solid.isSolid(s.faces())
55+
56+
57+
def test_shells():
58+
59+
s = box(2, 2, 2) - box(1, 1, 1).moved(z=0.5)
60+
61+
assert s.outerShell().Area() == approx(6 * 4)
62+
assert len(s.innerShells()) == 1
63+
assert s.innerShells()[0].Area() == approx(6 * 1)

0 commit comments

Comments
 (0)