From 5926316a253c6deb8e37dd54a9c35b160c62ef27 Mon Sep 17 00:00:00 2001 From: Henrik Muehe Date: Thu, 30 Apr 2026 22:24:55 +0200 Subject: [PATCH] Fix: real-hardware bring-up issues found at 1920x1080 on the Pi After testing the V09 redesign live on the Pi at onefinity.local (1920x1080, Chrome fullscreen) several real bugs surfaced. This commit fixes all of them. Layout fits at 1920x1080 - Cap .app-shell at 100vh height with overflow:hidden so child flex containers actually constrain to one screen. - Make .control-page / .program-page / .console-page use flex 1 1 auto + min-height 0 + overflow hidden so the page total no longer grows to ~36 000 px when the gcode-viewer is mounted. - Override clusterize.css default max-height: 200px on the .clusterize-scroll element with max-height: none + flex 1 1 0 + height 100% so the gcode listing fills the available column. E-Stop in the header - The legacy estop.pug SVG had width=130 height=130 but no viewBox, so CSS-only sizing did nothing and the SVG content spilled ~26 px off the right edge of the screen and ~70 px below the header. Add viewBox="0 0 130 130" plus preserveAspectRatio so CSS sizing actually shrinks the inner geometry. Drop the octagonal clip-path (the SVG already carries its own yellow safety ring + EMERGENCY/STOP text). 3D toolpath preview (path-viewer) - The legacy .path-viewer.small CSS clamped the canvas to 340 x 150 floated into the corner. In the new program-body grid we want it to fill the 600 px right column. Override with width 100%, height auto, float none, !important. - Make orbit.js wheel/touchstart/touchmove listeners {passive: false} so OrbitControls.preventDefault() actually works and the page no longer scrolls while panning the 3D view on a touch screen. Vue 1 template + reactivity bugs exposed by the live data - Replace v-else-if (Vue 1 has no v-else-if) in control-view.pug with three sibling v-if templates that mutually exclude on w.enabled and state['2an'] == 3. - axis-vars._get_motor_id: guard motor.axis.toLowerCase() against undefined motors (initial config is [{}, {}, ...]). - axis-vars._check_is_enabled: prefer config.motors[i].axis when present, fall back to state[N + 'an'] only for recognised axes (x/y/z/a) so undefined == undefined doesn't mistakenly enable b/c rows. - program-mixin: tolerate state.files / state.gcode_list being undefined right after connect. App-shell race conditions - Skip the early parse_hash() in app.js ready() when the initial hash is in the settings family. Those Svelte components read settings.units / settings.probing-prompts / motion.* etc. and crash on first paint with the empty placeholder config. Stay on loading-view until update() completes and routes us in itself. Misc - src/static/js/ui.js: null-guard the legacy burger menu code (#menuLink no longer exists). Was throwing 'Cannot set properties of null (setting onclick)'. - src/static/css/Audiowide.css: switch the gstatic font URL from http:// to https:// so it isn't blocked as mixed content under the home.muehe.org HTTPS proxy. - Macro buttons: drop the default 6 px yellow border-left. The stripe now only appears via .has-color when state.macros[i].color is actually configured. Removes the asymmetric/lopsided look from the screenshot. Tested live on http://10.1.10.55/ and via the HTTPS proxy at https://onefinity.home.muehe.org/. --- src/js/app.js | 18 +++++- src/js/axis-vars.js | 30 +++++++-- src/js/orbit.js | 10 ++- src/js/program-mixin.js | 17 +++-- src/pug/templates/control-view.pug | 15 +++-- src/pug/templates/estop.pug | 2 + src/static/css/Audiowide.css | 2 +- src/static/js/ui.js | 40 ++++++------ src/stylus/style.styl | 100 +++++++++++++++++++++-------- 9 files changed, 169 insertions(+), 65 deletions(-) diff --git a/src/js/app.js b/src/js/app.js index c8cd5ce..85dbcd7 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -358,8 +358,22 @@ module.exports = new Vue({ // Resolve the initial route before the websocket connects so // the shell shows the right view even on a slow / offline // controller. update() will call parse_hash() again once the - // first config is in. - this.parse_hash(); + // first config is in. Skip routing into the Svelte settings + // family before config has loaded — those components read + // many config keys (settings.units, settings.probing-prompts, + // motion.*, etc.) and would throw on first paint with the + // empty placeholder config. + const settingsFamily = [ + "settings", "admin-general", "admin-network", + "motor", "tool", "io", "macros", + "help", "cheat-sheet", + ]; + const initialHead = (location.hash || "").replace(/^#/, "").split(":")[0]; + if (settingsFamily.indexOf(initialHead) === -1) { + this.parse_hash(); + } + // else: stay on "loading" until update() completes and calls + // parse_hash() itself. this.connect(); diff --git a/src/js/axis-vars.js b/src/js/axis-vars.js index abe96c0..2d39f16 100644 --- a/src/js/axis-vars.js +++ b/src/js/axis-vars.js @@ -189,7 +189,11 @@ module.exports = { _get_motor_id: function(axis) { for (let i = 0; i < this.config.motors.length; i++) { const motor = this.config.motors[i]; - if (motor.axis.toLowerCase() == axis) { + // motor.axis can be undefined on initial load before + // config has streamed in. Guard so the computed does + // not throw and bubble a Vue warning into the console. + if (motor && typeof motor.axis === "string" && + motor.axis.toLowerCase() == axis) { return i; } } @@ -198,10 +202,28 @@ module.exports = { }, _check_is_enabled: function(axis){ + // Prefer config.motors[i].axis (always present once the + // config has loaded). Fall back to the per-motor state + // `Nan` field, which is what the legacy UI used. This + // avoids hiding axis rows during the brief window after + // config has loaded but before the controller has pushed + // its first state delta. const axes = { x: 0, y: 1, z: 2, a: 3 }; - for(let i = 0; i < this.config.motors.length; i++){ - if(this.state[`${i}an`] == axes[axis]){ - return true; + const wanted = axes[axis]; + for (let i = 0; i < this.config.motors.length; i++) { + const motor = this.config.motors[i] || {}; + if (typeof motor.axis === "string" && + motor.axis.toLowerCase() == axis) { + return motor.enabled !== false; + } + // Only use the state Nan fallback for axes we know + // about (x/y/z/a). Otherwise undefined == undefined + // would mistakenly match every axis (b, c, ...). + if (typeof wanted === "number") { + const an = this.state[`${i}an`]; + if (typeof an === "number" && an === wanted) { + return true; + } } } return false; diff --git a/src/js/orbit.js b/src/js/orbit.js index ef7a4fc..efeb8e9 100644 --- a/src/js/orbit.js +++ b/src/js/orbit.js @@ -683,12 +683,16 @@ const OrbitControls = function(object, domElement) { event.preventDefault(); } + // Chrome treats touch/wheel listeners as passive by default, + // which prevents OrbitControls.preventDefault() from suppressing + // page panning while interacting with the 3D viewer. Pass + // {passive: false} on the events that need to call preventDefault. scope.domElement.addEventListener("contextmenu", onContextMenu, false); scope.domElement.addEventListener("mousedown", onMouseDown, false); - scope.domElement.addEventListener("wheel", onMouseWheel, false); - scope.domElement.addEventListener("touchstart", onTouchStart, false); + scope.domElement.addEventListener("wheel", onMouseWheel, { passive: false }); + scope.domElement.addEventListener("touchstart", onTouchStart, { passive: false }); scope.domElement.addEventListener("touchend", onTouchEnd, false); - scope.domElement.addEventListener("touchmove", onTouchMove, false); + scope.domElement.addEventListener("touchmove", onTouchMove, { passive: false }); window.addEventListener("keydown", onKeyDown, false); this.update(); // force an update at start diff --git a/src/js/program-mixin.js b/src/js/program-mixin.js index b3b5ae4..f14c139 100644 --- a/src/js/program-mixin.js +++ b/src/js/program-mixin.js @@ -120,10 +120,12 @@ module.exports = { gcode_files: function () { if (!this.state.folder) return []; - const folder = this.state.gcode_list.find(item => item.name == this.state.folder); + const list = Array.isArray(this.state.gcode_list) ? this.state.gcode_list : []; + const folder = list.find(item => item.name == this.state.folder); if (!folder) return []; - const files = folder.files - .filter(item => this.state.files.includes(item.file_name)) + const stateFiles = Array.isArray(this.state.files) ? this.state.files : []; + const files = (folder.files || []) + .filter(item => stateFiles.includes(item.file_name)) .map(item => item.file_name); if (this.files_sortby == "A-Z") return files.sort(); if (this.files_sortby == "Z-A") return files.sort().reverse(); @@ -136,7 +138,8 @@ module.exports = { }, gcode_folders: function () { - return this.state.gcode_list + const list = Array.isArray(this.state.gcode_list) ? this.state.gcode_list : []; + return list .map(item => item.name) .filter(element => element !== "default") .sort(); @@ -174,7 +177,11 @@ module.exports = { const file = this.state.selected; if (this.last_file == file && this.last_file_time == file_time) return; - if (this.state.selected && !this.state.files.includes(this.state.selected)) { + // state.files can be undefined briefly after connect, before the + // controller has pushed its file list. Skip the existence check + // until we have a list to consult. + const files = Array.isArray(this.state.files) ? this.state.files : null; + if (this.state.selected && files && !files.includes(this.state.selected)) { this.GCodeNotFound = true; return; } diff --git a/src/pug/templates/control-view.pug b/src/pug/templates/control-view.pug index f88d0d5..e7a6df3 100644 --- a/src/pug/templates/control-view.pug +++ b/src/pug/templates/control-view.pug @@ -89,7 +89,7 @@ script#control-view-template(type="text/x-template") .fa.fa-arrow-down.ico(style="transform: rotate(-45deg)") button.jbtn(@click="jog_fn(0, 0, -1, 0)") Z− - // Row 4 — auxiliary axis (W or A) or probe shortcuts + // Row 4 — W axis (auxcnc) when enabled template(v-if="w.enabled") button.jbtn(@click="aux_jog_incr(-1)", :disabled="!w.enabled") .fa.fa-arrow-down.ico @@ -104,7 +104,10 @@ script#control-view-template(type="text/x-template") :class="{'load-on': !state['pw']}") .fa.fa-bullseye.ico span.lbl Probe - template(v-else-if="state['2an'] == 3") + + // Row 4 — A axis (rotary) when no W and rotary is enabled + // (Vue 1 has no v-else-if; we negate w.enabled explicitly.) + template(v-if="!w.enabled && state['2an'] == 3") button.jbtn.dir(@click="jog_fn(0, 0, 0, -1)") .fa.fa-rotate-left.ico span.lbl A− @@ -118,7 +121,9 @@ script#control-view-template(type="text/x-template") :class="{'load-on': !state['pw']}") .fa.fa-bullseye.ico span.lbl Probe - template(v-else) + + // Row 4 — fallback probe / zero / home shortcuts + template(v-if="!w.enabled && state['2an'] != 3") button.jbtn(@click="showProbeDialog('xyz')", :class="{'load-on': !state['pw']}") .fa.fa-bullseye.ico @@ -246,9 +251,9 @@ script#control-view-template(type="text/x-template") title="Click to run macro", @click="run_macro(index)", :disabled="!is_ready", - :style="{ borderLeftColor: macros.color || '#fde047' }") + :class="{'has-color': macros.color && macros.color !== '#ffffff' && macros.color !== '#fff'}", + :style="macros.color && macros.color !== '#ffffff' && macros.color !== '#fff' ? {borderLeftColor: macros.color} : {}") span.mnum {{index + 1}} - .fa.fa-circle-play.micon span.mname {{macros.name || ('Macro ' + (index + 1))}} // ----- Override drawer (anchored to bottom; toggled by Spindle KPI tile) ----- diff --git a/src/pug/templates/estop.pug b/src/pug/templates/estop.pug index 393fa88..5a01e3b 100644 --- a/src/pug/templates/estop.pug +++ b/src/pug/templates/estop.pug @@ -2,6 +2,8 @@ script#estop-template(type="text/x-template") svg(version="1.1", xmlns:svg="http://www.w3.org/2000/svg", xmlns="http://www.w3.org/2000/svg", xmlns:xlink="http://www.w3.org/1999/xlink", + viewBox="0 0 130 130", + preserveAspectRatio="xMidYMid meet", width="130", height="130") defs path#text-path-1(d="m 73.735,673.129 c 0,55.107 44.673,99.780 99.780,99.780 55.107,0 99.780,-44.673 99.780,-99.780 0,-55.107 -44.673,-99.780 -99.780,-99.780 -55.107,0 -99.780,44.673 -99.780,99.780 z") diff --git a/src/static/css/Audiowide.css b/src/static/css/Audiowide.css index a73be84..9ad61bf 100644 --- a/src/static/css/Audiowide.css +++ b/src/static/css/Audiowide.css @@ -2,5 +2,5 @@ font-family: 'Audiowide'; font-style: normal; font-weight: 400; - src: local('Audiowide'), local('Audiowide-Regular'), url(http://fonts.gstatic.com/s/audiowide/v4/8XtYtNKEyyZh481XVWfVOqCWcynf_cDxXwCLxiixG1c.ttf) format('truetype'); + src: local('Audiowide'), local('Audiowide-Regular'), url(https://fonts.gstatic.com/s/audiowide/v4/8XtYtNKEyyZh481XVWfVOqCWcynf_cDxXwCLxiixG1c.ttf) format('truetype'); } diff --git a/src/static/js/ui.js b/src/static/js/ui.js index acc38a0..13238c9 100644 --- a/src/static/js/ui.js +++ b/src/static/js/ui.js @@ -1,35 +1,37 @@ +// V09 redesign: the legacy side menu was removed. Keep this file +// shipped in case anything still references it, but no-op the click +// handler that used to wire up the burger menu so it does not throw +// "Cannot set properties of null" on the Settings tab. (function (window, document) { + var menuLink = document.getElementById("menuLink"); + if (!menuLink) { + return; + } - var layout = document.getElementById('layout'), - menu = document.getElementById('menu'), - menuLink = document.getElementById('menuLink'); + var layout = document.getElementById("layout"); + var menu = document.getElementById("menu"); function toggleClass(element, className) { - var classes = element.className.split(/\s+/), - length = classes.length, - i = 0; - - for(; i < length; i++) { - if (classes[i] === className) { - classes.splice(i, 1); - break; - } + if (!element) return; + var classes = element.className.split(/\s+/); + var i; + for (i = 0; i < classes.length; i++) { + if (classes[i] === className) { + classes.splice(i, 1); + break; + } } - // The className is not found - if (length === classes.length) { + if (i === classes.length) { classes.push(className); } - - element.className = classes.join(' '); + element.className = classes.join(" "); } menuLink.onclick = function (e) { - var active = 'active'; - + var active = "active"; e.preventDefault(); toggleClass(layout, active); toggleClass(menu, active); toggleClass(menuLink, active); }; - }(this, this.document)); diff --git a/src/stylus/style.styl b/src/stylus/style.styl index 007c7da..b8dec52 100644 --- a/src/stylus/style.styl +++ b/src/stylus/style.styl @@ -74,7 +74,9 @@ tt .app-shell display flex flex-direction column - min-height 100vh + height 100vh // cap at viewport so children that ask for 1fr/flex:1 + width 100% + overflow hidden background $body-bg .app-body @@ -83,9 +85,10 @@ tt display flex flex-direction column padding 18px + overflow auto // settings/motor pages can scroll inside the body > * - flex 1 + flex 1 1 auto min-height 0 .app-head @@ -496,28 +499,34 @@ span.unit 50% fill #ff9d00 -// Octagonal STOP wrapper around the existing SVG. The SVG -// rules below (`.button`, `.ring`, etc.) keep working unchanged. +// E-Stop in the header — wraps the legacy SVG component. +// Sized to fit the 96px header with breathing room. The SVG carries +// its own yellow safety ring and EMERGENCY/STOP text; we only frame +// it with a soft drop shadow and a hover/active hit target. .app-head .estop - width 88px - height 88px - background #dc2626 - clip-path polygon(30% 0, 70% 0, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0 70%, 0 30%) - display flex + width 80px + height 80px + display inline-flex align-items center justify-content center - border 3px solid #fff - box-shadow 0 0 0 3px #b91c1c, 0 8px 20px rgba(220, 38, 38, 0.35) + border-radius 9999px cursor pointer - transition transform 0.06s + transition transform 0.06s, filter 0.15s + flex 0 0 auto + // Make sure the SVG's internal coordinate space scales correctly + overflow visible + + &:hover + filter brightness(1.05) &:active transform scale(0.96) svg - width 56px - height 56px + width 80px + height 80px cursor pointer + display block .button:hover filter brightness(120%) @@ -1009,11 +1018,12 @@ tt.save // CONTROL page (V09) // ===================================================================== .control-page - flex 1 + flex 1 1 auto min-height 0 display flex flex-direction column gap 14px + overflow hidden .control-page .control-grid display grid @@ -1362,7 +1372,6 @@ tt.save height 84px border-radius 14px border 1px solid transparent - border-left 6px solid $accent color #fff background $jog-bg font-weight 800 @@ -1386,6 +1395,12 @@ tt.save opacity 0.45 cursor not-allowed + // Per-macro color stripe is opt-in via :class="has-color" set by + // the template only when state.macros[i].color is configured. + &.has-color + border-left-width 6px + border-left-style solid + .mnum display inline-flex align-items center @@ -1398,16 +1413,12 @@ tt.save font-size 0.85rem font-weight 900 text-shadow none - - .micon - font-size 1.1rem - opacity 0.75 + flex 0 0 auto .mname white-space nowrap overflow hidden text-overflow ellipsis - max-width 9em // Override drawer .override-drawer @@ -1487,10 +1498,11 @@ tt.save // PROGRAM page (V09) // ===================================================================== .program-page - flex 1 + flex 1 1 auto min-height 0 display flex flex-direction column + overflow hidden .program-card flex 1 @@ -1609,7 +1621,7 @@ tt.save min-width 300px .program-body - flex 1 + flex 1 1 auto display grid grid-template-columns 1fr 600px min-height 0 @@ -1617,13 +1629,36 @@ tt.save > .gcode border-right 1px solid $line-soft - overflow auto background #fafafa padding 0 margin 0 + overflow hidden + min-height 0 + display flex + flex-direction column + // 3D toolpath preview — fill the entire 600px column. Override the + // legacy `.path-viewer.small` rule which would clamp the canvas to + // 340x150 and float it into the corner. > .path-viewer overflow hidden + min-height 0 + display flex + flex-direction column + + .path-viewer-content + flex 1 1 auto + width 100% !important + height auto !important + min-height 0 + float none !important + margin 0 !important + + &.small .path-viewer-content + width 100% !important + height auto !important + float none !important + margin 0 !important .progress-bar height 28px @@ -1645,8 +1680,20 @@ tt.save font-size 13px line-height 1.55 + .clusterize + flex 1 1 0 + min-height 0 + overflow hidden + display flex + flex-direction column + height 100% + .clusterize-scroll - max-height 100% + flex 1 1 0 + min-height 0 + height 100% + max-height none // override clusterize.css default of 200px + width 100% overflow auto ul @@ -1675,9 +1722,10 @@ tt.save // CONSOLE page (V09) // ===================================================================== .console-page - flex 1 + flex 1 1 auto display flex min-height 0 + overflow hidden .console-card flex 1