Compare commits

...

1 Commits

Author SHA1 Message Date
znetsixe
ab0d4ed285 Editor: pin outlet, add zone labels + volume to the diagram
Three user-facing fixes:

1. Outlet was getting pushed below the tank floor by the top-down
   nudge because its ideal y is already near the bottom. Now
   outflowLevel is PINNED at its proportional y (like basinHeight
   is pinned at the rim) and a second bottom-up pass pushes
   non-pinned items upward from the outlet anchor. Result: outlet
   stays near the tank floor, dryRunLevel sits right above it, the
   rest of the stack stays readable. Two anchors, two passes.

2. Zone labels mirrored from the wiki basin-model drawio:
   - "Spare volume before spilling"  (overflowLevel ↔ maxLevel)
   - "Sewage + tank buffer"          (maxLevel      ↔ startLevel)
   - "Tank buffer"                    (startLevel    ↔ minLevel)
   - "Tank buffer"                    (minLevel      ↔ dryRunLevel)
   - "Dead volume"                    (outflowLevel  ↔ floor)
   Each sits at the midpoint of its pair of nudged thresholds and
   hides when the gap between them is too small to read (< 14 px).

3. basinVolume moved into the SVG as a pinned input above the tank
   rim (alongside basinHeight), replacing the separate form row.
   One editor, one diagram — the total volume belongs with the
   geometry.

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

View File

@@ -220,11 +220,12 @@
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
// tank rim; others are sorted by their proportional y and then
// pushed apart so every dashed line gets a minimum vertical
// gap for readability. The diagram is a schematic ordered
// list, not a strictly to-scale rendering.
// Right-column stack. TWO anchors: basinHeight pinned at the
// tank rim (top) and outflowLevel pinned at its proportional y
// (bottom). Everything between is nudged to maintain a minimum
// vertical gap via two passes — top-down from the rim, then
// bottom-up from the outlet — so the dashed lines keep their
// value-order and outlet stays near the floor where it belongs.
const items = [
{ id: 'basinHeight', yIdeal: DIAG.topY, pinned: true },
{ id: 'overflowLevel', yIdeal: yForLevel(fNum('overflowLevel'), basinH) },
@@ -232,19 +233,52 @@
{ id: 'startLevel', yIdeal: yForLevel(fNum('startLevel'), basinH) },
{ id: 'minLevel', yIdeal: yForLevel(fNum('minLevel'), basinH) },
{ id: 'dryRunLevel', yIdeal: yForLevel(dryLvl, basinH) },
{ id: 'outflowLevel', yIdeal: yForLevel(fNum('outflowLevel'), basinH) },
{ id: 'outflowLevel', yIdeal: yForLevel(fNum('outflowLevel'), basinH), pinned: true },
].filter(it => it.yIdeal != null);
const GAP = 36;
items.sort((a, b) => a.yIdeal - b.yIdeal);
let prev = -Infinity;
for (const it of items) {
if (it.pinned) { it.y = it.yIdeal; prev = it.y; continue; }
it.y = Math.max(it.yIdeal, prev + GAP);
prev = it.y;
for (const it of items) it.y = it.yIdeal;
// Pass 1: top-down — push DOWN to maintain GAP; pinned items don't move
for (let i = 1; i < items.length; i++) {
if (items[i].pinned) continue;
items[i].y = Math.max(items[i].y, items[i - 1].y + GAP);
}
// Pass 2: bottom-up — push UP so outflow's pin propagates up the stack
for (let i = items.length - 2; i >= 0; i--) {
if (items[i].pinned) continue;
items[i].y = Math.min(items[i].y, items[i + 1].y - GAP);
}
for (const it of items) placeItem(it.id, it.y);
// Zone labels between adjacent thresholds (italic, centered).
// Hidden if either bracketing threshold is missing, or the gap
// is too small to read (< 14 px).
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) {
el.setAttribute('visibility', 'hidden'); return;
}
el.setAttribute('y', (top.y + bot.y) / 2 + 3);
el.setAttribute('visibility', 'visible');
};
placeZone('spare', 'overflowLevel', 'maxLevel');
placeZone('sewage', 'maxLevel', 'startLevel');
placeZone('buffer1', 'startLevel', 'minLevel');
placeZone('buffer2', 'minLevel', 'dryRunLevel');
// "Dead volume" sits inside the blue band between outflowLevel and the floor
const outflowPinned = items.find(it => it.id === 'outflowLevel');
const deadLbl = document.getElementById('ps-zone-dead');
if (deadLbl && outflowPinned && (DIAG.botY - outflowPinned.y) > 14) {
deadLbl.setAttribute('y', (outflowPinned.y + DIAG.botY) / 2 + 3);
deadLbl.setAttribute('visibility', 'visible');
} else if (deadLbl) {
deadLbl.setAttribute('visibility', 'hidden');
}
// Inlet arrow — sole item on the left, no stacking concerns
const inflowY = yForLevel(fNum('inflowLevel'), basinH);
if (inflowY != null) {
@@ -385,6 +419,20 @@
<rect x="200" y="40" width="120" height="340" fill="#F0F8FF" stroke="#333" stroke-width="1.5" />
<!-- Dead-volume band (y + height updated dynamically below outflowLevel) -->
<rect id="ps-deadvol" x="201" width="118" fill="#AACCE0" />
<!-- basinVolume pinned above the rim -->
<text id="ps-label-basinVolume" x="330" y="19" fill="#333" font-weight="600">basin volume</text>
<foreignObject id="ps-fo-basinVolume" x="425" y="4" width="70" height="22">
<input xmlns="http://www.w3.org/1999/xhtml" type="number" id="node-input-basinVolume" min="0" step="0.1" />
</foreignObject>
<text id="ps-unit-basinVolume" x="500" y="19" fill="#555"></text>
<!-- Zone labels (mid-tank italic, positioned dynamically at midpoint between adjacent thresholds) -->
<text id="ps-zone-spare" x="260" text-anchor="middle" fill="#B78200" font-size="10" font-style="italic" visibility="hidden">Spare volume before spilling</text>
<text id="ps-zone-sewage" x="260" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Sewage + tank buffer</text>
<text id="ps-zone-buffer1" x="260" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Tank buffer</text>
<text id="ps-zone-buffer2" x="260" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Tank buffer</text>
<text id="ps-zone-dead" x="260" text-anchor="middle" fill="#444" font-size="10" font-style="italic" visibility="hidden">Dead volume</text>
<!-- basinHeight always at tank rim (y=40 in viewBox coords) -->
<line id="ps-line-basinHeight" x1="195" y1="40" x2="325" y2="40" stroke="#333" stroke-width="1.5" />
@@ -465,11 +513,6 @@
<text id="ps-warning" x="260" y="410" text-anchor="middle" fill="#C0392B" font-size="10" font-style="italic" visibility="hidden"></text>
</svg>
<div class="form-row">
<label for="node-input-basinVolume"><i class="fa fa-cube"></i> Basin Volume (m³)</label>
<input type="number" id="node-input-basinVolume" min="0" step="0.1" />
</div>
<hr>
<h4>Control Strategy</h4>