/* fl-render.jsx — renders free-placement parts for the Falstad-style sandbox.
   Reuses the shared primitives from cl-flow.jsx (ClWire, ClResistorV, ClFlow,
   clVoltColor, fmt*). Charges = yellow; pathways/parts = voltage-coloured.
   Names flr*. Exports FlPart. */

function flrVoltAt(point, link, v) {
  const id = link.nodeOf(point);
  return id === 0 ? 0 : (v[id] || 0);
}

/* hit geometry: the point-chain a part occupies (for hover/select hit-testing) */
function flPartHitPts(part) {
  if (part.type === "ground") { const p = part.ends[0]; return [p, { x: p.x, y: p.y + 22 }]; }
  if (part.type === "pot") return [part.ends[0], part.ends[2], part.ends[1]];   // track + wiper
  if (part.type === "npn") return [part.ends[1], part.ends[2], part.ends[0]];   // C–E + base
  return part.ends;
}

/* live readout for the corner panel: I (mA), V-drop, R, P, node V. */
function flPartReadout(part, parts, link, v, eng, wireCur) {
  const ei = link.partElem.get(part.id);
  const p0 = part.ends[0], p1 = part.ends[1];
  const v0 = p0 ? flrVoltAt(p0, link, v) : 0;
  const v1 = p1 ? flrVoltAt(p1, link, v) : 0;
  const names = { resistor: "Resistor", battery: "Battery", led: "LED", diode: "Diode", wire: "Wire / jumper", ground: "Ground", cap: "Capacitor", switch: "Switch", ind: "Inductor", pot: "Potentiometer", npn: "Transistor (NPN)" };
  const rows = [];
  const mA = (x) => window.fmtI(Math.abs(x));
  if (part.type === "wire") {
    const I = wireCur ? (wireCur.get(part.id) || 0) : 0;
    rows.push(["I", mA(I)], ["V", window.fmtV(v0)], ["R", "0 \u03a9 (ideal)"]);
  } else if (part.type === "switch") {
    const I = wireCur ? (wireCur.get(part.id) || 0) : 0;
    rows.push(["state", part.closed ? "● closed" : "○ open"], ["I", part.closed ? mA(I) : "0.0 mA"], ["V", window.fmtV(part.closed ? v0 : v0 - v1)]);
  } else if (part.type === "ground") {
    rows.push(["V", "0.00 V (reference)"]);
  } else if (part.type === "battery") {
    const I = ei != null ? eng.elementCurrent(ei, v) : 0;
    const Vd = v0 - v1;
    rows.push(["EMF", window.fmtV(part.volts != null ? part.volts : 9)], ["I", mA(I)], ["V\u208a\u208b", window.fmtV(Vd)], ["P", window.fmtP(Math.abs(Vd * I))]);
  } else if (part.type === "led") {
    const I = ei != null ? eng.elementCurrent(ei, v) : 0;
    const Vd = v0 - v1;
    const lit = I > 0.0005;
    rows.push(["state", lit ? "◉ lit" : "○ dark"], ["I", mA(I)], ["V\u1d48", window.fmtV(Math.abs(Vd))], ["P", window.fmtP(Math.abs(Vd * I))]);
  } else if (part.type === "diode") {
    const I = ei != null ? eng.elementCurrent(ei, v) : 0;
    const Vd = v0 - v1;
    const cond = I > 0.0005;
    rows.push(["state", cond ? "▶ conducting" : "✕ blocking"], ["I", mA(I)], ["V\u1d48", window.fmtV(Vd)], ["dir", cond ? "forward" : "reverse"]);
  } else if (part.type === "cap") {
    const I = ei != null ? eng.elementCurrent(ei, v) : 0;
    const Vc = v0 - v1;
    rows.push(["C", (part.value != null ? part.value : 2) + " \u00b5F*"], ["Vc", window.fmtV(Math.abs(Vc))], ["I", mA(I)], ["state", Math.abs(I) < 0.0004 ? "charged" : (I > 0 ? "charging" : "discharging")]);
  } else if (part.type === "pot") {
    const e0 = link.partElem.get(part.id), e1 = link.partElem2 ? link.partElem2.get(part.id) : null;
    const iTop = e0 != null ? eng.elementCurrent(e0, v) : 0;
    const vw = part.ends[1] ? flrVoltAt(part.ends[1], link, v) : 0;
    rows.push(["pos", Math.round((part.pos != null ? part.pos : 0.5) * 100) + "%"], ["Rtot", window.fmtOhm(part.value != null ? part.value : 10000)], ["V(wiper)", window.fmtV(vw)], ["I", mA(iTop)]);
  } else if (part.type === "npn") {
    const Ic = ei != null ? eng.elementCurrent(ei, v) : 0;
    const vb = part.ends[0] ? flrVoltAt(part.ends[0], link, v) : 0;
    const vc = part.ends[1] ? flrVoltAt(part.ends[1], link, v) : 0;
    const ve = part.ends[2] ? flrVoltAt(part.ends[2], link, v) : 0;
    const vce = vc - ve, on = Ic > 0.0005, sat = on && vce < 0.35;
    rows.push(["state", on ? (sat ? "◉ on · sat" : "◉ on · active") : "○ off"], ["Ic", mA(Ic)], ["Vbe", window.fmtV(vb - ve)], ["Vce", window.fmtV(vce)]);
  } else {
    const I = ei != null ? eng.elementCurrent(ei, v) : 0;
    const Vd = v0 - v1;
    if (part.type === "resistor") rows.push(["R", window.fmtOhm(part.value != null ? part.value : 330)]);
    rows.push(["I", mA(I)], ["V\u1d48", window.fmtV(Math.abs(Vd))], ["P", window.fmtP(Math.abs(Vd * I))]);
  }
  return { name: names[part.type] || part.type, rows };
}

/* highlight halo + endpoint markers, drawn when a part is hovered/selected */
function FlHalo({ part, hot }) {
  const pts = flPartHitPts(part);
  const col = hot === "sel" ? "#8be0ff" : "#dff3ff";
  return (
    <g style={{ pointerEvents: "none" }}>
      <polyline points={pts.map(p => `${p.x},${p.y}`).join(" ")} fill="none"
                stroke={col} strokeOpacity={hot === "sel" ? 0.32 : 0.2} strokeWidth="15"
                strokeLinejoin="round" strokeLinecap="round" />
      {part.ends.map((e, i) => (
        <g key={i}>
          <circle cx={e.x} cy={e.y} r="6.5" fill="#0b0d11" stroke={col} strokeWidth="2" />
          <circle cx={e.x} cy={e.y} r="2.4" fill={col} />
        </g>
      ))}
    </g>
  );
}

/* LED: anode triangle → cathode bar, two leads, charges along the body, and a
   warm GLOW whose size/opacity scales with forward current (dark when reverse or
   off). Forward current = elementCurrent>0 (anode p0 → cathode p1). */
function flrLed({ part, link, v, eng, vref, flowMul }) {
  const [p0, p1] = part.ends;
  const dx = p1.x - p0.x, dy = p1.y - p0.y, len = Math.hypot(dx, dy) || 1;
  const ux = dx / len, uy = dy / len, px = -uy, py = ux;
  const c = { x: (p0.x + p1.x) / 2, y: (p0.y + p1.y) / 2 };
  const ei = link.partElem.get(part.id);
  const cur = ei != null ? eng.elementCurrent(ei, v) : 0;   // >0 = forward
  const v0 = flrVoltAt(p0, link, v), v1 = flrVoltAt(p1, link, v);
  const bright = Math.max(0, Math.min(1, cur / 0.018));      // 18mA → full
  const base = { x: c.x - ux * 9, y: c.y - uy * 9 };          // triangle base (anode side)
  const apex = { x: c.x + ux * 9, y: c.y + uy * 9 };          // triangle tip (cathode side)
  const tri = [
    { x: base.x + px * 11, y: base.y + py * 11 },
    { x: base.x - px * 11, y: base.y - py * 11 },
    apex,
  ];
  const core = bright > 0.02
    ? `rgb(255,${Math.round(120 + 110 * bright)},${Math.round(40 + 50 * bright)})`
    : "#1c222b";
  return (
    <g>
      {/* glow (under the symbol), scales with brightness */}
      {bright > 0.02 && (
        <g style={{ pointerEvents: "none" }}>
          <circle cx={c.x} cy={c.y} r={26} fill="#ff6a2e" opacity={0.10 * bright} />
          <circle cx={c.x} cy={c.y} r={16} fill="#ff7e3a" opacity={0.20 * bright} />
          <circle cx={c.x} cy={c.y} r={9} fill="#ffd07a" opacity={0.45 * bright} />
        </g>
      )}
      {/* leads (voltage-coloured) + charges along the whole body */}
      <window.ClWire pts={[p0, base]} current={cur} vStart={v0} vEnd={v0} vref={vref} flowMul={flowMul} gap={22} width={2.6} dots={false} />
      <window.ClWire pts={[apex, p1]} current={cur} vStart={v1} vEnd={v1} vref={vref} flowMul={flowMul} gap={22} width={2.6} dots={false} />
      {/* triangle */}
      <polygon points={tri.map(t => `${t.x},${t.y}`).join(" ")} fill={core} stroke="var(--ink)" strokeWidth="1.8" strokeLinejoin="round" />
      {/* cathode bar at the apex */}
      <line x1={apex.x + px * 11} y1={apex.y + py * 11} x2={apex.x - px * 11} y2={apex.y - py * 11} stroke="var(--ink)" strokeWidth="3" strokeLinecap="round" />
      {/* emission arrows when lit */}
      {bright > 0.25 && [0, 1].map(i => {
        const o = 13 + i * 7, a = { x: c.x + px * o - ux * 4, y: c.y + py * o - uy * 4 };
        const tip = { x: a.x + px * 7 + ux * 3, y: a.y + py * 7 + uy * 3 };
        return <line key={i} x1={a.x} y1={a.y} x2={tip.x} y2={tip.y} stroke="#ffcf8a" strokeWidth="1.6" strokeLinecap="round" opacity={bright} />;
      })}
      <window.ClFlow path={window.clPath([p0, p1])} current={cur} speedMul={flowMul} gap={26} />
    </g>
  );
}

/* plain diode: anode triangle → cathode bar (like the LED, but no glow). The body
   tints amber while conducting so "forward vs blocked" reads at a glance. */
function flrDiode({ part, link, v, eng, vref, flowMul }) {
  const [p0, p1] = part.ends;
  const dx = p1.x - p0.x, dy = p1.y - p0.y, len = Math.hypot(dx, dy) || 1;
  const ux = dx / len, uy = dy / len, px = -uy, py = ux;
  const c = { x: (p0.x + p1.x) / 2, y: (p0.y + p1.y) / 2 };
  const ei = link.partElem.get(part.id);
  const cur = ei != null ? eng.elementCurrent(ei, v) : 0;   // >0 = forward
  const v0 = flrVoltAt(p0, link, v), v1 = flrVoltAt(p1, link, v);
  const cond = cur > 0.0005;
  const base = { x: c.x - ux * 9, y: c.y - uy * 9 };
  const apex = { x: c.x + ux * 9, y: c.y + uy * 9 };
  const tri = [
    { x: base.x + px * 10, y: base.y + py * 10 },
    { x: base.x - px * 10, y: base.y - py * 10 },
    apex,
  ];
  return (
    <g>
      <window.ClWire pts={[p0, base]} current={cur} vStart={v0} vEnd={v0} vref={vref} flowMul={flowMul} gap={22} width={2.6} dots={false} />
      <window.ClWire pts={[apex, p1]} current={cur} vStart={v1} vEnd={v1} vref={vref} flowMul={flowMul} gap={22} width={2.6} dots={false} />
      <polygon points={tri.map(t => `${t.x},${t.y}`).join(" ")} fill={cond ? "#e8a13a" : "#1c222b"} stroke="var(--ink)" strokeWidth="1.8" strokeLinejoin="round" />
      <line x1={apex.x + px * 10} y1={apex.y + py * 10} x2={apex.x - px * 10} y2={apex.y - py * 10} stroke="var(--ink)" strokeWidth="3" strokeLinecap="round" />
      <window.ClFlow path={window.clPath([p0, p1])} current={cur} speedMul={flowMul} gap={26} />
    </g>
  );
}
function flrCap({ part, link, v, eng, vref, flowMul }) {
  const [p0, p1] = part.ends;
  const dx = p1.x - p0.x, dy = p1.y - p0.y, len = Math.hypot(dx, dy) || 1;
  const ux = dx / len, uy = dy / len, px = -uy, py = ux;
  const c = { x: (p0.x + p1.x) / 2, y: (p0.y + p1.y) / 2 };
  const ei = link.partElem.get(part.id);
  const cur = ei != null ? eng.elementCurrent(ei, v) : 0;
  const v0 = flrVoltAt(p0, link, v), v1 = flrVoltAt(p1, link, v);
  const plA = { x: c.x - ux * 5, y: c.y - uy * 5 };
  const plB = { x: c.x + ux * 5, y: c.y + uy * 5 };
  const half = 15;
  return (
    <g>
      <window.ClWire pts={[p0, plA]} current={cur} vStart={v0} vEnd={v0} vref={vref} flowMul={flowMul} gap={22} width={2.6} />
      <window.ClWire pts={[plB, p1]} current={cur} vStart={v1} vEnd={v1} vref={vref} flowMul={flowMul} gap={22} width={2.6} />
      <line x1={plA.x + px * half} y1={plA.y + py * half} x2={plA.x - px * half} y2={plA.y - py * half} stroke="var(--ink)" strokeWidth="3.2" strokeLinecap="round" />
      <line x1={plB.x + px * half} y1={plB.y + py * half} x2={plB.x - px * half} y2={plB.y - py * half} stroke="var(--ink)" strokeWidth="3.2" strokeLinecap="round" />
    </g>
  );
}

/* inductor: a row of small loops (coil) between the two ends. */
function flrInductor({ part, link, v, eng, vref, flowMul }) {
  const [p0, p1] = part.ends;
  const dx = p1.x - p0.x, dy = p1.y - p0.y, len = Math.hypot(dx, dy) || 1;
  const ux = dx / len, uy = dy / len, px = -uy, py = ux;
  const ei = link.partElem.get(part.id);
  const cur = ei != null ? eng.elementCurrent(ei, v) : 0;
  const v0 = flrVoltAt(p0, link, v), v1 = flrVoltAt(p1, link, v);
  const coils = 4, span = Math.min(80, len * 0.6), s0 = (len - span) / 2, r = span / (coils * 2);
  const a0 = { x: p0.x + ux * s0, y: p0.y + uy * s0 };
  let d = `M ${a0.x} ${a0.y}`;
  for (let i = 0; i < coils; i++) {
    const cx = a0.x + ux * (r * (2 * i + 1)), cy = a0.y + uy * (r * (2 * i + 1));
    const sweepEnd = { x: a0.x + ux * (r * (2 * i + 2)), y: a0.y + uy * (r * (2 * i + 2)) };
    d += ` A ${r} ${r} 0 1 1 ${sweepEnd.x} ${sweepEnd.y}`;
  }
  return (
    <g>
      <window.ClWire pts={[p0, a0]} current={cur} vStart={v0} vEnd={v0} vref={vref} flowMul={flowMul} gap={22} width={2.6} />
      <window.ClWire pts={[{ x: a0.x + ux * span, y: a0.y + uy * span }, p1]} current={cur} vStart={v1} vEnd={v1} vref={vref} flowMul={flowMul} gap={22} width={2.6} />
      <path d={d} fill="none" stroke="var(--ink)" strokeWidth="2.6" strokeLinecap="round" />
      <window.ClFlow path={window.clPath([p0, p1])} current={cur} speedMul={flowMul} gap={26} />
    </g>
  );
}

/* potentiometer: a resistor track (end0→end2) with a wiper arrow tapping it at
   `pos`. Two halves coloured by their own voltage gradient; charges on each. */
function flrPot({ part, link, v, eng, vref, flowMul }) {
  const a = part.ends[0], b = part.ends[2], w = part.ends[1];
  const pos = part.pos != null ? part.pos : 0.5;
  const contact = { x: a.x + (b.x - a.x) * (1 - pos), y: a.y + (b.y - a.y) * (1 - pos) };
  const e0 = link.partElem.get(part.id), e1 = link.partElem2 ? link.partElem2.get(part.id) : null;
  const iTop = e0 != null ? eng.elementCurrent(e0, v) : 0;
  const iBot = e1 != null ? eng.elementCurrent(e1, v) : 0;
  const va = flrVoltAt(a, link, v), vb = flrVoltAt(b, link, v), vw = flrVoltAt(w, link, v);
  // wiper arrowhead
  const wdx = contact.x - w.x, wdy = contact.y - w.y, wl = Math.hypot(wdx, wdy) || 1;
  const wux = wdx / wl, wuy = wdy / wl, wpx = -wuy, wpy = wux;
  const tip = { x: contact.x - wux * 4, y: contact.y - wuy * 4 };
  return (
    <g>
      <window.ClResistorV x0={a.x} y0={a.y} x1={contact.x} y1={contact.y} vStart={va} vEnd={vw} vref={vref} width={3} />
      <window.ClResistorV x0={contact.x} y0={contact.y} x1={b.x} y1={b.y} vStart={vw} vEnd={vb} vref={vref} width={3} />
      <window.ClFlow path={window.clPath([a, contact])} current={iTop} speedMul={flowMul} gap={24} />
      <window.ClFlow path={window.clPath([contact, b])} current={iBot} speedMul={flowMul} gap={24} />
      {/* wiper lead + arrowhead */}
      <line x1={w.x} y1={w.y} x2={tip.x} y2={tip.y} stroke="var(--ink)" strokeWidth="2.4" strokeLinecap="round" />
      <path d={`M ${tip.x} ${tip.y} L ${tip.x - wux * 9 + wpx * 5} ${tip.y - wuy * 9 + wpy * 5} L ${tip.x - wux * 9 - wpx * 5} ${tip.y - wuy * 9 - wpy * 5} Z`} fill="var(--ink)" />
      <window.ClFlow path={window.clPath([w, contact])} current={iBot - iTop} speedMul={flowMul} gap={22} />
    </g>
  );
}

/* NPN transistor: base bar with collector & emitter leads; emitter arrow points
   out (NPN). Charges flow collector→emitter scaled by Ic; lit ring when ON. */
function flrNpn({ part, link, v, eng, vref, flowMul }) {
  const base = part.ends[0], col = part.ends[1], emi = part.ends[2];
  const c = { x: (col.x + emi.x) / 2, y: (col.y + emi.y) / 2 };
  const ei = link.partElem.get(part.id);
  const Ic = ei != null ? eng.elementCurrent(ei, v) : 0;
  const on = Ic > 0.0005;
  // base bar: perpendicular to base→center
  const bdx = c.x - base.x, bdy = c.y - base.y, bl = Math.hypot(bdx, bdy) || 1;
  const bux = bdx / bl, buy = bdy / bl, bpx = -buy, bpy = bux;
  const bp = { x: c.x - bux * 6, y: c.y - buy * 6 };           // bar sits just before center
  const barA = { x: bp.x + bpx * 14, y: bp.y + bpy * 14 };
  const barB = { x: bp.x - bpx * 14, y: bp.y - bpy * 14 };
  // contact points on the bar for collector & emitter (nearest)
  const dA = Math.hypot(barA.x - col.x, barA.y - col.y), dB = Math.hypot(barB.x - col.x, barB.y - col.y);
  const cC = dA < dB ? barA : barB, cE = dA < dB ? barB : barA;
  const vb = flrVoltAt(base, link, v), vc = flrVoltAt(col, link, v), ve = flrVoltAt(emi, link, v);
  // emitter arrowhead near its bar contact
  const emdx = cE.x - emi.x, emdy = cE.y - emi.y, eml = Math.hypot(emdx, emdy) || 1;
  const eux = emdx / eml, euy = emdy / eml, epx = -euy, epy = eux;
  const ah = { x: emi.x + eux * (eml - 10), y: emi.y + euy * (eml - 10) };
  return (
    <g>
      {on && <circle cx={c.x} cy={c.y} r={22} fill="#ff8a3a" opacity={Math.min(0.5, Ic / 0.1)} style={{ pointerEvents: "none" }} />}
      {/* device envelope circle */}
      <circle cx={c.x} cy={c.y} r={20} fill="var(--bg-deeper, #0b0d11)" stroke="var(--ink)" strokeWidth="2" opacity="0.9" />
      {/* leads */}
      <window.ClWire pts={[base, bp]} current={0} vStart={vb} vEnd={vb} vref={vref} flowMul={flowMul} gap={22} width={2.4} dots={false} />
      <window.ClWire pts={[col, cC]} current={Ic} vStart={vc} vEnd={vc} vref={vref} flowMul={flowMul} gap={22} width={2.4} dots={false} />
      <window.ClWire pts={[emi, cE]} current={Ic} vStart={ve} vEnd={ve} vref={vref} flowMul={flowMul} gap={22} width={2.4} dots={false} />
      {/* base bar */}
      <line x1={barA.x} y1={barA.y} x2={barB.x} y2={barB.y} stroke="var(--ink)" strokeWidth="3.4" strokeLinecap="round" />
      {/* emitter arrowhead */}
      <path d={`M ${ah.x} ${ah.y} L ${ah.x - eux * 9 + epx * 5} ${ah.y - euy * 9 + epy * 5} L ${ah.x - eux * 9 - epx * 5} ${ah.y - euy * 9 - epy * 5} Z`} fill="var(--ink)" />
      {/* terminal labels */}
      <text x={base.x - 4} y={base.y - 6} textAnchor="end" fontFamily="IBM Plex Mono, monospace" fontSize="11" fill="var(--ink-soft)">B</text>
      <text x={col.x + 7} y={col.y + 4} fontFamily="IBM Plex Mono, monospace" fontSize="11" fill="var(--ink-soft)">C</text>
      <text x={emi.x + 7} y={emi.y + 4} fontFamily="IBM Plex Mono, monospace" fontSize="11" fill="var(--ink-soft)">E</text>
      <window.ClFlow path={window.clPath([col, emi])} current={Ic} speedMul={flowMul} gap={24} />
    </g>
  );
}

/* schematic battery: two plates (long=+, short=−) midway between the ends.
   Convention copied from the accepted cl-schematic battery: charges ride the two
   LEADS (never through the plate gap), each ordered so the delivering current
   flows + terminal → outward and inward → − terminal. `cur` (V elementCurrent)
   is +ve when the battery delivers (current exits the + terminal). */
function flrBattery({ part, link, v, eng, vref, flowMul }) {
  const [p0, p1] = part.ends;           // p0 = +, p1 = −
  const mx = (p0.x + p1.x) / 2, my = (p0.y + p1.y) / 2;
  const dx = p1.x - p0.x, dy = p1.y - p0.y, len = Math.hypot(dx, dy) || 1;
  const ux = dx / len, uy = dy / len;   // unit vector along (+→−)
  const px = -uy, py = ux;              // perpendicular
  const ei = link.partElem.get(part.id);
  const cur = ei != null ? eng.elementCurrent(ei, v) : 0;
  const v0 = flrVoltAt(p0, link, v), v1 = flrVoltAt(p1, link, v);
  const plus = { x: mx + ux * -8, y: my + uy * -8 };   // long plate (+) side
  const minus = { x: mx + ux * 8, y: my + uy * 8 };    // short plate (−) side
  const plate = (off, halfW, w) => {
    const cx = mx + ux * off, cy = my + uy * off;
    return <line x1={cx + px * halfW} y1={cy + py * halfW} x2={cx - px * halfW} y2={cy - py * halfW}
                 stroke="var(--ink)" strokeWidth={w} strokeLinecap="round" />;
  };
  return (
    <g>
      {/* + lead: charges flow plate → p0 (out of +) when cur>0 */}
      <window.ClWire pts={[plus, p0]} current={cur} vStart={v0} vEnd={v0} vref={vref} flowMul={flowMul} gap={24} width={2.6} />
      {/* − lead: charges flow p1 → plate (into −) when cur>0 */}
      <window.ClWire pts={[p1, minus]} current={cur} vStart={v1} vEnd={v1} vref={vref} flowMul={flowMul} gap={24} width={2.6} />
      {plate(-8, 13, 3.4)}{/* long = + */}
      {plate(8, 7, 2.4)} {/* short = − */}
      <text x={mx + px * 20} y={my + py * 20 + 4} textAnchor="middle" fontFamily="IBM Plex Mono, monospace" fontSize="13" fill="var(--ink-soft)">{window.fmtV(part.volts != null ? part.volts : 9)}</text>
    </g>
  );
}

/* ground symbol: three shrinking bars below the end point */
function flrGround({ part }) {
  const p = part.ends[0];
  return (
    <g stroke="var(--ink-soft)" strokeWidth="2.2" strokeLinecap="round">
      <line x1={p.x} y1={p.y} x2={p.x} y2={p.y + 12} />
      <line x1={p.x - 11} y1={p.y + 12} x2={p.x + 11} y2={p.y + 12} />
      <line x1={p.x - 7} y1={p.y + 17} x2={p.x + 7} y2={p.y + 17} />
      <line x1={p.x - 3} y1={p.y + 22} x2={p.x + 3} y2={p.y + 22} />
    </g>
  );
}

/* switch: two terminals + a pivoting lever. Closed = lever bridges the gap and
   conducts (charges flow, current from wireCur); open = lever lifts, no current. */
function flrSwitch({ part, link, v, vref, flowMul, wireCur }) {
  const [p0, p1] = part.ends;
  const dx = p1.x - p0.x, dy = p1.y - p0.y, len = Math.hypot(dx, dy) || 1;
  const ux = dx / len, uy = dy / len, px = -uy, py = ux;
  const h0 = { x: p0.x + ux * len * 0.34, y: p0.y + uy * len * 0.34 };  // hinge
  const h1 = { x: p0.x + ux * len * 0.66, y: p0.y + uy * len * 0.66 };  // contact
  const v0 = flrVoltAt(p0, link, v), v1 = flrVoltAt(p1, link, v);
  const closed = !!part.closed;
  const I = wireCur ? (wireCur.get(part.id) || 0) : 0;
  const L = len * 0.40, a = 0.62;
  const lift = { x: h0.x + (ux * Math.cos(a) - px * Math.sin(a)) * L,
                 y: h0.y + (uy * Math.cos(a) - py * Math.sin(a)) * L };
  return (
    <g>
      <window.ClWire pts={[p0, h0]} current={closed ? I : 0} vStart={v0} vEnd={v0} vref={vref} flowMul={flowMul} gap={22} width={2.8} />
      <window.ClWire pts={[h1, p1]} current={closed ? I : 0} vStart={v1} vEnd={v1} vref={vref} flowMul={flowMul} gap={22} width={2.8} />
      <circle cx={h0.x} cy={h0.y} r="3.2" fill="var(--ink)" />
      <circle cx={h1.x} cy={h1.y} r="3.2" fill="var(--ink)" />
      <line x1={h0.x} y1={h0.y} x2={closed ? h1.x : lift.x} y2={closed ? h1.y : lift.y}
            stroke={closed ? "var(--ink)" : "var(--ink-soft)"} strokeWidth="3.2" strokeLinecap="round" />
      {closed && <window.ClFlow path={window.clPath([h0, h1])} current={I} speedMul={flowMul} gap={20} />}
    </g>
  );
}

function FlPart({ part, parts, link, v, eng, vref, flowMul, wireCur }) {
  const ei = link.partElem.get(part.id);
  const cur = ei != null ? eng.elementCurrent(ei, v) : 0;
  const [p0, p1] = part.ends;
  const v0 = p0 ? flrVoltAt(p0, link, v) : 0;
  const v1 = p1 ? flrVoltAt(p1, link, v) : 0;

  switch (part.type) {
    case "wire": {
      const wc = wireCur ? (wireCur.get(part.id) || 0) : 0;
      return <window.ClWire pts={part.ends} current={wc} vStart={v0} vEnd={v0} vref={vref} flowMul={flowMul} gap={26} width={3} />;
    }
    case "resistor":
      return (
        <g>
          <window.ClResistorV x0={p0.x} y0={p0.y} x1={p1.x} y1={p1.y} vStart={v0} vEnd={v1} vref={vref} width={3} />
          <window.ClFlow path={window.clPath([p0, p1])} current={cur} speedMul={flowMul} gap={26} />
        </g>
      );
    case "led":
      return flrLed({ part, link, v, eng, vref, flowMul });
    case "diode":
      return flrDiode({ part, link, v, eng, vref, flowMul });
    case "cap":
      return flrCap({ part, link, v, eng, vref, flowMul });
    case "ind":
      return flrInductor({ part, link, v, eng, vref, flowMul });
    case "pot":
      return flrPot({ part, link, v, eng, vref, flowMul });
    case "npn":
      return flrNpn({ part, link, v, eng, vref, flowMul });
    case "battery": return flrBattery({ part, link, v, eng, vref, flowMul });
    case "switch":  return flrSwitch({ part, link, v, vref, flowMul, wireCur });
    case "ground":  return flrGround({ part });
    default:        return null;
  }
}

Object.assign(window, { FlPart, FlHalo, flrVoltAt, flPartHitPts, flPartReadout });
