Compare commits

...

1 Commits

Author SHA1 Message Date
znetsixe
65fe68b87f Editor: nudge crowded threshold inputs off their lines with leader lines
When real wastewater values cluster near the basin floor (minLevel,
dryRunLevel, outflowLevel are often within a few cm of each other),
the threshold inputs were stacking on top of each other. Now:

- Threshold LINE stays at its proportional y on the tank (visual
  truth: that's where the level actually is).
- Input BOX / label / unit are positioned in a nudged right-column
  stack with a minimum 26-px gap so they never overlap.
- A dashed grey leader line connects each line to its input when
  they had to be pulled apart, so the association stays visible.
- Items are sorted by ideal y top-down and nudged downward once;
  basinHeight is pinned at the rim and acts as the anchor.

Also: viewBox extended 430 → 480 so the bottom-of-stack items have
room below the tank when the bottom cluster is tight. Warning ribbon
moved to y=460 accordingly.

No schema change; purely UI layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:41:16 +02:00

View File

@@ -188,52 +188,113 @@
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}`);
// Place a right-column item. yLine is the threshold's true
// proportional position on the tank; yInput is where the label
// and input box land (may be nudged away from yLine to avoid
// overlap with neighbouring items). A dashed leader line is
// shown only when the two differ by more than a pixel or two.
const placeItem = (id, yLine, yInput) => {
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 unit = document.getElementById(`ps-unit-${id}`);
const fo = document.getElementById(`ps-fo-${id}`);
const sub = document.getElementById(`ps-sub-${id}`);
const lead = document.getElementById(`ps-leader-${id}`);
if (line) { line.setAttribute('y1', yLine); line.setAttribute('y2', yLine); }
if (label) label.setAttribute('y', yInput + 4);
if (unit) unit.setAttribute('y', yInput + 4);
if (fo) fo.setAttribute('y', yInput - 11);
if (sub) sub.setAttribute('y', yInput + 15);
if (lead) {
if (Math.abs(yLine - yInput) > 2) {
lead.setAttribute('x1', 325); lead.setAttribute('y1', yLine);
lead.setAttribute('x2', 420); lead.setAttribute('y2', yInput);
lead.setAttribute('visibility', 'visible');
} else {
lead.setAttribute('visibility', 'hidden');
}
}
};
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));
// Derived safety levels (participate in the right-column stack)
const basedOn = document.getElementById('node-input-minHeightBasedOn')?.value || 'outlet';
const refLow = basedOn === 'inlet' ? fNum('inflowLevel') : fNum('outflowLevel');
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;
// Build the right-column items. basinHeight is pinned at the
// rim (DIAG.topY); others float on the proportional axis and
// get nudged apart so the input boxes don't overlap.
const items = [
{ id: 'basinHeight', yLine: DIAG.topY, pinned: true },
{ id: 'overflowLevel', yLine: yForLevel(fNum('overflowLevel'), basinH) },
{ id: 'maxLevel', yLine: yForLevel(fNum('maxLevel'), basinH) },
{ id: 'startLevel', yLine: yForLevel(fNum('startLevel'), basinH) },
{ id: 'minLevel', yLine: yForLevel(fNum('minLevel'), basinH) },
{ id: 'dryRunLevel', yLine: yForLevel(dryLvl, basinH) },
{ id: 'outflowLevel', yLine: yForLevel(fNum('outflowLevel'), basinH) },
].filter(it => it.yLine != null);
const GAP = 26;
items.sort((a, b) => a.yLine - b.yLine);
let prev = -Infinity;
for (const it of items) {
if (it.pinned) { it.yInput = it.yLine; prev = it.yInput; continue; }
it.yInput = Math.max(it.yLine, prev + GAP);
prev = it.yInput;
}
// Hide leader lines for items whose input is cleared
const active = new Set(items.map(it => it.id));
['overflowLevel','maxLevel','startLevel','minLevel','dryRunLevel','outflowLevel'].forEach(id => {
if (!active.has(id)) {
const lead = document.getElementById(`ps-leader-${id}`);
if (lead) lead.setAttribute('visibility', 'hidden');
}
});
for (const it of items) placeItem(it.id, it.yLine, it.yInput);
// Inlet arrow — sole item on the left, no stacking concerns
const inflowY = yForLevel(fNum('inflowLevel'), basinH);
if (inflowY != null) {
const line = document.getElementById('ps-line-inflowLevel');
const lbl = document.getElementById('ps-label-inflowLevel');
const sub = document.getElementById('ps-sub-inflowLevel');
const fo = document.getElementById('ps-fo-inflowLevel');
const unit = document.getElementById('ps-unit-inflowLevel');
if (line) { line.setAttribute('y1', inflowY); line.setAttribute('y2', inflowY); }
if (lbl) lbl.setAttribute('y', inflowY - 4);
if (sub) sub.setAttribute('y', inflowY + 8);
if (fo) fo.setAttribute('y', inflowY - 11);
if (unit) unit.setAttribute('y', inflowY + 4);
}
// Dead-volume band: from outflowLevel down to the floor
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');
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 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));
// dryRunLevel label text (derived, read-only)
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)
// Safety-section readouts (second view, beneath the diagram)
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 = [];
@@ -324,7 +385,7 @@
#ps-basin-diagram input[type=number]:focus { outline: 1px solid #0c99d9; border-color: #0c99d9; }
</style>
<svg id="ps-basin-diagram" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 430"
<svg id="ps-basin-diagram" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 480"
style="display:block;width:100%;max-width:540px;margin:0 0 12px 0;background:#fff;border:1px solid #e5e5e5;border-radius:4px;"
font-family="Arial,sans-serif" font-size="11">
<defs>
@@ -404,8 +465,17 @@
<line x1="195" y1="380" x2="325" y2="380" stroke="#000" stroke-width="2" />
<text x="330" y="384" fill="#000">0 m (datum)</text>
<!-- Leader lines: shown when the input row had to be nudged off its threshold's ideal y -->
<line id="ps-leader-basinHeight" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
<line id="ps-leader-overflowLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
<line id="ps-leader-maxLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
<line id="ps-leader-startLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
<line id="ps-leader-minLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
<line id="ps-leader-dryRunLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
<line id="ps-leader-outflowLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
<!-- Ordering-warning ribbon -->
<text id="ps-warning" x="260" y="410" text-anchor="middle" fill="#C0392B" font-size="10" font-style="italic" visibility="hidden"></text>
<text id="ps-warning" x="260" y="460" text-anchor="middle" fill="#C0392B" font-size="10" font-style="italic" visibility="hidden"></text>
</svg>
<div class="form-row">