UX redesign V09: replace shell, split Program/Console/Settings
Implements the V09 mock end-to-end (per plans/2026-04-30_ux_redesign.md):
Top shell
- index.pug rebuilt around .app-shell with a slim 96px header.
- Underline-ribbon tab bar (Control / Program / Console / Settings)
replaces the old side menu and the inline #tab1..#tab4 system.
- Single 'All systems' pill collapses the legacy WiFi/Camera/Rotary/
IP/Version chip-soup into one popover (sys-popover) anchored to the
header; rotary toggle, camera feed and shutdown live there.
- Octagonal 88x88px STOP button wraps the existing <estop> SVG; STATE
pill with pulse-dot honors prefers-reduced-motion.
Routing
- app.js parse_hash maps every existing hash:
#control -> Control
#program / #program:auto -> Program
#console / #console:mdi|messages|indicators -> Console
#settings, #admin-general,
#admin-network, #motor:N, #tool, #io, #macros, #help,
#cheat-sheet -> Settings (rail picks inner)
- All deep links are preserved.
Control panel (control-view.pug + .js)
- 720px jog grid + 4-axis DRO + 4 KPI cards + 8-macro row.
- Jog tiles use V09 flat slate (#3f4b63) with diagonal helpers and
a ghost row for XY/Z origin shortcuts.
- Per-axis Settings/Set-zero/Home buttons grow to 72x72px.
- Status strip cards: State / Velocity-Feed / Spindle / Job. Tapping
the Spindle card opens the new override-drawer with feed + spindle
range inputs (resolved decision in plans/...).
- Macro row binds to state.macros.slice(0, 8); >8 lives in Settings.
- Drops the old <table> control-buttons, .info, .override and .tabs
blocks entirely.
Program panel (program-view.pug + .js)
- Extracts the Auto bar, file selectors, gcode-viewer and path-viewer
out of control-view.
- Action buttons (RUN/STOP/UPLOAD-FOLDER/UPLOAD-FILE/DOWNLOAD-FILE/
DELETE) at 84px with explicit color affordances.
- Reuses control-view's existing methods via the new program-mixin.
Console panel (console-view.pug + .js)
- Three sub-tabs: MDI / Messages / Indicators. Sub-tab persists in the
URL fragment (#console:messages etc.).
- MDI: terminal-style prompt + SEND, plus an 8-wide on-screen keypad
(G0/G1/G2/G3/G28/G92/M3/M5 + axis letters + CLEAR/SEND).
- Messages: pulls from .messages_log (mirrored from
state.messages); badge in the header tab counts unread.
- Indicators: mounts the existing <indicators> component.
Settings shell (settings-shell.pug + .js)
- New left rail navigator listing Display, Network, General/Firmware,
Spindle&Tool, IO, Motors 0..3, Macros, Cheat Sheet, Help.
- Inner area mounts the existing settings family templates via an
explicit v-if cascade (avoiding a Vue 1 :is reactivity quirk).
- Shutdown / Save buttons relocated from the dropped side menu.
JS plumbing
- main.js: Vue.config.async = false to keep dependent watchers in
sync when reactive data is mutated outside Vue's normal event loop
(e.g. from a hashchange listener).
- program-mixin.js extracted so control-view.js no longer carries the
file/macro/gcode methods that are now Program-only.
- control-view.js trimmed to jog/DRO/probe/home logic.
- console-view.js / settings-shell-view.js use a hashchange listener
+ local data props because Vue 1 cannot reliably observe
.sub_tab from a child component.
Stylus rewrite
- Removes the old .header (140px), .nav-header, .brand subtree, #menu,
#main, .control-view block, .info, .override, .toolbar, .macros-div,
.macros-button, the .tabs > input radio-tab system and the .control-
view #control media-query overrides. None of these are referenced
any more.
- Adds V09 tokens (jog/macro palette + accent + line/card colors) at
the top, the new shell rules, .ktab / .sys-btn / .state-badge /
.estop chrome, the .control-page grid, status strip + override
drawer, .program-page action / file bars and program body,
.console-page MDI keypad / messages / indicators panes, and the
.settings-shell rail.
- Adds a 1820px breakpoint that stacks the right column under the jog
on smaller portable monitors.
Hard cut: no config.ui.layout flag, the old shell is removed in this
single commit. side-menu.css is no longer included from index.pug.
Tested locally with agent-browser (1920x1080) on every top tab and
every settings sub-route; routing, active tab highlighting and inner
view selection all work without a controller connection.
This commit is contained in:
208
src/js/app.js
208
src/js/app.js
@@ -103,6 +103,17 @@ module.exports = new Vue({
|
|||||||
return {
|
return {
|
||||||
status: "connecting",
|
status: "connecting",
|
||||||
currentView: "loading",
|
currentView: "loading",
|
||||||
|
// Top-level shell tab. Mapped from the URL hash by parse_hash().
|
||||||
|
// One of: control | program | console | settings
|
||||||
|
top_tab: "control",
|
||||||
|
// Sub-route when a tab has internal pages (e.g. console:mdi,
|
||||||
|
// settings:admin-network, settings:motor:0). The settings sub
|
||||||
|
// also drives which inner view is mounted.
|
||||||
|
sub_tab: "",
|
||||||
|
sys_open: false,
|
||||||
|
has_camera: true,
|
||||||
|
messages_log: [],
|
||||||
|
messages_seen: 0,
|
||||||
display_units: localStorage.getItem("display_units") || "METRIC",
|
display_units: localStorage.getItem("display_units") || "METRIC",
|
||||||
index: -1,
|
index: -1,
|
||||||
modified: false,
|
modified: false,
|
||||||
@@ -143,22 +154,15 @@ module.exports = new Vue({
|
|||||||
estop: { template: "#estop-template" },
|
estop: { template: "#estop-template" },
|
||||||
"loading-view": { template: "<h1>Loading...</h1>" },
|
"loading-view": { template: "<h1>Loading...</h1>" },
|
||||||
"control-view": require("./control-view"),
|
"control-view": require("./control-view"),
|
||||||
"settings-view": require("./settings-view"),
|
"program-view": require("./program-view"),
|
||||||
"motor-view": require("./motor-view"),
|
"console-view": require("./console-view"),
|
||||||
"tool-view": require("./tool-view"),
|
|
||||||
"io-view": require("./io-view"),
|
// The settings-shell renders the rail + an inner routed view.
|
||||||
"admin-general-view": require("./admin-general-view"),
|
// All settings-family hashes (settings, admin-general,
|
||||||
"admin-network-view": require("./admin-network-view"),
|
// admin-network, motor:N, tool, io, macros, help, cheat-sheet)
|
||||||
"macros-view": require('./macros'),
|
// resolve to this same shell; parse_hash() sets sub_tab so the
|
||||||
"help-view": require("./help-view"),
|
// shell knows which inner template to mount.
|
||||||
"cheat-sheet-view": {
|
"settings-shell-view": require("./settings-shell-view"),
|
||||||
template: "#cheat-sheet-view-template",
|
|
||||||
data: function() {
|
|
||||||
return {
|
|
||||||
showUnimplemented: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@@ -166,6 +170,25 @@ module.exports = new Vue({
|
|||||||
localStorage.setItem("display_units", value);
|
localStorage.setItem("display_units", value);
|
||||||
SvelteComponents.setDisplayUnits(value);
|
SvelteComponents.setDisplayUnits(value);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Mirror controller messages into a console log used by the
|
||||||
|
// Console > Messages tab and the header badge counter.
|
||||||
|
"state.messages": {
|
||||||
|
handler: function(messages) {
|
||||||
|
if (!Array.isArray(messages)) return;
|
||||||
|
this.messages_log = messages.map(m => ({
|
||||||
|
text: m.text,
|
||||||
|
id: m.id,
|
||||||
|
level: /^#/.test(m.text || "") ? "info" : "warning",
|
||||||
|
ts: m.ts || Date.now(),
|
||||||
|
}));
|
||||||
|
if (this.top_tab === "console" && this.sub_tab === "messages") {
|
||||||
|
this.messages_seen = this.messages_log.length;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
events: {
|
events: {
|
||||||
@@ -252,18 +275,106 @@ module.exports = new Vue({
|
|||||||
enable_rotary: function() {
|
enable_rotary: function() {
|
||||||
if(this.state["2an"] == 1 || this.state["2an"] == 3) return true;
|
if(this.state["2an"] == 1 || this.state["2an"] == 3) return true;
|
||||||
return false;
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------------- header chrome helpers ----------------
|
||||||
|
|
||||||
|
// Underlying machine state from the controller. Mirrors
|
||||||
|
// control-view's `mach_state` so the header has access without
|
||||||
|
// depending on the routed component.
|
||||||
|
mach_state: function() {
|
||||||
|
const cycle = this.state.cycle;
|
||||||
|
const xx = this.state.xx;
|
||||||
|
|
||||||
|
if (xx != "ESTOPPED" && (cycle == "jogging" || cycle == "homing")) {
|
||||||
|
return cycle.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return xx || "";
|
||||||
|
},
|
||||||
|
|
||||||
|
// Short text for the READY pill in the header.
|
||||||
|
state_label: function() {
|
||||||
|
const s = this.mach_state;
|
||||||
|
if (!s) return "--";
|
||||||
|
return s;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Class added to the READY pill (.state-badge) so styling can
|
||||||
|
// reflect ready / running / holding / fault / estop.
|
||||||
|
state_class: function() {
|
||||||
|
const s = this.mach_state;
|
||||||
|
if (s == "ESTOPPED" || s == "FAULT" || s == "SHUTDOWN") return "bad";
|
||||||
|
if (s == "HOLDING" || s == "STOPPING") return "warn";
|
||||||
|
if (s == "RUNNING" || s == "HOMING" || s == "JOGGING") return "busy";
|
||||||
|
if (s == "READY") return "ok";
|
||||||
|
return "unknown";
|
||||||
|
},
|
||||||
|
|
||||||
|
mach_state_full: function() {
|
||||||
|
const s = this.mach_state;
|
||||||
|
if (s == "ESTOPPED") return "E-Stopped \u2014 release to clear";
|
||||||
|
if (s == "HOLDING") return "Feed hold (" + (this.state.pr || "paused") + ")";
|
||||||
|
if (s == "RUNNING") return "Running program";
|
||||||
|
if (s == "HOMING") return "Homing axes";
|
||||||
|
if (s == "JOGGING") return "Jogging";
|
||||||
|
if (s == "READY") return "Ready";
|
||||||
|
return s;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Pip color for the unified system pill.
|
||||||
|
sys_class: function() {
|
||||||
|
const wifi_off = !this.config.wifiName || this.config.wifiName == "not connected";
|
||||||
|
const cam_off = !this.has_camera;
|
||||||
|
const hot = this.state && 80 <= this.state.rpi_temp;
|
||||||
|
if (hot) return "red";
|
||||||
|
if (wifi_off || cam_off) return "amber";
|
||||||
|
return "green";
|
||||||
|
},
|
||||||
|
|
||||||
|
// Compact summary for the system pill.
|
||||||
|
sys_summary: function() {
|
||||||
|
const issues = [];
|
||||||
|
if (!this.config.wifiName || this.config.wifiName == "not connected") {
|
||||||
|
issues.push("WiFi off");
|
||||||
|
}
|
||||||
|
if (!this.has_camera) issues.push("Camera offline");
|
||||||
|
if (this.state && 80 <= this.state.rpi_temp) issues.push("Pi hot");
|
||||||
|
if (this.is_rotary_active) issues.push("Rotary");
|
||||||
|
if (issues.length === 0) return "All systems";
|
||||||
|
if (issues.length === 1) return issues[0];
|
||||||
|
return issues.length + " notes";
|
||||||
|
},
|
||||||
|
|
||||||
|
// Number of unread Console > Messages entries.
|
||||||
|
messages_count: function() {
|
||||||
|
return Math.max(0, this.messages_log.length - this.messages_seen);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
ready: function() {
|
ready: function() {
|
||||||
window.onhashchange = () => this.parse_hash();
|
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
|
||||||
|
// first config is in.
|
||||||
|
this.parse_hash();
|
||||||
|
|
||||||
this.connect();
|
this.connect();
|
||||||
|
|
||||||
|
// Close the system popover when clicking anywhere else.
|
||||||
|
document.addEventListener("click", () => {
|
||||||
|
if (this.sys_open) this.sys_open = false;
|
||||||
|
});
|
||||||
|
|
||||||
SvelteComponents.registerControllerMethods({
|
SvelteComponents.registerControllerMethods({
|
||||||
dispatch: (...args) => this.$dispatch(...args)
|
dispatch: (...args) => this.$dispatch(...args)
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
block_error_dialog: function() {
|
block_error_dialog: function() {
|
||||||
this.errorTimeoutStart = Date.now();
|
this.errorTimeoutStart = Date.now();
|
||||||
@@ -421,6 +532,21 @@ module.exports = new Vue({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Maps a URL hash to (currentView, top_tab, sub_tab, index).
|
||||||
|
// Hash layouts supported (all kept for backward compat):
|
||||||
|
// #control -> control tab
|
||||||
|
// #program[:auto] -> program tab
|
||||||
|
// #console[:mdi|messages|indicators]
|
||||||
|
// -> console tab
|
||||||
|
// #settings -> settings tab home
|
||||||
|
// #admin-general -> settings tab, admin-general inside
|
||||||
|
// #admin-network -> settings tab, admin-network inside
|
||||||
|
// #motor:0..3 -> settings tab, motor 0..3
|
||||||
|
// #tool -> settings tab, tool view
|
||||||
|
// #io -> settings tab, io view
|
||||||
|
// #macros -> settings tab, macros view
|
||||||
|
// #help -> settings tab, help view
|
||||||
|
// #cheat-sheet -> settings tab, cheat sheet view
|
||||||
parse_hash: function() {
|
parse_hash: function() {
|
||||||
const hash = location.hash.substr(1);
|
const hash = location.hash.substr(1);
|
||||||
|
|
||||||
@@ -430,12 +556,56 @@ module.exports = new Vue({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parts = hash.split(":");
|
const parts = hash.split(":");
|
||||||
|
const head = parts[0];
|
||||||
|
|
||||||
if (parts.length == 2) {
|
this.index = parts.length > 1 ? parts[1] : -1;
|
||||||
this.index = parts[1];
|
|
||||||
|
// Legacy / settings-managed views resolve under the
|
||||||
|
// Settings tab while keeping their existing top-level
|
||||||
|
// hash. This preserves all existing deep links.
|
||||||
|
const settingsViews = [
|
||||||
|
"settings", "admin-general", "admin-network",
|
||||||
|
"motor", "tool", "io", "macros",
|
||||||
|
"help", "cheat-sheet",
|
||||||
|
];
|
||||||
|
|
||||||
|
if (head == "control") {
|
||||||
|
this.top_tab = "control";
|
||||||
|
this.sub_tab = "";
|
||||||
|
this.currentView = "control";
|
||||||
|
} else if (head == "program") {
|
||||||
|
this.top_tab = "program";
|
||||||
|
this.sub_tab = parts[1] || "auto";
|
||||||
|
this.currentView = "program";
|
||||||
|
} else if (head == "console") {
|
||||||
|
this.top_tab = "console";
|
||||||
|
this.sub_tab = parts[1] || "mdi";
|
||||||
|
this.currentView = "console";
|
||||||
|
} else if (settingsViews.indexOf(head) !== -1) {
|
||||||
|
this.top_tab = "settings";
|
||||||
|
this.sub_tab = head;
|
||||||
|
// All settings-family routes mount the same shell;
|
||||||
|
// shell picks inner view from sub_tab. Vary the
|
||||||
|
// currentView token so Vue 1 fully remounts the
|
||||||
|
// shell on every navigation — this avoids stale :class
|
||||||
|
// bindings against the local `sub` data prop.
|
||||||
|
this.currentView = "settings-shell";
|
||||||
|
} else {
|
||||||
|
// Unknown hash: route to settings shell anyway so we
|
||||||
|
// never end up rendering a bare loading screen.
|
||||||
|
this.top_tab = "settings";
|
||||||
|
this.sub_tab = head;
|
||||||
|
this.currentView = "settings-shell";
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentView = parts[0];
|
// Mark Console messages as seen when we enter that tab.
|
||||||
|
if (this.top_tab == "console" && this.sub_tab == "messages") {
|
||||||
|
this.messages_seen = this.messages_log.length;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggle_sys_popover: function() {
|
||||||
|
this.sys_open = !this.sys_open;
|
||||||
},
|
},
|
||||||
|
|
||||||
save: async function() {
|
save: async function() {
|
||||||
|
|||||||
125
src/js/console-view.js
Normal file
125
src/js/console-view.js
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const api = require("./api");
|
||||||
|
|
||||||
|
// Console tab — MDI command input, message log and live indicators.
|
||||||
|
// Sub-tab state syncs with the URL hash (#console:mdi |
|
||||||
|
// #console:messages | #console:indicators) so deep links work.
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
template: "#console-view-template",
|
||||||
|
props: ["config", "template", "state"],
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
mdi: "",
|
||||||
|
history: [],
|
||||||
|
sub: "mdi",
|
||||||
|
// Local mirror of $root.messages_count so Vue 1 reactivity works.
|
||||||
|
unread_messages_local: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
sub: function () {
|
||||||
|
// Switching to messages marks them as seen so the header badge
|
||||||
|
// clears.
|
||||||
|
if (this.sub === "messages") {
|
||||||
|
this.$root.messages_seen = this.$root.messages_log.length;
|
||||||
|
this.unread_messages_local = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
unread_messages: function () {
|
||||||
|
return this.unread_messages_local;
|
||||||
|
},
|
||||||
|
|
||||||
|
mach_state: function () {
|
||||||
|
const cycle = this.state.cycle;
|
||||||
|
const xx = this.state.xx;
|
||||||
|
if (xx != "ESTOPPED" && (cycle == "jogging" || cycle == "homing")) {
|
||||||
|
return cycle.toUpperCase();
|
||||||
|
}
|
||||||
|
return xx || "";
|
||||||
|
},
|
||||||
|
|
||||||
|
is_idle: function () { return this.state.cycle == "idle"; },
|
||||||
|
|
||||||
|
can_mdi: function () {
|
||||||
|
return this.is_idle || this.state.cycle == "mdi";
|
||||||
|
},
|
||||||
|
|
||||||
|
mach_units: function () {
|
||||||
|
return this.$root.display_units;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ready: function () {
|
||||||
|
this._onHash = () => this.refresh_from_hash();
|
||||||
|
window.addEventListener("hashchange", this._onHash);
|
||||||
|
this.refresh_from_hash();
|
||||||
|
this._poll = setInterval(() => {
|
||||||
|
// Cheap re-poll for unread message count; Vue 1 cannot observe
|
||||||
|
// `$root.messages_count` directly so we mirror it here.
|
||||||
|
const c = this.$root && this.$root.messages_count;
|
||||||
|
if (typeof c === "number" && c !== this.unread_messages_local) {
|
||||||
|
this.unread_messages_local = c;
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy: function () {
|
||||||
|
if (this._onHash) window.removeEventListener("hashchange", this._onHash);
|
||||||
|
if (this._poll) clearInterval(this._poll);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
refresh_from_hash: function () {
|
||||||
|
const hash = location.hash.substr(1);
|
||||||
|
const parts = hash.split(":");
|
||||||
|
const sub = parts[0] === "console" ? (parts[1] || "mdi") : "mdi";
|
||||||
|
this.sub = sub;
|
||||||
|
if (sub === "messages" && this.$root) {
|
||||||
|
this.$root.messages_seen = this.$root.messages_log.length;
|
||||||
|
this.unread_messages_local = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
select_sub: function (name) {
|
||||||
|
this.sub = name;
|
||||||
|
// Update URL hash for deep links / back-button.
|
||||||
|
const h = "#console" + (name && name !== "mdi" ? ":" + name : "");
|
||||||
|
if (location.hash !== h) {
|
||||||
|
history.replaceState(null, "", h);
|
||||||
|
}
|
||||||
|
if (name === "messages") {
|
||||||
|
this.$root.messages_seen = this.$root.messages_log.length;
|
||||||
|
this.unread_messages_local = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
prepend: function (token) {
|
||||||
|
this.mdi = token + this.mdi.trimStart();
|
||||||
|
},
|
||||||
|
|
||||||
|
append: function (token) {
|
||||||
|
const tail = this.mdi.endsWith(" ") || !this.mdi ? "" : " ";
|
||||||
|
this.mdi = this.mdi + tail + token;
|
||||||
|
},
|
||||||
|
|
||||||
|
submit_mdi: function () {
|
||||||
|
if (!this.mdi) return;
|
||||||
|
this.$dispatch("send", this.mdi);
|
||||||
|
if (!this.history.length || this.history[0] != this.mdi) {
|
||||||
|
this.history.unshift(this.mdi);
|
||||||
|
}
|
||||||
|
this.mdi = "";
|
||||||
|
},
|
||||||
|
|
||||||
|
load_history: function (index) {
|
||||||
|
this.mdi = this.history[index];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const api = require("./api");
|
const api = require("./api");
|
||||||
const utils = require("./utils");
|
|
||||||
const cookie = require("./cookie")("bbctrl-");
|
const cookie = require("./cookie")("bbctrl-");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -12,15 +11,7 @@ module.exports = {
|
|||||||
return {
|
return {
|
||||||
current_time: "",
|
current_time: "",
|
||||||
mach_units: this.$root.state.metric ? "METRIC" : "IMPERIAL",
|
mach_units: this.$root.state.metric ? "METRIC" : "IMPERIAL",
|
||||||
mdi: "",
|
|
||||||
last_file: undefined,
|
|
||||||
last_file_time: undefined,
|
|
||||||
toolpath: {},
|
|
||||||
toolpath_progress: 0,
|
|
||||||
axes: "xyzabc",
|
axes: "xyzabc",
|
||||||
history: [],
|
|
||||||
speed_override: 1,
|
|
||||||
feed_override: 1,
|
|
||||||
jog_incr_amounts: {
|
jog_incr_amounts: {
|
||||||
METRIC: {
|
METRIC: {
|
||||||
fine: 0.1,
|
fine: 0.1,
|
||||||
@@ -38,34 +29,14 @@ module.exports = {
|
|||||||
jog_incr: localStorage.getItem("jog_incr") || "small",
|
jog_incr: localStorage.getItem("jog_incr") || "small",
|
||||||
jog_step: cookie.get_bool("jog-step"),
|
jog_step: cookie.get_bool("jog-step"),
|
||||||
jog_adjust: parseInt(cookie.get("jog-adjust", 2)),
|
jog_adjust: parseInt(cookie.get("jog-adjust", 2)),
|
||||||
deleteGCode: false,
|
|
||||||
tab: "auto",
|
|
||||||
ask_home: true,
|
ask_home: true,
|
||||||
folder_name: "",
|
|
||||||
edited: false,
|
|
||||||
uploading_files: false,
|
|
||||||
confirmDelete: false,
|
|
||||||
create_folder: false,
|
|
||||||
showGcodeMessage: false,
|
|
||||||
showNoGcodeMessage: false,
|
|
||||||
macrosLoading: false,
|
|
||||||
show_gcodes: false,
|
|
||||||
GCodeNotFound: false,
|
|
||||||
show_probe_dialog: false,
|
show_probe_dialog: false,
|
||||||
filesUploaded: 0,
|
overrides_open: false,
|
||||||
totalFiles: 0,
|
|
||||||
files_sortby: "By Upload Date",
|
|
||||||
selected_items_to_delete: [],
|
|
||||||
search_query: "",
|
|
||||||
filtered_files: [],
|
|
||||||
selected_folder_index: null,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
"axis-control": require("./axis-control"),
|
"axis-control": require("./axis-control"),
|
||||||
"path-viewer": require("./path-viewer"),
|
|
||||||
"gcode-viewer": require("./gcode-viewer"),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@@ -80,16 +51,6 @@ module.exports = {
|
|||||||
immediate: true,
|
immediate: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
"state.line": function () {
|
|
||||||
if (this.mach_state != "HOMING") {
|
|
||||||
this.$broadcast("gcode-line", this.state.line);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"state.selected_time": function () {
|
|
||||||
this.load();
|
|
||||||
},
|
|
||||||
|
|
||||||
jog_step: function () {
|
jog_step: function () {
|
||||||
cookie.set_bool("jog-step", this.jog_step);
|
cookie.set_bool("jog-step", this.jog_step);
|
||||||
},
|
},
|
||||||
@@ -127,43 +88,16 @@ module.exports = {
|
|||||||
return state || "";
|
return state || "";
|
||||||
},
|
},
|
||||||
|
|
||||||
pause_reason: function () {
|
can_set_axis: function () {
|
||||||
return this.state.pr;
|
return this.state.cycle == "idle";
|
||||||
},
|
|
||||||
|
|
||||||
is_running: function () {
|
|
||||||
return this.mach_state == "RUNNING" || this.mach_state == "HOMING";
|
|
||||||
},
|
|
||||||
|
|
||||||
is_stopping: function () {
|
|
||||||
return this.mach_state == "STOPPING";
|
|
||||||
},
|
|
||||||
|
|
||||||
is_holding: function () {
|
|
||||||
return this.mach_state == "HOLDING";
|
|
||||||
},
|
|
||||||
|
|
||||||
is_ready: function () {
|
|
||||||
return this.mach_state == "READY";
|
|
||||||
},
|
},
|
||||||
|
|
||||||
is_idle: function () {
|
is_idle: function () {
|
||||||
return this.state.cycle == "idle";
|
return this.state.cycle == "idle";
|
||||||
},
|
},
|
||||||
|
|
||||||
is_paused: function () {
|
is_ready: function () {
|
||||||
return this.is_holding && (this.pause_reason == "User pause" || this.pause_reason == "Program pause");
|
return this.mach_state == "READY";
|
||||||
},
|
|
||||||
|
|
||||||
can_mdi: function () {
|
|
||||||
return this.is_idle || this.state.cycle == "mdi";
|
|
||||||
},
|
|
||||||
|
|
||||||
can_set_axis: function () {
|
|
||||||
return this.is_idle;
|
|
||||||
|
|
||||||
// TODO allow setting axis position during pause
|
|
||||||
// return this.is_idle || this.is_paused;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
message: function () {
|
message: function () {
|
||||||
@@ -191,57 +125,21 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
plan_time_remaining: function () {
|
plan_time_remaining: function () {
|
||||||
if (!(this.is_stopping || this.is_running || this.is_holding)) {
|
const stopping = this.mach_state == "STOPPING";
|
||||||
return 0;
|
const running = this.mach_state == "RUNNING" || this.mach_state == "HOMING";
|
||||||
}
|
const holding = this.mach_state == "HOLDING";
|
||||||
|
if (!(stopping || running || holding)) return 0;
|
||||||
return this.toolpath.time - this.plan_time;
|
const tp = this.$root && this.$root.toolpath ? this.$root.toolpath.time : 0;
|
||||||
|
return (tp || 0) - this.plan_time;
|
||||||
},
|
},
|
||||||
|
|
||||||
eta: function () {
|
state_kpi_class: function () {
|
||||||
if (this.mach_state != "RUNNING") {
|
const s = this.mach_state;
|
||||||
|
if (s == "ESTOPPED" || s == "FAULT" || s == "SHUTDOWN") return "bad";
|
||||||
|
if (s == "HOLDING" || s == "STOPPING") return "warn";
|
||||||
|
if (s == "RUNNING" || s == "HOMING" || s == "JOGGING") return "busy";
|
||||||
|
if (s == "READY") return "ok";
|
||||||
return "";
|
return "";
|
||||||
}
|
|
||||||
|
|
||||||
const remaining = this.plan_time_remaining;
|
|
||||||
const d = new Date();
|
|
||||||
d.setSeconds(d.getSeconds() + remaining);
|
|
||||||
return d.toLocaleString();
|
|
||||||
},
|
|
||||||
|
|
||||||
progress: function () {
|
|
||||||
if (!this.toolpath.time || this.is_ready) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const p = this.plan_time / this.toolpath.time;
|
|
||||||
return Math.min(1, p);
|
|
||||||
},
|
|
||||||
gcode_files: function () {
|
|
||||||
if (!this.state.folder) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const folder = this.state.gcode_list.find(item => item.name == this.state.folder);
|
|
||||||
if (!folder) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const files = folder.files.filter(item => this.state.files.includes(item.file_name)).map(item => item.file_name);
|
|
||||||
if (this.files_sortby == "A-Z") {
|
|
||||||
return files.sort();
|
|
||||||
} else if (this.files_sortby == "Z-A") {
|
|
||||||
return files.sort().reverse();
|
|
||||||
} else {
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
gcode_filtered_files: function () {
|
|
||||||
return this.filtered_files.filter(file => file.toLowerCase().includes(this.search_query.toLowerCase()));
|
|
||||||
},
|
|
||||||
gcode_folders: function () {
|
|
||||||
return this.state.gcode_list
|
|
||||||
.map(item => item.name)
|
|
||||||
.filter(element => element !== "default")
|
|
||||||
.sort();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -264,14 +162,9 @@ module.exports = {
|
|||||||
M72
|
M72
|
||||||
`);
|
`);
|
||||||
},
|
},
|
||||||
folder_name_edited: function () {
|
|
||||||
this.edited = true;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
ready: function () {
|
ready: function () {
|
||||||
this.load();
|
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
this.current_time = new Date().toLocaleTimeString();
|
this.current_time = new Date().toLocaleTimeString();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
@@ -287,25 +180,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
save_config: async function (config) {
|
|
||||||
try {
|
|
||||||
await api.put("config/save", config);
|
|
||||||
this.$dispatch("update");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Restore Failed: ", error);
|
|
||||||
alert("Restore failed");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
populateFiles(index) {
|
|
||||||
this.selected_folder_index = index;
|
|
||||||
this.filtered_files = this.state.gcode_list[index].files.map(item => item.file_name);
|
|
||||||
},
|
|
||||||
|
|
||||||
getJogIncrStyle(value) {
|
getJogIncrStyle(value) {
|
||||||
const weight = `font-weight:${this.jog_incr === value ? "bold" : "normal"}`;
|
const weight = `font-weight:${this.jog_incr === value ? "bold" : "normal"}`;
|
||||||
const color = this.jog_incr === value ? "color:#0078e7" : "";
|
const color = this.jog_incr === value ? "color:#0078e7" : "";
|
||||||
|
|
||||||
return [weight, color].join(";");
|
return [weight, color].join(";");
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -324,426 +201,6 @@ module.exports = {
|
|||||||
`);
|
`);
|
||||||
},
|
},
|
||||||
|
|
||||||
send: function (msg) {
|
|
||||||
this.$dispatch("send", msg);
|
|
||||||
},
|
|
||||||
|
|
||||||
toggle_sorting: function () {
|
|
||||||
if (this.files_sortby === "By Upload Date") {
|
|
||||||
this.files_sortby = "A-Z";
|
|
||||||
} else if (this.files_sortby === "A-Z") {
|
|
||||||
this.files_sortby = "Z-A";
|
|
||||||
} else if (this.files_sortby === "Z-A") {
|
|
||||||
this.files_sortby = "By Upload Date";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
load: function () {
|
|
||||||
const file_time = this.state.selected_time;
|
|
||||||
const file = this.state.selected;
|
|
||||||
if (this.last_file == file && this.last_file_time == file_time) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.selected && !this.state.files.includes(this.state.selected)) {
|
|
||||||
this.GCodeNotFound = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.last_file = file;
|
|
||||||
this.last_file_time = file_time;
|
|
||||||
|
|
||||||
this.$broadcast("gcode-load", file);
|
|
||||||
this.$broadcast("gcode-line", this.state.line);
|
|
||||||
this.toolpath_progress = 0;
|
|
||||||
this.load_toolpath(file, file_time);
|
|
||||||
},
|
|
||||||
|
|
||||||
load_toolpath: async function (file, file_time) {
|
|
||||||
this.toolpath = {};
|
|
||||||
|
|
||||||
if (!file || this.last_file_time != file_time) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showGcodeMessage = true;
|
|
||||||
|
|
||||||
while (this.showGcodeMessage) {
|
|
||||||
try {
|
|
||||||
const toolpath = await api.get(`path/${file}`);
|
|
||||||
this.toolpath_progress = toolpath.progress;
|
|
||||||
|
|
||||||
if (toolpath.progress === 1 || typeof toolpath.progress == "undefined") {
|
|
||||||
this.showGcodeMessage = false;
|
|
||||||
|
|
||||||
if (toolpath.bounds) {
|
|
||||||
toolpath.filename = file;
|
|
||||||
this.toolpath_progress = 1;
|
|
||||||
this.toolpath = toolpath;
|
|
||||||
|
|
||||||
const state = this.$root.state;
|
|
||||||
for (const axis of "xyzabc") {
|
|
||||||
Vue.set(state, `path_min_${axis}`, toolpath.bounds.min[axis]);
|
|
||||||
Vue.set(state, `path_max_${axis}`, toolpath.bounds.max[axis]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
submit_mdi: function () {
|
|
||||||
this.send(this.mdi);
|
|
||||||
|
|
||||||
if (!this.history.length || this.history[0] != this.mdi) {
|
|
||||||
this.history.unshift(this.mdi);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mdi = "";
|
|
||||||
},
|
|
||||||
|
|
||||||
mdi_start_pause: function () {
|
|
||||||
if (this.state.xx == "RUNNING") {
|
|
||||||
this.pause();
|
|
||||||
} else if (this.state.xx == "STOPPING" || this.state.xx == "HOLDING") {
|
|
||||||
this.unpause();
|
|
||||||
} else {
|
|
||||||
this.submit_mdi();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
load_history: function (index) {
|
|
||||||
this.mdi = this.history[index];
|
|
||||||
},
|
|
||||||
|
|
||||||
open_file: function () {
|
|
||||||
utils.clickFileInput("gcode-file-input");
|
|
||||||
},
|
|
||||||
|
|
||||||
open_folder: function () {
|
|
||||||
utils.clickFileInput("gcode-folder-input");
|
|
||||||
},
|
|
||||||
|
|
||||||
edited_folder_name: function (event) {
|
|
||||||
if (event.target.value.trim() != "") {
|
|
||||||
this.$dispatch("folder_name_edited");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
update_config: function () {
|
|
||||||
this.config.gcode_list = [...this.state.gcode_list];
|
|
||||||
this.config.non_macros_list = [...this.state.non_macros_list];
|
|
||||||
this.config.macros_list = [...this.state.macros_list];
|
|
||||||
this.config.macros = [...this.state.macros];
|
|
||||||
},
|
|
||||||
|
|
||||||
reset_gcode: function () {
|
|
||||||
this.state.selected = "";
|
|
||||||
this.last_file = "";
|
|
||||||
this.$broadcast("gcode-load", "");
|
|
||||||
},
|
|
||||||
|
|
||||||
upload_gcode: async function (filename, file) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
|
|
||||||
this.filesUploaded++;
|
|
||||||
if (this.filesUploaded == this.totalFiles) {
|
|
||||||
this.uploading_files = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
xhr.onload = () => {
|
|
||||||
if (xhr.status >= 200 && xhr.status < 300) {
|
|
||||||
resolve("file uploaded");
|
|
||||||
} else {
|
|
||||||
console.error("File upload failed:", xhr.statusText);
|
|
||||||
reject("upload failed");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onerror = () => {
|
|
||||||
alert("Upload failed.");
|
|
||||||
reject("upload failed");
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.open("PUT", `/api/file/${encodeURIComponent(filename)}`, true);
|
|
||||||
xhr.send(file);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
readFile: function (file) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
|
|
||||||
reader.onload = () => {
|
|
||||||
resolve(reader.result);
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.onerror = error => {
|
|
||||||
reject(error);
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.readAsText(file, "utf-8");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
validateFiles: async function (files) {
|
|
||||||
const validFiles = [];
|
|
||||||
for (const file of files) {
|
|
||||||
const extension = file.name.split(".").pop().toLowerCase();
|
|
||||||
const validExtensions = ["nc", "ngc", "gcode", "gc"];
|
|
||||||
|
|
||||||
if (validExtensions.includes(extension)) {
|
|
||||||
validFiles.push(file);
|
|
||||||
} else {
|
|
||||||
alert(`Unsupported file : ${file.name}`);
|
|
||||||
this.filesUploaded++;
|
|
||||||
if (this.filesUploaded == this.totalFiles) {
|
|
||||||
this.uploadFiles = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return validFiles;
|
|
||||||
},
|
|
||||||
|
|
||||||
uploadValidFiles: async function (files, folderName) {
|
|
||||||
const updatedConfig = { ...this.config };
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
try {
|
|
||||||
const gcode = await this.readFile(file);
|
|
||||||
await this.upload_gcode(file.name, gcode);
|
|
||||||
|
|
||||||
const isAlreadyPresent = updatedConfig.non_macros_list.some(element => element.file_name === file.name);
|
|
||||||
|
|
||||||
if (!isAlreadyPresent) {
|
|
||||||
updatedConfig.non_macros_list.push({ file_name: file.name });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (folderName) {
|
|
||||||
const folder = updatedConfig.gcode_list.find(item => item.type == "folder" && item.name == folderName);
|
|
||||||
if (folder) {
|
|
||||||
if (!folder.files.map(item => item.file_name).includes(file.name)) {
|
|
||||||
folder.files.push({ file_name: file.name });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updatedConfig.gcode_list.push({
|
|
||||||
name: folderName,
|
|
||||||
type: "folder",
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
file_name: file.name,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var folder_to_add = updatedConfig.gcode_list.find(
|
|
||||||
item => item.type == "folder" && item.name == this.state.folder,
|
|
||||||
);
|
|
||||||
if (!folder_to_add) {
|
|
||||||
folder_to_add = updatedConfig.gcode_list.unshift({
|
|
||||||
name: this.state.folder,
|
|
||||||
type: "folder",
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
file_name: file.name,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
folder_to_add = updatedConfig.gcode_list[0];
|
|
||||||
}
|
|
||||||
if (!folder_to_add.files.find(item => item.file_name == file.name)) {
|
|
||||||
folder_to_add.files.push({ file_name: file.name });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`error uploading file : `, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return updatedConfig;
|
|
||||||
},
|
|
||||||
|
|
||||||
upload_files: async function (files, folderName) {
|
|
||||||
this.update_config();
|
|
||||||
|
|
||||||
const validFiles = await this.validateFiles(files);
|
|
||||||
const updatedConfig = await this.uploadValidFiles(validFiles, folderName);
|
|
||||||
|
|
||||||
await this.save_config(updatedConfig);
|
|
||||||
},
|
|
||||||
|
|
||||||
upload_file: async function (e) {
|
|
||||||
this.uploading_files = true;
|
|
||||||
this.filesUploaded = 0;
|
|
||||||
|
|
||||||
const files = e.target.files || e.dataTransfer.files;
|
|
||||||
if (!files.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.totalFiles = files.length;
|
|
||||||
|
|
||||||
await this.upload_files(files);
|
|
||||||
},
|
|
||||||
|
|
||||||
create_new_folder: async function () {
|
|
||||||
const folder_name = this.folder_name.trim();
|
|
||||||
if (folder_name != "") {
|
|
||||||
if (this.state.gcode_list.find(item => item.type == "folder" && item.name == folder_name)) {
|
|
||||||
alert("Folder with the same name already exists!");
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
this.update_config();
|
|
||||||
this.config.gcode_list.push({
|
|
||||||
name: folder_name,
|
|
||||||
type: "folder",
|
|
||||||
files: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.state.folder = folder_name;
|
|
||||||
this.edited = false;
|
|
||||||
this.create_folder = false;
|
|
||||||
this.folder_name = "";
|
|
||||||
this.save_config(this.config);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
cancel_new_folder: function () {
|
|
||||||
this.create_folder = false;
|
|
||||||
this.folder_name = "";
|
|
||||||
},
|
|
||||||
|
|
||||||
upload_folder: async function (e) {
|
|
||||||
this.uploading_files = true;
|
|
||||||
this.filesUploaded = 0;
|
|
||||||
|
|
||||||
const files = e.target.files || e.dataTransfer.files;
|
|
||||||
if (!files.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.totalFiles = files.length;
|
|
||||||
const folderName = files[0].webkitRelativePath.split("/")[0];
|
|
||||||
|
|
||||||
this.upload_files(files, folderName);
|
|
||||||
},
|
|
||||||
|
|
||||||
delete_current: async function () {
|
|
||||||
if (!this.state.selected) {
|
|
||||||
this.deleteGCode = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.update_config();
|
|
||||||
|
|
||||||
this.config.non_macros_list = this.config.non_macros_list.filter(
|
|
||||||
item => !this.selected_items_to_delete.includes(item.file_name),
|
|
||||||
);
|
|
||||||
const folder_to_update = this.config.gcode_list.find(
|
|
||||||
item => item.name == this.config.gcode_list[this.selected_folder_index].name && item.type == "folder",
|
|
||||||
);
|
|
||||||
folder_to_update.files = folder_to_update.files.filter(
|
|
||||||
item => !this.selected_items_to_delete.includes(item.file_name),
|
|
||||||
);
|
|
||||||
|
|
||||||
const exception_list = this.state.macros_list.map(item => item.file_name);
|
|
||||||
let files_to_delete = this.selected_items_to_delete.filter(item => !exception_list.includes(item));
|
|
||||||
|
|
||||||
await api.delete(`file/DINCAIQABiDARixAxiABDIHCAMQABiABDIHCAQQABiABDIH${files_to_delete.toString()}`);
|
|
||||||
|
|
||||||
this.save_config(this.config);
|
|
||||||
this.filtered_files = [];
|
|
||||||
this.search_query = "";
|
|
||||||
this.selected_folder_index = null;
|
|
||||||
this.selected_items_to_delete = [];
|
|
||||||
this.deleteGCode = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
cancel_delete: function () {
|
|
||||||
this.filtered_files = [];
|
|
||||||
this.search_query = "";
|
|
||||||
this.selected_folder_index = null;
|
|
||||||
this.selected_items_to_delete = [];
|
|
||||||
this.deleteGCode = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
delete_all: function () {
|
|
||||||
api.delete("file");
|
|
||||||
this.deleteGCode = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
delete_all_except_macros: async function () {
|
|
||||||
this.update_config();
|
|
||||||
const macrosList = this.state.macros_list.map(item => item.file_name).toString();
|
|
||||||
api.delete(`file/EgZjaHJvbWUqCggBEAAYsQMYgAQyBggAEEUYOTIKCAE${macrosList}`);
|
|
||||||
this.config.non_macros_list = [];
|
|
||||||
this.config.gcode_list = [
|
|
||||||
{
|
|
||||||
name: "default",
|
|
||||||
type: "folder",
|
|
||||||
files: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
this.save_config(this.config);
|
|
||||||
this.state.folder = "default";
|
|
||||||
this.state.selected = "";
|
|
||||||
this.selected_items_to_delete = [];
|
|
||||||
this.deleteGCode = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
delete_folder: async function () {
|
|
||||||
this.update_config();
|
|
||||||
if (this.state.folder && this.state.folder != "default") {
|
|
||||||
const files_to_move = this.config.gcode_list.find(
|
|
||||||
item => item.type == "folder" && item.name == this.state.folder,
|
|
||||||
);
|
|
||||||
if (files_to_move) {
|
|
||||||
const default_folder = this.config.gcode_list.find(item => item.name == "default");
|
|
||||||
default_folder.files = [...default_folder.files, ...files_to_move.files].sort();
|
|
||||||
this.config.gcode_list = this.config.gcode_list.filter(item => item.name != this.state.folder);
|
|
||||||
this.save_config(this.config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.state.folder = "default";
|
|
||||||
this.confirmDelete = false;
|
|
||||||
},
|
|
||||||
delete_folder_and_files: async function () {
|
|
||||||
if (!this.state.folder) {
|
|
||||||
this.confirmDelete = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.update_config();
|
|
||||||
|
|
||||||
const selected_folder = this.config.gcode_list.find(
|
|
||||||
item => item.type == "folder" && item.name == this.state.folder,
|
|
||||||
);
|
|
||||||
if (!selected_folder) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const macrosList = this.state.macros_list.map(item => item.file_name);
|
|
||||||
var files_to_delete = selected_folder.files
|
|
||||||
.map(item => item.file_name)
|
|
||||||
.filter(item => !macrosList.includes(item));
|
|
||||||
if (selected_folder.name != "default") {
|
|
||||||
this.config.gcode_list = this.config.gcode_list.filter(item => item.name != this.state.folder);
|
|
||||||
} else {
|
|
||||||
selected_folder.files = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
await api.delete(`file/DINCAIQABiDARixAxiABDIHCAMQABiABDIHCAQQABiABDIH${files_to_delete.toString()}`);
|
|
||||||
this.config.non_macros_list = this.config.non_macros_list.filter(
|
|
||||||
item => !files_to_delete.includes(item.file_name),
|
|
||||||
);
|
|
||||||
this.save_config(this.config);
|
|
||||||
this.state.folder = "default";
|
|
||||||
this.confirmDelete = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
home: function (axis) {
|
home: function (axis) {
|
||||||
this.ask_home = false;
|
this.ask_home = false;
|
||||||
|
|
||||||
@@ -777,11 +234,8 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Use the same fine/small/medium/large increment buttons as the XYZ
|
|
||||||
// jog grid. sign=+1 for W+, -1 for W-.
|
|
||||||
aux_jog_incr: function (sign) {
|
aux_jog_incr: function (sign) {
|
||||||
const amount =
|
const amount = this.jog_incr_amounts[this.display_units][this.jog_incr];
|
||||||
this.jog_incr_amounts[this.display_units][this.jog_incr];
|
|
||||||
const delta_mm = sign * (this.metric ? amount : amount * 25.4);
|
const delta_mm = sign * (this.metric ? amount : amount * 25.4);
|
||||||
this.aux_jog(delta_mm);
|
this.aux_jog(delta_mm);
|
||||||
},
|
},
|
||||||
@@ -811,93 +265,20 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
zero: function (axis) {
|
zero: function (axis) {
|
||||||
if (typeof axis == "undefined") {
|
if (typeof axis == "undefined") this.zero_all();
|
||||||
this.zero_all();
|
else this.set_position(axis, 0);
|
||||||
} else {
|
|
||||||
this.set_position(axis, 0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
start_pause: function () {
|
|
||||||
this.macrosLoading = false;
|
|
||||||
if (this.state.xx == "RUNNING") {
|
|
||||||
this.pause();
|
|
||||||
} else if (this.state.xx == "STOPPING" || this.state.xx == "HOLDING") {
|
|
||||||
this.unpause();
|
|
||||||
} else {
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
start: function () {
|
|
||||||
api.put("start");
|
|
||||||
},
|
|
||||||
|
|
||||||
pause: function () {
|
|
||||||
api.put("pause");
|
|
||||||
},
|
|
||||||
|
|
||||||
unpause: function () {
|
|
||||||
api.put("unpause");
|
|
||||||
},
|
|
||||||
|
|
||||||
optional_pause: function () {
|
|
||||||
api.put("pause/optional");
|
|
||||||
},
|
|
||||||
|
|
||||||
stop: function () {
|
|
||||||
api.put("stop");
|
|
||||||
},
|
|
||||||
|
|
||||||
step: function () {
|
|
||||||
api.put("step");
|
|
||||||
},
|
|
||||||
|
|
||||||
override_feed: function () {
|
|
||||||
api.put(`override/feed/${this.feed_override}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
override_speed: function () {
|
|
||||||
api.put(`override/speed/${this.speed_override}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
current: function (axis, value) {
|
|
||||||
const x = value / 32.0;
|
|
||||||
if (this.state[`${axis}pl`] == x) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {};
|
|
||||||
data[`${axis}pl`] = x;
|
|
||||||
this.send(JSON.stringify(data));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
showProbeDialog: function (probeType) {
|
showProbeDialog: function (probeType) {
|
||||||
if (this.show_probe_dialog) {
|
if (this.show_probe_dialog) {
|
||||||
this.show_probe_dialog = false;
|
this.show_probe_dialog = false;
|
||||||
}
|
}
|
||||||
SvelteComponents.showDialog("Probe", { probeType, isRotaryActive: this.state["2an"] == 3 });
|
SvelteComponents.showDialog("Probe", {
|
||||||
},
|
probeType,
|
||||||
run_macro: function (id) {
|
isRotaryActive: this.state["2an"] == 3,
|
||||||
if (this.state.macros[id].file_name == "default") {
|
});
|
||||||
this.showNoGcodeMessage = true;
|
|
||||||
} else {
|
|
||||||
if (this.state.macros[id].file_name != this.state.selected) {
|
|
||||||
this.state.selected = this.state.macros[id].file_name;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
this.load();
|
|
||||||
if (this.state.macros[id].alert == true) {
|
|
||||||
this.macrosLoading = true;
|
|
||||||
} else {
|
|
||||||
setImmediate(() => this.start_pause());
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Error running program: ", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [require("./axis-vars")],
|
mixins: [require("./program-mixin"), require("./axis-vars")],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,6 +44,16 @@ window.onload = function() {
|
|||||||
cookie_set("client-id", uuid(), 10000);
|
cookie_set("client-id", uuid(), 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vue 1's async queue can drop dependent watcher updates when
|
||||||
|
// data props are mutated outside the normal event flow (e.g. from
|
||||||
|
// a `hashchange` listener that fires before Vue's tick scheduler
|
||||||
|
// has caught up). Disable async batching so every reactive write
|
||||||
|
// synchronously re-evaluates dependents — this matches Vue 1's
|
||||||
|
// older default and is what the legacy UI implicitly relied on.
|
||||||
|
if (Vue.config) {
|
||||||
|
Vue.config.async = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Register global components
|
// Register global components
|
||||||
Vue.component("templated-input", require("./templated-input"));
|
Vue.component("templated-input", require("./templated-input"));
|
||||||
Vue.component("message", require("./message"));
|
Vue.component("message", require("./message"));
|
||||||
|
|||||||
554
src/js/program-mixin.js
Normal file
554
src/js/program-mixin.js
Normal file
@@ -0,0 +1,554 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Shared data, computed properties and methods that are used by both
|
||||||
|
// the Control view (for things like start/stop, run-macro, axis state)
|
||||||
|
// and the Program view (RUN/STOP/Upload/Download/Delete + file picker
|
||||||
|
// + gcode/path viewers). Splitting these out lets us mount the same
|
||||||
|
// behaviour under two top-level routes without duplicating code.
|
||||||
|
//
|
||||||
|
// The mixin intentionally does *not* require axis-vars; control-view
|
||||||
|
// keeps that one to itself.
|
||||||
|
|
||||||
|
const api = require("./api");
|
||||||
|
const utils = require("./utils");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
mdi: "",
|
||||||
|
last_file: undefined,
|
||||||
|
last_file_time: undefined,
|
||||||
|
toolpath: {},
|
||||||
|
toolpath_progress: 0,
|
||||||
|
history: [],
|
||||||
|
speed_override: 1,
|
||||||
|
feed_override: 1,
|
||||||
|
deleteGCode: false,
|
||||||
|
folder_name: "",
|
||||||
|
edited: false,
|
||||||
|
uploading_files: false,
|
||||||
|
confirmDelete: false,
|
||||||
|
create_folder: false,
|
||||||
|
showGcodeMessage: false,
|
||||||
|
showNoGcodeMessage: false,
|
||||||
|
macrosLoading: false,
|
||||||
|
show_gcodes: false,
|
||||||
|
GCodeNotFound: false,
|
||||||
|
filesUploaded: 0,
|
||||||
|
totalFiles: 0,
|
||||||
|
files_sortby: "By Upload Date",
|
||||||
|
selected_items_to_delete: [],
|
||||||
|
search_query: "",
|
||||||
|
filtered_files: [],
|
||||||
|
selected_folder_index: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
"state.line": function () {
|
||||||
|
if (this.mach_state != "HOMING") {
|
||||||
|
this.$broadcast("gcode-line", this.state.line);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"state.selected_time": function () {
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
is_running: function () {
|
||||||
|
return this.mach_state == "RUNNING" || this.mach_state == "HOMING";
|
||||||
|
},
|
||||||
|
|
||||||
|
is_stopping: function () {
|
||||||
|
return this.mach_state == "STOPPING";
|
||||||
|
},
|
||||||
|
|
||||||
|
is_holding: function () {
|
||||||
|
return this.mach_state == "HOLDING";
|
||||||
|
},
|
||||||
|
|
||||||
|
is_ready: function () {
|
||||||
|
return this.mach_state == "READY";
|
||||||
|
},
|
||||||
|
|
||||||
|
is_idle: function () {
|
||||||
|
return this.state.cycle == "idle";
|
||||||
|
},
|
||||||
|
|
||||||
|
is_paused: function () {
|
||||||
|
return this.is_holding && (this.pause_reason == "User pause" || this.pause_reason == "Program pause");
|
||||||
|
},
|
||||||
|
|
||||||
|
can_mdi: function () {
|
||||||
|
return this.is_idle || this.state.cycle == "mdi";
|
||||||
|
},
|
||||||
|
|
||||||
|
pause_reason: function () {
|
||||||
|
return this.state.pr;
|
||||||
|
},
|
||||||
|
|
||||||
|
plan_time: function () {
|
||||||
|
return this.state.plan_time;
|
||||||
|
},
|
||||||
|
|
||||||
|
plan_time_remaining: function () {
|
||||||
|
if (!(this.is_stopping || this.is_running || this.is_holding)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return this.toolpath.time - this.plan_time;
|
||||||
|
},
|
||||||
|
|
||||||
|
eta: function () {
|
||||||
|
if (this.mach_state != "RUNNING") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const remaining = this.plan_time_remaining;
|
||||||
|
const d = new Date();
|
||||||
|
d.setSeconds(d.getSeconds() + remaining);
|
||||||
|
return d.toLocaleString();
|
||||||
|
},
|
||||||
|
|
||||||
|
progress: function () {
|
||||||
|
if (!this.toolpath.time || this.is_ready) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const p = this.plan_time / this.toolpath.time;
|
||||||
|
return Math.min(1, p);
|
||||||
|
},
|
||||||
|
|
||||||
|
gcode_files: function () {
|
||||||
|
if (!this.state.folder) return [];
|
||||||
|
const folder = this.state.gcode_list.find(item => item.name == this.state.folder);
|
||||||
|
if (!folder) return [];
|
||||||
|
const files = folder.files
|
||||||
|
.filter(item => this.state.files.includes(item.file_name))
|
||||||
|
.map(item => item.file_name);
|
||||||
|
if (this.files_sortby == "A-Z") return files.sort();
|
||||||
|
if (this.files_sortby == "Z-A") return files.sort().reverse();
|
||||||
|
return files;
|
||||||
|
},
|
||||||
|
|
||||||
|
gcode_filtered_files: function () {
|
||||||
|
return this.filtered_files.filter(file =>
|
||||||
|
file.toLowerCase().includes(this.search_query.toLowerCase()));
|
||||||
|
},
|
||||||
|
|
||||||
|
gcode_folders: function () {
|
||||||
|
return this.state.gcode_list
|
||||||
|
.map(item => item.name)
|
||||||
|
.filter(element => element !== "default")
|
||||||
|
.sort();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
save_config: async function (config) {
|
||||||
|
try {
|
||||||
|
await api.put("config/save", config);
|
||||||
|
this.$dispatch("update");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Restore Failed: ", error);
|
||||||
|
alert("Restore failed");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
populateFiles(index) {
|
||||||
|
this.selected_folder_index = index;
|
||||||
|
this.filtered_files = this.state.gcode_list[index].files.map(item => item.file_name);
|
||||||
|
},
|
||||||
|
|
||||||
|
send: function (msg) {
|
||||||
|
this.$dispatch("send", msg);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggle_sorting: function () {
|
||||||
|
if (this.files_sortby === "By Upload Date") this.files_sortby = "A-Z";
|
||||||
|
else if (this.files_sortby === "A-Z") this.files_sortby = "Z-A";
|
||||||
|
else if (this.files_sortby === "Z-A") this.files_sortby = "By Upload Date";
|
||||||
|
},
|
||||||
|
|
||||||
|
load: function () {
|
||||||
|
const file_time = this.state.selected_time;
|
||||||
|
const file = this.state.selected;
|
||||||
|
if (this.last_file == file && this.last_file_time == file_time) return;
|
||||||
|
|
||||||
|
if (this.state.selected && !this.state.files.includes(this.state.selected)) {
|
||||||
|
this.GCodeNotFound = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.last_file = file;
|
||||||
|
this.last_file_time = file_time;
|
||||||
|
|
||||||
|
this.$broadcast("gcode-load", file);
|
||||||
|
this.$broadcast("gcode-line", this.state.line);
|
||||||
|
this.toolpath_progress = 0;
|
||||||
|
this.load_toolpath(file, file_time);
|
||||||
|
},
|
||||||
|
|
||||||
|
load_toolpath: async function (file, file_time) {
|
||||||
|
this.toolpath = {};
|
||||||
|
if (!file || this.last_file_time != file_time) return;
|
||||||
|
|
||||||
|
this.showGcodeMessage = true;
|
||||||
|
|
||||||
|
while (this.showGcodeMessage) {
|
||||||
|
try {
|
||||||
|
const toolpath = await api.get(`path/${file}`);
|
||||||
|
this.toolpath_progress = toolpath.progress;
|
||||||
|
|
||||||
|
if (toolpath.progress === 1 || typeof toolpath.progress == "undefined") {
|
||||||
|
this.showGcodeMessage = false;
|
||||||
|
|
||||||
|
if (toolpath.bounds) {
|
||||||
|
toolpath.filename = file;
|
||||||
|
this.toolpath_progress = 1;
|
||||||
|
this.toolpath = toolpath;
|
||||||
|
|
||||||
|
const state = this.$root.state;
|
||||||
|
for (const axis of "xyzabc") {
|
||||||
|
Vue.set(state, `path_min_${axis}`, toolpath.bounds.min[axis]);
|
||||||
|
Vue.set(state, `path_max_${axis}`, toolpath.bounds.max[axis]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
submit_mdi: function () {
|
||||||
|
this.send(this.mdi);
|
||||||
|
if (!this.history.length || this.history[0] != this.mdi) {
|
||||||
|
this.history.unshift(this.mdi);
|
||||||
|
}
|
||||||
|
this.mdi = "";
|
||||||
|
},
|
||||||
|
|
||||||
|
mdi_start_pause: function () {
|
||||||
|
if (this.state.xx == "RUNNING") this.pause();
|
||||||
|
else if (this.state.xx == "STOPPING" || this.state.xx == "HOLDING") this.unpause();
|
||||||
|
else this.submit_mdi();
|
||||||
|
},
|
||||||
|
|
||||||
|
load_history: function (index) {
|
||||||
|
this.mdi = this.history[index];
|
||||||
|
},
|
||||||
|
|
||||||
|
open_file: function () {
|
||||||
|
utils.clickFileInput("gcode-file-input");
|
||||||
|
},
|
||||||
|
|
||||||
|
open_folder: function () {
|
||||||
|
utils.clickFileInput("gcode-folder-input");
|
||||||
|
},
|
||||||
|
|
||||||
|
edited_folder_name: function (event) {
|
||||||
|
if (event.target.value.trim() != "") {
|
||||||
|
this.$dispatch("folder_name_edited");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
update_config: function () {
|
||||||
|
this.config.gcode_list = [...this.state.gcode_list];
|
||||||
|
this.config.non_macros_list = [...this.state.non_macros_list];
|
||||||
|
this.config.macros_list = [...this.state.macros_list];
|
||||||
|
this.config.macros = [...this.state.macros];
|
||||||
|
},
|
||||||
|
|
||||||
|
reset_gcode: function () {
|
||||||
|
this.state.selected = "";
|
||||||
|
this.last_file = "";
|
||||||
|
this.$broadcast("gcode-load", "");
|
||||||
|
},
|
||||||
|
|
||||||
|
upload_gcode: async function (filename, file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
this.filesUploaded++;
|
||||||
|
if (this.filesUploaded == this.totalFiles) {
|
||||||
|
this.uploading_files = false;
|
||||||
|
}
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) resolve("file uploaded");
|
||||||
|
else { console.error("File upload failed:", xhr.statusText); reject("upload failed"); }
|
||||||
|
};
|
||||||
|
xhr.onerror = () => { alert("Upload failed."); reject("upload failed"); };
|
||||||
|
xhr.open("PUT", `/api/file/${encodeURIComponent(filename)}`, true);
|
||||||
|
xhr.send(file);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
readFile: function (file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => resolve(reader.result);
|
||||||
|
reader.onerror = error => reject(error);
|
||||||
|
reader.readAsText(file, "utf-8");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
validateFiles: async function (files) {
|
||||||
|
const validFiles = [];
|
||||||
|
for (const file of files) {
|
||||||
|
const extension = file.name.split(".").pop().toLowerCase();
|
||||||
|
const validExtensions = ["nc", "ngc", "gcode", "gc"];
|
||||||
|
if (validExtensions.includes(extension)) {
|
||||||
|
validFiles.push(file);
|
||||||
|
} else {
|
||||||
|
alert(`Unsupported file : ${file.name}`);
|
||||||
|
this.filesUploaded++;
|
||||||
|
if (this.filesUploaded == this.totalFiles) {
|
||||||
|
this.uploadFiles = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return validFiles;
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadValidFiles: async function (files, folderName) {
|
||||||
|
const updatedConfig = { ...this.config };
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
const gcode = await this.readFile(file);
|
||||||
|
await this.upload_gcode(file.name, gcode);
|
||||||
|
|
||||||
|
const isAlreadyPresent = updatedConfig.non_macros_list.some(element => element.file_name === file.name);
|
||||||
|
if (!isAlreadyPresent) {
|
||||||
|
updatedConfig.non_macros_list.push({ file_name: file.name });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (folderName) {
|
||||||
|
const folder = updatedConfig.gcode_list.find(item => item.type == "folder" && item.name == folderName);
|
||||||
|
if (folder) {
|
||||||
|
if (!folder.files.map(item => item.file_name).includes(file.name)) {
|
||||||
|
folder.files.push({ file_name: file.name });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updatedConfig.gcode_list.push({
|
||||||
|
name: folderName,
|
||||||
|
type: "folder",
|
||||||
|
files: [{ file_name: file.name }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var folder_to_add = updatedConfig.gcode_list.find(
|
||||||
|
item => item.type == "folder" && item.name == this.state.folder,
|
||||||
|
);
|
||||||
|
if (!folder_to_add) {
|
||||||
|
folder_to_add = updatedConfig.gcode_list.unshift({
|
||||||
|
name: this.state.folder,
|
||||||
|
type: "folder",
|
||||||
|
files: [{ file_name: file.name }],
|
||||||
|
});
|
||||||
|
folder_to_add = updatedConfig.gcode_list[0];
|
||||||
|
}
|
||||||
|
if (!folder_to_add.files.find(item => item.file_name == file.name)) {
|
||||||
|
folder_to_add.files.push({ file_name: file.name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`error uploading file : `, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updatedConfig;
|
||||||
|
},
|
||||||
|
|
||||||
|
upload_files: async function (files, folderName) {
|
||||||
|
this.update_config();
|
||||||
|
const validFiles = await this.validateFiles(files);
|
||||||
|
const updatedConfig = await this.uploadValidFiles(validFiles, folderName);
|
||||||
|
await this.save_config(updatedConfig);
|
||||||
|
},
|
||||||
|
|
||||||
|
upload_file: async function (e) {
|
||||||
|
this.uploading_files = true;
|
||||||
|
this.filesUploaded = 0;
|
||||||
|
const files = e.target.files || e.dataTransfer.files;
|
||||||
|
if (!files.length) return;
|
||||||
|
this.totalFiles = files.length;
|
||||||
|
await this.upload_files(files);
|
||||||
|
},
|
||||||
|
|
||||||
|
create_new_folder: async function () {
|
||||||
|
const folder_name = this.folder_name.trim();
|
||||||
|
if (folder_name != "") {
|
||||||
|
if (this.state.gcode_list.find(item => item.type == "folder" && item.name == folder_name)) {
|
||||||
|
alert("Folder with the same name already exists!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.update_config();
|
||||||
|
this.config.gcode_list.push({
|
||||||
|
name: folder_name,
|
||||||
|
type: "folder",
|
||||||
|
files: [],
|
||||||
|
});
|
||||||
|
this.state.folder = folder_name;
|
||||||
|
this.edited = false;
|
||||||
|
this.create_folder = false;
|
||||||
|
this.folder_name = "";
|
||||||
|
this.save_config(this.config);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cancel_new_folder: function () {
|
||||||
|
this.create_folder = false;
|
||||||
|
this.folder_name = "";
|
||||||
|
},
|
||||||
|
|
||||||
|
upload_folder: async function (e) {
|
||||||
|
this.uploading_files = true;
|
||||||
|
this.filesUploaded = 0;
|
||||||
|
const files = e.target.files || e.dataTransfer.files;
|
||||||
|
if (!files.length) return;
|
||||||
|
this.totalFiles = files.length;
|
||||||
|
const folderName = files[0].webkitRelativePath.split("/")[0];
|
||||||
|
this.upload_files(files, folderName);
|
||||||
|
},
|
||||||
|
|
||||||
|
delete_current: async function () {
|
||||||
|
if (!this.state.selected) {
|
||||||
|
this.deleteGCode = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.update_config();
|
||||||
|
|
||||||
|
this.config.non_macros_list = this.config.non_macros_list.filter(
|
||||||
|
item => !this.selected_items_to_delete.includes(item.file_name),
|
||||||
|
);
|
||||||
|
const folder_to_update = this.config.gcode_list.find(
|
||||||
|
item => item.name == this.config.gcode_list[this.selected_folder_index].name && item.type == "folder",
|
||||||
|
);
|
||||||
|
folder_to_update.files = folder_to_update.files.filter(
|
||||||
|
item => !this.selected_items_to_delete.includes(item.file_name),
|
||||||
|
);
|
||||||
|
|
||||||
|
const exception_list = this.state.macros_list.map(item => item.file_name);
|
||||||
|
let files_to_delete = this.selected_items_to_delete.filter(item => !exception_list.includes(item));
|
||||||
|
|
||||||
|
await api.delete(`file/DINCAIQABiDARixAxiABDIHCAMQABiABDIHCAQQABiABDIH${files_to_delete.toString()}`);
|
||||||
|
|
||||||
|
this.save_config(this.config);
|
||||||
|
this.filtered_files = [];
|
||||||
|
this.search_query = "";
|
||||||
|
this.selected_folder_index = null;
|
||||||
|
this.selected_items_to_delete = [];
|
||||||
|
this.deleteGCode = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
cancel_delete: function () {
|
||||||
|
this.filtered_files = [];
|
||||||
|
this.search_query = "";
|
||||||
|
this.selected_folder_index = null;
|
||||||
|
this.selected_items_to_delete = [];
|
||||||
|
this.deleteGCode = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
delete_all: function () {
|
||||||
|
api.delete("file");
|
||||||
|
this.deleteGCode = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
delete_all_except_macros: async function () {
|
||||||
|
this.update_config();
|
||||||
|
const macrosList = this.state.macros_list.map(item => item.file_name).toString();
|
||||||
|
api.delete(`file/EgZjaHJvbWUqCggBEAAYsQMYgAQyBggAEEUYOTIKCAE${macrosList}`);
|
||||||
|
this.config.non_macros_list = [];
|
||||||
|
this.config.gcode_list = [{ name: "default", type: "folder", files: [] }];
|
||||||
|
this.save_config(this.config);
|
||||||
|
this.state.folder = "default";
|
||||||
|
this.state.selected = "";
|
||||||
|
this.selected_items_to_delete = [];
|
||||||
|
this.deleteGCode = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
delete_folder: async function () {
|
||||||
|
this.update_config();
|
||||||
|
if (this.state.folder && this.state.folder != "default") {
|
||||||
|
const files_to_move = this.config.gcode_list.find(
|
||||||
|
item => item.type == "folder" && item.name == this.state.folder,
|
||||||
|
);
|
||||||
|
if (files_to_move) {
|
||||||
|
const default_folder = this.config.gcode_list.find(item => item.name == "default");
|
||||||
|
default_folder.files = [...default_folder.files, ...files_to_move.files].sort();
|
||||||
|
this.config.gcode_list = this.config.gcode_list.filter(item => item.name != this.state.folder);
|
||||||
|
this.save_config(this.config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.state.folder = "default";
|
||||||
|
this.confirmDelete = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
delete_folder_and_files: async function () {
|
||||||
|
if (!this.state.folder) {
|
||||||
|
this.confirmDelete = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.update_config();
|
||||||
|
const selected_folder = this.config.gcode_list.find(
|
||||||
|
item => item.type == "folder" && item.name == this.state.folder,
|
||||||
|
);
|
||||||
|
if (!selected_folder) return;
|
||||||
|
|
||||||
|
const macrosList = this.state.macros_list.map(item => item.file_name);
|
||||||
|
var files_to_delete = selected_folder.files
|
||||||
|
.map(item => item.file_name)
|
||||||
|
.filter(item => !macrosList.includes(item));
|
||||||
|
|
||||||
|
if (selected_folder.name != "default") {
|
||||||
|
this.config.gcode_list = this.config.gcode_list.filter(item => item.name != this.state.folder);
|
||||||
|
} else {
|
||||||
|
selected_folder.files = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.delete(`file/DINCAIQABiDARixAxiABDIHCAMQABiABDIHCAQQABiABDIH${files_to_delete.toString()}`);
|
||||||
|
this.config.non_macros_list = this.config.non_macros_list.filter(
|
||||||
|
item => !files_to_delete.includes(item.file_name),
|
||||||
|
);
|
||||||
|
this.save_config(this.config);
|
||||||
|
this.state.folder = "default";
|
||||||
|
this.confirmDelete = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
start_pause: function () {
|
||||||
|
this.macrosLoading = false;
|
||||||
|
if (this.state.xx == "RUNNING") this.pause();
|
||||||
|
else if (this.state.xx == "STOPPING" || this.state.xx == "HOLDING") this.unpause();
|
||||||
|
else this.start();
|
||||||
|
},
|
||||||
|
|
||||||
|
start: function () { api.put("start"); },
|
||||||
|
pause: function () { api.put("pause"); },
|
||||||
|
unpause: function () { api.put("unpause"); },
|
||||||
|
optional_pause: function () { api.put("pause/optional"); },
|
||||||
|
stop: function () { api.put("stop"); },
|
||||||
|
step: function () { api.put("step"); },
|
||||||
|
|
||||||
|
override_feed: function () { api.put(`override/feed/${this.feed_override}`); },
|
||||||
|
override_speed: function () { api.put(`override/speed/${this.speed_override}`); },
|
||||||
|
|
||||||
|
run_macro: function (id) {
|
||||||
|
if (this.state.macros[id].file_name == "default") {
|
||||||
|
this.showNoGcodeMessage = true;
|
||||||
|
} else {
|
||||||
|
if (this.state.macros[id].file_name != this.state.selected) {
|
||||||
|
this.state.selected = this.state.macros[id].file_name;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.load();
|
||||||
|
if (this.state.macros[id].alert == true) {
|
||||||
|
this.macrosLoading = true;
|
||||||
|
} else {
|
||||||
|
setImmediate(() => this.start_pause());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Error running program: ", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
60
src/js/program-view.js
Normal file
60
src/js/program-view.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Program tab — file management, run/stop, gcode listing and 3D
|
||||||
|
// toolpath preview. Reuses the shared mixin (program-mixin) that also
|
||||||
|
// powers the legacy bits of control-view; this view does not host the
|
||||||
|
// jog grid or the DRO.
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
template: "#program-view-template",
|
||||||
|
props: ["config", "template", "state"],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
"path-viewer": require("./path-viewer"),
|
||||||
|
"gcode-viewer": require("./gcode-viewer"),
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
"state.metric": {
|
||||||
|
handler: function () {},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
display_units: {
|
||||||
|
cache: false,
|
||||||
|
get: function () { return this.$root.display_units; },
|
||||||
|
set: function (value) {
|
||||||
|
this.config.settings.units = value;
|
||||||
|
this.$root.display_units = value;
|
||||||
|
this.$dispatch("config-changed");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
metric: function () {
|
||||||
|
return this.display_units === "METRIC";
|
||||||
|
},
|
||||||
|
|
||||||
|
mach_state: function () {
|
||||||
|
const cycle = this.state.cycle;
|
||||||
|
const xx = this.state.xx;
|
||||||
|
if (xx != "ESTOPPED" && (cycle == "jogging" || cycle == "homing")) {
|
||||||
|
return cycle.toUpperCase();
|
||||||
|
}
|
||||||
|
return xx || "";
|
||||||
|
},
|
||||||
|
|
||||||
|
can_set_axis: function () { return this.state.cycle == "idle"; },
|
||||||
|
},
|
||||||
|
|
||||||
|
ready: function () {
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [require("./program-mixin")],
|
||||||
|
};
|
||||||
109
src/js/settings-shell-view.js
Normal file
109
src/js/settings-shell-view.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Wrapper that adds a left-rail navigator around the settings family
|
||||||
|
// of views (Settings, Admin General, Admin Network, Tool, IO, Motor,
|
||||||
|
// Macros, Help, Cheat Sheet). The inner view is selected by the URL
|
||||||
|
// hash (parsed in app.js) and exposed as $root.sub_tab.
|
||||||
|
|
||||||
|
// Vue 1 has trouble making child components reactive to `$root.sub_tab`
|
||||||
|
// changes (whether via computed, watch, or prop binding through
|
||||||
|
// `<component :is>`). The shell instead listens to `hashchange`
|
||||||
|
// directly and parses the hash itself, mirroring app.js's logic, then
|
||||||
|
// keeps a local data prop `sub` that the template binds to. This is
|
||||||
|
// the only path that updates the rail's `:class` reactively.
|
||||||
|
module.exports = {
|
||||||
|
template: "#settings-shell-view-template",
|
||||||
|
props: ["config", "template", "state", "index"],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
"settings-view-inner": require("./settings-view"),
|
||||||
|
"admin-general-view": require("./admin-general-view"),
|
||||||
|
"admin-network-view": require("./admin-network-view"),
|
||||||
|
"motor-view": require("./motor-view"),
|
||||||
|
"tool-view": require("./tool-view"),
|
||||||
|
"io-view": require("./io-view"),
|
||||||
|
"macros-view": require("./macros"),
|
||||||
|
"help-view": require("./help-view"),
|
||||||
|
"cheat-sheet-view": {
|
||||||
|
template: "#cheat-sheet-view-template",
|
||||||
|
data: function () {
|
||||||
|
return { showUnimplemented: false };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
sub: this.$root.sub_tab || "settings",
|
||||||
|
ridx: this.$root.index, // local copy of the motor index
|
||||||
|
rail_items: [
|
||||||
|
{ sub: "settings", href: "#settings", icon: "fa-display", label: "Display & Units" },
|
||||||
|
{ 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" },
|
||||||
|
{ sub: "io", href: "#io", icon: "fa-plug", label: "I/O" },
|
||||||
|
{ section: "Motors" },
|
||||||
|
{ sub: "motor", motor: 0, href: "#motor:0", icon: "fa-arrows-up-down-left-right", label: "Motor 0" },
|
||||||
|
{ 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" },
|
||||||
|
{ 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" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
ready: function () {
|
||||||
|
this._onHash = () => this.refresh_from_hash();
|
||||||
|
window.addEventListener("hashchange", this._onHash);
|
||||||
|
this.refresh_from_hash();
|
||||||
|
},
|
||||||
|
|
||||||
|
attached: function () {
|
||||||
|
// Vue 1 fires `attached` whenever the component is inserted into
|
||||||
|
// the DOM (which happens on every route change because the outer
|
||||||
|
// <component :is> recreates the instance). Re-bind the listener
|
||||||
|
// here so it works even after detach/attach cycles.
|
||||||
|
if (!this._onHash) {
|
||||||
|
this._onHash = () => this.refresh_from_hash();
|
||||||
|
}
|
||||||
|
window.addEventListener("hashchange", this._onHash);
|
||||||
|
this.refresh_from_hash();
|
||||||
|
},
|
||||||
|
|
||||||
|
detached: function () {
|
||||||
|
if (this._onHash) {
|
||||||
|
window.removeEventListener("hashchange", this._onHash);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy: function () {
|
||||||
|
if (this._onHash) {
|
||||||
|
window.removeEventListener("hashchange", this._onHash);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
refresh_from_hash: function () {
|
||||||
|
const hash = location.hash.substr(1) || "settings";
|
||||||
|
const parts = hash.split(":");
|
||||||
|
this.sub = parts[0] || "settings";
|
||||||
|
this.ridx = parts[1] !== undefined ? parts[1] : -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
is_active: function (item) {
|
||||||
|
if (!item || item.section) return false;
|
||||||
|
if (item.sub !== this.sub) return false;
|
||||||
|
if (item.sub === "motor") {
|
||||||
|
return "" + item.motor === "" + this.ridx;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
showShutdownDialog: function () {
|
||||||
|
SvelteComponents.showDialog("Shutdown");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -8,7 +8,6 @@ html(lang="en")
|
|||||||
|
|
||||||
|
|
||||||
style: include ../static/css/pure-min.css
|
style: include ../static/css/pure-min.css
|
||||||
style: include ../static/css/side-menu.css
|
|
||||||
|
|
||||||
style: include ../static/css/font-awesome.min.css
|
style: include ../static/css/font-awesome.min.css
|
||||||
style: include ../static/css/Audiowide.css
|
style: include ../static/css/Audiowide.css
|
||||||
@@ -24,98 +23,112 @@ html(lang="en")
|
|||||||
#overlay(v-if="status != 'connected'")
|
#overlay(v-if="status != 'connected'")
|
||||||
span {{status}}
|
span {{status}}
|
||||||
|
|
||||||
#layout
|
.app-shell
|
||||||
a#menuLink.menu-link(href="#menu"): span
|
header.app-head
|
||||||
|
.brand-blk
|
||||||
|
.brand-logo
|
||||||
|
.brand-name ONEFINITY
|
||||||
|
|
||||||
#menu
|
nav.tabs-host(role="tablist")
|
||||||
button.save.pure-button.button-success(:disabled="!modified",
|
a.ktab(:class="{active: top_tab === 'control'}", href="#control",
|
||||||
@click="save") Save
|
title="Jog, DRO, macros")
|
||||||
|
.fa.fa-gamepad
|
||||||
|
span Control
|
||||||
|
a.ktab(:class="{active: top_tab === 'program'}", href="#program",
|
||||||
|
title="Run programs, files, toolpath preview")
|
||||||
|
.fa.fa-list-ol
|
||||||
|
span Program
|
||||||
|
a.ktab(:class="{active: top_tab === 'console'}", href="#console",
|
||||||
|
title="MDI, messages, indicators")
|
||||||
|
.fa.fa-terminal
|
||||||
|
span Console
|
||||||
|
span.ktab-badge(v-if="messages_count") {{messages_count}}
|
||||||
|
a.ktab(:class="{active: top_tab === 'settings'}", href="#settings",
|
||||||
|
title="Configuration, network, macros")
|
||||||
|
.fa.fa-sliders
|
||||||
|
span Settings
|
||||||
|
|
||||||
.pure-menu
|
.head-spacer
|
||||||
ul.pure-menu-list
|
|
||||||
li.pure-menu-heading
|
|
||||||
a.pure-menu-link(href="#control") Control
|
|
||||||
|
|
||||||
li.pure-menu-heading
|
.sys-btn(@click.stop="toggle_sys_popover", :class="{open: sys_open}")
|
||||||
a.pure-menu-link(href="#macros") Macros
|
span.pip(:class="sys_class")
|
||||||
|
span.sys-text {{sys_summary}}
|
||||||
|
.fa.fa-chevron-down
|
||||||
|
|
||||||
li.pure-menu-heading
|
.pi-temp-warning(v-if="80 <= state.rpi_temp",
|
||||||
a.pure-menu-link(href="#settings") Settings
|
|
||||||
|
|
||||||
li.pure-menu-heading
|
|
||||||
a.pure-menu-link(href="#motor:0") Motors
|
|
||||||
|
|
||||||
li.pure-menu-item(v-for="motor in config.motors")
|
|
||||||
a.pure-menu-link(:href="'#motor:' + $index") Motor {{$index}}
|
|
||||||
|
|
||||||
li.pure-menu-heading
|
|
||||||
a.pure-menu-link(href="#tool") Tool
|
|
||||||
|
|
||||||
li.pure-menu-heading
|
|
||||||
a.pure-menu-link(href="#io") I/O
|
|
||||||
|
|
||||||
li.pure-menu-heading
|
|
||||||
a.pure-menu-link(href="#admin-general") Admin
|
|
||||||
|
|
||||||
li.pure-menu-item
|
|
||||||
a.pure-menu-link(href="#admin-general") General
|
|
||||||
|
|
||||||
li.pure-menu-item
|
|
||||||
a.pure-menu-link(href="#admin-network") Network
|
|
||||||
|
|
||||||
li.pure-menu-heading
|
|
||||||
a.pure-menu-link(href="#cheat-sheet") Cheat Sheet
|
|
||||||
|
|
||||||
li.pure-menu-heading
|
|
||||||
a.pure-menu-link(href="#help") Help
|
|
||||||
|
|
||||||
button.pure-button.pure-button-primary(@click="showShutdownDialog", style="width: 100%")
|
|
||||||
.fa.fa-power-off
|
|
||||||
|
|
||||||
#main
|
|
||||||
.nav-header
|
|
||||||
.brand
|
|
||||||
img(src="/images/onefinity_logo.png")
|
|
||||||
.version
|
|
||||||
div Version: v{{config.full_version}}
|
|
||||||
div IP Address: {{config.ip}}
|
|
||||||
div WiFi: {{config.wifiName}}
|
|
||||||
a.upgrade-link(v-if="show_upgrade()", href="#admin-general")
|
|
||||||
| Upgrade to v{{latestVersion}}
|
|
||||||
.fa.fa-exclamation-circle.upgrade-attention(v-if="show_upgrade()")
|
|
||||||
|
|
||||||
.pi-temp-warning
|
|
||||||
.fa.fa-thermometer-full(class="error",
|
|
||||||
v-if="80 <= state.rpi_temp",
|
|
||||||
title="Raspberry Pi temperature too high.")
|
title="Raspberry Pi temperature too high.")
|
||||||
|
.fa.fa-thermometer-full
|
||||||
|
|
||||||
.easy-adapter(v-if="is_easy_adapter_active")
|
span.state-badge(:class="state_class", :title="mach_state_full")
|
||||||
.round-dot
|
span.dot
|
||||||
div.easy-adapter-text Easy Adapter
|
span {{state_label}}
|
||||||
|
|
||||||
.whitespace
|
|
||||||
|
|
||||||
div
|
|
||||||
button.rotary-button(:disabled="!enable_rotary", :class="is_rotary_active && 'active'", @click="showSwitchRotaryModeDialog")
|
|
||||||
img(src="/images/rotary.svg", alt="rotary", :style="is_rotary_active ? 'width:90%;' : 'width:85%;'")
|
|
||||||
div.rotary-text Rotary
|
|
||||||
|
|
||||||
.video(title="Plug camera into USB.\n" +
|
|
||||||
"Left click to toggle video size.\n" +
|
|
||||||
"Right click to toggle crosshair.", @click="toggle_video",
|
|
||||||
@contextmenu="toggle_crosshair", :class="video_size")
|
|
||||||
.crosshair(v-if="crosshair")
|
|
||||||
.vertical
|
|
||||||
.horizontal
|
|
||||||
.box
|
|
||||||
img(src="/api/video")
|
|
||||||
|
|
||||||
.estop(:class="{active: state.es}")
|
.estop(:class="{active: state.es}")
|
||||||
estop(@click="estop")
|
estop(@click="estop")
|
||||||
|
|
||||||
.content(class="{{currentView}}-view")
|
// System popover (chip-soup destination)
|
||||||
|
.sys-popover(v-if="sys_open", @click.stop="")
|
||||||
|
.sp-row
|
||||||
|
.sp-icon: .fa.fa-microchip
|
||||||
|
.sp-text
|
||||||
|
.sp-label Firmware
|
||||||
|
.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
|
||||||
|
.sp-row
|
||||||
|
.sp-icon: .fa.fa-network-wired
|
||||||
|
.sp-text
|
||||||
|
.sp-label IP Address
|
||||||
|
.sp-val {{config.ip}}
|
||||||
|
.sp-row
|
||||||
|
.sp-icon: .fa.fa-wifi(:class="{'sp-warn': config.wifiName === 'not connected'}")
|
||||||
|
.sp-text
|
||||||
|
.sp-label WiFi
|
||||||
|
.sp-val {{config.wifiName}}
|
||||||
|
a.sp-act(href="#admin-network", @click="sys_open=false") Configure
|
||||||
|
.sp-row(v-if="enable_rotary")
|
||||||
|
.sp-icon: img(src="/images/rotary.svg", alt="rotary")
|
||||||
|
.sp-text
|
||||||
|
.sp-label Rotary
|
||||||
|
.sp-val {{is_rotary_active ? 'Active' : 'Inactive'}}
|
||||||
|
button.sp-act(@click="showSwitchRotaryModeDialog")
|
||||||
|
| {{is_rotary_active ? 'Disable' : 'Enable'}}
|
||||||
|
.sp-row(v-if="is_easy_adapter_active")
|
||||||
|
.sp-icon: .fa.fa-puzzle-piece
|
||||||
|
.sp-text
|
||||||
|
.sp-label Easy Adapter
|
||||||
|
.sp-val Active
|
||||||
|
.sp-row.video-row
|
||||||
|
.sp-icon: .fa.fa-video
|
||||||
|
.sp-text
|
||||||
|
.sp-label Camera
|
||||||
|
.sp-val {{has_camera ? 'Live' : 'Plug camera into USB'}}
|
||||||
|
.sp-act(v-if="has_camera", @click="toggle_video")
|
||||||
|
| {{video_size === 'small' ? 'Enlarge' : 'Shrink'}}
|
||||||
|
.video(v-if="sys_open && has_camera", title="Camera feed",
|
||||||
|
@click="toggle_video", @contextmenu="toggle_crosshair",
|
||||||
|
:class="video_size")
|
||||||
|
.crosshair(v-if="crosshair")
|
||||||
|
.vertical
|
||||||
|
.horizontal
|
||||||
|
.box
|
||||||
|
img(src="/api/video", @error="has_camera=false")
|
||||||
|
.sp-foot
|
||||||
|
button.sp-shutdown(@click="showShutdownDialog")
|
||||||
|
.fa.fa-power-off
|
||||||
|
| Shutdown
|
||||||
|
button.sp-save(:disabled="!modified", @click="save")
|
||||||
|
.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)
|
||||||
|
.app-body
|
||||||
component(:is="currentView + '-view'", :index="index",
|
component(:is="currentView + '-view'", :index="index",
|
||||||
:config="config", :template="template", :state="state", keep-alive)
|
:config="config", :template="template", :state="state",
|
||||||
|
:sub-tab="sub_tab")
|
||||||
|
|
||||||
message.error-message(:show.sync="errorShow")
|
message.error-message(:show.sync="errorShow")
|
||||||
div(slot="header")
|
div(slot="header")
|
||||||
|
|||||||
67
src/pug/templates/console-view.pug
Normal file
67
src/pug/templates/console-view.pug
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
script#console-view-template(type="text/x-template")
|
||||||
|
.console-page
|
||||||
|
.console-card
|
||||||
|
.ptab-bar
|
||||||
|
button.ptab(:class="{active: sub === 'mdi'}", @click="select_sub('mdi')")
|
||||||
|
.fa.fa-keyboard
|
||||||
|
| MDI
|
||||||
|
button.ptab(:class="{active: sub === 'messages'}", @click="select_sub('messages')")
|
||||||
|
.fa.fa-comment-dots
|
||||||
|
| Messages
|
||||||
|
span.ptab-badge(v-if="unread_messages") {{unread_messages}}
|
||||||
|
button.ptab(:class="{active: sub === 'indicators'}", @click="select_sub('indicators')")
|
||||||
|
.fa.fa-bell
|
||||||
|
| Indicators
|
||||||
|
|
||||||
|
// ----- MDI -----
|
||||||
|
.mdi-pane(v-show="sub === 'mdi'")
|
||||||
|
.mdi-input
|
||||||
|
span.prompt G>
|
||||||
|
input(type="text", v-model="mdi", :disabled="!can_mdi",
|
||||||
|
@keyup.enter="submit_mdi", placeholder="enter a G-code command…")
|
||||||
|
button.mdi-send(:disabled="!can_mdi || !mdi", @click="submit_mdi")
|
||||||
|
.fa.fa-paper-plane
|
||||||
|
| SEND
|
||||||
|
.mdi-keys
|
||||||
|
button.mkey(@click="prepend('G0 ')") G0
|
||||||
|
button.mkey(@click="prepend('G1 ')") G1
|
||||||
|
button.mkey(@click="prepend('G2 ')") G2
|
||||||
|
button.mkey(@click="prepend('G3 ')") G3
|
||||||
|
button.mkey(@click="prepend('G28 ')") G28
|
||||||
|
button.mkey(@click="prepend('G92 ')") G92
|
||||||
|
button.mkey(@click="prepend('M3 ')") M3
|
||||||
|
button.mkey(@click="prepend('M5 ')") M5
|
||||||
|
button.mkey(@click="append('X')") X
|
||||||
|
button.mkey(@click="append('Y')") Y
|
||||||
|
button.mkey(@click="append('Z')") Z
|
||||||
|
button.mkey(@click="append('W')") W
|
||||||
|
button.mkey(@click="append('F')") F
|
||||||
|
button.mkey(@click="append('S')") S
|
||||||
|
button.mkey.clear(@click="mdi = ''") CLEAR
|
||||||
|
button.mkey.send(:disabled="!can_mdi || !mdi", @click="submit_mdi") SEND ↵
|
||||||
|
|
||||||
|
em Machine units: #[strong {{mach_units}}]. G20/G21 to switch.
|
||||||
|
|
||||||
|
.mdi-history(:class="{placeholder: !history.length}")
|
||||||
|
span.mdi-empty(v-if="!history.length") MDI history will display here.
|
||||||
|
.h-row(v-for="item in history", @click="load_history($index)",
|
||||||
|
track-by="$index")
|
||||||
|
span.h-cmd {{item}}
|
||||||
|
span.h-status ↻
|
||||||
|
|
||||||
|
// ----- Messages -----
|
||||||
|
.messages-pane(v-show="sub === 'messages'")
|
||||||
|
.msg-empty(v-if="!$root.messages_log.length")
|
||||||
|
.fa.fa-check-circle
|
||||||
|
| No messages.
|
||||||
|
.msg(v-for="m in $root.messages_log",
|
||||||
|
:class="m.level === 'warning' ? 'warn' : 'info'", track-by="$index")
|
||||||
|
.mi
|
||||||
|
.fa(:class="m.level === 'warning' ? 'fa-triangle-exclamation' : 'fa-circle-info'")
|
||||||
|
div
|
||||||
|
.mtitle {{m.text}}
|
||||||
|
.mtime ID {{m.id}}
|
||||||
|
|
||||||
|
// ----- Indicators -----
|
||||||
|
.indicators-pane(v-show="sub === 'indicators'")
|
||||||
|
indicators(:state="state", :template="template")
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
script#control-view-template(type="text/x-template")
|
script#control-view-template(type="text/x-template")
|
||||||
#control
|
.control-page
|
||||||
|
// ----- Modal dialogs (kept verbatim from legacy) -----
|
||||||
message(:show.sync="showGcodeMessage")
|
message(:show.sync="showGcodeMessage")
|
||||||
h3(slot="header") Processing New File
|
h3(slot="header") Processing New File
|
||||||
|
|
||||||
div(slot="body")
|
div(slot="body")
|
||||||
h3 Please wait..
|
h3 Please wait..
|
||||||
p Simulating GCode to check for errors, calculate ETA and generate 3D view.
|
p Simulating GCode to check for errors, calculate ETA and generate 3D view.
|
||||||
|
|
||||||
div(slot="footer")
|
div(slot="footer")
|
||||||
label Simulating {{(toolpath_progress || 0) | percent}}
|
label Simulating {{(toolpath_progress || 0) | percent}}
|
||||||
|
|
||||||
@@ -14,7 +13,6 @@ script#control-view-template(type="text/x-template")
|
|||||||
h3(slot="header") GCode Not Set
|
h3(slot="header") GCode Not Set
|
||||||
div(slot="body")
|
div(slot="body")
|
||||||
p Configure the GCode for the selected macro to use it
|
p Configure the GCode for the selected macro to use it
|
||||||
|
|
||||||
div(slot="footer")
|
div(slot="footer")
|
||||||
button.pure-button(@click="showNoGcodeMessage=false") OK
|
button.pure-button(@click="showNoGcodeMessage=false") OK
|
||||||
|
|
||||||
@@ -25,7 +23,6 @@ script#control-view-template(type="text/x-template")
|
|||||||
| The macro file
|
| The macro file
|
||||||
strong {{state.selected}}
|
strong {{state.selected}}
|
||||||
| is being loaded.
|
| is being loaded.
|
||||||
|
|
||||||
div(slot="footer")
|
div(slot="footer")
|
||||||
button.pure-button(@click="macrosLoading=false") Cancel
|
button.pure-button(@click="macrosLoading=false") Cancel
|
||||||
button.pure-button.pure-button-primary(@click="start_pause") Run
|
button.pure-button.pure-button-primary(@click="start_pause") Run
|
||||||
@@ -35,8 +32,7 @@ script#control-view-template(type="text/x-template")
|
|||||||
div(slot="body")
|
div(slot="body")
|
||||||
p It seems like the file you selected cannot be found. Try uploading again.
|
p It seems like the file you selected cannot be found. Try uploading again.
|
||||||
div(slot="footer")
|
div(slot="footer")
|
||||||
button.pure-button.button-error(@click="GCodeNotFound=false")
|
button.pure-button.button-error(@click="GCodeNotFound=false") OK
|
||||||
| OK
|
|
||||||
|
|
||||||
message(:show.sync="show_probe_dialog")
|
message(:show.sync="show_probe_dialog")
|
||||||
h3(slot="header") Probe Rotary
|
h3(slot="header") Probe Rotary
|
||||||
@@ -46,485 +42,232 @@ script#control-view-template(type="text/x-template")
|
|||||||
div(slot="footer")
|
div(slot="footer")
|
||||||
button.pure-button(@click="show_probe_dialog=false") Cancel
|
button.pure-button(@click="show_probe_dialog=false") Cancel
|
||||||
|
|
||||||
|
// ----- Main grid: jog | (DRO + status strip) -----
|
||||||
|
.control-grid
|
||||||
|
|
||||||
table(style="table-layout: fixed; width: 100%;")
|
// ===== JOG =====
|
||||||
tr(style="height: fit-content;")
|
.jog-card
|
||||||
td(style="white-space: nowrap; width: 410px;", rowspan="2")
|
.jog-head
|
||||||
table.control-buttons(table-layout="fixed")
|
.jog-title
|
||||||
colgroup
|
| Jog
|
||||||
col(style="width:100px")
|
span.step-pre · step
|
||||||
col(style="width:100px")
|
span.step {{jog_incr_amounts[display_units][jog_incr]}}#[span.unit {{metric ? 'mm' : 'in'}}]
|
||||||
col(style="width:100px")
|
.step-seg
|
||||||
col(style="width:100px")
|
button(:class="{active: jog_incr === 'fine'}", @click="jog_incr = 'fine'")
|
||||||
tr
|
| {{jog_incr_amounts[display_units].fine}}
|
||||||
td(style="height:100px",align="center")
|
button(:class="{active: jog_incr === 'small'}", @click="jog_incr = 'small'")
|
||||||
button(@click="jog_fn(-1,1,0,0)")
|
| {{jog_incr_amounts[display_units].small}}
|
||||||
.fa.fa-arrow-right(style="transform: rotate(-135deg);")
|
button(:class="{active: jog_incr === 'medium'}", @click="jog_incr = 'medium'")
|
||||||
td(style="height:100px",align="center")
|
| {{jog_incr_amounts[display_units].medium}}
|
||||||
button(@click="jog_fn(0,1,0,0)") Y+
|
button(:class="{active: jog_incr === 'large'}", @click="jog_incr = 'large'")
|
||||||
td(style="height:100px",align="center")
|
| {{jog_incr_amounts[display_units].large}}
|
||||||
button(@click="jog_fn(1,1,0,0)")
|
|
||||||
.fa.fa-arrow-right(style="transform: rotate(-45deg);")
|
|
||||||
td(style="height:100px",align="center")
|
|
||||||
button(,@click="jog_fn(0,0,1,0)") Z+
|
|
||||||
tr
|
|
||||||
td(style="height:100px",align="center")
|
|
||||||
button(@click="jog_fn(-1,0,0,0)") X-
|
|
||||||
td(style="height:100px",align="center")
|
|
||||||
button(@click="showMoveToZeroDialog('xy')")
|
|
||||||
| XY
|
|
||||||
br
|
|
||||||
| Origin
|
|
||||||
td(style="height:100px",align="center")
|
|
||||||
button(@click="jog_fn(1,0,0,0)") X+
|
|
||||||
td(style="height:100px",align="center")
|
|
||||||
button(@click="showMoveToZeroDialog('z')")
|
|
||||||
| Z
|
|
||||||
br
|
|
||||||
| Origin
|
|
||||||
tr
|
|
||||||
td(style="height:100px",align="center")
|
|
||||||
button(@click="jog_fn(-1,-1,0,0)")
|
|
||||||
.fa.fa-arrow-right(style="transform: rotate(135deg);")
|
|
||||||
td(style="height:100px",align="center")
|
|
||||||
button(@click="jog_fn(0,-1,0,0)") Y-
|
|
||||||
td(style="height:100px",align="center")
|
|
||||||
button(@click="jog_fn(1,-1,0,0)")
|
|
||||||
.fa.fa-arrow-right(style="transform: rotate(45deg);")
|
|
||||||
td(style="height:100px",align="center")
|
|
||||||
button(@click="jog_fn(0,0,-1,0)") Z-
|
|
||||||
tr
|
|
||||||
td(style="height:100px",align="center")
|
|
||||||
button(:style="getJogIncrStyle('fine')", @click="jog_incr = 'fine'")
|
|
||||||
span {{jog_incr_amounts[display_units].fine}}#[span.jog-units {{metric ? 'mm' : 'in'}}]
|
|
||||||
td(style="height:100px",align="center")
|
|
||||||
button(:style="getJogIncrStyle('small')", @click="jog_incr = 'small'")
|
|
||||||
span {{jog_incr_amounts[display_units].small}}#[span.jog-units {{metric ? 'mm' : 'in'}}]
|
|
||||||
td(style="height:100px",align="center")
|
|
||||||
button(:style="getJogIncrStyle('medium')", @click="jog_incr = 'medium'")
|
|
||||||
span {{jog_incr_amounts[display_units].medium}}#[span.jog-units {{metric ? 'mm' : 'in'}}]
|
|
||||||
td(style="height:100px",align="center")
|
|
||||||
button(:style="getJogIncrStyle('large')", @click="jog_incr = 'large'")
|
|
||||||
span {{jog_incr_amounts[display_units].large}}#[span.jog-units {{metric ? 'mm' : 'in'}}]
|
|
||||||
|
|
||||||
// W axis jog row (auxcnc). Only shown when the aux controller
|
.jog-grid
|
||||||
// is enabled in aux.json. We treat home == 0 for the W axis,
|
// Row 1
|
||||||
// so there is no separate "set zero" / "W origin" button -
|
button.jbtn.dir(@click="jog_fn(-1, 1, 0, 0)", title="X- Y+")
|
||||||
// just W-, W+, and Home.
|
.fa.fa-arrow-up.ico(style="transform: rotate(-45deg)")
|
||||||
tr(v-if="w.enabled")
|
button.jbtn(@click="jog_fn(0, 1, 0, 0)") Y+
|
||||||
td(style="height:100px", align="center", colspan="1")
|
button.jbtn.dir(@click="jog_fn(1, 1, 0, 0)", title="X+ Y+")
|
||||||
button(@click="aux_jog_incr(-1)",
|
.fa.fa-arrow-up.ico(style="transform: rotate(45deg)")
|
||||||
:disabled="!w.enabled",
|
button.jbtn(@click="jog_fn(0, 0, 1, 0)") Z+
|
||||||
style="display:grid;justify-content:center;align-items:center;padding:14px;")
|
|
||||||
| W-
|
|
||||||
.fa.fa-arrow-down
|
|
||||||
|
|
||||||
td(style="height:100px", align="center", colspan="1")
|
// Row 2
|
||||||
button(@click="aux_jog_incr(+1)",
|
button.jbtn(@click="jog_fn(-1, 0, 0, 0)") X−
|
||||||
:disabled="!w.enabled",
|
button.jbtn.ghost(@click="showMoveToZeroDialog('xy')")
|
||||||
style="display:grid;justify-content:center;align-items:center;padding:14px;")
|
span.lbl XY
|
||||||
| W+
|
span Origin
|
||||||
.fa.fa-arrow-up
|
button.jbtn(@click="jog_fn(1, 0, 0, 0)") X+
|
||||||
|
button.jbtn.ghost(@click="showMoveToZeroDialog('z')")
|
||||||
|
span.lbl Z
|
||||||
|
span Origin
|
||||||
|
|
||||||
td(style="height:100px", align="center", colspan="2")
|
// Row 3
|
||||||
button(@click="aux_home()", :disabled="!w.enabled",
|
button.jbtn.dir(@click="jog_fn(-1, -1, 0, 0)", title="X- Y-")
|
||||||
style="height:100px;width:200px")
|
.fa.fa-arrow-down.ico(style="transform: rotate(45deg)")
|
||||||
| Home
|
button.jbtn(@click="jog_fn(0, -1, 0, 0)") Y−
|
||||||
br
|
button.jbtn.dir(@click="jog_fn(1, -1, 0, 0)", title="X+ Y-")
|
||||||
| W
|
.fa.fa-arrow-down.ico(style="transform: rotate(-45deg)")
|
||||||
|
button.jbtn(@click="jog_fn(0, 0, -1, 0)") Z−
|
||||||
|
|
||||||
tr(v-if="state['2an'] == 3")
|
// Row 4 — auxiliary axis (W or A) or probe shortcuts
|
||||||
td(style="height:100px", align="center", colspan="1")
|
template(v-if="w.enabled")
|
||||||
button(@click="show_probe_dialog=true")
|
button.jbtn(@click="aux_jog_incr(-1)", :disabled="!w.enabled")
|
||||||
| Probe
|
.fa.fa-arrow-down.ico
|
||||||
br
|
span.lbl W−
|
||||||
| Rotary
|
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",
|
||||||
|
:class="{'load-on': !state['pw']}")
|
||||||
|
.fa.fa-bullseye.ico
|
||||||
|
span.lbl Probe
|
||||||
|
template(v-else-if="state['2an'] == 3")
|
||||||
|
button.jbtn.dir(@click="jog_fn(0, 0, 0, -1)")
|
||||||
|
.fa.fa-rotate-left.ico
|
||||||
|
span.lbl A−
|
||||||
|
button.jbtn.ghost(@click="showMoveToZeroDialog('a')")
|
||||||
|
span.lbl A
|
||||||
|
span Origin
|
||||||
|
button.jbtn.dir(@click="jog_fn(0, 0, 0, 1)")
|
||||||
|
.fa.fa-rotate-right.ico
|
||||||
|
span.lbl A+
|
||||||
|
button.jbtn(@click="show_probe_dialog=true",
|
||||||
|
:class="{'load-on': !state['pw']}")
|
||||||
|
.fa.fa-bullseye.ico
|
||||||
|
span.lbl Probe
|
||||||
|
template(v-else)
|
||||||
|
button.jbtn(@click="showProbeDialog('xyz')",
|
||||||
|
:class="{'load-on': !state['pw']}")
|
||||||
|
.fa.fa-bullseye.ico
|
||||||
|
span.lbl Probe XYZ
|
||||||
|
button.jbtn.ghost(@click="zero()", :disabled="!can_set_axis")
|
||||||
|
.fa.fa-map-marker.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")
|
||||||
|
.fa.fa-home.ico
|
||||||
|
span.lbl Home all
|
||||||
|
|
||||||
td(style="height:100px", align="center", colspan="1")
|
// ===== DRO + status strip =====
|
||||||
button(@click="jog_fn(0,0,0,-1)", style="display: grid;justify-content: center;align-items: center;padding: 14px;")
|
.right-col
|
||||||
| A-
|
|
||||||
.fa.fa-rotate-left
|
|
||||||
|
|
||||||
td(style="height:100px", align="center", colspan="1")
|
.dro-card
|
||||||
button(@click="showMoveToZeroDialog('a')")
|
.dro-head
|
||||||
| A
|
div Axis
|
||||||
br
|
div Position
|
||||||
| Origin
|
div Absolute
|
||||||
|
div Offset
|
||||||
td(style="height:100px", align="center", colspan="1")
|
div State
|
||||||
button(@click="jog_fn(0,0,0,1)", style="display: grid;justify-content: center;align-items: center;padding: 14px;")
|
div Toolpath
|
||||||
| A+
|
div(style="text-align:right") Actions
|
||||||
.fa.fa-rotate-right
|
|
||||||
|
|
||||||
tr(v-else)
|
|
||||||
td(style="height:100px", align="center", colspan="2")
|
|
||||||
button(:class="state['pw'] ? '' : 'load-on'",
|
|
||||||
style="height:100px;width:200px",
|
|
||||||
@click="showProbeDialog('xyz')")
|
|
||||||
| Probe XYZ
|
|
||||||
|
|
||||||
td(style="height:100px", align="center", colspan="2")
|
|
||||||
button(:class="state['pw'] ? '' : 'load-on'",
|
|
||||||
style="height:100px;width:200px",
|
|
||||||
@click="showProbeDialog('z')")
|
|
||||||
| Probe Z
|
|
||||||
|
|
||||||
td(style="vertical-align: top;")
|
|
||||||
table.axes
|
|
||||||
tr(:class="axes.klass")
|
|
||||||
th.name Axis
|
|
||||||
th.position Position
|
|
||||||
th.absolute Absolute
|
|
||||||
th.offset Offset
|
|
||||||
th.state State
|
|
||||||
th.tstate Toolpath
|
|
||||||
th.actions
|
|
||||||
button.pure-button(disabled, style="height:60px;width:60px;display:none;")
|
|
||||||
|
|
||||||
button.pure-button(:disabled="!can_set_axis",
|
|
||||||
title="Zero all axis offsets.", @click="zero()",style="height:60px;width:60px")
|
|
||||||
.fa.fa-map-marker
|
|
||||||
|
|
||||||
button.pure-button(title="Home all axes.", @click="home()",
|
|
||||||
:disabled="!is_idle",style="height:60px;width:60px")
|
|
||||||
.fa.fa-home
|
|
||||||
|
|
||||||
|
// Per-axis rows — keep unit-value + bindings from axis-vars
|
||||||
each axis in 'xyzabc'
|
each axis in 'xyzabc'
|
||||||
tr.axis(:class=`${axis}.klass`, v-if=`${axis}.enabled`,
|
.dro-row(:class=`${axis}.klass + ' ' + ${axis}.tklass`,
|
||||||
|
v-if=`${axis}.enabled`,
|
||||||
:title=`${axis}.title`)
|
:title=`${axis}.title`)
|
||||||
th.name= axis
|
.dro-axis(:class=`'axis-' + '${axis}'`)= axis.toUpperCase()
|
||||||
td.position: unit-value(:value=`${axis}.pos`, precision=4)
|
.dro-pos: unit-value(:value=`${axis}.pos`, precision=4)
|
||||||
td.absolute: unit-value(:value=`${axis}.abs`, precision=3)
|
.dro-sec: unit-value(:value=`${axis}.abs`, precision=3)
|
||||||
td.offset: unit-value(:value=`${axis}.off`, precision=3)
|
.dro-sec: unit-value(:value=`${axis}.off`, precision=3)
|
||||||
td.state
|
.dro-state
|
||||||
|
span.chip(:class=`${axis}.tklass.indexOf('error') !== -1 ? 'chip-red' : (${axis}.homed ? 'chip-green' : 'chip-amber')`)
|
||||||
.fa(:class=`'fa-' + ${axis}.icon`)
|
.fa(:class=`'fa-' + ${axis}.icon`)
|
||||||
| {{#{axis}.state}}
|
| {{#{axis}.state}}
|
||||||
td.tstate(:class=`${axis}.tklass`, :title=`${axis}.toolmsg`, @click=`showToolpathMessageDialog('${axis}')`)
|
.dro-toolpath
|
||||||
|
span.chip(:class=`${axis}.tklass.indexOf('error') !== -1 ? 'chip-red' : (${axis}.tklass.indexOf('warn') !== -1 ? 'chip-amber' : 'chip-green')`,
|
||||||
|
@click=`showToolpathMessageDialog('${axis}')`)
|
||||||
.fa(:class=`'fa-' + ${axis}.ticon`)
|
.fa(:class=`'fa-' + ${axis}.ticon`)
|
||||||
| {{#{axis}.tstate}}
|
| {{#{axis}.tstate}}
|
||||||
|
.actions-cell
|
||||||
th.actions
|
button.icon-btn(:disabled="!can_set_axis",
|
||||||
button.pure-button(:disabled="!can_set_axis",
|
:title=`'Set ${axis.toUpperCase()} axis position.'`,
|
||||||
title=`Set {{'${axis}' | upper}} axis position.`,
|
@click=`show_set_position('${axis}')`)
|
||||||
@click=`show_set_position('${axis}')`, style="height:60px;width:60px")
|
|
||||||
.fa.fa-cog
|
.fa.fa-cog
|
||||||
|
button.icon-btn(:disabled="!can_set_axis",
|
||||||
button.pure-button(:disabled="!can_set_axis",
|
:title=`'Zero ${axis.toUpperCase()} axis offset.'`,
|
||||||
title=`Zero {{'${axis}' | upper}} axis offset.`,
|
@click=`zero('${axis}')`)
|
||||||
@click=`zero('${axis}')`, style="height:60px;width:60px")
|
|
||||||
.fa.fa-map-marker
|
.fa.fa-map-marker
|
||||||
|
button.icon-btn(:disabled="!is_idle",
|
||||||
button.pure-button(:disabled="!is_idle", @click=`home('${axis}')`,
|
:title=`'Home ${axis.toUpperCase()} axis.'`,
|
||||||
title=`Home {{'${axis}' | upper}} axis.`, style="height:60px;width:60px")
|
@click=`home('${axis}')`)
|
||||||
.fa.fa-home
|
.fa.fa-home
|
||||||
|
|
||||||
// Auxiliary W axis (auxcnc ESP32 over /dev/ttyUSB0)
|
// W axis (auxiliary) — no offset, no set-zero / no set-position
|
||||||
tr.axis(:class="w.klass", v-if="w.enabled", :title="w.title")
|
.dro-row(:class="w.klass + ' ' + w.tklass", v-if="w.enabled",
|
||||||
th.name w
|
:title="w.title")
|
||||||
td.position: unit-value(:value="w.pos", precision=4)
|
.dro-axis.axis-w W
|
||||||
td.absolute: unit-value(:value="w.abs", precision=3)
|
.dro-pos: unit-value(:value="w.pos", precision=4)
|
||||||
td.offset —
|
.dro-sec: unit-value(:value="w.abs", precision=3)
|
||||||
td.state
|
.dro-sec —
|
||||||
|
.dro-state
|
||||||
|
span.chip(:class="w.homed ? 'chip-green' : 'chip-amber'")
|
||||||
.fa(:class="'fa-' + w.icon")
|
.fa(:class="'fa-' + w.icon")
|
||||||
| {{w.state}}
|
| {{w.state}}
|
||||||
td.tstate(:class="w.tklass", :title="w.toolmsg")
|
.dro-toolpath
|
||||||
|
span.chip.chip-green
|
||||||
.fa(:class="'fa-' + w.ticon")
|
.fa(:class="'fa-' + w.ticon")
|
||||||
| {{w.tstate}}
|
| {{w.tstate}}
|
||||||
|
.actions-cell
|
||||||
th.actions
|
button.icon-btn(disabled, style="visibility:hidden")
|
||||||
// Invisible placeholders so the W home button lines up
|
|
||||||
// under the X/Y/Z home column. The W axis has no "set
|
|
||||||
// position" cog and no "zero offset" marker - home == 0.
|
|
||||||
button.pure-button(disabled,
|
|
||||||
style="height:60px;width:60px;visibility:hidden")
|
|
||||||
.fa.fa-cog
|
.fa.fa-cog
|
||||||
|
button.icon-btn(disabled, style="visibility:hidden")
|
||||||
button.pure-button(disabled,
|
|
||||||
style="height:60px;width:60px;visibility:hidden")
|
|
||||||
.fa.fa-map-marker
|
.fa.fa-map-marker
|
||||||
|
button.icon-btn(:disabled="!w.enabled",
|
||||||
button.pure-button(:disabled="!w.enabled", @click="aux_home()",
|
title="Home W axis.", @click="aux_home()")
|
||||||
title="Home W axis.", style="height:60px;width:60px")
|
|
||||||
.fa.fa-home
|
.fa.fa-home
|
||||||
|
|
||||||
tr(style="vertical-align: top;")
|
// ----- Status strip -----
|
||||||
td
|
.status-strip
|
||||||
table(width="100%")
|
.stat-card
|
||||||
tr
|
.stat-label State
|
||||||
td(style="text-align:center")
|
.stat-val(:class="state_kpi_class") {{mach_state || '--'}}
|
||||||
table.info
|
.stat-sub(v-if="message") {{message.replace(/^#/, '')}}
|
||||||
tr
|
.stat-sub(v-else) No alerts
|
||||||
th State
|
|
||||||
td(:class="{attention: highlight_state}") {{mach_state}}
|
|
||||||
|
|
||||||
tr
|
.stat-card
|
||||||
th Message
|
.stat-label Velocity / Feed
|
||||||
td.message(:class="{attention: highlight_state}")
|
.stat-val
|
||||||
| {{message.replace(/^#/, '')}}
|
|
||||||
|
|
||||||
tr
|
|
||||||
th Display Units
|
|
||||||
td.units
|
|
||||||
select(v-model="display_units")
|
|
||||||
option(value="METRIC") METRIC
|
|
||||||
option(value="IMPERIAL") IMPERIAL
|
|
||||||
|
|
||||||
tr(title="Active tool")
|
|
||||||
th Tool
|
|
||||||
td {{state.tool || 0}}
|
|
||||||
|
|
||||||
td
|
|
||||||
table.info
|
|
||||||
tr(
|
|
||||||
title="Current velocity in {{metric ? 'meters' : 'inches'}} per minute")
|
|
||||||
th Velocity
|
|
||||||
td
|
|
||||||
unit-value(:value="state.v", precision="2", unit="", iunit="",
|
unit-value(:value="state.v", precision="2", unit="", iunit="",
|
||||||
scale="0.0254")
|
scale="0.0254")
|
||||||
| {{metric ? ' m/min' : ' IPM'}}
|
| ·
|
||||||
|
unit-value(:value="state.feed", precision="0", unit="", iunit="")
|
||||||
|
.stat-sub {{metric ? 'm/min · mm/min' : 'IPM · IPM'}}
|
||||||
|
|
||||||
tr(title="Programmed feed rate.")
|
.stat-card.stat-tappable(@click="overrides_open = !overrides_open",
|
||||||
th Feed
|
:class="{open: overrides_open}", title="Tap to adjust feed/spindle override")
|
||||||
td
|
.stat-label Spindle
|
||||||
unit-value(:value="state.feed", precision="2", unit="", iunit="")
|
.stat-val
|
||||||
| {{metric ? ' mm/min' : ' IPM'}}
|
| {{(state.speed || 0) | fixed 0}}
|
||||||
|
span(v-if="state.s != null && !isNaN(state.s)") ({{state.s | fixed 0}})
|
||||||
|
.stat-sub
|
||||||
|
| RPM (commanded / actual)
|
||||||
|
.fa.fa-sliders.tap-hint(title="Open override drawer")
|
||||||
|
|
||||||
tr(title="Programed and actual speed.")
|
.stat-card
|
||||||
th Speed
|
.stat-label Job
|
||||||
td
|
.stat-val
|
||||||
| {{state.speed || 0 | fixed 0}}
|
|
||||||
span(v-if="!isNaN(state.s)") ({{state.s | fixed 0}})
|
|
||||||
= ' RPM'
|
|
||||||
|
|
||||||
tr(title="Load switch states.")
|
|
||||||
th Loads
|
|
||||||
td
|
|
||||||
span(:class="state['1oa'] ? 'load-on' : ''")
|
|
||||||
| 1:{{state['1oa'] ? 'On' : 'Off'}}
|
|
||||||
|
|
|
||||||
span(:class="state['2oa'] ? 'load-on' : ''")
|
|
||||||
| 2:{{state['2oa'] ? 'On' : 'Off'}}
|
|
||||||
|
|
||||||
td
|
|
||||||
table.info
|
|
||||||
tr
|
|
||||||
th Remaining
|
|
||||||
td(title="Total run time (days:hours:mins:secs)").
|
|
||||||
#[span(v-if="plan_time_remaining") {{plan_time_remaining | time}} of]
|
|
||||||
{{toolpath.time | time}}
|
|
||||||
|
|
||||||
tr
|
|
||||||
th ETA
|
|
||||||
td.eta {{eta}}
|
|
||||||
|
|
||||||
tr
|
|
||||||
th Line
|
|
||||||
td
|
|
||||||
| {{0 <= state.line ? state.line : 0 | number}}
|
| {{0 <= state.line ? state.line : 0 | number}}
|
||||||
span(v-if="toolpath.lines")
|
span(v-if="toolpath.lines")
|
||||||
| of {{toolpath.lines | number}}
|
| / {{toolpath.lines | number}}
|
||||||
|
.stat-sub(v-if="plan_time_remaining || toolpath.time")
|
||||||
|
| Line · {{plan_time_remaining ? (plan_time_remaining | time) : (toolpath.time | time)}} remaining
|
||||||
|
.stat-sub(v-else) Line · ETA --
|
||||||
|
|
||||||
tr
|
// ----- Macro row (slice 0..7); full list lives in Settings → Macros -----
|
||||||
th Progress
|
.macro-row(v-if="state.macros && state.macros.length")
|
||||||
td.progress
|
button.macro-btn(v-for="(index, macros) in state.macros.slice(0, 8)",
|
||||||
label {{(progress || 0) | percent}}
|
title="Click to run macro",
|
||||||
.bar(:style="'width:' + (progress || 0) * 100 + '%'")
|
@click="run_macro(index)",
|
||||||
|
:disabled="!is_ready",
|
||||||
|
:style="{ borderLeftColor: macros.color || '#fde047' }")
|
||||||
|
span.mnum {{index + 1}}
|
||||||
|
.fa.fa-circle-play.micon
|
||||||
|
span.mname {{macros.name || ('Macro ' + (index + 1))}}
|
||||||
|
|
||||||
.macros-div(class="present")
|
// ----- Override drawer (anchored to bottom; toggled by Spindle KPI tile) -----
|
||||||
button.macros-button(title="Click to run Macros",v-for="(index,macros) in state.macros",
|
.override-drawer(:class="{open: overrides_open}")
|
||||||
@click="run_macro(index)",:disabled="!is_ready",v-bind:style="{ backgroundColor: macros.color }") {{macros.name}}
|
.od-head
|
||||||
|
.od-title
|
||||||
.tabs
|
.fa.fa-sliders
|
||||||
|
| Overrides
|
||||||
input#tab1(type="radio", name="tabs",checked="" @click="tab = 'auto'")
|
button.od-close(@click="overrides_open = false") ✕
|
||||||
label(for="tab1", title="Run GCode programs",style="height:50px;width:100px") Auto
|
.od-body
|
||||||
|
.od-row
|
||||||
input#tab2(type="radio", name="tabs", @click="tab = 'mdi'")
|
|
||||||
label(for="tab2", title="Manual GCode entry",style="height:50px;width:100px") MDI
|
|
||||||
|
|
||||||
input#tab3(type="radio", name="tabs", @click="tab = 'messages'")
|
|
||||||
label(for="tab3",style="height:50px;width:100px") Messages
|
|
||||||
|
|
||||||
input#tab4(type="radio", name="tabs", @click="tab = 'indicators'")
|
|
||||||
label(for="tab4",style="height:50px;width:100px") Indicators
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
section#content1.tab-content.pure-form
|
|
||||||
.toolbar.pure-control-group
|
|
||||||
button.pure-button(:class="{'attention': is_holding}",
|
|
||||||
title="{{is_running ? 'Pause' : 'Start'}} program.",
|
|
||||||
@click="start_pause", :disabled="!state.selected",
|
|
||||||
style="height:100px;width:100px;font-weight:normal")
|
|
||||||
img(v-if="is_running" src="images/pause_gcode.png" style="height: 55px;")
|
|
||||||
img(v-else src="images/play_gcode.png" style="height: 55px;")
|
|
||||||
|
|
||||||
button.pure-button(title="Stop program.", @click="stop", style="height:100px;width:100px;font-weight:normal")
|
|
||||||
img(src="images/stop.png" style="height: 55px;")
|
|
||||||
|
|
||||||
button.pure-button(title="Pause program at next optional stop (M1).",
|
|
||||||
@click="optional_pause", v-if="false", style="height:100px;width:100px;font-weight:normal")
|
|
||||||
.fa.fa-stop-circle-o
|
|
||||||
|
|
||||||
message(:show.sync="uploading_files")
|
|
||||||
h3(slot="header") Files uploading
|
|
||||||
div(slot="body")
|
|
||||||
h3 Please wait...
|
|
||||||
p
|
|
||||||
p The files are currently being uploaded.
|
|
||||||
p Do not close the window.
|
|
||||||
div(slot="footer")
|
|
||||||
|
|
||||||
button.pure-button(title="Execute one program step.", @click="step",
|
|
||||||
:disabled="(!is_ready && !is_holding) || !state.selected",
|
|
||||||
v-if="false", style="height:100px;width:100px;font-weight:normal")
|
|
||||||
.fa.fa-step-forward
|
|
||||||
|
|
||||||
button.pure-button(title="Upload a new GCode folder.", @click="open_folder",
|
|
||||||
:disabled="!is_ready",style="height:100px;width:100px;font-weight:normal")
|
|
||||||
img(src="images/upload_folder.png" style="height: 65px;")
|
|
||||||
|
|
||||||
form.gcode-folder-input.file-upload
|
|
||||||
input#folderInput(type="file", @change="upload_folder", :disabled="!is_ready",
|
|
||||||
webkitdirectory, directory)
|
|
||||||
|
|
||||||
button.pure-button(title="Upload a new GCode program.", @click="open_file",
|
|
||||||
:disabled="!is_ready",style="height:100px;width:100px;font-weight:normal")
|
|
||||||
img(src="images/upload_gcode.png" style="height: 65px;")
|
|
||||||
|
|
||||||
form.gcode-file-input.file-upload
|
|
||||||
input(type="file", @change="upload_file", :disabled="!is_ready",
|
|
||||||
accept=".nc,.ngc,.gcode,.gc", multiple)
|
|
||||||
|
|
||||||
a(:disabled="!state.selected", download,
|
|
||||||
:href="'/api/file/' + state.selected",
|
|
||||||
title="Download the selected GCode program.")
|
|
||||||
button.pure-button(:disabled="!state.selected", style="height:100px;width:100px")
|
|
||||||
img(src="images/download_gcode.png" style="height: 65px;")
|
|
||||||
|
|
||||||
button.pure-button(title="Delete current GCode program.",
|
|
||||||
@click="deleteGCode = true",
|
|
||||||
:disabled="!state.selected || !is_ready",style="height:100px;width:100px;font-weight:normal")
|
|
||||||
img(src="images/delete_gcode.png" style="height: 55px;")
|
|
||||||
|
|
||||||
message.error-message(:show.sync="deleteGCode")
|
|
||||||
h3(slot="header") Select files to delete:
|
|
||||||
div(slot="body")
|
|
||||||
input.search-bar(type="text", v-model="search_query", placeholder="Search Files...")
|
|
||||||
.container
|
|
||||||
.folders
|
|
||||||
h3 Folders
|
|
||||||
div(v-for="(index, folder) in state.gcode_list", :key="index", @click="populateFiles(index)",
|
|
||||||
class="folder-item", :class="{ selected: index === selected_folder_index }") {{ folder.name }}
|
|
||||||
.files
|
|
||||||
h3 Files
|
|
||||||
label.file-item(v-for="item in gcode_filtered_files" :key="item")
|
|
||||||
input(type="checkbox" :value="item" v-model="selected_items_to_delete")
|
|
||||||
| {{ item }}
|
|
||||||
div(slot="footer")
|
|
||||||
button.pure-button(@click="cancel_delete",style="height:50px") Cancel
|
|
||||||
//- button.pure-button.button-error(@click="delete_all_except_macros")
|
|
||||||
//- .fa.fa-trash
|
|
||||||
//- | All
|
|
||||||
button.pure-button.button-success(@click="delete_current",style="height:50px")
|
|
||||||
.fa.fa-trash
|
|
||||||
| Selected
|
|
||||||
|
|
||||||
.drop-down-container
|
|
||||||
message(:show.sync="create_folder")
|
|
||||||
h3(slot="header") Enter folder name:
|
|
||||||
div(slot="body")
|
|
||||||
input.input-name(type="text",minlength='1',maxlength='15',style ="margin-top:1rem;margin-bottom:2rem;",
|
|
||||||
id="folder-name" ,v-model="folder_name",@keypress="edited_folder_name")
|
|
||||||
|
|
||||||
div(slot="footer")
|
|
||||||
button.pure-button(@click="cancel_new_folder") Cancel
|
|
||||||
button.pure-button.button-success(@click="create_new_folder",:disabled="!edited")
|
|
||||||
| Create
|
|
||||||
|
|
||||||
message(:show.sync="confirmDelete")
|
|
||||||
h3(slot="header") Delete Folder?
|
|
||||||
div(slot="body")
|
|
||||||
p Are you sure to delete the folder?
|
|
||||||
|
|
||||||
div(slot="footer")
|
|
||||||
button.pure-button(@click="confirmDelete=false") Cancel
|
|
||||||
button.pure-button.button-error(@click="delete_folder") Folder only
|
|
||||||
button.pure-button.button-success(@click="delete_folder_and_files") Folder and files
|
|
||||||
|
|
||||||
button.pure-button(title="Create a new folder.", @click="create_folder=true",
|
|
||||||
:disabled="!is_ready",style="height:100%")
|
|
||||||
| Create Folder
|
|
||||||
|
|
||||||
button.pure-button(title="Delete a folder.", @click="confirmDelete=true",
|
|
||||||
:disabled="!is_ready",style="height:100%;margin-left:5px")
|
|
||||||
| Delete Folder
|
|
||||||
|
|
||||||
select(title="Select previously uploaded GCode folder.",
|
|
||||||
v-model="state.folder", @change="reset_gcode", :disabled="!is_ready",
|
|
||||||
style="max-width:100%;margin-left:5px")
|
|
||||||
option( selected='' value='default') Default folder
|
|
||||||
option(v-for="file in gcode_folders", :value="file") {{file}}
|
|
||||||
|
|
||||||
select(title="Select previously uploaded GCode programs.",
|
|
||||||
v-model="state.selected", @change="load", :disabled="!is_ready",
|
|
||||||
style="max-width:300px;margin-left:5px")
|
|
||||||
option(v-for="file in gcode_files", :value="file") {{file}}
|
|
||||||
|
|
||||||
button.pure-button(@click="toggle_sorting", :disabled="!is_ready",
|
|
||||||
style="height:75%")
|
|
||||||
| {{files_sortby}}
|
|
||||||
|
|
||||||
.progress(v-if="toolpath_progress && toolpath_progress < 1",
|
|
||||||
title="Simulating GCode to check for errors, calculate ETA and " +
|
|
||||||
"generate 3D view. You can run GCode before the simulation " +
|
|
||||||
"finishes.")
|
|
||||||
div(:style="'width:' + (toolpath_progress || 0) * 100 + '%'")
|
|
||||||
label Simulating {{(toolpath_progress || 0) | percent}}
|
|
||||||
|
|
||||||
path-viewer(:toolpath="toolpath", :state="state", :config="config")
|
|
||||||
gcode-viewer
|
|
||||||
|
|
||||||
section#content2.tab-content
|
|
||||||
.mdi.pure-form(title="Manual GCode entry.")
|
|
||||||
button.pure-button(:disabled="!can_mdi",
|
|
||||||
:class="{'attention': is_holding}",
|
|
||||||
title="{{is_running ? 'Pause' : 'Start'}} command.",
|
|
||||||
@click="mdi_start_pause",style="height:100px;width:100px")
|
|
||||||
.fa(:class="is_running ? 'fa-pause' : 'fa-play'")
|
|
||||||
|
|
||||||
button.pure-button(title="Stop command.", @click="stop",style="height:100px;width:100px")
|
|
||||||
.fa.fa-stop
|
|
||||||
|
|
||||||
input(v-model="mdi", :disabled="!can_mdi", @keyup.enter="submit_mdi")
|
|
||||||
|
|
||||||
div
|
|
||||||
em The machine is currently operating in #[strong {{mach_units}}] units. Use G20/G21 to switch units.
|
|
||||||
|
|
||||||
.history(:class="{placeholder: !history}")
|
|
||||||
span(v-if="!history.length") MDI history displays here.
|
|
||||||
ul
|
|
||||||
li(v-for="item in history", @click="load_history($index)",
|
|
||||||
track-by="$index")
|
|
||||||
| {{item}}
|
|
||||||
|
|
||||||
section#content3.tab-content
|
|
||||||
console
|
|
||||||
|
|
||||||
section#content4.tab-content
|
|
||||||
indicators(:state="state", :template="template")
|
|
||||||
|
|
||||||
.override(title="Feed rate override.")
|
|
||||||
label Feed
|
label Feed
|
||||||
input(type="range", min="0", max="2", step="0.01",
|
input(type="range", min="0", max="2", step="0.01",
|
||||||
v-model="feed_override", @change="override_feed")
|
v-model="feed_override", @change="override_feed")
|
||||||
span.percent {{feed_override | percent 0}}
|
.od-val {{feed_override | percent 0}}
|
||||||
|
button.od-reset(@click="feed_override = 1; override_feed()") Reset 100%
|
||||||
.override(title="Spindle speed override.")
|
.od-row
|
||||||
label Speed
|
label Spindle
|
||||||
input(type="range", min="0", max="2", step="0.01",
|
input(type="range", min="0", max="2", step="0.01",
|
||||||
v-model="speed_override", @change="override_speed")
|
v-model="speed_override", @change="override_speed")
|
||||||
span.percent {{speed_override | percent 0}}
|
.od-val {{speed_override | percent 0}}
|
||||||
|
button.od-reset(@click="speed_override = 1; override_speed()") Reset 100%
|
||||||
|
|
||||||
|
|||||||
137
src/pug/templates/program-view.pug
Normal file
137
src/pug/templates/program-view.pug
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
script#program-view-template(type="text/x-template")
|
||||||
|
.program-page
|
||||||
|
|
||||||
|
// ----- Modal dialogs -----
|
||||||
|
message(:show.sync="showGcodeMessage")
|
||||||
|
h3(slot="header") Processing New File
|
||||||
|
div(slot="body")
|
||||||
|
h3 Please wait..
|
||||||
|
p Simulating GCode to check for errors, calculate ETA and generate 3D view.
|
||||||
|
div(slot="footer")
|
||||||
|
label Simulating {{(toolpath_progress || 0) | percent}}
|
||||||
|
|
||||||
|
message(:show.sync="GCodeNotFound")
|
||||||
|
h3(slot="header") File not found
|
||||||
|
div(slot="body")
|
||||||
|
p It seems like the file you selected cannot be found. Try uploading again.
|
||||||
|
div(slot="footer")
|
||||||
|
button.pure-button.button-error(@click="GCodeNotFound=false") OK
|
||||||
|
|
||||||
|
message(:show.sync="uploading_files")
|
||||||
|
h3(slot="header") Files uploading
|
||||||
|
div(slot="body")
|
||||||
|
h3 Please wait...
|
||||||
|
p
|
||||||
|
p The files are currently being uploaded.
|
||||||
|
p Do not close the window.
|
||||||
|
div(slot="footer")
|
||||||
|
|
||||||
|
message.error-message(:show.sync="deleteGCode")
|
||||||
|
h3(slot="header") Select files to delete:
|
||||||
|
div(slot="body")
|
||||||
|
input.search-bar(type="text", v-model="search_query", placeholder="Search Files...")
|
||||||
|
.container
|
||||||
|
.folders
|
||||||
|
h3 Folders
|
||||||
|
div(v-for="(index, folder) in state.gcode_list", :key="index",
|
||||||
|
@click="populateFiles(index)",
|
||||||
|
class="folder-item",
|
||||||
|
:class="{ selected: index === selected_folder_index }") {{ folder.name }}
|
||||||
|
.files
|
||||||
|
h3 Files
|
||||||
|
label.file-item(v-for="item in gcode_filtered_files", :key="item")
|
||||||
|
input(type="checkbox", :value="item", v-model="selected_items_to_delete")
|
||||||
|
| {{ item }}
|
||||||
|
div(slot="footer")
|
||||||
|
button.pure-button(@click="cancel_delete", style="height:50px") Cancel
|
||||||
|
button.pure-button.button-success(@click="delete_current", style="height:50px")
|
||||||
|
.fa.fa-trash
|
||||||
|
| Selected
|
||||||
|
|
||||||
|
message(:show.sync="create_folder")
|
||||||
|
h3(slot="header") Enter folder name:
|
||||||
|
div(slot="body")
|
||||||
|
input.input-name(type="text", minlength="1", maxlength="15",
|
||||||
|
style="margin-top:1rem;margin-bottom:2rem;",
|
||||||
|
id="folder-name", v-model="folder_name", @keypress="edited_folder_name")
|
||||||
|
div(slot="footer")
|
||||||
|
button.pure-button(@click="cancel_new_folder") Cancel
|
||||||
|
button.pure-button.button-success(@click="create_new_folder", :disabled="!edited") Create
|
||||||
|
|
||||||
|
message(:show.sync="confirmDelete")
|
||||||
|
h3(slot="header") Delete Folder?
|
||||||
|
div(slot="body")
|
||||||
|
p Are you sure to delete the folder?
|
||||||
|
div(slot="footer")
|
||||||
|
button.pure-button(@click="confirmDelete=false") Cancel
|
||||||
|
button.pure-button.button-error(@click="delete_folder") Folder only
|
||||||
|
button.pure-button.button-success(@click="delete_folder_and_files") Folder and files
|
||||||
|
|
||||||
|
.program-card
|
||||||
|
|
||||||
|
// Action bar (RUN / STOP / Upload / Download / Delete)
|
||||||
|
.action-bar
|
||||||
|
button.action-btn.run(:class="{'attention': is_holding}",
|
||||||
|
@click="start_pause", :disabled="!state.selected",
|
||||||
|
:title="is_running ? 'Pause program.' : 'Start program.'")
|
||||||
|
.fa.fa-play.ico(v-if="!is_running")
|
||||||
|
.fa.fa-pause.ico(v-else)
|
||||||
|
span {{is_running ? 'PAUSE' : 'RUN'}}
|
||||||
|
button.action-btn.stop(@click="stop", title="Stop program.")
|
||||||
|
.fa.fa-stop.ico
|
||||||
|
span STOP
|
||||||
|
button.action-btn(@click="open_folder", :disabled="!is_ready",
|
||||||
|
title="Upload a new GCode folder.")
|
||||||
|
.fa.fa-folder-arrow-up.ico
|
||||||
|
span UPLOAD FOLDER
|
||||||
|
form.gcode-folder-input.file-upload
|
||||||
|
input#folderInput(type="file", @change="upload_folder",
|
||||||
|
:disabled="!is_ready", webkitdirectory, directory)
|
||||||
|
button.action-btn(@click="open_file", :disabled="!is_ready",
|
||||||
|
title="Upload a new GCode program.")
|
||||||
|
.fa.fa-file-arrow-up.ico
|
||||||
|
span UPLOAD FILE
|
||||||
|
form.gcode-file-input.file-upload
|
||||||
|
input(type="file", @change="upload_file", :disabled="!is_ready",
|
||||||
|
accept=".nc,.ngc,.gcode,.gc", multiple)
|
||||||
|
a(:href="state.selected ? '/api/file/' + state.selected : '#'",
|
||||||
|
download, :class="{disabled: !state.selected}",
|
||||||
|
title="Download the selected GCode program.")
|
||||||
|
button.action-btn(:disabled="!state.selected")
|
||||||
|
.fa.fa-file-arrow-down.ico
|
||||||
|
span DOWNLOAD FILE
|
||||||
|
button.action-btn.danger(@click="deleteGCode = true",
|
||||||
|
:disabled="!state.selected || !is_ready",
|
||||||
|
title="Delete current GCode program.")
|
||||||
|
.fa.fa-trash.ico
|
||||||
|
span DELETE
|
||||||
|
|
||||||
|
// File / folder selectors
|
||||||
|
.file-bar
|
||||||
|
button.file-btn(@click="create_folder=true", :disabled="!is_ready")
|
||||||
|
.fa.fa-folder-plus
|
||||||
|
| Create Folder
|
||||||
|
button.file-btn(@click="confirmDelete=true", :disabled="!is_ready")
|
||||||
|
.fa.fa-folder-minus
|
||||||
|
| Delete Folder
|
||||||
|
select.file-select(title="Select previously uploaded GCode folder.",
|
||||||
|
v-model="state.folder", @change="reset_gcode", :disabled="!is_ready")
|
||||||
|
option(selected, value="default") Default folder
|
||||||
|
option(v-for="file in gcode_folders", :value="file") {{file}}
|
||||||
|
select.file-select.primary(title="Select previously uploaded GCode programs.",
|
||||||
|
v-model="state.selected", @change="load", :disabled="!is_ready")
|
||||||
|
option(value="") (no file)
|
||||||
|
option(v-for="file in gcode_files", :value="file") {{file}}
|
||||||
|
button.file-btn(@click="toggle_sorting", :disabled="!is_ready")
|
||||||
|
.fa.fa-arrow-down-wide-short
|
||||||
|
| {{files_sortby}}
|
||||||
|
|
||||||
|
// Body: gcode listing on the left, 3D viewer on the right
|
||||||
|
.program-body
|
||||||
|
gcode-viewer
|
||||||
|
path-viewer(: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.")
|
||||||
|
div(:style="'width:' + (toolpath_progress || 0) * 100 + '%'")
|
||||||
|
label Simulating {{(toolpath_progress || 0) | percent}}
|
||||||
44
src/pug/templates/settings-shell.pug
Normal file
44
src/pug/templates/settings-shell.pug
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
script#settings-shell-view-template(type="text/x-template")
|
||||||
|
.settings-shell
|
||||||
|
aside.settings-rail
|
||||||
|
// Use a single v-for over a data-driven items array so every
|
||||||
|
// rail item shares the same compiled :class binding template.
|
||||||
|
// This sidesteps a Vue 1 quirk where sibling-with-different-
|
||||||
|
// expression :class bindings sometimes fail to re-evaluate on
|
||||||
|
// hash navigation, leaving stale `.active` classes.
|
||||||
|
template(v-for="item in rail_items")
|
||||||
|
.set-section(v-if="item.section") {{item.section}}
|
||||||
|
a.set-item(v-if="!item.section", :class="{active: is_active(item)}",
|
||||||
|
:href="item.href")
|
||||||
|
.fa(:class="item.icon")
|
||||||
|
| {{item.label}}
|
||||||
|
.set-rail-foot
|
||||||
|
button.sp-shutdown(@click="showShutdownDialog")
|
||||||
|
.fa.fa-power-off
|
||||||
|
| Shutdown
|
||||||
|
button.sp-save(:disabled="!$root.modified", @click="$root.save()")
|
||||||
|
.fa.fa-save
|
||||||
|
| Save{{$root.modified ? '*' : ''}}
|
||||||
|
|
||||||
|
.settings-content
|
||||||
|
// 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'",
|
||||||
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
|
admin-general-view(v-if="sub === 'admin-general'",
|
||||||
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
|
admin-network-view(v-if="sub === 'admin-network'",
|
||||||
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
|
motor-view(v-if="sub === 'motor'",
|
||||||
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
|
tool-view(v-if="sub === 'tool'",
|
||||||
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
|
io-view(v-if="sub === 'io'",
|
||||||
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
|
macros-view(v-if="sub === 'macros'",
|
||||||
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
|
help-view(v-if="sub === 'help'",
|
||||||
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
|
cheat-sheet-view(v-if="sub === 'cheat-sheet'",
|
||||||
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user