Compare commits
4 Commits
3d73e6c59d
...
19e6cc6c93
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19e6cc6c93 | ||
|
|
50839718e2 | ||
| 68a92bb297 | |||
|
|
41d720c1d0 |
6
Makefile
6
Makefile
@@ -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
|
||||
|
||||
@@ -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 "🚚 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 "Deployed to http://${HOST}/"
|
||||
echo " Logs: ssh ${REMOTE_USER}@${HOST} 'journalctl -u bbctrl -f'"
|
||||
echo " Open: open -a 'Google Chrome' http://${HOST}/"
|
||||
|
||||
102
scripts/deploy/patch_font_mime.py
Normal file
102
scripts/deploy/patch_font_mime.py
Normal 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())
|
||||
@@ -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") {
|
||||
|
||||
@@ -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"}`;
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
// 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);
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
|
||||
@@ -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; },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
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();
|
||||
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
20
src/js/w-axis-view.js
Normal 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();
|
||||
},
|
||||
};
|
||||
@@ -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
|
||||
| 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")
|
||||
|
||||
@@ -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
|
||||
| No messages.
|
||||
.msg(v-for="m in $root.messages_log",
|
||||
:class="m.level === 'warning' ? 'warn' : 'info'", track-by="$index")
|
||||
|
||||
@@ -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") {{state.selected}}
|
||||
span(v-else) {{(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")
|
||||
| {{metric ? 'm/min' : 'IPM'}}
|
||||
.running-stat
|
||||
.lbl Feed
|
||||
.val
|
||||
unit-value(:value="state.feed", precision="0", unit="", iunit="")
|
||||
| {{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}})
|
||||
| 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")
|
||||
| {{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
|
||||
|
||||
@@ -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()")
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
| {{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.")
|
||||
|
||||
@@ -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…
|
||||
|
||||
4
src/pug/templates/w-axis-view.pug
Normal file
4
src/pug/templates/w-axis-view.pug
Normal file
@@ -0,0 +1,4 @@
|
||||
script#w-axis-view-template(type="text/x-template")
|
||||
#w-axis-page
|
||||
h1 W Axis (auxcnc)
|
||||
#w-axis-mount
|
||||
@@ -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.
BIN
src/resources/webfonts/fa-brands-400.ttf
Normal file
BIN
src/resources/webfonts/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
src/resources/webfonts/fa-brands-400.woff2
Normal file
BIN
src/resources/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
src/resources/webfonts/fa-regular-400.ttf
Normal file
BIN
src/resources/webfonts/fa-regular-400.ttf
Normal file
Binary file not shown.
BIN
src/resources/webfonts/fa-regular-400.woff2
Normal file
BIN
src/resources/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
src/resources/webfonts/fa-solid-900.ttf
Normal file
BIN
src/resources/webfonts/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
src/resources/webfonts/fa-solid-900.woff2
Normal file
BIN
src/resources/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
9
src/static/css/fa6.min.css
vendored
Normal file
9
src/static/css/fa6.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
src/static/css/font-awesome.min.css
vendored
4
src/static/css/font-awesome.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user