// Pick & Place — TOP-DOWN VIEW // Single perspective: looking straight down on the machine. // Gantry rails appear as horizontal bars; head is a square carriage. // The nozzle, viewed from above, is a circle (down the barrel) with crosshairs. // Component "lifting" is shown by a soft shadow offset + slight scale, not perspective. const ORANGE = '#FF3D00'; const ORANGE_DIM = '#A82800'; const WHITE = '#FFFFFF'; // Component colors (physical objects stay dark) const INK_900 = '#0B0B0B'; const INK_800 = '#141414'; const INK_700 = '#1F1F1F'; const INK_600 = '#2B2B2B'; const INK_500 = '#3D3D3D'; const INK_400 = '#5C5C5C'; const INK_300 = '#8A8A8A'; const INK_200 = '#C4C4C4'; // Dark-theme machine surface colors const M_BG = '#080808'; const M_1 = '#111111'; const M_2 = '#1A1A1A'; const M_3 = '#242424'; const M_4 = '#2E2E2E'; const M_5 = '#404040'; // Text on dark background const T_PRI = '#EFEFEF'; const T_SEC = '#888888'; const T_TER = '#555555'; const W = 1920, H = 1080; // MACHINE BED (the work area visible from above) — fills most of the canvas const BED = { x: 120, y: 120, w: 1680, h: 840 }; // PCB — center-left of bed const PCB = { x: 280, y: 280, w: 820, h: 540 }; // FEEDER — right side; the reel itself is a circle, tape extends toward pick point const FEEDER = { reel: { cx: 1560, cy: 720, r: 130 }, tape: { x: 1380, y: 220, w: 36, h: 520 }, // vertical tape going up to pick point pick: { x: 1398, y: 250 } // where the head grabs from the tape }; // Component slots on PCB (pcb-local coords) const SLOTS = [ { x: 80, y: 90, w: 160, h: 80, type: 'ic', label: 'U1' }, { x: 280, y: 90, w: 70, h: 35, type: 'res', label: 'R1' }, { x: 380, y: 90, w: 70, h: 35, type: 'res', label: 'R2' }, { x: 490, y: 90, w: 100, h: 100, type: 'cap', label: 'C1' }, { x: 80, y: 240, w: 130, h: 130, type: 'qfn', label: 'U2' }, { x: 260, y: 260, w: 80, h: 80, type: 'led', label: 'D1' }, { x: 370, y: 260, w: 80, h: 80, type: 'led', label: 'D2' }, { x: 490, y: 240, w: 70, h: 35, type: 'res', label: 'R3' }, { x: 600, y: 240, w: 70, h: 35, type: 'res', label: 'R4' }, { x: 80, y: 420, w: 100, h: 50, type: 'cap', label: 'C2' }, { x: 220, y: 420, w: 100, h: 50, type: 'cap', label: 'C3' }, { x: 360, y: 420, w: 240, h: 60, type: 'conn', label: 'J1' }, ]; // t0 = when nozzle starts descending at PICK point. // Each cycle needs: prev place-up (~0.18) + travel-to-pick (~0.7) gap before the next t0. // Spacing minimum ~2.0s per step to avoid teleporting. const SEQUENCE = [ { slot: 0, t0: 1.4 }, { slot: 4, t0: 4.0 }, { slot: 11, t0: 6.6 }, { slot: 3, t0: 9.2 }, { slot: 5, t0: 11.4 }, { slot: 6, t0: 13.4 }, { slot: 1, t0: 15.4 }, { slot: 2, t0: 17.4 }, { slot: 7, t0: 19.4 }, { slot: 8, t0: 21.4 }, { slot: 9, t0: 23.4 }, { slot: 10, t0: 25.4 }, ]; // ───────────────────────────────────────────────────────────────────── // Component (top-down flat shape) // ───────────────────────────────────────────────────────────────────── function CompTop({ type, w, h, label }) { if (type === 'res') { return ( 10K ); } if (type === 'cap') { return ( ); } if (type === 'led') { return ( ); } if (type === 'ic') { return ( {Array.from({length: 10}).map((_,i) => ( ))} {label} ); } if (type === 'qfn') { return ( {Array.from({length: 7}).map((_,i) => { const step = w*0.84/7; const o = w*0.08 + i*step + step*0.18; const sz = step*0.65; return ( ); })} {label} ); } if (type === 'conn') { return ( {Array.from({length: 12}).map((_,i) => ( ))} ); } return ; } // Solder pad outline function SolderPad({ slot, highlight }) { return ( ); } // ───────────────────────────────────────────────────────────────────── // PCB (top view) — flat rectangle with copper traces visible // ───────────────────────────────────────────────────────────────────── function PCB_Board({ placedSet, activeSlot, placeFlash, targetSlot }) { return ( {/* mounting holes */} {[[24,24],[PCB.w-24,24],[24,PCB.h-24],[PCB.w-24,PCB.h-24]].map(([cx,cy],i) => ( ))} {/* copper traces */} {/* drill grid */} {Array.from({length: 14}).map((_,i) => Array.from({length: 9}).map((_,j) => ( )) )} {/* solder pads (unplaced) */} {SLOTS.map((s, i) => !placedSet.has(i) ? ( ) : null)} {/* placed components */} {SLOTS.map((s, i) => { if (!placedSet.has(i)) return null; const flash = i === activeSlot ? placeFlash : 0; return ( {flash > 0 && ( )} ); })} {/* silkscreen */} APW-PCB-001 REV.A APWAY ); } // ───────────────────────────────────────────────────────────────────── // Feeder (top view) — circular reel + vertical tape with components // ───────────────────────────────────────────────────────────────────── function FeederTop({ time, tapeOffset }) { const r = FEEDER.reel; const t = FEEDER.tape; const rotation = -time * 22; return ( {/* feeder housing */} {/* tape (vertical) */} {/* tape sprocket holes */} {Array.from({length: 18}).map((_,i) => ( ))} {Array.from({length: 18}).map((_,i) => ( ))} {/* component pockets visible on tape — small dark squares */} {Array.from({length: 16}).map((_,i) => { const py = t.y + 28 + i*30 + (tapeOffset%30); if (py < t.y + 4 || py > t.y + t.h - 16) return null; return ( ); })} {/* reel */} {/* spokes */} {Array.from({length: 8}).map((_,i) => ( ))} {/* outer perforations */} {Array.from({length: 32}).map((_,i) => ( ))} {/* feeder label (top) */} FEEDER · 08 ● LIVE {/* pickup point indicator */} ); } // ───────────────────────────────────────────────────────────────────── // Gantry (top view) // X-rail = horizontal bar across top of bed // Y-rail = vertical bar perpendicular, sliding along X-rail // Head/carriage = square that slides along Y-rail // Nozzle = circle with crosshair, looking down the barrel // "lift" of nozzle from work surface = scale of nozzle circle (bigger when lifted, smaller when down) // ───────────────────────────────────────────────────────────────────── function Gantry({ headX, headY, nozzleZ, holding, time, showLaser, gantryColor }) { const railW = BED.w; const xRailY = BED.y; // top of bed - X rail spans bed top const xRailH = 60; // Y-rail is a vertical bar that follows headX, spans bed height const yRailW = 60; // Head sits at intersection of Y-rail and current Y position // nozzleZ: 0 = up (high), 1 = down (touching surface) // Visually: when up, the nozzle circle has a wider outer ring (perspective of barrel further) // when down, it shrinks to the actual contact circle. const nozzleOuterR = 28 - nozzleZ * 8; // outer ring (barrel) shrinks as we descend const nozzleInnerR = 10 + nozzleZ * 6; // contact tip grows as we touch const nozzleBlur = (1 - nozzleZ) * 0.4; // when up, slight motion blur halo return ( {/* X-rail (horizontal, at top of bed) */} {/* tick marks */} {Array.from({length: Math.floor(railW/40) + 1}).map((_,i) => ( ))} {/* end caps */} {/* Y-rail — perpendicular bar that rides on X-rail at headX */} {/* X-carriage block (where Y-rail meets X-rail) */} {/* ID stripe */} {/* Y-rail bar going down across bed */} {/* Y-rail tick marks down its center */} {Array.from({length: Math.floor((BED.h)/40)}).map((_,i) => ( ))} {/* HEAD CARRIAGE — slides along Y-rail at headY */} {/* carriage outer body */} {/* top stripe */} {/* status lamps */} {/* head label */} HEAD·01 {/* corner bolts */} {[[-62,-42],[62,-42],[-62,42],[62,42]].map(([cx,cy],i) => ( ))} {/* mount plate around nozzle */} {/* NOZZLE (top view — concentric circles) */} {/* outer barrel ring */} {/* mid ring */} {/* held component (tiny, centered on nozzle, slightly visible from above) */} {holding && ( )} {/* inner contact tip */} 0.4 ? ORANGE : M_3} stroke={ORANGE} strokeWidth={1} opacity={nozzleZ > 0.4 ? 0.85 : 1}/> {/* center pinhole */} {/* crosshair */} {/* halo when lifted (suggests barrel sticking up) */} {nozzleBlur > 0 && ( )} ); } // ───────────────────────────────────────────────────────────────────── // Component shadow (drops on bed when component is held - shows it's lifted) // ───────────────────────────────────────────────────────────────────── function HeldShadow({ headX, headY, holding, nozzleZ }) { if (!holding || nozzleZ > 0.5) return null; // shadow offset proportional to lift (1 - nozzleZ): higher = bigger offset const lift = 1 - nozzleZ; const offset = lift * 6; return ( ); } // ───────────────────────────────────────────────────────────────────── // Telemetry HUD (top view — overlay corners) // ───────────────────────────────────────────────────────────────────── function Telemetry({ time, placedCount, total, headX, headY, currentLabel, phase }) { return ( {/* Top-left brand */} PICK · PLACE AP-PNP · TOP VIEW · LINE 04 {/* Top-right coords */} GANTRY POSITION X {String(Math.round(headX)).padStart(4,'0')} Y {String(Math.round(headY)).padStart(4,'0')} ● {phase} {/* Bottom-left stats */} PLACED {String(placedCount).padStart(2,'0')} /{String(total).padStart(2,'0')} T+ {time.toFixed(1)}s CURRENT {currentLabel || '—'} {/* Bottom-right logo */} APWAY APWAY · ASSEMBLY ); } // ───────────────────────────────────────────────────────────────────── // MAIN SCENE — computes head state + renders // ───────────────────────────────────────────────────────────────────── function PickPlaceScene({ tweaks }) { const rawTime = useTime(); const time = rawTime * tweaks.speed; const HEAD_PARK = { x: 960, y: 540 }; let headX = HEAD_PARK.x; let headY = HEAD_PARK.y; let nozzleZ = 0; let holding = null; let activeSlot = null; let placeFlash = 0; let targetSlot = null; let phase = 'IDLE'; const placedSet = new Set(); let currentLabel = ''; if (time < 0.6) { const t = Easing.easeOutCubic(clamp(time / 0.6, 0, 1)); headX = -200 + (HEAD_PARK.x + 200) * t; headY = HEAD_PARK.y; phase = 'INIT'; } for (let i = 0; i < SEQUENCE.length; i++) { const step = SEQUENCE[i]; const slot = SLOTS[step.slot]; const slotWorld = { x: PCB.x + slot.x + slot.w/2, y: PCB.y + slot.y + slot.h/2, }; const T_PICK_DOWN = 0.18; const T_PICK_HOLD = 0.10; const T_PICK_UP = 0.18; const T_TRAVEL_TO_PLACE = 0.55; const T_PLACE_DOWN = 0.20; const T_PLACE_HOLD = 0.12; const T_PLACE_UP = 0.18; const tPickStart = step.t0; const tPickDownEnd = tPickStart + T_PICK_DOWN; const tPickHoldEnd = tPickDownEnd + T_PICK_HOLD; const tPickUpEnd = tPickHoldEnd + T_PICK_UP; const tTravelEnd = tPickUpEnd + T_TRAVEL_TO_PLACE; const tPlaceDownEnd = tTravelEnd + T_PLACE_DOWN; const tPlaceHoldEnd = tPlaceDownEnd + T_PLACE_HOLD; const tPlaceUpEnd = tPlaceHoldEnd + T_PLACE_UP; // travel-to-pick starts exactly when previous place-up ends (or at 0 for first step) let tTravelToPickStart; if (i === 0) { tTravelToPickStart = Math.max(0, tPickStart - 0.8); } else { const prev = SEQUENCE[i-1]; tTravelToPickStart = prev.t0 + T_PICK_DOWN + T_PICK_HOLD + T_PICK_UP + T_TRAVEL_TO_PLACE + T_PLACE_DOWN + T_PLACE_HOLD + T_PLACE_UP; } const T_TRAVEL_TO_PICK = tPickStart - tTravelToPickStart; // mark earlier as placed for (let j = 0; j < i; j++) { const prev = SEQUENCE[j]; const prevPlaceUpEnd = prev.t0 + T_PICK_DOWN + T_PICK_HOLD + T_PICK_UP + T_TRAVEL_TO_PLACE + T_PLACE_DOWN + T_PLACE_HOLD; if (time >= prevPlaceUpEnd - 0.05) placedSet.add(prev.slot); } if (time >= tTravelToPickStart && time < tPickStart) { const t = Easing.easeInOutCubic(clamp((time - tTravelToPickStart) / T_TRAVEL_TO_PICK, 0, 1)); let startX = HEAD_PARK.x, startY = HEAD_PARK.y; if (i > 0) { const prev = SLOTS[SEQUENCE[i-1].slot]; startX = PCB.x + prev.x + prev.w/2; startY = PCB.y + prev.y + prev.h/2; } headX = startX + (FEEDER.pick.x - startX) * t; headY = startY + (FEEDER.pick.y - startY) * t; nozzleZ = 0; currentLabel = slot.label; targetSlot = step.slot; phase = 'TRAVEL → PICK'; break; } else if (time >= tPickStart && time < tPickDownEnd) { const t = Easing.easeOutCubic(clamp((time - tPickStart) / T_PICK_DOWN, 0, 1)); headX = FEEDER.pick.x; headY = FEEDER.pick.y; nozzleZ = t; currentLabel = slot.label; targetSlot = step.slot; phase = 'PICKING'; break; } else if (time >= tPickDownEnd && time < tPickHoldEnd) { headX = FEEDER.pick.x; headY = FEEDER.pick.y; nozzleZ = 1; holding = { ...slot }; currentLabel = slot.label; targetSlot = step.slot; phase = 'VACUUM ON'; break; } else if (time >= tPickHoldEnd && time < tPickUpEnd) { const t = Easing.easeOutCubic(clamp((time - tPickHoldEnd) / T_PICK_UP, 0, 1)); headX = FEEDER.pick.x; headY = FEEDER.pick.y; nozzleZ = 1 - t; holding = { ...slot }; currentLabel = slot.label; targetSlot = step.slot; phase = 'LIFTING'; break; } else if (time >= tPickUpEnd && time < tTravelEnd) { const t = Easing.easeInOutCubic(clamp((time - tPickUpEnd) / T_TRAVEL_TO_PLACE, 0, 1)); headX = FEEDER.pick.x + (slotWorld.x - FEEDER.pick.x) * t; headY = FEEDER.pick.y + (slotWorld.y - FEEDER.pick.y) * t; nozzleZ = 0; holding = { ...slot }; currentLabel = slot.label; targetSlot = step.slot; phase = 'TRAVEL → PLACE'; break; } else if (time >= tTravelEnd && time < tPlaceDownEnd) { const t = Easing.easeOutCubic(clamp((time - tTravelEnd) / T_PLACE_DOWN, 0, 1)); headX = slotWorld.x; headY = slotWorld.y; nozzleZ = t; holding = { ...slot }; activeSlot = step.slot; placeFlash = 0; currentLabel = slot.label; targetSlot = step.slot; phase = 'PLACING'; break; } else if (time >= tPlaceDownEnd && time < tPlaceHoldEnd) { headX = slotWorld.x; headY = slotWorld.y; nozzleZ = 1; holding = null; placedSet.add(step.slot); activeSlot = step.slot; placeFlash = 1 - (time - tPlaceDownEnd) / T_PLACE_HOLD; currentLabel = slot.label; phase = 'PLACED'; break; } else if (time >= tPlaceHoldEnd && time < tPlaceUpEnd) { const t = Easing.easeOutCubic(clamp((time - tPlaceHoldEnd) / T_PLACE_UP, 0, 1)); headX = slotWorld.x; headY = slotWorld.y; nozzleZ = 1 - t; holding = null; placedSet.add(step.slot); activeSlot = step.slot; placeFlash = (1 - t) * 0.4; currentLabel = slot.label; phase = 'RETRACT'; break; } else if (time >= tPlaceUpEnd) { placedSet.add(step.slot); headX = slotWorld.x; headY = slotWorld.y; nozzleZ = 0; holding = null; currentLabel = slot.label; phase = 'IDLE'; } } // tape moves down (reel feeds tape) when picking const tapeOffset = -time * 18; return ( {/* PCB sits on bed, beneath gantry */} {/* laser sight from head down to surface (only visible when nozzleZ < 0.6) */} {tweaks.showLaser && nozzleZ < 0.6 && ( )} {/* shadow of held component on bed below nozzle when lifted */} {/* gantry on top */} {tweaks.showHud && ( )} ); } // ───────────────────────────────────────────────────────────────────── // Background — black bed with technical grid // ───────────────────────────────────────────────────────────────────── function Background() { return ( {/* subtle dot grid only — no solid fill, no frame */} {/* bed grid */} {/* bed corner markers */} {[[BED.x, BED.y],[BED.x + BED.w, BED.y],[BED.x, BED.y+BED.h],[BED.x+BED.w, BED.y+BED.h]].map(([cx,cy],i) => ( ))} ); } // ───────────────────────────────────────────────────────────────────── // Time labeller — sets data-screen-label per second on root // ───────────────────────────────────────────────────────────────────── function TimeLabeler() { const time = useTime(); const sec = Math.floor(time); React.useEffect(() => { const root = document.querySelector('[data-screen-label]'); if (root) root.setAttribute('data-screen-label', `t=${sec}s`); }, [sec]); return null; } const TWEAK_DEFAULTS = { "speed": 1.0, "showHud": true, "showLaser": true, "gantryColor": "#1A1A1A", "bedScale": 1.0 }; // Export scene components for website.jsx Object.assign(window, { PickPlaceScene, Background, W, H, BED, FEEDER, PCB_Board, FeederTop, Gantry, Telemetry, HeldShadow, CompTop, SLOTS, SEQUENCE, TWEAK_DEFAULTS, TimeLabeler, SolderPad, M_BG, M_1, M_2, M_3, M_4, M_5, T_PRI, T_SEC, T_TER, ORANGE, ORANGE_DIM, }); // Only mount standalone App if website.jsx hasn't claimed the root window.__SCENE_APP = function() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); return (
); }; // Auto-mount only if website.jsx hasn't set a flag setTimeout(() => { if (!window.__WEBSITE_MOUNTED) { const App = window.__SCENE_APP; ReactDOM.createRoot(document.getElementById('root')).render(); } }, 0);