From ac03ae74ea793bc1883613b852b4e4bbc527a21b Mon Sep 17 00:00:00 2001 From: Matteo Scalia Date: Wed, 28 Jan 2026 17:59:59 +0100 Subject: [PATCH 01/13] add equal constraint and test --- cadquery/occ_impl/sketch_solver.py | 8 ++++++- tests/test_sketch.py | 37 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/cadquery/occ_impl/sketch_solver.py b/cadquery/occ_impl/sketch_solver.py index 1f4e10c50..038142385 100644 --- a/cadquery/occ_impl/sketch_solver.py +++ b/cadquery/occ_impl/sketch_solver.py @@ -30,6 +30,7 @@ "Radius", "Orientation", "ArcAngle", + "Equal", ] ConstraintInvariants = { # (arity, geometry types, param type, conversion func) @@ -47,6 +48,7 @@ "Radius": (1, ("CIRCLE",), Real, None), "Orientation": (1, ("LINE",), Tuple[Real, Real], None), "ArcAngle": (1, ("CIRCLE",), Real, radians), + "Equal": (2, ("LINE",), NoneType, None), } Constraint = Tuple[Tuple[int, Optional[int]], ConstraintKind, Optional[Any]] @@ -150,7 +152,6 @@ def angle_cost(x1, t1, x10, x2, t2, x20, val): v2 = arc_first_tangent(x2) else: raise invalid_args(t1, t2) - return v2.Angle(v1) - val @@ -219,6 +220,10 @@ def arc_angle_cost(x, t, x0, val): return rv +def equal_cost(x1, t1, x10, x2, t2, x20, val): + length1 = norm(x1[2:] - x1[:2]) if t1 == "LINE" else norm(x1[2] * x1[4]) + length2 = norm(x2[2:] - x2[:2]) if t2 == "LINE" else norm(x2[2] * x2[4]) + return length1 - length2 # dictionary of individual constraint cost functions costs: Dict[str, Callable[..., float]] = dict( @@ -231,6 +236,7 @@ def arc_angle_cost(x, t, x0, val): Radius=radius_cost, Orientation=orientation_cost, ArcAngle=arc_angle_cost, + Equal=equal_cost, ) diff --git a/tests/test_sketch.py b/tests/test_sketch.py index a75241d40..72f6910a5 100644 --- a/tests/test_sketch.py +++ b/tests/test_sketch.py @@ -745,6 +745,43 @@ def test_constraint_solver(): assert s7._faces.isValid() + w = 1.5 + s8 = ( + Sketch() + .segment((0, 0), (0, 1.88), "vsegment1") + .segment((0, 2), (w, 2), "hsegment2") + .segment((w, 1.6), (w, 0), "vsegment2") + .segment((w, 0), (0, 0), "hsegment1") + ) + + s8.constrain("vsegment1", "FixedPoint", 0) + s8.constrain("vsegment1", "hsegment2", "Coincident", None) + s8.constrain("hsegment2", "vsegment2", "Coincident", None) + s8.constrain("vsegment2", "hsegment1", "Coincident", None) + s8.constrain("hsegment1", "vsegment1", "Coincident", None) + s8.constrain("hsegment1", "vsegment1", "Angle", 90) + s8.constrain("hsegment2", "vsegment2", "Angle", 90) + + s8.constrain("vsegment1", "hsegment2", "Angle", 90) + + s8.constrain("vsegment1", "Orientation", (0, 1)) + s8.constrain("hsegment1", "vsegment1", "Equal", None) + s8.constrain("vsegment1", "Length", 2) + + s8.solve() + assert s8._solve_status["status"] == 4 + + s8.assemble() + + assert s8._faces.isValid() + + assert s8._tags["vsegment1"][0].Length() == approx(2) + assert s8._tags["hsegment1"][0].Length() == approx(2) + assert s8._tags["vsegment2"][0].Length() == approx(2) + assert s8._tags["hsegment2"][0].Length() == approx(2) + + assert s8._faces.Area() == approx(4) + def test_dxf_import(): filename = os.path.join(testdataDir, "gear.dxf") From 7cfc25667fc75633789e60fad74ea19d251fa459 Mon Sep 17 00:00:00 2001 From: Matteo Scalia Date: Wed, 28 Jan 2026 21:29:42 +0100 Subject: [PATCH 02/13] add EqualRadius constraint and broken test --- cadquery/occ_impl/sketch_solver.py | 6 +++ tests/test_sketch.py | 67 +++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/cadquery/occ_impl/sketch_solver.py b/cadquery/occ_impl/sketch_solver.py index 038142385..2f10f03f8 100644 --- a/cadquery/occ_impl/sketch_solver.py +++ b/cadquery/occ_impl/sketch_solver.py @@ -31,6 +31,7 @@ "Orientation", "ArcAngle", "Equal", + "EqualRadius", ] ConstraintInvariants = { # (arity, geometry types, param type, conversion func) @@ -49,6 +50,7 @@ "Orientation": (1, ("LINE",), Tuple[Real, Real], None), "ArcAngle": (1, ("CIRCLE",), Real, radians), "Equal": (2, ("LINE",), NoneType, None), + "EqualRadius": (2, ("CIRCLE",), NoneType, None), } Constraint = Tuple[Tuple[int, Optional[int]], ConstraintKind, Optional[Any]] @@ -225,6 +227,9 @@ def equal_cost(x1, t1, x10, x2, t2, x20, val): length2 = norm(x2[2:] - x2[:2]) if t2 == "LINE" else norm(x2[2] * x2[4]) return length1 - length2 +def equal_radius_cost(x1, t1, x10, x2, t2, x20, val): + return x1[2] - x2[2] + # dictionary of individual constraint cost functions costs: Dict[str, Callable[..., float]] = dict( Fixed=fixed_cost, @@ -237,6 +242,7 @@ def equal_cost(x1, t1, x10, x2, t2, x20, val): Orientation=orientation_cost, ArcAngle=arc_angle_cost, Equal=equal_cost, + EqualRadius=equal_radius_cost ) diff --git a/tests/test_sketch.py b/tests/test_sketch.py index 72f6910a5..744ba973d 100644 --- a/tests/test_sketch.py +++ b/tests/test_sketch.py @@ -2,7 +2,7 @@ from cadquery.sketch import Sketch, Vector, Location from cadquery.selectors import LengthNthSelector -from cadquery import Edge, Vertex +from cadquery import Edge, Vertex, exporters from pytest import approx, raises, fixture from math import pi, sqrt @@ -782,6 +782,71 @@ def test_constraint_solver(): assert s8._faces.Area() == approx(4) + s9 = ( + Sketch() + .segment((1, 0), (9, 0), "segment1") + .arc((9, 0.1), (10, 1), (9, 2), "arc1") + .segment((10, 1), (10, 3.9), "segment2") + .arc((10, 4), (9, 5), (8, 4), "arc2") + .segment((9, 5), (1, 5), "segment3") + .arc((1, 5), (0, 4.4), (1, 2.5), "arc3") + .segment((0, 4), (0.3, 1.1), "segment4") + .arc((0, 1), (1, 0), (2, 1), "arc4") + ) + + s9.constrain("segment1", "Orientation", (1, 0)) + s9.constrain("segment1", "FixedPoint", 0) + + s9.constrain("segment1", "arc1", "Coincident", None) + s9.constrain("arc1", "segment2", "Coincident", None) + s9.constrain("segment2", "arc2", "Coincident", None) + s9.constrain("arc2", "segment3", "Coincident", None) + s9.constrain("segment3", "arc3", "Coincident", None) + s9.constrain("arc3", "segment4", "Coincident", None) + s9.constrain("segment4", "arc4", "Coincident", None) + s9.constrain("arc4", "segment1", "Coincident", None) + + + s9.constrain("segment1", "arc1", "Angle", 0) + s9.constrain("arc1", "segment2", "Angle", 0) + s9.constrain("segment2", "arc2", "Angle", 0) + s9.constrain("arc2", "segment3", "Angle", 0) + s9.constrain("segment3", "arc3", "Angle", 0) + s9.constrain("arc3", "segment4", "Angle", 0) + s9.constrain("segment4", "arc4", "Angle", 0) + s9.constrain("arc4", "segment1", "Angle", 0) + + s9.constrain("segment1", "segment3", "Angle", 180) + s9.constrain("segment2", "segment4", "Angle", 180) + s9.constrain("segment2", "segment1", "Angle", 90) + + s9.constrain("arc1", "Radius", 1) + s9.constrain("segment1", "Length", 8) + s9.constrain("segment2", "Length", 3) + + s9.constrain("arc1", "arc2", "EqualRadius", None) + s9.constrain("arc2", "arc3", "EqualRadius", None) + s9.constrain("arc3", "arc4", "EqualRadius", None) + + s9.solve() + assert s9._solve_status["status"] == 4 + + s9.assemble() + + exporters.exportDXF(s9, '/tmp/s9.dxf') + assert s9._faces.isValid() + + assert s9._tags["segment1"][0].Length() == approx(8) + assert s9._tags["segment3"][0].Length() == approx(8) + assert s9._tags["segment2"][0].Length() == approx(3) + assert s9._tags["segment4"][0].Length() == approx(3) + assert s9._tags["arc1"][0].radius() == approx(1) + assert s9._tags["arc2"][0].radius() == approx(1) + assert s9._tags["arc3"][0].radius() == approx(1) + assert s9._tags["arc4"][0].radius() == approx(1) + + + def test_dxf_import(): filename = os.path.join(testdataDir, "gear.dxf") From 91c46aa83765db5f209ba66ecf13976db288372a Mon Sep 17 00:00:00 2001 From: Matteo Scalia Date: Thu, 29 Jan 2026 10:33:48 +0100 Subject: [PATCH 03/13] remove debug exporter --- tests/test_sketch.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_sketch.py b/tests/test_sketch.py index 744ba973d..9023f6438 100644 --- a/tests/test_sketch.py +++ b/tests/test_sketch.py @@ -2,7 +2,7 @@ from cadquery.sketch import Sketch, Vector, Location from cadquery.selectors import LengthNthSelector -from cadquery import Edge, Vertex, exporters +from cadquery import Edge, Vertex from pytest import approx, raises, fixture from math import pi, sqrt @@ -833,7 +833,6 @@ def test_constraint_solver(): s9.assemble() - exporters.exportDXF(s9, '/tmp/s9.dxf') assert s9._faces.isValid() assert s9._tags["segment1"][0].Length() == approx(8) From f57f47071102d2a71f998bfe157ae726a88efb46 Mon Sep 17 00:00:00 2001 From: Matteo Scalia Date: Thu, 29 Jan 2026 10:46:41 +0100 Subject: [PATCH 04/13] wrap around to mimimize angular differences across boundary --- cadquery/occ_impl/sketch_solver.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cadquery/occ_impl/sketch_solver.py b/cadquery/occ_impl/sketch_solver.py index 2f10f03f8..473d4f338 100644 --- a/cadquery/occ_impl/sketch_solver.py +++ b/cadquery/occ_impl/sketch_solver.py @@ -5,7 +5,7 @@ from itertools import accumulate, chain from math import sin, cos, radians -from numpy import array, full, inf, sign +from numpy import array, full, inf, sign, pi from numpy.linalg import norm import nlopt @@ -154,7 +154,8 @@ def angle_cost(x1, t1, x10, x2, t2, x20, val): v2 = arc_first_tangent(x2) else: raise invalid_args(t1, t2) - return v2.Angle(v1) - val + angle = v2.Angle(v1) - val + return (angle + pi) % (2 * pi) - pi def length_cost(x, t, x0, val): From 81dae9bc2c4f9de5265ff802398e3ebf208900db Mon Sep 17 00:00:00 2001 From: Matteo Scalia Date: Thu, 29 Jan 2026 11:08:58 +0100 Subject: [PATCH 05/13] document Equal and EqualRadius --- doc/sketch.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/sketch.rst b/doc/sketch.rst index 6dcc84b7f..d6d5296a2 100644 --- a/doc/sketch.rst +++ b/doc/sketch.rst @@ -187,6 +187,16 @@ Following constraints are implemented. Arguments are passed in as one tuple in : - Arc - `angle` - Specified entity is fixed angular span + * - Equal + - 2 + - Line + - None + - Specified lines have equal length + * - EqualRadius + - 2 + - Arc + - None + - Specified arcs have equal radius Workplane integration From 364650f033058b6d172a8eb216aa451134fb7b7a Mon Sep 17 00:00:00 2001 From: Matteo Scalia Date: Thu, 29 Jan 2026 12:35:32 +0100 Subject: [PATCH 06/13] remove unused else branches --- cadquery/occ_impl/sketch_solver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cadquery/occ_impl/sketch_solver.py b/cadquery/occ_impl/sketch_solver.py index 473d4f338..f35fcb0a2 100644 --- a/cadquery/occ_impl/sketch_solver.py +++ b/cadquery/occ_impl/sketch_solver.py @@ -224,8 +224,8 @@ def arc_angle_cost(x, t, x0, val): return rv def equal_cost(x1, t1, x10, x2, t2, x20, val): - length1 = norm(x1[2:] - x1[:2]) if t1 == "LINE" else norm(x1[2] * x1[4]) - length2 = norm(x2[2:] - x2[:2]) if t2 == "LINE" else norm(x2[2] * x2[4]) + length1 = norm(x1[2:] - x1[:2]) + length2 = norm(x2[2:] - x2[:2]) return length1 - length2 def equal_radius_cost(x1, t1, x10, x2, t2, x20, val): From 1039b3953279491ea15b068bef4e226f92d3af4c Mon Sep 17 00:00:00 2001 From: Matteo Scalia Date: Fri, 30 Jan 2026 23:06:23 +0100 Subject: [PATCH 07/13] Revert "wrap around to mimimize angular differences across boundary" This reverts commit f57f47071102d2a71f998bfe157ae726a88efb46. --- cadquery/occ_impl/sketch_solver.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cadquery/occ_impl/sketch_solver.py b/cadquery/occ_impl/sketch_solver.py index f35fcb0a2..959ed5bdb 100644 --- a/cadquery/occ_impl/sketch_solver.py +++ b/cadquery/occ_impl/sketch_solver.py @@ -5,7 +5,7 @@ from itertools import accumulate, chain from math import sin, cos, radians -from numpy import array, full, inf, sign, pi +from numpy import array, full, inf, sign from numpy.linalg import norm import nlopt @@ -154,8 +154,7 @@ def angle_cost(x1, t1, x10, x2, t2, x20, val): v2 = arc_first_tangent(x2) else: raise invalid_args(t1, t2) - angle = v2.Angle(v1) - val - return (angle + pi) % (2 * pi) - pi + return v2.Angle(v1) - val def length_cost(x, t, x0, val): From cac34526e6c7729d07a617071c27347e5de49e19 Mon Sep 17 00:00:00 2001 From: Matteo Scalia Date: Sat, 31 Jan 2026 13:42:59 +0100 Subject: [PATCH 08/13] add more helpful tags fix s9 test --- tests/test_sketch.py | 129 +++++++++++++++++++++---------------------- 1 file changed, 63 insertions(+), 66 deletions(-) diff --git a/tests/test_sketch.py b/tests/test_sketch.py index 9023f6438..fcd075116 100644 --- a/tests/test_sketch.py +++ b/tests/test_sketch.py @@ -748,25 +748,25 @@ def test_constraint_solver(): w = 1.5 s8 = ( Sketch() - .segment((0, 0), (0, 1.88), "vsegment1") - .segment((0, 2), (w, 2), "hsegment2") - .segment((w, 1.6), (w, 0), "vsegment2") - .segment((w, 0), (0, 0), "hsegment1") + .segment((0, 0), (0, 1.88), "left") + .segment((0, 2), (w, 2), "top") + .segment((w, 1.6), (w, 0), "right") + .segment((w, 0), (0, 0), "bottom") ) - s8.constrain("vsegment1", "FixedPoint", 0) - s8.constrain("vsegment1", "hsegment2", "Coincident", None) - s8.constrain("hsegment2", "vsegment2", "Coincident", None) - s8.constrain("vsegment2", "hsegment1", "Coincident", None) - s8.constrain("hsegment1", "vsegment1", "Coincident", None) - s8.constrain("hsegment1", "vsegment1", "Angle", 90) - s8.constrain("hsegment2", "vsegment2", "Angle", 90) + s8.constrain("left", "FixedPoint", 0) + s8.constrain("left", "top", "Coincident", None) + s8.constrain("top", "right", "Coincident", None) + s8.constrain("right", "bottom", "Coincident", None) + s8.constrain("bottom", "left", "Coincident", None) + s8.constrain("left", "bottom", "Angle", 90) + s8.constrain("right", "top", "Angle", 90) - s8.constrain("vsegment1", "hsegment2", "Angle", 90) + s8.constrain("top", "left", "Angle", 90) - s8.constrain("vsegment1", "Orientation", (0, 1)) - s8.constrain("hsegment1", "vsegment1", "Equal", None) - s8.constrain("vsegment1", "Length", 2) + s8.constrain("left", "Orientation", (0, 1)) + s8.constrain("bottom", "left", "Equal", None) + s8.constrain("left", "Length", 2) s8.solve() assert s8._solve_status["status"] == 4 @@ -775,74 +775,71 @@ def test_constraint_solver(): assert s8._faces.isValid() - assert s8._tags["vsegment1"][0].Length() == approx(2) - assert s8._tags["hsegment1"][0].Length() == approx(2) - assert s8._tags["vsegment2"][0].Length() == approx(2) - assert s8._tags["hsegment2"][0].Length() == approx(2) + assert s8._tags["left"][0].Length() == approx(2) + assert s8._tags["bottom"][0].Length() == approx(2) + assert s8._tags["right"][0].Length() == approx(2) + assert s8._tags["top"][0].Length() == approx(2) assert s8._faces.Area() == approx(4) s9 = ( Sketch() - .segment((1, 0), (9, 0), "segment1") - .arc((9, 0.1), (10, 1), (9, 2), "arc1") - .segment((10, 1), (10, 3.9), "segment2") - .arc((10, 4), (9, 5), (8, 4), "arc2") - .segment((9, 5), (1, 5), "segment3") - .arc((1, 5), (0, 4.4), (1, 2.5), "arc3") - .segment((0, 4), (0.3, 1.1), "segment4") - .arc((0, 1), (1, 0), (2, 1), "arc4") + .segment((1, 0), (9, 0), "bottom") + .arc((9, 1), 1.1, -90, 90, "bottom_right") + .segment((10, 1), (10, 3.9), "right") + .arc((9, 4), 1, 0, 90, "top_right") + .segment((9, 5), (1, 5), "top") + .arc((1, 4), 1, 90, 90, "top_left") + .segment((0, 4), (0.3, 1.1), "left") + .arc((1, 1), 1, 180, 90, "bottom_left") ) - s9.constrain("segment1", "Orientation", (1, 0)) - s9.constrain("segment1", "FixedPoint", 0) - - s9.constrain("segment1", "arc1", "Coincident", None) - s9.constrain("arc1", "segment2", "Coincident", None) - s9.constrain("segment2", "arc2", "Coincident", None) - s9.constrain("arc2", "segment3", "Coincident", None) - s9.constrain("segment3", "arc3", "Coincident", None) - s9.constrain("arc3", "segment4", "Coincident", None) - s9.constrain("segment4", "arc4", "Coincident", None) - s9.constrain("arc4", "segment1", "Coincident", None) - - - s9.constrain("segment1", "arc1", "Angle", 0) - s9.constrain("arc1", "segment2", "Angle", 0) - s9.constrain("segment2", "arc2", "Angle", 0) - s9.constrain("arc2", "segment3", "Angle", 0) - s9.constrain("segment3", "arc3", "Angle", 0) - s9.constrain("arc3", "segment4", "Angle", 0) - s9.constrain("segment4", "arc4", "Angle", 0) - s9.constrain("arc4", "segment1", "Angle", 0) + s9.constrain("bottom", "Orientation", (1, 0)) + s9.constrain("bottom", "FixedPoint", 0) + + s9.constrain("bottom", "bottom_right", "Coincident", None) + s9.constrain("bottom_right", "right", "Coincident", None) + s9.constrain("right", "top_right", "Coincident", None) + s9.constrain("top_right", "top", "Coincident", None) + s9.constrain("top", "top_left", "Coincident", None) + s9.constrain("top_left", "left", "Coincident", None) + s9.constrain("left", "bottom_left", "Coincident", None) + s9.constrain("bottom_left", "bottom", "Coincident", None) + + + s9.constrain("bottom", "bottom_right", "Angle", 0) + s9.constrain("bottom_right", "right", "Angle", 0) + s9.constrain("right", "top_right", "Angle", 0) + s9.constrain("top_right", "top", "Angle", 0) + s9.constrain("top", "top_left", "Angle", 0) + s9.constrain("top_left", "left", "Angle", 0) + s9.constrain("left", "bottom_left", "Angle", 0) - s9.constrain("segment1", "segment3", "Angle", 180) - s9.constrain("segment2", "segment4", "Angle", 180) - s9.constrain("segment2", "segment1", "Angle", 90) + s9.constrain("bottom", "top", "Equal", None) + s9.constrain("right", "left", "Equal", None) - s9.constrain("arc1", "Radius", 1) - s9.constrain("segment1", "Length", 8) - s9.constrain("segment2", "Length", 3) + s9.constrain("bottom_right", "Radius", 1) + s9.constrain("bottom", "Length", 8) + s9.constrain("right", "Length", 3) - s9.constrain("arc1", "arc2", "EqualRadius", None) - s9.constrain("arc2", "arc3", "EqualRadius", None) - s9.constrain("arc3", "arc4", "EqualRadius", None) + s9.constrain("bottom_right", "top_right", "EqualRadius", None) + s9.constrain("top_right", "top_left", "EqualRadius", None) + s9.constrain("top_left", "bottom_left", "EqualRadius", None) s9.solve() assert s9._solve_status["status"] == 4 - s9.assemble() assert s9._faces.isValid() - assert s9._tags["segment1"][0].Length() == approx(8) - assert s9._tags["segment3"][0].Length() == approx(8) - assert s9._tags["segment2"][0].Length() == approx(3) - assert s9._tags["segment4"][0].Length() == approx(3) - assert s9._tags["arc1"][0].radius() == approx(1) - assert s9._tags["arc2"][0].radius() == approx(1) - assert s9._tags["arc3"][0].radius() == approx(1) - assert s9._tags["arc4"][0].radius() == approx(1) + assert s9._tags["bottom"][0].Length() == approx(8) + assert s9._tags["top"][0].Length() == approx(8) + assert s9._tags["right"][0].Length() == approx(3) + assert s9._tags["left"][0].Length() == approx(3) + assert s9._tags["bottom_right"][0].radius() == approx(1) + assert s9._tags["top_right"][0].radius() == approx(1) + assert s9._tags["top_left"][0].radius() == approx(1) + assert s9._tags["bottom_left"][0].radius() == approx(1) From 5174cfc98b20743c062c0170c68cbbeb923635af Mon Sep 17 00:00:00 2001 From: Matteo Scalia Date: Sun, 1 Feb 2026 14:10:59 +0100 Subject: [PATCH 09/13] move Equal and EqualRadius constraint tests to a separate function --- tests/test_sketch.py | 137 +++++++++++++++++++++---------------------- 1 file changed, 68 insertions(+), 69 deletions(-) diff --git a/tests/test_sketch.py b/tests/test_sketch.py index fcd075116..75cf028ef 100644 --- a/tests/test_sketch.py +++ b/tests/test_sketch.py @@ -745,8 +745,9 @@ def test_constraint_solver(): assert s7._faces.isValid() +def test_equal_constraints(): w = 1.5 - s8 = ( + s1 = ( Sketch() .segment((0, 0), (0, 1.88), "left") .segment((0, 2), (w, 2), "top") @@ -754,35 +755,35 @@ def test_constraint_solver(): .segment((w, 0), (0, 0), "bottom") ) - s8.constrain("left", "FixedPoint", 0) - s8.constrain("left", "top", "Coincident", None) - s8.constrain("top", "right", "Coincident", None) - s8.constrain("right", "bottom", "Coincident", None) - s8.constrain("bottom", "left", "Coincident", None) - s8.constrain("left", "bottom", "Angle", 90) - s8.constrain("right", "top", "Angle", 90) + s1.constrain("left", "FixedPoint", 0) + s1.constrain("left", "top", "Coincident", None) + s1.constrain("top", "right", "Coincident", None) + s1.constrain("right", "bottom", "Coincident", None) + s1.constrain("bottom", "left", "Coincident", None) + s1.constrain("left", "bottom", "Angle", 90) + s1.constrain("right", "top", "Angle", 90) + + s1.constrain("top", "left", "Angle", 90) - s8.constrain("top", "left", "Angle", 90) + s1.constrain("left", "Orientation", (0, 1)) + s1.constrain("bottom", "left", "Equal", None) + s1.constrain("left", "Length", 2) - s8.constrain("left", "Orientation", (0, 1)) - s8.constrain("bottom", "left", "Equal", None) - s8.constrain("left", "Length", 2) - - s8.solve() - assert s8._solve_status["status"] == 4 + s1.solve() + assert s1._solve_status["status"] == 4 - s8.assemble() + s1.assemble() - assert s8._faces.isValid() + assert s1._faces.isValid() - assert s8._tags["left"][0].Length() == approx(2) - assert s8._tags["bottom"][0].Length() == approx(2) - assert s8._tags["right"][0].Length() == approx(2) - assert s8._tags["top"][0].Length() == approx(2) + assert s1._tags["left"][0].Length() == approx(2) + assert s1._tags["bottom"][0].Length() == approx(2) + assert s1._tags["right"][0].Length() == approx(2) + assert s1._tags["top"][0].Length() == approx(2) - assert s8._faces.Area() == approx(4) + assert s1._faces.Area() == approx(4) - s9 = ( + s2 = ( Sketch() .segment((1, 0), (9, 0), "bottom") .arc((9, 1), 1.1, -90, 90, "bottom_right") @@ -794,53 +795,51 @@ def test_constraint_solver(): .arc((1, 1), 1, 180, 90, "bottom_left") ) - s9.constrain("bottom", "Orientation", (1, 0)) - s9.constrain("bottom", "FixedPoint", 0) - - s9.constrain("bottom", "bottom_right", "Coincident", None) - s9.constrain("bottom_right", "right", "Coincident", None) - s9.constrain("right", "top_right", "Coincident", None) - s9.constrain("top_right", "top", "Coincident", None) - s9.constrain("top", "top_left", "Coincident", None) - s9.constrain("top_left", "left", "Coincident", None) - s9.constrain("left", "bottom_left", "Coincident", None) - s9.constrain("bottom_left", "bottom", "Coincident", None) - - - s9.constrain("bottom", "bottom_right", "Angle", 0) - s9.constrain("bottom_right", "right", "Angle", 0) - s9.constrain("right", "top_right", "Angle", 0) - s9.constrain("top_right", "top", "Angle", 0) - s9.constrain("top", "top_left", "Angle", 0) - s9.constrain("top_left", "left", "Angle", 0) - s9.constrain("left", "bottom_left", "Angle", 0) - - s9.constrain("bottom", "top", "Equal", None) - s9.constrain("right", "left", "Equal", None) - - s9.constrain("bottom_right", "Radius", 1) - s9.constrain("bottom", "Length", 8) - s9.constrain("right", "Length", 3) - - s9.constrain("bottom_right", "top_right", "EqualRadius", None) - s9.constrain("top_right", "top_left", "EqualRadius", None) - s9.constrain("top_left", "bottom_left", "EqualRadius", None) - - s9.solve() - assert s9._solve_status["status"] == 4 - s9.assemble() - - assert s9._faces.isValid() - - assert s9._tags["bottom"][0].Length() == approx(8) - assert s9._tags["top"][0].Length() == approx(8) - assert s9._tags["right"][0].Length() == approx(3) - assert s9._tags["left"][0].Length() == approx(3) - assert s9._tags["bottom_right"][0].radius() == approx(1) - assert s9._tags["top_right"][0].radius() == approx(1) - assert s9._tags["top_left"][0].radius() == approx(1) - assert s9._tags["bottom_left"][0].radius() == approx(1) + s2.constrain("bottom", "Orientation", (1, 0)) + s2.constrain("bottom", "FixedPoint", 0) + + s2.constrain("bottom", "bottom_right", "Coincident", None) + s2.constrain("bottom_right", "right", "Coincident", None) + s2.constrain("right", "top_right", "Coincident", None) + s2.constrain("top_right", "top", "Coincident", None) + s2.constrain("top", "top_left", "Coincident", None) + s2.constrain("top_left", "left", "Coincident", None) + s2.constrain("left", "bottom_left", "Coincident", None) + s2.constrain("bottom_left", "bottom", "Coincident", None) + + s2.constrain("bottom", "bottom_right", "Angle", 0) + s2.constrain("bottom_right", "right", "Angle", 0) + s2.constrain("right", "top_right", "Angle", 0) + s2.constrain("top_right", "top", "Angle", 0) + s2.constrain("top", "top_left", "Angle", 0) + s2.constrain("top_left", "left", "Angle", 0) + s2.constrain("left", "bottom_left", "Angle", 0) + + s2.constrain("bottom", "top", "Equal", None) + s2.constrain("right", "left", "Equal", None) + + s2.constrain("bottom_right", "Radius", 1) + s2.constrain("bottom", "Length", 8) + s2.constrain("right", "Length", 3) + + s2.constrain("bottom_right", "top_right", "EqualRadius", None) + s2.constrain("top_right", "top_left", "EqualRadius", None) + s2.constrain("top_left", "bottom_left", "EqualRadius", None) + + s2.solve() + assert s2._solve_status["status"] == 4 + s2.assemble() + + assert s2._faces.isValid() + assert s2._tags["bottom"][0].Length() == approx(8) + assert s2._tags["top"][0].Length() == approx(8) + assert s2._tags["right"][0].Length() == approx(3) + assert s2._tags["left"][0].Length() == approx(3) + assert s2._tags["bottom_right"][0].radius() == approx(1) + assert s2._tags["top_right"][0].radius() == approx(1) + assert s2._tags["top_left"][0].radius() == approx(1) + assert s2._tags["bottom_left"][0].radius() == approx(1) def test_dxf_import(): From f27bf471e06665da0d2f913636a87ecf9e2e3dd2 Mon Sep 17 00:00:00 2001 From: Matteo Scalia Date: Sun, 1 Feb 2026 14:11:40 +0100 Subject: [PATCH 10/13] black format --- cadquery/occ_impl/sketch_solver.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cadquery/occ_impl/sketch_solver.py b/cadquery/occ_impl/sketch_solver.py index 959ed5bdb..a2610e595 100644 --- a/cadquery/occ_impl/sketch_solver.py +++ b/cadquery/occ_impl/sketch_solver.py @@ -222,14 +222,17 @@ def arc_angle_cost(x, t, x0, val): return rv + def equal_cost(x1, t1, x10, x2, t2, x20, val): length1 = norm(x1[2:] - x1[:2]) length2 = norm(x2[2:] - x2[:2]) return length1 - length2 + def equal_radius_cost(x1, t1, x10, x2, t2, x20, val): return x1[2] - x2[2] + # dictionary of individual constraint cost functions costs: Dict[str, Callable[..., float]] = dict( Fixed=fixed_cost, @@ -242,7 +245,7 @@ def equal_radius_cost(x1, t1, x10, x2, t2, x20, val): Orientation=orientation_cost, ArcAngle=arc_angle_cost, Equal=equal_cost, - EqualRadius=equal_radius_cost + EqualRadius=equal_radius_cost, ) From 8ececd552ad3e88b9d81c465eea92798445ee530 Mon Sep 17 00:00:00 2001 From: Matteo Scalia Date: Mon, 9 Feb 2026 14:11:09 +0100 Subject: [PATCH 11/13] add circle equal constraint --- cadquery/occ_impl/sketch_solver.py | 12 +++++++++--- tests/test_sketch.py | 13 +++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/cadquery/occ_impl/sketch_solver.py b/cadquery/occ_impl/sketch_solver.py index a2610e595..fec4043ea 100644 --- a/cadquery/occ_impl/sketch_solver.py +++ b/cadquery/occ_impl/sketch_solver.py @@ -49,7 +49,7 @@ "Radius": (1, ("CIRCLE",), Real, None), "Orientation": (1, ("LINE",), Tuple[Real, Real], None), "ArcAngle": (1, ("CIRCLE",), Real, radians), - "Equal": (2, ("LINE",), NoneType, None), + "Equal": (2, ("LINE", "CIRCLE"), NoneType, None), "EqualRadius": (2, ("CIRCLE",), NoneType, None), } @@ -224,8 +224,14 @@ def arc_angle_cost(x, t, x0, val): def equal_cost(x1, t1, x10, x2, t2, x20, val): - length1 = norm(x1[2:] - x1[:2]) - length2 = norm(x2[2:] - x2[:2]) + if t1 == "LINE": + length1 = norm(x1[2:] - x1[:2]) + elif t1 == "CIRCLE": + length1 = norm(x1[2] * x1[4]) + if t2 == "LINE": + length2 = norm(x2[2:] - x2[:2]) + elif t2 == "CIRCLE": + length2 = norm(x2[2] * x2[4]) return length1 - length2 diff --git a/tests/test_sketch.py b/tests/test_sketch.py index 75cf028ef..5278f963b 100644 --- a/tests/test_sketch.py +++ b/tests/test_sketch.py @@ -841,6 +841,19 @@ def test_equal_constraints(): assert s2._tags["top_left"][0].radius() == approx(1) assert s2._tags["bottom_left"][0].radius() == approx(1) + s3 = ( + Sketch() + .segment((-1, 0), (1, 0), "segment") + .arc((0, 0), 0.8, 0, 180, "arc") + ) + s3.constrain("segment", "Fixed", None) + s3.constrain("arc", "FixedPoint", None) + s3.constrain("arc", "ArcAngle", 180) + s3.constrain("arc", "segment", "Equal", None) + s3.solve() + assert s3._solve_status["status"] == 4 + assert s3._tags["arc"][0].Length() == approx(2) + def test_dxf_import(): From 5df75eb037c613032a96a4bf56152da0bf956344 Mon Sep 17 00:00:00 2001 From: Matteo Scalia Date: Mon, 9 Feb 2026 14:12:49 +0100 Subject: [PATCH 12/13] add circle equal constraint to docs --- doc/sketch.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sketch.rst b/doc/sketch.rst index d6d5296a2..4bc1e32fb 100644 --- a/doc/sketch.rst +++ b/doc/sketch.rst @@ -189,7 +189,7 @@ Following constraints are implemented. Arguments are passed in as one tuple in : - Specified entity is fixed angular span * - Equal - 2 - - Line + - All - None - Specified lines have equal length * - EqualRadius From ed616e139d65d5a2dd030915b6d7eb911c3670ae Mon Sep 17 00:00:00 2001 From: Matteo Scalia Date: Mon, 9 Feb 2026 17:59:19 +0100 Subject: [PATCH 13/13] black fix --- tests/test_sketch.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_sketch.py b/tests/test_sketch.py index 5278f963b..845bb506c 100644 --- a/tests/test_sketch.py +++ b/tests/test_sketch.py @@ -841,11 +841,7 @@ def test_equal_constraints(): assert s2._tags["top_left"][0].radius() == approx(1) assert s2._tags["bottom_left"][0].radius() == approx(1) - s3 = ( - Sketch() - .segment((-1, 0), (1, 0), "segment") - .arc((0, 0), 0.8, 0, 180, "arc") - ) + s3 = Sketch().segment((-1, 0), (1, 0), "segment").arc((0, 0), 0.8, 0, 180, "arc") s3.constrain("segment", "Fixed", None) s3.constrain("arc", "FixedPoint", None) s3.constrain("arc", "ArcAngle", 180)