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/.
This commit is contained in:
2026-04-30 22:24:55 +02:00
parent ea23f94b87
commit 5926316a25
9 changed files with 169 additions and 65 deletions

View File

@@ -358,8 +358,22 @@ module.exports = new Vue({
// Resolve the initial route before the websocket connects so // Resolve the initial route before the websocket connects so
// the shell shows the right view even on a slow / offline // the shell shows the right view even on a slow / offline
// controller. update() will call parse_hash() again once the // controller. update() will call parse_hash() again once the
// first config is in. // 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(); this.parse_hash();
}
// else: stay on "loading" until update() completes and calls
// parse_hash() itself.
this.connect(); this.connect();

View File

@@ -189,7 +189,11 @@ module.exports = {
_get_motor_id: function(axis) { _get_motor_id: function(axis) {
for (let i = 0; i < this.config.motors.length; i++) { for (let i = 0; i < this.config.motors.length; i++) {
const motor = this.config.motors[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; return i;
} }
} }
@@ -198,12 +202,30 @@ module.exports = {
}, },
_check_is_enabled: function(axis){ _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 }; const axes = { x: 0, y: 1, z: 2, a: 3 };
const wanted = axes[axis];
for (let i = 0; i < this.config.motors.length; i++) { for (let i = 0; i < this.config.motors.length; i++) {
if(this.state[`${i}an`] == axes[axis]){ 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 true;
} }
} }
}
return false; return false;
}, },

View File

@@ -683,12 +683,16 @@ const OrbitControls = function(object, domElement) {
event.preventDefault(); 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("contextmenu", onContextMenu, false);
scope.domElement.addEventListener("mousedown", onMouseDown, false); scope.domElement.addEventListener("mousedown", onMouseDown, false);
scope.domElement.addEventListener("wheel", onMouseWheel, false); scope.domElement.addEventListener("wheel", onMouseWheel, { passive: false });
scope.domElement.addEventListener("touchstart", onTouchStart, false); scope.domElement.addEventListener("touchstart", onTouchStart, { passive: false });
scope.domElement.addEventListener("touchend", onTouchEnd, 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); window.addEventListener("keydown", onKeyDown, false);
this.update(); // force an update at start this.update(); // force an update at start

View File

@@ -120,10 +120,12 @@ module.exports = {
gcode_files: function () { gcode_files: function () {
if (!this.state.folder) return []; 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 []; if (!folder) return [];
const files = folder.files const stateFiles = Array.isArray(this.state.files) ? this.state.files : [];
.filter(item => this.state.files.includes(item.file_name)) const files = (folder.files || [])
.filter(item => stateFiles.includes(item.file_name))
.map(item => item.file_name); .map(item => item.file_name);
if (this.files_sortby == "A-Z") return files.sort(); if (this.files_sortby == "A-Z") return files.sort();
if (this.files_sortby == "Z-A") return files.sort().reverse(); if (this.files_sortby == "Z-A") return files.sort().reverse();
@@ -136,7 +138,8 @@ module.exports = {
}, },
gcode_folders: function () { 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) .map(item => item.name)
.filter(element => element !== "default") .filter(element => element !== "default")
.sort(); .sort();
@@ -174,7 +177,11 @@ module.exports = {
const file = this.state.selected; const file = this.state.selected;
if (this.last_file == file && this.last_file_time == file_time) return; 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; this.GCodeNotFound = true;
return; return;
} }

View File

@@ -89,7 +89,7 @@ script#control-view-template(type="text/x-template")
.fa.fa-arrow-down.ico(style="transform: rotate(-45deg)") .fa.fa-arrow-down.ico(style="transform: rotate(-45deg)")
button.jbtn(@click="jog_fn(0, 0, -1, 0)") Z 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") template(v-if="w.enabled")
button.jbtn(@click="aux_jog_incr(-1)", :disabled="!w.enabled") button.jbtn(@click="aux_jog_incr(-1)", :disabled="!w.enabled")
.fa.fa-arrow-down.ico .fa.fa-arrow-down.ico
@@ -104,7 +104,10 @@ script#control-view-template(type="text/x-template")
:class="{'load-on': !state['pw']}") :class="{'load-on': !state['pw']}")
.fa.fa-bullseye.ico .fa.fa-bullseye.ico
span.lbl Probe 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)") button.jbtn.dir(@click="jog_fn(0, 0, 0, -1)")
.fa.fa-rotate-left.ico .fa.fa-rotate-left.ico
span.lbl A span.lbl A
@@ -118,7 +121,9 @@ script#control-view-template(type="text/x-template")
:class="{'load-on': !state['pw']}") :class="{'load-on': !state['pw']}")
.fa.fa-bullseye.ico .fa.fa-bullseye.ico
span.lbl Probe 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')", button.jbtn(@click="showProbeDialog('xyz')",
:class="{'load-on': !state['pw']}") :class="{'load-on': !state['pw']}")
.fa.fa-bullseye.ico .fa.fa-bullseye.ico
@@ -246,9 +251,9 @@ script#control-view-template(type="text/x-template")
title="Click to run macro", title="Click to run macro",
@click="run_macro(index)", @click="run_macro(index)",
:disabled="!is_ready", :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}} span.mnum {{index + 1}}
.fa.fa-circle-play.micon
span.mname {{macros.name || ('Macro ' + (index + 1))}} span.mname {{macros.name || ('Macro ' + (index + 1))}}
// ----- Override drawer (anchored to bottom; toggled by Spindle KPI tile) ----- // ----- Override drawer (anchored to bottom; toggled by Spindle KPI tile) -----

View File

@@ -2,6 +2,8 @@ script#estop-template(type="text/x-template")
svg(version="1.1", xmlns:svg="http://www.w3.org/2000/svg", svg(version="1.1", xmlns:svg="http://www.w3.org/2000/svg",
xmlns="http://www.w3.org/2000/svg", xmlns="http://www.w3.org/2000/svg",
xmlns:xlink="http://www.w3.org/1999/xlink", xmlns:xlink="http://www.w3.org/1999/xlink",
viewBox="0 0 130 130",
preserveAspectRatio="xMidYMid meet",
width="130", height="130") width="130", height="130")
defs 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") 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")

View File

@@ -2,5 +2,5 @@
font-family: 'Audiowide'; font-family: 'Audiowide';
font-style: normal; font-style: normal;
font-weight: 400; 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');
} }

View File

@@ -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) { (function (window, document) {
var menuLink = document.getElementById("menuLink");
if (!menuLink) {
return;
}
var layout = document.getElementById('layout'), var layout = document.getElementById("layout");
menu = document.getElementById('menu'), var menu = document.getElementById("menu");
menuLink = document.getElementById('menuLink');
function toggleClass(element, className) { function toggleClass(element, className) {
var classes = element.className.split(/\s+/), if (!element) return;
length = classes.length, var classes = element.className.split(/\s+/);
i = 0; var i;
for (i = 0; i < classes.length; i++) {
for(; i < length; i++) {
if (classes[i] === className) { if (classes[i] === className) {
classes.splice(i, 1); classes.splice(i, 1);
break; break;
} }
} }
// The className is not found if (i === classes.length) {
if (length === classes.length) {
classes.push(className); classes.push(className);
} }
element.className = classes.join(" ");
element.className = classes.join(' ');
} }
menuLink.onclick = function (e) { menuLink.onclick = function (e) {
var active = 'active'; var active = "active";
e.preventDefault(); e.preventDefault();
toggleClass(layout, active); toggleClass(layout, active);
toggleClass(menu, active); toggleClass(menu, active);
toggleClass(menuLink, active); toggleClass(menuLink, active);
}; };
}(this, this.document)); }(this, this.document));

View File

@@ -74,7 +74,9 @@ tt
.app-shell .app-shell
display flex display flex
flex-direction column 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 background $body-bg
.app-body .app-body
@@ -83,9 +85,10 @@ tt
display flex display flex
flex-direction column flex-direction column
padding 18px padding 18px
overflow auto // settings/motor pages can scroll inside the body
> * > *
flex 1 flex 1 1 auto
min-height 0 min-height 0
.app-head .app-head
@@ -496,28 +499,34 @@ span.unit
50% 50%
fill #ff9d00 fill #ff9d00
// Octagonal STOP wrapper around the existing <estop> SVG. The SVG // E-Stop in the header wraps the legacy <estop> SVG component.
// rules below (`.button`, `.ring`, etc.) keep working unchanged. // 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 .app-head .estop
width 88px width 80px
height 88px height 80px
background #dc2626 display inline-flex
clip-path polygon(30% 0, 70% 0, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0 70%, 0 30%)
display flex
align-items center align-items center
justify-content center justify-content center
border 3px solid #fff border-radius 9999px
box-shadow 0 0 0 3px #b91c1c, 0 8px 20px rgba(220, 38, 38, 0.35)
cursor pointer 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 &:active
transform scale(0.96) transform scale(0.96)
svg svg
width 56px width 80px
height 56px height 80px
cursor pointer cursor pointer
display block
.button:hover .button:hover
filter brightness(120%) filter brightness(120%)
@@ -1009,11 +1018,12 @@ tt.save
// CONTROL page (V09) // CONTROL page (V09)
// ===================================================================== // =====================================================================
.control-page .control-page
flex 1 flex 1 1 auto
min-height 0 min-height 0
display flex display flex
flex-direction column flex-direction column
gap 14px gap 14px
overflow hidden
.control-page .control-grid .control-page .control-grid
display grid display grid
@@ -1362,7 +1372,6 @@ tt.save
height 84px height 84px
border-radius 14px border-radius 14px
border 1px solid transparent border 1px solid transparent
border-left 6px solid $accent
color #fff color #fff
background $jog-bg background $jog-bg
font-weight 800 font-weight 800
@@ -1386,6 +1395,12 @@ tt.save
opacity 0.45 opacity 0.45
cursor not-allowed 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 .mnum
display inline-flex display inline-flex
align-items center align-items center
@@ -1398,16 +1413,12 @@ tt.save
font-size 0.85rem font-size 0.85rem
font-weight 900 font-weight 900
text-shadow none text-shadow none
flex 0 0 auto
.micon
font-size 1.1rem
opacity 0.75
.mname .mname
white-space nowrap white-space nowrap
overflow hidden overflow hidden
text-overflow ellipsis text-overflow ellipsis
max-width 9em
// Override drawer // Override drawer
.override-drawer .override-drawer
@@ -1487,10 +1498,11 @@ tt.save
// PROGRAM page (V09) // PROGRAM page (V09)
// ===================================================================== // =====================================================================
.program-page .program-page
flex 1 flex 1 1 auto
min-height 0 min-height 0
display flex display flex
flex-direction column flex-direction column
overflow hidden
.program-card .program-card
flex 1 flex 1
@@ -1609,7 +1621,7 @@ tt.save
min-width 300px min-width 300px
.program-body .program-body
flex 1 flex 1 1 auto
display grid display grid
grid-template-columns 1fr 600px grid-template-columns 1fr 600px
min-height 0 min-height 0
@@ -1617,13 +1629,36 @@ tt.save
> .gcode > .gcode
border-right 1px solid $line-soft border-right 1px solid $line-soft
overflow auto
background #fafafa background #fafafa
padding 0 padding 0
margin 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 > .path-viewer
overflow hidden 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 .progress-bar
height 28px height 28px
@@ -1645,8 +1680,20 @@ tt.save
font-size 13px font-size 13px
line-height 1.55 line-height 1.55
.clusterize
flex 1 1 0
min-height 0
overflow hidden
display flex
flex-direction column
height 100%
.clusterize-scroll .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 overflow auto
ul ul
@@ -1675,9 +1722,10 @@ tt.save
// CONSOLE page (V09) // CONSOLE page (V09)
// ===================================================================== // =====================================================================
.console-page .console-page
flex 1 flex 1 1 auto
display flex display flex
min-height 0 min-height 0
overflow hidden
.console-card .console-card
flex 1 flex 1