Drawing with constraints

A constraint sketch draws with the same calls as a quick sketch: Move, Turn, Line, Arc, Circle, Spline, the profile tools and Close. Three things are added once constraints are involved:

  • flags such as px.Horizontal in a drawing call constrain the element while it is drawn;

  • the references of the drawing calls, such as tangent= or until=, can become constraints, with auto_constraint=True;

  • where a constraint has several solutions, the drawing decides which one is meant.

[1]:
import warnings
import planex as px

Flags while drawing

A flag after the numbers of a drawing call adds that constraint to the new element at once, and the sketch is solved. The example is a rectangle drawn roughly, with a fixed start, horizontal top and bottom and vertical sides (Figure 1). The solver squares it up and moves every side as little as it can. Its width and height are still free, which leaves two degrees of freedom.

[3]:
sk = px.Sketch((0, 0), px.Fix)
sk.Line(40, 1, px.Horizontal, id="bottom")
sk.Line(1, 20, px.Vertical, id="right")
sk.Line(-41, 2, px.Horizontal, id="top")
sk.Close(px.Vertical, id="left")
print(sk.status, sk.dof, "degrees of freedom")
print({i: round(sk.Geometry(i)["angle"], 9) for i in ("bottom", "right", "top", "left")})
under 2 degrees of freedom
{'bottom': 0.0, 'right': 90.0, 'top': 180.0, 'left': -90.0}
Drawn roughly, squared up by its flags
Figure 1: Drawn roughly, squared up by its flags

Helpers, with and without auto_constraint

The references of the drawing calls, such as until=, tangent=, perpendicular= and center=, place an element exactly. By default, that is all they do, and the element is afterwards as free as one drawn by eye. With auto_constraint=True, every reference also becomes the matching constraint.

The example draws the same sketch both ways: a line dropped perpendicular onto a base, a belt tangent to a hub, and a pin at the end of the base. Then the hub grows from a radius of 1 to 1.8. Without auto_constraint, the belt keeps its place and the grown hub swallows it (Figure 2a). With it, the belt stays tangent, so its distance from the hub’s center equals the new radius (Figure 2b).

[5]:
def drawing(auto):
    sk = px.Sketch(auto_constraint=auto)
    sk.Move((0, 0)).Line(4, 0, px.Fix, id="base")
    sk.Circle(6, 3, 1, id="hub")
    sk.Constrain("hub.center", px.Fix)
    sk.Constrain("hub", px.Radius, 1, id="r")
    sk.Move((1, 2)).Direction(0, -1).Line("base", perpendicular="base", id="drop")
    sk.Move((0, 3)).Line(tangent="hub", id="belt")
    sk.Circle(0.4, center="base.end", id="pin")
    return sk


plain, auto = drawing(False), drawing(True)
for kind, entries in auto.constraints.items():
    if kind not in ("Fix", "Radius"):
        print(kind, list(entries.values()))
plain["r"] = auto["r"] = 1.8
for name, sk in (("helpers only", plain), ("auto_constraint", auto)):
    print(name, "belt", sk.Measure("hub.center", to="belt")["distance"], "from the hub's center, radius", sk["r"])
Coincident ["drop.end", "base", px.Coincident, "belt.end", "hub", px.Coincident, "pin.center", "base.end", px.Coincident]
Perpendicular ["base", "drop", px.Perpendicular]
Tangent ["belt", "hub", px.Tangent]
helpers only belt 1.0 from the hub's center, radius 1.8
auto_constraint belt 1.8000000000000003 from the hub's center, radius 1.8
Helpers only, hub radius 1.8
(a) Helpers only, hub radius 1.8
auto_constraint, hub radius 1.8
(b) auto_constraint, hub radius 1.8
Figure 2: The same drawing with helpers only and with auto_constraint, after the hub grew

With auto_constraint, a constraint that a reference implies but that removes no degree of freedom, because it already follows from the others, is left out with a warning. In the example, top is horizontal by its flag, so parallel="base" adds nothing. A redundant constraint given on purpose stays. See diagnosis for how redundant constraints are reported.

[7]:
sk = px.Sketch(auto_constraint=True)
sk.Move((0, 0)).Line(3, 0, px.Horizontal, id="base")
with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    sk.Move((0, 1)).Line(2, px.Horizontal, parallel="base", id="top")
for w in caught:
    print(w.message)
auto_constraint: left out "base", "top", px.Parallel, it follows from Horizontal.1, Horizontal.2

Which tangent: the drawing decides

A line tangent to two circles has four solutions: above, below, and two that cross between the circles. The constraint takes the side from the drawing when it is created and keeps it. In the example, three lines are drawn roughly (Figure 3a), and each is made tangent to both circles (Figure 3b). When the left circle grows (Figure 3c), all three lines follow on their sides. They can still slide along themselves, so they are not fully constrained and stay black.

[8]:
sk = px.Sketch()
sk.Circle(0, 0, 1.0, id="left")
sk.Circle(4, 0.5, 0.6, px.Fix, id="right")
sk.Constrain("left.center", sk.origin, px.Coincident)
sk.Constrain("left", px.Radius, 1.0, id="r")
sk.Move((-0.3, 1.4)).Line((4.2, 1.3), id="upper")
sk.Move((0.2, -1.3)).Line((4.1, -0.3), id="lower")
sk.Move((0.1, 1.2)).Line((3.6, -0.2), id="cross")
drawn = sk.Copy()
for line in ("upper", "lower", "cross"):
    sk.Constrain(line, "left", px.Tangent)
    sk.Constrain(line, "right", px.Tangent)
tangent = sk.Copy()
sk["r"] = 1.6
for line in ("upper", "lower", "cross"):
    print(line, "distance from the left circle", sk.Measure(line, to="left")["distance"])
upper distance from the left circle 0.0
lower distance from the left circle 0.0
cross distance from the left circle 0.0
Drawn
(a) Drawn
Tangent to both
(b) Tangent to both
Left radius 1.6: all three follow
(c) Left radius 1.6: all three follow
Figure 3: Three tangents keep the side they were drawn on

See Constraints and dimensions for every kind of constraint, and Changing dimensions for how the solver follows a change.