Mechanisms: the sketch as a solver
A linkage can be modeled as a sketch. The links are lines of fixed length, the joints are shared points, and the input is an angle. Update turns the input and the solver finds the rest, moving in steps so that the linkage stays in the assembly it was drawn in. Location(ref) reads any solved point, Geometry(id) reads an element, and a reference dimension measures an output.
[1]:
import math
import planex as px
A four-bar linkage
The example is a four-bar linkage: a crank of 10 turns about a fixed pivot, and a coupler of 35 joins it to a rocker of 30 on a second pivot 40 away. The crank angle phi is the input. span, the distance from the coupler’s far end to the crank’s pivot, is a reference dimension that measures and drives nothing. Driven through a full turn in steps of 10°, the middle of the coupler traces the coupler curve.
[3]:
sk = px.Sketch()
sk.Move((0, 0), px.Fix).Line(10, 3, id="crank").Line(32, 10, id="coupler")
sk.Line(-2, -13, id="rocker")
sk.Move("crank.start", px.Coincident).Line(40, 0, id="ground", construction=True)
sk.Constrain("ground.end", px.Fix)
sk.Constrain("rocker.end", "ground.end", px.Coincident)
sk.Constrain("crank", px.Distance, 10, id="a")
sk.Constrain("coupler", px.Distance, 35, id="b")
sk.Constrain("rocker", px.Distance, 30, id="c")
sk.Constrain("ground", "crank", px.Angle, 20, id="phi")
sk.Constrain("coupler.end", "crank.start", px.Distance, id="span", reference=True)
print(sk.status, sk.dof, round(sk.condition, 2))
path, spans = [], []
for phi in range(20, 380, 10):
sk.Update({"phi": phi})
path.append(sk.Location("coupler.mid"))
spans.append(sk["span"])
full 0 2.6
For the check, the crank’s end B lies at angle φ and the rocker’s pivot D at (40, 0). The joint C lies on the circles of radius 35 about B and 30 about D, on the side the linkage was drawn in, so the span |C| has a closed form. The solver agrees with it at every angle.
[4]:
def joint(phi, a=10.0, b=35.0, c=30.0, d=40.0):
bx, by = a * math.cos(math.radians(phi)), a * math.sin(math.radians(phi))
e = math.hypot(d - bx, -by)
along = (b**2 - c**2 + e**2) / (2 * e)
up = math.sqrt(b**2 - along**2)
ux, uy = (d - bx) / e, -by / e
return bx + along * ux - up * uy, by + along * uy + up * ux
angles = range(20, 380, 10)
worst = max(abs(s - math.hypot(*joint(phi))) for phi, s in zip(angles, spans))
print("largest difference to the closed form over a full turn:", worst)
largest difference to the closed form over a full turn: 1.4210854715202004e-14
(a) The four-bar at phi 20
(b) The coupler curve of the middle
Bigger linkages
The same approach works for linkages of any size. Jansen’s leg is one sketch turned by Update, and the excavator arm and the hood hinge solve real mechanisms with reference dimensions as their outputs.