Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out

Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
  Arms when level rises past shiftLevel; disarms when level drops below
  startLevel. While armed, ramp foot moves to startLevel and ramp top
  to shiftLevel — both ends shift left, then saturate at 100 % up to
  maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
  the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.

Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.

Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
  basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
  oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
  flat from start→inlet, ramp inlet→max, optional shifted-down curve
  start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
  overflow), level markers (dryRun derived, start, inlet, max, shift,
  overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
  always visible.
- All level inputs moved to a side panel left of each diagram, color-
  coded to match line strokes; hover-couple highlights the paired SVG
  line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
  pipelineLength, maxDischargeHead, staticHead, defaultFluid,
  maxInflowRate, temperatureReferenceDegC,
  timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
  outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
  side panel).

Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
  trio so the basin can be exercised end-to-end without a connected
  rotating-machine downstream.

Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
  hold-zone behaviour and shift-armed/disarmed transitions through
  shiftLevel and startLevel boundaries. 53/53 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rene De Ren
2026-05-05 19:29:34 +02:00
parent da50403c76
commit 8a6ca1baeb
12 changed files with 1877 additions and 487 deletions

101
src/editor/oneditprepare.js Normal file
View File

@@ -0,0 +1,101 @@
// PumpingStation editor — oneditprepare entry. Wires up form-field
// initialization, control-mode toggle, safety toggles, and binds
// redraws for the basin diagram + level-based mode preview.
(function () {
const ns = window.PSEditor = window.PSEditor || {};
ns.oneditprepare = function () {
const node = this;
// Wait for menu data (asset/logger/position dropdowns) before init.
const waitForMenuData = () => {
if (window.EVOLV?.nodes?.pumpingStation?.initEditor) {
window.EVOLV.nodes.pumpingStation.initEditor(node);
} else {
setTimeout(waitForMenuData, 50);
}
};
waitForMenuData();
const refHeightEl = document.getElementById('node-input-refHeight');
if (refHeightEl) refHeightEl.value = node.refHeight || 'NAP';
// Safety toggle pairs — each toggle enables/disables its threshold input.
const dryRunToggle = document.getElementById('node-input-enableDryRunProtection');
const dryRunPercent = document.getElementById('node-input-dryRunThresholdPercent');
const highVolumeToggle = document.getElementById('node-input-enableHighVolumeSafety');
const highVolumePercent = document.getElementById('node-input-highVolumeSafetyThresholdPercent');
const toggleInput = (toggleEl, inputEl) => {
if (!toggleEl || !inputEl) return;
inputEl.disabled = !toggleEl.checked;
inputEl.parentElement.classList.toggle('disabled', inputEl.disabled);
};
if (dryRunToggle && dryRunPercent) {
dryRunToggle.checked = !!node.enableDryRunProtection;
dryRunPercent.value = Number.isFinite(node.dryRunThresholdPercent) ? node.dryRunThresholdPercent : 2;
dryRunToggle.addEventListener('change', () => toggleInput(dryRunToggle, dryRunPercent));
toggleInput(dryRunToggle, dryRunPercent);
}
if (highVolumeToggle && highVolumePercent) {
highVolumeToggle.checked = node.enableHighVolumeSafety !== undefined
? !!node.enableHighVolumeSafety
: !!node.enableOverfillProtection;
const highVolumePct = node.highVolumeSafetyThresholdPercent ?? node.overfillThresholdPercent;
highVolumePercent.value = Number.isFinite(highVolumePct) ? highVolumePct : 98;
highVolumeToggle.addEventListener('change', () => toggleInput(highVolumeToggle, highVolumePercent));
toggleInput(highVolumeToggle, highVolumePercent);
}
// Control-mode section toggle (levelbased / manual).
const toggleModeSections = (val) => {
document.querySelectorAll('.ps-mode-section').forEach((el) => el.style.display = 'none');
const active = document.getElementById(`ps-mode-${val}`);
if (active) active.style.display = '';
};
const modeSelect = document.getElementById('node-input-controlMode');
if (modeSelect) {
modeSelect.value = node.controlMode === 'manual' ? 'manual' : 'levelbased';
toggleModeSections(modeSelect.value);
modeSelect.addEventListener('change', (e) => toggleModeSections(e.target.value));
}
// Numeric field defaults.
ns.setNumberField('node-input-startLevel', node.startLevel);
ns.setNumberField('node-input-maxLevel', node.maxLevel);
ns.setNumberField('node-input-logCurveFactor', node.logCurveFactor);
ns.setNumberField('node-input-shiftLevel', node.shiftLevel);
ns.setNumberField('node-input-flowSetpoint', node.flowSetpoint);
ns.setNumberField('node-input-flowDeadband', node.flowDeadband);
const curveSelect = document.getElementById('node-input-levelCurveType');
if (curveSelect) curveSelect.value = node.levelCurveType || node.curveType || 'linear';
const shiftCheckbox = document.getElementById('node-input-enableShiftedRamp');
if (shiftCheckbox) shiftCheckbox.checked = !!node.enableShiftedRamp;
// Bind redraws to the inputs each diagram cares about.
ns.bindRedraw(
['basinHeight', 'overflowLevel', 'inflowLevel', 'outflowLevel',
'dryRunThresholdPercent', 'highVolumeSafetyThresholdPercent'],
ns.basinDiagram.redraw
);
ns.bindRedraw(
// dryRunLevel is derived (outflowLevel + dryRunThresholdPercent),
// so the mode preview must redraw when either of those change.
['startLevel', 'maxLevel', 'inflowLevel', 'outflowLevel', 'overflowLevel',
'dryRunThresholdPercent',
'levelCurveType', 'logCurveFactor', 'enableShiftedRamp', 'shiftLevel'],
ns.modePreview.redraw
);
// Initial render + hover-couple wiring once the DOM is settled.
setTimeout(() => {
ns.basinDiagram.redraw();
ns.modePreview.redraw();
ns.hoverCouple?.init();
}, 60);
};
})();