diff --git a/src/js/app.js b/src/js/app.js
index ceeb9ef..c8cd5ce 100644
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -103,12 +103,23 @@ module.exports = new Vue({
return {
status: "connecting",
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",
index: -1,
modified: false,
template: require("../resources/config-template.json"),
config: {
- settings: {
+ settings: {
units: "METRIC",
"easy-adapter": false
},
@@ -143,22 +154,15 @@ module.exports = new Vue({
estop: { template: "#estop-template" },
"loading-view": { template: "
Loading...
" },
"control-view": require("./control-view"),
- "settings-view": require("./settings-view"),
- "motor-view": require("./motor-view"),
- "tool-view": require("./tool-view"),
- "io-view": require("./io-view"),
- "admin-general-view": require("./admin-general-view"),
- "admin-network-view": require("./admin-network-view"),
- "macros-view": require('./macros'),
- "help-view": require("./help-view"),
- "cheat-sheet-view": {
- template: "#cheat-sheet-view-template",
- data: function() {
- return {
- showUnimplemented: false
- };
- },
- },
+ "program-view": require("./program-view"),
+ "console-view": require("./console-view"),
+
+ // The settings-shell renders the rail + an inner routed view.
+ // All settings-family hashes (settings, admin-general,
+ // admin-network, motor:N, tool, io, macros, help, cheat-sheet)
+ // resolve to this same shell; parse_hash() sets sub_tab so the
+ // shell knows which inner template to mount.
+ "settings-shell-view": require("./settings-shell-view"),
},
watch: {
@@ -166,6 +170,25 @@ module.exports = new Vue({
localStorage.setItem("display_units", 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: {
@@ -252,18 +275,106 @@ module.exports = new Vue({
enable_rotary: function() {
if(this.state["2an"] == 1 || this.state["2an"] == 3) return true;
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() {
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();
+ // Close the system popover when clicking anywhere else.
+ document.addEventListener("click", () => {
+ if (this.sys_open) this.sys_open = false;
+ });
+
SvelteComponents.registerControllerMethods({
dispatch: (...args) => this.$dispatch(...args)
});
},
+
+
methods: {
block_error_dialog: function() {
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() {
const hash = location.hash.substr(1);
@@ -430,12 +556,56 @@ module.exports = new Vue({
}
const parts = hash.split(":");
+ const head = parts[0];
- if (parts.length == 2) {
- this.index = parts[1];
+ this.index = parts.length > 1 ? parts[1] : -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() {
diff --git a/src/js/console-view.js b/src/js/console-view.js
new file mode 100644
index 0000000..d2cd677
--- /dev/null
+++ b/src/js/console-view.js
@@ -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];
+ },
+ },
+};
diff --git a/src/js/control-view.js b/src/js/control-view.js
index a9c23f6..7fa1134 100644
--- a/src/js/control-view.js
+++ b/src/js/control-view.js
@@ -1,7 +1,6 @@
"use strict";
const api = require("./api");
-const utils = require("./utils");
const cookie = require("./cookie")("bbctrl-");
module.exports = {
@@ -12,15 +11,7 @@ module.exports = {
return {
current_time: "",
mach_units: this.$root.state.metric ? "METRIC" : "IMPERIAL",
- mdi: "",
- last_file: undefined,
- last_file_time: undefined,
- toolpath: {},
- toolpath_progress: 0,
axes: "xyzabc",
- history: [],
- speed_override: 1,
- feed_override: 1,
jog_incr_amounts: {
METRIC: {
fine: 0.1,
@@ -38,34 +29,14 @@ module.exports = {
jog_incr: localStorage.getItem("jog_incr") || "small",
jog_step: cookie.get_bool("jog-step"),
jog_adjust: parseInt(cookie.get("jog-adjust", 2)),
- deleteGCode: false,
- tab: "auto",
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,
- filesUploaded: 0,
- totalFiles: 0,
- files_sortby: "By Upload Date",
- selected_items_to_delete: [],
- search_query: "",
- filtered_files: [],
- selected_folder_index: null,
+ overrides_open: false,
};
},
components: {
"axis-control": require("./axis-control"),
- "path-viewer": require("./path-viewer"),
- "gcode-viewer": require("./gcode-viewer"),
},
watch: {
@@ -80,16 +51,6 @@ module.exports = {
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 () {
cookie.set_bool("jog-step", this.jog_step);
},
@@ -127,43 +88,16 @@ module.exports = {
return state || "";
},
- pause_reason: function () {
- return this.state.pr;
- },
-
- 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";
+ can_set_axis: function () {
+ return this.state.cycle == "idle";
},
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";
- },
-
- can_set_axis: function () {
- return this.is_idle;
-
- // TODO allow setting axis position during pause
- // return this.is_idle || this.is_paused;
+ is_ready: function () {
+ return this.mach_state == "READY";
},
message: function () {
@@ -191,57 +125,21 @@ module.exports = {
},
plan_time_remaining: function () {
- if (!(this.is_stopping || this.is_running || this.is_holding)) {
- return 0;
- }
-
- return this.toolpath.time - this.plan_time;
+ const stopping = this.mach_state == "STOPPING";
+ const running = this.mach_state == "RUNNING" || this.mach_state == "HOMING";
+ const holding = this.mach_state == "HOLDING";
+ if (!(stopping || running || holding)) return 0;
+ const tp = this.$root && this.$root.toolpath ? this.$root.toolpath.time : 0;
+ return (tp || 0) - 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();
- } 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();
+ state_kpi_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 "";
},
},
@@ -264,14 +162,9 @@ module.exports = {
M72
`);
},
- folder_name_edited: function () {
- this.edited = true;
- },
},
ready: function () {
- this.load();
-
setInterval(() => {
this.current_time = new Date().toLocaleTimeString();
}, 1000);
@@ -287,25 +180,9 @@ module.exports = {
},
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) {
const weight = `font-weight:${this.jog_incr === value ? "bold" : "normal"}`;
const color = this.jog_incr === value ? "color:#0078e7" : "";
-
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) {
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) {
- const amount =
- this.jog_incr_amounts[this.display_units][this.jog_incr];
+ const amount = this.jog_incr_amounts[this.display_units][this.jog_incr];
const delta_mm = sign * (this.metric ? amount : amount * 25.4);
this.aux_jog(delta_mm);
},
@@ -811,93 +265,20 @@ module.exports = {
},
zero: function (axis) {
- if (typeof axis == "undefined") {
- this.zero_all();
- } 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));
+ if (typeof axis == "undefined") this.zero_all();
+ else this.set_position(axis, 0);
},
showProbeDialog: function (probeType) {
- if(this.show_probe_dialog){
+ if (this.show_probe_dialog) {
this.show_probe_dialog = false;
}
- SvelteComponents.showDialog("Probe", { probeType, isRotaryActive: this.state["2an"] == 3 });
- },
- 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);
- }
- }
+ SvelteComponents.showDialog("Probe", {
+ probeType,
+ isRotaryActive: this.state["2an"] == 3,
+ });
},
},
- mixins: [require("./axis-vars")],
+ mixins: [require("./program-mixin"), require("./axis-vars")],
};
diff --git a/src/js/main.js b/src/js/main.js
index f58ad13..06ebc9f 100644
--- a/src/js/main.js
+++ b/src/js/main.js
@@ -44,6 +44,16 @@ window.onload = function() {
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
Vue.component("templated-input", require("./templated-input"));
Vue.component("message", require("./message"));
diff --git a/src/js/program-mixin.js b/src/js/program-mixin.js
new file mode 100644
index 0000000..b3b5ae4
--- /dev/null
+++ b/src/js/program-mixin.js
@@ -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);
+ }
+ }
+ },
+ },
+};
diff --git a/src/js/program-view.js b/src/js/program-view.js
new file mode 100644
index 0000000..5f25627
--- /dev/null
+++ b/src/js/program-view.js
@@ -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")],
+};
diff --git a/src/js/settings-shell-view.js b/src/js/settings-shell-view.js
new file mode 100644
index 0000000..cf88802
--- /dev/null
+++ b/src/js/settings-shell-view.js
@@ -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
+// ``). 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
+ // 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");
+ },
+ },
+};
diff --git a/src/pug/index.pug b/src/pug/index.pug
index 79f1416..b9a65b5 100644
--- a/src/pug/index.pug
+++ b/src/pug/index.pug
@@ -8,7 +8,6 @@ html(lang="en")
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/Audiowide.css
@@ -23,99 +22,113 @@ html(lang="en")
#overlay(v-if="status != 'connected'")
span {{status}}
-
- #layout
- a#menuLink.menu-link(href="#menu"): span
- #menu
- button.save.pure-button.button-success(:disabled="!modified",
- @click="save") Save
+ .app-shell
+ header.app-head
+ .brand-blk
+ .brand-logo
+ .brand-name ONEFINITY
- .pure-menu
- ul.pure-menu-list
- li.pure-menu-heading
- a.pure-menu-link(href="#control") Control
+ nav.tabs-host(role="tablist")
+ a.ktab(:class="{active: top_tab === 'control'}", href="#control",
+ 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
- li.pure-menu-heading
- a.pure-menu-link(href="#macros") Macros
+ .head-spacer
- li.pure-menu-heading
- a.pure-menu-link(href="#settings") Settings
+ .sys-btn(@click.stop="toggle_sys_popover", :class="{open: sys_open}")
+ span.pip(:class="sys_class")
+ span.sys-text {{sys_summary}}
+ .fa.fa-chevron-down
- li.pure-menu-heading
- a.pure-menu-link(href="#motor:0") Motors
+ .pi-temp-warning(v-if="80 <= state.rpi_temp",
+ title="Raspberry Pi temperature too high.")
+ .fa.fa-thermometer-full
- li.pure-menu-item(v-for="motor in config.motors")
- a.pure-menu-link(:href="'#motor:' + $index") Motor {{$index}}
+ span.state-badge(:class="state_class", :title="mach_state_full")
+ span.dot
+ span {{state_label}}
- li.pure-menu-heading
- a.pure-menu-link(href="#tool") Tool
+ .estop(:class="{active: state.es}")
+ estop(@click="estop")
- 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.")
-
- .easy-adapter(v-if="is_easy_adapter_active")
- .round-dot
- div.easy-adapter-text Easy Adapter
-
- .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")
+ // 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")
+ 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 ? '*' : ''}}
- .estop(:class="{active: state.es}")
- estop(@click="estop")
-
- .content(class="{{currentView}}-view")
- component(:is="currentView + '-view'", :index="index",
- :config="config", :template="template", :state="state", keep-alive)
+ // 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",
+ :config="config", :template="template", :state="state",
+ :sub-tab="sub_tab")
message.error-message(:show.sync="errorShow")
div(slot="header")
diff --git a/src/pug/templates/console-view.pug b/src/pug/templates/console-view.pug
new file mode 100644
index 0000000..1bd6aa4
--- /dev/null
+++ b/src/pug/templates/console-view.pug
@@ -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")
diff --git a/src/pug/templates/control-view.pug b/src/pug/templates/control-view.pug
index 4393835..f88d0d5 100644
--- a/src/pug/templates/control-view.pug
+++ b/src/pug/templates/control-view.pug
@@ -1,43 +1,39 @@
script#control-view-template(type="text/x-template")
- #control
+ .control-page
+ // ----- Modal dialogs (kept verbatim from legacy) -----
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="showNoGcodeMessage")
- h3(slot="header") GCode Not Set
- div(slot="body")
- p Configure the GCode for the selected macro to use it
-
- div(slot="footer")
- button.pure-button(@click="showNoGcodeMessage=false") OK
+ h3(slot="header") GCode Not Set
+ div(slot="body")
+ p Configure the GCode for the selected macro to use it
+ div(slot="footer")
+ button.pure-button(@click="showNoGcodeMessage=false") OK
message(:show.sync="macrosLoading")
- h3(slot="header") Run Macro?
- div(slot="body")
- p
- | The macro file
- strong {{state.selected}}
- | is being loaded.
-
- div(slot="footer")
- button.pure-button(@click="macrosLoading=false") Cancel
- button.pure-button.pure-button-primary(@click="start_pause") Run
-
+ h3(slot="header") Run Macro?
+ div(slot="body")
+ p
+ | The macro file
+ strong {{state.selected}}
+ | is being loaded.
+ div(slot="footer")
+ button.pure-button(@click="macrosLoading=false") Cancel
+ button.pure-button.pure-button-primary(@click="start_pause") Run
+
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.
+ 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
-
+ button.pure-button.button-error(@click="GCodeNotFound=false") OK
+
message(:show.sync="show_probe_dialog")
h3(slot="header") Probe Rotary
div(slot="body")
@@ -46,485 +42,232 @@ script#control-view-template(type="text/x-template")
div(slot="footer")
button.pure-button(@click="show_probe_dialog=false") Cancel
+ // ----- Main grid: jog | (DRO + status strip) -----
+ .control-grid
- table(style="table-layout: fixed; width: 100%;")
- tr(style="height: fit-content;")
- td(style="white-space: nowrap; width: 410px;", rowspan="2")
- table.control-buttons(table-layout="fixed")
- colgroup
- col(style="width:100px")
- col(style="width:100px")
- col(style="width:100px")
- col(style="width:100px")
- 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(@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'}}]
+ // ===== JOG =====
+ .jog-card
+ .jog-head
+ .jog-title
+ | Jog
+ span.step-pre · step
+ span.step {{jog_incr_amounts[display_units][jog_incr]}}#[span.unit {{metric ? 'mm' : 'in'}}]
+ .step-seg
+ button(:class="{active: jog_incr === 'fine'}", @click="jog_incr = 'fine'")
+ | {{jog_incr_amounts[display_units].fine}}
+ button(:class="{active: jog_incr === 'small'}", @click="jog_incr = 'small'")
+ | {{jog_incr_amounts[display_units].small}}
+ button(:class="{active: jog_incr === 'medium'}", @click="jog_incr = 'medium'")
+ | {{jog_incr_amounts[display_units].medium}}
+ button(:class="{active: jog_incr === 'large'}", @click="jog_incr = 'large'")
+ | {{jog_incr_amounts[display_units].large}}
- // W axis jog row (auxcnc). Only shown when the aux controller
- // is enabled in aux.json. We treat home == 0 for the W axis,
- // so there is no separate "set zero" / "W origin" button -
- // just W-, W+, and Home.
- tr(v-if="w.enabled")
- td(style="height:100px", align="center", colspan="1")
- button(@click="aux_jog_incr(-1)",
- :disabled="!w.enabled",
- style="display:grid;justify-content:center;align-items:center;padding:14px;")
- | W-
- .fa.fa-arrow-down
+ .jog-grid
+ // Row 1
+ button.jbtn.dir(@click="jog_fn(-1, 1, 0, 0)", title="X- Y+")
+ .fa.fa-arrow-up.ico(style="transform: rotate(-45deg)")
+ button.jbtn(@click="jog_fn(0, 1, 0, 0)") Y+
+ button.jbtn.dir(@click="jog_fn(1, 1, 0, 0)", title="X+ Y+")
+ .fa.fa-arrow-up.ico(style="transform: rotate(45deg)")
+ button.jbtn(@click="jog_fn(0, 0, 1, 0)") Z+
- td(style="height:100px", align="center", colspan="1")
- button(@click="aux_jog_incr(+1)",
- :disabled="!w.enabled",
- style="display:grid;justify-content:center;align-items:center;padding:14px;")
- | W+
- .fa.fa-arrow-up
+ // Row 2
+ button.jbtn(@click="jog_fn(-1, 0, 0, 0)") X−
+ button.jbtn.ghost(@click="showMoveToZeroDialog('xy')")
+ span.lbl XY
+ span Origin
+ 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")
- button(@click="aux_home()", :disabled="!w.enabled",
- style="height:100px;width:200px")
- | Home
- br
- | W
+ // Row 3
+ button.jbtn.dir(@click="jog_fn(-1, -1, 0, 0)", title="X- Y-")
+ .fa.fa-arrow-down.ico(style="transform: rotate(45deg)")
+ button.jbtn(@click="jog_fn(0, -1, 0, 0)") Y−
+ button.jbtn.dir(@click="jog_fn(1, -1, 0, 0)", title="X+ Y-")
+ .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")
- td(style="height:100px", align="center", colspan="1")
- button(@click="show_probe_dialog=true")
- | Probe
- br
- | Rotary
+ // Row 4 — auxiliary axis (W or A) or probe shortcuts
+ template(v-if="w.enabled")
+ button.jbtn(@click="aux_jog_incr(-1)", :disabled="!w.enabled")
+ .fa.fa-arrow-down.ico
+ span.lbl W−
+ button.jbtn.ghost(@click="aux_home()", :disabled="!w.enabled")
+ span.lbl Home
+ span W
+ button.jbtn(@click="aux_jog_incr(+1)", :disabled="!w.enabled")
+ .fa.fa-arrow-up.ico
+ span.lbl W+
+ button.jbtn(@click="show_probe_dialog=true",
+ :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")
- button(@click="jog_fn(0,0,0,-1)", style="display: grid;justify-content: center;align-items: center;padding: 14px;")
- | A-
- .fa.fa-rotate-left
-
- td(style="height:100px", align="center", colspan="1")
- button(@click="showMoveToZeroDialog('a')")
- | A
- br
- | Origin
+ // ===== DRO + status strip =====
+ .right-col
- td(style="height:100px", align="center", colspan="1")
- button(@click="jog_fn(0,0,0,1)", style="display: grid;justify-content: center;align-items: center;padding: 14px;")
- | A+
- .fa.fa-rotate-right
+ .dro-card
+ .dro-head
+ div Axis
+ div Position
+ div Absolute
+ div Offset
+ div State
+ div Toolpath
+ div(style="text-align:right") Actions
- 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
-
- each axis in 'xyzabc'
- tr.axis(:class=`${axis}.klass`, v-if=`${axis}.enabled`,
- :title=`${axis}.title`)
- th.name= axis
- td.position: unit-value(:value=`${axis}.pos`, precision=4)
- td.absolute: unit-value(:value=`${axis}.abs`, precision=3)
- td.offset: unit-value(:value=`${axis}.off`, precision=3)
- td.state
+ // Per-axis rows — keep unit-value + bindings from axis-vars
+ each axis in 'xyzabc'
+ .dro-row(:class=`${axis}.klass + ' ' + ${axis}.tklass`,
+ v-if=`${axis}.enabled`,
+ :title=`${axis}.title`)
+ .dro-axis(:class=`'axis-' + '${axis}'`)= axis.toUpperCase()
+ .dro-pos: unit-value(:value=`${axis}.pos`, precision=4)
+ .dro-sec: unit-value(:value=`${axis}.abs`, precision=3)
+ .dro-sec: unit-value(:value=`${axis}.off`, precision=3)
+ .dro-state
+ span.chip(:class=`${axis}.tklass.indexOf('error') !== -1 ? 'chip-red' : (${axis}.homed ? 'chip-green' : 'chip-amber')`)
.fa(:class=`'fa-' + ${axis}.icon`)
- | {{#{axis}.state}}
- td.tstate(:class=`${axis}.tklass`, :title=`${axis}.toolmsg`, @click=`showToolpathMessageDialog('${axis}')`)
+ | {{#{axis}.state}}
+ .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`)
- | {{#{axis}.tstate}}
-
- th.actions
- button.pure-button(:disabled="!can_set_axis",
- title=`Set {{'${axis}' | upper}} axis position.`,
- @click=`show_set_position('${axis}')`, style="height:60px;width:60px")
- .fa.fa-cog
-
- button.pure-button(:disabled="!can_set_axis",
- title=`Zero {{'${axis}' | upper}} axis offset.`,
- @click=`zero('${axis}')`, style="height:60px;width:60px")
- .fa.fa-map-marker
-
- button.pure-button(:disabled="!is_idle", @click=`home('${axis}')`,
- title=`Home {{'${axis}' | upper}} axis.`, style="height:60px;width:60px")
- .fa.fa-home
-
- // Auxiliary W axis (auxcnc ESP32 over /dev/ttyUSB0)
- tr.axis(:class="w.klass", v-if="w.enabled", :title="w.title")
- th.name w
- td.position: unit-value(:value="w.pos", precision=4)
- td.absolute: unit-value(:value="w.abs", precision=3)
- td.offset —
- td.state
- .fa(:class="'fa-' + w.icon")
- | {{w.state}}
- td.tstate(:class="w.tklass", :title="w.toolmsg")
- .fa(:class="'fa-' + w.ticon")
- | {{w.tstate}}
-
- th.actions
- // 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")
+ | {{#{axis}.tstate}}
+ .actions-cell
+ button.icon-btn(:disabled="!can_set_axis",
+ :title=`'Set ${axis.toUpperCase()} axis position.'`,
+ @click=`show_set_position('${axis}')`)
.fa.fa-cog
-
- button.pure-button(disabled,
- style="height:60px;width:60px;visibility:hidden")
+ button.icon-btn(:disabled="!can_set_axis",
+ :title=`'Zero ${axis.toUpperCase()} axis offset.'`,
+ @click=`zero('${axis}')`)
.fa.fa-map-marker
-
- button.pure-button(:disabled="!w.enabled", @click="aux_home()",
- title="Home W axis.", style="height:60px;width:60px")
+ button.icon-btn(:disabled="!is_idle",
+ :title=`'Home ${axis.toUpperCase()} axis.'`,
+ @click=`home('${axis}')`)
.fa.fa-home
-
- tr(style="vertical-align: top;")
- td
- table(width="100%")
- tr
- td(style="text-align:center")
- table.info
- tr
- th State
- td(:class="{attention: highlight_state}") {{mach_state}}
- tr
- th Message
- td.message(:class="{attention: highlight_state}")
- | {{message.replace(/^#/, '')}}
+ // W axis (auxiliary) — no offset, no set-zero / no set-position
+ .dro-row(:class="w.klass + ' ' + w.tklass", v-if="w.enabled",
+ :title="w.title")
+ .dro-axis.axis-w W
+ .dro-pos: unit-value(:value="w.pos", precision=4)
+ .dro-sec: unit-value(:value="w.abs", precision=3)
+ .dro-sec —
+ .dro-state
+ span.chip(:class="w.homed ? 'chip-green' : 'chip-amber'")
+ .fa(:class="'fa-' + w.icon")
+ | {{w.state}}
+ .dro-toolpath
+ span.chip.chip-green
+ .fa(:class="'fa-' + w.ticon")
+ | {{w.tstate}}
+ .actions-cell
+ button.icon-btn(disabled, style="visibility:hidden")
+ .fa.fa-cog
+ button.icon-btn(disabled, style="visibility:hidden")
+ .fa.fa-map-marker
+ button.icon-btn(:disabled="!w.enabled",
+ title="Home W axis.", @click="aux_home()")
+ .fa.fa-home
- tr
- th Display Units
- td.units
- select(v-model="display_units")
- option(value="METRIC") METRIC
- option(value="IMPERIAL") IMPERIAL
+ // ----- Status strip -----
+ .status-strip
+ .stat-card
+ .stat-label State
+ .stat-val(:class="state_kpi_class") {{mach_state || '--'}}
+ .stat-sub(v-if="message") {{message.replace(/^#/, '')}}
+ .stat-sub(v-else) No alerts
- tr(title="Active tool")
- th Tool
- td {{state.tool || 0}}
+ .stat-card
+ .stat-label Velocity / Feed
+ .stat-val
+ unit-value(:value="state.v", precision="2", unit="", iunit="",
+ scale="0.0254")
+ | ·
+ unit-value(:value="state.feed", precision="0", unit="", iunit="")
+ .stat-sub {{metric ? 'm/min · mm/min' : 'IPM · IPM'}}
- 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="",
- scale="0.0254")
- | {{metric ? ' m/min' : ' IPM'}}
+ .stat-card.stat-tappable(@click="overrides_open = !overrides_open",
+ :class="{open: overrides_open}", title="Tap to adjust feed/spindle override")
+ .stat-label Spindle
+ .stat-val
+ | {{(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="Programmed feed rate.")
- th Feed
- td
- unit-value(:value="state.feed", precision="2", unit="", iunit="")
- | {{metric ? ' mm/min' : ' IPM'}}
+ .stat-card
+ .stat-label Job
+ .stat-val
+ | {{0 <= state.line ? state.line : 0 | number}}
+ span(v-if="toolpath.lines")
+ | / {{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(title="Programed and actual speed.")
- th Speed
- td
- | {{state.speed || 0 | fixed 0}}
- span(v-if="!isNaN(state.s)") ({{state.s | fixed 0}})
- = ' RPM'
+ // ----- Macro row (slice 0..7); full list lives in Settings → Macros -----
+ .macro-row(v-if="state.macros && state.macros.length")
+ button.macro-btn(v-for="(index, macros) in state.macros.slice(0, 8)",
+ title="Click to run macro",
+ @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))}}
- 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}}
- span(v-if="toolpath.lines")
- | of {{toolpath.lines | number}}
-
- tr
- th Progress
- td.progress
- label {{(progress || 0) | percent}}
- .bar(:style="'width:' + (progress || 0) * 100 + '%'")
-
- .macros-div(class="present")
- button.macros-button(title="Click to run Macros",v-for="(index,macros) in state.macros",
- @click="run_macro(index)",:disabled="!is_ready",v-bind:style="{ backgroundColor: macros.color }") {{macros.name}}
-
- .tabs
-
- input#tab1(type="radio", name="tabs",checked="" @click="tab = 'auto'")
- label(for="tab1", title="Run GCode programs",style="height:50px;width:100px") Auto
-
- 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
- input(type="range", min="0", max="2", step="0.01",
- v-model="feed_override", @change="override_feed")
- span.percent {{feed_override | percent 0}}
-
- .override(title="Spindle speed override.")
- label Speed
- input(type="range", min="0", max="2", step="0.01",
- v-model="speed_override", @change="override_speed")
- span.percent {{speed_override | percent 0}}
-
-
+ // ----- Override drawer (anchored to bottom; toggled by Spindle KPI tile) -----
+ .override-drawer(:class="{open: overrides_open}")
+ .od-head
+ .od-title
+ .fa.fa-sliders
+ | Overrides
+ button.od-close(@click="overrides_open = false") ✕
+ .od-body
+ .od-row
+ label Feed
+ input(type="range", min="0", max="2", step="0.01",
+ v-model="feed_override", @change="override_feed")
+ .od-val {{feed_override | percent 0}}
+ button.od-reset(@click="feed_override = 1; override_feed()") Reset 100%
+ .od-row
+ label Spindle
+ input(type="range", min="0", max="2", step="0.01",
+ v-model="speed_override", @change="override_speed")
+ .od-val {{speed_override | percent 0}}
+ button.od-reset(@click="speed_override = 1; override_speed()") Reset 100%
diff --git a/src/pug/templates/program-view.pug b/src/pug/templates/program-view.pug
new file mode 100644
index 0000000..bc4d4c0
--- /dev/null
+++ b/src/pug/templates/program-view.pug
@@ -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}}
diff --git a/src/pug/templates/settings-shell.pug b/src/pug/templates/settings-shell.pug
new file mode 100644
index 0000000..3c605b1
--- /dev/null
+++ b/src/pug/templates/settings-shell.pug
@@ -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 `` 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")
diff --git a/src/stylus/style.styl b/src/stylus/style.styl
index f64568a..007c7da 100644
--- a/src/stylus/style.styl
+++ b/src/stylus/style.styl
@@ -1,12 +1,44 @@
+// =====================================================================
+// V09 redesign tokens & chrome
+// =====================================================================
+//
+// The new shell wraps everything in `.app-shell { .app-head | .app-body }`.
+// Inner views use the shared tokens below for jog/macro tiles, status
+// chips and segmented controls. Anything legacy that's still needed by
+// settings/admin/motor/tool/io templates remains lower in this file.
+
+$ink = #0f172a
+$ink-soft = #334155
+$muted = #64748b
+$muted-2 = #94a3b8
+$line = #e5e7eb
+$line-soft = #f1f5f9
+$bg = #ffffff
+$body-bg = #f1f5f9
+
+$accent = #fde047
+$accent-ink = $ink
+$accent-text = #0ea5e9
+
+// Jog tile palette — V09 (flat soft slate, no shadow)
+$jog-bg = #3f4b63
+$jog-hover = #4a5777
+$jog-dir = #5b6885
+$jog-dir-hov = #6a779a
+$jog-ghost = #8c97ad
+$jog-ghost-hov = #9ba6bb
+$jog-ink = #fff
+$jog-ghost-ink = $ink
+
body
- overflow-y scroll
+ margin 0
+ font-family 'Inter', system-ui, -apple-system, sans-serif
+ background $body-bg
+ color $ink
[v-cloak]
display none
-.menu-link
- z-index unset
-
tt
color #000
background #eee
@@ -32,168 +64,340 @@ tt
background-color #0078e7
color #fff
-
-.header, .content
- padding 0
-
.clear
clear left
clear right
-.header
- height 140px
- padding 0
+// =====================================================================
+// App shell
+// =====================================================================
+.app-shell
+ display flex
+ flex-direction column
+ min-height 100vh
+ background $body-bg
-.nav-header
- padding-left 60px
- display flex
+.app-body
+ flex 1
+ min-height 0
+ display flex
+ flex-direction column
+ padding 18px
- .brand
+ > *
+ flex 1
+ min-height 0
+
+.app-head
+ flex 0 0 96px
+ height 96px
+ display flex
+ align-items center
+ gap 18px
+ padding 0 24px
+ background $bg
+ border-bottom 1px solid $line
+ position relative
+ z-index 30
+
+ .brand-blk
display flex
- flex-direction row
- align-self center
- white-space nowrap
+ align-items center
+ gap 14px
+
+ .brand-logo
+ width 42px
+ height 42px
+ border-radius 8px
+ background repeating-linear-gradient(135deg, #a7c7a3 0 6px, transparent 6px 14px)
+
+ .brand-name
+ font-weight 900
+ font-size 22px
+ letter-spacing -0.01em
+
+ .head-spacer
+ flex 1
+
+// Underline-ribbon tabs
+.tabs-host
+ display inline-flex
+ gap 0
+ margin-right auto
+ padding-left 18px
+ align-items stretch
+ height 96px
+
+.ktab
+ position relative
+ height 96px
+ padding 0 26px
+ display inline-flex
+ align-items center
+ gap 0.55rem
+ background transparent
+ border none
+ text-decoration none
+ color $ink-soft
+ font-size 1.05rem
+ font-weight 700
+ cursor pointer
+ transition color .15s
+
+ .fa
+ font-size 1.1rem
+ color $muted-2
+ transition color .15s
+
+ &:hover
+ color $ink
+ .fa
+ color $ink-soft
+
+ &.active
+ color $ink
+ .fa
+ color $ink
+
+ &.active::after
+ content ""
+ position absolute
+ left 14px
+ right 14px
+ bottom 0
+ height 5px
+ background $accent
+ border-radius 5px 5px 0 0
+
+ .ktab-badge
+ background #fee2e2
+ color #991b1b
+ font-size 0.7rem
+ padding 3px 8px
+ border-radius 9999px
+ font-weight 800
+ line-height 1
+
+ &.active .ktab-badge
+ background $accent
+ color $accent-ink
+
+// System pill (collapses old chip-soup)
+.sys-btn
+ display inline-flex
+ align-items center
+ gap 0.55rem
+ height 54px
+ padding 0 1.1rem
+ border-radius 14px
+ background $line-soft
+ border 1px solid $line
+ color $ink
+ font-size 0.9rem
+ font-weight 600
+ cursor pointer
+ user-select none
+
+ &:hover
+ background #e2e8f0
+
+ .pip
+ width 9px
+ height 9px
+ border-radius 9999px
+ background #22c55e
+
+ .pip.amber
+ background #f59e0b
+
+ .pip.red
+ background #dc2626
+
+ .fa-chevron-down
+ color $muted-2
+ font-size 12px
+
+ &.open
+ background #e2e8f0
+
+.pi-temp-warning
+ align-self center
+ margin 0 4px
+ color #dc2626
+ font-size 24px
+
+.state-badge
+ display inline-flex
+ align-items center
+ gap 0.6rem
+ height 54px
+ padding 0 1.1rem
+ border-radius 14px
+ background #dcfce7
+ color #166534
+ font-weight 800
+ font-size 1rem
+ letter-spacing 0.04em
+
+ .dot
+ width 10px
+ height 10px
+ border-radius 9999px
+ background currentColor
+ position relative
+
+ .dot::after
+ content ""
+ position absolute
+ inset -3px
+ border-radius 9999px
+ border 2px solid currentColor
+ opacity 0.5
+ animation pulse-dot 1.6s ease-out infinite
+
+ &.warn
+ background #fef3c7
+ color #92400e
+
+ &.bad
+ background #fee2e2
+ color #991b1b
+
+ &.busy
+ background #dbeafe
+ color #1e40af
+
+ &.unknown
+ background $line-soft
+ color $muted
+
+ &.unknown .dot::after
+ display none
+
+@keyframes pulse-dot
+ 0%
+ transform scale(0.7)
+ opacity 0.6
+ 100%
+ transform scale(2.2)
+ opacity 0
+
+@media (prefers-reduced-motion: reduce)
+ .state-badge .dot::after
+ animation none
+
+// System popover
+.sys-popover
+ position absolute
+ top 96px
+ right 240px
+ width 360px
+ background $bg
+ border 1px solid $line
+ border-radius 14px
+ box-shadow 0 18px 40px rgba(15, 23, 42, 0.18)
+ padding 12px
+ z-index 40
+ display flex
+ flex-direction column
+ gap 10px
+
+ .sp-row
+ display grid
+ grid-template-columns 32px 1fr auto
+ gap 12px
+ align-items center
+
+ .sp-icon
+ width 32px
+ height 32px
+ border-radius 8px
+ background $line-soft
+ color $ink-soft
+ display inline-flex
+ align-items center
+ justify-content center
img
- width 300px
- height 15%
+ width 18px
+ height 18px
- .version
- font-size 18pt
- color #777
- display flex
- flex-direction column
- justify-content space-evenly
- border-left #777 2px solid;
- margin-left 15px
- padding 0px 10px
- font-weight bold
+ .fa.sp-warn
+ color #f59e0b
- .upgrade-link
- margin-left 20px
- font-size 16pt
- align-self center
- color blue
+ .sp-label
+ font-size 0.7rem
+ text-transform uppercase
+ letter-spacing 0.1em
+ color $muted-2
+ font-weight 800
- .upgrade-attention
- color red
- font-size 18pt
- align-self center
- margin-left 5px
+ .sp-val
+ font-size 0.95rem
+ color $ink
+ font-weight 600
- .pi-temp-warning
- align-self center
- font-size 30pt
- font-family Audiowide
- display inline
- margin 0 30px
-
- .left
- color #444
- .right
- color #e5aa3d
-
- .easy-adapter
- display flex
- align-items center
- gap 5px
- padding 5px
- margin 5px
-
- .round-dot
- width 8px
- height 8px
- border-radius 50%
- background-color #000000
- flex-shrink 0
-
- .easy-adapter-text
- margin 0
- padding 0
-
-
- .whitespace
- flex-grow 1
-
- .rotary-button
- border 1px solid transparent
- background transparent
+ .sp-act
+ height 36px
+ padding 0 12px
+ border-radius 8px
+ background $line-soft
+ border 1px solid $line
+ color $ink
+ font-size 0.8rem
+ font-weight 700
cursor pointer
- width 100px
- height 100px
- border-radius 100px
- margin 5px 0 5px 0
- display flex
- justify-content center
+ text-decoration none
+ display inline-flex
align-items center
+ gap 4px
- &.active
- border 1px solid #54ed54
- background #ccffcb
-
- &:focus
- outline none
- box-shadow none
-
- &:disabled
- opacity 0.5
- background #e0e0e0
- border 1px solid black
- cursor not-allowed
-
- .rotary-text
- text-align center
+ .sp-act:hover
+ background #e2e8f0
.video
+ width 100%
+ height 180px
+ border-radius 8px
+ background #000
+ overflow hidden
position relative
- width 174px
- height 130px
- border 2px solid transparent
- border-radius 5px
-
- &:hover
- border-color #aaa
-
- &.large
- width 100%
- margin 5px 0
- height inherit
-
- .crosshair
- > *
- border 1px dashed #ccc
- position absolute
-
- .vertical
- height 100%
- width 0
- left 50%
- margin-left -1px
-
- .horizontal
- height 0
- width 100%
- top 50%
- margin-top -1px
-
- .box
- width 16px
- height 16px
- top 50%
- left 50%
- margin-top -9px
- margin-left -9px
img
width 100%
height 100%
+ object-fit cover
- .estop
- align-self center
- margin 0 30px
+ .sp-foot
+ display flex
+ gap 8px
+ margin-top 4px
+ padding-top 10px
+ border-top 1px solid $line-soft
+
+ .sp-shutdown, .sp-save
+ flex 1
+ height 40px
+ border-radius 10px
+ border 1px solid $line
+ background $line-soft
+ color $ink
+ font-weight 700
+ cursor pointer
+
+ .sp-shutdown
+ background #fef2f2
+ color #991b1b
+ border-color #fecaca
+
+ .sp-save:not([disabled])
+ background $accent
+ color $ink
+ border-color $accent
.error
background red
+ color #fff
.warn
background orange
@@ -268,467 +472,61 @@ span.unit
position relative
top 50%
-#menu
- .save
- display block
- margin 0.25em 0.6em
- height 3.5em
- width 8em
+// Form rules used by the settings/admin/motor/tool/io templates that
+// still rely on Pure form classes.
+.app-body .pure-control-group
+ label.units
+ width 6em
+ text-align left
+ textarea
+ width 24em
+ height 12em
- .pure-menu-heading
- background inherit
- padding 0
+ > select, > input:not([type=checkbox])
+ min-width 300px
- .pure-menu-link
- padding 0.6em
- color #fff
-
- .pure-menu-item .pure-menu-link
- padding-left 1.5em
-
-
-#main
- margin-left 0.5em
-
- .content
- h2
- text-transform capitalize
-
- .pure-control-group
- label.units
- width 6em
- text-align left
-
- textarea
- width 24em
- height 12em
-
- > select, > input:not([type=checkbox])
- min-width 300px
-
- > tt
- min-width 15.25em
- padding 0.7em 1em
- border-radius 3px
- display inline-block
-
+ > tt
+ min-width 15.25em
+ padding 0.7em 1em
+ border-radius 3px
+ display inline-block
@keyframes blink
50%
fill #ff9d00
-.estop
- width 130px
- transition 250ms
+// Octagonal STOP wrapper around the existing SVG. The SVG
+// rules below (`.button`, `.ring`, etc.) keep working unchanged.
+.app-head .estop
+ width 88px
+ height 88px
+ background #dc2626
+ clip-path polygon(30% 0, 70% 0, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0 70%, 0 30%)
+ display flex
+ align-items center
+ justify-content center
+ border 3px solid #fff
+ box-shadow 0 0 0 3px #b91c1c, 0 8px 20px rgba(220, 38, 38, 0.35)
+ cursor pointer
+ transition transform 0.06s
- &.active .ring
- animation blink 2s step-start 0s infinite
-
- &:hover .button circle
- fill #b72424 !important
+ &:active
+ transform scale(0.96)
svg
+ width 56px
+ height 56px
cursor pointer
.button:hover
filter brightness(120%)
-.control-view
- max-width 95%
+.app-head .estop.active .ring
+ animation blink 2s step-start 0s infinite
- .drop-down-container
- height 50px
-
- .container
- display flex
- width 100%
- height 25rem
- margin-bottom 30px
-
- .folders
- width 30%
- border-right 1px solid #ccc
- padding 10px
- overflow-y auto
-
- .files
- width 70%
- padding 10px
- display grid
- overflow-y auto
-
- .search-bar
- margin-bottom 10px
- width 50%
-
- .file-item
- padding 3px
-
- .folder-item
- padding 10px
-
- .folder-item.selected
- font-weight bold
- background-color #add1ad
- border-radius 5px
-
- table
- border-collapse collapse
-
- // Make sure buttons don't turn into circles
- button
- -webkit-appearance none
- border-radius 0px
- border-width 1px
- border-color darkgrey
-
- // The jogging buttons, etc.
- .control-buttons button
- font-size 150%
- width 100px
- height 100px
-
- .jog-units
- font-size initial
- margin-left 5px
-
- &:first-child
- margin 0.5em 0
-
- td, th
- border none
-
- .radio-toolbar
- input
- opacity 0
- position fixed
- width 0
- label
- display inline-block
- background-color #ddd
- padding 10px 20px
- font-family sans-serif Arial
- font-size 16px
- border 2px solid #444
- border-radius 4px
-
- .axes
- width 100%
-
- .axis-x .name
- color #f00
-
- .axis-y .name
- color #0f0
-
- .axis-z .name
- color #00f
-
- .axis-a .name
- color #f80
-
- .axis-b .name
- color #0ff
-
- .axis-c .name
- color #f0f
-
- td, th
- padding 2px
- white-space nowrap
- border 1px solid #ddd
-
- th
- text-align center
- vertical-align bottom
-
- td
- text-align right
- font-family Courier
-
- .homed
- background-color #ccffcc
- color #000
-
- .warn
- background-color #ffffcc
-
- .error
- background-color #ffcccc
-
- .axis
- .name
- text-transform capitalize
- vertical-align middle
-
- .name, .position
- font-size 24pt
- line-height 24pt
-
- .position
- width 99%
-
- td.state
- text-align left
-
- .fa
- margin-left 2px
- margin-right 6px
-
- .absolute, .offset
- min-width 6em
-
- td.tstate
- text-align left
-
- .fa
- margin-left 2px
- margin-right 6px
-
- tr:nth-child(1) th.actions
- text-align right
-
- .jog svg
- text
- user-select none
- font-family Sans
- font-weight bold
- stroke transparent
- fill #444
-
- .button
- cursor pointer
- stroke #4c4c4c
-
- &:hover
- stroke #e55
-
- path
- overflow visible
-
- .house
- stroke #444
- fill #444
-
- .ring
- cursor pointer
- overflow visible
-
- .button
- stroke transparent
-
- &:hover
- stroke #e55
-
- text
- font-size 8pt
- text-anchor middle
-
- .info
- empty-cells show
- width 100%
- display inline-block
-
- th, td
- height 1.75em
- padding 3px
- text-align right
- overflow hidden
- text-overflow ellipsis
- white-space nowrap
- border 1px solid #ddd
-
- th
- min-width 5.25em
- width 5.25em
-
- td
- min-width 8em
- width 100%
-
- .units
- padding 0
-
- select
- width 100%
- height 1.9em
- background-color transparent
- border 0
- padding 3px
- text-align right
-
- .eta
- font-size 90%
-
- .progress
- height 1.75em
-
- label
- float right
-
- .bar
- height 1.75em
- background #f2ac45
-
- .override
- display none /* Hidden for now */
- margin 0.5em 0
- white-space nowrap
-
- label
- font-weight bold
- min-width 3.5em
- display inline-block
-
- .percent
- display inline-block
- width 3em
-
- input
- border-radius 0
- margin -0.4em 0.5em
-
- .override:nth-of-type(1)
- clear left
- float left
-
- .override:nth-of-type(2)
- clear right
- float right
-
- .toolbar
- clear both
- padding 0 0.125em
- margin-bottom 50px
-
- > *
- margin 0.25em 0.125em
-
- select
- max-width 11em
- min-width inherit !important
-
- .progress
- display inline-block
- background #fff
- line-height 2em
- border 1px solid #aaa
- border-radius 3px
- width 330px
- vertical-align middle
- text-align center
-
- div
- height 2em
- background #f2ac45
-
- label
- margin 0 0.125em
- white-space nowrap
-
- .tabs
- section
- min-height 500px
- overflow-x hidden
- overflow-y auto
- padding 0
- margin 0
-
- .path-viewer
- width 100%
-
- .path-viewer-content
- height 500px
-
- .gcode, .history
- font-family courier
- clear both
- overflow auto
- width 100%
- height 450px
- white-space nowrap
-
- .clusterize-scroll
- max-height 450px
-
- &.placeholder
- color #aaa
-
- .history
- padding 0.25em
-
- .gcode ul, .history ul
- margin 0
- padding 0
- list-style none
-
- .gcode ul
- li
- line-height 15px
-
- li:nth-child(even)
- background-color #fafafa
-
- li.highlight
- background-color #eaeaea
-
- li > b
- font-weight normal
- display inline-block
- padding 0 0.25em
- color #e5aa3d
- min-width 4em
-
- .history ul li
- cursor pointer
-
- .mdi
- clear both
- white-space nowrap
- padding 0.125em
- display flex
-
- > *
- margin 0.125em
-
- input
- flex 2
-
- .jog
- text-align center
-
- > svg
- margin 1em
-
- .jog-settings
- margin-bottom 1em
-
- input
- margin 0 0.5em
- vertical-align middle
-
- .macros-div
- display flex
- flex-wrap wrap
- justify-content flex-start
- margin 10px
- margin-left 50px
- margin-right 50px
- margin-bottom 30px
-
- .macros-button
- height 60px
- width 115px
- font-weight normal
- border-radius 10px
- margin-left 1rem
- margin-top 1rem
- border 0
- overflow-wrap break-word
- color #fff
- box-shadow rgba(0, 0, 0, 0.3) 0px 0px 5px
- text-shadow: rgba(0, 0, 0, 0.8) 0px 0px 3px
+.app-head .estop:hover .button circle
+ fill #b72424 !important
#macros
width 104%
@@ -990,66 +788,12 @@ tt.save
background-color #f3f3f3
-.tabs
+// Legacy `.tabs` selector retained only for #macros (Settings → Macros)
+// which uses .tabs as a content container with a