Skip to content
RPRichard PruittTech & Cyber
Navigation
repo://poppyup/drawings/generate_drawings.py

drawings/generate_drawings.py

36 KB · 516 lines · python · Download from the repository page

drawings/generate_drawings.py
import mathS=4.0  # px per mmOUT="/Users/dpruitt/Documents/poppyup/drawings/"DATE="2026-09-27" def esc(t): return t.replace("&","&amp;").replace("<","&lt;").replace(">","&gt;") class D:    def __init__(s,w,h,title,view,scale="1 mm = 4 px"):        s.w,s.h=w,h; s.parts=[]; s.title=title; s.view=view; s.scale=scale    def add(s,x): s.parts.append(x)    def g(s,id,inner,comment=""):        s.add(f"<!-- {comment or id} -->\n<g id=\"{id}\">\n{inner}\n</g>")    def rect(s,x,y,w,h,fill="#f4f4f4",stroke="#222",sw=1.2,rx=0,extra=""):        return f'<rect x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{h:.1f}" rx="{rx}" fill="{fill}" stroke="{stroke}" stroke-width="{sw}" {extra}/>'    def line(s,x1,y1,x2,y2,stroke="#222",sw=1,dash=""):        d=f' stroke-dasharray="{dash}"' if dash else ""        return f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" stroke="{stroke}" stroke-width="{sw}"{d}/>'    def text(s,x,y,t,size=11,anchor="start",fill="#111",extra=""):        return f'<text x="{x:.1f}" y="{y:.1f}" font-size="{size}" text-anchor="{anchor}" fill="{fill}" font-family="Helvetica, Arial, sans-serif" {extra}>{esc(t)}</text>'    def circle(s,cx,cy,r,fill="#fff",stroke="#222",sw=1.2):        return f'<circle cx="{cx:.1f}" cy="{cy:.1f}" r="{r:.1f}" fill="{fill}" stroke="{stroke}" stroke-width="{sw}"/>'    def hdim(s,x1,x2,y,label,off=0,size=10):        # horizontal dimension line with arrows at y; label above        o=[]        o.append(s.line(x1,y,x2,y,"#333",0.9))        o.append(f'<path d="M{x1:.1f},{y:.1f} l7,-3 l0,6 z" fill="#333"/>')        o.append(f'<path d="M{x2:.1f},{y:.1f} l-7,-3 l0,6 z" fill="#333"/>')        o.append(s.text((x1+x2)/2,y-4+off,label,size,"middle","#333"))        return "\n".join(o)    def vdim(s,y1,y2,x,label,size=10,side=1):        o=[]        o.append(s.line(x,y1,x,y2,"#333",0.9))        o.append(f'<path d="M{x:.1f},{y1:.1f} l-3,7 l6,0 z" fill="#333"/>')        o.append(f'<path d="M{x:.1f},{y2:.1f} l-3,-7 l6,0 z" fill="#333"/>')        cy=(y1+y2)/2        o.append(f'<text x="{x+6*side:.1f}" y="{cy:.1f}" font-size="{size}" fill="#333" font-family="Helvetica, Arial, sans-serif" transform="rotate(-90 {x+6*side:.1f} {cy:.1f})" text-anchor="middle">{esc(label)}</text>')        return "\n".join(o)    def ext(s,x1,y1,x2,y2):  # extension line        return s.line(x1,y1,x2,y2,"#777",0.6)    def leader(s,x1,y1,x2,y2,t,size=10,anchor="start"):        return s.line(x1,y1,x2,y2,"#444",0.7)+"\n"+s.circle(x1,y1,1.6,"#444","#444",0)+"\n"+s.text(x2+(3 if anchor=="start" else -3),y2+3,t,size,anchor,"#222")    def titleblock(s,x,y):        w,h=470,74        o=[s.rect(x,y,w,h,"#fff","#222",1.2)]        o.append(s.line(x,y+20,x+w,y+20,"#222",0.8))        o.append(s.line(x+290,y+20,x+290,y+h,"#222",0.8))        o.append(s.text(x+8,y+14,"PoppyUp — Phase 1 Hardware Design",12,"start","#111",'font-weight="bold"'))        o.append(s.text(x+8,y+36,f"View: {s.view}",10))        o.append(s.text(x+8,y+52,f"Scale: {s.scale}",10))        o.append(s.text(x+8,y+66,"Units: mm   Proj: third angle",10))        o.append(s.text(x+298,y+36,f"Date: {DATE}",10))        o.append(s.text(x+298,y+52,"Rev: C (ADR v0.3)   Status: concept",10))        o.append(s.text(x+298,y+66,"Drawn: systems/mech architecture",10))        return "\n".join(o)    def scalebar(s,x,y):        o=[]        for i in range(5):            o.append(s.rect(x+i*10*S,y,10*S,6,"#222" if i%2==0 else "#fff","#222",0.8))        for i in range(0,6):            o.append(s.text(x+i*10*S,y+18,str(i*10),9,"middle"))        o.append(s.text(x+25*S,y+30,"scale bar, mm",9,"middle","#333"))        return "\n".join(o)    def write(s,fn):        head=f'<?xml version="1.0" encoding="UTF-8"?>\n<svg xmlns="http://www.w3.org/2000/svg" width="{s.w}" height="{s.h}" viewBox="0 0 {s.w} {s.h}">\n<title>{esc(s.title)}</title>\n<defs>\n<pattern id="hatch" patternUnits="userSpaceOnUse" width="6" height="6" patternTransform="rotate(45)"><line x1="0" y1="0" x2="0" y2="6" stroke="#888" stroke-width="1"/></pattern>\n<pattern id="hatchAl" patternUnits="userSpaceOnUse" width="5" height="5" patternTransform="rotate(-45)"><line x1="0" y1="0" x2="0" y2="5" stroke="#556" stroke-width="0.8"/></pattern>\n<pattern id="grid" patternUnits="userSpaceOnUse" width="40" height="40"><path d="M40,0 L0,0 L0,40" fill="none" stroke="#e6e6e6" stroke-width="0.6"/></pattern>\n</defs>\n<rect width="100%" height="100%" fill="#fff"/>\n<rect width="100%" height="100%" fill="url(#grid)"/>\n'        body="\n".join(s.parts)        open(OUT+fn,"w").write(head+body+"\n</svg>\n") # ---------------- geometry (mm) ----------------L=190; H=96; T=38slots=[("CFast 2.0",13.5,45,6),("CFexpress B",62.5,40,6),("CFexpress A",106.5,22,5),("SD UHS-II",132.5,27,4),("microSD",163.5,13,3)]END=10.0; SL0,SL1=10.0,180.0  # end-cap zone width; shutter slat X extent (170 mm)slot_zc=15.0   # slot centre in Z (front->rear) on top face, mmchan_z0,chan_z1=8,30 # ============ TOP VIEW ============d=D(1120,560,"PoppyUp top view (top edge, slots)","TOP — top edge / slot row")ox,oy=140,150   # origin px of device top-left corner; x = length, y = front->rear (front at top)X=lambda mm: ox+mm*S; Y=lambda mm: oy+mm*So=[]o.append(d.text(ox,oy-100,"TOP VIEW — looking down onto the top edge. Front face is at the TOP of this drawing, rear face at the BOTTOM.",11,"start","#333"))o.append(d.text(ox,oy-86,"Shutter: two 20 mm slats slide FRONT→REAR (down the page) and park on the rear face. Slot channel Z 8–30 exposed when open.",10,"start","#555"))# outline (TPU corners)o.append(d.rect(X(0),Y(0),L*S,T*S,"#eeeeee","#222",1.6,rx=10*S))# end caps / rail zonesfor x0 in (0,180):    o.append(d.rect(X(x0),Y(0),END*S,T*S,"url(#hatch)","#555",0.8))for xr in (7,180):    o.append(d.rect(X(xr),Y(-1),3*S,(T+2)*S,"none","#c33",0.9,extra='stroke-dasharray="3 2"'))o.append(d.text(X(0),Y(T)+2*S+6+7*S+84,"end caps (X 0–10 and 180–190, hatched): POM rail groove (red dashed) in the inner 3 mm + drain exit below the rail; no RF requirement; the 170 mm shutter never covers them",8,"start","#444"))# channel recesso.append(d.rect(X(12),Y(chan_z0),166*S,(chan_z1-chan_z0)*S,"#dcdcdc","#333",1.0))o.append(d.text(X(16),Y(chan_z1)-3,"slot channel, recess 6 mm deep, floor slopes 3° to end drains",8,"start","#444"))# slotsfor name,x0,w,hgt in slots:    o.append(d.rect(X(x0),Y(slot_zc-hgt/2),w*S,hgt*S,"#222","#000",0.8,rx=2))    o.append(d.text(X(x0+w/2),Y(slot_zc)+3.5,name,8,"middle","#fff"))# drainsfor xd in (12,176.5):    o.append(d.rect(X(xd),Y(27),1.5*S,2.5*S,"#999","#333",0.6))# LED bar at front edge (front face; shown as strip)for name,x0,w,hgt in slots:    o.append(d.rect(X(x0+w/2-6),Y(0)-6,12*S,4,"#ffb347","#a65",0.6))o.append(d.text(X(60),Y(0)-10,"LED bar segments (front face, just below top edge)",8,"start","#a65"))# Hall / magnetso.append(d.circle(X(95),Y(33),2.2*S,"#c33","#800",1))o.append(d.text(X(95)+12,Y(33)+3,"Hall sensor (reader board, under rear rim)",8))o.append(d.circle(X(55),Y(33),1.6*S,"#88c","#336",1)); o.append(d.circle(X(150),Y(33),1.6*S,"#88c","#336",1))o.append(d.text(X(55)-8,Y(33)+3,"N52 magnet (in slat 2)",7,"end","#336")); o.append(d.text(X(150)+8,Y(33)+3,"N52 magnet",7,"start","#336"))# detentsfor x0 in (8.5,181.5):    for zc,lab in ((3,"detent CLOSED"),(35,"detent OPEN")):        o.append(d.circle(X(x0),Y(zc),1.2*S,"#fff","#c33",1.2))o.append(d.text(X(0)-10,Y(3)+3,"detent, closed",8,"end","#c33"))o.append(d.text(X(0)-10,Y(35)+3,"detent, open",8,"end","#c33"))# shutter closed dashed outlineo.append(d.rect(X(SL0),Y(-1),170*S,20*S,"none","#0a5",1.2,extra='stroke-dasharray="6 4"'))o.append(d.rect(X(SL0),Y(19),170*S,21*S,"none","#0a5",1.2,extra='stroke-dasharray="6 4"'))o.append(d.text(X(16),Y(-1)+12,"shutter CLOSED (dashed): two 170 mm slats, slat 1 Z 0–20, slat 2 Z 20–38 + 2 mm wrap; ends stop 10 mm short of the end caps",8,"start","#0a5"))# shutter open translucent: slat 1 on corner/rear, slat 2 on rear face -> projected below rear edgeo.append(d.rect(X(SL0),Y(36),170*S,2*S,"#0a5","#063",0.8,extra='opacity="0.35"'))o.append(d.rect(X(SL0),Y(T)+2*S+6,170*S,7*S,"#0a5","#063",0.8,extra='opacity="0.25"'))o.append(d.text(X(SL0)+6,Y(T)+2*S+6+16,"shutter OPEN (translucent): slat 1 on top-rear R10 corner + 14 mm down rear face; slat 2 parked 20–40 mm down rear-face pocket (edge-on here, see side view)",8,"start","#063"))d.g("device","\n".join(o),"device top face, slots, shutter states")# dimensionsdm=[]dm.append(d.hdim(X(0),X(L),Y(0)-52,"190"))dm.append(d.ext(X(0),Y(0)-56,X(0),Y(0))); dm.append(d.ext(X(L),Y(0)-56,X(L),Y(0)))dm.append(d.vdim(Y(0),Y(T),X(L)+40,"38",9,1))dm.append(d.ext(X(L),Y(0),X(L)+44,Y(0))); dm.append(d.ext(X(L),Y(T),X(L)+44,Y(T)))dm.append(d.vdim(Y(chan_z0),Y(chan_z1),X(L)+16,"22",9,-1))# chain dims belowyc=Y(T)+2*S+6+7*S+30prev=0pts=[0]for name,x0,w,hgt in slots:    pts+= [x0,x0+w]pts.append(190)labels=["13.5","45","4","40","4","22","4","27","4","13","13.5"]for i in range(len(pts)-1):    dm.append(d.hdim(X(pts[i]),X(pts[i+1]),yc,labels[i],0,9))    dm.append(d.ext(X(pts[i]),Y(T),X(pts[i]),yc+4))dm.append(d.ext(X(190),Y(T),X(190),yc+4))dm.append(d.text(X(95),yc+18,"slot housing widths and 4 mm gaps (chain): slot row = 163 mm, centred; shutter = 170 mm (X 10–180); end caps = 10 mm each",9,"middle","#333"))dm.append(d.hdim(X(SL0),X(SL1),Y(0)-22,"170 (shutter slats)",0,9)); dm.append(d.ext(X(SL0),Y(0)-26,X(SL0),Y(0))); dm.append(d.ext(X(SL1),Y(0)-26,X(SL1),Y(0)))dm.append(d.hdim(X(0),X(SL0),Y(0)-22,"10",0,8)); dm.append(d.hdim(X(SL1),X(190),Y(0)-22,"10",0,8))dm.append(d.hdim(X(13.5),X(176.5),yc+34,"163 (slot row)",0,9))# slot centre dimdm.append(d.vdim(Y(0),Y(slot_zc),X(0)-95,"15 (slot centreline)",8,-1))dm.append(d.ext(X(0),Y(slot_zc),X(0)-99,Y(slot_zc))); dm.append(d.ext(X(0),Y(0),X(0)-99,Y(0)))# section marksdm.append(d.text(X(150),Y(0)-60,"A",12,"middle","#c00",'font-weight="bold"')); dm.append(d.line(X(150),Y(0)-54,X(150),Y(0)-46,"#c00",2))dm.append(d.text(X(150),Y(T)+2*S+6+7*S+8,"A",12,"middle","#c00",'font-weight="bold"'))dm.append(d.text(X(150)+8,Y(0)-60,"section A-A (battery band) → side-view.svg",8,"start","#c00"))d.g("dimensions","\n".join(dm),"dimensions in mm")d.add(d.scalebar(140,470)); d.add(d.titleblock(620,470))d.write("top-view.svg") # ============ FRONT VIEW ============d=D(1180,660,"PoppyUp front view","FRONT — 190 × 96 face, shutter closed")ox,oy=150,110X=lambda mm: ox+mm*S; Y=lambda mm: oy+mm*So=[]o.append(d.text(ox,oy-70,"FRONT VIEW — user-facing face. Top edge (slots under shutter) at top, ports on bottom edge. Wi-Fi antennas in the front upper band (the only RF window band); plain PC end caps (rail grooves + drains, no RF requirement); the rear face is all-metal.",10,"start","#333"))# closed shutter edge aboveo.append(d.rect(X(SL0),Y(0)-2*S,170*S,2*S,"#9aa","#333",0.8))o.append(d.text(X(60),Y(0)-2*S-3,"shutter slat 1 edge, 170 mm (hard-anodized Al, 2 mm) with thumb ridge",8,"start","#444"))# bodyo.append(d.rect(X(0),Y(0),L*S,H*S,"#e9e9e9","#222",1.6,rx=10*S))# TPU corner bumpersfor cx,cy in ((0,0),(L,0),(0,H),(L,H)):    sx=1 if cx==0 else -1; sy=1 if cy==0 else -1    o.append(f'<path d="M{X(cx):.1f},{Y(cy)+sy*24*S:.1f} L{X(cx):.1f},{Y(cy)+sy*10*S:.1f} Q{X(cx):.1f},{Y(cy):.1f} {X(cx)+sx*10*S:.1f},{Y(cy):.1f} L{X(cx)+sx*24*S:.1f},{Y(cy):.1f} L{X(cx)+sx*24*S:.1f},{Y(cy)+sy*7*S:.1f} L{X(cx)+sx*10*S:.1f},{Y(cy)+sy*7*S:.1f} Q{X(cx)+sx*7*S:.1f},{Y(cy)+sy*7*S:.1f} {X(cx)+sx*7*S:.1f},{Y(cy)+sy*10*S:.1f} L{X(cx)+sx*7*S:.1f},{Y(cy)+sy*24*S:.1f} Z" fill="#333" stroke="#111" stroke-width="0.8"/>')o.append(d.text(X(9),Y(22),"TPU corner bumper (7 mm), ×4",7,"start","#333"))# plain PC end caps (no RF window in v0.2): rail grooves + drain exits onlyfor x0 in (0,180):    o.append(d.rect(X(x0),Y(24),END*S,48*S,"#dcdcdc","#777",0.6,extra='stroke-dasharray="3 2"'))for ex in (5,185):    o.append(d.text(X(ex),Y(48),"PC end cap 10 mm: rail groove + drain (no RF window)",6.5,"middle","#555",'transform="rotate(-90 %.1f %.1f)"'%(X(ex),Y(48))))# Wi-Fi antennas in upper band flanking the OLEDfor ax in (12,130):    o.append(d.rect(X(ax),Y(30),40*S,8*S,"#cfe0ff","#1a4fa0",1.0,rx=3))    o.append(d.rect(X(ax),Y(30),40*S,8*S,"url(#hatch)","none",0))o.append(d.text(X(32),Y(35.5),"Wi-Fi ant 1 (behind PC)",7,"middle","#1a4fa0")); o.append(d.text(X(150),Y(35.5),"Wi-Fi ant 2 (behind PC)",7,"middle","#1a4fa0"))# battery dooro.append(d.rect(X(18),Y(42),154*S,50*S,"#e2e2e2","#333",1.0,rx=6))for sx in (24,95,166):    for sy in (46,88):        o.append(d.circle(X(sx),Y(sy),1.4*S,"#fff","#333",0.8)); o.append(d.line(X(sx)-3,Y(sy),X(sx)+3,Y(sy),"#333",0.7))o.append(d.text(X(95),Y(56),"battery door (PC+GF, gasketed, 6 × M2.5 captive) — service-replaceable pack, EU Batt. Reg. 2023/1542",7,"middle","#333"))# strap slots in end capsfor x0 in (2.5,180+2.5):    o.append(d.rect(X(x0),Y(78),5*S,10*S,"#fff","#333",0.8,rx=3))o.append(d.text(X(20),Y(84),"strap slot 25×4 (end cap), ×2",7,"start","#444"))# LED barfor name,x0,w,hgt in slots:    o.append(d.rect(X(x0+w/2-6),Y(3),12*S,3*S,"#ffb347","#a65",0.8,rx=2))o.append(d.text(X(130),Y(9.5),"LED bar: 5 RGB segments (one per slot)",7,"start","#a65"))# OLED (right half): module 62x36 at X 97.5-159.5, Y 7-43; active 57x30 at X100-157, Y10-40o.append(d.rect(X(64),Y(7),62*S,36*S,"#cfd8dc","#333",0.8,rx=4))o.append(d.rect(X(66.5),Y(10),57*S,30*S,"#0b0f14","#000",1.0,rx=2))o.append(d.text(X(69.5),Y(17),"UPLOADING  ETH 5GbE  ▮▮▮▮▯ 78%",8,"start","#9fe"))o.append(d.text(X(69.5),Y(24),"CFe B  412/512 GB  →  s3://prod-dailies",8,"start","#9fe"))o.append(d.text(X(69.5),Y(31),"SD     done ✓  verified xxh3",8,"start","#9fe"))o.append(d.text(X(69.5),Y(38),"BAT 64%  3h10m   LINK 4.6 Gb/s",8,"start","#9fe"))o.append(d.text(X(95),Y(96)+3.3*S+56+14,"2.42\" OLED 128×64, active 57 × 30 (PC lens, recessed 0.5)",8,"middle","#333"))# power buttono.append(d.circle(X(30),Y(12),6*S,"#333","#111",1)); o.append(d.circle(X(30),Y(12),4.5*S,"#555","#222",0.8))o.append(d.text(X(38),Y(13.5),"power button (sealed tact, TPU cap)",7,"start","#333"))# logo / texto.append(d.text(X(14),Y(26),"PoppyUp",13,"start","#c44",'font-weight="bold"'))# battery band ghost (hidden) and reader band boundaryo.append(d.line(X(0),Y(40),X(L),Y(40),"#aaa",0.6,"4 4"))o.append(d.line(X(0),Y(88),X(L),Y(88),"#aaa",0.6,"4 4"))o.append(d.text(X(95),Y(66),"hidden behind door: battery pack 4×21700 (2 rows × 2), X 23–167, Y 44–88",7,"middle","#888"))o.append(d.rect(X(23),Y(44),144*S,44*S,"none","#bbb",0.6,extra='stroke-dasharray="3 3"'))# BLE chip antenna window, bottom-left of the front frame (EC corner of the main board, 09-pcb-architecture.md §1.1)o.append(d.rect(X(11),Y(74),7*S,14*S,"#fff9c4","#f9a825",0.8,rx=2))o.append(d.text(X(14.5),Y(81),"BLE",6,"middle","#7a5c00"))o.append(d.text(X(20),Y(78),"BLE chip-antenna PC window (X 11–18, Y 74–88; 6 × 12 keep-out)",6.5,"start","#7a5c00"))# ports at bottom edge (cutouts below body)py=Y(H)for xc,lab in ((25,"Port A  USB-C\nPD/PHONE"),(43,"Port B  USB-C\nSSD host"),(61,"Port C  USB-C\nUSB4 FAST (Pro)")):    o.append(d.rect(X(xc-4.5),py,9*S,3.3*S,"#fff","#222",1.0,rx=6))    l1,l2=lab.split("\n")    o.append(d.text(X(xc),py+3.3*S+12,l1,7,"middle")); o.append(d.text(X(xc),py+3.3*S+21,l2,7,"middle"))o.append(d.rect(X(167),py,18*S,3*S,"#333","#111",1.0,rx=2))o.append(d.text(X(176),py+3*S+12,"RJ45 5GBASE-T",7,"middle")); o.append(d.text(X(176),py+3*S+21,"hinged TPU door, over-center latch",7,"middle"))o.append(d.circle(X(95),py+1.5*S,2*S,"#fff","#333",0.8)); o.append(d.text(X(95),py+3*S+12,"1/4\"-20 insert",7,"middle")); o.append(d.text(X(95),py+3*S+21,"+ vent (Gore) + reset pinhole",7,"middle"))o.append(d.text(X(0),py+3.3*S+56,"bottom edge (ports) shown unfolded below the body — all three USB-C are IP67 mid-mount receptacles, shells screwed to chassis brackets",8,"start","#444"))d.g("device","\n".join(o),"front face features")dm=[]dm.append(d.hdim(X(0),X(L),Y(0)-52,"190")); dm.append(d.ext(X(0),Y(0)-56,X(0),Y(0))); dm.append(d.ext(X(L),Y(0)-56,X(L),Y(0)))dm.append(d.vdim(Y(0),Y(H),X(L)+80,"96",10,1)); dm.append(d.ext(X(L),Y(0),X(L)+84,Y(0))); dm.append(d.ext(X(L),Y(H),X(L)+84,Y(H)))dm.append(d.vdim(Y(0),Y(40),X(L)+52,"40 reader band",8,1))dm.append(d.vdim(Y(40),Y(88),X(L)+52,"48 battery band",8,1))dm.append(d.vdim(Y(88),Y(96),X(L)+52,"8",8,1))# OLED offsetsdm.append(d.hdim(X(0),X(66.5),Y(0)-30,"66.5 (OLED active X)",0,8)); dm.append(d.ext(X(66.5),Y(0)-34,X(66.5),Y(10)))dm.append(d.hdim(X(66.5),X(123.5),Y(0)-30,"57",0,8)); dm.append(d.ext(X(123.5),Y(0)-34,X(123.5),Y(10)))dm.append(d.hdim(X(32),X(150),Y(96)+3.3*S+56+30,"118 (Wi-Fi antenna centres, ≥ 60)",0,8)); dm.append(d.ext(X(32),Y(38),X(32),Y(96)+3.3*S+56+34)); dm.append(d.ext(X(150),Y(38),X(150),Y(96)+3.3*S+56+34))dm.append(d.vdim(Y(0),Y(10),X(0)-30,"10",8,-1)); dm.append(d.vdim(Y(10),Y(40),X(0)-30,"30 (OLED active Y)",8,-1))dm.append(d.ext(X(0),Y(10),X(66.5),Y(10))); dm.append(d.ext(X(0),Y(40),X(0)-34,Y(40)))# port pitchdm.append(d.hdim(X(25),X(43),py+3.3*S+34,"18",0,8)); dm.append(d.hdim(X(43),X(61),py+3.3*S+34,"18",0,8))dm.append(d.hdim(X(0),X(25),py+3.3*S+34,"25",0,8))dm.append(d.hdim(X(167),X(185),py+3.3*S+34,"18",0,8))d.g("dimensions","\n".join(dm),"dimensions in mm")d.add(d.scalebar(150,610)); d.add(d.titleblock(680,580))d.write("front-view.svg") # ============ SIDE VIEW + SECTION ============d=D(1100,720,"PoppyUp side view and section A-A","RIGHT-END PROFILE + SECTION A-A")# Profile: x = front->rear (T=38), y = top->bottom (H=96). Front at left.def side(ox,oy,section):    X=lambda mm: ox+mm*S; Y=lambda mm: oy+mm*S    o=[]    # body path with R10 top-rear corner, R8 others    p=f"M{X(0)+8*S:.1f},{Y(0):.1f} L{X(T)-10*S:.1f},{Y(0):.1f} Q{X(T):.1f},{Y(0):.1f} {X(T):.1f},{Y(0)+10*S:.1f} L{X(T):.1f},{Y(H)-8*S:.1f} Q{X(T):.1f},{Y(H):.1f} {X(T)-8*S:.1f},{Y(H):.1f} L{X(0)+8*S:.1f},{Y(H):.1f} Q{X(0):.1f},{Y(H):.1f} {X(0):.1f},{Y(H)-8*S:.1f} L{X(0):.1f},{Y(0)+8*S:.1f} Q{X(0):.1f},{Y(0):.1f} {X(0)+8*S:.1f},{Y(0):.1f} Z"    o.append(f'<path d="{p}" fill="{"#f7f7f7" if section else "#e4e4e4"}" stroke="#222" stroke-width="1.6"/>')    if not section:        # plain PC+GF end cap (no RF window, no SIM tray in v0.2)        o.append(d.text(X(19),Y(48),"PC+GF end cap",8,"middle","#333"))        o.append(d.text(X(19),Y(52),"(no RF window)",7,"middle","#333"))        o.append(d.text(X(19),Y(56),"lanyard hole Ø3",7,"middle","#333"))        o.append(d.circle(X(19),Y(62),1.5*S,"#fff","#333",0.8))        o.append(d.rect(X(14),Y(80),4*S,8*S,"#fff","#333",0.8,rx=2)); o.append(d.text(X(16),Y(92),"strap slot",7,"middle","#333"))        # rail groove visible on end cap (track path)        o.append(f'<path d="M{X(1):.1f},{Y(-1):.1f} L{X(T)-14*S:.1f},{Y(-1):.1f} A{14*S:.1f},{14*S:.1f} 0 0 1 {X(T)+1*S:.1f},{Y(14):.1f} L{X(T)+1*S:.1f},{Y(40):.1f}" fill="none" stroke="#c33" stroke-width="1.6" stroke-dasharray="3 2"/>')        o.append(d.text(X(T)+8,Y(24),"T-track (pin centres)",7,"start","#c33")); o.append(d.text(X(T)+8,Y(27),"R14 corner, 40 mm",7,"start","#c33")); o.append(d.text(X(T)+8,Y(30),"down rear face",7,"start","#c33"))    # top recess: channel Z 8-30 depth 6    o.append(d.rect(X(chan_z0),Y(0),(chan_z1-chan_z0)*S,6*S,"#cfcfcf","#333",0.8))    # shutter closed: slat1 Z0-20 on top (y -2..0), slat2 Z20-38 + wrap    o.append(d.rect(X(0)+2,Y(0)-2*S,20*S-2,2*S,"#8fa","#063",1.0))    p2=f"M{X(20):.1f},{Y(0)-2*S:.1f} L{X(T)-10*S:.1f},{Y(0)-2*S:.1f} A{12*S:.1f},{12*S:.1f} 0 0 1 {X(T)+2*S:.1f},{Y(10):.1f} L{X(T)+2*S:.1f},{Y(12):.1f} L{X(T):.1f},{Y(12):.1f} L{X(T):.1f},{Y(10):.1f} A{10*S:.1f},{10*S:.1f} 0 0 0 {X(T)-10*S:.1f},{Y(0):.1f} L{X(20):.1f},{Y(0):.1f} Z"    o.append(f'<path d="{p2}" fill="#8fa" stroke="#063" stroke-width="1.0"/>')    o.append(d.circle(X(20),Y(0)-1*S,1.2*S,"#fff","#063",0.8))  # hinge    # rear pocket step (2.5 mm) top 42 mm    o.append(d.rect(X(T)-2.5*S,Y(12),2.5*S,30*S,"#fff","#333",0.8))    o.append(d.rect(X(T)-2.5*S,Y(12),2.5*S,30*S,"none","#0a5",0.8,extra='stroke-dasharray="3 2"'))    # ribs on rear face y 44-96    for yy in range(46,94,8):        o.append(d.rect(X(T)-1.5*S,Y(yy),1.5*S,3*S,"#bbb","#333",0.5))    # TPU bumpers top/bottom    o.append(d.rect(X(0),Y(0)+8*S,7*S,16*S,"#333","#111",0.6))    o.append(d.rect(X(0),Y(H)-24*S,7*S,16*S,"#333","#111",0.6))    if section:        # internal: front frame        o.append(d.rect(X(0),Y(2),2.5*S,92*S,"#b0c4de","#345",0.8))        # foam+cradle        o.append(d.rect(X(2.5),Y(42),1.6*S,46*S,"#ffe9a8","#a80",0.6))        o.append(d.rect(X(24.6),Y(42),1.8*S,46*S,"#ffe9a8","#a80",0.6))        # cells: 2 circles dia 21 at x centre 14.1, y 53.5 & 76.5        for cy in (53.5,76.5):            o.append(d.circle(X(14.1),Y(cy),10.5*S,"#c8e6c9","#2e7d32",1.2))            o.append(d.text(X(14.1),Y(cy)+3,"21700",8,"middle","#1b5e20"))        # main board        o.append(d.rect(X(27.9),Y(12),1.6*S,80*S,"#2e7d32","#1b5e20",0.8))        # front comps budget 1.0        o.append(d.rect(X(26.9),Y(44),1.0*S,44*S,"#9e9e9e","#555",0.4))        # rear comps: SoM stack 4.5 at y 46-72 (60mm long in X of device, here shown as band); NVMe/Wi-Fi M.2 stack <= 4.7; 6.5 mm budget retained (v0.2)        o.append(d.rect(X(29.5),Y(46),4.5*S,26*S,"#90caf9","#1565c0",0.8)); o.append(d.text(X(31.7),Y(60),"SoM",7,"middle","#0d47a1"))        o.append(d.rect(X(29.5),Y(14),4.7*S,30*S,"#ffcc80","#e65100",0.8)); o.append(d.text(X(31.8),Y(30),"M.2",7,"middle","#bf360c"))        o.append(d.rect(X(34.2),Y(14),1.8*S,30*S,"none","#e65100",0.5,extra='stroke-dasharray="2 2"'))  # unused slack of the 6.5 mm budget        # thermal pads (SoM only in this section; switch/PHY pads are outside A-A)        o.append(d.rect(X(34),Y(46),0.8*S,26*S,"#f48fb1","#880e4f",0.4))        # rear skin 2.0        o.append(d.rect(X(36),Y(10),2*S,86*S,"url(#hatchAl)","#222",0.8))        # boss under SoM        o.append(d.rect(X(34.8),Y(46),1.2*S,26*S,"url(#hatchAl)","#222",0.5))        # reader band: reader board vertical x 18.6-20.2, y 8-40; card at x 13-17 y 6-36; connector x 11-18 y 30-40        o.append(d.rect(X(18.6),Y(8),1.6*S,32*S,"#2e7d32","#1b5e20",0.8))        o.append(d.rect(X(11),Y(30),7*S,10*S,"#777","#333",0.6))        o.append(d.rect(X(13.2),Y(7),3.8*S,29.6*S,"#37474f","#000",0.8)); o.append(d.text(X(15.1),Y(22),"CFe B",6,"middle","#fff",'transform="rotate(-90 %.1f %.1f)"'%(X(15.1),Y(22))))        o.append(d.rect(X(20.2),Y(10),1.5*S,26*S,"#9e9e9e","#555",0.4))        # mezzanine        o.append(d.rect(X(21.7),Y(20),5.2*S,10*S,"#ffd54f","#f57f17",0.8)); o.append(d.text(X(24.3),Y(26),"mezz",6,"middle","#5d4037"))        # OLED/LED region front top        o.append(d.rect(X(2.5),Y(7),2.3*S,36*S,"#0b0f14","#000",0.6))        # bottom: USB-C on rear face at y 88-94        o.append(d.rect(X(29.5),Y(86),3.3*S,8*S,"#cfd8dc","#455a64",0.8)); o.append(d.text(X(31.2),Y(96)+11,"USB-C",7,"middle","#333"))    return "\n".join(o) # left: profiled.add(d.text(80,50,"RIGHT-END PROFILE (shutter closed)",11,"start","#333",'font-weight="bold"'))d.g("profile",side(120,120,False),"external right-end profile")# right: sectiond.add(d.text(520,50,"SECTION A-A through battery band (X ≈ 150), from the right end. Front at left, rear Mg (AZ91D) skin at right.",11,"start","#333",'font-weight="bold"'))d.g("section",side(560,120,True),"section A-A")# dims for sectionox,oy=560,120X=lambda mm: ox+mm*S; Y=lambda mm: oy+mm*Sdm=[]dm.append(d.hdim(X(0),X(T),Y(H)+58,"38",0,9))dm.append(d.ext(X(0),Y(H),X(0),Y(H)+62)); dm.append(d.ext(X(T),Y(H),X(T),Y(H)+62))dm.append(d.vdim(Y(0),Y(H),X(T)+70,"96",10,1)); dm.append(d.ext(X(T),Y(0),X(T)+74,Y(0))); dm.append(d.ext(X(T),Y(H),X(T)+74,Y(H)))dm.append(d.vdim(Y(0),Y(6),X(0)-24,"6 recess",8,-1)); dm.append(d.ext(X(chan_z0),Y(6),X(0)-28,Y(6)))dm.append(d.hdim(X(chan_z0),X(chan_z1),Y(0)-26,"22 channel",0,8)); dm.append(d.ext(X(chan_z0),Y(0)-30,X(chan_z0),Y(0))); dm.append(d.ext(X(chan_z1),Y(0)-30,X(chan_z1),Y(0)))dm.append(d.hdim(X(3.6),X(24.6),Y(65),"21 cell",0,8))dm.append(d.hdim(X(36),X(38),Y(H)+24,"2 skin",0,7))dm.append(d.hdim(X(29.5),X(36),Y(H)+38,"6.5 rear budget",0,7))dm.append(d.text(X(T)+8,Y(H)+41,"rear-face parts: SoM stack 4.5; NVMe/Wi-Fi M.2 ≤ 4.7; 6.5 mm budget retained (≈ 1.8–2 mm slack → SoM boss / gap pads)",7,"start","#333"))dm.append(d.hdim(X(27.9),X(29.5),Y(9),"1.6",0,7))dm.append(d.vdim(Y(12),Y(42),X(T)+30,"30 rear pocket",7,1))dm.append(d.vdim(Y(44),Y(88),X(0)-40,"44 pack",8,-1))# leadersdm.append(d.leader(X(1.2),Y(92),X(0)-60,Y(100),"PC+GF front frame 2.5",8,"end"))dm.append(d.leader(X(3.3),Y(80),X(0)-60,Y(80),"foam + cradle wall",8,"end"))dm.append(d.leader(X(28.7),Y(80),X(0)-60,Y(90),"main board 1.6, 10-layer",8,"end"))dm.append(d.leader(X(37),Y(60),X(T)+30,Y(60),"Mg AZ91D rear skin + ribs",8,"start"))dm.append(d.leader(X(35.5),Y(50),X(T)+30,Y(50),"boss + gap pad",8,"start"))dm.append(d.leader(X(19.4),Y(9),X(0)-60,Y(9),"reader board (vertical)",8,"end"))dm.append(d.leader(X(15),Y(15),X(0)-60,Y(17),"CFe B card inserted",8,"end"))dm.append(d.leader(X(10),Y(-1),X(0)-60,Y(-4),"slat 1 (2 mm Al)",8,"end"))dm.append(d.leader(X(36),Y(6),X(T)+30,Y(6),"slat 2 wraps R10 corner",8,"start"))dm.append(d.leader(X(37),Y(30),X(T)+30,Y(36),"pocket step 2.5",8,"start"))d.g("dimensions","\n".join(dm),"dimensions in mm")# dims for profileox,oy=120,120X=lambda mm: ox+mm*S; Y=lambda mm: oy+mm*Sdp=[]dp.append(d.hdim(X(0),X(T),Y(H)+40,"38",0,9)); dp.append(d.vdim(Y(0),Y(H),X(0)-30,"96",10,-1))dp.append(d.text(X(0),Y(H)+70,"Legend: green = shutter slats; red dashed = pin track; hatched = magnesium AZ91D (section); orange dashed = unused rear-budget slack",8,"start","#444"))d.g("dimensions-profile","\n".join(dp),"profile dims")d.add(d.scalebar(120,610)); d.add(d.titleblock(600,630))d.write("side-view.svg") # ============ EXPLODED VIEW ============d=D(1500,980,"PoppyUp exploded view","EXPLODED — front→rear stack, ≈1:2")d.add(d.text(40,40,"EXPLODED VIEW — parts stacked front (top-left) → rear (bottom-right). Plates drawn as skewed 190 × 96 outlines at ≈ 1 mm = 2 px (drawing scale differs from the 1 mm = 4 px scale bar).",11,"start","#333",'font-weight="bold"'))d.add(d.text(40,56,"Balloon numbers reference the parts list at right. Antenna cables and FPCs omitted for clarity.",9,"start","#555"))K=2.0  # px/mm for explodeddef plate(x,y,w,h,fill,stroke="#222",sw=1.2,rx=8,op=1.0,extra=""):    return f'<g transform="translate({x},{y}) skewY(-18)"><rect x="0" y="0" width="{w*K:.0f}" height="{h*K:.0f}" rx="{rx}" fill="{fill}" stroke="{stroke}" stroke-width="{sw}" opacity="{op}" {extra}/></g>'def inplate(x,y,inner):    return f'<g transform="translate({x},{y}) skewY(-18)">{inner}</g>'def balloon(n,x,y,lx,ly):    return d.line(lx,ly,x,y,"#444",0.7)+d.circle(x,y,10,"#fff","#c00",1.4)+d.text(x,y+4,str(n),10,"middle","#c00",'font-weight="bold"')parts=[]x0,y0=60,240; dx,dy=54,35  # step per layerlayers=[]# 1 shutter slatsi=0; x=x0+i*dx; y=y0+i*dyparts.append(plate(x+10*K,y,170,20,"#8fa","#063",1.2,3)); parts.append(plate(x+10*K,y+20*K+4,170,20,"#8fa","#063",1.2,3))parts.append(inplate(x,y,d.text(26,14,"slat 1, 170 mm (thumb ridge)",8,"start","#063")+d.text(26,14+20*K+4,"slat 2, 170 mm (magnets ×2)",8,"start","#063")))parts.append(balloon(1,x-30,y+10,x,y+8))# 2 railsi=1; x=x0+i*dx; y=y0+i*dyparts.append(plate(x+7*K,y,3,60,"#fff3e0","#e65100",1,2)); parts.append(plate(x+180*K,y,3,60,"#fff3e0","#e65100",1,2))parts.append(balloon(2,x-30,y+30,x,y+30))# 3 PC framei=2; x=x0+i*dx; y=y0+i*dyparts.append(plate(x,y,190,96,"#b0c4de","#345",1.4,12,0.95))parts.append(inplate(x,y,d.rect(12*K,0,166*K,6*K,"#8fa4b8","#345",0.6)+d.rect(66.5*K,10*K,57*K,30*K,"#0b0f14","#000",0.8)+d.rect(0,0,10*K,96*K,"none","#345",0.5,extra='stroke-dasharray="2 2"')+d.rect(180*K,0,10*K,96*K,"none","#345",0.5,extra='stroke-dasharray="2 2"')+d.rect(12*K,30*K,40*K,8*K,"#cfe0ff","#1a4fa0",0.6)+d.rect(130*K,30*K,40*K,8*K,"#cfe0ff","#1a4fa0",0.6)+d.rect(11*K,74*K,7*K,14*K,"#fff9c4","#f9a825",0.6)+d.rect(18*K,42*K,154*K,50*K,"#fff","#345",0.8,extra='stroke-dasharray="4 2"')+d.circle(30*K,12*K,6*K,"#333","#111",0.6)+d.text(24*K,60*K,"PC+30%GF front frame + plain end caps (rail grooves + drains, no RF window): lens,",7,"start","#123")+d.text(24*K,70*K,"Wi-Fi antenna zones (upper band), BLE window (bottom-left), button, battery-door aperture",7,"start","#123")))parts.append(balloon(3,x-30,y+60,x,y+60))# 15 battery doorparts.append(plate(x+18*K-90,y+42*K+180,154,50,"#e2e2e2","#333",1.0,6))parts.append(inplate(x+18*K-90,y+42*K+180,d.text(6*K,25*K,"battery door (PC+GF, gasket, 6 × M2.5)",7,"start","#333")))parts.append(balloon(15,x+18*K-120,y+42*K+210,x+18*K-90,y+42*K+200))# 4 OLED + LED flexi=3; x=x0+i*dx; y=y0+i*dyparts.append(plate(x+64*K,y+7*K,62,36,"#cfd8dc","#333",1,3)); parts.append(plate(x+12*K,y,166,5,"#ffb347","#a65",1,2))parts.append(balloon(4,x-30,y+30,x+12*K,y+6))# 5 battery packi=4; x=x0+i*dx; y=y0+i*dyparts.append(plate(x+23*K,y+44*K,144,44,"#fff8e1","#a80",1.2,6))cells=""for r in range(2):    for c in range(2):        cells+=d.rect((23+2+c*71)*K,(44+1.5+r*21.5)*K,70*K,21*K,"#c8e6c9","#2e7d32",0.8,rx=10)cells+=d.rect((23+2)*K,(44+44-3)*K,60*K,3*K,"#2e7d32","#1b5e20",0.5)+d.text((23+64)*K,(44+44-0.5)*K,"pack PCB: BQ40Z50-R2, protector, fuse, NTC",7,"start","#1b5e20")parts.append(inplate(x,y,cells))parts.append(balloon(5,x-30,y+44*K+40,x+23*K,y+44*K+44))# 6 reader daughterboardi=5; x=x0+i*dx; y=y0+i*dyparts.append(plate(x+9*K,y+2*K,172,38,"#2e7d32","#1b5e20",1.2,4))conns=""for name,sx,w,hgt in slots:    conns+=d.rect(sx*K,4*K,w*K,26*K,"#9e9e9e","#333",0.6,rx=2)+d.text((sx+w/2)*K,20*K,name.split()[0],7,"middle","#111")conns+=d.circle(95*K,36*K,1.5*K,"#c33","#800",0.6)+d.text(12*K,37*K,"172 × 38 · GL9755 · GL9750 · ASM1064 · eFuses · Hall (rear face)",7,"start","#dfe")parts.append(inplate(x,y,conns))parts.append(balloon(6,x-30,y+40,x+5*K,y+20))# 7 mezzaninei=6; x=x0+i*dx; y=y0+i*dyparts.append(plate(x+80*K,y+30*K,30,6,"#ffd54f","#f57f17",1,2))parts.append(balloon(7,x-30,y+30*K+10,x+80*K,y+30*K+6))# 8 main boardi=7; x=x0+i*dx; y=y0+i*dyparts.append(plate(x+12*K,y+12*K,165,80,"#2e7d32","#1b5e20",1.4,4))mb=""mb+=d.rect(30*K,46*K,60*K,26*K,"#90caf9","#1565c0",0.8)+d.text(60*K,60*K,"RK3588 SoM",8,"middle","#0d47a1")mb+=d.rect(30*K,16*K,20*K,20*K,"#bbdefb","#1565c0",0.6)+d.text(40*K,27*K,"ASM2812",6,"middle","#0d47a1")mb+=d.rect(55*K,16*K,22*K,20*K,"#bbdefb","#1565c0",0.6)+d.text(66*K,27*K,"RTL8126",6,"middle","#0d47a1")mb+=d.rect(80*K,16*K,14*K,14*K,"#bbdefb","#1565c0",0.6)+d.text(87*K,24*K,"2464PD",5,"middle","#0d47a1")mb+=d.rect(104*K,14*K,30*K,52*K,"none","#777",0.5,extra='stroke-dasharray="2 2"')+d.text(119*K,36*K,"power spread /",5,"middle","#555")+d.text(119*K,44*K,"bulk caps (ex-M.2 B)",5,"middle","#555")mb+=d.rect(140*K,14*K,22*K,30*K,"#ffe0b2","#e65100",0.8)+d.text(151*K,30*K,"Wi-Fi 2230",6,"middle","#bf360c")mb+=d.rect(166*K,12*K,22*K,80*K,"#ffe0b2","#e65100",0.8,extra='stroke-dasharray="3 2"')+d.text(177*K,50*K,"NVMe 2280 (Pro)",5,"middle","#bf360c",'transform="rotate(-90 %.0f %.0f)"'%(177*K,50*K))mb+=d.rect(30*K,76*K,40*K,12*K,"#e0e0e0","#555",0.6)+d.text(50*K,83*K,"BQ25792 + PD",6,"middle","#333")for xc in (25,43,61): mb+=d.rect((xc-4.5)*K,88*K,9*K,4*K,"#cfd8dc","#455a64",0.6)mb+=d.rect(167*K,80*K,18*K,12*K,"#333","#111",0.6)+d.text(176*K,87*K,"RJ45",6,"middle","#eee")mb+=d.rect(13*K,74*K,10*K,8*K,"#f8bbd0","#880e4f",0.6)+d.rect(13*K,83*K,6*K,8*K,"#fff9c4","#f9a825",0.5)+d.text(13*K,72.5*K,"EC + BLE ant",4.5,"start","#333")parts.append(inplate(x,y,mb))parts.append(balloon(8,x-30,y+60,x+12*K,y+40))# 9 thermal padsi=8; x=x0+i*dx; y=y0+i*dyparts.append(plate(x+30*K,y+46*K,60,26,"#f48fb1","#880e4f",1,2,0.8))  # SoMparts.append(plate(x+30*K,y+16*K,20,20,"#f48fb1","#880e4f",1,2,0.8)); parts.append(plate(x+55*K,y+16*K,22,20,"#f48fb1","#880e4f",1,2,0.8))  # ASM2812, RTL8126 (card-cage pads are on the reader board)parts.append(balloon(9,x-30,y+60,x+30*K,y+50*K))# 10 chassisi=9; x=x0+i*dx; y=y0+i*dyparts.append(plate(x,y,190,96,"url(#hatchAl)","#222",1.6,12))ch=""for yy in range(46,94,8): ch+=d.rect(8*K,yy*K,174*K,1.5*K,"#889","#222",0.4)ch+=d.rect(0,0,190*K,42*K,"#e8e8ee","#222",0.6)+d.text(10*K,20*K,"rear pocket step (2.5) for parked shutter",8,"start","#333")ch+=d.text(10*K,34*K,"Magnesium AZ91D die-cast (EVT/DVT: machined AZ31B), conversion coat + light grey powder coat; masked bosses under SoM, ASM2812, RTL8126; no RF window (all metal)",8,"start","#333")parts.append(inplate(x,y,ch))parts.append(balloon(10,x-30,y+100,x,y+100))# 11 TPU bumpersi=10; x=x0+i*dx; y=y0+i*dyfor cx,cy in ((0,0),(190-14,0),(0,96-14),(190-14,96-14)):    parts.append(plate(x+cx*K,y+cy*K,14,14,"#333","#111",0.8,4))parts.append(balloon(11,x-30,y+14,x,y+14))# 12 antennas (v0.2: 2 × Wi-Fi flex + 1 × BLE chip antenna; the four end-cap cellular flexes are deleted)i=11; x=x0+i*dx; y=y0+i*dyparts.append(plate(x+12*K,y+30*K,40,8,"#cfe0ff","#1a4fa0",1,1))  # Wi-Fi ant 1, front upper bandparts.append(plate(x+130*K,y+30*K,40,8,"#cfe0ff","#1a4fa0",1,1))  # Wi-Fi ant 2parts.append(plate(x+12*K,y+75*K,6,12,"#fff9c4","#f9a825",1,1))  # BLE chip antenna, bottom-left (on EC), keep-out X 12–18, Y 75–87parts.append(balloon(12,x-30,y+60,x+12*K,y+34*K))# 13 gasketsi=12; x=x0+i*dx; y=y0+i*dyparts.append(plate(x,y,190,96,"none","#c62828",2.0,12,1,'stroke-dasharray="5 3"'))parts.append(balloon(13,x-30,y+120,x,y+120))# 14 screws (few)i=13; x=x0+i*dx; y=y0+i*dyfor k in range(6):    parts.append(d.circle(x+20+k*60,y+30-k*18,4,"#ddd","#222",0.8))parts.append(balloon(14,x-30,y+40,x+20,y+30))d.g("parts","\n".join(parts),"exploded parts stack")# parts listpl=[]px,py=1110,110rows=[("1","Shutter slats, 2 × 6061-T6 2.0 × 20 × 170, Type III hard anodize, hinge pin, 8 slide pins, 2 N52 magnets","2"),("2","POM-C T-track rails with detent cartridges (ball Ø3 + spring), brush seals","2"),("3","PC+30 %GF front frame + plain end caps (10 mm, rail grooves + drains, no RF window): OLED lens, Wi-Fi antenna zones (upper band), BLE window, button seat, battery-door aperture","1"),("4","2.42\" OLED 128×64 (SSD1309) + FPC; LED bar flex, 5 RGB + light guide","1+1"),("5","Battery pack: 4 × 21700 2S2P 72 Wh in PC+GF cradle, pack PCB (BQ40Z50-R2), foam, JST-GH","1"),("6","Reader daughterboard 172 × 38 (8 L): CFast, CFe B, CFe A, SD UHS-II, microSD (4 mm gaps, 163 mm row); GL9755, GL9750, ASM1064, eFuses, Hall","1"),("7","Mezzanine pair Samtec ADM6/ADF6, 80-pin, 5 mm stack","1"),("8","Main board 165 × 80 (10 L): RK3588 SoM, ASM2812, RTL8126, ASM2464PD + PI3DBS16412, ASM3142, M.2 ×2 (Wi-Fi 2230, NVMe 2280), BQ25792, TPS25750, EC nRF5340, TPM, USB-C ×3, RJ45","1"),("9","Thermal gap pads (SoM, switch, PHY, CFe B cage, CFast cage)","5"),("10","Magnesium AZ91D die-cast chassis (no RF window): rear + bottom skins, 1.6 walls, ribs, masked bosses, pocket step, vent, 1/4-20 insert; conversion coat + light grey powder coat","1"),("11","TPU Shore 80A bumpers / overmold (corners ×4, edges, RJ45 door, button cap)","1 set"),("12","Antennas: 2 × Wi-Fi 2.4/5/6 GHz tri-band flex (front frame upper band, 118 mm apart, 2 × MHF4), 1 × BLE chip antenna on the EC (bottom-left PC window)","3"),("13","Gaskets: frame perimeter silicone 40 A, battery-door perimeter, shutter perimeter, wiper lip, shingle strip, EMI fabric-over-foam","1 set"),("14","Hardware (Zn-Ni steel / Al, no stainless on Mg): 14 × M2.5 frame screws, 6 captive door screws, 8 + 6 standoff, 4 SoM, 4 pack screws, USB-C brackets ×3, RJ45 rivets","1 set"),("15","Battery door, PC+30 %GF, gasketed, 154 × 50, service access to pack (EU Battery Regulation 2023/1542)","1")]pl.append(d.rect(px-10,py-24,360,24+len(rows)*46,"#fff","#222",1))pl.append(d.text(px,py-8,"PARTS LIST",11,"start","#111",'font-weight="bold"')); pl.append(d.text(px+320,py-8,"Qty",10,"start","#111",'font-weight="bold"'))for k,(n,desc,q) in enumerate(rows):    yy=py+k*46    pl.append(d.circle(px+8,yy+8,9,"#fff","#c00",1.2)); pl.append(d.text(px+8,yy+12,n,9,"middle","#c00",'font-weight="bold"'))    # wrap desc at ~60 chars    words=desc.split(); lines=[]; cur=""    for w in words:        if len(cur)+len(w)+1>62: lines.append(cur); cur=w        else: cur=(cur+" "+w).strip()    lines.append(cur)    for j,ln in enumerate(lines[:3]):        pl.append(d.text(px+24,yy+6+j*12,ln,8,"start","#222"))    pl.append(d.text(px+330,yy+12,q,9,"start","#222"))d.g("parts-list","\n".join(pl),"parts list")d.add(d.text(40,890,"Assembly order: 10 → 9 → 8 → 12 → 7 → 6 → 3 (+4, 13) → 14 → 5 → 15 → 2 → 1. Part 11 is 2-shot overmolded onto 3. See 08-mechanical.md §12.",9,"start","#444"))d.add(d.scalebar(40,925)); d.add(d.titleblock(1000,890))d.write("exploded-view.svg")print("ok")