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

@@ -146,7 +146,9 @@
}
}
// Vertical level markers + axis labels.
// Vertical level markers — line only. Axis labels were removed;
// identification comes from line colour + side-panel labels +
// hover coupling.
[
['dryRunLevel', dryRun],
['startLevel', start],
@@ -155,18 +157,14 @@
['overflowLevel', overflow],
].forEach(([id, level]) => {
const line = document.getElementById(`ps-mode-line-${id}`);
const label = document.getElementById(`ps-mode-label-${id}`);
if (!line || !label) return;
if (!line) return;
if (!Number.isFinite(level)) {
line.style.display = 'none';
label.style.display = 'none';
return;
}
const x = xFor(level);
line.style.display = '';
label.style.display = '';
line.setAttribute('x1', x); line.setAttribute('x2', x);
label.setAttribute('x', x);
});
// Background zone bands.
@@ -192,19 +190,15 @@
setBand('ps-zone-safetyHigh', xMax, xOvf);
setBand('ps-zone-overflow', xOvf, plotR);
// Shift level marker.
// Shift level marker (line only).
const shiftLine = document.getElementById('ps-mode-line-shiftLevel');
const shiftLabel = document.getElementById('ps-mode-label-shiftLevel');
if (shiftLine && shiftLabel) {
if (shiftLine) {
if (shiftEnabled && Number.isFinite(shift)) {
const x = xFor(shift);
shiftLine.setAttribute('x1', x); shiftLine.setAttribute('x2', x);
shiftLabel.setAttribute('x', x);
shiftLine.style.display = '';
shiftLabel.style.display = '';
} else {
shiftLine.style.display = 'none';
shiftLabel.style.display = 'none';
}
}
@@ -237,16 +231,11 @@
}
}
// Validation: ordering constraints.
// Validation: only mode-specific (shift) ordering. Basin-level
// hierarchy (start ≤ inlet ≤ max ≤ overflow ≤ basinHeight,
// dryRun < start) is owned by basin-diagram.js so it shows in the
// basin section near the offending inputs.
const issues = [];
if (Number.isFinite(dryRun) && Number.isFinite(start) && dryRun >= start)
issues.push('dryRunLevel (derived) must be < startLevel — increase startLevel or lower dryRun%');
if (Number.isFinite(start) && Number.isFinite(inlet) && start >= inlet)
issues.push('startLevel must be < inflowLevel (set in basin above)');
if (Number.isFinite(inlet) && Number.isFinite(max) && inlet >= max)
issues.push('inflowLevel must be < maxLevel');
if (Number.isFinite(max) && Number.isFinite(overflow) && max > overflow)
issues.push('maxLevel must be ≤ overflowLevel');
if (shiftEnabled) {
const shiftVal = Number(shiftInput?.value);
if (Number.isFinite(shiftVal)) {