feat(mgc): consume shared icon-picker visuals + modernize editor menu

* compact-fields.js (new): trimmed to MGC-only output-format pickers
  (processOutputFormat / dbaseOutputFormat). The logger toggle/level
  and physical-position visuals now come from generalFunctions'
  shared iconHelpers, auto-injected via /machineGroupControl/menu.js.
* mode-cards.js: strategy cards re-styled — Most-efficient (BEP bell
  with dot on the curve peak), Priority (clean staircase), Maintenance
  (Font Awesome fa-wrench). Rendezvous toggle flips Active / Inactive
  label dynamically.
* mgc.html: dropped the duplicated .mgc-icon-* CSS rules (now live in
  the shared iconHelpers stylesheet). Strategy + rendezvous CSS stays
  local (MGC-specific). Output picker holders switched to the shared
  .evolv-icon-picker class.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-18 11:10:54 +02:00
parent 472402c62d
commit 6833e9f3a8
4 changed files with 218 additions and 115 deletions

View File

@@ -0,0 +1,92 @@
// compact-fields.js — MGC-only output-format icon picker.
//
// Logger toggle/level and physical-position visuals now live in the shared
// generalFunctions/src/menu/iconHelpers.js (auto-injected by MenuManager), so
// the only MGC-local visuals left are the two output-format dropdowns
// (processOutputFormat, dbaseOutputFormat) — those fields aren't part of any
// shared menu.
(function () {
const editor = window.EVOLV?.nodes?.machineGroupControl?.editor;
if (!editor) return;
const BLUE = '#1F4E79';
const STEEL = '#607484';
// MGC-only SVGs (output formats only — logger/position SVGs come from
// window.EVOLV.iconHelpers.SVG).
const SVG = {
process: `
<svg viewBox="0 0 80 58" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<rect x="10" y="14" width="20" height="30" rx="2" fill="#f7fafc" stroke="${STEEL}" stroke-width="2.4"/>
<rect x="50" y="14" width="20" height="30" rx="2" fill="#f7fafc" stroke="${STEEL}" stroke-width="2.4"/>
<line x1="30" y1="29" x2="46" y2="29" stroke="${BLUE}" stroke-width="3" stroke-linecap="round"/>
<path d="M42 24 L48 29 L42 34" fill="none" stroke="${BLUE}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`,
json: `
<svg viewBox="0 0 80 58" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<g fill="none" stroke="${BLUE}" stroke-width="3.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M30 14 C22 16 22 26 27 29 C22 32 22 42 30 44"/>
<path d="M50 14 C58 16 58 26 53 29 C58 32 58 42 50 44"/>
</g>
<g fill="${STEEL}">
<circle cx="36" cy="29" r="2.2"/>
<circle cx="44" cy="29" r="2.2"/>
</g>
</svg>`,
csv: `
<svg viewBox="0 0 80 58" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<rect x="12" y="12" width="56" height="34" rx="2" fill="#fff" stroke="${STEEL}" stroke-width="2.4"/>
<line x1="12" y1="22" x2="68" y2="22" stroke="${STEEL}" stroke-width="2"/>
<g stroke="${STEEL}" stroke-width="1.6">
<line x1="12" y1="34" x2="68" y2="34"/>
<line x1="31" y1="12" x2="31" y2="46"/>
<line x1="49" y1="12" x2="49" y2="46"/>
</g>
</svg>`,
influxdb: `
<svg viewBox="0 0 80 58" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<ellipse cx="40" cy="15" rx="22" ry="6" fill="#f7fafc" stroke="${STEEL}" stroke-width="2.4"/>
<path d="M18 15 V42 C18 46 28 49 40 49 C52 49 62 46 62 42 V15" fill="#f7fafc" stroke="${STEEL}" stroke-width="2.4"/>
<path d="M18 28 C26 32 54 32 62 28" fill="none" stroke="${STEEL}" stroke-width="1.6" opacity="0.6"/>
<path d="M22 39 L30 32 L38 41 L46 34 L54 38" fill="none" stroke="${BLUE}" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`,
};
const outputIcons = {
process: SVG.process,
json: SVG.json,
csv: SVG.csv,
influxdb: SVG.influxdb,
};
const outputLabels = {
process: 'Process',
json: 'JSON',
csv: 'CSV',
influxdb: 'Influx',
};
function initOutputFormats() {
const helpers = window.EVOLV?.iconHelpers;
if (!helpers) return;
const processSelect = document.getElementById('node-input-processOutputFormat');
const processHolder = document.getElementById('mgc-process-output-picker');
if (processSelect && processHolder) {
helpers.renderSelectPicker(processSelect, processHolder, outputIcons, outputLabels);
}
const dbaseSelect = document.getElementById('node-input-dbaseOutputFormat');
const dbaseHolder = document.getElementById('mgc-dbase-output-picker');
if (dbaseSelect && dbaseHolder) {
helpers.renderSelectPicker(dbaseSelect, dbaseHolder, outputIcons, outputLabels);
}
}
function init() {
initOutputFormats();
}
editor.compactFields = { init };
})();

View File

@@ -1,14 +1,10 @@
// mode-cards.js — visual radio picker for the three control-strategy modes.
// mode-cards.js — visual pickers for control-strategy modes and planner flags.
//
// Replaces the plain <select id="node-input-mode"> with three illustrated
// cards. The original <input> stays in the DOM but is hidden — Node-RED reads
// its value on save, exactly as before. Clicking a card sets that value and
// fires editor.emitModeChange so downstream UI (none today, future widgets
// such as a parameter panel) can re-render.
// Replaces the plain mode field with compact illustrated controls. The
// original inputs stay in the DOM but are hidden — Node-RED reads their values
// on save, exactly as before.
//
// Three cards: optimalControl (BEP-curve), priorityControl (flow ladder),
// maintenance (status-only badge). SVGs are inline so the editor doesn't
// need to fetch additional assets.
// SVGs are inline so the editor doesn't need to fetch additional assets.
(function () {
const editor = window.EVOLV?.nodes?.machineGroupControl?.editor;
@@ -17,95 +13,61 @@
const MODES = [
{
value: 'optimalControl',
label: 'optimalControl',
caption: 'Picks the pump combination whose BEP sits closest to current demand.',
ariaLabel: 'Optimal control',
label: 'Most-efficient',
svg: `
<svg viewBox="0 0 160 90" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<text x="6" y="14" font-size="9" fill="#444">η</text>
<line x1="14" y1="78" x2="154" y2="78" stroke="#444" stroke-width="1"/>
<line x1="14" y1="78" x2="14" y2="14" stroke="#444" stroke-width="1"/>
<text x="118" y="88" font-size="8" fill="#666">demand →</text>
<!-- Three pump-combination efficiency humps. Each combination has
its own BEP (peak). The optimizer "gravitates" toward whichever
peak sits closest to the current demand. Quadratic-Bezier peak
formula: peak_y = (y0 + 2*cy + y1)/4 — so for y0=y1=78 (foot on
x-axis), cy=22 → peak_y=50, cy=-26 → peak_y=26, cy=10 → peak_y=44. -->
<path d="M 16 78 Q 32 22 50 78" fill="none" stroke="#888" stroke-width="1.1"/>
<path d="M 44 78 Q 72 -26 100 78" fill="none" stroke="#1E8449" stroke-width="2"/>
<path d="M 92 78 Q 122 10 152 78" fill="none" stroke="#888" stroke-width="1.1"/>
<!-- BEP markers sit ON each hump's apex — small grey for unpicked
combos, large red for the selected (winner) combination. -->
<circle cx="33" cy="50" r="2" fill="#888"/>
<circle cx="72" cy="26" r="3.2" fill="#C0392B" stroke="#fff" stroke-width="1"/>
<circle cx="122" cy="44" r="2" fill="#888"/>
<!-- Current demand (dashed line) lines up with combo #2's BEP, so
combo #2 wins — drawn thicker/green above. -->
<line x1="72" y1="14" x2="72" y2="78" stroke="#1F4E79" stroke-dasharray="2 2" stroke-width="0.9"/>
<text x="46" y="20" font-size="7" fill="#1F4E79">demand</text>
<text x="80" y="22" font-size="7" fill="#C0392B" font-weight="bold">BEP</text>
<!-- Combination labels under each curve. -->
<text x="33" y="86" font-size="6" fill="#666" text-anchor="middle">P1</text>
<text x="72" y="86" font-size="6" fill="#1E8449" text-anchor="middle" font-weight="bold">P1+P2</text>
<text x="122" y="86" font-size="6" fill="#666" text-anchor="middle">P1+P2+P3</text>
<svg viewBox="0 0 120 72" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<line x1="12" y1="60" x2="112" y2="60" stroke="#888" stroke-width="1.4" stroke-linecap="round"/>
<line x1="12" y1="60" x2="12" y2="10" stroke="#888" stroke-width="1.4" stroke-linecap="round"/>
<path d="M 16 60 Q 62 -30 108 60" fill="none" stroke="#1E8449" stroke-width="3" stroke-linecap="round"/>
<line x1="62" y1="15" x2="62" y2="60" stroke="#1F4E79" stroke-dasharray="3 3" stroke-width="1.2"/>
<circle cx="62" cy="15" r="5.5" fill="#1E8449" stroke="#fff" stroke-width="1.6"/>
</svg>`,
},
{
value: 'priorityControl',
label: 'priorityControl',
caption: 'Sequential equal-flow ramp — fill pumps one-by-one in priority order.',
ariaLabel: 'Priority control',
label: 'Priority',
svg: `
<svg viewBox="0 0 160 90" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<text x="6" y="14" font-size="9" fill="#444">flow</text>
<line x1="14" y1="78" x2="154" y2="78" stroke="#444" stroke-width="1"/>
<line x1="14" y1="78" x2="14" y2="14" stroke="#444" stroke-width="1"/>
<text x="118" y="88" font-size="8" fill="#666">demand →</text>
<polyline points="14,72 50,72 50,52 86,52 86,32 122,32 122,16 154,16"
fill="none" stroke="#1F4E79" stroke-width="2"/>
<text x="28" y="86" font-size="7" fill="#666">P1</text>
<text x="64" y="86" font-size="7" fill="#666">P2</text>
<text x="100" y="86" font-size="7" fill="#666">P3</text>
<svg viewBox="0 0 120 72" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<line x1="12" y1="60" x2="112" y2="60" stroke="#888" stroke-width="1.4" stroke-linecap="round"/>
<line x1="12" y1="60" x2="12" y2="10" stroke="#888" stroke-width="1.4" stroke-linecap="round"/>
<polyline points="14,54 38,54 38,40 62,40 62,26 86,26 86,14 110,14"
fill="none" stroke="#1F4E79" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/>
</svg>`,
},
{
value: 'maintenance',
label: 'maintenance',
caption: 'Monitor only. Dispatch and stop-all commands are rejected; status messages still flow.',
svg: `
<svg viewBox="0 0 160 90" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<circle cx="80" cy="42" r="22" fill="none" stroke="#888" stroke-width="2"/>
<circle cx="80" cy="42" r="8" fill="#888"/>
<g stroke="#888" stroke-width="3" stroke-linecap="round">
<line x1="80" y1="14" x2="80" y2="24"/>
<line x1="80" y1="60" x2="80" y2="70"/>
<line x1="52" y1="42" x2="62" y2="42"/>
<line x1="98" y1="42" x2="108" y2="42"/>
<line x1="60" y1="22" x2="67" y2="29"/>
<line x1="93" y1="55" x2="100" y2="62"/>
<line x1="60" y1="62" x2="67" y2="55"/>
<line x1="93" y1="29" x2="100" y2="22"/>
</g>
<text x="80" y="84" text-anchor="middle" font-size="8" fill="#888">monitor only</text>
</svg>`,
ariaLabel: 'Maintenance',
label: 'Maintenance',
svg: `<i class="fa fa-wrench" style="font-size:40px;color:#607484;" aria-hidden="true"></i>`,
},
];
// Render the three cards into the placeholder div. The hidden <select> stays
// intact — the card click handler writes its value back to that <select> so
// Node-RED's save path is unchanged.
const RENDEZVOUS_SVG = `
<svg viewBox="0 0 120 72" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<line x1="12" y1="60" x2="112" y2="60" stroke="#888" stroke-width="1.4" stroke-linecap="round"/>
<line x1="12" y1="60" x2="12" y2="10" stroke="#888" stroke-width="1.4" stroke-linecap="round"/>
<line x1="96" y1="12" x2="96" y2="60" stroke="#1F4E79" stroke-dasharray="3 3" stroke-width="1.4"/>
<path d="M 18 52 C 38 50, 64 38, 96 20" fill="none" stroke="#1E8449" stroke-width="2.6" stroke-linecap="round"/>
<path d="M 18 58 C 40 56, 64 42, 96 20" fill="none" stroke="#50a8d9" stroke-width="2.6" stroke-linecap="round"/>
<path d="M 18 44 C 42 44, 66 34, 96 20" fill="none" stroke="#C0392B" stroke-width="2.6" stroke-linecap="round"/>
<circle cx="96" cy="20" r="6" fill="#1F4E79" stroke="#fff" stroke-width="1.6"/>
</svg>`;
// Render the three cards into the placeholder div. The hidden input stays
// intact; the card click handler writes its value back so Node-RED's save
// path is unchanged.
function init(/* node */) {
const placeholder = document.getElementById('mgc-mode-cards');
const hidden = document.getElementById('node-input-mode');
if (!placeholder || !hidden) return;
placeholder.innerHTML = MODES.map((m) => `
<div class="mgc-mode-card" data-mode="${m.value}" role="radio" tabindex="0" aria-checked="false">
<div class="mgc-mode-card" data-mode="${m.value}" role="radio" tabindex="0"
aria-label="${m.ariaLabel}" aria-checked="false" title="${m.ariaLabel}">
<div class="mgc-mode-card-svg">${m.svg}</div>
<div class="mgc-mode-card-label">${m.label}</div>
<div class="mgc-mode-card-caption">${m.caption}</div>
</div>
`).join('');
@@ -138,5 +100,40 @@
syncHighlight();
}
function initRendezvousToggle(/* node */) {
const placeholder = document.getElementById('mgc-rendezvous-toggle');
const checkbox = document.getElementById('node-input-useRendezvous');
if (!placeholder || !checkbox) return;
placeholder.innerHTML = `
<div class="mgc-toggle-card-svg">${RENDEZVOUS_SVG}</div>
<div class="mgc-toggle-card-label">Inactive</div>
`;
const labelEl = placeholder.querySelector('.mgc-toggle-card-label');
function syncHighlight() {
const on = checkbox.checked;
placeholder.classList.toggle('mgc-toggle-card-on', on);
placeholder.setAttribute('aria-checked', String(on));
if (labelEl) labelEl.textContent = on ? 'Active' : 'Inactive';
}
function toggle() {
checkbox.checked = !checkbox.checked;
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
syncHighlight();
}
placeholder.addEventListener('click', toggle);
placeholder.addEventListener('keydown', (e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
toggle();
}
});
checkbox.addEventListener('change', syncHighlight);
syncHighlight();
}
editor.modeCards = { init };
editor.rendezvousToggle = { init: initRendezvousToggle };
})();

View File

@@ -12,5 +12,11 @@
if (ns.editor.modeCards && typeof ns.editor.modeCards.init === 'function') {
ns.editor.modeCards.init(node);
}
if (ns.editor.rendezvousToggle && typeof ns.editor.rendezvousToggle.init === 'function') {
ns.editor.rendezvousToggle.init(node);
}
if (ns.editor.compactFields && typeof ns.editor.compactFields.init === 'function') {
ns.editor.compactFields.init(node);
}
};
})();