// INVYONE Stage-2 — SVG Component Templates
// 각 함수는 SVG <g> 프래그먼트 문자열을 반환. 메인 씬 SVG에 innerHTML로 박는다.
// 내부 ID 컨벤션: ${type}-${id}-${role}  (예: tank-raw-liquid, pump-uf-impeller)
// data-comp-id / data-comp-type 으로 클릭 핸들러가 식별

(function (global) {
  const COMP = {};

  // ===== TANK (vertical rectangular liquid tank with side scale ticks + optional nozzles) =====
  COMP.tank = function (p) {
    const { id, x, y, w = 120, h = 200, label = 'TANK', color = '#5af9ff', nozzles = [] } = p;
    const ticks = [0, 25, 50, 75, 100].map(pct => {
      const ty = (h - 3) - (h - 6) * pct / 100;
      const isMajor = pct % 50 === 0;
      return `<line x1="${-(isMajor ? 5 : 3)}" y1="${ty}" x2="0" y2="${ty}" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>` +
             `<text x="-7" y="${ty + 2}" font-size="6" fill="#cfd3d8" text-anchor="end">${pct}</text>`;
    }).join('');
    const minorTicks = [10, 20, 30, 40, 60, 70, 80, 90].map(pct => {
      const ty = (h - 3) - (h - 6) * pct / 100;
      return `<line x1="-2" y1="${ty}" x2="0" y2="${ty}" stroke="#888" stroke-width="0.5" vector-effect="non-scaling-stroke"/>`;
    }).join('');
    let nozzleSVG = '';
    nozzles.forEach(n => {
      const len = n.len || 9;
      const pos = n.pos != null ? n.pos : 0.5;
      if (n.side === 'left') {
        const ny = (h - 6) * pos + 3;
        nozzleSVG += `<rect x="${-len}" y="${ny - 4}" width="${len}" height="8" fill="url(#metal-h)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>`;
        nozzleSVG += `<rect x="${-len - 2}" y="${ny - 6}" width="2" height="12" fill="url(#metal-v)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>`;
      } else if (n.side === 'right') {
        const ny = (h - 6) * pos + 3;
        nozzleSVG += `<rect x="${w}" y="${ny - 4}" width="${len}" height="8" fill="url(#metal-h)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>`;
        nozzleSVG += `<rect x="${w + len}" y="${ny - 6}" width="2" height="12" fill="url(#metal-v)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>`;
      } else if (n.side === 'top') {
        const nx = (w - 6) * pos + 3;
        nozzleSVG += `<rect x="${nx - 4}" y="${-len}" width="8" height="${len}" fill="url(#metal-v)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>`;
        nozzleSVG += `<rect x="${nx - 6}" y="${-len - 2}" width="12" height="2" fill="url(#metal-h)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>`;
      } else if (n.side === 'bot') {
        const nx = (w - 6) * pos + 3;
        nozzleSVG += `<rect x="${nx - 4}" y="${h}" width="8" height="${len}" fill="url(#metal-v)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>`;
        nozzleSVG += `<rect x="${nx - 6}" y="${h + len}" width="12" height="2" fill="url(#metal-h)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>`;
      }
    });
    return `
<g class="comp comp-tank" data-comp-id="${id}" data-comp-type="tank" transform="translate(${x} ${y})">
  <text x="${w / 2}" y="-12" class="comp-label" font-size="15" font-weight="700" text-anchor="middle">${label}</text>
  ${minorTicks}
  ${ticks}
  ${nozzleSVG}
  <rect class="tank-shell" x="0" y="0" width="${w}" height="${h}" fill="url(#metal-tank)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <rect x="3" y="3" width="${w - 6}" height="${h - 6}" fill="#071326" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <rect x="4" y="4" width="${w - 8}" height="${h - 8}" fill="url(#glass-grad)" pointer-events="none"/>
  <clipPath id="tank-${id}-clip"><rect x="3" y="3" width="${w - 6}" height="${h - 6}"/></clipPath>
  <rect id="tank-${id}-liquid" x="3" y="${h - 3}" width="${w - 6}" height="0" fill="${color}" clip-path="url(#tank-${id}-clip)"/>
  <ellipse id="tank-${id}-surface" cx="${w / 2}" cy="${h - 3}" rx="${(w - 6) / 2 - 2}" ry="2.2" fill="${color}" opacity="0" clip-path="url(#tank-${id}-clip)"/>
  <rect id="tank-${id}-level-bg" x="${w / 2 - 26}" y="${h / 2 - 10}" width="52" height="20" rx="3" fill="#000" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <text id="tank-${id}-text" x="${w / 2}" y="${h / 2 + 5}" class="tank-text" font-size="13" text-anchor="middle">0.0%</text>
</g>`;
  };

  // ===== SMALL DOSING TANK =====
  COMP.dosingTank = function (p) {
    const { id, x, y, label = 'CHEM', color = '#7cff3a' } = p;
    const w = 50, h = 70;
    return `
<g class="comp comp-tank comp-tank-small" data-comp-id="${id}" data-comp-type="tank" transform="translate(${x} ${y})">
  <text x="${w / 2}" y="-8" class="comp-label" font-size="12" font-weight="700" text-anchor="middle">${label}</text>
  <rect class="tank-shell" x="0" y="0" width="${w}" height="${h}" fill="url(#metal-tank)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <rect x="2" y="2" width="${w - 4}" height="${h - 4}" fill="#071326" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <clipPath id="tank-${id}-clip"><rect x="2" y="2" width="${w - 4}" height="${h - 4}"/></clipPath>
  <rect id="tank-${id}-liquid" x="2" y="${h - 2}" width="${w - 4}" height="0" fill="${color}" clip-path="url(#tank-${id}-clip)"/>
  <ellipse id="tank-${id}-surface" cx="${w / 2}" cy="${h - 2}" rx="${(w - 4) / 2 - 1}" ry="1.5" fill="${color}" opacity="0" clip-path="url(#tank-${id}-clip)"/>
  <text id="tank-${id}-text" x="${w / 2}" y="${h / 2 + 3}" class="tank-text" font-size="9" text-anchor="middle">0%</text>
</g>`;
  };

  // ===== AC / SAND TANK (conical top + cylindrical body + solid skirt base) =====
  COMP.acTank = function (p) {
    const { id, x, y, w = 96, h = 150, label = 'AC TANK', color = '#5af9ff' } = p;
    const coneH = 22;
    const bodyY = coneH;
    const skirtH = 8;
    const bodyH = h - coneH - skirtH;
    return `
<g class="comp comp-tank comp-tank-ac" data-comp-id="${id}" data-comp-type="tank" transform="translate(${x} ${y})">
  <text x="${w / 2}" y="-12" class="comp-label" font-size="14" font-weight="700" text-anchor="middle">${label}</text>
  <path d="M${w / 2 - 10} 0 L${w / 2 + 10} 0 L${w} ${coneH} L0 ${coneH} Z" fill="url(#metal-tank)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <rect x="2" y="${bodyY}" width="${w - 4}" height="${bodyH}" fill="url(#metal-tank)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <rect x="6" y="${bodyY + 4}" width="${w - 12}" height="${bodyH - 8}" fill="#071326" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <clipPath id="tank-${id}-clip"><rect x="6" y="${bodyY + 4}" width="${w - 12}" height="${bodyH - 8}"/></clipPath>
  <rect id="tank-${id}-liquid" x="6" y="${bodyY + bodyH - 4}" width="${w - 12}" height="0" fill="${color}" clip-path="url(#tank-${id}-clip)"/>
  <ellipse id="tank-${id}-surface" cx="${w / 2}" cy="${bodyY + bodyH - 4}" rx="${(w - 12) / 2 - 1}" ry="1.6" fill="${color}" opacity="0" clip-path="url(#tank-${id}-clip)"/>
  <text id="tank-${id}-text" x="${w / 2}" y="${bodyY + bodyH / 2 + 5}" class="tank-text" font-size="11" text-anchor="middle">0%</text>
  <rect x="${w / 2 - (w / 2 - 6)}" y="${bodyY + bodyH}" width="${w - 12}" height="${skirtH}" fill="url(#metal-tank)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <rect x="${w / 2 - 5}" y="${h}" width="10" height="6" fill="url(#metal-v)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
</g>`;
  };

  // ===== CENTRIFUGAL PUMP (with motor cooling fins + flow direction arrow) =====
  COMP.pump = function (p) {
    const { id, x, y, label = '', accent = '#7cff3a' } = p;
    return `
<g class="comp comp-pump" data-comp-id="${id}" data-comp-type="pump" transform="translate(${x} ${y})">
  ${label ? `<text x="30" y="80" class="comp-label" font-size="13" font-weight="700" text-anchor="middle">${label}</text>` : ''}
  <rect id="pump-${id}-inlet" x="1" y="25" width="16" height="10" fill="url(#metal-h)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <rect id="pump-${id}-outlet" x="43" y="25" width="16" height="10" fill="url(#metal-h)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <rect class="pump-motor" x="36" y="14" width="14" height="14" rx="2" fill="url(#metal-h)" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <line x1="39" y1="16" x2="39" y2="26" stroke="#666" stroke-width="0.4" vector-effect="non-scaling-stroke"/>
  <line x1="42" y1="16" x2="42" y2="26" stroke="#666" stroke-width="0.4" vector-effect="non-scaling-stroke"/>
  <line x1="45" y1="16" x2="45" y2="26" stroke="#666" stroke-width="0.4" vector-effect="non-scaling-stroke"/>
  <line x1="48" y1="16" x2="48" y2="26" stroke="#666" stroke-width="0.4" vector-effect="non-scaling-stroke"/>
  <circle class="pump-housing" cx="30" cy="30" r="17" fill="#071326" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <circle cx="30" cy="30" r="12" fill="none" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <g id="pump-${id}-impeller" class="pump-impeller">
    <path d="M30 30L30 18A12 12 0 0 1 40 24Z" fill="#5af9ff" opacity="0.88"/>
    <path d="M30 30L42 30A12 12 0 0 1 34 41Z" fill="#5af9ff" opacity="0.65"/>
    <path d="M30 30L20 37A12 12 0 0 1 19 25Z" fill="#5af9ff" opacity="0.5"/>
    <circle cx="30" cy="30" r="3" fill="#fff"/>
  </g>
  <path class="pump-arrow" d="M22 49 L34 49 L34 47 L38 50 L34 53 L34 51 L22 51 Z" fill="#666"/>
  <circle id="pump-${id}-led" class="pump-led" cx="47" cy="47" r="4" fill="#2a3f5a" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
</g>`;
  };

  // ===== VERTICAL PUMP (top-inlet, right-outlet — for tank-foot pumps and chem dosing) =====
  COMP.pumpV = function (p) {
    const { id, x, y, label = '' } = p;
    return `
<g class="comp comp-pump comp-pump-v" data-comp-id="${id}" data-comp-type="pump" transform="translate(${x} ${y})">
  ${label ? `<text x="30" y="90" class="comp-label" font-size="13" font-weight="700" text-anchor="middle">${label}</text>` : ''}
  <rect id="pump-${id}-inlet" x="25" y="0" width="10" height="14" fill="url(#metal-v)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <rect class="pump-motor" x="15" y="12" width="30" height="14" rx="2" fill="url(#metal-h)" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <line x1="20" y1="14" x2="20" y2="24" stroke="#666" stroke-width="0.4" vector-effect="non-scaling-stroke"/>
  <line x1="25" y1="14" x2="25" y2="24" stroke="#666" stroke-width="0.4" vector-effect="non-scaling-stroke"/>
  <line x1="30" y1="14" x2="30" y2="24" stroke="#666" stroke-width="0.4" vector-effect="non-scaling-stroke"/>
  <line x1="35" y1="14" x2="35" y2="24" stroke="#666" stroke-width="0.4" vector-effect="non-scaling-stroke"/>
  <line x1="40" y1="14" x2="40" y2="24" stroke="#666" stroke-width="0.4" vector-effect="non-scaling-stroke"/>
  <circle class="pump-housing" cx="30" cy="46" r="18" fill="#071326" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <circle cx="30" cy="46" r="13" fill="none" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <g id="pump-${id}-impeller" class="pump-impeller">
    <path d="M30 46L30 33A13 13 0 0 1 41 40Z" fill="#5af9ff" opacity="0.88"/>
    <path d="M30 46L43 46A13 13 0 0 1 35 58Z" fill="#5af9ff" opacity="0.65"/>
    <path d="M30 46L19 53A13 13 0 0 1 18 41Z" fill="#5af9ff" opacity="0.5"/>
    <circle cx="30" cy="46" r="3" fill="#fff"/>
  </g>
  <rect id="pump-${id}-outlet" x="48" y="42" width="12" height="10" fill="url(#metal-h)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <circle id="pump-${id}-led" class="pump-led" cx="50" cy="62" r="3" fill="#2a3f5a" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
</g>`;
  };

  // ===== AIR BLOWER / COMPRESSOR =====
  // Visual: cylindrical body, fan housing on left with 3 curved blades, motor on right
  COMP.airBlower = function (p) {
    const { id, x, y, label = 'AIR BLOWER' } = p;
    return `
<g class="comp comp-pump comp-blower" data-comp-id="${id}" data-comp-type="pump" transform="translate(${x} ${y})">
  <text x="40" y="-8" class="comp-label" font-size="13" font-weight="700" text-anchor="middle">${label}</text>
  <rect x="0" y="0" width="80" height="50" rx="6" fill="url(#metal-h)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <circle class="pump-housing" cx="22" cy="25" r="15" fill="#071326" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <circle cx="22" cy="25" r="11" fill="none" stroke="#aaa" stroke-width="0.6" vector-effect="non-scaling-stroke"/>
  <g id="pump-${id}-impeller" class="pump-impeller blower-fan">
    <path d="M22 25 Q22 14 31 13 Q26 19 22 25 Z" fill="#5af9ff" opacity="0.88"/>
    <path d="M22 25 Q33 25 34 34 Q28 29 22 25 Z" fill="#5af9ff" opacity="0.65"/>
    <path d="M22 25 Q22 36 13 37 Q18 31 22 25 Z" fill="#5af9ff" opacity="0.5"/>
    <path d="M22 25 Q11 25 10 16 Q16 21 22 25 Z" fill="#5af9ff" opacity="0.75"/>
    <circle cx="22" cy="25" r="2.5" fill="#fff"/>
  </g>
  <rect class="pump-motor" x="44" y="14" width="32" height="22" rx="3" fill="url(#metal-h)" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <line x1="37" y1="25" x2="44" y2="25" stroke="#666" stroke-width="2" vector-effect="non-scaling-stroke"/>
  <rect id="pump-${id}-outlet" x="76" y="20" width="6" height="10" fill="url(#metal-h)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <circle id="pump-${id}-led" class="pump-led" cx="60" cy="25" r="2.5" fill="#2a3f5a" stroke="#fff" stroke-width="0.8" vector-effect="non-scaling-stroke"/>
</g>`;
  };

  // ===== VALVE (gate / ball) — sits on top of the underlying pipe; no own pipe rect =====
  // orientation: 'h' (default, horizontal pipe) or 'v' (rotated 90° for vertical pipe)
  COMP.valve = function (p) {
    const { id, x, y, label = '', orientation = 'h' } = p;
    const rot = orientation === 'v' ? ' transform="rotate(90 25 26)"' : '';
    const labelY = orientation === 'v' ? -16 : -2;
    const labelX = orientation === 'v' ? 38 : 25;
    const labelAnchor = orientation === 'v' ? 'start' : 'middle';
    return `
<g class="comp comp-valve" data-comp-id="${id}" data-comp-type="valve" data-orient="${orientation}" transform="translate(${x} ${y})">
  ${label ? `<text x="${labelX}" y="${labelY}" class="comp-label" font-size="8" text-anchor="${labelAnchor}">${label}</text>` : ''}
  <g class="valve-rot"${rot}>
    <circle id="valve-${id}-body" class="valve-body" cx="25" cy="26" r="11" fill="#071326" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
    <rect x="14" y="23" width="22" height="6" rx="2" fill="#2a3f5a" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
    <line id="valve-${id}-flow-l" class="valve-flow flow" x1="0" y1="26" x2="14" y2="26" stroke="#5af9ff" stroke-width="2" stroke-dasharray="4 4" stroke-linecap="round" vector-effect="non-scaling-stroke" fill="none"/>
    <line id="valve-${id}-flow-r" class="valve-flow flow" x1="36" y1="26" x2="50" y2="26" stroke="#5af9ff" stroke-width="2" stroke-dasharray="4 4" stroke-linecap="round" vector-effect="non-scaling-stroke" fill="none"/>
    <line x1="25" y1="15" x2="25" y2="8" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
    <g id="valve-${id}-handle" class="valve-handle">
      <rect x="14" y="5" width="22" height="6" rx="2" fill="#7cff3a" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
    </g>
  </g>
</g>`;
  };

  // ===== ORTHOGONAL PIPE (rect-based duct segments + flow path overlay) =====
  // Supports `gaps`: array of { x, y } where this pipe should leave a 16px crossover gap
  // (used to render P&ID-style "duck under" at orthogonal crossings)
  COMP.pipe = function (p) {
    if (Array.isArray(p.points) && p.points.length >= 2) {
      const d = p.points.map((pt, i) => `${i === 0 ? 'M' : 'L'}${pt[0]} ${pt[1]}`).join(' ');
      const gaps = p.gaps || [];
      const GH = 9; // half-gap in px
      const segs = [];
      const pushH = (xMin, xMax, y) => {
        const w = xMax - xMin;
        if (w <= 0) return;
        segs.push(`<rect class="pipe-outer" x="${xMin}" y="${y - 6}" width="${w}" height="12" fill="url(#metal-h)" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>`);
        segs.push(`<rect class="pipe-inner" x="${xMin + 2}" y="${y - 3}" width="${Math.max(0, w - 4)}" height="6" fill="#2a3f5a" vector-effect="non-scaling-stroke"/>`);
      };
      const pushV = (yMin, yMax, x) => {
        const h = yMax - yMin;
        if (h <= 0) return;
        segs.push(`<rect class="pipe-outer" x="${x - 6}" y="${yMin}" width="12" height="${h}" fill="url(#metal-v)" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>`);
        segs.push(`<rect class="pipe-inner" x="${x - 3}" y="${yMin + 2}" width="6" height="${Math.max(0, h - 4)}" fill="#2a3f5a" vector-effect="non-scaling-stroke"/>`);
      };
      const jumps = [];
      for (let i = 1; i < p.points.length; i++) {
        const [x1, y1] = p.points[i - 1];
        const [x2, y2] = p.points[i];
        if (y1 === y2) {
          const xMin = Math.min(x1, x2) - 6, xMax = Math.max(x1, x2) + 6;
          const segGaps = gaps.filter(g => g.y === y1 && g.x > Math.min(x1, x2) && g.x < Math.max(x1, x2)).sort((a, b) => a.x - b.x);
          let cur = xMin;
          segGaps.forEach(g => {
            pushH(cur, g.x - GH, y1);
            // a small jumper arc above the gap
            jumps.push(`<path d="M${g.x - GH} ${y1 - 6} Q${g.x} ${y1 - 14} ${g.x + GH} ${y1 - 6} L${g.x + GH} ${y1 + 6} Q${g.x} ${y1 - 2} ${g.x - GH} ${y1 + 6} Z" fill="url(#metal-h)" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>`);
            cur = g.x + GH;
          });
          pushH(cur, xMax, y1);
        } else if (x1 === x2) {
          const yMin = Math.min(y1, y2) - 6, yMax = Math.max(y1, y2) + 6;
          const segGaps = gaps.filter(g => g.x === x1 && g.y > Math.min(y1, y2) && g.y < Math.max(y1, y2)).sort((a, b) => a.y - b.y);
          let cur = yMin;
          segGaps.forEach(g => {
            pushV(cur, g.y - GH, x1);
            // small jumper arc to the right of the gap (vertical-going-over visualization)
            jumps.push(`<path d="M${x1 - 6} ${g.y - GH} Q${x1 + 8} ${g.y} ${x1 - 6} ${g.y + GH} L${x1 + 6} ${g.y + GH} Q${x1 - 6} ${g.y} ${x1 + 6} ${g.y - GH} Z" fill="url(#metal-v)" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>`);
            cur = g.y + GH;
          });
          pushV(cur, yMax, x1);
        }
      }
      // Build flow path with arc detours at gaps so the dash flow visually goes over
      const flowSegs = [];
      flowSegs.push(`M${p.points[0][0]} ${p.points[0][1]}`);
      for (let i = 1; i < p.points.length; i++) {
        const [x1, y1] = p.points[i - 1];
        const [x2, y2] = p.points[i];
        if (y1 === y2) {
          const segGaps = gaps.filter(g => g.y === y1 && g.x > Math.min(x1, x2) && g.x < Math.max(x1, x2)).sort((a, b) => x2 > x1 ? a.x - b.x : b.x - a.x);
          segGaps.forEach(g => {
            const dir = x2 > x1 ? 1 : -1;
            flowSegs.push(`L${g.x - GH * dir} ${y1}`);
            flowSegs.push(`A${GH} ${GH} 0 0 1 ${g.x + GH * dir} ${y1}`);
          });
          flowSegs.push(`L${x2} ${y2}`);
        } else if (x1 === x2) {
          const segGaps = gaps.filter(g => g.x === x1 && g.y > Math.min(y1, y2) && g.y < Math.max(y1, y2)).sort((a, b) => y2 > y1 ? a.y - b.y : b.y - a.y);
          segGaps.forEach(g => {
            const dir = y2 > y1 ? 1 : -1;
            flowSegs.push(`L${x1} ${g.y - GH * dir}`);
            flowSegs.push(`A${GH} ${GH} 0 0 0 ${x1} ${g.y + GH * dir}`);
          });
          flowSegs.push(`L${x2} ${y2}`);
        }
      }
      const flowD = flowSegs.join(' ');
      return `
<g class="comp comp-pipe" data-comp-id="${p.id}" data-comp-type="pipe">
  ${segs.join('\n  ')}
  ${jumps.join('\n  ')}
  <path id="pipe-${p.id}-fluid" class="pipe-fluid" d="${flowD}" fill="none" stroke="#1ab2c9" stroke-width="6" stroke-linejoin="round" stroke-linecap="butt" stroke-dasharray="9999" stroke-dashoffset="9999" vector-effect="non-scaling-stroke"/>
  <path id="pipe-${p.id}-flow" class="pipe-flow flow" d="${flowD}" fill="none" stroke="#5af9ff" stroke-width="2" stroke-dasharray="4 4" stroke-linecap="round" stroke-linejoin="round" vector-effect="non-scaling-stroke"/>
</g>`;
    }
    // fallback for x1/y1/x2/y2 only
    const { id, x1, y1, x2, y2 } = p;
    const horizontal = y1 === y2;
    if (horizontal) {
      const x = Math.min(x1, x2), w = Math.abs(x2 - x1);
      return `
<g class="comp comp-pipe" data-comp-id="${id}" data-comp-type="pipe" transform="translate(${x} ${y1 - 6})">
  <rect class="pipe-outer" x="0" y="0" width="${w}" height="12" fill="url(#metal-h)" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <rect class="pipe-inner" x="2" y="3" width="${w - 4}" height="6" fill="#2a3f5a" vector-effect="non-scaling-stroke"/>
  <line id="pipe-${id}-flow" class="pipe-flow flow" x1="4" y1="6" x2="${w - 4}" y2="6" stroke="#5af9ff" stroke-width="2" stroke-dasharray="4 4" stroke-linecap="round" vector-effect="non-scaling-stroke" fill="none"/>
</g>`;
    } else {
      const y = Math.min(y1, y2), h = Math.abs(y2 - y1);
      return `
<g class="comp comp-pipe" data-comp-id="${id}" data-comp-type="pipe" transform="translate(${x1 - 6} ${y})">
  <rect class="pipe-outer" x="0" y="0" width="12" height="${h}" fill="url(#metal-v)" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <rect class="pipe-inner" x="3" y="2" width="6" height="${h - 4}" fill="#2a3f5a" vector-effect="non-scaling-stroke"/>
  <line id="pipe-${id}-flow" class="pipe-flow flow" x1="6" y1="4" x2="6" y2="${h - 4}" stroke="#5af9ff" stroke-width="2" stroke-dasharray="4 4" stroke-linecap="round" vector-effect="non-scaling-stroke" fill="none"/>
</g>`;
    }
  };

  // ===== PRESSURE GAUGE (analog dial) =====
  COMP.pressureGauge = function (p) {
    const { id, x, y, label = 'P', max = 8, unit = 'bar' } = p;
    return `
<g class="comp comp-gauge" data-comp-id="${id}" data-comp-type="gauge" data-max="${max}" data-unit="${unit}" transform="translate(${x - 20} ${y - 20}) scale(1.5)">
  ${label ? `<text x="40" y="11" class="comp-label" font-size="11" font-weight="700" text-anchor="middle">${label}</text>` : ''}
  <circle cx="40" cy="40" r="25" fill="url(#metal-h)" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <circle cx="40" cy="40" r="22" fill="#0b1528" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <circle id="gauge-${id}-bg" cx="40" cy="40" r="18" fill="#081326" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <path d="M24.4 48 A17 17 0 0 1 30.7 25.8" fill="none" stroke="#7cff3a" stroke-width="2" vector-effect="non-scaling-stroke"/>
  <path d="M30.7 25.8 A17 17 0 0 1 49.3 25.8" fill="none" stroke="#ff8a3a" stroke-width="2" vector-effect="non-scaling-stroke"/>
  <path d="M49.3 25.8 A17 17 0 0 1 55.6 48" fill="none" stroke="#ff4f9a" stroke-width="2" vector-effect="non-scaling-stroke"/>
  <g stroke="#fff" fill="none" vector-effect="non-scaling-stroke">
    <path d="M40 17 L40 21" transform="rotate(-120 40 40)"/>
    <path d="M40 17 L40 21" transform="rotate(-60 40 40)"/>
    <path d="M40 17 L40 21" transform="rotate(0 40 40)"/>
    <path d="M40 17 L40 21" transform="rotate(60 40 40)"/>
    <path d="M40 17 L40 21" transform="rotate(120 40 40)"/>
  </g>
  <text x="40" y="32" class="gauge-title" font-size="6" font-weight="700" text-anchor="middle">PRESS</text>
  <text x="40" y="55" class="gauge-unit" font-size="6" text-anchor="middle">${unit}</text>
  <g id="gauge-${id}-needle" class="gauge-needle">
    <path d="M40 40 L40 24" fill="none" stroke="#ff8a3a" stroke-width="2" stroke-linecap="round" vector-effect="non-scaling-stroke"/>
    <path d="M38.5 41.5 L40 22.5 L41.5 41.5 Z" fill="#ff8a3a" opacity="0.18"/>
  </g>
  <circle cx="40" cy="40" r="4" fill="#111a2b" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <circle cx="40" cy="40" r="2" fill="url(#metal-h)" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <rect x="22" y="59" width="36" height="13" rx="2" fill="#000" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <text id="gauge-${id}-text" x="40" y="68.5" class="gauge-readout" font-size="11" font-weight="700" text-anchor="middle">0.0</text>
</g>`;
  };

  // ===== SENSOR READOUT =====
  COMP.sensor = function (p) {
    const { id, x, y, label = 'SENSOR', value = 0, unit = '' } = p;
    const w = 130, h = 52;
    return `
<g class="comp comp-sensor" data-comp-id="${id}" data-comp-type="sensor" data-unit="${unit}" transform="translate(${x} ${y})">
  <rect x="0" y="0" width="${w}" height="${h}" rx="3" fill="#071326" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <text id="sensor-${id}-label" x="7" y="17" class="sensor-label" font-size="11" font-weight="700">${label}</text>
  <text id="sensor-${id}-value" x="${w - 7}" y="36" class="sensor-value" font-size="20" font-weight="700" text-anchor="end">${value}</text>
  <text id="sensor-${id}-unit" x="${w - 7}" y="47" class="sensor-unit" font-size="10" text-anchor="end">${unit}</text>
</g>`;
  };

  // ===== STATUS LED =====
  COMP.led = function (p) {
    const { id, x, y, label = '' } = p;
    return `
<g class="comp comp-led" data-comp-id="${id}" data-comp-type="led" transform="translate(${x} ${y})">
  ${label ? `<text x="14" y="22" class="comp-label" font-size="7" text-anchor="middle">${label}</text>` : ''}
  <circle id="led-${id}-status" class="led-status" cx="7" cy="7" r="5" fill="#2a3f5a" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
</g>`;
  };

  // ===== INLINE CARTRIDGE FILTER (housing + visible cartridge element + active fluid flow) =====
  COMP.filter = function (p) {
    const { id, x, y, label = 'FILTER' } = p;
    const w = 32, h = 82;
    let mesh = '';
    const elTop = 14, elBot = h - 14, elL = w / 2 - 6, elR = w / 2 + 6;
    const rows = 9;
    for (let i = 0; i < rows; i++) {
      const ly = elTop + (elBot - elTop) * i / rows;
      mesh += `<path d="M${elL} ${ly} L${w / 2} ${ly + 3} L${elR} ${ly}" stroke="#aaa" stroke-width="0.5" fill="none" opacity="0.55" vector-effect="non-scaling-stroke"/>`;
    }
    const midY = h / 2;
    return `
<g class="comp comp-filter" data-comp-id="${id}" data-comp-type="filter" transform="translate(${x} ${y})">
  ${label ? `<text x="${w / 2}" y="-10" class="comp-label" font-size="12" font-weight="700" text-anchor="middle">${label}</text>` : ''}
  <!-- top end cap with bolt ring -->
  <ellipse cx="${w / 2}" cy="4" rx="${w / 2 - 1}" ry="4" fill="url(#metal-h)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <line x1="${w / 2 - 8}" y1="3" x2="${w / 2 - 8}" y2="6" stroke="#666" stroke-width="0.5" vector-effect="non-scaling-stroke"/>
  <line x1="${w / 2 + 8}" y1="3" x2="${w / 2 + 8}" y2="6" stroke="#666" stroke-width="0.5" vector-effect="non-scaling-stroke"/>
  <!-- transparent housing shell -->
  <rect x="1" y="4" width="${w - 2}" height="${h - 8}" fill="url(#metal-tank)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke" opacity="0.55"/>
  <rect class="filter-housing-bg" x="3" y="6" width="${w - 6}" height="${h - 12}" fill="#0b1528" stroke="#aaa" stroke-width="0.5" vector-effect="non-scaling-stroke"/>
  <!-- housing fluid fill (수조 안에 액체가 차오르는 시각) -->
  <rect class="filter-housing-fluid" x="3" y="6" width="${w - 6}" height="${h - 12}" fill="#5af9ff" opacity="0" vector-effect="non-scaling-stroke"/>
  <!-- inner cartridge element with pleated media -->
  <rect class="filter-element" x="${elL}" y="${elTop}" width="${elR - elL}" height="${elBot - elTop}" rx="2" fill="#1a2a45" stroke="#aaa" stroke-width="0.6" vector-effect="non-scaling-stroke"/>
  <rect class="filter-fluid" x="${elL + 0.5}" y="${elTop + 0.5}" width="${elR - elL - 1}" height="${elBot - elTop - 1}" rx="1.5" fill="#5af9ff" opacity="0"/>
  ${mesh}
  <!-- core perforation -->
  <line class="filter-core-flow" x1="${w / 2}" y1="${elTop + 1}" x2="${w / 2}" y2="${elBot - 1}" stroke="#5af9ff" stroke-width="0.7" stroke-dasharray="2 2" opacity="0.5" vector-effect="non-scaling-stroke"/>
  <!-- support rods (housing structure) -->
  <line x1="${w / 2 - 4}" y1="14" x2="${w / 2 - 4}" y2="${h - 14}" stroke="#aaa" stroke-width="0.5" opacity="0.4" vector-effect="non-scaling-stroke"/>
  <line x1="${w / 2 + 4}" y1="14" x2="${w / 2 + 4}" y2="${h - 14}" stroke="#aaa" stroke-width="0.5" opacity="0.4" vector-effect="non-scaling-stroke"/>
  <!-- internal flow dashes (좌측 housing → 카트리지 → 우측 housing) -->
  <line class="filter-internal-flow flow" x1="3"     y1="${midY}" x2="${elL}" y2="${midY}" stroke="#5af9ff" stroke-width="1.6" stroke-dasharray="3 3" stroke-linecap="round" vector-effect="non-scaling-stroke" fill="none"/>
  <line class="filter-internal-flow flow" x1="${elL}" y1="${midY}" x2="${elR}" y2="${midY}" stroke="#5af9ff" stroke-width="1.6" stroke-dasharray="3 3" stroke-linecap="round" vector-effect="non-scaling-stroke" fill="none"/>
  <line class="filter-internal-flow flow" x1="${elR}" y1="${midY}" x2="${w - 3}" y2="${midY}" stroke="#5af9ff" stroke-width="1.6" stroke-dasharray="3 3" stroke-linecap="round" vector-effect="non-scaling-stroke" fill="none"/>
  <!-- inlet/outlet flow arrows (외부, housing 양 끝) -->
  <path class="filter-arrow filter-arrow-in"  d="M-3 ${midY - 4} L4 ${midY} L-3 ${midY + 4} Z" fill="#5af9ff" opacity="0.5"/>
  <path class="filter-arrow filter-arrow-out" d="M${w - 4} ${midY - 4} L${w + 3} ${midY} L${w - 4} ${midY + 4} Z" fill="#5af9ff" opacity="0.5"/>
  <!-- bottom end cap with bolt ring -->
  <ellipse cx="${w / 2}" cy="${h - 4}" rx="${w / 2 - 1}" ry="4" fill="url(#metal-h)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <line x1="${w / 2 - 8}" y1="${h - 6}" x2="${w / 2 - 8}" y2="${h - 3}" stroke="#666" stroke-width="0.5" vector-effect="non-scaling-stroke"/>
  <line x1="${w / 2 + 8}" y1="${h - 6}" x2="${w / 2 + 8}" y2="${h - 3}" stroke="#666" stroke-width="0.5" vector-effect="non-scaling-stroke"/>
  <!-- drain valve indicator -->
  <rect x="${w / 2 - 1.5}" y="${h - 1}" width="3" height="3" fill="url(#metal-v)" stroke="#aaa" stroke-width="0.4" vector-effect="non-scaling-stroke"/>
</g>`;
  };

  // ===== MEMBRANE BANK (UF=vertical bottles / RO=horizontal cylinders) =====
  COMP.membraneBank = function (p) {
    const { id, x, y, count = 5, label = 'MEMBRANE', accent = '#5af9ff', orientation = 'horizontal' } = p;
    if (orientation === 'vertical') {
      // UF style: vertical cylinders + cylindrical manifold headers + per-cell inlet valves + MODE readout
      const cellW = 22, cellH = 130, gap = 10;
      const w = count * cellW + (count - 1) * gap + 24;
      const h = cellH + 26;
      const headerH = 8;
      const topManifoldY = 6;
      const botManifoldY = h - 14;
      let cells = '';
      let internalFlows = '';
      let inletValves = '';
      let outletValves = '';
      let cellLabels = '';
      let permeateMarks = '';
      for (let i = 0; i < count; i++) {
        const cx = 12 + i * (cellW + gap);
        const fcx = cx + cellW / 2;
        cells += `
    <g id="cell-${id}-${i + 1}" class="membrane-cell" transform="translate(${cx} 12)">
      <rect x="0" y="0" width="${cellW}" height="${cellH}" rx="${cellW / 2}" fill="url(#metal-v)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
      <rect x="3" y="${cellW / 2}" width="${cellW - 6}" height="${cellH - cellW}" rx="${(cellW - 6) / 2}" fill="${accent}" opacity="0.30" class="cell-fluid"/>
      <line class="cell-fiber" x1="${cellW / 2 - 5}" y1="14" x2="${cellW / 2 - 5}" y2="${cellH - 14}" stroke="#fff" stroke-width="0.5" opacity="0.35" vector-effect="non-scaling-stroke"/>
      <line class="cell-fiber" x1="${cellW / 2 - 2}" y1="14" x2="${cellW / 2 - 2}" y2="${cellH - 14}" stroke="#fff" stroke-width="0.4" opacity="0.30" vector-effect="non-scaling-stroke"/>
      <line class="cell-fiber" x1="${cellW / 2 + 2}" y1="14" x2="${cellW / 2 + 2}" y2="${cellH - 14}" stroke="#fff" stroke-width="0.4" opacity="0.30" vector-effect="non-scaling-stroke"/>
      <line class="cell-fiber" x1="${cellW / 2 + 5}" y1="14" x2="${cellW / 2 + 5}" y2="${cellH - 14}" stroke="#fff" stroke-width="0.5" opacity="0.35" vector-effect="non-scaling-stroke"/>
      <rect x="2" y="3" width="${cellW - 4}" height="6" rx="1.5" fill="#ff4f9a" opacity="0.85"/>
      <rect x="2" y="${cellH - 9}" width="${cellW - 4}" height="6" rx="1.5" fill="#ff4f9a" opacity="0.85"/>
    </g>`;
        internalFlows += `
    <line class="membrane-flow flow" x1="${fcx}" y1="14" x2="${fcx}" y2="${cellH + 10}" stroke="${accent}" stroke-width="1.5" stroke-dasharray="3 3" stroke-linecap="round" vector-effect="non-scaling-stroke" fill="none"/>`;
        // 매니폴드 위에 inlet valve indicator (작은 ball valve symbol)
        inletValves += `
    <g class="cell-inlet-valve" transform="translate(${fcx} ${topManifoldY - 1})">
      <line x1="0" y1="0" x2="0" y2="3" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
      <circle cx="0" cy="-1.5" r="2" fill="#0b1528" stroke="#aaa" stroke-width="0.6" vector-effect="non-scaling-stroke"/>
      <line x1="-1.8" y1="-1.5" x2="1.8" y2="-1.5" stroke="#7cff3a" stroke-width="1" vector-effect="non-scaling-stroke"/>
    </g>`;
        // 하단 매니폴드 outlet (permeate collection) 위에 작은 stub
        outletValves += `
    <g class="cell-outlet-stub" transform="translate(${fcx} ${botManifoldY + headerH + 1})">
      <line x1="0" y1="0" x2="0" y2="3" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
      <circle cx="0" cy="3.5" r="1.4" fill="${accent}" stroke="#aaa" stroke-width="0.4" opacity="0.7"/>
    </g>`;
        // 모듈 ID (M1~M5) 매니폴드 위쪽 살짝
        cellLabels += `
    <text x="${fcx}" y="${topManifoldY - 6}" font-size="4.5" fill="#8590a3" text-anchor="middle">M${i + 1}</text>`;
        if (i === count - 1) {
          permeateMarks += `<line x1="${cx + cellW + 2}" y1="${cellH / 2 + 12}" x2="${w - 4}" y2="${cellH / 2 + 12}" stroke="${accent}" stroke-width="1" stroke-dasharray="2 2" opacity="0.6" vector-effect="non-scaling-stroke"/>`;
          permeateMarks += `<text x="${w - 2}" y="${cellH / 2 + 9}" font-size="6" fill="${accent}" text-anchor="end" opacity="0.85">PERM</text>`;
        }
      }
      // Manifold cylindrical pipe (caps on both ends + bolt rings)
      const manifoldPipe = (my) => `
    <g class="manifold-pipe">
      <rect class="manifold-header" x="2" y="${my}" width="${w - 4}" height="${headerH}" fill="url(#metal-h)" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
      <ellipse cx="2" cy="${my + headerH / 2}" rx="3.5" ry="${headerH / 2 + 0.5}" fill="url(#metal-v)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
      <ellipse cx="${w - 2}" cy="${my + headerH / 2}" rx="3.5" ry="${headerH / 2 + 0.5}" fill="url(#metal-v)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
      <line x1="6"      y1="${my + 1}" x2="6"      y2="${my + headerH - 1}" stroke="#666" stroke-width="0.4" vector-effect="non-scaling-stroke"/>
      <line x1="${w-6}" y1="${my + 1}" x2="${w-6}" y2="${my + headerH - 1}" stroke="#666" stroke-width="0.4" vector-effect="non-scaling-stroke"/>
    </g>`;
      return `
<g class="comp comp-module comp-module-vertical" data-comp-id="${id}" data-comp-type="module" transform="translate(${x} ${y})">
  <text x="${w / 2}" y="-18" class="comp-label" font-size="15" text-anchor="middle" font-weight="700">${label}</text>
  <g class="mode-readout" transform="translate(0 -4)">
    <text id="module-${id}-mode" x="0" y="-1" font-size="6" fill="#7cff3a" font-family="Consolas, monospace">▶ STANDBY</text>
  </g>
  <g class="tmp-readout" transform="translate(${w} -4)">
    <text id="module-${id}-tmp" x="0" y="-1" font-size="6" fill="${accent}" text-anchor="end" font-family="Consolas, monospace">TMP 0.0 bar</text>
  </g>
  <rect x="0" y="0" width="${w}" height="${h}" rx="3" fill="none" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
  ${cellLabels}
  ${manifoldPipe(topManifoldY)}
  ${manifoldPipe(botManifoldY)}
  ${inletValves}
  ${cells}
  ${outletValves}
  ${internalFlows}
  ${permeateMarks}
  <line id="module-${id}-flow-in"  class="module-flow flow" x1="2" y1="${topManifoldY + headerH / 2}" x2="${w - 2}" y2="${topManifoldY + headerH / 2}" stroke="${accent}" stroke-width="2" stroke-dasharray="4 4" stroke-linecap="round" vector-effect="non-scaling-stroke" fill="none"/>
  <line id="module-${id}-flow-out" class="module-flow flow" x1="2" y1="${botManifoldY + headerH / 2}" x2="${w - 2}" y2="${botManifoldY + headerH / 2}" stroke="${accent}" stroke-width="2" stroke-dasharray="4 4" stroke-linecap="round" vector-effect="non-scaling-stroke" fill="none"/>
</g>`;
    }
    // RO style: horizontal cylinders stacked vertically
    const cellW = 180, cellH = 16, gap = 8;
    const w = cellW + 28;
    const h = count * cellH + (count - 1) * gap + 24;
    let cells = '';
    for (let i = 0; i < count; i++) {
      const cy = 12 + i * (cellH + gap);
      cells += `
    <g id="cell-${id}-${i + 1}" class="membrane-cell" transform="translate(14 ${cy})">
      <rect x="0" y="0" width="${cellW}" height="${cellH}" rx="${cellH / 2}" fill="url(#metal-h)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
      <rect x="12" y="3" width="${cellW - 24}" height="${cellH - 6}" rx="${(cellH - 6) / 2}" fill="${accent}" opacity="0.25" class="cell-fluid"/>
      <rect x="3" y="2" width="5" height="${cellH - 4}" rx="1" fill="#ff4f9a" opacity="0.85"/>
      <rect x="${cellW - 8}" y="2" width="5" height="${cellH - 4}" rx="1" fill="#ff4f9a" opacity="0.85"/>
    </g>`;
    }
    return `
<g class="comp comp-module" data-comp-id="${id}" data-comp-type="module" transform="translate(${x} ${y})">
  <text x="${w / 2}" y="-8" class="comp-label" font-size="14" font-weight="700" text-anchor="middle">${label}</text>
  <rect x="0" y="0" width="${w}" height="${h}" rx="3" fill="none" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
  ${cells}
  <line id="module-${id}-flow-in" class="module-flow flow" x1="0" y1="${h / 2}" x2="10" y2="${h / 2}" stroke="${accent}" stroke-width="2" stroke-dasharray="4 4" stroke-linecap="round" vector-effect="non-scaling-stroke" fill="none"/>
  <line id="module-${id}-flow-out" class="module-flow flow" x1="${w - 10}" y1="${h / 2}" x2="${w}" y2="${h / 2}" stroke="${accent}" stroke-width="2" stroke-dasharray="4 4" stroke-linecap="round" vector-effect="non-scaling-stroke" fill="none"/>
</g>`;
  };

  // ===== CARBON CARTRIDGE BANK (vertical, MBR style) =====
  COMP.carbonBank = function (p) {
    const { id, x, y, count = 5, label = 'CARBON BANK' } = p;
    const cellW = 14, gap = 5, cellH = 64;
    const w = count * cellW + (count - 1) * gap + 16;
    const h = cellH + 32;
    let cells = '';
    for (let i = 0; i < count; i++) {
      const cx = 8 + i * (cellW + gap);
      cells += `
    <g id="cartridge-${id}-${i + 1}" class="cartridge" transform="translate(${cx} 24)">
      <rect class="cartridge-shell" x="0" y="0" width="${cellW}" height="${cellH}" rx="6" fill="url(#metal-v)" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
      <rect class="cartridge-fluid" x="3" y="8" width="${cellW - 6}" height="${cellH - 16}" rx="3" fill="#5af9ff" opacity="0.25"/>
      <rect x="1" y="2" width="${cellW - 2}" height="4" rx="1" fill="#11203a" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
      <rect x="1" y="${cellH - 6}" width="${cellW - 2}" height="4" rx="1" fill="#11203a" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
    </g>`;
    }
    return `
<g class="comp comp-module comp-carbon" data-comp-id="${id}" data-comp-type="module" transform="translate(${x} ${y})">
  <text x="${w / 2}" y="-7" class="comp-label" font-size="13" font-weight="700" text-anchor="middle">${label}</text>
  ${cells}
  <line id="module-${id}-flow-in" class="module-flow flow" x1="2" y1="16" x2="${w - 2}" y2="16" stroke="#5af9ff" stroke-width="2" stroke-dasharray="4 4" stroke-linecap="round" vector-effect="non-scaling-stroke" fill="none"/>
</g>`;
  };

  // ===== STATUS TILE (KPI mini panel) =====
  COMP.statusTile = function (p) {
    const { id, x, y, label = 'TILE', value = '--', unit = '', w = 150, h = 64 } = p;
    return `
<g class="comp comp-tile" data-comp-id="${id}" data-comp-type="tile" transform="translate(${x} ${y})">
  <rect x="0" y="0" width="${w}" height="${h}" rx="3" fill="#071326" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <text x="8" y="18" class="tile-label" font-size="12" font-weight="700">${label}</text>
  <text id="tile-${id}-value" x="${w - 8}" y="44" class="tile-value" font-size="28" font-weight="700" text-anchor="end">${value}</text>
  <text x="${w - 8}" y="56" class="tile-unit" font-size="12" text-anchor="end">${unit}</text>
</g>`;
  };

  // ===== PANEL FRAME (외곽 그룹 박스) =====
  COMP.panel = function (p) {
    const { id, x, y, w, h, label = '' } = p;
    return `
<g class="comp comp-panel" data-comp-id="${id}" data-comp-type="panel" transform="translate(${x} ${y})">
  <rect x="0" y="0" width="${w}" height="${h}" rx="3" fill="none" stroke="#ff8a3a" stroke-width="1" stroke-dasharray="4 2" vector-effect="non-scaling-stroke"/>
  ${label ? `<rect x="6" y="-8" width="${label.length * 6 + 12}" height="14" rx="2" fill="#050a18" stroke="#ff8a3a" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <text x="12" y="2" class="panel-label" font-size="9">${label}</text>` : ''}
</g>`;
  };

  global.INVYONE_COMP = COMP;
})(window);
