Files
sandTable/docs/compose_guide.py
T
ckochandClaude Opus 5.5 f9b4c08d60 Point local work folders at ~/Documents/projects/sandTable-local
The KiCad working copy and the Pi image work dir moved out of ~/kicad and
~/Documents into ~/Documents/projects/sandTable-local.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 10:48:07 -05:00

143 lines
6.3 KiB
Python

from PIL import Image, ImageDraw, ImageFont
SRC = '/home/ckoch/ComfyUI/output/sandtable/step{}_00001_.png'
OUT = '/home/ckoch/Documents/projects/sandTable/docs/build-guide.png'
F = '/usr/share/fonts/truetype/noto/NotoSans-{}.ttf'
MONO = '/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf'
INK, GRAY, TEAL, RULE = (27, 36, 32), (90, 103, 98), (31, 128, 116), (214, 222, 218)
STEPS = [
("Shell and deck",
["Join the 4 shell quarters at the seams.",
"Screw the 4 deck quarters onto the wall ledge. Deck Q1 (controller standoffs) goes under shell Q1 (cable notch)."],
"8x M3x16 + nuts · 8x M3x8 button"),
("Center plate and hub",
["Screw the center plate onto the deck.",
"Press the two 608 bearings into the housing, one from each side, and drop the hub shaft through."],
"8x M3x8 button · 2x 608"),
("Slip ring (seen from below)",
["Glue the adapter onto the bottom of the shaft, pressing up on the lower bearing: no play, still spins freely.",
"Push the printed coupler's 3 pegs into the adapter's holes (drop of CA); its slot slides over the top wires.",
"Only the slip ring's center boss turns: press it into the coupler's socket with a dab of gel CA. The flange stays free.",
"Slide the holder (part 27) up around the slip-ring body and onto the end of the tower; peg in a flange hole. Glue only its block to the tower."],
"CA glue · coupler (26) · holder (27)"),
("Spin motor (seen from below)",
["Hang the motor under the center plate with its 20T pulley, and loop the 200 mm belt over the hub.",
"Slide the motor in its slots to tension the belt, then tighten."],
"4x M3x12 socket"),
("Arm",
["Bolt the 220 mm extrusion to the hub, 80 mm behind center and 140 mm ahead.",
"Tip: idler bracket, 608 and sleeve. Back: motor bracket, arm motor, and the switch mount on top."],
"4x M5x12 socket · 3x M5x8 · 7x T-nuts · 4x M3x6 · 1x 608"),
("Carriage",
["Fit the 4 roller bearings with washers and slide the carriage on.",
"Loop the belt around the motor pulley and idler; clamp both ends in the carriage.",
"Magnets and spacer in the cup, felt on top, glue in. Glue the striker on the back edge."],
"5x M3x8 button · 4x 608 · 2x magnets"),
("Electronics (seen from below)",
["Mount the Arduino + CNC shield under deck Q1, components facing down.",
"The arm motor (4 wires) and home switch (2 wires) run through the slip ring.",
"Fit all 3 microstep jumpers per driver and set each DRV8825's current before plugging in motors."],
"4x M3x6 socket"),
("Close it up",
["Test both axes with the floor off first.",
"Then the plexi floor, sand ring, about 4 mm of fine sand, the 3/8\" ball, and finally the glass."],
"~0.4 kg sand · 3/8\" steel ball"),
]
W, M, G = 2400, 80, 70
PW = (W - 2 * M - G) // 2
f_title = ImageFont.truetype(F.format('Bold'), 84)
f_sub = ImageFont.truetype(F.format('Regular'), 34)
f_step = ImageFont.truetype(F.format('Bold'), 46)
f_num = ImageFont.truetype(F.format('Bold'), 40)
f_body = ImageFont.truetype(F.format('Regular'), 31)
f_hw = ImageFont.truetype(MONO, 27)
f_hwl = ImageFont.truetype(F.format('Bold'), 25)
f_foot = ImageFont.truetype(F.format('Regular'), 29)
f_footb = ImageFont.truetype(F.format('Bold'), 29)
def wrap(draw, text, font, width):
words, lines, cur = text.split(), [], ''
for w in words:
t = (cur + ' ' + w).strip()
if draw.textlength(t, font=font) <= width:
cur = t
else:
lines.append(cur)
cur = w
lines.append(cur)
return lines
def panel(i, title, bullets, hw):
img = Image.open(SRC.format(i)).convert('RGB')
img = img.resize((PW, round(img.height * PW / img.width)), Image.LANCZOS)
tmp = ImageDraw.Draw(Image.new('RGB', (1, 1)))
body = []
for b in bullets:
body.append(wrap(tmp, b, f_body, PW - 40))
lh = 44
hw_lines, cur = [], ''
for tok in hw.split(' · '):
t = (cur + ' · ' + tok) if cur else tok
if tmp.textlength(t, font=f_hw) <= PW - 160:
cur = t
else:
hw_lines.append(cur)
cur = tok
hw_lines.append(cur)
h = 80 + img.height + 20 + sum(len(b) * lh + 10 for b in body) + 20 + 40 * len(hw_lines) + 10
p = Image.new('RGB', (PW, h), 'white')
d = ImageDraw.Draw(p)
d.ellipse((0, 4, 64, 68), fill=TEAL)
d.text((32, 36), str(i), font=f_num, fill='white', anchor='mm')
d.text((86, 36), title, font=f_step, fill=INK, anchor='lm')
y = 80
p.paste(img, (0, y))
y += img.height + 20
for lines in body:
d.ellipse((6, y + 15, 18, y + 27), fill=TEAL)
for ln in lines:
d.text((40, y), ln, font=f_body, fill=INK)
y += lh
y += 10
y += 14
d.text((0, y + 6), 'HARDWARE', font=f_hwl, fill=TEAL)
for ln in hw_lines:
d.text((150, y + 4), ln, font=f_hw, fill=GRAY)
y += 40
return p
panels = [panel(i + 1, *s) for i, s in enumerate(STEPS)]
rows = [panels[k:k + 2] for k in range(0, 8, 2)]
head_h = 260
foot_lines = [
("Home switch: ", "use a KW12-B with a straight lever. Reprint the switch mount and glue on the striker from print/v2.1-switch-update."),
("Fit first: ", "print 00-fit-test-coupon; every printed part is set for shaft 8.1 mm and bearing pocket 22.2 mm."),
]
foot_h = 70 + 50 * len(foot_lines) + 60
H = head_h + sum(max(p.height for p in r) + 70 for r in rows) + foot_h
canvas = Image.new('RGB', (W, H), 'white')
d = ImageDraw.Draw(canvas)
d.text((M, 70), 'Sand table build guide', font=f_title, fill=INK)
d.text((M, 180), '12" polar kinetic sand table · 8 steps · printed parts: print/v2-current + print/v2.1-switch-update', font=f_sub, fill=GRAY)
y = head_h
for r in rows:
d.line((M, y, W - M, y), fill=RULE, width=3)
y += 40
for k, p in enumerate(r):
canvas.paste(p, (M + k * (PW + G), y))
y += max(p.height for p in r) + 30
d.line((M, y, W - M, y), fill=RULE, width=3)
y += 40
for b, t in foot_lines:
d.text((M, y), b, font=f_footb, fill=INK)
d.text((M + d.textlength(b, font=f_footb), y), t, font=f_foot, fill=INK)
y += 50
d.text((M, y + 10), 'Drawings: renders of the SolidCraft model, restyled with Qwen-Image-Edit in ComfyUI.', font=f_foot, fill=GRAY)
canvas.save(OUT, optimize=True)
print(OUT, canvas.size)