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:
92
src/editor/compact-fields.js
Normal file
92
src/editor/compact-fields.js
Normal 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 };
|
||||
})();
|
||||
@@ -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 };
|
||||
})();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user