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>
143 lines
6.3 KiB
Python
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)
|