Editor: dynamic input bounds + full hierarchy validation, layout polish

Bounds (new src/editor/bounds.js):
- Sets HTML5 min/max on every level + percent input each redraw,
  derived from the current values of related inputs so the spinner
  stops at the basin hierarchy:
  0 < outflowLevel < dryRunLevel < startLevel ≤ inflowLevel
      ≤ shiftLevel ≤ maxLevel ≤ overflowLevel ≤ basinHeight
- dryRunPercent capped so dryRunLevel ≤ startLevel given current outflow.
- shiftArmPercent ∈ [1, 100]; highVolumeSafety% ∈ [1, 100].

Validation:
- New visible ribbon above the basin diagram (#ps-basin-validation)
  listing every hierarchy violation. The in-SVG warning text is now a
  small reminder ("⚠ N ordering issues").
- basin-diagram.js owns hierarchy issues; mode-preview.js trimmed to
  only own shift-specific issues (shift > start, shift ≤ max,
  shiftArmPercent range, shiftLevel required-when-enabled).
- oneditsave blocks Deploy on the union of _psBasinValidationIssues
  and _psModeValidationIssues with a RED.notify listing all problems.

Layout polish:
- Side panel widened to 220 px with minmax(0, 1fr) first column so long
  labels can no longer push the rows past the panel edge.
- Basin SVG max-width 380 → 360, gap between side panel and SVG bumped
  14 → 28 px. Tank shifted right (x=145 width=110) so the inlet
  "bottom of pipe" sub-label is no longer clipped on the left edge.
- "0 m (datum)" moved below the tank (y=395, centred) so it can't
  collide with "Outlet / top of pipe" when outflowLevel is near floor.
- Zone labels shortened (Spare / Sewage + buffer / Buffer / Dead vol)
  and only show when the bracketing thresholds are ≥ 28 px apart, so
  they never sit on a threshold label.
- Mode preview axis labels under the chart removed — line colour +
  side-panel labels + hover-couple already identify each line. Stub
  <text> elements left hidden to keep the redraw loop simple. Arm-%
  line + label trimmed in 10 px on the right so they're not clipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rene De Ren
2026-05-06 14:10:22 +02:00
parent de9a79b888
commit 62bc73f2f9
6 changed files with 225 additions and 78 deletions

View File

@@ -14,6 +14,7 @@
<!-- Editor JS modules — see nodes/pumpingStation/src/editor/. Loaded in
dependency order: index.js (namespace + helpers) → diagrams → handlers. -->
<script src="/pumpingStation/editor/index.js"></script>
<script src="/pumpingStation/editor/bounds.js"></script>
<script src="/pumpingStation/editor/basin-diagram.js"></script>
<script src="/pumpingStation/editor/mode-preview.js"></script>
<script src="/pumpingStation/editor/hover-couple.js"></script>
@@ -127,17 +128,19 @@
<h4>Basin parameters</h4>
<p style="font-size:12px;color:#777;margin:0 0 8px 0;">Heights are measured from the basin floor (0 m). Each input on the left controls a line in the diagram on the right hover an input to highlight its line.</p>
<div id="ps-basin-validation" style="display:none;color:#C0392B;font-size:11px;margin:0 0 8px 0;border:1px solid #C0392B;border-radius:3px;padding:6px 8px;background:#FDECEA;"></div>
<style>
/* Two-column layout: stacked colour-coded inputs on the left,
SVG on the right. Hover an input row → its paired SVG line
(referenced by data-couples-line) gets a thicker stroke. */
.ps-diag { display:flex; gap:14px; align-items:flex-start; margin:0 0 14px 0; }
.ps-diag-side { width: 200px; flex: 0 0 200px; display:flex; flex-direction:column; gap:6px; }
.ps-diag { display:flex; gap:28px; align-items:flex-start; margin:0 0 14px 0; }
.ps-diag-side { width: 220px; flex: 0 0 220px; display:flex; flex-direction:column; gap:6px; }
.ps-diag-side .ps-row {
display:grid; grid-template-columns: 1fr 78px 18px; align-items:center;
display:grid; grid-template-columns: minmax(0,1fr) 70px 16px; align-items:center;
gap:6px; padding:4px 6px 4px 10px; border-left:4px solid #ccc;
background:#fafafa; border-radius:3px; font-size:11px; cursor:pointer;
min-width:0;
}
.ps-diag-side .ps-row:hover { background:#f0f0f0; }
.ps-diag-side .ps-row.ps-readonly { background:#fff; cursor:default; opacity:0.85; }
@@ -267,55 +270,58 @@
<!-- RIGHT: SVG. The viewBox is now narrower (320 wide) since the right
input column is gone labels render inside the tank's right margin. -->
<svg id="ps-basin-diagram" class="ps-diag-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 430"
style="display:block;width:100%;max-width:380px;background:#fff;border:1px solid #e5e5e5;border-radius:4px;"
style="display:block;width:100%;max-width:360px;background:#fff;border:1px solid #e5e5e5;border-radius:4px;"
font-family="Arial,sans-serif" font-size="11">
<defs>
<marker id="ps-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#1F4E79" />
</marker>
</defs>
<!-- Tank body — x=120,width=120 (was 200,120 in the old wider viewBox).
Threshold tick lines extend a few px outside the tank walls. -->
<rect x="120" y="40" width="120" height="340" fill="#F0F8FF" stroke="#333" stroke-width="1.5" />
<rect id="ps-deadvol" x="121" width="118" fill="#AACCE0" />
<!-- Tank body — shifted right (x=145, width=110) to give the inlet
sub-label "bottom of pipe" room on the left without clipping.
Threshold tick lines extend 5 px outside the tank walls. -->
<rect x="145" y="40" width="110" height="340" fill="#F0F8FF" stroke="#333" stroke-width="1.5" />
<rect id="ps-deadvol" x="146" width="108" fill="#AACCE0" />
<!-- Mid-tank zone labels — centred at x=180. -->
<text id="ps-zone-spare" x="180" text-anchor="middle" fill="#B78200" font-size="10" font-style="italic" visibility="hidden">Spare volume before spilling</text>
<text id="ps-zone-sewage" x="180" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Sewage + tank buffer</text>
<text id="ps-zone-buffer1" x="180" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Tank buffer</text>
<text id="ps-zone-buffer2" x="180" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Tank buffer</text>
<text id="ps-zone-dead" x="180" text-anchor="middle" fill="#444" font-size="10" font-style="italic" visibility="hidden">Dead volume</text>
<!-- Mid-tank zone labels — centred at x=200 (tank centre). -->
<text id="ps-zone-spare" x="200" text-anchor="middle" fill="#B78200" font-size="10" font-style="italic" visibility="hidden">Spare</text>
<text id="ps-zone-sewage" x="200" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Sewage + buffer</text>
<text id="ps-zone-buffer1" x="200" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Buffer</text>
<text id="ps-zone-buffer2" x="200" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Buffer</text>
<text id="ps-zone-dead" x="200" text-anchor="middle" fill="#444" font-size="10" font-style="italic" visibility="hidden">Dead vol</text>
<!-- basinHeight tick at tank rim (y=40, static). -->
<line id="ps-line-basinHeight" x1="115" y1="40" x2="245" y2="40" stroke="#333" stroke-width="1.5" />
<text id="ps-label-basinHeight" x="250" y="44" fill="#333">basinHeight</text>
<line id="ps-line-basinHeight" x1="140" y1="40" x2="260" y2="40" stroke="#333" stroke-width="1.5" />
<text id="ps-label-basinHeight" x="265" y="44" fill="#333">basinHeight</text>
<line id="ps-line-overflowLevel" x1="115" x2="245" stroke="#C0392B" stroke-dasharray="4 2" stroke-width="1.5" />
<text id="ps-label-overflowLevel" x="250" fill="#C0392B">overflowLevel</text>
<line id="ps-line-overflowLevel" x1="140" x2="260" stroke="#C0392B" stroke-dasharray="4 2" stroke-width="1.5" />
<text id="ps-label-overflowLevel" x="265" fill="#C0392B">overflowLevel</text>
<line id="ps-line-highVolumeSafetyLevel" x1="115" x2="245" stroke="#D68910" stroke-dasharray="1 2" stroke-width="1" opacity="0.7" />
<text id="ps-label-highVolumeSafetyLevel" x="250" fill="#D68910" font-size="10" font-style="italic">highVolumeSafety</text>
<line id="ps-line-highVolumeSafetyLevel" x1="140" x2="260" stroke="#D68910" stroke-dasharray="1 2" stroke-width="1" opacity="0.7" />
<text id="ps-label-highVolumeSafetyLevel" x="265" fill="#D68910" font-size="10" font-style="italic">highVolSafety</text>
<line id="ps-line-inflowLevelGuide" x1="120" x2="240" stroke="#1F4E79" stroke-dasharray="2 3" stroke-width="1" opacity="0.55" />
<text id="ps-label-inflowLevelGuide" x="250" fill="#1F4E79" font-size="10" font-style="italic">inlet invert</text>
<line id="ps-line-inflowLevelGuide" x1="145" x2="255" stroke="#1F4E79" stroke-dasharray="2 3" stroke-width="1" opacity="0.55" />
<text id="ps-label-inflowLevelGuide" x="265" fill="#1F4E79" font-size="10" font-style="italic">inlet invert</text>
<line id="ps-line-inflowLevel" x1="60" x2="120" stroke="#1F4E79" stroke-width="2" marker-end="url(#ps-arrow)" />
<text id="ps-label-inflowLevel" x="55" text-anchor="end" fill="#1F4E79" font-weight="bold">Inlet</text>
<text id="ps-sub-inflowLevel" x="55" text-anchor="end" fill="#777" font-size="9">bottom of pipe</text>
<line id="ps-line-inflowLevel" x1="85" x2="145" stroke="#1F4E79" stroke-width="2" marker-end="url(#ps-arrow)" />
<text id="ps-label-inflowLevel" x="80" text-anchor="end" fill="#1F4E79" font-weight="bold">Inlet</text>
<text id="ps-sub-inflowLevel" x="80" text-anchor="end" fill="#777" font-size="9">bottom of pipe</text>
<line id="ps-line-dryRunLevel" x1="115" x2="245" stroke="#C0392B" stroke-dasharray="1 2" stroke-width="1" opacity="0.6" />
<text id="ps-label-dryRunLevel" x="250" fill="#C0392B" font-size="10" font-style="italic">dryRunLevel</text>
<line id="ps-line-dryRunLevel" x1="140" x2="260" stroke="#C0392B" stroke-dasharray="1 2" stroke-width="1" opacity="0.6" />
<text id="ps-label-dryRunLevel" x="265" fill="#C0392B" font-size="10" font-style="italic">dryRunLevel</text>
<line id="ps-line-outflowLevel" x1="240" x2="280" stroke="#1F4E79" stroke-width="2" marker-end="url(#ps-arrow)" />
<text id="ps-label-outflowLevel" x="285" fill="#1F4E79" font-weight="bold">Outlet</text>
<text id="ps-sub-outflowLevel" x="285" fill="#777" font-size="9">top of pipe</text>
<line id="ps-line-outflowLevel" x1="255" x2="295" stroke="#1F4E79" stroke-width="2" marker-end="url(#ps-arrow)" />
<text id="ps-label-outflowLevel" x="300" fill="#1F4E79" font-weight="bold">Outlet</text>
<text id="ps-sub-outflowLevel" x="300" fill="#777" font-size="9">top of pipe</text>
<!-- Floor / datum -->
<line x1="115" y1="380" x2="245" y2="380" stroke="#000" stroke-width="2" />
<text x="250" y="384" fill="#000">0 m (datum)</text>
<!-- Floor / datum — datum label sits BELOW the tank (y=395) so it
never collides with the Outlet / top-of-pipe sub-label when
outflowLevel is near the floor. -->
<line x1="140" y1="380" x2="260" y2="380" stroke="#000" stroke-width="2" />
<text x="200" y="395" text-anchor="middle" fill="#000" font-size="10">0 m (datum)</text>
<!-- Ordering-warning ribbon -->
<text id="ps-warning" x="180" y="410" text-anchor="middle" fill="#C0392B" font-size="10" font-style="italic" visibility="hidden"></text>
<text id="ps-warning" x="200" y="410" text-anchor="middle" fill="#C0392B" font-size="10" font-style="italic" visibility="hidden"></text>
</svg>
</div>
@@ -469,15 +475,19 @@
<line id="ps-mode-line-shiftLevel" y1="24" y2="140" stroke="#D68910" stroke-dasharray="2 2" style="display:none;" />
<!-- Horizontal arming-% line — y is set DYNAMICALLY by the JS to the
shiftArmPercent value (in plot-y space). Spans full plot width. -->
<line id="ps-mode-line-armPercent" x1="52" x2="402" stroke="#D68910" stroke-dasharray="4 3" stroke-width="1" opacity="0.7" style="display:none;" />
<text id="ps-mode-label-armPercent" x="404" text-anchor="start" fill="#D68910" font-size="9" style="display:none;">arm%</text>
<!-- Axis labels y=180 row sits below the OFF baseline (y=160). x set dynamically. -->
<text id="ps-mode-label-dryRunLevel" y="180" text-anchor="middle" fill="#C0392B">dry run</text>
<text id="ps-mode-label-startLevel" y="180" text-anchor="middle" fill="#1E8449">start</text>
<text id="ps-mode-label-inflowLevel" y="180" text-anchor="middle" fill="#1F4E79">inlet</text>
<text id="ps-mode-label-maxLevel" y="180" text-anchor="middle" fill="#D68910">max</text>
<text id="ps-mode-label-overflowLevel" y="180" text-anchor="middle" fill="#C0392B">overflow</text>
<text id="ps-mode-label-shiftLevel" y="180" text-anchor="middle" fill="#D68910" style="display:none;">shift</text>
<line id="ps-mode-line-armPercent" x1="52" x2="392" stroke="#D68910" stroke-dasharray="4 3" stroke-width="1" opacity="0.7" style="display:none;" />
<text id="ps-mode-label-armPercent" x="394" text-anchor="start" fill="#D68910" font-size="9" style="display:none;">arm%</text>
<!-- Axis labels under the plot were removed — they crowded each other
when levels were close. Identification comes from the line colour
(matched to the side-panel input row) and hover-coupling. -->
<!-- Empty <text> stubs kept for the redraw loop's getElementById calls
(cheaper than guarding each one). They're hidden via display:none. -->
<text id="ps-mode-label-dryRunLevel" style="display:none;"></text>
<text id="ps-mode-label-startLevel" style="display:none;"></text>
<text id="ps-mode-label-inflowLevel" style="display:none;"></text>
<text id="ps-mode-label-maxLevel" style="display:none;"></text>
<text id="ps-mode-label-overflowLevel" style="display:none;"></text>
<text id="ps-mode-label-shiftLevel" style="display:none;"></text>
<!-- Legend captions placed BELOW the axis labels (y=200) on their own row,
so they never collide with the title (y=14). Up-caption left-aligned at
x=60; down-caption to its right at x=210. Both font-size 10. -->