Skip to content

Commit 4108c7d

Browse files
adam-urbanczykmarcus7070lorenzncodejmwright
authored
Additional constraints (#975)
* Started assy solver refactoring * First working version * Added validation +initial work on FixedPoint * Fixed tests * Unary constraints support and finish Fixed Point * Added Fixed and FixedAxis * Changed the locking logic * Test for unary constraints * Update cadquery/occ_impl/solver.py Co-authored-by: Marcus Boyd <[email protected]> * Simple validation test * PointOnLine constraint * PointOnLine test * FixedRotation and FixedRotationAxis * FixedRotation test * Describe PointOnLine * Add new constraints to the docs * Use gp_Intrinsic_XYZ * Apply suggestions from code review Co-authored-by: Marcus Boyd <[email protected]> * Apply suggestions from code review Co-authored-by: Lorenz <[email protected]> * Black fix * Apply suggestions from code review Co-authored-by: Lorenz <[email protected]> Co-authored-by: Jeremy Wright <[email protected]> * Use windows-latest Co-authored-by: Marcus Boyd <[email protected]> Co-authored-by: Lorenz <[email protected]> Co-authored-by: Jeremy Wright <[email protected]>
1 parent ed8e62c commit 4108c7d

File tree

5 files changed

+722
-260
lines changed

5 files changed

+722
-260
lines changed

azure-pipelines.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
- template: conda-build.yml@templates
4444
parameters:
4545
name: Windows
46-
vmImage: 'vs2017-win2016'
46+
vmImage: 'windows-latest'
4747
py_maj: 3
4848
py_min: ${{minor}}
4949
conda_bld: 3.21.6

cadquery/assembly.py

Lines changed: 65 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
from functools import reduce
22
from typing import Union, Optional, List, Dict, Any, overload, Tuple, Iterator, cast
33
from typing_extensions import Literal
4+
from typish import instance_of
45
from uuid import uuid1 as uuid
56

67
from .cq import Workplane
7-
from .occ_impl.shapes import Shape, Compound, Face, Edge, Wire
8-
from .occ_impl.geom import Location, Vector, Plane
8+
from .occ_impl.shapes import Shape, Compound
9+
from .occ_impl.geom import Location
910
from .occ_impl.assembly import Color
1011
from .occ_impl.solver import (
1112
ConstraintSolver,
12-
ConstraintMarker,
13-
Constraint as ConstraintPOD,
13+
ConstraintSpec as Constraint,
14+
UnaryConstraintKind,
15+
BinaryConstraintKind,
1416
)
1517
from .occ_impl.exporters.assembly import (
1618
exportAssembly,
@@ -21,9 +23,6 @@
2123
)
2224

2325
from .selectors import _expression_grammar as _selector_grammar
24-
from OCP.BRepTools import BRepTools
25-
from OCP.gp import gp_Pln, gp_Pnt
26-
from OCP.Precision import Precision
2726

2827
# type definitions
2928
AssemblyObjects = Union[Shape, Workplane, None]
@@ -67,112 +66,6 @@ def _define_grammar():
6766
_grammar = _define_grammar()
6867

6968

70-
class Constraint(object):
71-
"""
72-
Geometrical constraint between two shapes of an assembly.
73-
"""
74-
75-
objects: Tuple[str, ...]
76-
args: Tuple[Shape, ...]
77-
sublocs: Tuple[Location, ...]
78-
kind: ConstraintKinds
79-
param: Any
80-
81-
def __init__(
82-
self,
83-
objects: Tuple[str, ...],
84-
args: Tuple[Shape, ...],
85-
sublocs: Tuple[Location, ...],
86-
kind: ConstraintKinds,
87-
param: Any = None,
88-
):
89-
"""
90-
Construct a constraint.
91-
92-
:param objects: object names referenced in the constraint
93-
:param args: subshapes (e.g. faces or edges) of the objects
94-
:param sublocs: locations of the objects (only relevant if the objects are nested in a sub-assembly)
95-
:param kind: constraint kind
96-
:param param: optional arbitrary parameter passed to the solver
97-
"""
98-
99-
self.objects = objects
100-
self.args = args
101-
self.sublocs = sublocs
102-
self.kind = kind
103-
self.param = param
104-
105-
def _getAxis(self, arg: Shape) -> Vector:
106-
107-
if isinstance(arg, Face):
108-
rv = arg.normalAt()
109-
elif isinstance(arg, Edge) and arg.geomType() != "CIRCLE":
110-
rv = arg.tangentAt()
111-
elif isinstance(arg, Edge) and arg.geomType() == "CIRCLE":
112-
rv = arg.normal()
113-
else:
114-
raise ValueError(f"Cannot construct Axis for {arg}")
115-
116-
return rv
117-
118-
def _getPln(self, arg: Shape) -> gp_Pln:
119-
120-
if isinstance(arg, Face):
121-
rv = gp_Pln(self._getPnt(arg), arg.normalAt().toDir())
122-
elif isinstance(arg, (Edge, Wire)):
123-
normal = arg.normal()
124-
origin = arg.Center()
125-
plane = Plane(origin, normal=normal)
126-
rv = plane.toPln()
127-
else:
128-
raise ValueError(f"Can not construct a plane for {arg}.")
129-
130-
return rv
131-
132-
def _getPnt(self, arg: Shape) -> gp_Pnt:
133-
134-
# check for infinite face
135-
if isinstance(arg, Face) and any(
136-
Precision.IsInfinite_s(x) for x in BRepTools.UVBounds_s(arg.wrapped)
137-
):
138-
# fall back to gp_Pln center
139-
pln = arg.toPln()
140-
center = Vector(pln.Location())
141-
else:
142-
center = arg.Center()
143-
144-
return center.toPnt()
145-
146-
def toPOD(self) -> ConstraintPOD:
147-
"""
148-
Convert the constraint to a representation used by the solver.
149-
"""
150-
151-
rv: List[Tuple[ConstraintMarker, ...]] = []
152-
153-
for idx, (arg, loc) in enumerate(zip(self.args, self.sublocs)):
154-
155-
arg = arg.located(loc * arg.location())
156-
157-
if self.kind == "Axis":
158-
rv.append((self._getAxis(arg).toDir(),))
159-
elif self.kind == "Point":
160-
rv.append((self._getPnt(arg),))
161-
elif self.kind == "Plane":
162-
rv.append((self._getAxis(arg).toDir(), self._getPnt(arg)))
163-
elif self.kind == "PointInPlane":
164-
if idx == 0:
165-
rv.append((self._getPnt(arg),))
166-
else:
167-
rv.append((self._getPln(arg),))
168-
else:
169-
raise ValueError(f"Unknown constraint kind {self.kind}")
170-
171-
rv.append(self.param)
172-
173-
return cast(ConstraintPOD, tuple(rv))
174-
175-
17669
class Assembly(object):
17770
"""Nested assembly of Workplane and Shape objects defining their relative positions."""
17871

@@ -389,6 +282,12 @@ def constrain(
389282
) -> "Assembly":
390283
...
391284

285+
@overload
286+
def constrain(
287+
self, q1: str, kind: ConstraintKinds, param: Any = None
288+
) -> "Assembly":
289+
...
290+
392291
@overload
393292
def constrain(
394293
self,
@@ -401,12 +300,25 @@ def constrain(
401300
) -> "Assembly":
402301
...
403302

303+
@overload
304+
def constrain(
305+
self, id1: str, s1: Shape, kind: ConstraintKinds, param: Any = None,
306+
) -> "Assembly":
307+
...
308+
404309
def constrain(self, *args, param=None):
405310
"""
406311
Define a new constraint.
407312
"""
408313

409-
if len(args) == 3:
314+
# dispatch on arguments
315+
if len(args) == 2:
316+
q1, kind = args
317+
id1, s1 = self._query(q1)
318+
elif len(args) == 3 and instance_of(args[1], UnaryConstraintKind):
319+
q1, kind, param = args
320+
id1, s1 = self._query(q1)
321+
elif len(args) == 3:
410322
q1, q2, kind = args
411323
id1, s1 = self._query(q1)
412324
id2, s2 = self._query(q2)
@@ -421,11 +333,18 @@ def constrain(self, *args, param=None):
421333
else:
422334
raise ValueError(f"Incompatible arguments: {args}")
423335

424-
loc1, id1_top = self._subloc(id1)
425-
loc2, id2_top = self._subloc(id2)
426-
self.constraints.append(
427-
Constraint((id1_top, id2_top), (s1, s2), (loc1, loc2), kind, param)
428-
)
336+
# handle unary and binary constraints
337+
if instance_of(kind, UnaryConstraintKind):
338+
loc1, id1_top = self._subloc(id1)
339+
c = Constraint((id1_top,), (s1,), (loc1,), kind, param)
340+
elif instance_of(kind, BinaryConstraintKind):
341+
loc1, id1_top = self._subloc(id1)
342+
loc2, id2_top = self._subloc(id2)
343+
c = Constraint((id1_top, id2_top), (s1, s2), (loc1, loc2), kind, param)
344+
else:
345+
raise ValueError(f"Unknown constraint: {kind}")
346+
347+
self.constraints.append(c)
429348

430349
return self
431350

@@ -434,32 +353,53 @@ def solve(self) -> "Assembly":
434353
Solve the constraints.
435354
"""
436355

437-
# get all entities and number them
356+
# Get all entities and number them. First entity is marked as locked
438357
ents = {}
439358

440359
i = 0
441-
lock_ix = 0
360+
locked = []
442361
for c in self.constraints:
443362
for name in c.objects:
444363
if name not in ents:
445364
ents[name] = i
446-
if name == self.name:
447-
lock_ix = i
448365
i += 1
366+
if c.kind == "Fixed" or name == self.name:
367+
locked.append(ents[name])
368+
369+
# Lock the first occuring entity if needed.
370+
if not locked:
371+
unary_objects = [
372+
c.objects[0]
373+
for c in self.constraints
374+
if instance_of(c.kind, UnaryConstraintKind)
375+
]
376+
binary_objects = [
377+
c.objects[0]
378+
for c in self.constraints
379+
if instance_of(c.kind, BinaryConstraintKind)
380+
]
381+
for b in binary_objects:
382+
if b not in unary_objects:
383+
locked.append(ents[b])
384+
break
449385

450386
locs = [self.objects[n].loc for n in ents]
451387

452388
# construct the constraint mapping
453389
constraints = []
454390
for c in self.constraints:
455-
constraints.append(((ents[c.objects[0]], ents[c.objects[1]]), c.toPOD()))
391+
ixs = tuple(ents[obj] for obj in c.objects)
392+
pods = c.toPODs()
393+
394+
for pod in pods:
395+
constraints.append((ixs, pod))
456396

457397
# check if any constraints were specified
458398
if not constraints:
459399
raise ValueError("At least one constraint required")
460400

461401
# instantiate the solver
462-
solver = ConstraintSolver(locs, constraints, locked=[lock_ix])
402+
solver = ConstraintSolver(locs, constraints, locked=locked)
463403

464404
# solve
465405
locs_new, self._solve_result = solver.solve()

0 commit comments

Comments
 (0)