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>
This commit is contained in:
@@ -220,31 +220,65 @@
|
|||||||
const dryLvl = (refLow != null && dryPct != null) ? refLow * (1 + dryPct / 100) : null;
|
const dryLvl = (refLow != null && dryPct != null) ? refLow * (1 + dryPct / 100) : null;
|
||||||
const ovfLvl = (ovf != null && ovfPct != null) ? ovf * (ovfPct / 100) : null;
|
const ovfLvl = (ovf != null && ovfPct != null) ? ovf * (ovfPct / 100) : null;
|
||||||
|
|
||||||
// Build the right-column items. basinHeight is pinned at the
|
// Right-column stack. TWO anchors: basinHeight pinned at the
|
||||||
// tank rim; others are sorted by their proportional y and then
|
// tank rim (top) and outflowLevel pinned at its proportional y
|
||||||
// pushed apart so every dashed line gets a minimum vertical
|
// (bottom). Everything between is nudged to maintain a minimum
|
||||||
// gap for readability. The diagram is a schematic ordered
|
// vertical gap via two passes — top-down from the rim, then
|
||||||
// list, not a strictly to-scale rendering.
|
// bottom-up from the outlet — so the dashed lines keep their
|
||||||
|
// value-order and outlet stays near the floor where it belongs.
|
||||||
const items = [
|
const items = [
|
||||||
{ id: 'basinHeight', yIdeal: DIAG.topY, pinned: true },
|
{ id: 'basinHeight', yIdeal: DIAG.topY, pinned: true },
|
||||||
{ id: 'overflowLevel', yIdeal: yForLevel(fNum('overflowLevel'), basinH) },
|
{ id: 'overflowLevel', yIdeal: yForLevel(fNum('overflowLevel'), basinH) },
|
||||||
{ id: 'maxLevel', yIdeal: yForLevel(fNum('maxLevel'), basinH) },
|
{ id: 'maxLevel', yIdeal: yForLevel(fNum('maxLevel'), basinH) },
|
||||||
{ id: 'startLevel', yIdeal: yForLevel(fNum('startLevel'), basinH) },
|
{ id: 'startLevel', yIdeal: yForLevel(fNum('startLevel'), basinH) },
|
||||||
{ id: 'minLevel', yIdeal: yForLevel(fNum('minLevel'), basinH) },
|
{ id: 'minLevel', yIdeal: yForLevel(fNum('minLevel'), basinH) },
|
||||||
{ id: 'dryRunLevel', yIdeal: yForLevel(dryLvl, 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);
|
].filter(it => it.yIdeal != null);
|
||||||
|
|
||||||
const GAP = 36;
|
const GAP = 36;
|
||||||
items.sort((a, b) => a.yIdeal - b.yIdeal);
|
items.sort((a, b) => a.yIdeal - b.yIdeal);
|
||||||
let prev = -Infinity;
|
for (const it of items) it.y = it.yIdeal;
|
||||||
for (const it of items) {
|
// Pass 1: top-down — push DOWN to maintain GAP; pinned items don't move
|
||||||
if (it.pinned) { it.y = it.yIdeal; prev = it.y; continue; }
|
for (let i = 1; i < items.length; i++) {
|
||||||
it.y = Math.max(it.yIdeal, prev + GAP);
|
if (items[i].pinned) continue;
|
||||||
prev = it.y;
|
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);
|
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
|
// Inlet arrow — sole item on the left, no stacking concerns
|
||||||
const inflowY = yForLevel(fNum('inflowLevel'), basinH);
|
const inflowY = yForLevel(fNum('inflowLevel'), basinH);
|
||||||
if (inflowY != null) {
|
if (inflowY != null) {
|
||||||
@@ -385,6 +419,20 @@
|
|||||||
<rect x="200" y="40" width="120" height="340" fill="#F0F8FF" stroke="#333" stroke-width="1.5" />
|
<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) -->
|
<!-- Dead-volume band (y + height updated dynamically below outflowLevel) -->
|
||||||
<rect id="ps-deadvol" x="201" width="118" fill="#AACCE0" />
|
<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">m³</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) -->
|
<!-- 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" />
|
<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>
|
<text id="ps-warning" x="260" y="410" text-anchor="middle" fill="#C0392B" font-size="10" font-style="italic" visibility="hidden"></text>
|
||||||
</svg>
|
</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>
|
<hr>
|
||||||
|
|
||||||
<h4>Control Strategy</h4>
|
<h4>Control Strategy</h4>
|
||||||
|
|||||||
Reference in New Issue
Block a user