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

@@ -69,12 +69,18 @@
}
for (const it of items) placeItem(it.id, it.y);
// Zone labels show only when the gap between the bracketing
// thresholds is at least MIN_ZONE_GAP px high — otherwise the label
// collides with one of the threshold labels (which sit at threshold
// y ±6 px text-height). 28 px keeps a 6 px clear gap above and
// below the zone label.
const MIN_ZONE_GAP = 28;
const placeZone = (zoneId, topId, botId) => {
const el = document.getElementById(`ps-zone-${zoneId}`);
if (!el) return;
const top = items.find(it => it.id === topId);
const bot = items.find(it => it.id === botId);
if (!top || !bot || (bot.y - top.y) < 14) {
if (!top || !bot || (bot.y - top.y) < MIN_ZONE_GAP) {
el.setAttribute('visibility', 'hidden'); return;
}
el.setAttribute('y', (top.y + bot.y) / 2 + 3);
@@ -127,21 +133,59 @@
const d2 = document.getElementById('derived-highVolumeSafetyLevel');
if (d2) d2.textContent = fmt(highLvl);
const warn = document.getElementById('ps-warning');
// Hierarchy validation. Soft '≤' relations follow the user's choice:
// start ≤ inflow, max ≤ overflow, overflow ≤ basinHeight (equality OK).
// dryRunLevel must be < startLevel strictly (otherwise the runtime
// would trip dry-run before it could ramp).
// Re-read the raw value (basinH falls back to 5 for diagram scaling;
// here we want null when the user hasn't entered anything so the
// ≤-checks below are skipped rather than false-flagged).
const basinHraw = fNum('basinHeight');
const start = fNum('startLevel');
const inlet = fNum('inflowLevel');
const max = fNum('maxLevel');
const ovfl = fNum('overflowLevel');
const issues = [];
const pairs = [
['outflowLevel', 'inflowLevel', '<'],
['inflowLevel', '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}`);
const ok = (a, b, op) => {
if (!Number.isFinite(a) || !Number.isFinite(b)) return true;
return op === '<' ? a < b : a <= b;
};
if (Number.isFinite(refLow) && refLow <= 0)
issues.push('outflowLevel must be > 0');
if (!ok(dryLvl, start, '<'))
issues.push(`dryRunLevel (${(dryLvl ?? NaN).toFixed(2)} m, derived) must be < startLevel — lower dryRun% or raise startLevel`);
if (!ok(start, inlet, '<='))
issues.push('startLevel must be ≤ inflowLevel');
if (!ok(inlet, max, '<='))
issues.push('inflowLevel must be ≤ maxLevel');
if (!ok(max, ovfl, '<='))
issues.push('maxLevel must be ≤ overflowLevel');
if (!ok(ovfl, basinHraw, '<='))
issues.push('overflowLevel must be ≤ basinHeight');
// Visible ribbon above the basin diagram.
const warnDiv = document.getElementById('ps-basin-validation');
if (warnDiv) {
if (issues.length) {
warnDiv.innerHTML = '⚠ Fix before deploy:<ul style="margin:4px 0 0 18px;padding:0;">'
+ issues.map((i) => `<li>${i}</li>`).join('') + '</ul>';
warnDiv.style.display = '';
} else {
warnDiv.style.display = 'none';
}
}
// Legacy in-SVG warning text — kept for the small reminder inside
// the diagram. Only shows the count.
const warn = document.getElementById('ps-warning');
if (warn) {
if (issues.length) { warn.setAttribute('visibility', 'visible'); warn.textContent = `⚠ Check ordering: ${issues.join(', ')}`; }
else { warn.setAttribute('visibility', 'hidden'); }
if (issues.length) {
warn.setAttribute('visibility', 'visible');
warn.textContent = `${issues.length} ordering issue${issues.length > 1 ? 's' : ''}`;
} else {
warn.setAttribute('visibility', 'hidden');
}
}
window._psBasinValidationIssues = issues;
},
};
})();