Skip to content

Commit 6a440b6

Browse files
authored
Handle constraints from infinite faces (#797)
* Face.toPln method added * solver: set xtol_abs * Constraint: handle infinite faces Infinite faces have their center at 1e99, which was causing overflows in the solver and also was not what the user intended when creating the Shape. They are now converted to the expected values.
1 parent 3cb7237 commit 6a440b6

File tree

5 files changed

+127
-22
lines changed

5 files changed

+127
-22
lines changed

cadquery/assembly.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
from .occ_impl.exporters.assembly import exportAssembly, exportCAF
1616

1717
from .selectors import _expression_grammar as _selector_grammar
18+
from OCP.BRepTools import BRepTools
19+
from OCP.gp import gp_Pln, gp_Pnt
20+
from OCP.Precision import Precision
1821

1922
# type definitions
2023
AssemblyObjects = Union[Shape, Workplane, None]
@@ -106,16 +109,33 @@ def _getAxis(self, arg: Shape) -> Vector:
106109

107110
return rv
108111

109-
def _getPlane(self, arg: Shape) -> Plane:
112+
def _getPln(self, arg: Shape) -> gp_Pln:
110113

111114
if isinstance(arg, Face):
112-
normal = arg.normalAt()
115+
rv = gp_Pln(self._getPnt(arg), arg.normalAt().toDir())
113116
elif isinstance(arg, (Edge, Wire)):
114117
normal = arg.normal()
118+
origin = arg.Center()
119+
plane = Plane(origin, normal=normal)
120+
rv = plane.toPln()
115121
else:
116-
raise ValueError(f"Can not get normal from {arg}.")
117-
origin = arg.Center()
118-
return Plane(origin, normal=normal)
122+
raise ValueError(f"Can not construct a plane for {arg}.")
123+
124+
return rv
125+
126+
def _getPnt(self, arg: Shape) -> gp_Pnt:
127+
128+
# check for infinite face
129+
if isinstance(arg, Face) and any(
130+
Precision.IsInfinite_s(x) for x in BRepTools.UVBounds_s(arg.wrapped)
131+
):
132+
# fall back to gp_Pln center
133+
pln = arg.toPln()
134+
center = Vector(pln.Location())
135+
else:
136+
center = arg.Center()
137+
138+
return center.toPnt()
119139

120140
def toPOD(self) -> ConstraintPOD:
121141
"""
@@ -131,14 +151,14 @@ def toPOD(self) -> ConstraintPOD:
131151
if self.kind == "Axis":
132152
rv.append((self._getAxis(arg).toDir(),))
133153
elif self.kind == "Point":
134-
rv.append((arg.Center().toPnt(),))
154+
rv.append((self._getPnt(arg),))
135155
elif self.kind == "Plane":
136-
rv.append((self._getAxis(arg).toDir(), arg.Center().toPnt()))
156+
rv.append((self._getAxis(arg).toDir(), self._getPnt(arg)))
137157
elif self.kind == "PointInPlane":
138158
if idx == 0:
139-
rv.append((arg.Center().toPnt(),))
159+
rv.append((self._getPnt(arg),))
140160
else:
141-
rv.append((self._getPlane(arg).toPln(),))
161+
rv.append((self._getPln(arg),))
142162
else:
143163
raise ValueError(f"Unknown constraint kind {self.kind}")
144164

cadquery/occ_impl/shapes.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2264,6 +2264,17 @@ def chamfer2D(self, d: float, vertices: Iterable[Vertex]) -> "Face":
22642264

22652265
return self.__class__(chamfer_builder.Shape()).fix()
22662266

2267+
def toPln(self) -> gp_Pln:
2268+
"""
2269+
Convert this face to a gp_Pln.
2270+
2271+
Note the Location of the resulting plane may not equal the center of this face,
2272+
however the resulting plane will still contain the center of this face.
2273+
"""
2274+
2275+
adaptor = BRepAdaptor_Surface(self.wrapped)
2276+
return adaptor.Plane()
2277+
22672278

22682279
class Shell(Shape):
22692280
"""

cadquery/occ_impl/solver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ def func(x, grad):
256256
opt.set_ftol_abs(0)
257257
opt.set_ftol_rel(0)
258258
opt.set_xtol_rel(TOL)
259-
opt.set_xtol_abs(0)
259+
opt.set_xtol_abs(TOL * 1e-3)
260260
opt.set_maxeval(MAXITER)
261261

262262
x = opt.optimize(x0)

tests/test_assembly.py

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ def box_and_vertex():
7171
return assy
7272

7373

74+
def solve_result_check(solve_result: dict) -> bool:
75+
checks = [
76+
solve_result["status"] == nlopt.XTOL_REACHED,
77+
solve_result["cost"] < 1e-9,
78+
solve_result["iters"] > 0,
79+
]
80+
return all(checks)
81+
82+
7483
def test_color():
7584

7685
c1 = cq.Color("red")
@@ -228,9 +237,7 @@ def test_constrain(simple_assy, nested_assy):
228237

229238
simple_assy.solve()
230239

231-
assert simple_assy._solve_result["status"] == nlopt.XTOL_REACHED
232-
assert simple_assy._solve_result["cost"] < 1e-9
233-
assert simple_assy._solve_result["iters"] > 0
240+
assert solve_result_check(simple_assy._solve_result)
234241

235242
assert (
236243
simple_assy.loc.wrapped.Transformation()
@@ -247,9 +254,7 @@ def test_constrain(simple_assy, nested_assy):
247254

248255
nested_assy.solve()
249256

250-
assert nested_assy._solve_result["status"] == nlopt.XTOL_REACHED
251-
assert nested_assy._solve_result["cost"] < 1e-9
252-
assert nested_assy._solve_result["iters"] > 0
257+
assert solve_result_check(nested_assy._solve_result)
253258

254259
assert (
255260
nested_assy.children[0]
@@ -302,6 +307,7 @@ def test_PointInPlane_constraint(box_and_vertex):
302307
param=0,
303308
)
304309
box_and_vertex.solve()
310+
solve_result_check(box_and_vertex._solve_result)
305311

306312
x_pos = (
307313
box_and_vertex.children[0].loc.wrapped.Transformation().TranslationPart().X()
@@ -311,6 +317,7 @@ def test_PointInPlane_constraint(box_and_vertex):
311317
# add a second PointInPlane constraint
312318
box_and_vertex.constrain("vertex", "box@faces@>Y", "PointInPlane", param=0)
313319
box_and_vertex.solve()
320+
solve_result_check(box_and_vertex._solve_result)
314321

315322
vertex_translation_part = (
316323
box_and_vertex.children[0].loc.wrapped.Transformation().TranslationPart()
@@ -323,6 +330,7 @@ def test_PointInPlane_constraint(box_and_vertex):
323330
# add a third PointInPlane constraint
324331
box_and_vertex.constrain("vertex", "box@faces@>Z", "PointInPlane", param=0)
325332
box_and_vertex.solve()
333+
solve_result_check(box_and_vertex._solve_result)
326334

327335
# should now be on the >X and >Y and >Z corner
328336
assert (
@@ -342,6 +350,7 @@ def test_PointInPlane_3_parts(box_and_vertex):
342350
box_and_vertex.constrain("vertex", "cylinder@faces@>Z", "PointInPlane")
343351
box_and_vertex.constrain("vertex", "box@faces@>X", "PointInPlane")
344352
box_and_vertex.solve()
353+
solve_result_check(box_and_vertex._solve_result)
345354
vertex_translation_part = (
346355
box_and_vertex.children[0].loc.wrapped.Transformation().TranslationPart()
347356
)
@@ -356,6 +365,7 @@ def test_PointInPlane_param(box_and_vertex, param0, param1):
356365
box_and_vertex.constrain("vertex", "box@faces@>Z", "PointInPlane", param=param0)
357366
box_and_vertex.constrain("vertex", "box@faces@>X", "PointInPlane", param=param1)
358367
box_and_vertex.solve()
368+
solve_result_check(box_and_vertex._solve_result)
359369

360370
vertex_translation_part = (
361371
box_and_vertex.children[0].loc.wrapped.Transformation().TranslationPart()
@@ -364,9 +374,9 @@ def test_PointInPlane_param(box_and_vertex, param0, param1):
364374
assert vertex_translation_part.X() - 0.5 == pytest.approx(param1, abs=1e-6)
365375

366376

367-
def test_constraint_getPlane():
377+
def test_constraint_getPln():
368378
"""
369-
Test that _getPlane does the right thing with different arguments
379+
Test that _getPln does the right thing with different arguments
370380
"""
371381
ids = (0, 1)
372382
sublocs = (cq.Location(), cq.Location())
@@ -377,11 +387,19 @@ def make_constraint(shape0):
377387
def fail_this(shape0):
378388
c0 = make_constraint(shape0)
379389
with pytest.raises(ValueError):
380-
c0._getPlane(c0.args[0])
390+
c0._getPln(c0.args[0])
381391

382-
def resulting_plane(shape0):
392+
def resulting_pln(shape0):
383393
c0 = make_constraint(shape0)
384-
return c0._getPlane(c0.args[0])
394+
return c0._getPln(c0.args[0])
395+
396+
def resulting_plane(shape0):
397+
p0 = resulting_pln(shape0)
398+
return cq.Plane(
399+
cq.Vector(p0.Location()),
400+
cq.Vector(p0.XAxis().Direction()),
401+
cq.Vector(p0.Axis().Direction()),
402+
)
385403

386404
# point should fail
387405
fail_this(cq.Vertex.makeVertex(0, 0, 0))
@@ -423,7 +441,7 @@ def resulting_plane(shape0):
423441
wonky_shape = cq.Wire.makePolygon(points3)
424442
fail_this(wonky_shape)
425443

426-
# all faces should succeed
444+
# all makePlane faces should succeed
427445
for length, width in product([None, 10], [None, 11]):
428446
f0 = cq.Face.makePlane(
429447
length=length, width=width, basePnt=(1, 2, 3), dir=(1, 0, 0)
@@ -482,3 +500,36 @@ def test_toCompound(simple_assy, nested_assy):
482500
assert cq.Vector(0, 0, 18) in [x.Center() for x in c3.Faces()]
483501
# also check with bounding box
484502
assert c3.BoundingBox().zlen == pytest.approx(18)
503+
504+
505+
@pytest.mark.parametrize("origin", [(0, 0, 0), (10, -10, 10)])
506+
@pytest.mark.parametrize("normal", [(0, 0, 1), (-1, -1, 1)])
507+
def test_infinite_face_constraint_PointInPlane(origin, normal):
508+
"""
509+
An OCCT infinite face has a center at (1e99, 1e99), but when a user uses it
510+
in a constraint, the center should be basePnt.
511+
"""
512+
513+
f0 = cq.Face.makePlane(length=None, width=None, basePnt=origin, dir=normal)
514+
515+
c0 = cq.assembly.Constraint(
516+
("point", "plane"),
517+
(cq.Vertex.makeVertex(10, 10, 10), f0),
518+
sublocs=(cq.Location(), cq.Location()),
519+
kind="PointInPlane",
520+
)
521+
p0 = c0._getPln(c0.args[1]) # a gp_Pln
522+
derived_origin = cq.Vector(p0.Location())
523+
assert derived_origin == cq.Vector(origin)
524+
525+
526+
@pytest.mark.parametrize("kind", ["Plane", "PointInPlane", "Point"])
527+
def test_infinite_face_constraint_Plane(kind):
528+
529+
assy = cq.Assembly(cq.Workplane().sphere(1), name="part0")
530+
assy.add(cq.Workplane().sphere(1), name="part1")
531+
assy.constrain(
532+
"part0", cq.Face.makePlane(), "part1", cq.Face.makePlane(), kind,
533+
)
534+
assy.solve()
535+
assert solve_result_check(assy._solve_result)

tests/test_cadquery.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4595,3 +4595,26 @@ def testBrepImportExport(self):
45954595

45964596
self.assertTrue(si.isValid())
45974597
self.assertAlmostEqual(si.Volume(), 1)
4598+
4599+
def testFaceToPln(self):
4600+
4601+
origin = (1, 2, 3)
4602+
normal = (1, 1, 1)
4603+
f0 = Face.makePlane(length=None, width=None, basePnt=origin, dir=normal)
4604+
p0 = f0.toPln()
4605+
4606+
self.assertTrue(Vector(p0.Location()) == Vector(origin))
4607+
self.assertTrue(Vector(p0.Axis().Direction()) == Vector(normal).normalized())
4608+
4609+
origin1 = (0, 0, -3)
4610+
normal1 = (-1, 1, -1)
4611+
f1 = Face.makePlane(length=0.1, width=100, basePnt=origin1, dir=normal1)
4612+
p1 = f1.toPln()
4613+
4614+
self.assertTrue(Vector(p1.Location()) == Vector(origin1))
4615+
self.assertTrue(Vector(p1.Axis().Direction()) == Vector(normal1).normalized())
4616+
4617+
f2 = Workplane().box(1, 1, 10, centered=False).faces(">Z").val()
4618+
p2 = f2.toPln()
4619+
self.assertTrue(p2.Contains(f2.Center().toPnt(), 0.1))
4620+
self.assertTrue(Vector(p2.Axis().Direction()) == f2.normalAt())

0 commit comments

Comments
 (0)