From 320ec2e166af2505df76d74891d3a08953affb3a Mon Sep 17 00:00:00 2001
From: Leni Aniva
Date: Thu, 28 Aug 2025 09:14:23 -0700
Subject: [PATCH 01/10] feat: Add tree-initialize stub
---
cadquery/assembly.py | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/cadquery/assembly.py b/cadquery/assembly.py
index 1aac2cb3d..7c089c749 100644
--- a/cadquery/assembly.py
+++ b/cadquery/assembly.py
@@ -409,9 +409,12 @@ def constrain(self, *args, param=None):
return self
- def solve(self, verbosity: int = 0) -> "Assembly":
+ def solve(self, verbosity: int = 0, tree_initialize: bool = False) -> "Assembly":
"""
Solve the constraints.
+
+ Set `tree_initialize` to true to set the constraints by tree
+ exploration.
"""
# Get all entities and number them. First entity is marked as locked
@@ -470,6 +473,18 @@ def solve(self, verbosity: int = 0) -> "Assembly":
if len(ents) < 2:
raise ValueError("At least two entities need to be constrained")
+ if tree_initialize:
+ """
+ Set the locations
+ """
+ for obj in locked:
+ print("Locked", obj)
+
+ for ixs, pod in constraints:
+ (src, dst), t, _ = pod
+ print(f"{t}[{ixs}], {src.Coord()} <- {dst.Coord()}")
+ src.SetCoord(*dst.Coord())
+
# instantiate the solver
scale = self.toCompound().BoundingBox().DiagonalLength
solver = ConstraintSolver(locs, constraints, locked=locked, scale=scale)
From 1c2a6efbc273d4b36049552f8e7f048aa0904606 Mon Sep 17 00:00:00 2001
From: Leni Aniva
Date: Thu, 28 Aug 2025 10:57:45 -0700
Subject: [PATCH 02/10] feat: Add constraint graph
---
cadquery/assembly.py | 47 +++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 46 insertions(+), 1 deletion(-)
diff --git a/cadquery/assembly.py b/cadquery/assembly.py
index 7c089c749..98e361d53 100644
--- a/cadquery/assembly.py
+++ b/cadquery/assembly.py
@@ -78,6 +78,40 @@ def _define_grammar():
_grammar = _define_grammar()
+class ConstraintGraph:
+ """
+ Auxiliary structure for tracking constraint relations
+
+ Each constraint connects two quantities together. This is a undirected
+ graph.
+ """
+ def __init__(self):
+ self.adjlist = dict()
+ # Index of fixed objects
+ self.locked = set()
+ # Map from ordered tuples to constraints for fast solving
+ self.constraints = dict()
+
+ def add_locked(self, i):
+ self.locked.add(i)
+ def add_binary(self, i, j, pods):
+ d = self.adjlist.get(i, set())
+ d[j] = pods
+ self.adjlist[i] = d
+
+ d = self.adjlist.get(j, set())
+ d[i] = pods
+ self.adjlist[j] = d
+
+ def __str__(self):
+ def format_i(i):
+ return f"!{i}" if i in self.locked else str(i)
+ adjlist = [
+ f"{format_i(src)} -> {[format_i(j) for j in dst]}"
+ for src, dst in self.adjlist.items()
+ ]
+ return "\n".join(adjlist)
+
class Assembly(object):
"""Nested assembly of Workplane and Shape objects defining their relative positions."""
@@ -456,12 +490,19 @@ def solve(self, verbosity: int = 0, tree_initialize: bool = False) -> "Assembly"
locs = [self.objects[n].loc for n in ents]
+ cgraph = ConstraintGraph()
+
# construct the constraint mapping
constraints = []
for c in self.constraints:
ixs = tuple(ents[obj] for obj in c.objects)
pods = c.toPODs()
+ if tree_initialize:
+ if len(ixs) == 2:
+ i, j = ixs
+ cgraph.add_binary(i, j, pods)
+
for pod in pods:
constraints.append((ixs, pod))
@@ -477,8 +518,12 @@ def solve(self, verbosity: int = 0, tree_initialize: bool = False) -> "Assembly"
"""
Set the locations
"""
+
+ # Resolve locks
for obj in locked:
- print("Locked", obj)
+ cgraph.add_locked(obj)
+
+ print(cgraph)
for ixs, pod in constraints:
(src, dst), t, _ = pod
From 6df67cd4912f0970c9c079ae2cb056c3b89c91fa Mon Sep 17 00:00:00 2001
From: Leni Aniva
Date: Sat, 30 Aug 2025 17:21:20 -0700
Subject: [PATCH 03/10] feat: Constraint graph solving
---
cadquery/assembly.py | 157 ++++++++++++++++++++++++++++++++++++-------
1 file changed, 131 insertions(+), 26 deletions(-)
diff --git a/cadquery/assembly.py b/cadquery/assembly.py
index 98e361d53..80e9d43ea 100644
--- a/cadquery/assembly.py
+++ b/cadquery/assembly.py
@@ -14,6 +14,7 @@
from typing_extensions import Literal
from typish import instance_of
from uuid import uuid1 as uuid
+from enum import Enum
from .cq import Workplane
from .occ_impl.shapes import Shape, Compound
@@ -78,30 +79,88 @@ def _define_grammar():
_grammar = _define_grammar()
+
+class _Quantity(Enum):
+ POINT = 1
+ AXIS = 2
+
+ def __repr__(self):
+ match self:
+ case _Quantity.POINT: return "p"
+ case _Quantity.AXIS: return "a"
+
+
class ConstraintGraph:
"""
Auxiliary structure for tracking constraint relations
Each constraint connects two quantities together. This is a undirected
- graph.
+ graph. Moreover, each node on the graph is a pair (objid, property)
"""
- def __init__(self):
+ def __init__(self, n_objs: int):
+ self.n_objs = n_objs
self.adjlist = dict()
# Index of fixed objects
self.locked = set()
# Map from ordered tuples to constraints for fast solving
self.constraints = dict()
- def add_locked(self, i):
- self.locked.add(i)
- def add_binary(self, i, j, pods):
- d = self.adjlist.get(i, set())
- d[j] = pods
- self.adjlist[i] = d
+ def add_unary(self, i, pods):
+ """
+ Unary constraint
+ """
+ (_, kind, _) = pods
+ match kind:
+ case "Fixed":
+ self.locked.add((i, _Quantity.POINT))
+ self.locked.add((i, _Quantity.AXIS))
+ case "FixedPoint":
+ self.locked.add((i, _Quantity.POINT))
+ case "FixedAxis":
+ self.locked.add((i, _Quantity.AXIS))
+ case "FixedRotation":
+ self.locked.add((i, _Quantity.AXIS))
- d = self.adjlist.get(j, set())
- d[i] = pods
- self.adjlist[j] = d
+ def add_binary(self, i, j, pods):
+ """
+ Binary constraint
+ """
+ quantity = []
+ ((src, dst), kind, param), = pods
+
+ # Only keep the constraints with 0 degrees of freedom
+ match kind:
+ case "Axis":
+ if param and float(param) not in {0.0, 180.0}:
+ return
+ quantity = [_Quantity.AXIS]
+ case "PointInPlane":
+ return
+ case "PointOnLine":
+ return
+ case "Point":
+ if param and float(param) != 0.0:
+ return
+ quantity = [_Quantity.POINT]
+ case "Plane":
+ if param and float(param) not in {0.0, 180.0}:
+ return
+ quantity = [
+ _Quantity.POINT,
+ _Quantity.AXIS,
+ ]
+
+ for q in quantity:
+ d = self.adjlist.get((i, q), dict())
+ d[(j, q)] = pods
+ self.adjlist[(j, q)] = d
+
+ d = self.adjlist.get((j, q), dict())
+ d[(j, q)] = [
+ ((dst, src), kind, param)
+ for ((src, dst), kind, param) in pods
+ ]
+ self.adjlist[(j, q)] = d
def __str__(self):
def format_i(i):
@@ -112,6 +171,57 @@ def format_i(i):
]
return "\n".join(adjlist)
+ @staticmethod
+ def zero_point_binary_constraint(pods):
+ print("pods:", pods)
+ ((src, dst), kind, param), = pods
+ match kind:
+ case "Point":
+ dst.SetCoord(*src.Coord())
+ case "Axis":
+ if param == 0.0:
+ dst.SetCoord(*src.Coord())
+ else:
+ dst.SetCoord(*(-src).Coord())
+ case "Plane":
+ raise ValueError("not supported yet")
+ case _:
+ raise ValueError(f"Unknown binary constraint: {kind}")
+
+ def solve(self, verbosity: int = 0):
+ remaining = {
+ (i, key)
+ for i in range(self.n_objs)
+ for key in _Quantity
+ if (i, key) not in self.locked
+ }
+ def start_solve(node):
+ li = [node]
+ while li:
+ (i, qty) = li.pop()
+ if (i, qty) in remaining:
+ remaining.remove((i, qty))
+ # Query the adjlist
+ for ((j, p), pods) in self.adjlist.get((i, qty), {}).items():
+ if (j, p) in remaining:
+ li.append((j, p))
+ else:
+ continue
+
+ if verbosity > 3:
+ print(f"Solving {(i, qty)} -> {(j, p)} with {len(pods)} pods")
+ # Zero the constraint (i,qty) to (j, p)
+ ConstraintGraph.zero_point_binary_constraint(pods)
+ # Start solving from each of the locked nodes
+ if verbosity > 3:
+ print(f"Start solving from locked: {len(self.locked)}")
+ for node in self.locked:
+ start_solve(node)
+ if verbosity > 3:
+ print(f"Remaining: {len(self.locked)}")
+ while remaining:
+ node = remaining.pop()
+ start_solve(node)
class Assembly(object):
"""Nested assembly of Workplane and Shape objects defining their relative positions."""
@@ -490,7 +600,7 @@ def solve(self, verbosity: int = 0, tree_initialize: bool = False) -> "Assembly"
locs = [self.objects[n].loc for n in ents]
- cgraph = ConstraintGraph()
+ cgraph = ConstraintGraph(len(ents))
# construct the constraint mapping
constraints = []
@@ -499,9 +609,12 @@ def solve(self, verbosity: int = 0, tree_initialize: bool = False) -> "Assembly"
pods = c.toPODs()
if tree_initialize:
- if len(ixs) == 2:
- i, j = ixs
- cgraph.add_binary(i, j, pods)
+ match len(ixs):
+ case 1:
+ cgraph.add_unary(ixs[0], pods)
+ case 2:
+ i, j = ixs
+ cgraph.add_binary(i, j, pods)
for pod in pods:
constraints.append((ixs, pod))
@@ -518,17 +631,9 @@ def solve(self, verbosity: int = 0, tree_initialize: bool = False) -> "Assembly"
"""
Set the locations
"""
-
- # Resolve locks
- for obj in locked:
- cgraph.add_locked(obj)
-
- print(cgraph)
-
- for ixs, pod in constraints:
- (src, dst), t, _ = pod
- print(f"{t}[{ixs}], {src.Coord()} <- {dst.Coord()}")
- src.SetCoord(*dst.Coord())
+ if verbosity > 3:
+ print(cgraph)
+ cgraph.solve(verbosity)
# instantiate the solver
scale = self.toCompound().BoundingBox().DiagonalLength
From a27744eeceda036fa3b7526430facd224438c70a Mon Sep 17 00:00:00 2001
From: Leni Aniva
Date: Sat, 30 Aug 2025 21:51:22 -0700
Subject: [PATCH 04/10] fix: Break compound constraints
---
cadquery/assembly.py | 138 +++++++++++++++++++------------------------
1 file changed, 62 insertions(+), 76 deletions(-)
diff --git a/cadquery/assembly.py b/cadquery/assembly.py
index 80e9d43ea..47712a9d8 100644
--- a/cadquery/assembly.py
+++ b/cadquery/assembly.py
@@ -85,17 +85,17 @@ class _Quantity(Enum):
AXIS = 2
def __repr__(self):
- match self:
- case _Quantity.POINT: return "p"
- case _Quantity.AXIS: return "a"
-
+ if self == _Quantity.POINT:
+ return "p"
+ else:
+ return "a"
class ConstraintGraph:
"""
Auxiliary structure for tracking constraint relations
Each constraint connects two quantities together. This is a undirected
- graph. Moreover, each node on the graph is a pair (objid, property)
+ graph. Moreover, each node on the graph is a pair (object id, property)
"""
def __init__(self, n_objs: int):
self.n_objs = n_objs
@@ -105,62 +105,52 @@ def __init__(self, n_objs: int):
# Map from ordered tuples to constraints for fast solving
self.constraints = dict()
- def add_unary(self, i, pods):
+ def add_unary(self, i, pod):
"""
Unary constraint
"""
- (_, kind, _) = pods
- match kind:
- case "Fixed":
- self.locked.add((i, _Quantity.POINT))
- self.locked.add((i, _Quantity.AXIS))
- case "FixedPoint":
- self.locked.add((i, _Quantity.POINT))
- case "FixedAxis":
- self.locked.add((i, _Quantity.AXIS))
- case "FixedRotation":
- self.locked.add((i, _Quantity.AXIS))
+ src, kind, param = pod
+ if kind == "Fixed":
+ self.locked.add((i, _Quantity.POINT))
+ self.locked.add((i, _Quantity.AXIS))
+ elif kind == "FixedPoint":
+ src.setCoord(*param)
+ self.locked.add((i, _Quantity.POINT))
+ elif kind == "FixedAxis":
+ src.setCoord(*param)
+ self.locked.add((i, _Quantity.AXIS))
- def add_binary(self, i, j, pods):
+ def add_binary(self, i, j, pod):
"""
Binary constraint
"""
- quantity = []
- ((src, dst), kind, param), = pods
+ (src, dst), kind, param = pod
+ q = None
# Only keep the constraints with 0 degrees of freedom
- match kind:
- case "Axis":
- if param and float(param) not in {0.0, 180.0}:
- return
- quantity = [_Quantity.AXIS]
- case "PointInPlane":
+ assert kind != "Plane", "Compound constraints should not exist here"
+ if kind in {"PointInPlane", "PointOnLine"}:
+ return
+ elif kind == "Axis":
+ if param and float(param) not in {0.0, 180.0}:
return
- case "PointOnLine":
+ q = _Quantity.AXIS
+ elif kind == "Point":
+ if param and param != 0.0:
return
- case "Point":
- if param and float(param) != 0.0:
- return
- quantity = [_Quantity.POINT]
- case "Plane":
- if param and float(param) not in {0.0, 180.0}:
- return
- quantity = [
- _Quantity.POINT,
- _Quantity.AXIS,
- ]
-
- for q in quantity:
- d = self.adjlist.get((i, q), dict())
- d[(j, q)] = pods
- self.adjlist[(j, q)] = d
-
- d = self.adjlist.get((j, q), dict())
- d[(j, q)] = [
- ((dst, src), kind, param)
- for ((src, dst), kind, param) in pods
- ]
- self.adjlist[(j, q)] = d
+ q = _Quantity.POINT
+ else:
+ raise ValueError(f"Unknown kind {kind}")
+
+ # Insert into adjacency lists
+
+ d = self.adjlist.get((i, q), dict())
+ d[(j, q)] = pod
+ self.adjlist[(j, q)] = d
+
+ d = self.adjlist.get((j, q), dict())
+ d[(j, q)] = ((dst, src), kind, param)
+ self.adjlist[(j, q)] = d
def __str__(self):
def format_i(i):
@@ -172,21 +162,17 @@ def format_i(i):
return "\n".join(adjlist)
@staticmethod
- def zero_point_binary_constraint(pods):
- print("pods:", pods)
- ((src, dst), kind, param), = pods
- match kind:
- case "Point":
+ def zero_point_binary_constraint(pod):
+ (src, dst), kind, param = pod
+ if kind == "Axis":
+ if param and float(param) == 180.0:
+ dst.SetCoord(*(-src).Coord())
+ else:
dst.SetCoord(*src.Coord())
- case "Axis":
- if param == 0.0:
- dst.SetCoord(*src.Coord())
- else:
- dst.SetCoord(*(-src).Coord())
- case "Plane":
- raise ValueError("not supported yet")
- case _:
- raise ValueError(f"Unknown binary constraint: {kind}")
+ elif kind == "Point":
+ dst.SetCoord(*src.Coord())
+ else:
+ raise ValueError(f"Illegal binary constraint {kind}")
def solve(self, verbosity: int = 0):
remaining = {
@@ -202,16 +188,16 @@ def start_solve(node):
if (i, qty) in remaining:
remaining.remove((i, qty))
# Query the adjlist
- for ((j, p), pods) in self.adjlist.get((i, qty), {}).items():
+ for ((j, p), pod) in self.adjlist.get((i, qty), {}).items():
if (j, p) in remaining:
li.append((j, p))
else:
continue
if verbosity > 3:
- print(f"Solving {(i, qty)} -> {(j, p)} with {len(pods)} pods")
+ print(f"Solving {(i, qty)} -> {(j, p)} with kind {pod[1]}")
# Zero the constraint (i,qty) to (j, p)
- ConstraintGraph.zero_point_binary_constraint(pods)
+ ConstraintGraph.zero_point_binary_constraint(pod)
# Start solving from each of the locked nodes
if verbosity > 3:
print(f"Start solving from locked: {len(self.locked)}")
@@ -223,6 +209,7 @@ def start_solve(node):
node = remaining.pop()
start_solve(node)
+
class Assembly(object):
"""Nested assembly of Workplane and Shape objects defining their relative positions."""
@@ -553,7 +540,7 @@ def constrain(self, *args, param=None):
return self
- def solve(self, verbosity: int = 0, tree_initialize: bool = False) -> "Assembly":
+ def solve(self, verbosity: int = 0, tree_initialize: bool = True) -> "Assembly":
"""
Solve the constraints.
@@ -608,17 +595,16 @@ def solve(self, verbosity: int = 0, tree_initialize: bool = False) -> "Assembly"
ixs = tuple(ents[obj] for obj in c.objects)
pods = c.toPODs()
- if tree_initialize:
- match len(ixs):
- case 1:
- cgraph.add_unary(ixs[0], pods)
- case 2:
- i, j = ixs
- cgraph.add_binary(i, j, pods)
-
for pod in pods:
constraints.append((ixs, pod))
+ if tree_initialize:
+ if len(ixs) == 1:
+ cgraph.add_unary(ixs[0], pod)
+ elif len(ixs) == 2:
+ i, j = ixs
+ cgraph.add_binary(i, j, pod)
+
# check if any constraints were specified
if not constraints:
raise ValueError("At least one constraint required")
From 0839a4c184448bae0fe7dc1c8ec2fec8f8865158 Mon Sep 17 00:00:00 2001
From: Leni Aniva
Date: Sat, 30 Aug 2025 21:59:13 -0700
Subject: [PATCH 05/10] fix: Skip islands
---
cadquery/assembly.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/cadquery/assembly.py b/cadquery/assembly.py
index 47712a9d8..be7232ee7 100644
--- a/cadquery/assembly.py
+++ b/cadquery/assembly.py
@@ -177,8 +177,7 @@ def zero_point_binary_constraint(pod):
def solve(self, verbosity: int = 0):
remaining = {
(i, key)
- for i in range(self.n_objs)
- for key in _Quantity
+ for i, key in self.adjlist
if (i, key) not in self.locked
}
def start_solve(node):
@@ -204,7 +203,7 @@ def start_solve(node):
for node in self.locked:
start_solve(node)
if verbosity > 3:
- print(f"Remaining: {len(self.locked)}")
+ print(f"Remaining points: {len(remaining)}")
while remaining:
node = remaining.pop()
start_solve(node)
From 222fe4550dfd32e8afd4b58b57bad9d127a4ff9f Mon Sep 17 00:00:00 2001
From: Leni Aniva
Date: Sat, 30 Aug 2025 22:12:21 -0700
Subject: [PATCH 06/10] fix: Object sharing
---
cadquery/assembly.py | 13 ++++---------
1 file changed, 4 insertions(+), 9 deletions(-)
diff --git a/cadquery/assembly.py b/cadquery/assembly.py
index be7232ee7..8cb3285f1 100644
--- a/cadquery/assembly.py
+++ b/cadquery/assembly.py
@@ -15,6 +15,7 @@
from typish import instance_of
from uuid import uuid1 as uuid
from enum import Enum
+from collections import defaultdict
from .cq import Workplane
from .occ_impl.shapes import Shape, Compound
@@ -99,7 +100,7 @@ class ConstraintGraph:
"""
def __init__(self, n_objs: int):
self.n_objs = n_objs
- self.adjlist = dict()
+ self.adjlist = defaultdict(lambda: defaultdict(dict))
# Index of fixed objects
self.locked = set()
# Map from ordered tuples to constraints for fast solving
@@ -143,14 +144,8 @@ def add_binary(self, i, j, pod):
raise ValueError(f"Unknown kind {kind}")
# Insert into adjacency lists
-
- d = self.adjlist.get((i, q), dict())
- d[(j, q)] = pod
- self.adjlist[(j, q)] = d
-
- d = self.adjlist.get((j, q), dict())
- d[(j, q)] = ((dst, src), kind, param)
- self.adjlist[(j, q)] = d
+ self.adjlist[(i, q)][(j, q)] = pod
+ self.adjlist[(j, q)][(i, q)] = ((dst, src), kind, param)
def __str__(self):
def format_i(i):
From 90b948367767add118a8c9ff12ab7e3ff34a16c4 Mon Sep 17 00:00:00 2001
From: Leni Aniva
Date: Sat, 30 Aug 2025 22:18:13 -0700
Subject: [PATCH 07/10] chore: Format code
---
cadquery/assembly.py | 42 +++++++++++++++++++++++++-----------------
1 file changed, 25 insertions(+), 17 deletions(-)
diff --git a/cadquery/assembly.py b/cadquery/assembly.py
index 8cb3285f1..d4d658198 100644
--- a/cadquery/assembly.py
+++ b/cadquery/assembly.py
@@ -46,6 +46,7 @@
PATH_DELIM = "/"
+
# entity selector grammar definition
def _define_grammar():
@@ -91,6 +92,7 @@ def __repr__(self):
else:
return "a"
+
class ConstraintGraph:
"""
Auxiliary structure for tracking constraint relations
@@ -98,6 +100,7 @@ class ConstraintGraph:
Each constraint connects two quantities together. This is a undirected
graph. Moreover, each node on the graph is a pair (object id, property)
"""
+
def __init__(self, n_objs: int):
self.n_objs = n_objs
self.adjlist = defaultdict(lambda: defaultdict(dict))
@@ -150,6 +153,7 @@ def add_binary(self, i, j, pod):
def __str__(self):
def format_i(i):
return f"!{i}" if i in self.locked else str(i)
+
adjlist = [
f"{format_i(src)} -> {[format_i(j) for j in dst]}"
for src, dst in self.adjlist.items()
@@ -170,11 +174,8 @@ def zero_point_binary_constraint(pod):
raise ValueError(f"Illegal binary constraint {kind}")
def solve(self, verbosity: int = 0):
- remaining = {
- (i, key)
- for i, key in self.adjlist
- if (i, key) not in self.locked
- }
+ remaining = {(i, key) for i, key in self.adjlist if (i, key) not in self.locked}
+
def start_solve(node):
li = [node]
while li:
@@ -182,7 +183,7 @@ def start_solve(node):
if (i, qty) in remaining:
remaining.remove((i, qty))
# Query the adjlist
- for ((j, p), pod) in self.adjlist.get((i, qty), {}).items():
+ for (j, p), pod in self.adjlist.get((i, qty), {}).items():
if (j, p) in remaining:
li.append((j, p))
else:
@@ -192,6 +193,7 @@ def start_solve(node):
print(f"Solving {(i, qty)} -> {(j, p)} with kind {pod[1]}")
# Zero the constraint (i,qty) to (j, p)
ConstraintGraph.zero_point_binary_constraint(pod)
+
# Start solving from each of the locked nodes
if verbosity > 3:
print(f"Start solving from locked: {len(self.locked)}")
@@ -467,12 +469,12 @@ def _subloc(self, name: str) -> Tuple[Location, str]:
@overload
def constrain(
self, q1: str, q2: str, kind: ConstraintKind, param: Any = None
- ) -> "Assembly":
- ...
+ ) -> "Assembly": ...
@overload
- def constrain(self, q1: str, kind: ConstraintKind, param: Any = None) -> "Assembly":
- ...
+ def constrain(
+ self, q1: str, kind: ConstraintKind, param: Any = None
+ ) -> "Assembly": ...
@overload
def constrain(
@@ -483,14 +485,16 @@ def constrain(
s2: Shape,
kind: ConstraintKind,
param: Any = None,
- ) -> "Assembly":
- ...
+ ) -> "Assembly": ...
@overload
def constrain(
- self, id1: str, s1: Shape, kind: ConstraintKind, param: Any = None,
- ) -> "Assembly":
- ...
+ self,
+ id1: str,
+ s1: Shape,
+ kind: ConstraintKind,
+ param: Any = None,
+ ) -> "Assembly": ...
def constrain(self, *args, param=None):
"""
@@ -813,8 +817,12 @@ def __iter__(
color = self.color if self.color else color
if self.obj:
- yield self.obj if isinstance(self.obj, Shape) else Compound.makeCompound(
- s for s in self.obj.vals() if isinstance(s, Shape)
+ yield (
+ self.obj
+ if isinstance(self.obj, Shape)
+ else Compound.makeCompound(
+ s for s in self.obj.vals() if isinstance(s, Shape)
+ )
), name, loc, color
for ch in self.children:
From 0631676c87fa680adb08ee310dcc67f3bd5748ca Mon Sep 17 00:00:00 2001
From: Leni Aniva
Date: Sat, 30 Aug 2025 22:21:23 -0700
Subject: [PATCH 08/10] doc: Update documentation
---
cadquery/assembly.py | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/cadquery/assembly.py b/cadquery/assembly.py
index d4d658198..151122bc2 100644
--- a/cadquery/assembly.py
+++ b/cadquery/assembly.py
@@ -95,10 +95,11 @@ def __repr__(self):
class ConstraintGraph:
"""
- Auxiliary structure for tracking constraint relations
+ Auxiliary structure for tracking constraint relations.
- Each constraint connects two quantities together. This is a undirected
- graph. Moreover, each node on the graph is a pair (object id, property)
+ We store an undirected graph of (object id, quantity) nodes. Every binary
+ constraint with 0 degrees of freedom links two property nodes. We then
+ traverse through the graph to solve each constraint to its local minimum.
"""
def __init__(self, n_objs: int):
From cc47296bd7c50002c30d8489a3d45cb8b94be0af Mon Sep 17 00:00:00 2001
From: Leni Aniva
Date: Sat, 30 Aug 2025 22:34:52 -0700
Subject: [PATCH 09/10] fix: Handle tuples for Fixed{Point,Axis}
---
cadquery/assembly.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/cadquery/assembly.py b/cadquery/assembly.py
index 151122bc2..8ec192813 100644
--- a/cadquery/assembly.py
+++ b/cadquery/assembly.py
@@ -114,14 +114,16 @@ def add_unary(self, i, pod):
"""
Unary constraint
"""
- src, kind, param = pod
+ srcs, kind, param = pod
if kind == "Fixed":
self.locked.add((i, _Quantity.POINT))
self.locked.add((i, _Quantity.AXIS))
elif kind == "FixedPoint":
+ src, = srcs
src.setCoord(*param)
self.locked.add((i, _Quantity.POINT))
elif kind == "FixedAxis":
+ src, = srcs
src.setCoord(*param)
self.locked.add((i, _Quantity.AXIS))
From 49ce861247e03a2bff1776f0f3b4638cfd75d23e Mon Sep 17 00:00:00 2001
From: Leni Aniva
Date: Sat, 30 Aug 2025 22:57:26 -0700
Subject: [PATCH 10/10] chore: Format code
---
cadquery/assembly.py | 20 ++++++++++++--------
1 file changed, 12 insertions(+), 8 deletions(-)
diff --git a/cadquery/assembly.py b/cadquery/assembly.py
index 8ec192813..1c960c11e 100644
--- a/cadquery/assembly.py
+++ b/cadquery/assembly.py
@@ -119,12 +119,12 @@ def add_unary(self, i, pod):
self.locked.add((i, _Quantity.POINT))
self.locked.add((i, _Quantity.AXIS))
elif kind == "FixedPoint":
- src, = srcs
- src.setCoord(*param)
+ (src,) = srcs
+ src.SetCoord(*param)
self.locked.add((i, _Quantity.POINT))
elif kind == "FixedAxis":
- src, = srcs
- src.setCoord(*param)
+ (src,) = srcs
+ src.SetCoord(*param)
self.locked.add((i, _Quantity.AXIS))
def add_binary(self, i, j, pod):
@@ -472,12 +472,14 @@ def _subloc(self, name: str) -> Tuple[Location, str]:
@overload
def constrain(
self, q1: str, q2: str, kind: ConstraintKind, param: Any = None
- ) -> "Assembly": ...
+ ) -> "Assembly":
+ ...
@overload
def constrain(
self, q1: str, kind: ConstraintKind, param: Any = None
- ) -> "Assembly": ...
+ ) -> "Assembly":
+ ...
@overload
def constrain(
@@ -488,7 +490,8 @@ def constrain(
s2: Shape,
kind: ConstraintKind,
param: Any = None,
- ) -> "Assembly": ...
+ ) -> "Assembly":
+ ...
@overload
def constrain(
@@ -497,7 +500,8 @@ def constrain(
s1: Shape,
kind: ConstraintKind,
param: Any = None,
- ) -> "Assembly": ...
+ ) -> "Assembly":
+ ...
def constrain(self, *args, param=None):
"""