diff --git a/CLAUDE.md b/CLAUDE.md index 990c958..0fa9902 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,3 +21,20 @@ Key points for this node: - Stack same-level siblings vertically. - Parent/children sit on adjacent lanes (children one lane left, parent one lane right). - Wrap in a Node-RED group box coloured `#86bbdd` (Equipment Module). + +## Folder & File Layout + +Every per-node file MUST use the folder name (`rotatingMachine`) **exactly**, case-sensitive. Full rule: [`.claude/rules/node-architecture.md`](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/rules/node-architecture.md) in the EVOLV superproject. + +| Path | Required name | +|---|---| +| Entry file | `rotatingMachine.js` | +| Editor HTML | `rotatingMachine.html` | +| Node adapter | `src/nodeClass.js` | +| Domain logic | `src/specificClass.js` | +| Editor JS modules | `src/editor/*.js` (extract when inline editor JS exceeds ~50 lines) | +| Tests | `test/{basic,integration,edge}/*.test.js` | +| Example flows | `examples/*.flow.json` | + + +When adding new files, read the rule above first to avoid drift. diff --git a/rotatingMachine.html b/rotatingMachine.html index f123a20..32ffb33 100644 --- a/rotatingMachine.html +++ b/rotatingMachine.html @@ -69,12 +69,14 @@ }, oneditprepare: function() { - // wait for the menu scripts to load + const node = this; + + // wait for the menu scripts to load (asset/logger/position injected via menu.js) let menuRetries = 0; const maxMenuRetries = 100; // 5 seconds at 50ms intervals const waitForMenuData = () => { if (window.EVOLV?.nodes?.rotatingMachine?.initEditor) { - window.EVOLV.nodes.rotatingMachine.initEditor(this); + window.EVOLV.nodes.rotatingMachine.initEditor(node); } else if (++menuRetries < maxMenuRetries) { setTimeout(waitForMenuData, 50); } else { @@ -83,17 +85,189 @@ }; waitForMenuData(); - // your existing project‐settings & asset dropdown logic can remain here - document.getElementById("node-input-speed"); - document.getElementById("node-input-startup"); - document.getElementById("node-input-warmup"); - document.getElementById("node-input-shutdown"); - document.getElementById("node-input-cooldown"); - const movementMode = document.getElementById("node-input-movementMode"); - if (movementMode) { - movementMode.value = this.movementMode || "staticspeed"; + // ----------------------------------------------------------- + // Movement-mode visual cards (replaces the old s carry the value; the icon-picker divs are + // upgraded in place. Same visuals as machineGroupControl. + // ----------------------------------------------------------- + const helpers = window.EVOLV?.iconHelpers; + if (helpers && typeof helpers.renderOutputFormatPicker === "function") { + helpers.renderOutputFormatPicker( + document.getElementById("node-input-processOutputFormat"), + document.getElementById("rm-process-output-picker") + ); + helpers.renderOutputFormatPicker( + document.getElementById("node-input-dbaseOutputFormat"), + document.getElementById("rm-dbase-output-picker") + ); } + // ----------------------------------------------------------- + // Circular state-machine diagram (replaces the linear bars). + // Idle is a small fixed slice at the top; operational is a + // fixed dominant arc at the bottom; starting+warmingup and + // stopping+coolingdown each share one of the two side bands + // proportional to their seconds. Reaction speed shown as a + // small slope inside the donut hole. + // ----------------------------------------------------------- + const TL = { + cx: 170, cy: 130, + innerR: 46, outerR: 80, + idleDeg: 30, // fixed slice at top, the loop-around + operationalDeg: 100, // fixed dominant arc at the bottom + sideMinDeg: 28 // each timed phase keeps at least this so labels fit + }; + TL.sideDeg = (360 - TL.idleDeg - TL.operationalDeg) / 2; // 115° per side + + function p2c(r, deg) { + const rad = deg * Math.PI / 180; + return [TL.cx + r * Math.sin(rad), TL.cy - r * Math.cos(rad)]; + } + function arcPath(rIn, rOut, startDeg, endDeg) { + const [x1, y1] = p2c(rOut, startDeg); + const [x2, y2] = p2c(rOut, endDeg); + const [x3, y3] = p2c(rIn, endDeg); + const [x4, y4] = p2c(rIn, startDeg); + const largeArc = (endDeg - startDeg) > 180 ? 1 : 0; + return "M " + x1.toFixed(2) + " " + y1.toFixed(2) + + " A " + rOut + " " + rOut + " 0 " + largeArc + " 1 " + x2.toFixed(2) + " " + y2.toFixed(2) + + " L " + x3.toFixed(2) + " " + y3.toFixed(2) + + " A " + rIn + " " + rIn + " 0 " + largeArc + " 0 " + x4.toFixed(2) + " " + y4.toFixed(2) + + " Z"; + } + function splitPair(a, b, total, minDeg) { + let aDeg, bDeg; + if (a + b === 0) { aDeg = bDeg = total / 2; } + else { aDeg = total * a / (a + b); bDeg = total - aDeg; } + if (aDeg < minDeg) { aDeg = minDeg; bDeg = total - minDeg; } + else if (bDeg < minDeg) { bDeg = minDeg; aDeg = total - minDeg; } + return [aDeg, bDeg]; + } + + function redrawTimeline() { + const speed = Math.max(0.01, parseFloat(document.getElementById("node-input-speed").value) || 1); + const startup = Math.max(0, parseFloat(document.getElementById("node-input-startup").value) || 0); + const warmup = Math.max(0, parseFloat(document.getElementById("node-input-warmup").value) || 0); + const shutdown = Math.max(0, parseFloat(document.getElementById("node-input-shutdown").value) || 0); + const cooldown = Math.max(0, parseFloat(document.getElementById("node-input-cooldown").value) || 0); + + const [startingDeg, warmingupDeg] = splitPair(startup, warmup, TL.sideDeg, TL.sideMinDeg); + const [stoppingDeg, coolingdownDeg] = splitPair(shutdown, cooldown, TL.sideDeg, TL.sideMinDeg); + + // Clockwise from top (0° = idle centre). Wrap idle across ±idleDeg/2. + const idleHalf = TL.idleDeg / 2; + const states = [ + { id: "idle", startDeg: -idleHalf, endDeg: idleHalf, label: "idle", time: null, above: true }, + { id: "starting", startDeg: idleHalf, endDeg: idleHalf + startingDeg, label: "starting", time: startup, above: false }, + { id: "warmingup", startDeg: idleHalf + startingDeg, endDeg: idleHalf + startingDeg + warmingupDeg, label: "\u{1F6E1}︎ warm-up", time: warmup, above: false }, + { id: "operational", startDeg: idleHalf + TL.sideDeg, endDeg: idleHalf + TL.sideDeg + TL.operationalDeg, label: "operational", time: null, above: false }, + { id: "stopping", startDeg: idleHalf + TL.sideDeg + TL.operationalDeg, endDeg: idleHalf + TL.sideDeg + TL.operationalDeg + stoppingDeg, label: "stopping", time: shutdown, above: false }, + { id: "coolingdown", startDeg: idleHalf + TL.sideDeg + TL.operationalDeg + stoppingDeg, endDeg: idleHalf + TL.sideDeg + TL.operationalDeg + stoppingDeg + coolingdownDeg, label: "\u{1F6E1}︎ cool-down", time: cooldown, above: false } + ]; + + const labelR = (TL.innerR + TL.outerR) / 2; + const titleR = TL.outerR + 22; + + states.forEach((s) => { + const arc = document.getElementById("rm-tl-" + s.id); + if (arc) arc.setAttribute("d", arcPath(TL.innerR, TL.outerR, s.startDeg, s.endDeg)); + + const midDeg = (s.startDeg + s.endDeg) / 2; + const normMid = ((midDeg % 360) + 360) % 360; + + // State name OUTSIDE the ring. + const lbl = document.getElementById("rm-tl-lbl-" + s.id); + if (lbl) { + const [lx, ly] = p2c(titleR, midDeg); + lbl.setAttribute("x", lx.toFixed(2)); + lbl.setAttribute("y", ly.toFixed(2)); + let ta; + if (Math.abs(normMid) < 12 || Math.abs(normMid - 180) < 12 || normMid > 348) ta = "middle"; + else if (normMid > 0 && normMid < 180) ta = "start"; + else ta = "end"; + lbl.setAttribute("text-anchor", ta); + const dy = (normMid < 12 || normMid > 348) ? "-4" + : (Math.abs(normMid - 180) < 12) ? "14" + : "4"; + lbl.setAttribute("dy", dy); + lbl.textContent = s.label; + } + + // Time value INSIDE arc. + const t = document.getElementById("rm-tl-time-" + s.id); + if (t) { + const [tx, ty] = p2c(labelR, midDeg); + t.setAttribute("x", tx.toFixed(2)); + t.setAttribute("y", ty.toFixed(2)); + t.setAttribute("text-anchor", "middle"); + t.setAttribute("dy", "4"); + t.textContent = (s.time == null) ? "" : (s.time + "s"); + } + }); + + // Reaction-speed value in the donut hole. + const rampVal = document.getElementById("rm-tl-ramp-value"); + if (rampVal) rampVal.textContent = speed + " %/s"; + } + + // Hover-couple: hover an input row → glow its arc. + document.querySelectorAll(".rm-row[data-couples]").forEach((row) => { + const targetId = row.dataset.couples; + row.addEventListener("mouseenter", () => { + document.getElementById(targetId)?.classList.add("rm-arc-highlight"); + }); + row.addEventListener("mouseleave", () => { + document.getElementById(targetId)?.classList.remove("rm-arc-highlight"); + }); + }); + + ["speed", "startup", "warmup", "shutdown", "cooldown"].forEach((field) => { + const el = document.getElementById("node-input-" + field); + if (el) el.addEventListener("input", redrawTimeline); + }); + + // Size the donut SVG so its top/bottom line up with the side panel: + // measure the side-panel's computed height and apply it to the SVG. + // Re-runs on every dialog open (oneditprepare is per-edit). + function syncSvgHeight() { + const sidePanel = document.querySelector(".rm-diag-side"); + const svg = document.getElementById("rm-timeline"); + if (!sidePanel || !svg) return; + const h = sidePanel.getBoundingClientRect().height; + if (h > 0) svg.style.height = h + "px"; + } + + // First paint (next tick so the dialog is in the DOM). + // Use requestAnimationFrame chain so the side-panel height is measured + // AFTER the dialog has actually laid out — getBoundingClientRect on a + // freshly-created element returns 0 inside the same synchronous tick. + setTimeout(() => { + redrawTimeline(); + requestAnimationFrame(() => requestAnimationFrame(syncSvgHeight)); + }, 0); }, oneditsave: function() { const node = this; @@ -114,13 +288,11 @@ ["speed", "startup", "warmup", "shutdown", "cooldown"].forEach((field) => { const element = document.getElementById(`node-input-${field}`); const value = parseFloat(element?.value) || 0; - console.log(`----------------> Saving ${field}: ${value}`); node[field] = value; }); - node.movementMode = document.getElementById("node-input-movementMode").value; - console.log(`----------------> Saving movementMode: ${node.movementMode}`); - + const modeEl = document.getElementById("node-input-movementMode"); + node.movementMode = (modeEl && modeEl.value) ? modeEl.value : "staticspeed"; } }); @@ -128,65 +300,275 @@ @@ -196,11 +578,11 @@

Configuration