feat: SCADA 데모 가독성 개선 + 컴포넌트 좌표 조정 도구 추가
- 압력 게이지 1.5x 확대, PRESS/판독값/라벨 폰트 강조 - 모든 설비(탱크/펌프/시스템/MBR/필터/카본) 라벨 폰트 +2~4 단계 키움 - 센서/타일 박스 및 폰트 확대, 위치 간격 보정 - 위쪽 처리 라인 30px 위로 + TSS-RAW 자기 자리 유지하도록 dy 보정 - AIR BLOWER 를 CIP 패널 내려간 만큼 같이 내림 - 게이지/밸브/모듈/탱크 및 파이프 라우팅 미세 조정 (드래그로 잡은 좌표 일괄 반영) - dev-drag.js: Shift+D 임시 드래그 모드. 컴포넌트/파이프 via/절대 끝점 핸들로 좌표 인스펙션 후 토폴로지 재빌드, 변경 이력 누적 패널 + 전체 복사
This commit is contained in:
@@ -30,7 +30,7 @@
|
||||
<main class="main">
|
||||
|
||||
<div class="scene-wrap">
|
||||
<svg id="scene" viewBox="0 0 2000 880" preserveAspectRatio="xMidYMid meet">
|
||||
<svg id="scene" viewBox="0 0 2000 960" preserveAspectRatio="xMidYMid meet">
|
||||
<defs>
|
||||
<linearGradient id="metal-h" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="0" y2="20">
|
||||
<stop offset="0" stop-color="#d9d9d9"/>
|
||||
@@ -380,6 +380,7 @@
|
||||
<script src="js/ui.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
<script src="js/voice.js"></script>
|
||||
<script src="js/dev-drag.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
});
|
||||
return `
|
||||
<g class="comp comp-tank" data-comp-id="${id}" data-comp-type="tank" transform="translate(${x} ${y})">
|
||||
<text x="${w / 2}" y="-8" class="comp-label" font-size="11" text-anchor="middle">${label}</text>
|
||||
<text x="${w / 2}" y="-12" class="comp-label" font-size="15" font-weight="700" text-anchor="middle">${label}</text>
|
||||
${minorTicks}
|
||||
${ticks}
|
||||
${nozzleSVG}
|
||||
@@ -64,7 +64,7 @@
|
||||
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="-4" class="comp-label" font-size="8" text-anchor="middle">${label}</text>
|
||||
<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>
|
||||
@@ -83,7 +83,7 @@
|
||||
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="-8" class="comp-label" font-size="10" text-anchor="middle">${label}</text>
|
||||
<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"/>
|
||||
@@ -101,7 +101,7 @@
|
||||
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="76" class="comp-label" font-size="9" text-anchor="middle">${label}</text>` : ''}
|
||||
${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"/>
|
||||
@@ -127,7 +127,7 @@
|
||||
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="86" class="comp-label" font-size="9" text-anchor="middle">${label}</text>` : ''}
|
||||
${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"/>
|
||||
@@ -154,7 +154,7 @@
|
||||
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="-4" class="comp-label" font-size="9" text-anchor="middle">${label}</text>
|
||||
<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"/>
|
||||
@@ -304,8 +304,8 @@
|
||||
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} ${y})">
|
||||
${label ? `<text x="40" y="-3" class="comp-label" font-size="8" text-anchor="middle">${label}</text>` : ''}
|
||||
<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"/>
|
||||
@@ -319,29 +319,29 @@
|
||||
<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="33" class="gauge-title" font-size="5" text-anchor="middle">PRESS</text>
|
||||
<text x="40" y="55" class="gauge-unit" font-size="5" text-anchor="middle">${unit}</text>
|
||||
<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="25" y="59" width="30" height="10" rx="2" fill="#000" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
|
||||
<text id="gauge-${id}-text" x="40" y="66.5" class="gauge-readout" font-size="8" text-anchor="middle">0.0</text>
|
||||
<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 = 110, h = 36;
|
||||
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="6" y="13" class="sensor-label" font-size="8">${label}</text>
|
||||
<text id="sensor-${id}-value" x="${w - 6}" y="26" class="sensor-value" font-size="14" text-anchor="end">${value}</text>
|
||||
<text id="sensor-${id}-unit" x="${w - 6}" y="33" class="sensor-unit" font-size="7" text-anchor="end">${unit}</text>
|
||||
<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>`;
|
||||
};
|
||||
|
||||
@@ -369,7 +369,7 @@
|
||||
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="-6" class="comp-label" font-size="8" text-anchor="middle">${label}</text>` : ''}
|
||||
${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"/>
|
||||
@@ -469,7 +469,7 @@
|
||||
</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="-14" class="comp-label" font-size="11" text-anchor="middle" font-weight="600">${label}</text>
|
||||
<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>
|
||||
@@ -506,7 +506,7 @@
|
||||
}
|
||||
return `
|
||||
<g class="comp comp-module" data-comp-id="${id}" data-comp-type="module" transform="translate(${x} ${y})">
|
||||
<text x="${w / 2}" y="-4" class="comp-label" font-size="10" text-anchor="middle">${label}</text>
|
||||
<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"/>
|
||||
@@ -533,7 +533,7 @@
|
||||
}
|
||||
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="-3" class="comp-label" font-size="9" text-anchor="middle">${label}</text>
|
||||
<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>`;
|
||||
@@ -541,13 +541,13 @@
|
||||
|
||||
// ===== STATUS TILE (KPI mini panel) =====
|
||||
COMP.statusTile = function (p) {
|
||||
const { id, x, y, label = 'TILE', value = '--', unit = '', w = 130, h = 50 } = 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="6" y="13" class="tile-label" font-size="8">${label}</text>
|
||||
<text id="tile-${id}-value" x="${w - 6}" y="32" class="tile-value" font-size="18" text-anchor="end">${value}</text>
|
||||
<text x="${w - 6}" y="44" class="tile-unit" font-size="8" text-anchor="end">${unit}</text>
|
||||
<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>`;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
// SCADA 데모 — 컴포넌트 드래그 좌표 인스펙터 (개발용)
|
||||
// 사용법: Shift+D 로 드래그 모드 토글. 컴포넌트 잡고 옮기면 새 좌표가 화면 + 콘솔 + 클립보드.
|
||||
// 좌표 자동 적용은 안 함 — 사용자가 보고 코드(topology.js)에 반영.
|
||||
|
||||
(function () {
|
||||
let DRAG_MODE = false;
|
||||
let dragging = null;
|
||||
let svg = null;
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
// e.code 는 키보드 레이아웃/한영 입력기와 무관하게 동작
|
||||
if (e.shiftKey && e.code === 'KeyD') {
|
||||
e.preventDefault();
|
||||
DRAG_MODE = !DRAG_MODE;
|
||||
updateUI();
|
||||
console.log('[DRAG] mode:', DRAG_MODE ? 'ON' : 'OFF');
|
||||
}
|
||||
if (e.code === 'Escape' && dragging) {
|
||||
dragging.el.setAttribute('transform', dragging.origTransform);
|
||||
dragging.el.style.opacity = '';
|
||||
dragging = null;
|
||||
}
|
||||
}, true);
|
||||
|
||||
function updateUI() {
|
||||
let ind = document.getElementById('drag-mode-indicator');
|
||||
if (DRAG_MODE) {
|
||||
if (!ind) {
|
||||
ind = document.createElement('div');
|
||||
ind.id = 'drag-mode-indicator';
|
||||
ind.style.cssText = `position:fixed;top:10px;left:50%;transform:translateX(-50%);
|
||||
background:#d63031;color:#fff;padding:6px 16px;border-radius:4px;
|
||||
font:700 13px/1.2 system-ui;z-index:9999;box-shadow:0 2px 8px rgba(0,0,0,0.4);`;
|
||||
ind.textContent = 'DRAG MODE — 컴포넌트 잡고 옮기기 (Shift+D 끄기 / Esc 취소)';
|
||||
document.body.appendChild(ind);
|
||||
}
|
||||
svg.style.cursor = 'grab';
|
||||
drawPipeHandles();
|
||||
} else {
|
||||
if (ind) ind.remove();
|
||||
svg.style.cursor = '';
|
||||
removePipeHandles();
|
||||
}
|
||||
}
|
||||
|
||||
// 파이프 핸들 (via 점 + 절대 to 끝점)
|
||||
function drawPipeHandles() {
|
||||
if (!window.INVYONE_TOPO) return;
|
||||
removePipeHandles();
|
||||
const sceneContent = document.getElementById('scene-content');
|
||||
if (!sceneContent) return;
|
||||
const NS = 'http://www.w3.org/2000/svg';
|
||||
const layer = document.createElementNS(NS, 'g');
|
||||
layer.setAttribute('id', 'pipe-handles-layer');
|
||||
|
||||
let ports;
|
||||
try { ports = window.INVYONE_TOPO.computePorts(); } catch { ports = {}; }
|
||||
|
||||
(window.INVYONE_TOPO.PIPES || []).forEach(pipe => {
|
||||
// 1) 절대 to 끝점
|
||||
if (pipe.to && typeof pipe.to === 'object' && typeof pipe.to.x === 'number' && typeof pipe.to.y === 'number') {
|
||||
layer.appendChild(makeHandle(pipe.to.x, pipe.to.y, '#ff8a3a', { pipeId: pipe.id, kind: 'to-abs' }));
|
||||
}
|
||||
// 2) via 점들 — via 가 있으면 path 의 corner 가 via 좌표에 정확히 떨어짐
|
||||
if (Array.isArray(pipe.via)) {
|
||||
pipe.via.forEach((v, i) => {
|
||||
// via.x 만 있으면 핸들 위치는 (v.x, 시작점 y 또는 다음 corner y) 추정
|
||||
// via.y 만 있으면 핸들 위치는 (시작점 x, v.y) 추정
|
||||
const fromXY = portXY(pipe.from, ports);
|
||||
const toXY = (typeof pipe.to === 'string') ? portXY(pipe.to, ports) :
|
||||
(pipe.to && typeof pipe.to.x === 'number') ? { x: pipe.to.x, y: pipe.to.y } : null;
|
||||
let hx, hy;
|
||||
if (typeof v.x === 'number' && typeof v.y === 'number') {
|
||||
hx = v.x; hy = v.y;
|
||||
} else if (typeof v.x === 'number') {
|
||||
hx = v.x;
|
||||
hy = fromXY ? fromXY.y : (toXY ? toXY.y : 0);
|
||||
} else if (typeof v.y === 'number') {
|
||||
hy = v.y;
|
||||
hx = fromXY ? fromXY.x : (toXY ? toXY.x : 0);
|
||||
}
|
||||
if (hx == null || hy == null) return;
|
||||
layer.appendChild(makeHandle(hx, hy, '#5af9ff', { pipeId: pipe.id, kind: 'via', viaIdx: i }));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
sceneContent.appendChild(layer);
|
||||
}
|
||||
|
||||
function makeHandle(x, y, color, data) {
|
||||
const NS = 'http://www.w3.org/2000/svg';
|
||||
const c = document.createElementNS(NS, 'circle');
|
||||
c.setAttribute('cx', x);
|
||||
c.setAttribute('cy', y);
|
||||
c.setAttribute('r', 7);
|
||||
c.setAttribute('fill', color);
|
||||
c.setAttribute('stroke', '#fff');
|
||||
c.setAttribute('stroke-width', 2);
|
||||
c.setAttribute('class', 'pipe-handle');
|
||||
c.style.cursor = 'grab';
|
||||
c.style.opacity = '0.85';
|
||||
Object.entries(data).forEach(([k, v]) => c.dataset[k] = v);
|
||||
return c;
|
||||
}
|
||||
|
||||
function removePipeHandles() {
|
||||
const old = document.getElementById('pipe-handles-layer');
|
||||
if (old) old.remove();
|
||||
}
|
||||
|
||||
function portXY(ref, ports) {
|
||||
if (!ref || typeof ref !== 'string') return null;
|
||||
const [compId, portName] = ref.split('.');
|
||||
return ports[compId] && ports[compId][portName] ? ports[compId][portName] : null;
|
||||
}
|
||||
|
||||
function toSvg(clientX, clientY) {
|
||||
const pt = svg.createSVGPoint();
|
||||
pt.x = clientX;
|
||||
pt.y = clientY;
|
||||
return pt.matrixTransform(svg.getScreenCTM().inverse());
|
||||
}
|
||||
|
||||
function parseTranslate(transform) {
|
||||
const m = (transform || '').match(/translate\(([^)]+)\)/);
|
||||
if (!m) return { x: 0, y: 0 };
|
||||
const parts = m[1].split(/[ ,]+/).map(Number);
|
||||
return { x: parts[0] || 0, y: parts[1] || 0 };
|
||||
}
|
||||
|
||||
function onMouseDown(e) {
|
||||
if (!DRAG_MODE) return;
|
||||
if (!svg.contains(e.target)) return;
|
||||
|
||||
// 1) 파이프 핸들 우선 처리
|
||||
if (e.target.classList && e.target.classList.contains('pipe-handle')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const sp = toSvg(e.clientX, e.clientY);
|
||||
dragging = {
|
||||
isHandle: true,
|
||||
el: e.target,
|
||||
pipeId: e.target.dataset.pipeId,
|
||||
kind: e.target.dataset.kind,
|
||||
viaIdx: e.target.dataset.viaIdx ? parseInt(e.target.dataset.viaIdx, 10) : null,
|
||||
startMouseX: sp.x,
|
||||
startMouseY: sp.y,
|
||||
origCx: parseFloat(e.target.getAttribute('cx')),
|
||||
origCy: parseFloat(e.target.getAttribute('cy')),
|
||||
};
|
||||
e.target.style.cursor = 'grabbing';
|
||||
return;
|
||||
}
|
||||
|
||||
const compEl = e.target.closest('.comp');
|
||||
if (!compEl) {
|
||||
console.log('[DRAG] mousedown target:', e.target.tagName, e.target.getAttribute && e.target.getAttribute('class'), '— .comp 못 찾음');
|
||||
return;
|
||||
}
|
||||
console.log('[DRAG] grabbed:', compEl.getAttribute('data-comp-id'));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const sp = toSvg(e.clientX, e.clientY);
|
||||
const tr = compEl.getAttribute('transform') || '';
|
||||
const orig = parseTranslate(tr);
|
||||
|
||||
dragging = {
|
||||
el: compEl,
|
||||
compId: compEl.getAttribute('data-comp-id'),
|
||||
compType: compEl.getAttribute('data-comp-type'),
|
||||
origTransform: tr,
|
||||
origX: orig.x,
|
||||
origY: orig.y,
|
||||
startMouseX: sp.x,
|
||||
startMouseY: sp.y,
|
||||
};
|
||||
compEl.style.opacity = '0.65';
|
||||
}
|
||||
|
||||
function onMouseMove(e) {
|
||||
if (!dragging) return;
|
||||
e.preventDefault();
|
||||
const sp = toSvg(e.clientX, e.clientY);
|
||||
const dx = sp.x - dragging.startMouseX;
|
||||
const dy = sp.y - dragging.startMouseY;
|
||||
if (dragging.isHandle) {
|
||||
const newCx = dragging.origCx + dx;
|
||||
const newCy = dragging.origCy + dy;
|
||||
dragging.el.setAttribute('cx', newCx.toFixed(1));
|
||||
dragging.el.setAttribute('cy', newCy.toFixed(1));
|
||||
return;
|
||||
}
|
||||
const newX = dragging.origX + dx;
|
||||
const newY = dragging.origY + dy;
|
||||
const newT = dragging.origTransform.replace(
|
||||
/translate\([^)]+\)/,
|
||||
`translate(${newX.toFixed(1)} ${newY.toFixed(1)})`
|
||||
);
|
||||
dragging.el.setAttribute('transform', newT);
|
||||
}
|
||||
|
||||
function onMouseUp(e) {
|
||||
if (!dragging) return;
|
||||
e.preventDefault();
|
||||
const sp = toSvg(e.clientX, e.clientY);
|
||||
const dx = sp.x - dragging.startMouseX;
|
||||
const dy = sp.y - dragging.startMouseY;
|
||||
|
||||
if (dragging.isHandle) {
|
||||
const newX = dragging.origCx + dx;
|
||||
const newY = dragging.origCy + dy;
|
||||
const moved = (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5);
|
||||
if (moved) applyHandleToTopology(dragging.pipeId, dragging.kind, dragging.viaIdx, newX, newY);
|
||||
showHandleResult(dragging.pipeId, dragging.kind, dragging.viaIdx, newX, newY);
|
||||
dragging.el.style.cursor = 'grab';
|
||||
dragging = null;
|
||||
if (moved) {
|
||||
rebuildScene();
|
||||
drawPipeHandles();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
showResult(dragging.compId, dragging.compType, dx, dy, dragging.origX, dragging.origY);
|
||||
if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
|
||||
const applied = applyToTopology(dragging.compId, dx, dy);
|
||||
if (applied) {
|
||||
rebuildScene();
|
||||
drawPipeHandles();
|
||||
}
|
||||
}
|
||||
|
||||
dragging.el.style.opacity = '';
|
||||
dragging = null;
|
||||
}
|
||||
|
||||
function applyHandleToTopology(pipeId, kind, viaIdx, newX, newY) {
|
||||
const PIPES = (window.INVYONE_TOPO && window.INVYONE_TOPO.PIPES) || [];
|
||||
const pipe = PIPES.find(p => p.id === pipeId);
|
||||
if (!pipe) return;
|
||||
if (kind === 'to-abs' && pipe.to && typeof pipe.to === 'object') {
|
||||
pipe.to.x = Math.round(newX);
|
||||
pipe.to.y = Math.round(newY);
|
||||
} else if (kind === 'via' && Array.isArray(pipe.via) && pipe.via[viaIdx]) {
|
||||
const v = pipe.via[viaIdx];
|
||||
if (typeof v.x === 'number') v.x = Math.round(newX);
|
||||
if (typeof v.y === 'number') v.y = Math.round(newY);
|
||||
}
|
||||
}
|
||||
|
||||
function showHandleResult(pipeId, kind, viaIdx, x, y) {
|
||||
let msg;
|
||||
if (kind === 'to-abs') {
|
||||
msg = `▶ ${pipeId} (pipe to)\n새 끝점: { x: ${Math.round(x)}, y: ${Math.round(y)} }`;
|
||||
} else {
|
||||
const v = window.INVYONE_TOPO.PIPES.find(p => p.id === pipeId).via[viaIdx];
|
||||
const parts = [];
|
||||
if (typeof v.x === 'number') parts.push(`x: ${Math.round(x)}`);
|
||||
if (typeof v.y === 'number') parts.push(`y: ${Math.round(y)}`);
|
||||
msg = `▶ ${pipeId} (pipe via[${viaIdx}])\n새 좌표: { ${parts.join(', ')} }`;
|
||||
}
|
||||
console.log('%c[DRAG]\n' + msg, 'color:#ff8a3a;font-weight:700;');
|
||||
appendToPanel(msg, '#ff8a3a');
|
||||
}
|
||||
|
||||
// 결과 누적 패널 — 모든 변경 사항이 한 곳에 쌓임
|
||||
const HISTORY = [];
|
||||
function appendToPanel(msg, color) {
|
||||
HISTORY.push({ time: new Date().toLocaleTimeString('ko-KR'), msg, color });
|
||||
let panel = document.getElementById('drag-result-panel');
|
||||
if (!panel) {
|
||||
panel = document.createElement('div');
|
||||
panel.id = 'drag-result-panel';
|
||||
panel.style.cssText = `position:fixed;top:60px;right:10px;background:#11102a;
|
||||
color:#fff;padding:0;border-radius:4px;border:1px solid #5af9ff;
|
||||
font:700 11px/1.5 monospace;z-index:9999;width:380px;max-height:80vh;
|
||||
display:flex;flex-direction:column;box-shadow:0 2px 10px rgba(0,0,0,0.5);`;
|
||||
const header = document.createElement('div');
|
||||
header.style.cssText = `padding:8px 12px;background:#1a1936;border-bottom:1px solid #5af9ff;
|
||||
display:flex;justify-content:space-between;align-items:center;flex-shrink:0;`;
|
||||
header.innerHTML = `<span style="color:#5af9ff;">📋 변경 기록 (<span id="drag-history-count">0</span>)</span>
|
||||
<span><button id="drag-copy-all" style="background:#5af9ff;color:#11102a;border:0;padding:3px 8px;border-radius:3px;cursor:pointer;font-weight:700;font-size:10px;margin-right:4px;">전체 복사</button>
|
||||
<button id="drag-clear" style="background:#444;color:#fff;border:0;padding:3px 8px;border-radius:3px;cursor:pointer;font-size:10px;margin-right:4px;">지우기</button>
|
||||
<button id="drag-close" style="background:#d63031;color:#fff;border:0;padding:3px 8px;border-radius:3px;cursor:pointer;font-size:10px;">닫기</button></span>`;
|
||||
const body = document.createElement('div');
|
||||
body.id = 'drag-history-body';
|
||||
body.style.cssText = `padding:8px 12px;overflow-y:auto;flex:1;white-space:pre-wrap;`;
|
||||
panel.appendChild(header);
|
||||
panel.appendChild(body);
|
||||
document.body.appendChild(panel);
|
||||
document.getElementById('drag-copy-all').addEventListener('click', () => {
|
||||
const all = HISTORY.map(h => h.msg).join('\n\n');
|
||||
if (navigator.clipboard) navigator.clipboard.writeText(all).then(() => {
|
||||
const btn = document.getElementById('drag-copy-all');
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = '복사됨!';
|
||||
setTimeout(() => { btn.textContent = orig; }, 1200);
|
||||
});
|
||||
});
|
||||
document.getElementById('drag-clear').addEventListener('click', () => {
|
||||
HISTORY.length = 0;
|
||||
renderHistory();
|
||||
});
|
||||
document.getElementById('drag-close').addEventListener('click', () => panel.remove());
|
||||
}
|
||||
renderHistory();
|
||||
if (navigator.clipboard) navigator.clipboard.writeText(msg).catch(() => {});
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
const body = document.getElementById('drag-history-body');
|
||||
const count = document.getElementById('drag-history-count');
|
||||
if (!body) return;
|
||||
count.textContent = HISTORY.length;
|
||||
body.innerHTML = HISTORY.map((h, i) => {
|
||||
return `<div style="margin-bottom:10px;padding:6px 8px;background:#0a0a1a;border-left:3px solid ${h.color};border-radius:3px;">
|
||||
<div style="color:#888;font-size:10px;margin-bottom:3px;">#${i + 1} · ${h.time}</div>
|
||||
<div style="color:${h.color};">${h.msg.replace(/</g, '<')}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
body.scrollTop = body.scrollHeight;
|
||||
}
|
||||
|
||||
function applyToTopology(compId, dx, dy) {
|
||||
const meta = findCompMeta(compId);
|
||||
if (!meta) return false;
|
||||
const c = meta.comp;
|
||||
if (c.anchor && c.anchor.absolute) {
|
||||
c.anchor.absolute.x = (c.anchor.absolute.x || 0) + dx;
|
||||
c.anchor.absolute.y = (c.anchor.absolute.y || 0) + dy;
|
||||
return true;
|
||||
}
|
||||
if (c.anchor && !c.anchor.absolute) {
|
||||
c.anchor.dx = (c.anchor.dx ?? 0) + dx;
|
||||
c.anchor.dy = (c.anchor.dy ?? 0) + dy;
|
||||
return true;
|
||||
}
|
||||
if (typeof c.x === 'number' && typeof c.y === 'number') {
|
||||
c.x += dx;
|
||||
c.y += dy;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function rebuildScene() {
|
||||
const sceneEl = document.getElementById('scene-content');
|
||||
if (!sceneEl || !window.INVYONE_TOPO) return;
|
||||
// anchor 기반 컴포넌트의 _resolved 캐시 리셋 (dx/dy/at 등 변경 사항 재계산되도록)
|
||||
const T = window.INVYONE_TOPO;
|
||||
['TANKS','PUMPS','VALVES','FILTERS','MODULES','GAUGES','SENSORS','LEDS'].forEach(b => {
|
||||
(T[b] || []).forEach(c => {
|
||||
if (c.anchor) {
|
||||
c._resolved = false;
|
||||
if (!c.anchor.absolute) { c.x = undefined; c.y = undefined; }
|
||||
}
|
||||
});
|
||||
});
|
||||
sceneEl.innerHTML = window.INVYONE_TOPO.buildScene();
|
||||
if (window.INVYONE_ENGINE && typeof window.INVYONE_ENGINE.init === 'function') {
|
||||
try { window.INVYONE_ENGINE.init(); } catch (err) { console.warn('[DRAG] engine.init re-call failed:', err); }
|
||||
}
|
||||
}
|
||||
|
||||
function findCompMeta(compId) {
|
||||
if (!window.INVYONE_TOPO) return null;
|
||||
const T = window.INVYONE_TOPO;
|
||||
const buckets = ['TANKS', 'PUMPS', 'GAUGES', 'SENSORS', 'FILTERS', 'MODULES', 'LEDS', 'VALVES', 'TILES', 'PANELS'];
|
||||
for (const b of buckets) {
|
||||
const arr = T[b] || [];
|
||||
const c = arr.find((x) => x.id === compId);
|
||||
if (c) return { bucket: b, comp: c };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function showResult(compId, compType, dx, dy, origX, origY) {
|
||||
const meta = findCompMeta(compId);
|
||||
const isAnchor = !!(meta && meta.comp && meta.comp.anchor && !meta.comp.anchor.absolute);
|
||||
|
||||
// 게이지는 transform 안에 (x-20, y-20) 적용돼 있으니 anchor 절대 좌표는 +20 보정
|
||||
const isGauge = compType === 'gauge';
|
||||
const baseX = isGauge ? origX + 20 : origX;
|
||||
const baseY = isGauge ? origY + 20 : origY;
|
||||
const newAnchorX = baseX + dx;
|
||||
const newAnchorY = baseY + dy;
|
||||
|
||||
const dxStr = (dx >= 0 ? '+' : '') + dx.toFixed(1);
|
||||
const dyStr = (dy >= 0 ? '+' : '') + dy.toFixed(1);
|
||||
|
||||
let lines = [`▶ ${compId} (${compType || '-'})`, `이동량: dx=${dxStr}, dy=${dyStr}`];
|
||||
|
||||
if (isAnchor) {
|
||||
const a = meta.comp.anchor;
|
||||
const oldDx = a.dx ?? 0;
|
||||
const oldDy = a.dy ?? 0;
|
||||
lines.push(`anchor 기반 → 기존 dx:${oldDx}, dy:${oldDy}`);
|
||||
lines.push(`수정 후: dx:${(oldDx + dx).toFixed(1)}, dy:${(oldDy + dy).toFixed(1)}`);
|
||||
} else {
|
||||
lines.push(`절대 좌표 → 새 위치: x=${newAnchorX.toFixed(0)}, y=${newAnchorY.toFixed(0)}`);
|
||||
}
|
||||
|
||||
const msg = lines.join('\n');
|
||||
console.log('%c[DRAG]\n' + msg, 'color:#5af9ff;font-weight:700;');
|
||||
appendToPanel(msg, '#5af9ff');
|
||||
}
|
||||
|
||||
function init() {
|
||||
svg = document.getElementById('scene');
|
||||
if (!svg) {
|
||||
setTimeout(init, 100);
|
||||
return;
|
||||
}
|
||||
window.addEventListener('mousedown', onMouseDown, true);
|
||||
window.addEventListener('mousemove', onMouseMove, true);
|
||||
window.addEventListener('mouseup', onMouseUp, true);
|
||||
console.log('%c[DEV] Drag mode 사용 가능: Shift+D 로 토글', 'color:#5af9ff;font-weight:700;');
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -5,14 +5,14 @@
|
||||
(function (global) {
|
||||
// ===== TANKS =====
|
||||
const TANKS = [
|
||||
{ id: 'sand', x: 340, y: 120, w: 96, h: 150, type: 'ac', label: 'SAND TANK', capacity: 5000, level: 62.0, color: '#5af9ff', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
|
||||
{ id: 'ac', x: 500, y: 120, w: 96, h: 150, type: 'ac', label: 'AC TANK', capacity: 5000, level: 58.4, color: '#5af9ff', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
|
||||
{ id: 'raw', x: 660, y: 90, w: 120, h: 230, type: 'tank', label: 'RAW WATER TANK', capacity: 30000, level: 75.0, color: '#3aa3ff', alarms: { hh: 95, hl: 85, ll: 20, lh: 10 } },
|
||||
{ id: 'chem', x: 895, y: 110, w: 50, h: 70, type: 'dosing', label: 'Chem', capacity: 500, level: 71.0, color: '#7cff3a', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
|
||||
{ id: 'water-filter', x: 1600, y: 90, w: 120, h: 230, type: 'tank', label: 'WATER FILTER TANK', capacity: 25000, level: 48.0, color: '#3aa3ff', alarms: { hh: 95, hl: 85, ll: 20, lh: 10 }, nozzles: [{ side: 'left', pos: 0.30 }, { side: 'bot', pos: 0.5 }] },
|
||||
{ id: 'chem-cep-1', x: 80, y: 545, w: 50, h: 70, type: 'dosing', label: 'CEP-1', capacity: 500, level: 64.2, color: '#ff8a3a', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
|
||||
{ id: 'chem-cep-2', x: 150, y: 545, w: 50, h: 70, type: 'dosing', label: 'CEP-2', capacity: 500, level: 55.8, color: '#ff4f9a', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
|
||||
{ id: 'cip-tank', x: 80, y: 765, w: 60, h: 80, type: 'dosing', label: 'CIP TANK', capacity: 1500, level: 80.5, color: '#5af9ff', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
|
||||
{ id: 'sand', x: 340, y: 90, w: 96, h: 150, type: 'ac', label: 'SAND TANK', capacity: 5000, level: 62.0, color: '#5af9ff', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
|
||||
{ id: 'ac', x: 500, y: 90, w: 96, h: 150, type: 'ac', label: 'AC TANK', capacity: 5000, level: 58.4, color: '#5af9ff', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
|
||||
{ id: 'raw', x: 661, y: 51, w: 120, h: 230, type: 'tank', label: 'RAW WATER TANK', capacity: 30000, level: 75.0, color: '#3aa3ff', alarms: { hh: 95, hl: 85, ll: 20, lh: 10 } },
|
||||
{ id: 'chem', x: 895, y: 80, w: 50, h: 70, type: 'dosing', label: 'Chem', capacity: 500, level: 71.0, color: '#7cff3a', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
|
||||
{ id: 'water-filter', x: 1600, y: 60, w: 120, h: 230, type: 'tank', label: 'WATER FILTER TANK', capacity: 25000, level: 48.0, color: '#3aa3ff', alarms: { hh: 95, hl: 85, ll: 20, lh: 10 }, nozzles: [{ side: 'left', pos: 0.30 }, { side: 'bot', pos: 0.5 }] },
|
||||
{ id: 'chem-cep-1', x: 60, y: 545, w: 50, h: 70, type: 'dosing', label: 'CEP-1', capacity: 500, level: 64.2, color: '#ff8a3a', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
|
||||
{ id: 'chem-cep-2', x: 130, y: 545, w: 50, h: 70, type: 'dosing', label: 'CEP-2', capacity: 500, level: 55.8, color: '#ff4f9a', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
|
||||
{ id: 'cip-tank', x: 80, y: 845, w: 60, h: 80, type: 'dosing', label: 'CIP TANK', capacity: 1500, level: 80.5, color: '#5af9ff', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
|
||||
{ id: 'mbr', x: 760, y: 600, w: 500, h: 200, type: 'mbr', label: 'MBR TANK', capacity: 40000, level: 52.0, color: '#ffd682', alarms: { hh: 92, hl: 80, ll: 20, lh: 10 } },
|
||||
{ id: 'dp', x: 1320, y: 545, w: 90, h: 180, type: 'tank', label: 'DP TANK', capacity: 8000, level: 87.0, color: '#3aa3ff', alarms: { hh: 95, hl: 85, ll: 20, lh: 10 } },
|
||||
{ id: 'regulating', x: 1580, y: 640, w: 100, h: 180, type: 'tank', label: 'REGULATING TANK', capacity: 12000, level: 54.06, color: '#ff8a3a', alarms: { hh: 95, hl: 85, ll: 20, lh: 10 } },
|
||||
@@ -26,7 +26,7 @@
|
||||
];
|
||||
|
||||
const MODULES = [
|
||||
{ id: 'uf-system', anchor: { to: 'uf-filter.right', myPort: 'topLeft', dx: 62, dy: -122 }, type: 'membrane', count: 5, label: 'UF SYSTEM', accent: '#5af9ff', orientation: 'vertical' },
|
||||
{ id: 'uf-system', anchor: { to: 'uf-filter.right', myPort: 'topLeft', dx: 82, dy: -87 }, type: 'membrane', count: 5, label: 'UF SYSTEM', accent: '#5af9ff', orientation: 'vertical' },
|
||||
{ id: 'ro-system', anchor: { to: 'ro-press.out', myPort: 'in', dx: 55, dy: -47 }, type: 'membrane', count: 5, label: 'RO SYSTEM', accent: '#5af9ff', orientation: 'horizontal' },
|
||||
{ id: 'mbr-carb-l', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: -183, dy: 106 }, type: 'carbon', count: 5, label: 'CARBON L', accent: '#5af9ff' },
|
||||
{ id: 'mbr-carb-r', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: 77, dy: 106 }, type: 'carbon', count: 5, label: 'CARBON R', accent: '#5af9ff' },
|
||||
@@ -34,8 +34,8 @@
|
||||
|
||||
const PUMPS = [
|
||||
// Root pumps (no natural upstream tank — keep absolute)
|
||||
{ id: 'bw-a1', x: 78, y: 220, label: 'FEED-A', source: null, dest: 'sand', lpm: 80, valves: ['v-input'], pipes: ['p-input-bwa1', 'p-bwa1-pf', 'p-pf-sand'] },
|
||||
{ id: 'air-blower', x: 380, y: 785, label: 'AIR BLOWER', source: null, dest: 'mbr', lpm: 0, valves: [], pipes: ['p-blower-mbr'] },
|
||||
{ id: 'bw-a1', x: 78, y: 190, label: 'FEED-A', source: null, dest: 'sand', lpm: 80, valves: ['v-input'], pipes: ['p-input-bwa1', 'p-bwa1-pf', 'p-pf-sand'] },
|
||||
{ id: 'air-blower', x: 380, y: 865, label: 'AIR BLOWER', source: null, dest: 'mbr', lpm: 0, valves: [], pipes: ['p-blower-mbr'] },
|
||||
|
||||
// Tank-foot pumps (anchor to tank bottom, my top inlet aligns)
|
||||
{ id: 'sand-out', anchor: { to: 'sand.bot', myPort: 'in', dy: 14 }, label: 'SAND PUMP', source: 'sand', dest: 'ac', lpm: 80, valves: ['v-sand-out'], pipes: ['p-sand-down', 'p-sand-ac'], orient: 'v' },
|
||||
@@ -61,11 +61,11 @@
|
||||
// All valves placed on their gating pipe — onPipe + at fraction
|
||||
{ id: 'v-input', anchor: { onPipe: 'p-bwa1-pf', at: 0.30 }, label: 'V-IN', open: false },
|
||||
{ id: 'v-sand-out', anchor: { onPipe: 'p-sand-ac', at: 0.55 }, label: '', open: false },
|
||||
{ id: 'v-ac-out', anchor: { onPipe: 'p-ac-raw', at: 0.78 }, label: '', open: false },
|
||||
{ id: 'v-ac-out', anchor: { onPipe: 'p-ac-raw', at: 0.78, dx: -46, dy: -1 }, label: '', open: false },
|
||||
{ id: 'v-raw-out', anchor: { onPipe: 'p-raw-bw2', at: 0.78 }, label: '', open: false },
|
||||
{ id: 'v-chem-uf', anchor: { onPipe: 'p-chem-uf', at: 0.5 }, label: '', open: false },
|
||||
{ id: 'v-uf-in', anchor: { onPipe: 'p-bw2-uf', at: 0.75 }, label: 'V-UF-IN', open: false },
|
||||
{ id: 'v-uf-out', anchor: { onPipe: 'p-uf-wf', at: 0.20 }, label: 'V-UF-OUT', open: false },
|
||||
{ id: 'v-uf-out', anchor: { onPipe: 'p-uf-wf', at: 0.20, dx: -24, dy: 0 }, label: 'V-UF-OUT', open: false },
|
||||
{ id: 'v-ro-in', anchor: { onPipe: 'p-rofeed-filter', at: 0.5 }, label: '', open: false },
|
||||
{ id: 'v-ro-press-out', anchor: { onPipe: 'p-ropress-ro', at: 0.4 }, label: '', open: false },
|
||||
{ id: 'v-cip-out', anchor: { onPipe: 'p-cip-ro', at: 0.4 }, label: '', open: false },
|
||||
@@ -433,18 +433,18 @@
|
||||
{ id: 'p-bwa1-pf', from: 'bw-a1.out', to: 'pump-filter.left' },
|
||||
{ id: 'p-pf-sand', from: 'pump-filter.right', to: 'sand.left', via: [{ y: 200 }] },
|
||||
{ id: 'p-sand-down', from: 'sand.bot', to: 'sand-out.top' },
|
||||
{ id: 'p-sand-ac', from: 'sand-out.out', to: 'ac-out.in', via: [{ y: 290 }] },
|
||||
{ id: 'p-sand-ac', from: 'sand-out.out', to: 'ac-out.in', via: [{ y: 279 }] },
|
||||
{ id: 'p-ac-down', from: 'ac.bot', to: 'ac-out.top' },
|
||||
{ id: 'p-ac-raw', from: 'ac-out.out', to: 'raw.top', via: [{ x: 605 }, { y: 90 }] },
|
||||
{ id: 'p-raw-bw2', from: 'raw.right', to: 'bw-a2.in', via: [{ y: 250 }] },
|
||||
{ id: 'p-raw-bw2', from: 'raw.right', to: 'bw-a2.in', via: [{ y: 211 }] },
|
||||
{ id: 'p-bw2-uf', from: 'bw-a2.out', to: 'uf-pump-a.in' },
|
||||
{ id: 'p-chem-uf', from: 'chem.bot', to: 'chem-uf.in', hint: 'v-first' },
|
||||
{ id: 'p-chemuf-out', from: 'chem-uf.out', to: { x: 970, y: 250 } },
|
||||
{ id: 'p-raw-uf', from: 'raw.rightLow', to: 'uf-pump-b.in', via: [{ x: 990 }] },
|
||||
{ id: 'p-chemuf-out', from: 'chem-uf.out', to: { x: 980, y: 209 } },
|
||||
{ id: 'p-raw-uf', from: 'raw.rightLow', to: 'uf-pump-b.in', via: [{ x: 999 }] },
|
||||
{ id: 'p-uf-filter', from: 'uf-pump-a.out', to: 'uf-filter.left' },
|
||||
{ id: 'p-ufpb-filter',from: 'uf-pump-b.out', to: 'uf-filter.left' },
|
||||
{ id: 'p-filter-uf', from: 'uf-filter.right', to: 'uf-system.topLeft' },
|
||||
{ id: 'p-uf-wf', from: 'uf-system.botRight', to: 'water-filter.leftHigh', via: [{ x: 1490 }, { y: 159 }] },
|
||||
{ id: 'p-uf-wf', from: 'uf-system.botRight', to: 'water-filter.leftHigh', via: [{ x: 1490 }, { y: 130 }] },
|
||||
|
||||
// Bottom row — RO / MBR / DP / REG
|
||||
{ id: 'p-wf-ro', from: 'water-filter.bot', to: 'ro-feed.in', via: [{ y: 420 }, { x: 200 }, { y: 660 }] },
|
||||
@@ -452,13 +452,13 @@
|
||||
{ id: 'p-filter-ropress',from: 'ro-filter.right', to: 'ro-press.in' },
|
||||
{ id: 'p-ropress-ro', from: 'ro-press.out', to: 'ro-system.in' },
|
||||
{ id: 'p-ro-mbr', from: 'ro-system.out', to: 'mbr.leftHigh' },
|
||||
{ id: 'p-cip-ro', from: 'cip-pump.out', to: 'ro-system.bot', via: [{ y: 730 }] },
|
||||
{ id: 'p-ciptank-pump', from: 'cip-tank.bot', to: 'cip-pump.in', via: [{ y: 820 }] },
|
||||
{ id: 'p-cip-ro', from: 'cip-pump.out', to: 'ro-system.bot', via: [{ y: 834 }] },
|
||||
{ id: 'p-ciptank-pump', from: 'cip-tank.bot', to: 'cip-pump.in', via: [{ y: 900 }] },
|
||||
{ id: 'p-blower-mbr', from: 'air-blower.out', to: 'mbr.bot', via: [{ x: 700 }] },
|
||||
|
||||
// MBR → AERATION DRAIN → DP TANK
|
||||
{ id: 'p-mbr-aer', from: 'mbr.botRight', to: 'aer-drain.in' },
|
||||
{ id: 'p-aer-dp', from: 'aer-drain.out', to: 'dp.bot', via: [{ x: 1290 }, { y: 760 }, { x: 1365 }] },
|
||||
{ id: 'p-aer-dp', from: 'aer-drain.out', to: 'dp.bot', via: [{ x: 1289 }, { y: 760 }, { x: 1365 }] },
|
||||
|
||||
// DP TANK → DP PUMP → REGULATING
|
||||
{ id: 'p-dp-down', from: 'dp.bot', to: 'dp-out.in' },
|
||||
@@ -468,23 +468,23 @@
|
||||
const GAUGES = [
|
||||
{ id: 'p-in-1', anchor: { to: 'bw-a1.top', myPort: 'center', dx: -23, dy: -110 }, label: 'P-IN', max: 8, unit: 'bar', source: 'bw-a1' },
|
||||
{ id: 'p-in-2', anchor: { to: 'bw-a1.top', myPort: 'center', dx: 87, dy: -110 }, label: 'P-PF', max: 8, unit: 'bar', source: 'bw-a1' },
|
||||
{ id: 'p-raw', anchor: { to: 'raw.bot', myPort: 'center', dx: -35, dy: 60 }, label: 'P-RAW', max: 8, unit: 'bar', source: 'bw-a2' },
|
||||
{ id: 'p-uf-1', anchor: { to: 'uf-pump-a.bot', myPort: 'center', dx: 210, dy: 75 }, label: 'P-UF-IN', max: 10, unit: 'bar', source: 'uf-pump-a' },
|
||||
{ id: 'p-uf-2', anchor: { to: 'uf-pump-a.bot', myPort: 'center', dx: 515, dy: 75 }, label: 'P-UF-OUT', max: 10, unit: 'bar', source: 'uf-pump-a' },
|
||||
{ id: 'p-raw', anchor: { to: 'raw.bot', myPort: 'center', dx: -31, dy: 64 }, label: 'P-RAW', max: 8, unit: 'bar', source: 'bw-a2' },
|
||||
{ id: 'p-uf-1', anchor: { to: 'uf-pump-a.bot', myPort: 'center', dx: 218, dy: 100 }, label: 'P-UF-IN', max: 10, unit: 'bar', source: 'uf-pump-a' },
|
||||
{ id: 'p-uf-2', anchor: { to: 'uf-pump-a.bot', myPort: 'center', dx: 497, dy: 100 }, label: 'P-UF-OUT', max: 10, unit: 'bar', source: 'uf-pump-a' },
|
||||
{ id: 'p-wf', anchor: { to: 'water-filter.right', myPort: 'center', dx: 90, dy: -45 }, label: 'P-WF', max: 8, unit: 'bar', source: 'bw-a2' },
|
||||
{ id: 'p-cep-1', anchor: { to: 'chem-cep-1.bot', myPort: 'center', dx: -15, dy: 65 }, label: 'P1', max: 8, unit: 'bar', source: 'ro-feed' },
|
||||
{ id: 'p-cep-2', anchor: { to: 'chem-cep-2.bot', myPort: 'center', dx: -5, dy: 65 }, label: 'P2', max: 8, unit: 'bar', source: 'ro-feed' },
|
||||
{ id: 'p-ro-in', anchor: { to: 'ro-feed.top', myPort: 'center', dx: 20, dy: -45 }, label: 'P-RO-IN', max: 16, unit: 'bar', source: 'ro-feed' },
|
||||
{ id: 'p-ro-out', anchor: { to: 'ro-press.top', myPort: 'center', dx: 315, dy: -45 }, label: 'P-RO-OUT', max: 16, unit: 'bar', source: 'ro-press' },
|
||||
{ id: 'p-dp-1', anchor: { to: 'dp.top', myPort: 'center', dx: 0, dy: -50 }, label: 'P-DP', max: 8, unit: 'bar', source: 'aer-drain' },
|
||||
{ id: 'p-cep-1', anchor: { to: 'chem-cep-1.bot', myPort: 'center', dx: -15, dy: 95 }, label: 'P1', max: 8, unit: 'bar', source: 'ro-feed' },
|
||||
{ id: 'p-cep-2', anchor: { to: 'chem-cep-2.bot', myPort: 'center', dx: -5, dy: 95 }, label: 'P2', max: 8, unit: 'bar', source: 'ro-feed' },
|
||||
{ id: 'p-ro-in', anchor: { to: 'ro-feed.top', myPort: 'center', dx: 20, dy: -57 }, label: 'P-RO-IN', max: 16, unit: 'bar', source: 'ro-feed' },
|
||||
{ id: 'p-ro-out', anchor: { to: 'ro-press.top', myPort: 'center', dx: 315, dy: -130 }, label: 'P-RO-OUT', max: 16, unit: 'bar', source: 'ro-press' },
|
||||
{ id: 'p-dp-1', anchor: { to: 'dp.top', myPort: 'center', dx: 95, dy: -50 }, label: 'P-DP', max: 8, unit: 'bar', source: 'aer-drain' },
|
||||
];
|
||||
|
||||
const SENSORS = [
|
||||
{ id: 'do', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: -250, dy: -80 }, label: 'DO', value: 8.5, unit: 'ppm', min: 4, max: 12, alarms: { lo: 5, hi: 11 } },
|
||||
{ id: 'temp', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: -130, dy: -80 }, label: 'Temp', value: 25.0, unit: 'C', min: 0, max: 50, alarms: { lo: 10, hi: 35 } },
|
||||
{ id: 'tss', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: -10, dy: -80 }, label: 'TSS', value: 0.28, unit: 'mg/L', min: 0, max: 5, alarms: { lo: 0, hi: 2 } },
|
||||
{ id: 'ph', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: 110, dy: -80 }, label: 'pH', value: 8.5, unit: 'pH', min: 0, max: 14, alarms: { lo: 6.5, hi: 8.5 } },
|
||||
{ id: 'tss-raw', anchor: { to: 'bw-a1.bot', myPort: 'topLeft', dx: -68, dy: 75 }, label: 'TSS-RAW', value: 1.42, unit: 'mg/L', min: 0, max: 5, alarms: { lo: 0, hi: 3 } },
|
||||
{ id: 'do', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: -250, dy: -90 }, label: 'DO', value: 8.5, unit: 'ppm', min: 4, max: 12, alarms: { lo: 5, hi: 11 } },
|
||||
{ id: 'temp', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: -110, dy: -90 }, label: 'Temp', value: 25.0, unit: 'C', min: 0, max: 50, alarms: { lo: 10, hi: 35 } },
|
||||
{ id: 'tss', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: 30, dy: -90 }, label: 'TSS', value: 0.28, unit: 'mg/L', min: 0, max: 5, alarms: { lo: 0, hi: 2 } },
|
||||
{ id: 'ph', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: 170, dy: -90 }, label: 'pH', value: 8.5, unit: 'pH', min: 0, max: 14, alarms: { lo: 6.5, hi: 8.5 } },
|
||||
{ id: 'tss-raw', anchor: { to: 'bw-a1.bot', myPort: 'topLeft', dx: -68, dy: 105 }, label: 'TSS-RAW', value: 1.42, unit: 'mg/L', min: 0, max: 5, alarms: { lo: 0, hi: 3 } },
|
||||
];
|
||||
|
||||
const LEDS = [
|
||||
@@ -493,15 +493,15 @@
|
||||
];
|
||||
|
||||
const TILES = [
|
||||
{ id: 'flow-in', x: 1830, y: 520, label: 'FLOW IN', value: '0.0', unit: 'L/min', w: 130, h: 50 },
|
||||
{ id: 'flow-out', x: 1830, y: 580, label: 'FLOW OUT', value: '0.0', unit: 'L/min', w: 130, h: 50 },
|
||||
{ id: 'recovery', x: 1830, y: 640, label: 'RECOVERY', value: '0.0', unit: '%', w: 130, h: 50 },
|
||||
{ id: 'alarms', x: 1830, y: 700, label: 'ACTIVE ALARMS', value: '0', unit: '', w: 130, h: 50 },
|
||||
{ id: 'flow-in', x: 1830, y: 520, label: 'FLOW IN', value: '0.0', unit: 'L/min', w: 150, h: 64 },
|
||||
{ id: 'flow-out', x: 1830, y: 594, label: 'FLOW OUT', value: '0.0', unit: 'L/min', w: 150, h: 64 },
|
||||
{ id: 'recovery', x: 1830, y: 668, label: 'RECOVERY', value: '0.0', unit: '%', w: 150, h: 64 },
|
||||
{ id: 'alarms', x: 1830, y: 742, label: 'ACTIVE ALARMS', value: '0', unit: '', w: 150, h: 64 },
|
||||
];
|
||||
|
||||
const PANELS = [
|
||||
{ id: 'cep', x: 40, y: 510, w: 410, h: 210, label: 'CEP SYSTEM' },
|
||||
{ id: 'cip', x: 40, y: 740, w: 470, h: 125, label: 'CIP SYSTEM' },
|
||||
{ id: 'cep', x: 40, y: 510, w: 410, h: 300, label: 'CEP SYSTEM' },
|
||||
{ id: 'cip', x: 40, y: 820, w: 470, h: 125, label: 'CIP SYSTEM' },
|
||||
{ id: 'mbr-frame', x: 760, y: 600, w: 500, h: 200, label: '' },
|
||||
];
|
||||
|
||||
@@ -569,7 +569,7 @@
|
||||
const bubbles = bubbleSpots.map((bx, i) => `<circle class="bubble" cx="${bx}" cy="${t.h - 10}" r="${1.6 + (i % 3) * 0.5}" fill="#5af9ff" opacity="0" style="animation-delay:${(i * 0.42).toFixed(2)}s"/>`).join('\n ');
|
||||
out.push(`
|
||||
<g class="comp comp-tank comp-tank-mbr" data-comp-id="${t.id}" data-comp-type="tank" transform="translate(${t.x} ${t.y})">
|
||||
<text x="${t.w / 2}" y="-8" class="comp-label" font-size="13" text-anchor="middle">${t.label}</text>
|
||||
<text x="${t.w / 2}" y="-12" class="comp-label" font-size="15" font-weight="700" text-anchor="middle">${t.label}</text>
|
||||
<rect class="tank-shell" x="0" y="0" width="${t.w}" height="${t.h}" rx="4" fill="url(#metal-tank)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
|
||||
<rect x="3" y="3" width="${t.w - 6}" height="${t.h - 6}" fill="#071326" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
|
||||
<clipPath id="tank-${t.id}-clip"><rect x="3" y="3" width="${t.w - 6}" height="${t.h - 6}"/></clipPath>
|
||||
|
||||
Reference in New Issue
Block a user