Loading noctua/web/pages/macros/widgets.html +1 −1 Original line number Diff line number Diff line Loading @@ -88,7 +88,7 @@ <small class="col-md-4 d-flex gap-3 mt-1 small text-muted"> <div class="form-check form-check-inline m-0"> <input class="form-check-input force-cb" type="checkbox" id="force-{{ safe_id }}"> <label class="form-check-label" for="force-{{ safe_id }}">Force</label> <label class="form-check-label" for="force-{{ safe_id }}">Ignore deps</label> </div> <div class="form-check form-check-inline m-0"> <input class="form-check-input raw-cb" type="checkbox" id="raw-{{ safe_id }}" data-target="{{ safe_id }}"> Loading noctua/web/static/img/synoptic.svg +7 −7 Original line number Diff line number Diff line Loading @@ -23,9 +23,9 @@ inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="2.7930718" inkscape:cx="129.06936" inkscape:cy="133.90275" inkscape:zoom="3.95" inkscape:cx="200.37975" inkscape:cy="213.16456" inkscape:window-width="2511" inkscape:window-height="1371" inkscape:window-x="26" Loading Loading @@ -659,19 +659,19 @@ id="beams" inkscape:label="beams"><path id="beam-photometry" style="fill:none;stroke:#ff6600;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" style="fill:none;stroke:#37c8ab;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 202.22718,216.19623 43.3783,43.92979 m 23.12946,22.95946 7.91835,9.30074 m 37.69479,10.90406 34.60451,-0.0996 m -75.69139,-15.59218 24.38124,-24.04571 57.62634,0.0633" sodipodi:nodetypes="ccccccccc" /><path id="beam-spectroscopy" style="display:inline;fill:none;stroke:#b3b3b3;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" style="display:inline;fill:none;stroke:#37c8ab;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 200.55953,216.46393 -84.48558,84.72949 h -66.3242 m 103.4728,-37.30999 -45.85232,3e-5 -67.852326,10e-6" sodipodi:nodetypes="cccccc" /><path id="beam-echelle" style="display:inline;fill:none;stroke:#b3b3b3;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" style="display:inline;fill:none;stroke:#37c8ab;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 203.17512,214.14623 84.48557,-84.72949 66.3242,-2e-5 m -103.4728,37.31002 45.85233,-3e-5 47.85234,-1e-5" sodipodi:nodetypes="cccccc" /><path id="beam-incoming" style="fill:none;stroke:#ff6600;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" style="fill:none;stroke:#37c8ab;stroke-width:1px" d="m 200.55953,215.34038 -60.48559,-62.72949 h -34.3242" sodipodi:nodetypes="ccc" /></g><path id="telemetry:dome:light:indicator" Loading noctua/web/static/js/synoptic.js +108 −14 Original line number Diff line number Diff line Loading @@ -75,6 +75,57 @@ function initializeGeometry() { } } /** * Update the colors of the optical beams based on the stage's named position. * Uses 'stroke' coloring for lines. * * Parameters * ---------- * namedPosition : string or null * The name of the current stage position (e.g., 'imaging', 'spectro'). * If null or empty, all path beams turn grey. * * Returns * ------- * void */ function animateBeams(namedPosition) { // SVG Elements const beamIn = document.getElementById('structure-beam-incoming'); const beamImg = document.getElementById('structure-beam-imaging'); const beamSpc = document.getElementById('structure-beam-spectro'); const beamEch = document.getElementById('structure-beam-echelle'); // Define colors for the stroke const COLOR_OFF = "rgba(128, 128, 128, 0.3)"; // Grey, off const COLOR_ON = "rgba(255, 165, 0, 0.8)"; // Orange, illuminated // Optional: Incoming beam is always ON (as per your request) if (beamIn) applyFillForcefully(beamIn, COLOR_ON, 'stroke'); // 1. Reset all destination beams to OFF (targeting 'stroke') if (beamImg) applyFillForcefully(beamImg, COLOR_OFF, 'stroke'); if (beamSpc) applyFillForcefully(beamSpc, COLOR_OFF, 'stroke'); if (beamEch) applyFillForcefully(beamEch, COLOR_OFF, 'stroke'); // 2. Turn ON the active beam if it matches a named position let activeBeam = null; if (namedPosition === 'imaging') { activeBeam = beamImg; } else if (namedPosition === 'spectro') { activeBeam = beamSpc; } else if (namedPosition === 'echelle') { activeBeam = beamEch; } // Apply the ON color to the selected stroke if (activeBeam) { applyFillForcefully(activeBeam, COLOR_ON, 'stroke'); } } /** * Rotate the dome graphic using CSS to preserve Inkscape base transforms. Loading Loading @@ -240,21 +291,58 @@ function animateStage(posMm) { // Translate only on X axis mirrorEl.style.transform = `translate(${deltaX}px, 0px)`; } /** * Forcefully apply fill color to an element and its inline styles. * Forcefully apply color to an element and all its children. * Erases existing inline styles of the specified type to guarantee the change. * * Parameters * ---------- * el : SVGElement * The DOM element to colorize. * color : string * The CSS color (e.g. 'rgba(0,255,0,1)' or 'red'). * type : string, optional * 'fill' or 'stroke'. Defaults to 'fill'. * * Returns * ------- * void */ function applyFillForcefully(el, color) { el.setAttribute('fill', color); function applyFillForcefully(el, color, type = 'fill') { if (!el) return; // A helper function to apply the forceful styling to a single node const forceNode = (node) => { // Apply directly as an SVG attribute node.setAttribute(type, color); let style = el.getAttribute('style') || ''; if (style.match(/fill\s*:/i)) { // Replace existing fill with the new color and force !important style = style.replace(/fill\s*:[^;]+;?/gi, `fill: ${color} !important;`); // Parse the existing inline style string let style = node.getAttribute('style') || ''; // Dynamic Regex to match either 'fill' or 'stroke' const regex = new RegExp(`${type}\\s*:[^;]+;?`, 'gi'); if (style.match(regex)) { // Replace existing rule with the new color and force !important style = style.replace(regex, `${type}: ${color} !important;`); } else { // Append fill if it doesn't exist style += `; fill: ${color} !important;`; // Append rule if it doesn't exist style += `; ${type}: ${color} !important;`; } el.setAttribute('style', style); node.setAttribute('style', style); }; // 1. Apply to the main element itself forceNode(el); // 2. Apply to all graphic children (Crucial if the ID is on an Inkscape <g> Group) const children = el.querySelectorAll('path, rect, circle, ellipse, polygon, line, polyline'); children.forEach(child => { forceNode(child); }); } /** Loading Loading @@ -359,7 +447,6 @@ document.addEventListener('noctua-telemetry', (e) => { } }); // ---- B. Animate Graphics ---- if (subsystem === 'dome') { Loading Loading @@ -389,10 +476,17 @@ document.addEventListener('noctua-telemetry', (e) => { } if (subsystem === 'stage') { // Translation of the mirror if (data.position && data.position.response !== undefined) { const stageMm = data.position.response; if (typeof stageMm === 'number') animateStage(stageMm); } // Colorization of the beams if (data.named && data.named.response !== undefined) { // response could be 'imaging', 'spectro', 'echelle', or null/error animateBeams(data.named.response); } } }); Loading
noctua/web/pages/macros/widgets.html +1 −1 Original line number Diff line number Diff line Loading @@ -88,7 +88,7 @@ <small class="col-md-4 d-flex gap-3 mt-1 small text-muted"> <div class="form-check form-check-inline m-0"> <input class="form-check-input force-cb" type="checkbox" id="force-{{ safe_id }}"> <label class="form-check-label" for="force-{{ safe_id }}">Force</label> <label class="form-check-label" for="force-{{ safe_id }}">Ignore deps</label> </div> <div class="form-check form-check-inline m-0"> <input class="form-check-input raw-cb" type="checkbox" id="raw-{{ safe_id }}" data-target="{{ safe_id }}"> Loading
noctua/web/static/img/synoptic.svg +7 −7 Original line number Diff line number Diff line Loading @@ -23,9 +23,9 @@ inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="2.7930718" inkscape:cx="129.06936" inkscape:cy="133.90275" inkscape:zoom="3.95" inkscape:cx="200.37975" inkscape:cy="213.16456" inkscape:window-width="2511" inkscape:window-height="1371" inkscape:window-x="26" Loading Loading @@ -659,19 +659,19 @@ id="beams" inkscape:label="beams"><path id="beam-photometry" style="fill:none;stroke:#ff6600;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" style="fill:none;stroke:#37c8ab;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 202.22718,216.19623 43.3783,43.92979 m 23.12946,22.95946 7.91835,9.30074 m 37.69479,10.90406 34.60451,-0.0996 m -75.69139,-15.59218 24.38124,-24.04571 57.62634,0.0633" sodipodi:nodetypes="ccccccccc" /><path id="beam-spectroscopy" style="display:inline;fill:none;stroke:#b3b3b3;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" style="display:inline;fill:none;stroke:#37c8ab;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 200.55953,216.46393 -84.48558,84.72949 h -66.3242 m 103.4728,-37.30999 -45.85232,3e-5 -67.852326,10e-6" sodipodi:nodetypes="cccccc" /><path id="beam-echelle" style="display:inline;fill:none;stroke:#b3b3b3;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" style="display:inline;fill:none;stroke:#37c8ab;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 203.17512,214.14623 84.48557,-84.72949 66.3242,-2e-5 m -103.4728,37.31002 45.85233,-3e-5 47.85234,-1e-5" sodipodi:nodetypes="cccccc" /><path id="beam-incoming" style="fill:none;stroke:#ff6600;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" style="fill:none;stroke:#37c8ab;stroke-width:1px" d="m 200.55953,215.34038 -60.48559,-62.72949 h -34.3242" sodipodi:nodetypes="ccc" /></g><path id="telemetry:dome:light:indicator" Loading
noctua/web/static/js/synoptic.js +108 −14 Original line number Diff line number Diff line Loading @@ -75,6 +75,57 @@ function initializeGeometry() { } } /** * Update the colors of the optical beams based on the stage's named position. * Uses 'stroke' coloring for lines. * * Parameters * ---------- * namedPosition : string or null * The name of the current stage position (e.g., 'imaging', 'spectro'). * If null or empty, all path beams turn grey. * * Returns * ------- * void */ function animateBeams(namedPosition) { // SVG Elements const beamIn = document.getElementById('structure-beam-incoming'); const beamImg = document.getElementById('structure-beam-imaging'); const beamSpc = document.getElementById('structure-beam-spectro'); const beamEch = document.getElementById('structure-beam-echelle'); // Define colors for the stroke const COLOR_OFF = "rgba(128, 128, 128, 0.3)"; // Grey, off const COLOR_ON = "rgba(255, 165, 0, 0.8)"; // Orange, illuminated // Optional: Incoming beam is always ON (as per your request) if (beamIn) applyFillForcefully(beamIn, COLOR_ON, 'stroke'); // 1. Reset all destination beams to OFF (targeting 'stroke') if (beamImg) applyFillForcefully(beamImg, COLOR_OFF, 'stroke'); if (beamSpc) applyFillForcefully(beamSpc, COLOR_OFF, 'stroke'); if (beamEch) applyFillForcefully(beamEch, COLOR_OFF, 'stroke'); // 2. Turn ON the active beam if it matches a named position let activeBeam = null; if (namedPosition === 'imaging') { activeBeam = beamImg; } else if (namedPosition === 'spectro') { activeBeam = beamSpc; } else if (namedPosition === 'echelle') { activeBeam = beamEch; } // Apply the ON color to the selected stroke if (activeBeam) { applyFillForcefully(activeBeam, COLOR_ON, 'stroke'); } } /** * Rotate the dome graphic using CSS to preserve Inkscape base transforms. Loading Loading @@ -240,21 +291,58 @@ function animateStage(posMm) { // Translate only on X axis mirrorEl.style.transform = `translate(${deltaX}px, 0px)`; } /** * Forcefully apply fill color to an element and its inline styles. * Forcefully apply color to an element and all its children. * Erases existing inline styles of the specified type to guarantee the change. * * Parameters * ---------- * el : SVGElement * The DOM element to colorize. * color : string * The CSS color (e.g. 'rgba(0,255,0,1)' or 'red'). * type : string, optional * 'fill' or 'stroke'. Defaults to 'fill'. * * Returns * ------- * void */ function applyFillForcefully(el, color) { el.setAttribute('fill', color); function applyFillForcefully(el, color, type = 'fill') { if (!el) return; // A helper function to apply the forceful styling to a single node const forceNode = (node) => { // Apply directly as an SVG attribute node.setAttribute(type, color); let style = el.getAttribute('style') || ''; if (style.match(/fill\s*:/i)) { // Replace existing fill with the new color and force !important style = style.replace(/fill\s*:[^;]+;?/gi, `fill: ${color} !important;`); // Parse the existing inline style string let style = node.getAttribute('style') || ''; // Dynamic Regex to match either 'fill' or 'stroke' const regex = new RegExp(`${type}\\s*:[^;]+;?`, 'gi'); if (style.match(regex)) { // Replace existing rule with the new color and force !important style = style.replace(regex, `${type}: ${color} !important;`); } else { // Append fill if it doesn't exist style += `; fill: ${color} !important;`; // Append rule if it doesn't exist style += `; ${type}: ${color} !important;`; } el.setAttribute('style', style); node.setAttribute('style', style); }; // 1. Apply to the main element itself forceNode(el); // 2. Apply to all graphic children (Crucial if the ID is on an Inkscape <g> Group) const children = el.querySelectorAll('path, rect, circle, ellipse, polygon, line, polyline'); children.forEach(child => { forceNode(child); }); } /** Loading Loading @@ -359,7 +447,6 @@ document.addEventListener('noctua-telemetry', (e) => { } }); // ---- B. Animate Graphics ---- if (subsystem === 'dome') { Loading Loading @@ -389,10 +476,17 @@ document.addEventListener('noctua-telemetry', (e) => { } if (subsystem === 'stage') { // Translation of the mirror if (data.position && data.position.response !== undefined) { const stageMm = data.position.response; if (typeof stageMm === 'number') animateStage(stageMm); } // Colorization of the beams if (data.named && data.named.response !== undefined) { // response could be 'imaging', 'spectro', 'echelle', or null/error animateBeams(data.named.response); } } });