OverviewFilesCommitsIssuesDocsReleasesPeers

electrical-review / design / racking-diagrams.jsx

351 lines · jsx ref:
/* ============================================================================
   PRSM SOLAR — RACKING STRUCTURAL DIAGRAMS (native SVG)
   Side elevation (section), array face, footing plan + the two option diagrams.
   Faithful to locked geometry: 55° tilt · 6×6 back / 4×4 front posts ·
   2×10 inside joists · 2×8 beams · 4–5 ft frost footings · 2-high × 7-wide.
   Reuses the global ZoomPan from schematics.jsx.
   ========================================================================== */
const { useState: useStateRD } = React;

const RWire = ({ d, cls = "" }) => <path className={"wire " + cls} d={d} />;
const RSym = ({ d, cls = "" }) => <path className={"sym " + cls} d={d} />;

function Dim({ x1, y1, x2, y2, label, off = 0, anchor = "middle", flip = false }) {
  const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
  const horiz = Math.abs(x2 - x1) > Math.abs(y2 - y1);
  const tx = horiz ? 0 : 5, ty = horiz ? 5 : 0;
  return (
    <g>
      <line className="dim-l" x1={x1} y1={y1} x2={x2} y2={y2} />
      <line className="dim-l" x1={x1 - tx} y1={y1 - ty} x2={x1 + tx} y2={y1 + ty} />
      <line className="dim-l" x1={x2 - tx} y1={y2 - ty} x2={x2 + tx} y2={y2 + ty} />
      <text className="dim-t" x={mx + (horiz ? 0 : (flip ? -10 : 10))} y={my + (horiz ? (flip ? -7 : 16) : 4)}
        textAnchor={horiz ? "middle" : (flip ? "end" : "start")}>{label}</text>
    </g>
  );
}
function Call({ x, y, tx, ty, label, anchor = "start" }) {
  return (
    <g>
      <line className="lead" x1={x} y1={y} x2={tx} y2={ty} />
      <circle className="lead-dot" cx={x} cy={y} r="2.6" />
      <text className="call-t" x={tx + (anchor === "end" ? -6 : 6)} y={ty + 3.5} textAnchor={anchor}>{label}</text>
    </g>
  );
}
function Concrete({ x, w, yTop, yBot, label }) {
  const dots = [];
  for (let i = 0; i < 9; i++) {
    dots.push(<circle key={i} cx={x - w / 2 + 6 + Math.random() * (w - 12)} cy={yTop + 14 + Math.random() * (yBot - yTop - 20)} r={1.3 + Math.random()} className="agg" />);
  }
  return (
    <g>
      <path className="footing" d={`M${x - w / 2} ${yTop} L${x + w / 2} ${yTop} L${x + w / 2 - 4} ${yBot} Q${x} ${yBot + 8} ${x - w / 2 + 4} ${yBot} Z`} />
      {dots}
      {label && <text className="slabel" x={x} y={(yTop + yBot) / 2 + 30} textAnchor="middle">{label}</text>}
    </g>
  );
}

// reusable 1.8 m human silhouette for scale (front view)
function Human({ cx, feetY, S, label }) {
  const head = 0.13 * S, topY = feetY - 1.8 * S;
  const shoulder = topY + head * 2;
  return (
    <g>
      <g className="human">
        <circle cx={cx} cy={topY + head} r={head} />
        <path d={`M ${cx} ${shoulder}
          L ${cx - head * 1.15} ${shoulder + 0.6 * S}
          L ${cx - head * 0.7} ${feetY}
          L ${cx + head * 0.15} ${feetY}
          L ${cx + head * 0.22} ${shoulder + 0.48 * S}
          L ${cx + head * 1.18} ${shoulder + 0.6 * S} Z`} />
      </g>
      {label && <text className="slabel" x={cx} y={feetY + 16} textAnchor="middle">{label}</text>}
    </g>
  );
}

// graphic scale bar (alternating 0.5 m segments)
function ScaleBar({ x, y, S, meters = 2 }) {
  const seg = 0.5, n = Math.round(meters / seg), w = meters * S, h = 7;
  const rects = [];
  for (let i = 0; i < n; i++)
    rects.push(<rect key={i} x={x + i * seg * S} y={y} width={seg * S} height={h} className={i % 2 ? "sb-b" : "sb-a"} />);
  const ft = (meters * 3.28084).toFixed(1);
  return (
    <g>
      {rects}
      <rect x={x} y={y} width={w} height={h} className="sb-frame" />
      <text className="slabel" x={x} y={y - 6}>SCALE</text>
      <text className="slabel" x={x} y={y + 20}>0</text>
      <text className="slabel" x={x + w} y={y + 20} textAnchor="middle">{meters} m / {ft} ft</text>
    </g>
  );
}

/* ============================ SIDE ELEVATION (TRUE SCALE) ============= */
function RackElevation() {
  const S = 86, G = 392, X0 = 340;
  const M = (xm, ym) => ({ x: X0 + xm * S, y: G - ym * S });
  const t = 55 * Math.PI / 180;
  const cT = Math.cos(t), sT = Math.sin(t);
  const L = 2.26, clr = 0.5;
  const run = L * cT, rise = L * sT;
  const footM = 1.4, over = 0.26;

  const lowE = M(0, clr);
  const highE = M(run, clr + rise);
  const highO = M(run + over * cT, clr + rise + over * sT);
  const fpBase = M(0, -footM), bpBase = M(run, -footM);
  const bpTopM = clr + rise - 0.12;
  const bpTop = M(run, bpTopM);

  const dx = highO.x - lowE.x, dy = highO.y - lowE.y;
  const len = Math.hypot(dx, dy); const ux = dx / len, uy = dy / len;
  const nx = uy, ny = -ux;
  const off = (p, d) => ({ x: p.x + nx * d, y: p.y + ny * d });

  const INCH = 0.0254;
  const post4 = 3.5 * INCH * S, post6 = 5.5 * INCH * S;
  const joistD = 9.25 * INCH * S;
  const beamW = 1.5 * INCH * S, beamD = 7.25 * INCH * S;
  const panelT = 1.6 * INCH * S;

  const po = joistD / 2 + 0.05 * S + panelT / 2;
  const joistPoly = [off(lowE, joistD / 2), off(highO, joistD / 2), off(highO, -joistD / 2), off(lowE, -joistD / 2)].map(p => `${p.x},${p.y}`).join(" ");
  const panelPoly = [off(lowE, po - panelT / 2), off(highO, po - panelT / 2), off(highO, po + panelT / 2), off(lowE, po + panelT / 2)].map(p => `${p.x},${p.y}`).join(" ");
  const midC = { x: (lowE.x + highO.x) / 2, y: (lowE.y + highO.y) / 2 };
  const pMid = off(midC, po);
  const fpTopY = off(lowE, -joistD / 2).y, bpTopY = off(highE, -joistD / 2).y;
  const hx = X0 - 1.05 * S;

  return (
    <ZoomPan contentW={720} contentH={560} height={530} fig="FIG. R1 · SIDE ELEVATION (TRUE SCALE)">
      <line className="grade" x1={120} y1={G} x2={660} y2={G} />
      {[...Array(17)].map((_, i) => <line key={i} className="hatch" x1={130 + i * 32} y1={G} x2={122 + i * 32} y2={G + 12} />)}
      <text className="slabel" x={130} y={G + 28}>GRADE</text>
      <line className="frost" x1={290} y1={G + footM * S - 6} x2={520} y2={G + footM * S - 6} />
      <text className="slabel" x={520} y={G + footM * S - 12} textAnchor="end">FROST LINE ≈ 4 ft</text>
      <Concrete x={lowE.x} w={0.32 * S} yTop={G - 6} yBot={fpBase.y} label="30 MPa" />
      <Concrete x={highE.x} w={0.36 * S} yTop={G - 6} yBot={bpBase.y} label="30 MPa" />
      <rect className="post" x={lowE.x - post4 / 2} y={fpTopY} width={post4} height={fpBase.y - fpTopY - 22} rx="1" />
      <rect className="post" x={highE.x - post6 / 2} y={bpTopY} width={post6} height={bpBase.y - bpTopY - 22} rx="1" />
      <rect className="beam" x={lowE.x - beamW / 2} y={fpTopY - beamD} width={beamW} height={beamD} />
      <rect className="beam" x={highE.x - beamW / 2} y={bpTopY - beamD} width={beamW} height={beamD} />
      <polygon className="joist-band" points={joistPoly} />
      <polygon className="panel-poly" points={panelPoly} />
      <line className="panel-div" x1={pMid.x - nx * (panelT / 2 + 1.5)} y1={pMid.y - ny * (panelT / 2 + 1.5)} x2={pMid.x + nx * (panelT / 2 + 1.5)} y2={pMid.y + ny * (panelT / 2 + 1.5)} />
      <line className="dim-l" x1={lowE.x} y1={lowE.y} x2={lowE.x + 86} y2={lowE.y} />
      <path className="arc" d={`M ${lowE.x + 64} ${lowE.y} A 64 64 0 0 0 ${lowE.x + 64 * cT} ${lowE.y - 64 * sT}`} />
      <text className="dim-t" x={lowE.x + 52} y={lowE.y - 22}>55° tilt</text>
      <Human cx={hx} feetY={G} S={S} label="1.8 m / 5′11″" />
      <Dim x1={lowE.x - 0.42 * S} y1={lowE.y} x2={lowE.x - 0.42 * S} y2={G} label="0.5 m / 20 in" flip />
      <Dim x1={lowE.x - 0.42 * S} y1={G} x2={lowE.x - 0.42 * S} y2={fpBase.y} label="4–5 ft" />
      <Dim x1={highO.x + 0.5 * S} y1={highE.y} x2={highO.x + 0.5 * S} y2={G} label="2.35 m / 7′9″ to top edge" />
      <Call x={highE.x} y={(bpTop.y + G) / 2} tx={highO.x + 0.5 * S} ty={(bpTop.y + G) / 2 + 60} label="6×6 back post" />
      <Call x={lowE.x} y={(lowE.y + G) / 2} tx={lowE.x + 0.5 * S} ty={(lowE.y + G) / 2 + 28} label="4×4 front post" />
      <Call x={pMid.x} y={pMid.y} tx={pMid.x - 1.1 * S} ty={pMid.y - 0.5 * S} label="14 × 575 W · 2-high" anchor="end" />
      <Call x={off(midC, 0).x} y={off(midC, 0).y} tx={lowE.x - 0.1 * S} ty={highO.y + 0.7 * S} label="2×10 joist · cut 55°" anchor="end" />
      <text className="slabel" x={120} y={48}>MEMBERS DRAWN TO TRUE DRESSED-LUMBER CROSS-SECTION</text>
      <text className="slabel" x={120} y={62}>4×4 = 3.5″ · 6×6 = 5.5″ · 2×10 = 9.25″ deep · panel ≈ 40 mm</text>
    </ZoomPan>
  );
}

/* ============================ ARRAY FACE ============================== */
function RackFront() {
  const S = 42;
  const pw = 2.3, ph = 1.13, cols = 7, rows = 2;
  const cw = pw * S, rh = ph * S;
  const clr = 0.5, x0 = 250, yTop = 150;
  const x1 = x0 + cw * cols, yBot = yTop + rh * rows;
  const grade = yBot + clr * S;
  const cells = [];
  const gx = cw * 0.03, gy = rh * 0.03;
  for (let r = 0; r < rows; r++)
    for (let c = 0; c < cols; c++)
      cells.push(<rect key={r + "-" + c} className="panel-cell" x={x0 + c * cw + gx} y={yTop + r * rh + gy} width={cw - 2 * gx} height={rh - 2 * gy} rx="2" />);
  const posts = [];
  const post4f = 3.5 * 0.0254 * S;
  for (let i = 0; i <= cols; i++) {
    const px = x0 + i * cw;
    posts.push(<line key={i} className="post-front" x1={px} y1={yBot} x2={px} y2={grade} strokeWidth={post4f} />);
    posts.push(<line key={"d" + i} className="joist-back" x1={px} y1={yTop - 5} x2={px} y2={yBot} />);
  }
  const hx = 150;
  return (
    <ZoomPan contentW={1080} contentH={384} height={420} fig="FIG. R2 · ARRAY FACE (FRONT ELEVATION)">
      <line className="grade" x1={90} y1={grade} x2={x1 + 0.7 * S} y2={grade} />
      {[...Array(27)].map((_, i) => <line key={i} className="hatch" x1={100 + i * 32} y1={grade} x2={92 + i * 32} y2={grade + 12} />)}
      <text className="slabel" x={100} y={grade + 28}>GRADE</text>
      {posts}
      {cells}
      <rect className="array-frame" x={x0} y={yTop} width={x1 - x0} height={yBot - yTop} rx="3" />
      <Human cx={hx} feetY={grade} S={S} label="1.8 m / 5′11″" />
      <Dim x1={x0} y1={yBot + 52} x2={x1} y2={yBot + 52} label="16.1 m (52.8 ft) · 7 panels wide · 7 bays" />
      <line className="dim-l" x1={x0 - 24} y1={yTop} x2={x0 - 24} y2={yBot} />
      <line className="dim-l" x1={x0 - 29} y1={yTop} x2={x0 - 19} y2={yTop} />
      <line className="dim-l" x1={x0 - 29} y1={yBot} x2={x0 - 19} y2={yBot} />
      <text className="dim-t" transform={`translate(${x0 - 34} ${(yTop + yBot) / 2}) rotate(-90)`} textAnchor="middle">2.26 m (7.4 ft) · 2 high</text>
      <Dim x1={x1 + 0.5 * S} y1={yBot} x2={x1 + 0.5 * S} y2={grade} label="0.5 m / 20 in" />
      <ScaleBar x={x0} y={grade + 58} S={S} meters={2} />
      <text className="slabel" x={x0} y={yTop - 46}>ARRAY FACE · 14 PANELS · LANDSCAPE 2-HIGH × 7-WIDE</text>
      <text className="slabel" x={x0} y={yTop - 32}>EACH PANEL 90.6 × 44.6 in (2.3 × 1.13 m) · DRAWN TO SCALE</text>
      <text className="slabel" x={x0} y={yTop - 18}>4×4 FRONT POSTS · LOW EDGE 0.5 m / 20 in OFF GRADE · TILT & TOP → SIDE ELEVATION</text>
    </ZoomPan>
  );
}

/* ============================ FOOTING PLAN =========================== */
function RackPlan() {
  const x0 = 90, cols = 8, cw = 96;
  const yBack = 110, yFront = 240;
  const Sp = cw / 2.3;
  const footR = 0.16 * Sp;
  const p6 = 5.5 * 0.0254 * Sp, p4 = 3.5 * 0.0254 * Sp;
  const beamW = 1.5 * 0.0254 * Sp;
  const pts = [];
  for (let i = 0; i < cols; i++) {
    const px = x0 + i * cw;
    pts.push(<g key={i}>
      <circle className="foot-plan" cx={px} cy={yBack} r={footR} />
      <rect className="post-top" x={px - p6 / 2} y={yBack - p6 / 2} width={p6} height={p6} />
      <circle className="foot-plan" cx={px} cy={yFront} r={footR} />
      <rect className="post-top" x={px - p4 / 2} y={yFront - p4 / 2} width={p4} height={p4} />
      <line className="joist-back" x1={px} y1={yBack} x2={px} y2={yFront} />
    </g>);
  }
  return (
    <ZoomPan contentW={980} contentH={368} height={400} fig="FIG. R3 · FOOTING PLAN (TOP VIEW)">
      <line className="beam-line" x1={x0} y1={yBack} x2={x0 + (cols - 1) * cw} y2={yBack} strokeWidth={beamW} />
      <line className="beam-line" x1={x0} y1={yFront} x2={x0 + (cols - 1) * cw} y2={yFront} strokeWidth={beamW} />
      {pts}
      <Dim x1={x0} y1={290} x2={x0 + (cols - 1) * cw} y2={290} label="16.1 m (52.8 ft) · 7 bays" />
      <Dim x1={x0 + (cols - 1) * cw + 46} y1={yBack} x2={x0 + (cols - 1) * cw + 46} y2={yFront} label="~1.3 m / 4.3 ft run" />
      <ScaleBar x={x0} y={322} S={Sp} meters={4} />
      <text className="slabel" x={x0 - 13} y={yBack - 28}>BACK ROW · 6×6 posts · 8 footings</text>
      <text className="slabel" x={x0 - 13} y={yFront + 40}>FRONT ROW · 4×4 posts · 8 footings</text>
      <text className="slabel" x={x0 - 13} y={yBack - 44}>16 FROST FOOTINGS · ≈320 mm PIERS · 4–5 ft DEEP · 30 MPa</text>
    </ZoomPan>
  );
}

/* ===================== OPTION 2 — SEASONAL PRESET ==================== */
function Opt2Elevation() {
  const S = 80, G = 392, X0 = 250;
  const M = (xm, ym) => ({ x: X0 + xm * S, y: G - ym * S });
  const D = Math.PI / 180, L = 2.26, clr = 0.5, footM = 1.4;
  const P0 = M(0, clr);
  const HE = (deg) => M(L * Math.cos(deg * D), clr + L * Math.sin(deg * D));
  const he = HE(55);
  const dx = he.x - P0.x, dy = he.y - P0.y, len = Math.hypot(dx, dy);
  const nx = dy / len, ny = -dx / len, po = 8;
  const pL = { x: P0.x + nx * po, y: P0.y + ny * po }, pH = { x: he.x + nx * po, y: he.y + ny * po };
  const bx = M(1.35, 0).x;
  const holes = [55, 45, 35, 30].map((a, i) => ({ a, y: M(1.35, 1.18 - i * 0.2).y }));
  const hx = X0 - 0.95 * S;
  return (
    <ZoomPan contentW={760} contentH={560} height={520} fig="FIG. R4 · OPTION 2 — SEASONAL ADJUSTABLE (PRESETS)">
      <line className="grade" x1={120} y1={G} x2={700} y2={G} />
      {[...Array(18)].map((_, i) => <line key={i} className="hatch" x1={130 + i * 32} y1={G} x2={122 + i * 32} y2={G + 12} />)}
      <text className="slabel" x={130} y={G + 28}>GRADE</text>
      <Concrete x={P0.x} w={0.3 * S} yTop={G - 6} yBot={M(0, -footM).y} label="" />
      <Concrete x={bx} w={0.3 * S} yTop={G - 6} yBot={M(1.35, -footM).y} label="30 MPa · 4–5 ft" />
      {[45, 35, 30].map((a) => { const h = HE(a); return <line key={a} className="ghost-panel" x1={P0.x} y1={P0.y} x2={h.x} y2={h.y} />; })}
      {[45, 35, 30].map((a) => { const h = HE(a); return <text key={"t" + a} className="ghost-lbl" x={h.x + 7} y={h.y + 4}>{a}°</text>; })}
      <rect className="post" x={P0.x - 4} y={P0.y} width={8} height={M(0, -footM).y - P0.y - 22} rx="1" />
      <rect className="post" x={bx - 5} y={holes[0].y - 10} width={10} height={M(1.35, -footM).y - holes[0].y - 12} rx="1" />
      {holes.map((h, i) => <g key={i}>
        <circle className={"hole" + (i === 0 ? " on" : "")} cx={bx} cy={h.y} r={3.4} />
        <text className="slabel" x={bx + 10} y={h.y + 3.5}>{h.a}°</text>
      </g>)}
      <line className="lock-strut" x1={he.x - nx * 6} y1={he.y - ny * 6} x2={bx} y2={holes[0].y} />
      <line className="joist2" x1={P0.x} y1={P0.y} x2={he.x} y2={he.y} />
      <line className="panel-line" x1={pL.x} y1={pL.y} x2={pH.x} y2={pH.y} />
      <circle className="pivot-bolt" cx={P0.x} cy={P0.y} r={5} />
      <Call x={P0.x} y={P0.y} tx={P0.x - 0.7 * S} ty={P0.y + 0.5 * S} label="T-hinge pivot (low edge)" anchor="end" />
      <Call x={bx} y={holes[1].y} tx={bx + 0.7 * S} ty={holes[1].y + 0.4 * S} label="drilled rear brace · 4 presets" />
      <Call x={(P0.x + he.x) / 2 + nx * po} y={(P0.y + he.y) / 2 + ny * po} tx={(P0.x + he.x) / 2 - 1.0 * S} ty={(P0.y + he.y) / 2 - 0.6 * S} label="55° (set) · ghosts 45/35/30°" anchor="end" />
      <Human cx={hx} feetY={G} S={S} label="1.8 m" />
      <text className="slabel" x={120} y={44}>BUILT AS 3–4 INDEPENDENTLY-TILTABLE 4-PANEL BAYS (~260 lb EACH)</text>
      <text className="slabel" x={120} y={58}>FLIP ~4×/YR · OPEN-SOURCE (PEARCE, DESIGNS 2022)</text>
    </ZoomPan>
  );
}

/* ===================== OPTION 3 — SINGLE-PIVOT TABLE ================= */
function Opt3Elevation() {
  const S = 80, G = 392, X0 = 360;
  const M = (xm, ym) => ({ x: X0 + xm * S, y: G - ym * S });
  const D = Math.PI / 180, L = 2.26, footM = 1.4;
  const Pc = M(0, 1.45);
  const ang = (deg) => ({ ux: Math.cos(deg * D), uy: Math.sin(deg * D) });
  const half = L / 2;
  const seg = (deg) => { const a = ang(deg); return { lo: { x: Pc.x - a.ux * half * S, y: Pc.y + a.uy * half * S }, hi: { x: Pc.x + a.ux * half * S, y: Pc.y - a.uy * half * S } }; };
  const s55 = seg(55);
  const a55 = ang(55), nx = a55.uy, ny = a55.ux, po = 8;
  const pL = { x: s55.lo.x + nx * po, y: s55.lo.y - ny * po }, pH = { x: s55.hi.x + nx * po, y: s55.hi.y - ny * po };
  const postTop = M(0, 1.5), postBase = M(0, -footM);
  const hx = X0 - 1.45 * S;
  return (
    <ZoomPan contentW={780} contentH={560} height={520} fig="FIG. R5 · OPTION 3 — SINGLE-PIVOT TILTING TABLE">
      <line className="grade" x1={120} y1={G} x2={720} y2={G} />
      {[...Array(19)].map((_, i) => <line key={i} className="hatch" x1={130 + i * 32} y1={G} x2={122 + i * 32} y2={G + 12} />)}
      <text className="slabel" x={130} y={G + 28}>GRADE</text>
      <Concrete x={Pc.x} w={0.34 * S} yTop={G - 6} yBot={postBase.y} label="30 MPa · 4–5 ft" />
      {[30, 75].map((a) => { const s = seg(a); return <line key={a} className="ghost-panel" x1={s.lo.x} y1={s.lo.y} x2={s.hi.x} y2={s.hi.y} />; })}
      <path className="arc" d={`M ${Pc.x + 0.6 * S} ${Pc.y} A ${0.6 * S} ${0.6 * S} 0 0 0 ${Pc.x + 0.6 * S * Math.cos(75 * D)} ${Pc.y - 0.6 * S * Math.sin(75 * D)}`} />
      <rect className="post" x={postTop.x - 6} y={postTop.y} width={12} height={postBase.y - postTop.y - 22} rx="1" />
      <line className="joist2" x1={s55.lo.x} y1={s55.lo.y} x2={s55.hi.x} y2={s55.hi.y} />
      <line className="panel-line" x1={pL.x} y1={pL.y} x2={pH.x} y2={pH.y} />
      <line className="lock-strut" x1={s55.lo.x + (s55.hi.x - s55.lo.x) * 0.28} y1={s55.lo.y + (s55.hi.y - s55.lo.y) * 0.28} x2={postTop.x} y2={M(0, 0.7).y} />
      <circle className="hole on" cx={postTop.x} cy={M(0, 0.7).y} r={3.4} />
      <circle className="bushing-out" cx={Pc.x} cy={Pc.y} r={9} />
      <circle className="pivot-bolt" cx={Pc.x} cy={Pc.y} r={4.5} />
      <path className="bond-jumper" d={`M ${Pc.x - 16} ${Pc.y + 16} q 6 -8 12 0 q 6 8 12 0 q 6 -8 12 0`} />
      <path className="service-loop" d={`M ${Pc.x + 14} ${Pc.y + 6} a 7 7 0 1 1 -0.1 0`} />
      <Call x={Pc.x} y={Pc.y} tx={Pc.x + 0.7 * S} ty={Pc.y - 0.7 * S} label="through-bolt pivot + steel bushing" />
      <Call x={postTop.x} y={M(0, 0.7).y} tx={postTop.x - 0.8 * S} ty={M(0, 0.7).y + 0.3 * S} label="positive wind-lock pin (mandatory)" anchor="end" />
      <Call x={Pc.x - 10} y={Pc.y + 18} tx={Pc.x - 0.9 * S} ty={Pc.y + 0.8 * S} label="bonding jumper + PV service loop" anchor="end" />
      <Call x={pH.x} y={pH.y} tx={pH.x - 0.4 * S} ty={pH.y - 0.5 * S} label="≈390 ft² plane = a sail" anchor="end" />
      <Human cx={hx} feetY={G} S={S} label="1.8 m" />
      <text className="slabel" x={120} y={44}>SUB-SECTION ~910 lb PLANE INTO ~4-PANEL BAYS ON 6–8 POSTS</text>
      <text className="slabel" x={120} y={58}>UP-SIZED FOR 575 W / VT · ENGINEER REVIEW BEFORE BUILD</text>
    </ZoomPan>
  );
}

/* ============================ GALLERY ================================ */
const RACK_VIEWS = [
  { id: "elev", label: "Side elevation", Comp: RackElevation },
  { id: "front", label: "Array face", Comp: RackFront },
  { id: "plan", label: "Footing plan", Comp: RackPlan },
];

function RackingVisual() {
  const [i, setI] = useStateRD(0);
  const Cur = RACK_VIEWS[i].Comp;
  return (
    <div className="sheet pad" style={{ marginBottom: 26 }}>
      <div className="tab-toolbar" style={{ marginBottom: 12 }}>
        <SectionHead n="—" kicker="Visualization" title="What it looks like"
          sub="Engineering views of the Phase-1 ground-mount. Schematic, not to scale — drag to pan, scroll to zoom." />
        <div className="seg">
          {RACK_VIEWS.map((v, idx) => (
            <button key={v.id} className={idx === i ? "on" : ""} onClick={() => setI(idx)}>{v.label}</button>
          ))}
        </div>
      </div>
      <Cur key={RACK_VIEWS[i].id} />
      <div className="sch-notes" style={{ marginTop: 14 }}>
        <div className="stamp warn">Est · not stamped</div>
        <p className="mono">Geometry reflects the locked design — 55° tilt, 6×6 back / 4×4 front posts, 2×10 inside joists, 4–5 ft frost footings, 2-high × 7-wide. Dimensions are nominal; the cut list (exact lengths + 55° miters) follows once Waaree mounting-hole spacing is confirmed (action A2). For a photoreal site render, drop a file into the 3D Flyover tab.</p>
      </div>
    </div>
  );
}

Object.assign(window, { RackingVisual, RackElevation, RackFront, RackPlan, Opt2Elevation, Opt3Elevation });