Compare commits

4 Commits

Author SHA1 Message Date
DDD1542 48d74170fc merge: gbpark-node → main (SCADA/화재알람 데모 가독성 개선)
Build & Deploy to K8s / build-and-deploy (push) Failing after 6m31s
- SCADA 데모: 게이지 1.5x 확대, 설비 라벨 폰트 일괄 키움, 컴포넌트 위치 미세 조정
- SCADA 데모: dev-drag.js 추가 (Shift+D 임시 드래그 모드 + via/끝점 핸들)
- 화재 알람 데모: 건물 4종 색상 분류, 라벨/아이콘 폰트 확대
- chore: @anthropic-ai/claude-code dependency 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:07:54 +09:00
DDD1542 e8ba13f52b chore: @anthropic-ai/claude-code 패키지 dependencies 추가
Build & Deploy to K8s / build-and-deploy (push) Successful in 4m3s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:05:46 +09:00
DDD1542 5d2283cb47 feat: 화재 알람 데모 건물별 색상 분류 + 라벨/아이콘 가독성 개선
- 건물 4종(utility/service/factory1/office)에 배경 그라디언트 분기
- zone 영역도 같은 톤 색상 클래스 (za-utility/service/factory1/office)
- room-label 폰트 5.7 → 7.5, zone-label 10 → 13.5 키움
- 센서/manual-call 아이콘 26 → 34, 18 → 24 로 확대 + 위치 보정

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:05:42 +09:00
DDD1542 de0bfc1af4 feat: SCADA 데모 가독성 개선 + 컴포넌트 좌표 조정 도구 추가
- 압력 게이지 1.5x 확대, PRESS/판독값/라벨 폰트 강조
- 모든 설비(탱크/펌프/시스템/MBR/필터/카본) 라벨 폰트 +2~4 단계 키움
- 센서/타일 박스 및 폰트 확대, 위치 간격 보정
- 위쪽 처리 라인 30px 위로 + TSS-RAW 자기 자리 유지하도록 dy 보정
- AIR BLOWER 를 CIP 패널 내려간 만큼 같이 내림
- 게이지/밸브/모듈/탱크 및 파이프 라우팅 미세 조정 (드래그로 잡은 좌표 일괄 반영)
- dev-drag.js: Shift+D 임시 드래그 모드. 컴포넌트/파이프 via/절대 끝점 핸들로 좌표 인스펙션 후 토폴로지 재빌드, 변경 이력 누적 패널 + 전체 복사
2026-05-07 10:42:33 +09:00
7 changed files with 754 additions and 170 deletions
+125 -94
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
@@ -713,6 +713,29 @@
<stop offset="0.58" stop-color="#087033" />
<stop offset="1" stop-color="#04461f" />
</linearGradient>
<linearGradient id="building-bg-utility" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#1c8aa8" />
<stop offset="0.58" stop-color="#155f73" />
<stop offset="1" stop-color="#082c38" />
</linearGradient>
<linearGradient id="building-bg-service" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#c47018" />
<stop offset="0.58" stop-color="#8a4d0c" />
<stop offset="1" stop-color="#4d2806" />
</linearGradient>
<linearGradient id="building-bg-factory1" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#7c44b8" />
<stop offset="0.58" stop-color="#5a3088" />
<stop offset="1" stop-color="#2c1448" />
</linearGradient>
<linearGradient id="building-bg-office" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#5a6678" />
<stop offset="0.58" stop-color="#3e4858" />
<stop offset="1" stop-color="#232a36" />
</linearGradient>
<linearGradient id="core-room" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#0a6630" />
@@ -777,7 +800,11 @@
.road-dash { fill: none; stroke: #d6ecff; stroke-width: 1.05; stroke-dasharray: 8 6; opacity: .58; }
.route { fill: none; stroke: rgba(234,250,255,.65); stroke-width: .9; stroke-dasharray: 4 4; }
.building { fill: url(#building-bg); stroke: #f3fbff; stroke-width: 1.45; filter: url(#soft-shadow); }
.building { fill: url(#building-bg); stroke: #f3fbff; stroke-width: 1.45; filter: url(#soft-shadow); }
.building.bg-utility { fill: url(#building-bg-utility); }
.building.bg-service { fill: url(#building-bg-service); }
.building.bg-factory1 { fill: url(#building-bg-factory1); }
.building.bg-office { fill: url(#building-bg-office); }
.building-inset { fill: none; stroke: rgba(255,255,255,.36); stroke-width: .9; }
.room-core { fill: url(#core-room); stroke: rgba(255,255,255,.58); stroke-width: .8; }
.wall { stroke: #edf9ff; stroke-width: 1; opacity: .66; }
@@ -786,16 +813,20 @@
.detail-light { stroke: #dff4ff; stroke-width: .45; opacity: .31; }
.hatch { stroke: #dff4ff; stroke-width: .55; opacity: .28; }
.zone-area { fill: rgba(27,146,63,.12); stroke: rgba(255,255,255,.26); stroke-width: .78; cursor: pointer; }
.zone-area { fill: rgba(27,146,63,.12); stroke: rgba(255,255,255,.26); stroke-width: .78; cursor: pointer; }
.zone-area.za-utility { fill: rgba(40,170,200,.16); }
.zone-area.za-service { fill: rgba(220,140,40,.18); }
.zone-area.za-factory1 { fill: rgba(150,90,210,.18); }
.zone-area.za-office { fill: rgba(140,160,190,.14); }
.zone:hover .zone-area { fill: rgba(255,255,255,.075); }
.zone.warn .zone-area { fill: rgba(255,207,78,.32); stroke: #ffe595; }
.zone.alarm .zone-area { fill: rgba(255,56,82,.42); stroke: #ffffff; animation: alarmPulse .82s infinite; }
.building-name { font-family: Arial, 'Noto Sans KR', sans-serif; font-size: 18px; font-weight: 900; fill: #ffffff; text-anchor: middle; paint-order: stroke; stroke: rgba(0,0,0,.58); stroke-width: 3px; }
.building-sub { font-family: Arial, 'Noto Sans KR', sans-serif; font-size: 8px; font-weight: 800; fill: #e8fff1; text-anchor: middle; letter-spacing: .55px; }
.room-label { font-family: Arial, 'Noto Sans KR', sans-serif; font-size: 5.7px; font-weight: 700; fill: rgba(240,255,248,.82); text-transform: uppercase; }
.room-label.dim { fill: rgba(240,255,248,.54); }
.zone-label { font-family: Arial, 'Noto Sans KR', sans-serif; font-size: 10px; font-weight: 900; fill: #ffffff; text-anchor: middle; paint-order: stroke; stroke: rgba(0,0,0,.68); stroke-width: 3px; }
.room-label { font-family: Arial, 'Noto Sans KR', sans-serif; font-size: 7.5px; font-weight: 700; fill: rgba(240,255,248,.88); text-transform: uppercase; }
.room-label.dim { fill: rgba(240,255,248,.6); }
.zone-label { font-family: Arial, 'Noto Sans KR', sans-serif; font-size: 13.5px; font-weight: 900; fill: #ffffff; text-anchor: middle; paint-order: stroke; stroke: rgba(0,0,0,.78); stroke-width: 3.4px; }
.small-note { font-family: Arial, 'Noto Sans KR', sans-serif; font-size: 5.5px; font-weight: 700; fill: rgba(237,252,255,.58); }
.sensor-outer { fill: #0e1826; stroke: #ffffff; stroke-width: 1.4; }
@@ -851,31 +882,31 @@
<!-- Wastewater -->
<g id="zone-01" class="zone" data-zone="ZONE 01">
<rect x="48" y="44" width="158" height="90" class="building" />
<rect x="56" y="52" width="142" height="74" class="zone-area" />
<rect x="48" y="44" width="158" height="90" class="building bg-utility" />
<rect x="56" y="52" width="142" height="74" class="zone-area za-utility" />
<path d="M56 88H198 M96 52V126 M148 52V126" class="detail" />
<path d="M62 56V126 M70 56V126 M78 56V126 M184 56V126" class="hatch" />
<text x="127" y="89" class="building-name" style="font-size:13px;stroke-width:2.2px">WASTEWATER</text>
<use href="#sensor" x="114" y="98" width="26" height="26" />
<use href="#manual-call" x="82" y="100" width="18" height="18" />
<text x="127" y="70" class="building-name" style="font-size:16px;stroke-width:2.6px">WASTEWATER</text>
<use href="#sensor" x="110" y="78" width="34" height="34" />
<use href="#manual-call" x="79" y="80" width="24" height="24" />
<text x="127" y="130" class="zone-label">ZONE 01</text>
</g>
<!-- SBG -->
<g id="zone-02" class="zone" data-zone="ZONE 02">
<rect x="48" y="140" width="158" height="92" class="building" />
<rect x="56" y="148" width="142" height="76" class="zone-area" />
<rect x="48" y="140" width="158" height="92" class="building bg-utility" />
<rect x="56" y="148" width="142" height="76" class="zone-area za-utility" />
<path d="M56 184H198 M100 148V224 M152 148V224" class="detail" />
<path d="M62 152V224 M70 152V224 M78 152V224 M184 152V224" class="hatch" />
<text x="127" y="184" class="building-name" style="font-size:13px;stroke-width:2.2px">SBG TREATMENT</text>
<use href="#sensor" x="114" y="196" width="26" height="26" />
<use href="#manual-call" x="166" y="198" width="18" height="18" />
<text x="127" y="164" class="building-name" style="font-size:15px;stroke-width:2.6px">SBG TREATMENT</text>
<use href="#sensor" x="110" y="174" width="34" height="34" />
<use href="#manual-call" x="163" y="176" width="24" height="24" />
<text x="127" y="226" class="zone-label">ZONE 02</text>
</g>
<!-- Utility building -->
<g>
<rect x="246" y="44" width="560" height="164" class="building" />
<rect x="246" y="44" width="560" height="164" class="building bg-utility" />
<rect x="256" y="54" width="540" height="144" class="building-inset" />
<text x="526" y="74" class="building-name">UTILITY BUILDING</text>
@@ -894,31 +925,31 @@
<path d="M688 112h86 M688 128h86 M688 144h86 M688 160h86" class="detail-light" />
<g id="zone-03" class="zone" data-zone="ZONE 03">
<rect x="256" y="96" width="130" height="102" class="zone-area" />
<text x="321" y="160" class="zone-label">ZONE 03</text>
<use href="#sensor" x="308" y="125" width="26" height="26" />
<use href="#manual-call" x="260" y="174" width="17" height="17" />
<rect x="256" y="96" width="130" height="102" class="zone-area za-utility" />
<text x="321" y="180" class="zone-label">ZONE 03</text>
<use href="#sensor" x="304" y="106" width="34" height="34" />
<use href="#manual-call" x="256" y="170" width="24" height="24" />
</g>
<g id="zone-04" class="zone" data-zone="ZONE 04">
<rect x="386" y="96" width="140" height="102" class="zone-area" />
<text x="456" y="160" class="zone-label">ZONE 04</text>
<use href="#sensor" x="443" y="125" width="26" height="26" />
<use href="#manual-call" x="393" y="174" width="17" height="17" />
<rect x="386" y="96" width="140" height="102" class="zone-area za-utility" />
<text x="456" y="180" class="zone-label">ZONE 04</text>
<use href="#sensor" x="439" y="106" width="34" height="34" />
<use href="#manual-call" x="389" y="170" width="24" height="24" />
</g>
<g id="zone-05" class="zone" data-zone="ZONE 05">
<rect x="526" y="96" width="140" height="102" class="zone-area" />
<text x="596" y="160" class="zone-label">ZONE 05</text>
<use href="#sensor" x="583" y="125" width="26" height="26" />
<use href="#manual-call" x="534" y="174" width="17" height="17" />
<rect x="526" y="96" width="140" height="102" class="zone-area za-utility" />
<text x="596" y="180" class="zone-label">ZONE 05</text>
<use href="#sensor" x="579" y="106" width="34" height="34" />
<use href="#manual-call" x="530" y="170" width="24" height="24" />
</g>
<g id="zone-06" class="zone" data-zone="ZONE 06">
<rect x="666" y="96" width="130" height="102" class="zone-area" />
<text x="731" y="160" class="zone-label">ZONE 06</text>
<use href="#sensor" x="718" y="125" width="26" height="26" />
<use href="#manual-call" x="672" y="174" width="17" height="17" />
<rect x="666" y="96" width="130" height="102" class="zone-area za-utility" />
<text x="731" y="180" class="zone-label">ZONE 06</text>
<use href="#sensor" x="714" y="106" width="34" height="34" />
<use href="#manual-call" x="668" y="170" width="24" height="24" />
</g>
<use href="#door-leaf" x="310" y="197" width="28" height="20" />
@@ -929,13 +960,13 @@
<!-- Cafeteria -->
<g id="zone-07" class="zone" data-zone="ZONE 07">
<rect x="844" y="58" width="178" height="124" class="building" />
<rect x="856" y="70" width="154" height="100" class="zone-area" />
<rect x="844" y="58" width="178" height="124" class="building bg-service" />
<rect x="856" y="70" width="154" height="100" class="zone-area za-service" />
<path d="M868 126h130 M916 70v100 M958 70v100" class="detail" />
<path d="M868 86h130 M868 102h130 M868 142h130" class="detail-light" />
<text x="933" y="106" class="building-name" style="font-size:14px;stroke-width:2.2px">CAFETERIA</text>
<use href="#sensor" x="920" y="125" width="26" height="26" />
<use href="#manual-call" x="864" y="148" width="18" height="18" />
<text x="933" y="106" class="building-name" style="font-size:17px;stroke-width:2.8px">CAFETERIA</text>
<use href="#sensor" x="916" y="121" width="34" height="34" />
<use href="#manual-call" x="861" y="145" width="24" height="24" />
<text x="933" y="170" class="zone-label">ZONE 07</text>
<use href="#door-leaf" x="904" y="170" width="28" height="20" />
<use href="#door-leaf" x="964" y="170" width="28" height="20" />
@@ -943,13 +974,13 @@
<!-- Safety center -->
<g id="zone-08" class="zone" data-zone="ZONE 08">
<rect x="1040" y="58" width="98" height="124" class="building" />
<rect x="1048" y="70" width="82" height="100" class="zone-area" />
<rect x="1040" y="58" width="98" height="124" class="building bg-service" />
<rect x="1048" y="70" width="82" height="100" class="zone-area za-service" />
<path d="M1068 70v100 M1048 124h82" class="detail" />
<path d="M1078 70v100 M1088 70v100" class="detail-light" />
<text x="1089" y="106" class="building-name" style="font-size:11px;stroke-width:2px">SAFETY CENTER</text>
<use href="#sensor" x="1076" y="129" width="26" height="26" />
<use href="#manual-call" x="1053" y="147" width="18" height="18" />
<text x="1089" y="106" class="building-name" style="font-size:13px;stroke-width:2.4px">SAFETY CENTER</text>
<use href="#sensor" x="1072" y="125" width="34" height="34" />
<use href="#manual-call" x="1050" y="144" width="24" height="24" />
<text x="1089" y="170" class="zone-label">ZONE 08</text>
<use href="#door-leaf" x="1070" y="170" width="28" height="20" />
</g>
@@ -1007,58 +1038,58 @@
<g id="zone-09" class="zone" data-zone="ZONE 09">
<rect x="60" y="392" width="150" height="114" class="zone-area" />
<text x="135" y="456" class="zone-label">ZONE 09</text>
<use href="#sensor" x="122" y="421" width="26" height="26" />
<use href="#manual-call" x="72" y="480" width="18" height="18" />
<text x="135" y="461" class="zone-label">ZONE 09</text>
<use href="#sensor" x="118" y="417" width="34" height="34" />
<use href="#manual-call" x="69" y="477" width="24" height="24" />
</g>
<g id="zone-10" class="zone" data-zone="ZONE 10">
<rect x="210" y="392" width="150" height="114" class="zone-area" />
<text x="285" y="456" class="zone-label">ZONE 10</text>
<use href="#sensor" x="272" y="421" width="26" height="26" />
<use href="#manual-call" x="222" y="480" width="18" height="18" />
<text x="285" y="461" class="zone-label">ZONE 10</text>
<use href="#sensor" x="268" y="417" width="34" height="34" />
<use href="#manual-call" x="219" y="477" width="24" height="24" />
</g>
<g id="zone-11" class="zone" data-zone="ZONE 11">
<rect x="360" y="392" width="150" height="114" class="zone-area" />
<text x="435" y="456" class="zone-label">ZONE 11</text>
<use href="#sensor" x="422" y="421" width="26" height="26" />
<use href="#manual-call" x="372" y="480" width="17" height="17" />
<text x="435" y="461" class="zone-label">ZONE 11</text>
<use href="#sensor" x="418" y="417" width="34" height="34" />
<use href="#manual-call" x="368" y="476" width="24" height="24" />
</g>
<g id="zone-12" class="zone" data-zone="ZONE 12">
<rect x="510" y="392" width="170" height="114" class="zone-area" />
<text x="595" y="456" class="zone-label">ZONE 12</text>
<use href="#sensor" x="582" y="421" width="26" height="26" />
<use href="#manual-call" x="522" y="480" width="18" height="18" />
<text x="595" y="461" class="zone-label">ZONE 12</text>
<use href="#sensor" x="578" y="417" width="34" height="34" />
<use href="#manual-call" x="519" y="477" width="24" height="24" />
</g>
<g id="zone-13" class="zone" data-zone="ZONE 13">
<rect x="60" y="506" width="150" height="122" class="zone-area" />
<text x="135" y="574" class="zone-label">ZONE 13</text>
<use href="#sensor" x="122" y="539" width="26" height="26" />
<use href="#manual-call" x="184" y="600" width="18" height="18" />
<text x="135" y="579" class="zone-label">ZONE 13</text>
<use href="#sensor" x="118" y="535" width="34" height="34" />
<use href="#manual-call" x="181" y="597" width="24" height="24" />
</g>
<g id="zone-14" class="zone" data-zone="ZONE 14">
<rect x="210" y="506" width="150" height="122" class="zone-area" />
<text x="285" y="574" class="zone-label">ZONE 14</text>
<use href="#sensor" x="272" y="539" width="26" height="26" />
<use href="#manual-call" x="334" y="600" width="18" height="18" />
<text x="285" y="579" class="zone-label">ZONE 14</text>
<use href="#sensor" x="268" y="535" width="34" height="34" />
<use href="#manual-call" x="331" y="597" width="24" height="24" />
</g>
<g id="zone-15" class="zone" data-zone="ZONE 15">
<rect x="360" y="506" width="150" height="122" class="zone-area" />
<text x="435" y="574" class="zone-label">ZONE 15</text>
<use href="#sensor" x="422" y="539" width="26" height="26" />
<use href="#manual-call" x="484" y="600" width="18" height="18" />
<text x="435" y="579" class="zone-label">ZONE 15</text>
<use href="#sensor" x="418" y="535" width="34" height="34" />
<use href="#manual-call" x="481" y="597" width="24" height="24" />
</g>
<g id="zone-16" class="zone" data-zone="ZONE 16">
<rect x="510" y="506" width="170" height="122" class="zone-area" />
<text x="595" y="574" class="zone-label">ZONE 16</text>
<use href="#sensor" x="582" y="539" width="26" height="26" />
<use href="#manual-call" x="650" y="600" width="18" height="18" />
<text x="595" y="579" class="zone-label">ZONE 16</text>
<use href="#sensor" x="578" y="535" width="34" height="34" />
<use href="#manual-call" x="647" y="597" width="24" height="24" />
</g>
<use href="#door-leaf" x="108" y="618" width="28" height="20" />
@@ -1069,23 +1100,23 @@
<!-- Office -->
<g>
<rect x="696" y="306" width="96" height="322" class="building" />
<rect x="696" y="306" width="96" height="322" class="building bg-office" />
<rect x="704" y="318" width="80" height="298" class="building-inset" />
<text x="744" y="328" class="building-name" style="font-size:11px">OFFICE BLDG</text>
<text x="744" y="328" class="building-name" style="font-size:13px;stroke-width:2.4px">OFFICE BLDG</text>
<line x1="696" y1="466" x2="792" y2="466" class="wall" />
<g id="zone-17" class="zone" data-zone="ZONE 17">
<rect x="696" y="306" width="96" height="160" class="zone-area" />
<text x="744" y="448" class="zone-label">ZONE 17</text>
<use href="#sensor" x="731" y="411" width="26" height="26" />
<use href="#manual-call" x="704" y="442" width="18" height="18" />
<rect x="696" y="306" width="96" height="160" class="zone-area za-office" />
<text x="744" y="453" class="zone-label">ZONE 17</text>
<use href="#sensor" x="727" y="407" width="34" height="34" />
<use href="#manual-call" x="701" y="439" width="24" height="24" />
</g>
<g id="zone-18" class="zone" data-zone="ZONE 18">
<rect x="696" y="466" width="96" height="162" class="zone-area" />
<text x="744" y="552" class="zone-label">ZONE 18</text>
<use href="#sensor" x="731" y="517" width="26" height="26" />
<use href="#manual-call" x="704" y="600" width="18" height="18" />
<rect x="696" y="466" width="96" height="162" class="zone-area za-office" />
<text x="744" y="557" class="zone-label">ZONE 18</text>
<use href="#sensor" x="727" y="513" width="34" height="34" />
<use href="#manual-call" x="701" y="597" width="24" height="24" />
</g>
<use href="#door-leaf" x="730" y="618" width="28" height="20" />
@@ -1093,7 +1124,7 @@
<!-- Factory 1 -->
<g>
<rect x="906" y="286" width="304" height="350" class="building" />
<rect x="906" y="286" width="304" height="350" class="building bg-factory1" />
<rect x="918" y="298" width="280" height="326" class="building-inset" />
<text x="1058" y="340" class="building-name">FACTORY 1</text>
@@ -1118,31 +1149,31 @@
<use href="#stairs" x="914" y="590" width="42" height="34" opacity=".88" />
<g id="zone-19" class="zone" data-zone="ZONE 19">
<rect x="906" y="382" width="152" height="124" class="zone-area" />
<text x="982" y="452" class="zone-label">ZONE 19</text>
<use href="#sensor" x="969" y="417" width="26" height="26" />
<use href="#manual-call" x="916" y="480" width="18" height="18" />
<rect x="906" y="382" width="152" height="124" class="zone-area za-factory1" />
<text x="982" y="457" class="zone-label">ZONE 19</text>
<use href="#sensor" x="965" y="413" width="34" height="34" />
<use href="#manual-call" x="913" y="477" width="24" height="24" />
</g>
<g id="zone-20" class="zone" data-zone="ZONE 20">
<rect x="1058" y="382" width="152" height="124" class="zone-area" />
<text x="1134" y="452" class="zone-label">ZONE 20</text>
<use href="#sensor" x="1121" y="417" width="26" height="26" />
<use href="#manual-call" x="1182" y="480" width="18" height="18" />
<rect x="1058" y="382" width="152" height="124" class="zone-area za-factory1" />
<text x="1134" y="457" class="zone-label">ZONE 20</text>
<use href="#sensor" x="1117" y="413" width="34" height="34" />
<use href="#manual-call" x="1179" y="477" width="24" height="24" />
</g>
<g id="zone-21" class="zone" data-zone="ZONE 21">
<rect x="906" y="506" width="152" height="130" class="zone-area" />
<text x="982" y="578" class="zone-label">ZONE 21</text>
<use href="#sensor" x="969" y="543" width="26" height="26" />
<use href="#manual-call" x="1030" y="606" width="18" height="18" />
<rect x="906" y="506" width="152" height="130" class="zone-area za-factory1" />
<text x="982" y="583" class="zone-label">ZONE 21</text>
<use href="#sensor" x="965" y="539" width="34" height="34" />
<use href="#manual-call" x="1027" y="603" width="24" height="24" />
</g>
<g id="zone-22" class="zone" data-zone="ZONE 22">
<rect x="1058" y="506" width="152" height="130" class="zone-area" />
<text x="1134" y="578" class="zone-label">ZONE 22</text>
<use href="#sensor" x="1121" y="543" width="26" height="26" />
<use href="#manual-call" x="1182" y="606" width="18" height="18" />
<rect x="1058" y="506" width="152" height="130" class="zone-area za-factory1" />
<text x="1134" y="583" class="zone-label">ZONE 22</text>
<use href="#sensor" x="1117" y="539" width="34" height="34" />
<use href="#manual-call" x="1179" y="603" width="24" height="24" />
</g>
<use href="#door-leaf" x="948" y="626" width="28" height="20" />
+2 -1
View File
@@ -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>
+24 -24
View File
@@ -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>`;
};
+427
View File
@@ -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, '&lt;')}</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();
}
})();
+41 -41
View File
@@ -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>
+134 -10
View File
@@ -1,10 +1,11 @@
{
"name": "ERP-node",
"name": "invyone",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@anthropic-ai/claude-code": "^2.1.132",
"@prisma/client": "^6.16.2",
"@react-three/drei": "^10.7.6",
"@react-three/fiber": "^9.4.0",
@@ -21,6 +22,133 @@
"playwright": "^1.58.2"
}
},
"node_modules/@anthropic-ai/claude-code": {
"version": "2.1.132",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-2.1.132.tgz",
"integrity": "sha512-y0Exs+/jST92n0Hr+GLVCxQ54TYoLnphnwnXztgGAgkrOVMiGJ1uzvmqOds69eO7SQwKwBc7RiLzMZKQjvoiQA==",
"hasInstallScript": true,
"license": "SEE LICENSE IN README.md",
"bin": {
"claude": "bin/claude.exe"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"@anthropic-ai/claude-code-darwin-arm64": "2.1.132",
"@anthropic-ai/claude-code-darwin-x64": "2.1.132",
"@anthropic-ai/claude-code-linux-arm64": "2.1.132",
"@anthropic-ai/claude-code-linux-arm64-musl": "2.1.132",
"@anthropic-ai/claude-code-linux-x64": "2.1.132",
"@anthropic-ai/claude-code-linux-x64-musl": "2.1.132",
"@anthropic-ai/claude-code-win32-arm64": "2.1.132",
"@anthropic-ai/claude-code-win32-x64": "2.1.132"
}
},
"node_modules/@anthropic-ai/claude-code-darwin-arm64": {
"version": "2.1.132",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-darwin-arm64/-/claude-code-darwin-arm64-2.1.132.tgz",
"integrity": "sha512-imBlTl4dJ+IqGPMLTbLjSefBSd7c3iUjHGkz7/Q3RVDQJvcb2G83LFsaQGR3JwZ+fEwC7T6E6B8Mprwe9En08g==",
"cpu": [
"arm64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@anthropic-ai/claude-code-darwin-x64": {
"version": "2.1.132",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-darwin-x64/-/claude-code-darwin-x64-2.1.132.tgz",
"integrity": "sha512-CnsFT78zXkcR2wcEVivP6aqOMcIKSnegPZZierlSQ/zaXWCVNoW+3OHloMnDm+7BX6BT41K2bz7GckCr0pHiyg==",
"cpu": [
"x64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@anthropic-ai/claude-code-linux-arm64": {
"version": "2.1.132",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-linux-arm64/-/claude-code-linux-arm64-2.1.132.tgz",
"integrity": "sha512-VJgVycbS6u/lD05vGKmd+mMFypYt098jYt6yYGibqIvFPQcNkmMZJQfPIJBuVB/+XeySNlI4Pplo6jhwRM0HUA==",
"cpu": [
"arm64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@anthropic-ai/claude-code-linux-arm64-musl": {
"version": "2.1.132",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-linux-arm64-musl/-/claude-code-linux-arm64-musl-2.1.132.tgz",
"integrity": "sha512-RhGYFnGf6IE3PpOG8IWkrYoW1uS1i23cqvGKBHxlLhWsbvxxWUSOHd5IlzCS7udG7L4/EQU2UEAcdLaFEF8ZdQ==",
"cpu": [
"arm64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@anthropic-ai/claude-code-linux-x64": {
"version": "2.1.132",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-linux-x64/-/claude-code-linux-x64-2.1.132.tgz",
"integrity": "sha512-ElubH7haoKIXy9SNn8dTGMym0n+zy6A70wWXhhcdAFo7P/M8bdLT6zRyp5TttrkhAZcA8xPVxEB/zNU3sPCMLg==",
"cpu": [
"x64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@anthropic-ai/claude-code-linux-x64-musl": {
"version": "2.1.132",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-linux-x64-musl/-/claude-code-linux-x64-musl-2.1.132.tgz",
"integrity": "sha512-Xu6kajnzX5igVtjuqTdohGoat8bXMccs9TsS3n14Ith+3mgB0IX+zTxiR/0V9TCv7nkYe1tRrO2DHEOl9GzR4A==",
"cpu": [
"x64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@anthropic-ai/claude-code-win32-arm64": {
"version": "2.1.132",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-win32-arm64/-/claude-code-win32-arm64-2.1.132.tgz",
"integrity": "sha512-FuNMUO6tERKJZrayqDf1ScAcFhzhs2joQ9PE8POc2Y7Bv2E+aq/bmaj1Nxff+RKgpYSnoGfru9Sk3lSLtIUzGQ==",
"cpu": [
"arm64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@anthropic-ai/claude-code-win32-x64": {
"version": "2.1.132",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-win32-x64/-/claude-code-win32-x64-2.1.132.tgz",
"integrity": "sha512-prA22ML+CCHsMI0jj9BD5EVjzhZ38vPc+Udz/GvPs6h+HPH+EeNSjGDNzn9YvkiYpjUun2HYF24ztEXMJigGVw==",
"cpu": [
"x64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@azure-rest/core-client": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.5.1.tgz",
@@ -471,7 +599,6 @@
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz",
"integrity": "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.17.8",
"@types/react-reconciler": "^0.32.0",
@@ -712,7 +839,6 @@
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz",
"integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dimforge/rapier3d-compat": "~0.12.0",
"@tweenjs/tween.js": "~23.1.3",
@@ -1079,7 +1205,8 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/d3-color": {
"version": "3.1.0",
@@ -1138,7 +1265,6 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -2166,7 +2292,6 @@
"integrity": "sha512-aRvldGE5UUJTtVmFiH3WfNFNiqFlAtePUxcI0UEGlnXCX7DqhiMT5TRYwncHFeA/Reca5W6ToXXyCMTeFPdSXA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@prisma/config": "6.16.2",
"@prisma/engines": "6.16.2"
@@ -2382,7 +2507,8 @@
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/semver": {
"version": "7.7.2",
@@ -2501,8 +2627,7 @@
"version": "0.180.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz",
"integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/three-mesh-bvh": {
"version": "0.8.3",
@@ -2598,7 +2723,6 @@
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
+1
View File
@@ -1,5 +1,6 @@
{
"dependencies": {
"@anthropic-ai/claude-code": "^2.1.132",
"@prisma/client": "^6.16.2",
"@react-three/drei": "^10.7.6",
"@react-three/fiber": "^9.4.0",