Compare commits

..

4 Commits

Author SHA1 Message Date
muehe
19e6cc6c93 ui: unify jog button sizing across tablet and kiosk
Big jog labels were only set inside the kiosk-mode override block,
which made the 1920x1080 tablet preview look small and inconsistent
with the Pi kiosk. Move the larger sizes to the base .jbtn rule
(font 1.6rem, ico 2.4rem, lbl 1.5rem) and drop the kiosk-mode
.jbtn override so both viewports use the same single source of
truth.
2026-05-01 14:24:33 +02:00
muehe
50839718e2 kiosk: chromium 72 mime + flex-gap fixes
Pi's onboard chromium is 72 (Jan 2019). Two issues:

1. Python 3.5's mimetypes doesn't know woff/woff2/ttf, so Tornado
   serves them as application/octet-stream which chromium 72 refuses
   to use as web fonts -> all FA6 icons render as empty boxes. Add
   scripts/deploy/patch_font_mime.py that monkey-patches bbctrl
   Web.py's StaticFileHandler with correct content types. Run
   automatically by deploy-hardware.sh (idempotent).

2. flex-gap landed in chromium 84. Add '> * + *' margin fallbacks
   for the flex containers that show up on Program tab (action-bar,
   action-btn, file-bar, file-btn) and tighten the kiosk-mode
   settings rail so all 14 items fit in 768px height.
2026-05-01 11:16:28 +02:00
68a92bb297 AuxAxis: pre-load home_zero via HOMECFG, drop post-home WPOS
home() previously matched the wrong [home] line (firmware-side bug,
fixed in auxcnc) and even when it would have matched, it tried to
shift the step counter by writing 'WPOS <n>' after homing. The ESP's
WPOS handler clears HOMED, so a bbctrl restart would forget the home
state.

Push the desired step counter via HOMECFG zero= (firmware writes it
into the counter at the end of a successful HOME, leaving HOMED set).
home() now only reads the terminal [home] line; no post-home counter
fixup.
2026-05-01 11:08:51 +02:00
muehe
41d720c1d0 kiosk: pi-friendly compact mode + chromium 72 fallbacks
- Detect kiosk mode (localhost / ?kiosk=1) and add html.kiosk-mode
- Suppress 3D path-viewer in kiosk mode (Pi 3B too slow)
- Compact 1366x768 layout: 56px header, smaller jog grid, 4-col macros
  2-col status, 540px jog column
- Flex-gap fallbacks for Chromium 72 (header tabs, sys-btn, state-badge,
  ktab, sp-row, etc.) using "> * + *" margin-* rules
- Path-viewer: opaque WebGL canvas, ResizeObserver-gated render loop,
  no first-frame size flash
- Path-viewer renderer cleared properly on component teardown
- W axis row: W- | W+ | Probe XYZ | Probe Z (was W-|HomeW|W+|Probe)
- Running panel only for actual program execution (not jogging)
- Settings sectioned (Display+Units / Probing / G-code+Motion)
- Routed component now keep-alive across tab swaps
- FA4 -> FA6 webfonts
2026-05-01 11:05:39 +02:00
36 changed files with 1195 additions and 145 deletions

View File

@@ -68,7 +68,11 @@ update: pkg
build/templates.pug: $(TEMPLS)
mkdir -p build
cat $(TEMPLS) >$@
# Use awk to ensure each template is followed by a newline so the
# next file's first line never gets glued onto the previous file's
# last line (some templates ship without a trailing newline, which
# would produce subtle Pug parse failures).
awk 'FNR==1 && NR>1 {print ""} {print} END{print ""}' $(TEMPLS) >$@
node_modules: package.json
npm install && touch node_modules

View File

@@ -8,7 +8,7 @@
#
# Defaults:
# HOST=onefinity.local
# USER=bbmc
# REMOTE_USER=bbmc
# PASSWORD=onefinity (used for sudo on the Pi)
#
# Override:
@@ -20,46 +20,65 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$SCRIPT_DIR"
HOST="${HOST:-onefinity.local}"
# REMOTE_USER (not USER, which the shell pre-populates with the local
# logged-in account).
REMOTE_USER="${REMOTE_USER:-bbmc}"
PASSWORD="${PASSWORD:-onefinity}"
echo "🛠 Building UI bundle..."
echo "Building UI bundle (HTML + resources)..."
make build/http/index.html >/dev/null
# Copy src/resources/* into build/http/. The Makefile's "all" target
# also does this, but pulls in cross-compiled subprojects (avr/boot/
# pwr/jig) we don't have toolchains for on macOS. This rsync mirrors
# only the resource tree.
rsync -a src/resources/ build/http/
# Discover the on-Pi http path; the bbctrl egg version may change.
echo "🔍 Locating bbctrl http/ directory on $HOST..."
echo "Locating bbctrl http/ directory on $HOST..."
REMOTE_HTTP_DIR="$(ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" \
"ls -d /usr/local/lib/python*/dist-packages/bbctrl-*-py*.egg/bbctrl/http 2>/dev/null | head -1")"
if [[ -z "$REMOTE_HTTP_DIR" ]]; then
echo "❌ Could not find bbctrl http/ directory on $HOST"
echo "ERROR: could not find bbctrl http/ directory on $HOST"
exit 1
fi
echo " $REMOTE_HTTP_DIR"
echo " $REMOTE_HTTP_DIR"
echo "🚚 Rsyncing build/http/ $HOST:$REMOTE_HTTP_DIR/"
# Stage to a tmp dir owned by $REMOTE_USER, then sudo-mv into place.
# This avoids needing root over rsync.
echo "Rsyncing build/http/ -> $HOST:$REMOTE_HTTP_DIR/"
# Stage to a tmp dir owned by $REMOTE_USER, then sudo-rsync into
# place. This avoids needing root over rsync. We do NOT use --delete
# anywhere -- the Pi's egg ships extra runtime files (config-template
# .json, default machine JSON, buildbotics.nc, etc.) that come with
# the bbctrl package and are not in this repo's src/resources. If
# they were deleted the controller's API would 500 because Python
# imports fail.
REMOTE_TMP="/tmp/onefin_ui_$$"
ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" "mkdir -p '${REMOTE_TMP}'"
rsync -avz --delete \
rsync -avz \
--exclude='hostinfo.txt' \
-e "ssh -o ConnectTimeout=5" \
build/http/ "${REMOTE_USER}@${HOST}:${REMOTE_TMP}/"
echo "📦 Installing into ${REMOTE_HTTP_DIR}/ (sudo)..."
echo "Installing into ${REMOTE_HTTP_DIR}/ (sudo)..."
ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" \
"echo '${PASSWORD}' | sudo -S bash -c '
rsync -a --delete --exclude=hostinfo.txt \"${REMOTE_TMP}/\" \"${REMOTE_HTTP_DIR}/\" \
rsync -a --exclude=hostinfo.txt \"${REMOTE_TMP}/\" \"${REMOTE_HTTP_DIR}/\" \
&& rm -rf \"${REMOTE_TMP}\"
'" 2>&1 | tail -3
echo "🔁 Restarting bbctrl service..."
# Patch bbctrl Web.py so font files get the correct MIME type. The
# Pi ships Python 3.5, whose `mimetypes` module doesn't know about
# woff/woff2/ttf, so Tornado serves them as application/octet-stream
# which Chromium 72 (the Pi's onboard browser) refuses to use as a
# web font, leading to all FontAwesome icons rendering as empty
# boxes in the kiosk UI. The patch is idempotent.
echo "Patching bbctrl font MIME types (idempotent)..."
scp -o ConnectTimeout=5 "$SCRIPT_DIR/scripts/deploy/patch_font_mime.py" \
"${REMOTE_USER}@${HOST}:/tmp/patch_font_mime.py" >/dev/null
ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" \
"echo '${PASSWORD}' | sudo -S python3 /tmp/patch_font_mime.py" 2>&1 | tail -3
echo "Restarting bbctrl service..."
ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" \
"echo '${PASSWORD}' | sudo -S systemctl restart bbctrl" 2>&1 | tail -3
echo ""
echo "Deployed to http://${HOST}/"
echo " Logs: ssh ${REMOTE_USER}@${HOST} 'journalctl -u bbctrl -f'"
echo " Open: open -a 'Google Chrome' http://${HOST}/"
echo "Deployed to http://${HOST}/"
echo " Logs: ssh ${REMOTE_USER}@${HOST} 'journalctl -u bbctrl -f'"
echo " Open: open -a 'Google Chrome' http://${HOST}/"

View File

@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""
Patch bbctrl Web.py so font files get the correct MIME type.
Background
----------
The Onefinity controller (Pi 3B running Raspbian stretch) ships Python
3.5, whose ``mimetypes`` module does not recognize ``.woff``, ``.woff2``
or ``.ttf``. Tornado's ``StaticFileHandler`` therefore falls back to
``application/octet-stream`` for those, and Chromium 72 (the Pi's
onboard kiosk browser) refuses to use such payloads as web fonts. The
result is that every FontAwesome icon renders as an empty box on the
kiosk display.
This patch monkey-patches ``StaticFileHandler.get_content_type`` to
emit the right MIME types. It is idempotent: running it twice is a
no-op. Run with ``sudo`` so it can rewrite the egg's Web.py.
Used by:
scripts/deploy/hardware.sh
"""
from __future__ import print_function
import os
import sys
def find_web_py():
"""Return the absolute path to the bbctrl Web.py shipped in the egg."""
base = "/usr/local/lib"
for entry in os.listdir(base):
if not entry.startswith("python"):
continue
candidate_dir = os.path.join(base, entry, "dist-packages")
if not os.path.isdir(candidate_dir):
continue
for sub in os.listdir(candidate_dir):
if sub.startswith("bbctrl-") and sub.endswith(".egg"):
p = os.path.join(candidate_dir, sub, "bbctrl", "Web.py")
if os.path.isfile(p):
return p
return None
OLD_BLOCK = (
"class StaticFileHandler(tornado.web.StaticFileHandler):\n"
" def set_extra_headers(self, path):\n"
" self.set_header('Cache-Control',\n"
" 'no-store, no-cache, must-revalidate, max-age=0')"
)
NEW_BLOCK = (
"class StaticFileHandler(tornado.web.StaticFileHandler):\n"
" # FONT_MIME_FIX: Python 3.5's mimetypes module does not know\n"
" # woff/woff2/ttf, so Tornado serves them as application/octet-\n"
" # stream which Chromium 72 (the Pi's onboard kiosk browser)\n"
" # refuses to use as web fonts. Set explicit types so the FA6\n"
" # icon set actually renders on the kiosk display.\n"
" def get_content_type(self):\n"
" path = self.absolute_path or ''\n"
" if path.endswith('.woff2'): return 'font/woff2'\n"
" if path.endswith('.woff'): return 'font/woff'\n"
" if path.endswith('.ttf'): return 'font/ttf'\n"
" if path.endswith('.otf'): return 'font/otf'\n"
" if path.endswith('.eot'): return 'application/vnd.ms-fontobject'\n"
" return super().get_content_type()\n"
"\n"
" def set_extra_headers(self, path):\n"
" self.set_header('Cache-Control',\n"
" 'no-store, no-cache, must-revalidate, max-age=0')"
)
def main():
target = find_web_py()
if target is None:
print("ERROR: could not locate bbctrl Web.py under /usr/local/lib",
file=sys.stderr)
return 1
with open(target) as f:
src = f.read()
if "FONT_MIME_FIX" in src:
print("font mime: already patched ({})".format(target))
return 0
if OLD_BLOCK not in src:
print("font mime: expected block not found in {} -- skipping".format(target),
file=sys.stderr)
# Don't fail the deploy; just log and continue.
return 0
new_src = src.replace(OLD_BLOCK, NEW_BLOCK, 1)
with open(target, "w") as f:
f.write(new_src)
print("font mime: patched {}".format(target))
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -251,6 +251,19 @@ module.exports = new Vue({
},
computed: {
// True when the UI is in kiosk mode — i.e. running on the
// controller's own onboard browser (Pi 3B at 1366x768) or
// explicitly forced via ?kiosk=1. Source-of-truth is the
// `kiosk-mode` class added to <html> by the inline script
// in index.pug, which already honors hostname + URL param +
// localStorage. The Pi's VideoCore IV is too slow for the
// three.js toolpath preview, so we suppress that panel in
// kiosk mode and let the gcode listing take the full width.
is_kiosk: function() {
return typeof document !== "undefined"
&& document.documentElement.classList.contains("kiosk-mode");
},
popupMessages: function() {
const msgs = [];
@@ -356,6 +369,8 @@ module.exports = new Vue({
ready: function() {
window.onhashchange = () => this.parse_hash();
// 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
@@ -365,9 +380,11 @@ module.exports = new Vue({
// motion.*, etc.) and would throw on first paint with the
// empty placeholder config.
const settingsFamily = [
"settings", "admin-general", "admin-network",
"settings", "probing", "gcode",
"admin-general", "admin-network",
"motor", "tool", "io", "macros",
"help", "cheat-sheet",
"w-axis",
];
const initialHead = (location.hash || "").replace(/^#/, "").split(":")[0];
if (settingsFamily.indexOf(initialHead) === -1) {
@@ -593,9 +610,11 @@ module.exports = new Vue({
// Settings tab while keeping their existing top-level
// hash. This preserves all existing deep links.
const settingsViews = [
"settings", "admin-general", "admin-network",
"settings", "probing", "gcode",
"admin-general", "admin-network",
"motor", "tool", "io", "macros",
"help", "cheat-sheet",
"w-axis",
];
if (head == "control") {

View File

@@ -49,14 +49,17 @@ module.exports = {
methods: {
get_io_state_class: function(active, state) {
if (typeof active == "undefined" || typeof state == "undefined") {
return "fa-exclamation-triangle warn";
return "fa-triangle-exclamation warn";
}
// Tristated: render as the regular (outline) circle to
// distinguish from active/inactive solid circles. Adding
// `far` switches to the FA6 regular family.
if (state == 2) {
return "fa-circle-o";
return "far fa-circle";
}
const icon = state ? "fa-plus-circle" : "fa-minus-circle";
const icon = state ? "fa-circle-plus" : "fa-circle-minus";
return `${icon} ${active ? "active" : "inactive"}`;
},

View File

@@ -101,6 +101,13 @@ module.exports = {
Vue.nextTick(this.update);
},
beforeDestroy: function() {
if (this._sizeWatcher) {
this._sizeWatcher.disconnect();
this._sizeWatcher = null;
}
},
methods: {
update: async function() {
if (!this.webglAvailable) {
@@ -201,6 +208,12 @@ module.exports = {
}
const dims = this.get_dims();
// Skip layouts where the target has no measurable size.
// The render loop guard below will not draw frames until
// a real size has been observed at least once.
if (!(dims.width > 0 && dims.height > 0)) {
return;
}
this.camera.aspect = dims.width / dims.height;
this.camera.updateProjectionMatrix();
@@ -274,12 +287,23 @@ module.exports = {
}
try {
// Renderer
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
// Renderer. Use an opaque canvas with a clear color
// that matches the page-side gradient so the moment
// the canvas is appended (and before the first 3D
// frame is drawn) the user does not see a flash from
// the page background through transparency.
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: false,
});
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setClearColor(0, 0);
this.renderer.setClearColor(0x222222, 1);
// Same color on the DOM element itself so the very
// first paint (before the WebGL context has cleared)
// is dark too.
this.renderer.domElement.style.background = "#222222";
this.renderer.domElement.style.display = "block";
this.target.appendChild(this.renderer.domElement);
} catch (e) {
console.log("WebGL not supported: ", e);
return;
@@ -333,8 +357,46 @@ module.exports = {
// Events
window.addEventListener("resize", this.update_view, false);
// Start it
this.render();
// Start the render loop only after the target has a real,
// stable size. Without this, the first frame paints into
// a 0×0 / collapsed-flex canvas and a second frame paints
// again at the right size — visible as a flash on the
// very first mount of the Program tab.
const startRendering = () => {
if (this._rendering) return;
this._rendering = true;
this.update_view();
this.render();
};
const dims = this.get_dims();
if (dims.width > 0 && dims.height > 0) {
startRendering();
} else if (typeof ResizeObserver !== "undefined") {
this._sizeWatcher = new ResizeObserver(entries => {
for (const entry of entries) {
const r = entry.contentRect;
if (r.width > 0 && r.height > 0) {
this._sizeWatcher.disconnect();
this._sizeWatcher = null;
startRendering();
return;
}
}
});
this._sizeWatcher.observe(this.target);
} else {
// Old browser fallback: poll for a non-zero size.
const tick = () => {
const d = this.get_dims();
if (d.width > 0 && d.height > 0) {
startRendering();
} else {
requestAnimationFrame(tick);
}
};
requestAnimationFrame(tick);
}
},
create_surface_material: function() {
@@ -646,6 +708,14 @@ module.exports = {
return;
}
// Don't paint frames while the target has no size; this
// prevents an initial single-frame clear from painting
// before the layout has settled (visible as a dark flash).
const dims = this.get_dims();
if (!(dims.width > 0 && dims.height > 0)) {
return;
}
if (this.controls.update() || this.dirty) {
this.dirty = false;
this.renderer.render(this.scene, this.camera);

View File

@@ -77,6 +77,21 @@ module.exports = {
return this.state.cycle == "idle";
},
// True only while a loaded G-code program is actually being
// executed (running, paused/holding, or stopping). Excludes
// jogging, homing, MDI commands and other one-off motion that
// also leave state.cycle != 'idle' but should not bring up the
// "Now Running" panel on the Control tab.
is_program_executing: function () {
const xx = this.state && this.state.xx;
if (xx == "RUNNING" || xx == "HOLDING" || xx == "STOPPING") {
// Only count it as a program run if a file is selected.
// Otherwise an MDI submission also reads xx=RUNNING.
return !!(this.state && this.state.selected);
}
return false;
},
is_paused: function () {
return this.is_holding && (this.pause_reason == "User pause" || this.pause_reason == "Program pause");
},

View File

@@ -26,6 +26,8 @@ module.exports = {
},
computed: {
is_kiosk: function () { return !!this.$root.is_kiosk; },
display_units: {
cache: false,
get: function () { return this.$root.display_units; },

View File

@@ -24,6 +24,7 @@ module.exports = {
"io-view": require("./io-view"),
"macros-view": require("./macros"),
"help-view": require("./help-view"),
"w-axis-view": require("./w-axis-view"),
"cheat-sheet-view": {
template: "#cheat-sheet-view-template",
data: function () {
@@ -36,8 +37,17 @@ module.exports = {
return {
sub: this.$root.sub_tab || "settings",
ridx: this.$root.index, // local copy of the motor index
// Whether the controller config has streamed in. The Svelte
// settings views crash on first paint with the placeholder
// config (settings.units / settings.easy-adapter / motion.*
// are all undefined). Gate the inner mount on this flag.
config_ready: false,
rail_items: [
{ sub: "settings", href: "#settings", icon: "fa-display", label: "Display & Units" },
{ sub: "probing", href: "#probing", icon: "fa-bullseye", label: "Probing" },
{ sub: "gcode", href: "#gcode", icon: "fa-code", label: "G-code & Motion" },
{ sub: "macros", href: "#macros", icon: "fa-keyboard", label: "Macros" },
{ sub: "cheat-sheet", href: "#cheat-sheet", icon: "fa-book", label: "G-code Cheat Sheet" },
{ sub: "admin-network", href: "#admin-network", icon: "fa-network-wired", label: "Network" },
{ sub: "admin-general", href: "#admin-general", icon: "fa-shield-halved", label: "General / Firmware" },
{ sub: "tool", href: "#tool", icon: "fa-bolt", label: "Spindle & Tool" },
@@ -47,13 +57,10 @@ module.exports = {
{ sub: "motor", motor: 1, href: "#motor:1", icon: "fa-arrows-up-down-left-right", label: "Motor 1" },
{ sub: "motor", motor: 2, href: "#motor:2", icon: "fa-arrows-up-down-left-right", label: "Motor 2" },
{ sub: "motor", motor: 3, href: "#motor:3", icon: "fa-arrows-up-down-left-right", label: "Motor 3" },
// W axis is auxiliary (auxcnc ESP32). Its config lives inside
// the main Settings page; we route to #settings and scroll to
// the #w-axis anchor on click.
{ sub: "w-axis", href: "#settings", anchor: "w-axis", icon: "fa-arrows-up-down", label: "W Axis" },
// W axis is auxiliary (auxcnc ESP32). It mounts the existing
// WAxisSettings Svelte component on its own page.
{ sub: "w-axis", href: "#w-axis", icon: "fa-arrows-up-down", label: "W Axis" },
{ section: " " },
{ sub: "macros", href: "#macros", icon: "fa-keyboard", label: "Macros" },
{ sub: "cheat-sheet", href: "#cheat-sheet", icon: "fa-book", label: "G-code Cheat Sheet" },
{ sub: "help", href: "#help", icon: "fa-circle-question", label: "Help" },
],
};
@@ -63,6 +70,12 @@ module.exports = {
this._onHash = () => this.refresh_from_hash();
window.addEventListener("hashchange", this._onHash);
this.refresh_from_hash();
this._configPoll = setInterval(() => {
const c = this.$root && this.$root.config;
const ready = !!(c && c.full_version && c.full_version !== "<loading>"
&& c.settings && typeof c.settings === "object");
if (ready !== this.config_ready) this.config_ready = ready;
}, 200);
},
attached: function () {
@@ -87,6 +100,7 @@ module.exports = {
if (this._onHash) {
window.removeEventListener("hashchange", this._onHash);
}
if (this._configPoll) clearInterval(this._configPoll);
},
methods: {
@@ -99,12 +113,6 @@ module.exports = {
is_active: function (item) {
if (!item || item.section) return false;
// The W Axis rail item is a soft-link into #settings; we mark
// it active only when the user is on Display & Units AND the
// last clicked rail item was W Axis.
if (item.sub === "w-axis") {
return this.sub === "settings" && this._w_axis_focus === true;
}
if (item.sub !== this.sub) return false;
if (item.sub === "motor") {
return "" + item.motor === "" + this.ridx;
@@ -113,19 +121,50 @@ module.exports = {
},
on_rail_click: function (item, ev) {
// Soft-link rail items use an anchor and a scrollIntoView call.
if (item && item.anchor) {
ev.preventDefault();
// Navigate to settings if not already there, then scroll.
if (!item) return;
// Always preventDefault on rail clicks. Letting the browser
// anchor-scroll to <div id="settings"> etc. inside .app-body
// can pull the .app-head out of view; we drive navigation
// ourselves through location.hash and our hashchange handler.
if (ev && ev.preventDefault) ev.preventDefault();
if (item.anchor) {
// Soft-link rail items use a #settings hash plus an in-page
// anchor scroll once the Svelte page has mounted. We scroll
// ONLY the .settings-content overflow container by setting
// its scrollTop directly — element.scrollIntoView() walks all
// ancestor scroll containers and can tug the .app-body / html
// layout, which under tablet mode pulls the fixed header out
// of view.
if (location.hash !== item.href) location.hash = item.href;
this._w_axis_focus = (item.sub === "w-axis");
// Defer the scroll so Vue mounts the inner Svelte page first.
const reset = () => {
// Force any inadvertent ancestor scroll back to 0 before
// we move .settings-content explicitly.
window.scrollTo(0, 0);
const body = document.querySelector(".app-body");
if (body) body.scrollTop = 0;
document.documentElement.scrollTop = 0;
};
setTimeout(() => {
reset();
const el = document.getElementById(item.anchor);
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
}, 250);
const scroller = document.querySelector(".settings-content");
if (el && scroller) {
const elTop = el.getBoundingClientRect().top;
const scTop = scroller.getBoundingClientRect().top;
scroller.scrollTop = scroller.scrollTop + (elTop - scTop) - 12;
}
// Re-assert ancestor scroll = 0 in case the assignment above
// moved things.
requestAnimationFrame(reset);
}, 320);
} else {
this._w_axis_focus = false;
if (location.hash !== item.href) location.hash = item.href;
// Reset .app-body scroll so each route starts at the top.
const body = document.querySelector(".app-body");
if (body) body.scrollTop = 0;
}
},

View File

@@ -1,14 +1,60 @@
// V09 wraps the legacy Svelte SettingsView and filters its big page
// down to a single rail section so each rail item shows only the
// relevant controls. The Svelte component is left untouched (it is
// shared with the legacy UI) — we just hide the `<h2>` and `<fieldset>`
// elements whose `data-sec` does not match the active section.
module.exports = {
template: "#settings-view-template",
attached: function() {
props: {
// "display" | "probing" | "gcode". Default is "display" which
// keeps the rail's "Display & Units" item working unchanged.
section: { default: "display" },
},
attached: function () {
this.svelteComponent = SvelteComponents.createComponent(
"SettingsView",
document.getElementById("settings")
);
// Defer one tick so Svelte has rendered the section markup.
setTimeout(() => this.apply_section_filter(), 0);
},
detached: function() {
this.svelteComponent.$destroy();
}
detached: function () {
if (this.svelteComponent) this.svelteComponent.$destroy();
},
watch: {
section: function () {
this.apply_section_filter();
},
},
methods: {
apply_section_filter: function () {
const root = document.getElementById("settings");
if (!root) return;
const want = this.section || "display";
// Hide every section block that does not match.
root.querySelectorAll("[data-sec]").forEach(el => {
el.style.display = el.dataset.sec === want ? "" : "none";
});
// Hide the global <h1>Settings</h1> on subsections so the
// page reads as a focused panel.
const h1 = root.querySelector(".settings-view > h1");
if (h1) {
if (want === "display") {
h1.textContent = "Display & Units";
} else if (want === "probing") {
h1.textContent = "Probing";
} else if (want === "gcode") {
h1.textContent = "G-code & Motion";
} else {
h1.textContent = "Settings";
}
}
},
},
};

20
src/js/w-axis-view.js Normal file
View File

@@ -0,0 +1,20 @@
"use strict";
// V09 W-axis page \u2014 mounts the existing WAxisSettings Svelte component
// inside the settings shell so it gets a real top-level rail entry
// instead of being a soft-link anchor inside Display & Units.
module.exports = {
template: "#w-axis-view-template",
attached: function () {
this.svelteComponent = SvelteComponents.createComponent(
"WAxisSettings",
document.getElementById("w-axis-mount")
);
},
detached: function () {
if (this.svelteComponent) this.svelteComponent.$destroy();
},
};

View File

@@ -9,7 +9,7 @@ html(lang="en")
style: include ../static/css/pure-min.css
style: include ../static/css/font-awesome.min.css
style: include ../static/css/fa6.min.css
style: include ../static/css/Audiowide.css
style: include ../static/css/clusterize.css
style: include ../svelte-components/node_modules/svelte-material-ui/bare.css
@@ -18,6 +18,51 @@ html(lang="en")
style: include:stylus ../stylus/style.styl
body(v-cloak)
// Tablet (kiosk) mode — pins the .app-shell to 1920x1080 and
// scales it to fit the actual viewport so the UI always looks
// exactly like the 10.8" 1920x1080 portable monitor.
//
// Toggle: ?tablet=1 to enable
// ?tablet=0 to disable
// Sticky in localStorage; once set, no querystring is needed.
script.
(function () {
try {
var p = new URLSearchParams(location.search);
if (p.has("tablet")) {
var on = p.get("tablet") !== "0" && p.get("tablet") !== "false";
localStorage.setItem("ui-tablet-mode", on ? "1" : "0");
}
if (localStorage.getItem("ui-tablet-mode") === "1") {
document.documentElement.classList.add("tablet-mode");
}
function fit() {
if (!document.documentElement.classList.contains("tablet-mode")) return;
var s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
document.documentElement.style.setProperty("--tablet-scale", s);
}
fit();
window.addEventListener("resize", fit);
// Kiosk mode: when the UI is loaded by the controller's
// own onboard browser (Chromium pointing at localhost on
// the Pi 3B at 1366x768), apply a tighter layout that
// packs the V09 UI into the smaller, slower display.
// Override with ?kiosk=0 to force the desktop layout.
if (p.has("kiosk")) {
var k = p.get("kiosk") !== "0" && p.get("kiosk") !== "false";
localStorage.setItem("ui-kiosk-mode", k ? "1" : "0");
}
var stored = localStorage.getItem("ui-kiosk-mode");
var auto = location.hostname === "localhost"
|| location.hostname === "127.0.0.1"
|| location.hostname === "::1";
if (stored === "1" || (stored !== "0" && auto)) {
document.documentElement.classList.add("kiosk-mode");
}
} catch (_e) {}
})();
#svelte-dialog-host
#overlay(v-if="status != 'connected'")
@@ -57,7 +102,7 @@ html(lang="en")
.pi-temp-warning(v-if="80 <= state.rpi_temp",
title="Raspberry Pi temperature too high.")
.fa.fa-thermometer-full
.fa.fa-temperature-full
span.state-badge(:class="state_class", :title="mach_state_full")
span.dot
@@ -75,7 +120,7 @@ html(lang="en")
.sp-val v{{config.full_version}}
a.sp-act(v-if="show_upgrade()", href="#admin-general")
| Upgrade to v{{latestVersion}}
.fa.fa-exclamation-circle.upgrade-attention
.fa.fa-circle-exclamation.upgrade-attention
.sp-row
.sp-icon: .fa.fa-network-wired
.sp-text
@@ -122,13 +167,22 @@ html(lang="en")
.fa.fa-save
| &nbsp;Save{{modified ? '*' : ''}}
// Routed view (no keep-alive: Vue 1 has issues re-evaluating
// dynamic :class / v-if bindings on cached components when the
// route changes within the same kept-alive tree)
// Routed view. We keep instances alive across tab swaps so:
// - The Program tab's WebGL <path-viewer> canvas does not
// get destroyed and recreated each time (which caused a
// dark flash as the GL context cleared the new canvas
// before its first frame).
// - The Program tab's clusterize.js gcode list does not
// re-virtualize from scratch on every visit.
// - The Settings shell's child Svelte components stay
// mounted, preserving any in-flight form state.
// The settings-shell handles its own inner v-if cascade so
// the Vue 1 reactivity quirk that motivated removing
// keep-alive earlier no longer applies here.
.app-body
component(:is="currentView + '-view'", :index="index",
:config="config", :template="template", :state="state",
:sub-tab="sub_tab")
:sub-tab="sub_tab", keep-alive)
message.error-message(:show.sync="errorShow")
div(slot="header")

View File

@@ -52,7 +52,7 @@ script#console-view-template(type="text/x-template")
// ----- Messages -----
.messages-pane(v-show="sub === 'messages'")
.msg-empty(v-if="!$root.messages_log.length")
.fa.fa-check-circle
.fa.fa-circle-check
| &nbsp;No messages.
.msg(v-for="m in $root.messages_log",
:class="m.level === 'warning' ? 'warn' : 'info'", track-by="$index")

View File

@@ -35,8 +35,9 @@ script#control-view-template(type="text/x-template")
button.pure-button.button-error(@click="GCodeNotFound=false") OK
message(:show.sync="show_probe_dialog")
h3(slot="header") Probe Rotary
h3(slot="header") Choose probe type
div(slot="body")
p Pick which probe routine to run.
button.pure-button(:class="state['pw'] ? '' : 'load-on'", @click="showProbeDialog('xyz')") Probe XYZ
button.pure-button(:class="state['pw'] ? '' : 'load-on'", @click="showProbeDialog('z')") Probe Z
div(slot="footer")
@@ -46,7 +47,9 @@ script#control-view-template(type="text/x-template")
.control-grid
// ===== JOG =====
.jog-card
// Hidden only while a G-code program is running / paused /
// stopping. Jogging / homing / MDI moves do not hide it.
.jog-card(v-if="!is_program_executing")
.jog-head
.jog-title
| Jog
@@ -89,21 +92,25 @@ 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 — W axis (auxcnc) when enabled
// Row 4 — W axis (auxcnc) when enabled.
// W- | W+ | Probe XYZ | Probe Z
// "Home W" lives in the DRO table's actions column on the
// right, so it doesn't need a tile here.
template(v-if="w.enabled")
button.jbtn(@click="aux_jog_incr(-1)", :disabled="!w.enabled")
.fa.fa-arrow-down.ico
span.lbl W
button.jbtn.ghost(@click="aux_home()", :disabled="!w.enabled")
span.lbl Home
span W
button.jbtn(@click="aux_jog_incr(+1)", :disabled="!w.enabled")
.fa.fa-arrow-up.ico
span.lbl W+
button.jbtn(@click="show_probe_dialog=true",
button.jbtn(@click="showProbeDialog('xyz')",
:class="{'load-on': !state['pw']}")
.fa.fa-bullseye.ico
span.lbl Probe
span.lbl Probe XYZ
button.jbtn(@click="showProbeDialog('z')",
:class="{'load-on': !state['pw']}")
.fa.fa-bullseye.ico
span.lbl Probe Z
// Row 4 — A axis (rotary) when no W and rotary is enabled
// (Vue 1 has no v-else-if; we negate w.enabled explicitly.)
@@ -129,16 +136,72 @@ script#control-view-template(type="text/x-template")
.fa.fa-bullseye.ico
span.lbl Probe XYZ
button.jbtn.ghost(@click="zero()", :disabled="!can_set_axis")
.fa.fa-map-marker.ico
.fa.fa-location-dot.ico
span.lbl Zero all
button.jbtn(@click="showProbeDialog('z')",
:class="{'load-on': !state['pw']}")
.fa.fa-bullseye.ico
span.lbl Probe Z
button.jbtn.ghost(@click="home()", :disabled="!is_idle")
button.jbtn.ghost(@click="home()")
.fa.fa-home.ico
span.lbl Home all
// ===== NOW RUNNING (replaces jog grid only while a G-code
// program is actually executing). Jogging is excluded.
.running-panel(v-if="is_program_executing")
.running-top
div
.running-file
.fa.fa-file-code
span(v-if="state.selected") &nbsp;{{state.selected}}
span(v-else) &nbsp;{{(mach_state || 'BUSY').toLowerCase()}}
.running-meta
span(v-if="is_running") {{ (mach_state || 'RUNNING').toLowerCase() }}
span(v-if="is_holding") paused
span(v-if="is_holding && pause_reason") · {{pause_reason}}
span(v-if="is_stopping") stopping
span(v-if="toolpath.lines") · line {{state.line || 0 | number}} / {{toolpath.lines | number}}
span(v-if="plan_time_remaining") · ETA {{plan_time_remaining | time}}
.running-pct
| {{((progress || 0) * 100) | fixed 0}}
span %
.running-progress
div(:style="'width:' + ((progress || 0) * 100) + '%'")
.running-stats
.running-stat
.lbl Velocity
.val
unit-value(:value="state.v", precision="2", unit="", iunit="", scale="0.0254")
| &nbsp;{{metric ? 'm/min' : 'IPM'}}
.running-stat
.lbl Feed
.val
unit-value(:value="state.feed", precision="0", unit="", iunit="")
| &nbsp;{{metric ? 'mm/min' : 'IPM'}}
.running-stat
.lbl Spindle
.val
| {{(state.speed || 0) | fixed 0}}
span(v-if="state.s != null && !isNaN(state.s)") ({{state.s | fixed 0}})
| &nbsp;RPM
.running-stat
.lbl Tool
.val T{{state.tool || 0}}
.running-row
// While RUNNING the primary action is Pause; while HOLDING / STOPPING it's Resume.
button.tx-btn.pause(v-if="is_running", @click="pause()")
.fa.fa-pause
span.lbl PAUSE
button.tx-btn.run(v-if="is_holding || is_stopping", @click="unpause()")
.fa.fa-play
span.lbl RESUME
button.tx-btn.stop(@click="stop()")
.fa.fa-stop
span.lbl STOP
button.tx-btn.step(v-if="is_holding", @click="step()")
.fa.fa-forward-step
span.lbl STEP
// ===== DRO + status strip =====
.right-col
@@ -174,11 +237,11 @@ script#control-view-template(type="text/x-template")
button.icon-btn(:disabled="!can_set_axis",
:title=`'Set ${axis.toUpperCase()} axis position.'`,
@click=`show_set_position('${axis}')`)
.fa.fa-cog
.fa.fa-gear
button.icon-btn(:disabled="!can_set_axis",
:title=`'Zero ${axis.toUpperCase()} axis offset.'`,
@click=`zero('${axis}')`)
.fa.fa-map-marker
.fa.fa-location-dot
button.icon-btn(:disabled="!is_idle",
:title=`'Home ${axis.toUpperCase()} axis.'`,
@click=`home('${axis}')`)
@@ -201,9 +264,9 @@ script#control-view-template(type="text/x-template")
| &nbsp;{{w.tstate}}
.actions-cell
button.icon-btn(disabled, style="visibility:hidden")
.fa.fa-cog
.fa.fa-gear
button.icon-btn(disabled, style="visibility:hidden")
.fa.fa-map-marker
.fa.fa-location-dot
button.icon-btn(:disabled="!w.enabled",
title="Home W axis.", @click="aux_home()")
.fa.fa-home

View File

@@ -6,11 +6,11 @@ script#indicators-template(type="text/x-template")
tr
td
.fa.fa-plus-circle.io
.fa.fa-circle-plus.io
th Hi/+3.3v
th.separator
td
.fa.fa-minus-circle.io
.fa.fa-circle-minus.io
th Lo/Gnd
th.separator
td
@@ -22,7 +22,7 @@ script#indicators-template(type="text/x-template")
th Inactive
th.separator
td
.fa.fa-circle-o.io
.far.fa-circle.io
th Tristated/Disabled
table.inputs
@@ -169,14 +169,14 @@ script#indicators-template(type="text/x-template")
tr
th Motor
th(title="Overtemperature fault"): .fa.fa-thermometer-full
th(title="Overtemperature fault"): .fa.fa-temperature-full
th(title="Overcurrent motor channel A") A #[.fa.fa-bolt]
th(title="Predriver fault motor channel A")
| A #[.fa.fa-exclamation-triangle]
| A #[.fa.fa-triangle-exclamation]
th(title="Overcurrent motor channel B") B #[.fa.fa-bolt]
th(title="Predriver fault motor channel B")
| B #[.fa.fa-exclamation-triangle]
th(title="Driver communication failure"): .fa.fa-handshake-o
| B #[.fa.fa-triangle-exclamation]
th(title="Driver communication failure"): .fa.fa-handshake
th(title="Reset all motor flags")
.fa.fa-eraser(@click="motor_reset()")

View File

@@ -3,7 +3,7 @@ script#path-viewer-template(type="text/x-template")
.path-viewer-toolbar
.tool-button(title="Toggle path view size.",
@click="small = !small", :class="{active: !small}")
.fa.fa-arrows-alt
.fa.fa-up-down-left-right
.tool-button(@click="showTool = !showTool", :class="{active: showTool}",
title="Show/hide tool.")

View File

@@ -82,7 +82,7 @@ script#program-view-template(type="text/x-template")
span STOP
button.action-btn(@click="open_folder", :disabled="!is_ready",
title="Upload a new GCode folder.")
.fa.fa-folder-arrow-up.ico
.fa.fa-folder-plus.ico
span UPLOAD FOLDER
form.gcode-folder-input.file-upload
input#folderInput(type="file", @change="upload_folder",
@@ -126,10 +126,15 @@ script#program-view-template(type="text/x-template")
.fa.fa-arrow-down-wide-short
| &nbsp;{{files_sortby}}
// Body: gcode listing on the left, 3D viewer on the right
.program-body
// Body: gcode listing on the left, 3D viewer on the right.
// The 3D path-viewer is suppressed when the UI is loaded by
// the Pi's onboard kiosk browser — the VideoCore IV cannot
// run three.js at a usable frame rate. Off-Pi clients still
// see the full split.
.program-body(:class="{'no-preview': is_kiosk}")
gcode-viewer
path-viewer(:toolpath="toolpath", :state="state", :config="config")
path-viewer(v-if="!is_kiosk", :toolpath="toolpath",
:state="state", :config="config")
.progress-bar(v-if="toolpath_progress && toolpath_progress < 1",
title="Simulating GCode to check for errors, calculate ETA and generate 3D view.")

View File

@@ -24,21 +24,35 @@ script#settings-shell-view-template(type="text/x-template")
// Explicit v-if cascade so the inner template swaps reactively
// when sub changes (Vue 1's `<component :is>` does not always
// re-evaluate dynamic strings inside a kept-alive parent).
settings-view-inner(v-if="sub === 'settings'",
// The Svelte settings views read many config keys eagerly on
// attach (settings.units, settings.easy-adapter, motion.*),
// so we gate the inner mount on config_ready.
settings-view-inner(v-if="sub === 'settings' && config_ready",
section="display",
:index="index", :config="config", :template="template", :state="state")
admin-general-view(v-if="sub === 'admin-general'",
settings-view-inner(v-if="sub === 'probing' && config_ready",
section="probing",
:index="index", :config="config", :template="template", :state="state")
admin-network-view(v-if="sub === 'admin-network'",
settings-view-inner(v-if="sub === 'gcode' && config_ready",
section="gcode",
:index="index", :config="config", :template="template", :state="state")
motor-view(v-if="sub === 'motor'",
admin-general-view(v-if="sub === 'admin-general' && config_ready",
:index="index", :config="config", :template="template", :state="state")
tool-view(v-if="sub === 'tool'",
admin-network-view(v-if="sub === 'admin-network' && config_ready",
:index="index", :config="config", :template="template", :state="state")
io-view(v-if="sub === 'io'",
motor-view(v-if="sub === 'motor' && config_ready",
:index="index", :config="config", :template="template", :state="state")
macros-view(v-if="sub === 'macros'",
tool-view(v-if="sub === 'tool' && config_ready",
:index="index", :config="config", :template="template", :state="state")
help-view(v-if="sub === 'help'",
io-view(v-if="sub === 'io' && config_ready",
:index="index", :config="config", :template="template", :state="state")
cheat-sheet-view(v-if="sub === 'cheat-sheet'",
w-axis-view(v-if="sub === 'w-axis' && config_ready",
:index="index", :config="config", :template="template", :state="state")
macros-view(v-if="sub === 'macros' && config_ready",
:index="index", :config="config", :template="template", :state="state")
help-view(v-if="sub === 'help' && config_ready",
:index="index", :config="config", :template="template", :state="state")
cheat-sheet-view(v-if="sub === 'cheat-sheet' && config_ready",
:index="index", :config="config", :template="template", :state="state")
.settings-loading(v-if="!config_ready")
| Loading configuration…

View File

@@ -433,4 +433,4 @@ script#tool-view-template(type="text/x-template")
| Other settings according to the
|
a(href="https://buildbotics.com/upload/vfd/stepperonline-v70.pdf",
target="_blank") Stepper Online V70 VFD manual
target="_blank") Stepper Online V70 VFD manual

View File

@@ -0,0 +1,4 @@
script#w-axis-view-template(type="text/x-template")
#w-axis-page
h1 W Axis (auxcnc)
#w-axis-mount

View File

@@ -154,22 +154,22 @@ class AuxAxis(object):
def home(self):
"""Run the homing cycle on the ESP. Blocks until done. Raises on
failure. Updates aux_homed and aux_pos."""
failure. Updates aux_homed and aux_pos.
The ESP's home_zero is pre-loaded via HOMECFG so when the cycle
completes the step counter already corresponds to home_position_mm.
That way the homed-state survives a bbctrl restart correctly
(we don't need a post-home WPOS write, which would clear HOMED)."""
self._require_present()
# Make sure home_zero on the ESP matches our current
# home_position_mm in case the user just edited config.
self._push_homecfg()
line = self._rpc('HOME', topic='home', timeout=120.0)
# line is the body after '[home] '
# line is the body after '[home] '. Only terminal lines use
# the [home] topic now (done / failed); progress is [home_log].
if line.startswith('done'):
# ESP set its counter to home_zero; mirror that.
new_pos = self._parse_kv_int(line, 'pos', 0)
self._pos_steps = new_pos
self._pos_steps = self._parse_kv_int(line, 'pos', 0)
self._homed = True
# Translate to home_position_mm. Conceptually the host says
# "after homing, W is here in mm". We achieve that by setting
# the ESP counter (WPOS) so the mm conversion works out.
target_pos = self._mm_to_steps(self._cfg['home_position_mm'])
if target_pos != new_pos:
self._rpc('WPOS %d' % target_pos, topic='ok', timeout=2.0)
self._pos_steps = target_pos
self._publish_state()
return
# failure
@@ -312,13 +312,15 @@ class AuxAxis(object):
def _push_homecfg(self):
c = self._cfg
zero_steps = self._mm_to_steps(c['home_position_mm'])
cmd = ('HOMECFG dir=%s fast=%d slow=%d backoff=%d maxtravel=%d '
'zero=0 accel=%d step_max=%d step_start=%d limit_low=%d') % (
'zero=%d accel=%d step_max=%d step_start=%d limit_low=%d') % (
c['home_dir'],
int(c['home_fast_sps']),
int(c['home_slow_sps']),
int(c['home_backoff_steps']),
int(c['home_maxtravel_steps']),
int(zero_steps),
int(c['step_accel_sps2']),
int(c['step_max_sps']),
int(c['step_start_sps']),

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

9
src/static/css/fa6.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -30,6 +30,16 @@ $jog-ghost-hov = #9ba6bb
$jog-ink = #fff
$jog-ghost-ink = $ink
// Lock html + body so nothing other than the explicit .app-body or
// inner scroll containers can scroll. Without this, autofocus inside
// nested Svelte components (Settings, Admin Network, etc.) can call
// scrollIntoView() on the html element and push our fixed header off
// the top of the viewport.
html, body
height 100%
overflow hidden
overscroll-behavior none
body
margin 0
font-family 'Inter', system-ui, -apple-system, sans-serif
@@ -78,6 +88,66 @@ tt
width 100%
overflow hidden
background $body-bg
// Hint to the browser that this layer is a stable composited
// surface so tab swaps inside it don't invalidate the whole page.
contain layout paint
isolation isolate
// Program tab pre-warmer. Mounts the program-view at app start so
// the WebGL canvas is already initialized when the user first
// clicks the Program tab avoids the first-time dark flash that
// happens when WebGL is created on demand. We render the component
// at full size off-screen with the same width as the live tab so
// the canvas dims match.
.program-warmer
position absolute
left -10000px
top 0
width 1920px
height 980px
pointer-events none
visibility hidden
// =====================================================================
// Tablet / kiosk mode
//
// When <html class="tablet-mode"> is set (via ?tablet=1, sticky in
// localStorage), pin the app shell to a fixed 1920 x 1080 box and
// scale it to fit the real viewport. The scale ratio is published as
// the --tablet-scale CSS variable by the inline script in index.pug.
//
// On the actual 10.8" 1920x1080 portable monitor the scale is 1; on
// a desktop browser at e.g. 1440x900 you get a faithfully shrunk
// preview of exactly what the kiosk renders.
// =====================================================================
html.tablet-mode
background #0f172a
overflow hidden
html.tablet-mode body
margin 0
width 100vw
height 100vh
overflow hidden
display flex
align-items center
justify-content center
html.tablet-mode .app-shell
width 1920px
height 1080px
flex 0 0 auto
transform scale(var(--tablet-scale, 1))
transform-origin center center
box-shadow 0 30px 60px rgba(0, 0, 0, 0.45)
border-radius 14px
// Keep dialog hosts above the scaled shell.
html.tablet-mode #svelte-dialog-host
position fixed
top 0
left 0
z-index 10000
.app-body
flex 1
@@ -100,7 +170,10 @@ tt
padding 0 24px
background $bg
border-bottom 1px solid $line
position relative
// sticky so the header stays visible even if a nested scroll
// container manages to move under it.
position sticky
top 0
z-index 30
.brand-blk
@@ -644,8 +717,12 @@ span.unit
text-transform capitalize
.path-viewer-content
background-color #333
background linear-gradient(to bottom, #666 0%, #222 100%);
// Solid dark background matching the WebGL renderer's clear
// colour. We used to use a gradient (#666 -> #222) but the
// visible-during-mount color difference between the gradient
// top and the GL clear bottom caused a brief dark flash when
// the canvas was reattached on every tab switch.
background-color #222
margin-bottom 0.5em
&.small
@@ -1073,6 +1150,150 @@ tt.save
font-size 0.85em
margin-left 2px
// =====================================================================
// NOW RUNNING panel
// Replaces the jog grid (left column) while the machine is not idle.
// Same outer dimensions as .jog-card so the rest of the layout doesn't
// reflow.
// =====================================================================
.control-page .running-panel
flex 1 1 auto
min-height 0
display flex
flex-direction column
gap 18px
padding 24px
border-radius 18px
color #fff
background linear-gradient(160deg, #0f172a 0%, #1e293b 100%)
.running-top
display flex
align-items flex-start
justify-content space-between
gap 18px
.running-file
font-size 1.6rem
font-weight 900
line-height 1.1
display flex
align-items center
gap 4px
.fa
color $accent
.running-meta
font-size 0.9rem
color #cbd5e1
font-weight 600
margin-top 6px
text-transform capitalize
.running-pct
font-family 'JetBrains Mono', monospace
font-size 3.6rem
font-weight 900
line-height 1
color #fff
span
font-size 2rem
color $accent
font-weight 800
margin-left 4px
.running-progress
height 14px
background rgba(255, 255, 255, 0.18)
border-radius 9999px
overflow hidden
width 100%
> div
height 100%
background $accent
transition width 0.4s ease-out
.running-stats
display grid
grid-template-columns repeat(4, 1fr)
gap 14px
.running-stat
background rgba(255, 255, 255, 0.06)
border-radius 14px
padding 14px 16px
.lbl
font-size 0.7rem
letter-spacing 0.14em
text-transform uppercase
color #cbd5e1
font-weight 700
.val
font-family 'JetBrains Mono', monospace
font-weight 900
font-size 1.5rem
margin-top 4px
color #fff
.running-row
display grid
grid-template-columns repeat(auto-fit, minmax(140px, 1fr))
gap 14px
margin-top auto
.running-row .tx-btn
display inline-flex
flex-direction column
align-items center
justify-content center
gap 6px
height 120px
border-radius 18px
border none
font-weight 800
letter-spacing 0.05em
cursor pointer
background #1e293b
color #fff
transition transform 0.06s, background 0.15s
.running-row .tx-btn .fa
font-size 2.4rem
.running-row .tx-btn .lbl
font-size 0.85rem
opacity 0.9
.running-row .tx-btn:hover
background #334155
.running-row .tx-btn:active
transform translateY(2px)
.running-row .tx-btn.pause
background #f59e0b
color #0f172a
.running-row .tx-btn.pause:hover
background #d97706
.running-row .tx-btn.run
background #16a34a
.running-row .tx-btn.run:hover
background #15803d
.running-row .tx-btn.stop
background #dc2626
.running-row .tx-btn.stop:hover
background #b91c1c
// Step segmented control
.step-seg
display inline-flex
@@ -1112,12 +1333,16 @@ tt.save
flex-direction column
align-items center
justify-content center
gap 4px
gap 6px
user-select none
-webkit-tap-highlight-color transparent
cursor pointer
font-weight 700
font-size 1.05rem
// Single sizing used by both the 1920x1080 portable touchscreen and
// the Pi 1366x768 kiosk large enough to be readable at arm's
// length on the smaller display, still proportionate on the bigger
// one. No mode-specific override.
font-size 1.6rem
border none
background $jog-bg
color $jog-ink
@@ -1126,13 +1351,13 @@ tt.save
min-width 0
.ico
font-size 1.6rem
font-size 2.4rem
.lbl
font-size 0.8rem
font-size 1.5rem
color inherit
opacity 0.85
font-weight 600
opacity 0.95
font-weight 700
&:hover:not([disabled])
background $jog-hover
@@ -1627,6 +1852,15 @@ tt.save
min-height 0
overflow hidden
// On the Pi's onboard kiosk browser the 3D toolpath preview is
// suppressed (Pi 3B's VideoCore IV can't run three.js fast
// enough), so the gcode listing claims the full width.
&.no-preview
grid-template-columns 1fr
> .gcode
border-right none
> .gcode
border-right 1px solid $line-soft
background #fafafa
@@ -1643,22 +1877,28 @@ tt.save
> .path-viewer
overflow hidden
min-height 0
height 100%
background #222 // matches the WebGL clear colour so any
// first-frame mismatch reads as the same dark
// panel rather than a flash.
display flex
flex-direction column
.path-viewer-content
flex 1 1 auto
width 100% !important
height auto !important
height 100% !important
min-height 0
float none !important
margin 0 !important
background #222
&.small .path-viewer-content
width 100% !important
height auto !important
height 100% !important
float none !important
margin 0 !important
background #222
.progress-bar
height 28px
@@ -2054,3 +2294,321 @@ tt.save
h1, h2, h3
margin-top 0
.settings-loading
color $muted
font-style italic
padding 24px
// =====================================================================
// KIOSK MODE compact layout for the controller's own onboard browser
// (Pi 3B at 1366x768). Activated by `html.kiosk-mode` (auto-applied
// when location.hostname is localhost). All overrides target the V09
// shell so the desktop / portable touchscreen layout is unaffected.
// =====================================================================
html.kiosk-mode
font-size 13px
.app-head
flex 0 0 56px
height 56px
padding 0 12px
gap 10px
.brand-blk
display none
.estop
transform scale(0.6)
transform-origin right center
.tabs-host
height 56px
padding-left 0
.ktab
height 56px
padding 0 14px
font-size 14px
gap 6px
.fa
font-size 16px
.ktab .ktab-underline,
.ktab.active::after
bottom 0
.app-body
padding 8px
gap 8px
// Control page: tighten everything
.control-page
gap 8px
// Keep two columns at 1366x768 vertical space is the constraint.
// Shrink the jog column from 720px to 540px so the DRO has more
// breathing room.
.control-page .control-grid
grid-template-columns 540px 1fr
gap 8px
.control-page .right-col
grid-template-rows 1fr 110px
gap 8px
.control-page .jog-card
padding 10px
.control-page .jog-head
margin-bottom 8px
.control-page .jog-title
font-size 14px
.control-page .jog-grid
gap 6px
.control-page .dro-head, .control-page .dro-row
grid-template-columns 56px 1fr 0.85fr 0.85fr 90px 90px 1fr
column-gap 0.4rem
padding 6px 10px
.control-page .dro-head
font-size 0.65rem
.control-page .dro-row
font-size 0.95rem
// Axis-action buttons in DRO rows (settings/zero/home).
.control-page .dro-row .icon-btn
width 56px
height 56px
font-size 1.25rem
border-radius 11px
.control-page .status-strip
grid-template-columns repeat(2, 1fr)
gap 8px
.control-page .stat-card
padding 8px 12px
.stat-label
font-size 9px
.stat-val
font-size 18px
margin-top 2px
.stat-sub
font-size 11px
margin-top 0
// Macros: 8 -> 4 column grid; shorter buttons.
.control-page .macro-row
grid-template-columns repeat(4, 1fr)
gap 6px
.macro-btn
height 56px
font-size 0.85rem
border-radius 10px
// Now-running panel: tighten paddings, smaller percent text.
.control-page .running-panel
padding 12px
gap 10px
.running-file
font-size 1.15rem
.running-pct
font-size 2.2rem
span
font-size 1.2rem
.running-stats
grid-template-columns repeat(2, 1fr)
gap 6px
.running-stat
padding 8px 10px
.val
font-size 1rem
// Program page: gcode listing fills (path-viewer is hidden via JS).
.program-page
gap 8px
// Settings shell: tighter rail.
.settings-shell .rail
padding 8px
.settings-shell .rail .rail-item
padding 8px 10px
font-size 0.9rem
.settings-content
padding 10px
// System pill / sidebar headers smaller.
.system-pill, .sidebar-pill
font-size 0.8rem
// Inside system-pill, the icon + text need explicit spacing.
.system-pill > * + *,
.sidebar-pill > * + *
margin-left 6px
// Modal dialogs scaled down for the smaller viewport.
.modal-bg
font-size 13px
// =====================================================================
// LEGACY-CHROMIUM FLEX-GAP FALLBACK
// =====================================================================
// Chromium 72 (which ships on the controller's Pi 3B) does not support
// `gap` on flexbox containers it landed in Chrome 84 (2020-05). Grid
// `gap` IS supported (Chrome 57+) so anything `display: grid` is fine.
//
// We can't use @supports not (gap: 1px) because Chromium 72 reports
// it supports `gap` (the spec considers it a grid-only property in
// older snapshots). Instead, we add explicit margin-based spacing for
// every known flex container in the V09 shell that visibly breaks on
// the Pi kiosk. The modern CSS gap rule still applies in newer Chrome
// these rules are inert (margin-left:auto rules elsewhere keep
// their meaning) because the gap pushes children apart anyway.
// App header brand block, tabs, system pill, estop
.app-head > * + *
margin-left 18px
.app-head .brand-blk > * + *
margin-left 14px
// Tabs ribbon the tab button itself uses flex+gap to space its
// icon and label. Without flex-gap support those collapse.
.ktab > * + *
margin-left 0.55rem
// Header system pill (sys-btn) and machine state badge
.sys-btn > * + *
margin-left 0.55rem
.state-badge > * + *
margin-left 0.6rem
// Jog card title row
.control-page .jog-head > * + *
margin-left 12px
// Now-running panel top, file/meta, stats, row, transport
.control-page .running-panel > * + *
margin-top 18px
.running-top > * + *
margin-left 18px
.running-row > * + *
margin-left 14px
.transport-row > * + *
margin-left 14px
// Console card tabs
.console-card .ptab-bar > * + *
margin-left 6px
// Settings shell rail and content
.settings-shell > * + *
margin-left 18px
// Macro buttons (.macro-btn icon + label)
.macro-btn > * + *
margin-left 0.6rem
// Jog buttons (.jbtn ico + lbl) column flex, vertical gap
.jbtn > * + *
margin-top 4px
// DRO actions cell already uses gap; emulate via margin
.actions-cell > * + *
margin-left 10px
// Generic ".header"-style flex rows in older subpages
.app-body .pure-form > * + *
margin-top 4px
// =====================================================================
// KIOSK-MODE-SPECIFIC LEGACY FALLBACKS
// =====================================================================
html.kiosk-mode
.app-head > * + *
margin-left 10px
.control-page
> * + *
margin-top 8px
.control-page .control-grid
// grid-gap works on Chromium 72 so nothing here.
.control-page .right-col
// grid-gap works.
grid-template-rows 1fr 158px // tighter than the desktop 158px
.running-row > * + *
margin-left 6px
.control-page .jog-head > * + *
margin-left 8px
// Settings rail must be scrollable in kiosk mode \u2014 the 14+
// item list overflows the 768px viewport at default heights.
.settings-shell
grid-template-columns 220px 1fr
gap 10px
.settings-rail
position static
align-self stretch
max-height 100%
overflow-y auto
.settings-rail .set-item
height 36px
font-size 0.85rem
padding 0 10px
.fa
width 14px
font-size 0.9rem
.settings-rail .set-section
margin 6px 4px 2px
font-size 0.62rem
.settings-rail .set-rail-foot
margin-top 4px
padding-top 6px
.sp-shutdown, .sp-save
height 32px
font-size 0.85rem
// Program tab flex-gap fallbacks for Chromium 72.
// Action bar (RUN/STOP/UPLOAD/.../DELETE) and the action buttons
// themselves (icon stacked over label).
.action-bar > * + *
margin-left 12px
.action-btn > * + *
margin-top 4px
// File bar (Create Folder / folder select / file select / sort).
.file-bar > * + *
margin-left 10px
.file-btn > * + *
margin-left 0.4rem

View File

@@ -2,7 +2,10 @@
import configTemplate from "../../../resources/config-template.json";
import ScreenRotationDialog from "$dialogs/ScreenRotationDialog.svelte";
import ConfigTemplatedInput from "./ConfigTemplatedInput.svelte";
import WAxisSettings from "./WAxisSettings.svelte";
// WAxisSettings is mounted directly by the V09 settings shell at
// #w-axis instead of being embedded here — see
// src/pug/templates/w-axis-view.pug.
// import WAxisSettings from "./WAxisSettings.svelte";
import SetTimeDialog from "$dialogs/SetTimeDialog.svelte";
import Button, { Label } from "@smui/button";
@@ -19,8 +22,8 @@
<h1>Settings</h1>
<div class="pure-form pure-form-aligned">
<h2>User Interface</h2>
<fieldset>
<h2 id="sec-display" data-sec="display">User Interface</h2>
<fieldset data-sec="display">
<div class="pure-control-group">
<label for="screen-rotation" />
<Button
@@ -46,8 +49,8 @@
</div> -->
</fieldset>
<h2>Units</h2>
<fieldset>
<h2 id="sec-units" data-sec="display">Units</h2>
<fieldset data-sec="display">
<ConfigTemplatedInput key={`settings.units`} />
<div class="tip">
Note, units sets both the machine default units and the units used in motor configuration. GCode program-start,
@@ -55,13 +58,13 @@
</div>
</fieldset>
<h2>Easy Adapter</h2>
<fieldset>
<h2 id="sec-easy-adapter" data-sec="display">Easy Adapter</h2>
<fieldset data-sec="display">
<ConfigTemplatedInput key={`settings.easy-adapter`} />
</fieldset>
<h2>Probing</h2>
<fieldset>
<h2 id="sec-probing" data-sec="probing">Probing</h2>
<fieldset data-sec="probing">
<ConfigTemplatedInput key={`settings.probing-prompts`} />
<div class="tip">
Onefinity highly recommends that you keep the safety prompts
@@ -88,20 +91,19 @@
{/each}
</fieldset>
<fieldset>
<h2>GCode</h2>
<fieldset data-sec="gcode">
<h2 id="sec-gcode" data-sec="gcode">GCode</h2>
{#each Object.keys(configTemplate.gcode) as key}
<ConfigTemplatedInput key={`gcode.${key}`} />
{/each}
</fieldset>
<h2 id="w-axis">W Axis (auxcnc)</h2>
<fieldset>
<WAxisSettings />
</fieldset>
<!-- W Axis (auxcnc) is now its own routed page in the V09
settings shell (#w-axis). Keep the SettingsView free of
that section so we don't render it twice. -->
<h2>Path Accuracy</h2>
<fieldset>
<h2 id="sec-path-accuracy" data-sec="gcode">Path Accuracy</h2>
<fieldset data-sec="gcode">
<ConfigTemplatedInput key={`settings.max-deviation`} />
<div class="tip">
@@ -124,8 +126,8 @@
</div>
</fieldset>
<h2>Cornering Speed (Advanced)</h2>
<fieldset>
<h2 id="sec-cornering" data-sec="gcode">Cornering Speed (Advanced)</h2>
<fieldset data-sec="gcode">
<ConfigTemplatedInput key={`settings.junction-accel`} />
<div class="tip">
Junction acceleration limits the cornering speed the planner

View File

@@ -51,7 +51,7 @@
>
<div slot="trailingIcon">
{#if valid}
<Icon class="fa fa-check-circle-o" style="color: green;" />
<Icon class="fa fa-circle-check" style="color: green;" />
{/if}
</div>
<HelperText persistent slot="helper">{helperText}</HelperText>

View File

@@ -6,6 +6,7 @@ matchAll.shim();
import AdminNetworkView from "$components/AdminNetworkView.svelte";
import SettingsView from "$components/SettingsView.svelte";
import HelpView from "$components/HelpView.svelte";
import WAxisSettings from "$components/WAxisSettings.svelte";
import DialogHost, { showDialog } from "$dialogs/DialogHost.svelte";
import { handleConfigUpdate, setDisplayUnits } from "$lib/ConfigStore";
import { handleControllerStateUpdate } from "$lib/ControllerState";
@@ -22,6 +23,9 @@ export function createComponent(component: string, target: HTMLElement, props: R
case "HelpView":
return new HelpView({ target, props });
case "WAxisSettings":
return new WAxisSettings({ target, props });
case "DialogHost":
return new DialogHost({ target, props });