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}
{/* 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 };