The haberdasher’s puzzle
Can an equilateral triangle be cut into pieces that form a square? Henry Dudeney’s answer, the haberdasher’s puzzle, has four pieces. They can be joined by three hinges into a chain that folds one way into the triangle and the other way into the square. Demaine, Kamata and Uehara proved that three pieces are not enough if they may not be turned over.
The example cuts the triangle by Dudeney’s construction, hinges the four pieces in one constraint sketch and swings them by one angle. The square must have the side \(a \cdot 3^{1/4}/2\), the area must stay the same in every position, and no two pieces may overlap on the way.

[1]:
import itertools
import math
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
import planex as px
Dudeney’s construction
The triangle \(ABC\) has the side \(a = 100\), and \(D\) and \(E\) are the midpoints of \(AB\) and \(BC\). The construction finds the side of the square first and then the cuts:
Extend \(AE\) beyond \(E\) to \(F\), with \(EF = EB\), and let \(G\) be the midpoint of \(AF\).
The circle around \(G\) through \(A\) meets the line \(EB\) beyond \(B\) in \(H\).
The circle around \(E\) through \(H\) meets \(AC\) in \(J\).
\(K\) lies on \(AC\) with \(JK = EB\), and \(L\) and \(M\) are the feet of the perpendiculars from \(D\) and \(K\) on \(EJ\).
The cuts are \(EJ\), \(DL\) and \(KM\). along and foot are the two helpers the steps need, and planex2d intersects the circles with the lines (Figure 1). Since \(H\) lies on the circle over the diameter \(AF\), \(EH^2 = AE \cdot EF\), which is the triangle’s area. So \(EH\) is the side of the square, \(a \cdot 3^{1/4}/2\), which the print compares:
[3]:
a = 100.0
A, B, C = (0.0, 0.0), (a, 0.0), (a / 2, a * math.sqrt(3) / 2)
def along(p, q, t):
"""the point a fraction t of the way from p to q"""
return (p[0] + t * (q[0] - p[0]), p[1] + t * (q[1] - p[1]))
def foot(p, q, x):
"""the foot of the perpendicular from x on the line pq"""
dx, dy = q[0] - p[0], q[1] - p[1]
return along(p, q, ((x[0] - p[0]) * dx + (x[1] - p[1]) * dy) / (dx * dx + dy * dy))
D, E = along(A, B, 0.5), along(B, C, 0.5)
F = along(A, E, 1 + math.dist(E, B) / math.dist(A, E))
G = along(A, F, 0.5)
h = px.Circle(G, math.dist(G, A)).Intersections(px.Line(E, along(E, B, 2)))[0] # E lies inside the circle
H = (h.x, h.y)
j = px.Circle(E, math.dist(E, H)).Intersections(px.Line(A, C))[0]
J = (j.x, j.y)
K = along(A, C, (math.dist(A, J) + math.dist(E, B)) / a)
L, M = foot(E, J, D), foot(E, J, K)
print("EH", math.dist(E, H), "square side", a * 3**0.25 / 2)
EH 65.80370064762461 square side 65.80370064762462
The four pieces
The three cuts split the triangle into four pieces, each named by the corners it has in the triangle, in order around it (Figure 2). Glued with the three cuts, the triangle falls into four faces, and their areas add up to the triangle’s area \(\sqrt{3}\,a^2/4\):
[5]:
pieces = {"a": [A, D, L, J], "b": [D, B, E, M, L], "c": [E, C, K, M], "d": [K, J, L, M]}
faces = {name: px.FromPolyline(corners, closed=True).Face() for name, corners in pieces.items()}
cut = px.Glue([triangle, px.Line(E, J), px.Line(D, L), px.Line(K, M)])
print(len(cut.faces), "faces of", sorted(round(f.mass, 6) for f in cut.faces))
print("pieces", sorted(round(f.mass, 6) for f in faces.values()))
print("sum", sum(f.mass for f in faces.values()), "triangle", math.sqrt(3) / 4 * a**2)
4 faces of [619.365479, 994.672877, 1014.191429, 1701.897234]
pieces [619.365479, 994.672877, 1014.191429, 1701.897234]
sum 4330.127018922193 triangle 4330.127018922193
A hinged sketch
In one constraint sketch, each piece is a closed chain of lines whose ids name the piece and the corner they start from: b1 runs from \(B\) to \(E\). RigidGroup keeps each piece’s shape, and piece a stays where it is, with \(A\) at the origin and its side on \(AB\) horizontal. The hinges are the corners that two pieces share, \(D\), where a0 ends and b0 starts, \(E\) between b1 and c0, and \(K\) between c1 and d0. At each hinge, the two
lines lie on one side of the triangle, and the angle between them, 0 in the triangle, is the hinge’s angle. Expressions turn the three hinges together, so the angle at \(D\) is the one dimension that moves them all:
[7]:
sk = px.Sketch()
for name, corners in pieces.items():
sk.Move(corners[0])
for k, p in enumerate(corners[1:] + corners[:1]):
sk.Line(p, id=f"{name}{k}")
sk.RigidGroup([f"{name}{k}" for k in range(len(corners))], id=f"piece_{name}")
sk.Constrain("a0.start", sk.origin, px.Coincident)
sk.Constrain("a0", px.Horizontal)
for end, start in (("a0.end", "b0.start"), ("b1.end", "c0.start"), ("c1.end", "d0.start")):
sk.Constrain(end, start, px.Coincident)
sk.Constrain("a0", "b0", px.Angle, 0, id="hinge_D")
sk.Constrain("b1", "c0", px.Angle, 0, id="hinge_E")
sk.Constrain("c1", "d0", px.Angle, 0, id="hinge_K")
sk.Expression("hinge_E = hinge_D")
sk.Expression("hinge_K = hinge_D")
print("status", sk.status, "| degrees of freedom", sk.dof)
status full | degrees of freedom 0
Swinging into the square
Update turns the hinge at \(D\) in steps of 5°, each step solved from the one before, and placed reads the pieces back from their solved corners. A negative angle turns clockwise. Turned this way, all three hinges swing the pieces past each other. Turned the other way, they run into each other. The area of the four pieces must stay the triangle’s in every position, and no two of them may overlap:
[8]:
def placed():
return [px.FromPolyline([sk.Location(f"{name}{k}.start") for k in range(len(corners))], closed=True).Face()
for name, corners in pieces.items()]
frames, overlap = [], 0.0
for t in range(0, 181, 5):
sk.Update({"hinge_D": -t})
frames.append(placed())
overlap = max([overlap] + [(p * q).mass for p, q in itertools.combinations(frames[-1], 2)])
areas = [sum(f.mass for f in frame) for frame in frames]
print(len(frames), "positions, area from", min(areas), "to", max(areas), "| largest overlap", overlap)
37 positions, area from 4330.127018922191 to 4330.127018922194 | largest overlap 0.0
After half a turn, the pieces fit together into one face with four corners. Its four sides have the length \(a \cdot 3^{1/4}/2\), and its two diagonals are \(\sqrt{2}\) times longer, so it is a square:
[9]:
square = px.Unify(frames[-1]).Face() # Face raises if the union has more than one face
corners = [(e.start.x, e.start.y) for e in square.outer.edges]
print("one face with", len(corners), "corners")
print("sides", [math.dist(corners[i], corners[i - 1]) for i in range(4)], "exact", a * 3**0.25 / 2)
print("diagonals", math.dist(corners[0], corners[2]), math.dist(corners[1], corners[3]), "exact",
math.sqrt(2) * a * 3**0.25 / 2)
one face with 4 corners
sides [65.80370064762464, 65.80370064762461, 65.80370064762462, 65.80370064762462] exact 65.80370064762462
diagonals 93.06048591020996 93.06048591020996 exact 93.06048591020996
[10]:
fig, ax = plt.subplots(figsize=(6, 5))
colors = ["#6fb0cf", "#d98c2b", "#0f7ea8", "#00b4d8"]
def frame(k):
ax.clear()
for face, color in zip(frames[min(k, len(frames) - 1)], colors):
face.Plot(ax=ax, face_col=color, legend=False)
ax.set_aspect("equal", adjustable="box")
ax.set_xlim(-40, 110)
ax.set_ylim(-60, 95)
ax.axis("off")
animation = FuncAnimation(fig, frame, frames=len(frames) + 12, interval=80)
plt.close(fig)
HTML(animation.to_jshtml())
[10]:
See Symmetry and patterns for RigidGroup and Expressions: design rules for dimensions that follow other dimensions.