From d641d2248dedd0fcf5b6d42d49fbcb81502604d1 Mon Sep 17 00:00:00 2001 From: znetsixe Date: Thu, 23 Apr 2026 10:28:18 +0200 Subject: [PATCH] =?UTF-8?q?Editor:=20interactive=20basin=20diagram=20?= =?UTF-8?q?=E2=80=94=20inputs=20placed=20at=20each=20threshold=20line?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the static parameters-diagram-above-form-rows layout with a single interactive SVG where every threshold input sits directly on the tank at its proportional y-position. Typing a value repositions the corresponding line + input + label live. What moved into the diagram (via holding real elements with their existing node-input-* IDs so Node-RED save/restore is untouched): basinHeight — top of tank (fixed at rim by definition) overflowLevel — weir crest (red, dashed) maxLevel — 100 % demand line (orange, dashed) startLevel — ramp-start line (green, dashed) minLevel — MGC-shutdown line (purple, dashed) inflowLevel — Inlet arrow + input on left outflowLevel — Outlet arrow + input on right dryRunLevel — read-only, computed from outflow × (1+dryRunPct/100) Also in the diagram: - Dead-volume band fills the area below outflowLevel dynamically - Warning ribbon appears below the tank if ordering invariants break (mirrors specificClass._validateThresholdOrdering) - All positions scale against the user's basinHeight; if empty, a default 5 m scale is used just to keep the diagram readable What stayed as regular form rows: - Basin Volume (m³) — not a height, can't be placed on a y-axis - minLevel / startLevel / maxLevel were in the Control Strategy > Level-based section; removed from there and moved into the diagram (the level-based subsection now contains a one-line pointer) - Safety % inputs (dryRun, overfill) stay in the Safety section with their derived-level readouts, now synced with the diagram No schema changes, no field additions, no behaviour changes in the runtime. Pure editor-UX. Co-Authored-By: Claude Opus 4.7 (1M context) --- pumpingStation.html | 277 ++++++++++++++++++++++++++++---------------- 1 file changed, 179 insertions(+), 98 deletions(-) diff --git a/pumpingStation.html b/pumpingStation.html index a74c25e..417e720 100644 --- a/pumpingStation.html +++ b/pumpingStation.html @@ -173,31 +173,93 @@ setNumberField('node-input-flowSetpoint', this.flowSetpoint); setNumberField('node-input-flowDeadband', this.flowDeadband); - // Live-compute derived safety levels so the operator can see - // what the % will actually trip at. Mirrors the code formula - // in specificClass._validateThresholdOrdering. - const fNum = (id) => parseFloat(document.getElementById(`node-input-${id}`)?.value); - const updateDerivedLevels = () => { + // Interactive diagram: place every threshold line/input at its + // proportional y on the tank, plus compute derived safety levels + // (dryRunLevel, overfillLevel) that are shown both in the diagram + // and next to the safety-% fields. Same formulas as + // specificClass._validateThresholdOrdering. + const DIAG = { topY: 40, botY: 380 }; + const fNum = (id) => { + const v = parseFloat(document.getElementById(`node-input-${id}`)?.value); + return Number.isFinite(v) ? v : null; + }; + const yForLevel = (val, basinH) => { + if (val == null || !basinH) return null; + const y = DIAG.botY - (val / basinH) * (DIAG.botY - DIAG.topY); + return Math.max(DIAG.topY - 8, Math.min(DIAG.botY + 8, y)); + }; + const placeRow = (id, y) => { + if (y == null) return; + const line = document.getElementById(`ps-line-${id}`); + const label = document.getElementById(`ps-label-${id}`); + const unit = document.getElementById(`ps-unit-${id}`); + const fo = document.getElementById(`ps-fo-${id}`); + const sub = document.getElementById(`ps-sub-${id}`); + if (line) { line.setAttribute('y1', y); line.setAttribute('y2', y); } + if (label) label.setAttribute('y', y + 4); + if (unit) unit.setAttribute('y', y + 4); + if (fo) fo.setAttribute('y', y - 11); + if (sub) sub.setAttribute('y', y + 15); + }; + const redraw = () => { + const basinH = fNum('basinHeight') || 5; + placeRow('overflowLevel', yForLevel(fNum('overflowLevel'), basinH)); + placeRow('maxLevel', yForLevel(fNum('maxLevel'), basinH)); + placeRow('startLevel', yForLevel(fNum('startLevel'), basinH)); + placeRow('minLevel', yForLevel(fNum('minLevel'), basinH)); + placeRow('inflowLevel', yForLevel(fNum('inflowLevel'), basinH)); + const outflowY = yForLevel(fNum('outflowLevel'), basinH); + placeRow('outflowLevel', outflowY); + // Dead-volume band fills from outflowLevel down to the floor + const deadvol = document.getElementById('ps-deadvol'); + if (deadvol && outflowY != null) { + deadvol.setAttribute('y', outflowY); + deadvol.setAttribute('height', Math.max(0, DIAG.botY - outflowY)); + } + // Derived dryRunLevel (safety, from %) const basedOn = document.getElementById('node-input-minHeightBasedOn')?.value || 'outlet'; const refLow = basedOn === 'inlet' ? fNum('inflowLevel') : fNum('outflowLevel'); - const dryRunPct = fNum('dryRunThresholdPercent'); - const overfillPct = fNum('overfillThresholdPercent'); - const overflow = fNum('overflowLevel'); - const dryRunLvl = Number.isFinite(refLow) && Number.isFinite(dryRunPct) - ? refLow * (1 + dryRunPct / 100) : null; - const overfillLvl = Number.isFinite(overflow) && Number.isFinite(overfillPct) - ? overflow * (overfillPct / 100) : null; - const dryEl = document.getElementById('derived-dryRunLevel'); - const ovfEl = document.getElementById('derived-overfillLevel'); - if (dryEl) dryEl.textContent = dryRunLvl != null ? `→ dryRunLevel ≈ ${dryRunLvl.toFixed(2)} m` : '→ dryRunLevel ≈ — m'; - if (ovfEl) ovfEl.textContent = overfillLvl != null ? `→ overfillLevel ≈ ${overfillLvl.toFixed(2)} m` : '→ overfillLevel ≈ — m'; + const dryPct = fNum('dryRunThresholdPercent'); + const ovfPct = fNum('overfillThresholdPercent'); + const ovf = fNum('overflowLevel'); + const dryLvl = (refLow != null && dryPct != null) ? refLow * (1 + dryPct / 100) : null; + const ovfLvl = (ovf != null && ovfPct != null) ? ovf * (ovfPct / 100) : null; + placeRow('dryRunLevel', yForLevel(dryLvl, basinH)); + const dryLbl = document.getElementById('ps-label-dryRunLevel'); + if (dryLbl) dryLbl.textContent = dryLvl != null + ? `dryRunLevel ≈ ${dryLvl.toFixed(2)} m (safety — from %)` + : 'dryRunLevel ≈ — m (safety — from %)'; + // Safety-section readouts (same values, second view) + const d1 = document.getElementById('derived-dryRunLevel'); + if (d1) d1.textContent = dryLvl != null ? `→ dryRunLevel ≈ ${dryLvl.toFixed(2)} m` : '→ dryRunLevel ≈ — m'; + const d2 = document.getElementById('derived-overfillLevel'); + if (d2) d2.textContent = ovfLvl != null ? `→ overfillLevel ≈ ${ovfLvl.toFixed(2)} m` : '→ overfillLevel ≈ — m'; + // Ordering warning ribbon + const warn = document.getElementById('ps-warning'); + const issues = []; + const pairs = [ + ['outflowLevel', 'inflowLevel', '<'], + ['inflowLevel', 'overflowLevel', '<'], + ['minLevel', 'startLevel', '<='], + ['startLevel', 'maxLevel', '<'], + ['maxLevel', 'overflowLevel', '<='], + ]; + for (const [a, b, op] of pairs) { + const av = fNum(a), bv = fNum(b); + if (av == null || bv == null) continue; + if (op === '<' ? !(av < bv) : !(av <= bv)) issues.push(`${a} ${op} ${b}`); + } + if (warn) { + if (issues.length) { warn.setAttribute('visibility', 'visible'); warn.textContent = `⚠ Check ordering: ${issues.join(', ')}`; } + else { warn.setAttribute('visibility', 'hidden'); } + } }; - ['inflowLevel','outflowLevel','overflowLevel','minHeightBasedOn','dryRunThresholdPercent','overfillThresholdPercent'] - .forEach((id) => { - const el = document.getElementById(`node-input-${id}`); - if (el) { el.addEventListener('input', updateDerivedLevels); el.addEventListener('change', updateDerivedLevels); } - }); - setTimeout(updateDerivedLevels, 50); + ['basinHeight','overflowLevel','maxLevel','startLevel','minLevel','inflowLevel','outflowLevel', + 'dryRunThresholdPercent','overfillThresholdPercent','minHeightBasedOn'].forEach((id) => { + const el = document.getElementById(`node-input-${id}`); + if (el) { el.addEventListener('input', redraw); el.addEventListener('change', redraw); } + }); + setTimeout(redraw, 60); //------------------- END OF CUSTOM config UI ELEMENTS ------------------- // }, @@ -250,76 +312,106 @@
-

Basin Geometry

-

All heights measured from the basin floor (0 m).

+

Basin parameters

+

Heights are measured from the basin floor (0 m). Enter values next to each line — the diagram scales to whatever you enter.

-
- 📐 Parameters diagram - - - - - - - - - - - - - basinHeight - - - overflowLevel - - - maxLevel - - - startLevel - - - Inlet - bottom of pipe - - - minLevel - - - dryRunLevel - safety — from % - - - Outlet - top of pipe - - - 0 m (datum) - -
+ + + + + + + + + + + + + + + + + basinHeight + + + + m + + + + overflowLevel + + + + m + + + + maxLevel + + + + m + + + + startLevel + + + + m + + + + Inlet + bottom of pipe + + + + m + + + + minLevel + + + + m + + + + dryRunLevel ≈ — m (safety — from %) + + + + Outlet + top of pipe + + + + m + + + + 0 m (datum) + + + +
-
- - -
- - -
- - -
-
- - -
-
- - -

@@ -334,18 +426,7 @@
-
- - -
-
- - -
-
- - -
+

Level-based uses minLevel / startLevel / maxLevel from the diagram above.