@@ -15,7 +15,7 @@
< script >
RED . nodes . registerType ( "rotatingMachine" , {
category : "EVOLV" ,
color : "#86bbdd " ,
color : "#E89B3A " ,
defaults : {
name : { value : "" } ,
@@ -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 movemen tMode = document . getElementById ( "node-input-movementMode" ) ;
if ( movementMode ) {
movementMode . value = this . movementMode || "staticspeed" ;
// -----------------------------------------------------------
// Movement-mode visual cards (replaces the old <select>).
// Same compact 94× 86 card sizing as machineGroupControl.
// -----------------------------------------------------------
const modeInput = document . getElementById ( "node-input-movementMode " ) ;
const cards = document . querySelectorAll ( ".rm-mode-card ") ;
const se tMode = ( val ) => {
if ( modeInput ) modeInput . value = val ;
cards . forEach ( ( c ) => {
const on = c . dataset . value === val ;
c . classList . toggle ( "rm-mode-card-on" , on ) ;
c . setAttribute ( "aria-checked" , String ( on ) ) ;
} ) ;
} ;
const initialMode = ( node . movementMode === "dynspeed" ) ? "dynspeed" : "staticspeed" ;
setMode ( initialMode ) ;
cards . forEach ( ( card ) => {
card . addEventListener ( "click" , ( ) => setMode ( card . dataset . value ) ) ;
card . addEventListener ( "keydown" , ( e ) => {
if ( e . key === " " || e . key === "Enter" ) { e . preventDefault ( ) ; setMode ( card . dataset . value ) ; }
} ) ;
} ) ;
// -----------------------------------------------------------
// Output-format pickers (shared widget from iconHelpers).
// Hidden <select>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: ${ n ode. movementMode } ` ) ;
const modeEl = document . getElementById ( "node-input-movementMode" ) ;
node . movementMode = ( modeEl && m odeEl . value ) ? modeEl . value : "staticspeed" ;
}
} ) ;
< / script >
@@ -128,65 +300,276 @@
<!-- Main UI Template -->
< script type = "text/html" data-template-name = "rotatingMachine" >
<!-- Machine - specific controls -- >
< div class = "form-row" >
< label for = "node-input-speed" > < i class = "fa fa-clock-o" > < / i > R e a c t i o n S p e e d < / l a b e l >
< input type = "number" id = "node-input-speed" style = " wid th:60%;" placeholder = "position units / second" / >
< div style = "font-size:11px;color:#666;margin-left:160px;" > Ramp rate of the controller position in units per second ( 0 – 100 % controller range ; e . g . 1 = 1 % / s ) . < / d i v >
< / d i v >
< div class = "form-row" >
< label for = "node-input-startup" > < i class = "fa fa-clock-o" > < / i > S t a r t u p T i m e < / l a b e l >
< input type = "number" id = "node-input-startup" style = "width:60%;" placeholder = "seconds" / >
< div style = "font-size:11px;color:#666;margin-left:160px;" > Seconds spent in the < code > starting < / c o d e > s t a t e b e f o r e m o v i n g t o < c o d e > w a r m i n g u p < / c o d e > . < / d i v >
< / d i v >
< div class = "form-row" >
< label for = "node-input-warmup" > < i class = "fa fa-clock-o" > < / i > W a r m u p T i m e < / l a b e l >
< input type = "number" id = "node-input-warmup" style = "width:60%;" placeholder = "seconds" / >
< div style = "font-size:11px;color:#666;margin-left:160px;" > Seconds spent in the protected < code > warmingup < / c o d e > s t a t e b e f o r e r e a c h i n g < c o d e > o p e r a t i o n a l < / c o d e > . < / d i v >
< / d i v >
< div class = "form-row" >
< label for = "node-input-shutdown" > < i class = "fa fa-clock-o" > < / i > S h u t d o w n T i m e < / l a b e l >
< input type = "number" id = "node-input-shutdown" style = "width:60%;" placeholder = "seconds" / >
< div style = "font-size:11px;color:#666;margin-left:160px;" > Seconds spent in the < code > stopping < / c o d e > s t a t e b e f o r e m o v i n g t o < c o d e > c o o l i n g d o w n < / c o d e > . < / d i v >
< / d i v >
< div class = "form-row" >
< label for = "node-input-cooldown" > < i class = "fa fa-clock-o" > < / i > C o o l d o w n T i m e < / l a b e l >
< input type = "number " id = "node-input-cooldown " styl e = "width:60%;" placeholder = "seconds " / >
< div style = "font-size:11px;color:#666;margin-left:160px;" > Seconds spent in the protected < code > coolingdown < / c o d e > s t a t e b e f o r e r e t u r n i n g t o < c o d e > i d l e < / c o d e > . < / d i v >
< / d i v >
< div class = "form-row" >
< label for = "node-input-movementMode" > < i class = "fa fa-exchange" > < / i > M o v e m e n t M o d e < / l a b e l >
< sel ect id = "node-input-movementMode " styl e = "width:60%;" >
< option value = "staticspee d"> Static < / o p ti o n >
< option value = "dynspeed" > Dynamic < / o p t i o n >
< / s e l e c t >
<!-- === === === === === === === === === === === === === === === === === === === === -- >
<!-- PUMP / ROTATING MACHINE BANNER -- >
<!-- Visual orientation only — no inputs . Shows what the node -- >
<!-- represents ( centrifugal pump with suction + discharge ) . -- >
<!-- === === === === === === === === === === === === === === === === === === === === -- >
< div style = "margin: 4px 0 14px 0; background: #fafcff; border: 1px solid #d9e6f2; border-radius: 4px; padding: 8px;" >
< svg xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 600 200"
style = "display:block;width:100%;"
font - family = "Arial,sans-serif" font - size = "11" >
< defs >
< marker id = "rm-arrow-flow" viewBox = "0 0 10 10" refX = "9" refY = "5" markerWidth = "8" markerHeight = "8" orient = "auto-start-reverse" >
< path d = "M 0 0 L 10 5 L 0 10 z" fill = "#1F4E79" / >
< / m a r k e r >
< marker id = "rm-arrow-rot" viewBox = "0 0 10 10" refX = "6" refY = "5" markerWidth = "6" markerHeight = "6" orient = "auto-start-reverse" >
< path d = "M 0 0 L 10 5 L 0 10 z" fill = "#0c99d9" / >
< / m a r k e r >
< / d e f s >
<!-- Title -- >
< text x = "300" y = "18" text - anchor = "middle" fill = "#1F4E79" font - size = "13" font - weight = "bold" > Rotating machine — pump / compressor / blower < / t e x t >
<!-- Suction pipe ( left → in ) -- >
< rect x = "20" y = "100" width = "160" height = "38" fill = "#dde7f0" stroke = "#1F4E79" stroke - width = "2" / >
< l ine x1 = "40" y1 = "119" x2 = "170 " y2 = "119 " strok e = "#1F4E79" stroke - width = "2" marker - end = "url(#rm-arrow-flow) " / >
< text x = "100" y = "92" text - anchor = "middle" fill = "#1F4E79" font - weight = "bold" > Suction < / t e x t >
< text x = "100" y = "156" text - anchor = "middle" fill = "#777" font - size = "10" font - style = "italic" > upstream / inlet pressure < / t e x t >
<!-- Motor housing ( top ) + shaft -- >
< r ect x = "220" y = "30" width = "44" height = "40" rx = "3" fill = "#7f8c8d " strok e = "#333" stroke - width = "1.5" / >
< text x = "242" y = "55" text - anchor = "middle" fill = "#fff" font - size = "13" font - weight = "bol d"> M < / t e x t >
< line x1 = "242" y1 = "70" x2 = "242" y2 = "90" stroke = "#333" stroke - width = "2" / >
< text x = "295" y = "50" fill = "#555" font - size = "10" font - style = "italic" > motor / drive < / t e x t >
<!-- Volute ( pump body ) -- >
< circle cx = "242" cy = "119" r = "40" fill = "#fff" stroke = "#333" stroke - width = "2" / >
<!-- Impeller curves ( decorative ) -- >
< path d = "M 242 95 Q 268 105 268 119 Q 268 133 242 143 Q 216 133 216 119 Q 216 105 242 95" fill = "none" stroke = "#86bbdd" stroke - width = "1.5" / >
< path d = "M 234 100 Q 258 110 258 119 Q 258 128 234 138" fill = "none" stroke = "#a9daee" stroke - width = "1" / >
<!-- Rotation arrow inside volute -- >
< path d = "M 222 109 A 22 22 0 0 1 262 109" fill = "none" stroke = "#0c99d9" stroke - width = "2" marker - end = "url(#rm-arrow-rot)" / >
< text x = "242" y = "175" text - anchor = "middle" fill = "#333" font - size = "10" > impeller < / t e x t >
<!-- Discharge pipe ( right → out ) -- >
< rect x = "304" y = "100" width = "160" height = "38" fill = "#dde7f0" stroke = "#1F4E79" stroke - width = "2" / >
< line x1 = "314" y1 = "119" x2 = "454" y2 = "119" stroke = "#1F4E79" stroke - width = "2" marker - end = "url(#rm-arrow-flow)" / >
< text x = "384" y = "92" text - anchor = "middle" fill = "#1F4E79" font - weight = "bold" > Discharge < / t e x t >
< text x = "384" y = "156" text - anchor = "middle" fill = "#777" font - size = "10" font - style = "italic" > downstream / outlet pressure < / t e x t >
<!-- Hint band right -- >
< text x = "484" y = "92" fill = "#1E8449" font - size = "11" font - weight = "bold" > → flow Q < / t e x t >
< text x = "484" y = "108" fill = "#1E8449" font - size = "10" font - style = "italic" > m³ / h ( configurable ) < / t e x t >
< text x = "484" y = "130" fill = "#C0392B" font - size = "11" font - weight = "bold" > ↑ Δp head < / t e x t >
< text x = "484" y = "146" fill = "#C0392B" font - size = "10" font - style = "italic" > predicted from curve < / t e x t >
<!-- Hint footer -- >
< text x = "300" y = "194" text - anchor = "middle" fill = "#777" font - size = "10" font - style = "italic" >
Flow direction → Pressure rises across the impeller Performance follows the Q - H / Q - P curves of the selected asset
< / t e x t >
< / s v g >
< / d i v >
<!-- === === === === === === === === === === === === === === === === === === === === -- >
<!-- SEQUENCE & REACTION TIMING -- >
<!-- Side - panel inputs hover - coupled to a timeline of FSM phases . -- >
<!-- Bar widths grow with the entered seconds . Protected phases -- >
<!-- ( warmingup / coolingdown ) carry a 🛡 marker . The reaction - -- >
<!-- speed value tilts the slope inside the operational bar . -- >
<!-- === === === === === === === === === === === === === === === === === === === === -- >
< h4 > Sequence & amp ; reaction timing < / h 4 >
< p style = "font-size:12px;color:#777;margin:0 0 6px 0;" > Each timing input on the left sizes its phase on the timeline . < b > 🛡 protected < / b > p h a s e s ( w a r m - u p & a m p ; c o o l - d o w n ) c a n n o t b e a b o r t e d b y a n e w c o m m a n d . H o v e r a n i n p u t r o w t o h i g h l i g h t t h e p h a s e i t c o n t r o l s . < / p >
< style >
. rm - diag { display : flex ; gap : 20 px ; align - items : flex - start ; margin : 0 0 14 px 0 ; }
. rm - diag - side { width : 230 px ; flex : 0 0 230 px ; display : flex ; flex - direction : column ; gap : 6 px ; }
/* SVG height is set at runtime by syncSvgHeight() in oneditprepare to
match the side-panel's computed height exactly. Width follows the
viewBox aspect ratio. The hard-coded fallback height covers the brief
window before the first sync runs. */
. rm - diag - svg { height : 195 px ; width : auto ; max - width : 100 % ; display : block ; }
. rm - diag - side . rm - row {
display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) 70 px 18 px ; align - items : center ;
gap : 6 px ; padding : 4 px 6 px 4 px 10 px ; border - left : 4 px solid # ccc ;
background : # fafafa ; border - radius : 3 px ; font - size : 11 px ; cursor : pointer ; min - width : 0 ;
}
. rm - diag - side . rm - row : hover { background : # f0f0f0 ; }
. rm - diag - side . rm - row label { font - weight : 600 ; margin : 0 ; line - height : 1.2 ; }
. rm - diag - side . rm - row . rm - sub { grid - column : 1 ; font - size : 10 px ; color : # 888 ; font - weight : 400 ; }
. rm - diag - side . rm - row input [ type = number ] {
width : 100 % ; height : 22 px ; box - sizing : border - box ; font - size : 11 px ;
padding : 1 px 4 px ; margin : 0 ; border : 1 px solid # ccc ; border - radius : 3 px ; background : # fff ;
}
. rm - diag - side . rm - row input [ type = number ] : focus { outline : 1 px solid # 0 c99d9 ; border - color : # 0 c99d9 ; }
. rm - diag - side . rm - row . rm - unit { color : # 888 ; font - size : 10 px ; text - align : right ; }
/* Border colours matched to arc fills. */
. rm - row [ data - stroke = "#0c99d9" ] { border - left - color : # 0 c99d9 ; }
. rm - row [ data - stroke = "#f39c12" ] { border - left - color : # f39c12 ; }
. rm - row [ data - stroke = "#e67e22" ] { border - left - color : # e67e22 ; }
. rm - row [ data - stroke = "#0c99d9" ] label { color : # 0 c99d9 ; }
. rm - row [ data - stroke = "#f39c12" ] label { color : # b9770e ; }
. rm - row [ data - stroke = "#e67e22" ] label { color : # af601a ; }
/* Highlight class applied to a state's arc path on input-row hover. */
. rm - arc - highlight { stroke : # 1 F4E79 ! important ; stroke - width : 3 ! important ; filter : brightness ( 1.08 ) ; }
/* Movement-mode cards — same compact 94× 86 sizing as machineGroupControl. */
. rm - mode - cards { display : flex ; gap : 6 px ; flex - wrap : wrap ; margin : 6 px 0 4 px 0 ; }
. rm - mode - card {
width : 94 px ; height : 86 px ; box - sizing : border - box ;
border : 2 px solid # d0d0d0 ; border - radius : 4 px ; background : # fafafa ;
padding : 4 px ; cursor : pointer ; user - select : none ;
display : flex ; flex - direction : column ; align - items : center ; justify - content : center ; gap : 2 px ;
transition : border - color 80 ms ease - out , background 80 ms ease - out ;
}
. rm - mode - card : hover { border - color : # 86 bbdd ; background : # f5fafd ; }
. rm - mode - card : focus { outline : 2 px solid # 1 F4E79 ; outline - offset : 2 px ; }
. rm - mode - card - on { border - color : # 50 a8d9 ; background : # eaf4fb ; }
. rm - mode - card - svg { width : 100 % ; height : 54 px ; display : flex ; align - items : center ; justify - content : center ; }
. rm - mode - card - svg svg { width : 100 % ; height : 100 % ; display : block ; }
. rm - mode - card - label { font - size : 10 px ; line - height : 1 ; font - weight : 600 ; color : # 333 ; white - space : nowrap ; letter - spacing : 0 ; }
. rm - mode - card : not ( . rm - mode - card - on ) . rm - mode - card - label { color : # 888 ; }
/* Output-format rows mirror the mgc layout: nowrap label, native select
hidden, icon picker rendered alongside by iconHelpers. */
. rm - output - row > label { white - space : nowrap ; width : 130 px ; }
< / s t y l e >
< div class = "rm-diag" >
<!-- LEFT : stacked colour - coded inputs . Hover a row → matching SVG bar highlights . -- >
< div class = "rm-diag-side" >
< div class = "rm-row" data - stroke = "#0c99d9" data - couples = "rm-tl-operational" >
< div > < label > Reaction speed < / l a b e l > < d i v c l a s s = " r m - s u b " > c o n t r o l l e r r a m p r a t e ( s l o p e i n s i d e o p e r a t i o n a l ) < / d i v > < / d i v >
< input type = "number" id = "node-input-speed" min = "0.1" step = "0.1" / >
< span class = "rm-unit" > % / s < / s p a n >
< / d i v >
< div class = "rm-row" data - stroke = "#f39c12" data - couples = "rm-tl-starting" >
< div > < label > Startup time < / l a b e l > < d i v c l a s s = " r m - s u b " > i d l e → s t a r t i n g → w a r m i n g u p < / d i v > < / d i v >
< input type = "number" id = "node-input-startup" min = "0" step = "1" / >
< span class = "rm-unit" > s < / s p a n >
< / d i v >
< div class = "rm-row" data - stroke = "#e67e22" data - couples = "rm-tl-warmingup" >
< div > < label > Warm - up time 🛡 ︎ < / l a b e l > < d i v c l a s s = " r m - s u b " > p r o t e c t e d — c a n n o t b e a b o r t e d < / d i v > < / d i v >
< input type = "number" id = "node-input-warmup" min = "0" step = "1" / >
< span class = "rm-unit" > s < / s p a n >
< / d i v >
< div class = "rm-row" data - stroke = "#f39c12" data - couples = "rm-tl-stopping" >
< div > < label > Shutdown time < / l a b e l > < d i v c l a s s = " r m - s u b " > o p e r a t i o n a l → s t o p p i n g → c o o l i n g d o w n < / d i v > < / d i v >
< input type = "number" id = "node-input-shutdown" min = "0" step = "1" / >
< span class = "rm-unit" > s < / s p a n >
< / d i v >
< div class = "rm-row" data - stroke = "#e67e22" data - couples = "rm-tl-coolingdown" >
< div > < label > Cool - down time 🛡 ︎ < / l a b e l > < d i v c l a s s = " r m - s u b " > p r o t e c t e d — c a n n o t b e a b o r t e d < / d i v > < / d i v >
< input type = "number" id = "node-input-cooldown" min = "0" step = "1" / >
< span class = "rm-unit" > s < / s p a n >
< / d i v >
< / d i v >
<!-- RIGHT : circular state - machine donut . All arc ` d ` and label x / y
values are written by redrawTimeline ( ) . Each state is a wedge of
the ring ; arc angle is proportional to its seconds .
Idle sits at the top ( small fixed slice , the loop - around ) ;
operational sits at the bottom ( fixed dominant arc ) . -- >
< svg id = "rm-timeline" class = "rm-diag-svg" xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 340 260"
style = "background:#fff;border:1px solid #e5e5e5;border-radius:4px;"
preserveAspectRatio = "xMidYMid meet"
font - family = "Arial,sans-serif" font - size = "11" >
<!-- Title -- >
< text x = "170" y = "14" text - anchor = "middle" fill = "#1F4E79" font - size = "11" font - weight = "bold" > State machine — sequence loop < / t e x t >
<!-- State arc wedges . Order in DOM = clockwise from top .
` d ` attribute populated by redrawTimeline ( ) . -- >
< path id = "rm-tl-idle" fill = "#bdc3c7" stroke = "#7f8c8d" stroke - width = "1" / >
< path id = "rm-tl-starting" fill = "#f39c12" stroke = "#b9770e" stroke - width = "1" / >
< path id = "rm-tl-warmingup" fill = "#e67e22" stroke = "#af601a" stroke - width = "1" / >
< path id = "rm-tl-operational" fill = "#2ecc71" stroke = "#239b56" stroke - width = "1" / >
< path id = "rm-tl-stopping" fill = "#f39c12" stroke = "#b9770e" stroke - width = "1" / >
< path id = "rm-tl-coolingdown" fill = "#e67e22" stroke = "#af601a" stroke - width = "1" / >
<!-- State - name labels OUTSIDE the ring . x / y / text - anchor / dy set in JS . -- >
< text id = "rm-tl-lbl-idle" fill = "#555" font - size = "11" font - weight = "bold" > < / t e x t >
< text id = "rm-tl-lbl-starting" fill = "#b9770e" font - size = "11" font - weight = "bold" > < / t e x t >
< text id = "rm-tl-lbl-warmingup" fill = "#af601a" font - size = "11" font - weight = "bold" > < / t e x t >
< text id = "rm-tl-lbl-operational" fill = "#239b56" font - size = "11" font - weight = "bold" > < / t e x t >
< text id = "rm-tl-lbl-stopping" fill = "#b9770e" font - size = "11" font - weight = "bold" > < / t e x t >
< text id = "rm-tl-lbl-coolingdown" fill = "#af601a" font - size = "11" font - weight = "bold" > < / t e x t >
<!-- Duration values INSIDE each arc . x / y set in JS . -- >
< text id = "rm-tl-time-idle" fill = "#fff" font - size = "10" font - weight = "bold" > < / t e x t >
< text id = "rm-tl-time-starting" fill = "#fff" font - size = "10" font - weight = "bold" > < / t e x t >
< text id = "rm-tl-time-warmingup" fill = "#fff" font - size = "10" font - weight = "bold" > < / t e x t >
< text id = "rm-tl-time-operational" fill = "#fff" font - size = "10" font - weight = "bold" > < / t e x t >
< text id = "rm-tl-time-stopping" fill = "#fff" font - size = "10" font - weight = "bold" > < / t e x t >
< text id = "rm-tl-time-coolingdown" fill = "#fff" font - size = "10" font - weight = "bold" > < / t e x t >
<!-- Centre : reaction - speed value ( no slope line — donut hole stays clean ) . -- >
< text x = "170" y = "125" text - anchor = "middle" fill = "#1F4E79" font - size = "10" font - weight = "bold" > Reaction speed < / t e x t >
< text id = "rm-tl-ramp-value" x = "170" y = "146" text - anchor = "middle" fill = "#0c99d9" font - size = "16" font - weight = "bold" > 1 % / s < / t e x t >
< / s v g >
< / d i v >
<!-- === === === === === === === === === === === === === === === === === === === === -- >
<!-- MOVEMENT MODE — visual cards ( was a < select > ) -- >
<!-- Hidden # node - input - movementMode keeps the save path working . -- >
<!-- === === === === === === === === === === === === === === === === === === === === -- >
< h4 > Movement mode < / h 4 >
< p style = "font-size:12px;color:#777;margin:0 0 6px 0;" > How the controller travels between setpoints during < code > accelerating < /code> / < code > decelerating < / c o d e > . < / p >
< div class = "rm-mode-cards" role = "radiogroup" aria - label = "Movement mode" >
< div class = "rm-mode-card" data - value = "staticspeed" tabindex = "0" role = "radio" aria - checked = "false" aria - label = "Static — constant ramp rate" title = "Static — constant ramp rate" >
< div class = "rm-mode-card-svg" >
< svg viewBox = "0 0 80 58" xmlns = "http://www.w3.org/2000/svg" aria - hidden = "true" >
< line x1 = "12" y1 = "48" x2 = "70" y2 = "48" stroke = "#888" stroke - width = "1.4" stroke - linecap = "round" / >
< line x1 = "12" y1 = "48" x2 = "12" y2 = "8" stroke = "#888" stroke - width = "1.4" stroke - linecap = "round" / >
< line x1 = "14" y1 = "46" x2 = "68" y2 = "12" stroke = "#1F4E79" stroke - width = "3" stroke - linecap = "round" / >
< circle cx = "14" cy = "46" r = "2.6" fill = "#1F4E79" / >
< circle cx = "68" cy = "12" r = "2.6" fill = "#1F4E79" / >
< / s v g >
< / d i v >
< div class = "rm-mode-card-label" > Static < / d i v >
< / d i v >
< div class = "rm-mode-card" data - value = "dynspeed" tabindex = "0" role = "radio" aria - checked = "false" aria - label = "Dynamic — ease in/out" title = "Dynamic — ease in/out" >
< div class = "rm-mode-card-svg" >
< svg viewBox = "0 0 80 58" xmlns = "http://www.w3.org/2000/svg" aria - hidden = "true" >
< line x1 = "12" y1 = "48" x2 = "70" y2 = "48" stroke = "#888" stroke - width = "1.4" stroke - linecap = "round" / >
< line x1 = "12" y1 = "48" x2 = "12" y2 = "8" stroke = "#888" stroke - width = "1.4" stroke - linecap = "round" / >
<!-- More pronounced sigmoid : control points pull the mid - section nearly flat
( y ≈ 29 mid ) so the S - shape reads clearly at thumbnail size . -- >
< path d = "M 14 46 C 22 46, 26 30, 41 29 C 56 28, 60 12, 68 12" fill = "none" stroke = "#1F4E79" stroke - width = "3" stroke - linecap = "round" / >
< circle cx = "14" cy = "46" r = "2.6" fill = "#1F4E79" / >
< circle cx = "68" cy = "12" r = "2.6" fill = "#1F4E79" / >
< / s v g >
< / d i v >
< div class = "rm-mode-card-label" > Dynamic < / d i v >
< / d i v >
< / d i v >
<!-- Hidden field — kept for the save path , written by the cards above . -- >
< input type = "hidden" id = "node-input-movementMode" / >
<!-- === === === === === === === === === === === === === === === === === === === === -- >
<!-- OUTPUT FORMATS — same shared widget as machineGroupControl . -- >
<!-- Native selects stay in the DOM ( hidden ) as save targets ; the -- >
<!-- icon - picker divs are upgraded by iconHelpers . -- >
<!-- === === === === === === === === === === === === === === === === === === === === -- >
< h3 > Output Formats < / h 3 >
< div class = "form-row" >
< div class = "form-row rm-output-row " >
< label for = "node-input-processOutputFormat" > < i class = "fa fa-random" > < / i > P r o c e s s O u t p u t < / l a b e l >
< select id = "node-input-processOutputFormat" style = "width:60%;" >
< select id = "node-input-processOutputFormat" class = "evolv-native-hidden" style = "width:60%;" >
< option value = "process" > process < / o p t i o n >
< option value = "json" > json < / o p t i o n >
< option value = "csv" > csv < / o p t i o n >
< / s e l e c t >
< div id = "rm-process-output-picker" class = "evolv-icon-picker"
role = "radiogroup" aria - label = "Process output format" > < / d i v >
< / d i v >
< div class = "form-row" >
< div class = "form-row rm-output-row " >
< label for = "node-input-dbaseOutputFormat" > < i class = "fa fa-database" > < / i > D a t a b a s e O u t p u t < / l a b e l >
< select id = "node-input-dbaseOutputFormat" style = "width:60%;" >
< select id = "node-input-dbaseOutputFormat" class = "evolv-native-hidden" style = "width:60%;" >
< option value = "influxdb" > influxdb < / o p t i o n >
< option value = "frost" > frost < / o p t i o n >
< option value = "json" > json < / o p t i o n >
< option value = "csv" > csv < / o p t i o n >
< / s e l e c t >
< div id = "rm-dbase-output-picker" class = "evolv-icon-picker"
role = "radiogroup" aria - label = "Database output format" > < / d i v >
< / d i v >
<!-- Asset fields injected here -- >
<!-- Asset / Logger / Position menus injected by menu . js -- >
< div id = "asset-fields-placeholder" > < / d i v >
<!-- Logger fields injected here -- >
< div id = "logger-fields-placeholder" > < / d i v >
<!-- Position fields injected here -- >
< div id = "position-fields-placeholder" > < / d i v >
< / script >
@@ -196,11 +579,11 @@
< h3 > Configuration < / h 3 >
< ul >
< li > < b > Reaction S peed< /b>: controller ramp rate (position units / second ) . E . g . < code > 1 < / c o d e > = 1 % / s , s o S e t 6 0 % f r o m i d l e r e a c h e s 6 0 % i n ~ 6 0 & n b s p ; s . < / l i >
< li > < b > Startup / Warmup / Shutdown / Cooldown < /b>: seconds per FSM phase. Warmup and Cooldown are <i>protected</i > — they cannot be aborted by a new command . </ l i >
< li > < b > Movement M ode< / b > : < c o d e > s t a t i c s p e e d < / c o d e > = l i n e a r r a m p ; < c o d e > d y n s p e e d < / c o d e > = e a s e - i n / o u t . < / l i >
< li > < b > Reaction s peed< /b>: controller ramp rate (position units / second ) . E . g . < code > 1 < / c o d e > = 1 % / s , s o a s e t p o i n t o f 6 0 % f r o m i d l e r e a c h e s 6 0 % i n ~ 6 0 & n b s p ; s . V i s u a l i s e d a s t h e s l o p e i n s i d e t h e < i > o p e r a t i o n a l < / i > b a r . </ l i >
< li > < b > Startup / Warm- up / Shutdown / Cool- down < / b > : s e c o n d s p e r F S M p h a s e . W a r m - u p & a m p ; c o o l - d o w n a r e < b > p r o t e c t e d < / b > — t h e y c a n n o t b e a b o r t e d b y a n e w c o m m a n d ( s h o w n w i t h 🛡 i n t h e t i m e l i n e ) . </ l i >
< li > < b > Movement m ode< / b > : < c o d e > s t a t i c s p e e d < / c o d e > = l i n e a r r a m p ; < c o d e > d y n s p e e d < / c o d e > = e a s e - i n / o u t . P i c k a c a r d . </ l i >
< li > < b > Asset < / b > ( m e n u ) : s u p p l i e r , c a t e g o r y , m o d e l ( m u s t m a t c h a c u r v e i n < c o d e > g e n e r a l F u n c t i o n s < / c o d e > ) , f l o w u n i t ( e . g . m ³ / h ) , c u r v e u n i t s . < / l i >
< li > < b > Output F ormats< / b > : < c o d e > p r o c e s s < / c o d e > / < c o d e > j s o n < / c o d e > / < c o d e > c s v < / c o d e > o n p o r t 0 ; < c o d e > i n f l u x d b < / c o d e > / < c o d e > j s o n < / c o d e > / < c o d e > c s v < / c o d e > o n p o r t 1 . < / l i >
< li > < b > Output f ormats< / b > : < c o d e > p r o c e s s < / c o d e > / < c o d e > j s o n < / c o d e > / < c o d e > c s v < / c o d e > o n p o r t 0 ; < c o d e > i n f l u x d b < / c o d e > / < c o d e > j s o n < / c o d e > / < c o d e > c s v < / c o d e > o n p o r t 1 . < / l i >
< li > < b > Position < / b > ( m e n u ) : < c o d e > u p s t r e a m < / c o d e > / < c o d e > a t E q u i p m e n t < / c o d e > / < c o d e > d o w n s t r e a m < / c o d e > r e l a t i v e t o a p a r e n t g r o u p / s t a t i o n . < / l i >
< / u l >