const { useEffect, useState } = React; // ── Docking Ring (the centerpiece) ───────────────────────── function DockingRing({ pilder, body, hover, setHover }) { // P0-DOCKRING-NULL-BODY · 教練 2026-05-15 字面 spec: // if (!pilder || !body) → return null (不 render · 不 crash) // console.warn 留追跡 · 之後 Sprint 6 補 body 創建 endpoint 再 render // 紅線 (教練字面): // ❌ 不在這裡自動補 body // ❌ 不 fake data // ❌ 不改後端 schema // React Rules of Hooks: hooks 必須在 early-return 之前 (順序固定) const insc = window.HG2.INSCRIPTIONS; const [t, setT] = useState(0); useEffect(() => { if (!pilder || !body) return; // hook 內 guard · 不啟 raf let raf; const tick = () => { setT(performance.now() * 0.001); raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [pilder, body]); // 教練 spec 字面 early-return (hooks 之後 · 對齊 React Rules of Hooks) if (!pilder || !body) { if (pilder && !body) { console.warn("[HG2] pilder has no body", pilder.id); } return null; } const W = 1000, H = 720; const cx = W/2, cy = H/2; const ringR = 230; // main ring const ringR2 = 280; // outer const ringR3 = 180; // inner const tagPos = (a, r) => ({ x: cx + Math.cos((a) * Math.PI/180) * r, y: cy + Math.sin((a) * Math.PI/180) * r, }); return (
{/* P0-DOCKRING-NULL-BODY · 教練 2026-05-15 到此處時 body 必非 null (上方 early return 已守) · 直接讀 body.code 安全 */} DOCK · {pilder.code} → {body.code}
{/* outer ambience */} {/* concentric rings */} {/* rotating ticks */} {Array.from({length: 60}).map((_, i) => { const a = (i / 60) * Math.PI * 2; const r1 = ringR2 - 6, r2 = ringR2 + 4; const x1 = cx + Math.cos(a)*r1, y1 = cy + Math.sin(a)*r1; const x2 = cx + Math.cos(a)*r2, y2 = cy + Math.sin(a)*r2; return ; })} {/* counter-rotating inner */} {/* spokes from each inscription point to core */} {insc.map((ins, i) => { const p = tagPos(ins.angle, ringR); const isHover = hover === ins.id; const phase = (t * 0.4 + i * 0.14) % 1; return ( {/* particle */} {/* anchor pin */} ); })} {/* Pilder side line (left) */} {/* Body side line (right) */} {/* central core: diamond inside hex */} { const pts = []; for (let i=0; i<6; i++) { const a = (i/6) * Math.PI*2 - Math.PI/2; pts.push(`${Math.cos(a)*60},${Math.sin(a)*60}`); } return pts.join(" "); })()} fill="rgba(0,0,0,0.85)" stroke="var(--energy)" /> SOUL · BODY DOCK {/* API tags positioned around ring */} {insc.map((ins) => { const p = tagPos(ins.angle, ringR + 32); const align = Math.cos(ins.angle * Math.PI/180); const xPct = (p.x / W) * 100; const yPct = (p.y / H) * 100; return ( ); })} {/* Pilder card (left) */}
PILDER · 指揮艇
{pilder.name}
{pilder.code} · {pilder.tag}
RESONANCE{pilder.resonance.toFixed(1)}%
FIRMWARE
soul-{pilder.id}.fw.json
v3.14.{pilder.id.length}-stable
{/* Body card (right) · 到此處時 body 必非 null (上方 early-return guard) */}
ROBOT BODY · 機械身體
{body.name}
{body.code} · {body.subsidiary}
LOAD{body.load}%
CHANNELS
{body.channels.join(" · ")}
REGION · {body.region}
); } window.HG2Hub = { DockingRing };