diff --git a/cadquery/occ_impl/sketch_solver.py b/cadquery/occ_impl/sketch_solver.py index 1f4e10c50..a2610e595 100644 --- a/cadquery/occ_impl/sketch_solver.py +++ b/cadquery/occ_impl/sketch_solver.py @@ -30,6 +30,8 @@ "Radius", "Orientation", "ArcAngle", + "Equal", + "EqualRadius", ] ConstraintInvariants = { # (arity, geometry types, param type, conversion func) @@ -47,6 +49,8 @@ "Radius": (1, ("CIRCLE",), Real, None), "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]] @@ -150,7 +154,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 @@ -220,6 +223,16 @@ 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, @@ -231,6 +244,8 @@ def arc_angle_cost(x, t, x0, val): Radius=radius_cost, Orientation=orientation_cost, ArcAngle=arc_angle_cost, + Equal=equal_cost, + EqualRadius=equal_radius_cost, ) 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 diff --git a/tests/test_sketch.py b/tests/test_sketch.py index a75241d40..75cf028ef 100644 --- a/tests/test_sketch.py +++ b/tests/test_sketch.py @@ -745,6 +745,103 @@ def test_constraint_solver(): assert s7._faces.isValid() +def test_equal_constraints(): + w = 1.5 + s1 = ( + Sketch() + .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") + ) + + 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) + + s1.constrain("left", "Orientation", (0, 1)) + s1.constrain("bottom", "left", "Equal", None) + s1.constrain("left", "Length", 2) + + s1.solve() + assert s1._solve_status["status"] == 4 + + s1.assemble() + + assert s1._faces.isValid() + + 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 s1._faces.Area() == approx(4) + + s2 = ( + Sketch() + .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") + ) + + 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(): filename = os.path.join(testdataDir, "gear.dxf")