Replaces the legacy side-menu chrome with a 4-tab top header. - index.pug: tablet/kiosk fit-to-viewport script, header tab nav, estop/state badges in header. - app.js: route hash to (control|program|console|<settings-family>), multi-section settings shell. - control-view: header DRO, jog grid, MDI/probe/macros panels. - program-view + program-mixin: file browser + toolpath preview + run/pause/stop, replaces the legacy 'macros' tab content. - console-view: MDI shell, message log, indicators. - settings-shell-view: rail-driven inner pages (Display & Units, Probing, G-code & Motion, Macros, Network, etc.). - settings-view: filter Svelte SettingsView to one rail section. - SettingsView.svelte: tag every section with data-sec=… so the filter above can hide non-matching ones. - style.styl: ~2700 lines of V09 layout, DRO, jog grid, status strip, and tablet/kiosk variants. No A-axis / auxiliary-axis content lives on this branch.
608 lines
20 KiB
JavaScript
608 lines
20 KiB
JavaScript
"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";
|
|
},
|
|
|
|
// True only while a loaded G-code program is actually being
|
|
// executed (running, paused/holding, or stopping). Excludes
|
|
// jogging, homing, probing, MDI commands and other one-off
|
|
// motion that also leave state.xx == "RUNNING" but must not
|
|
// swap the jog grid out for the "Now Running" panel.
|
|
//
|
|
// Distinguishing signal is state.cycle:
|
|
// - "idle" : nothing happening
|
|
// - "jogging" : user-initiated jog
|
|
// - "homing" : home cycle
|
|
// - "probing" : probe cycle
|
|
// - "mdi" : single MDI command
|
|
// - "running" : an actual loaded program is being run
|
|
// Only "running" (combined with a selected file) is what we want.
|
|
is_program_executing: function () {
|
|
if (!this.state) return false;
|
|
const xx = this.state.xx;
|
|
const cycle = this.state.cycle;
|
|
const isExecState = xx == "RUNNING" || xx == "HOLDING" || xx == "STOPPING";
|
|
if (!isExecState) return false;
|
|
// The cycle string narrows it to a real program run; anything
|
|
// else (jogging / homing / probing / mdi) is a one-off.
|
|
if (cycle && cycle != "running" && cycle != "idle") return false;
|
|
return !!this.state.selected;
|
|
},
|
|
|
|
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 list = Array.isArray(this.state.gcode_list) ? this.state.gcode_list : [];
|
|
const folder = list.find(item => item.name == this.state.folder);
|
|
if (!folder) return [];
|
|
const stateFiles = Array.isArray(this.state.files) ? this.state.files : [];
|
|
const files = (folder.files || [])
|
|
.filter(item => stateFiles.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 () {
|
|
const list = Array.isArray(this.state.gcode_list) ? this.state.gcode_list : [];
|
|
return 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;
|
|
|
|
// state.files can be undefined briefly after connect, before the
|
|
// controller has pushed its file list. Skip the existence check
|
|
// until we have a list to consult.
|
|
const files = Array.isArray(this.state.files) ? this.state.files : null;
|
|
if (this.state.selected && files && !files.includes(this.state.selected)) {
|
|
this.GCodeNotFound = true;
|
|
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: async function (id) {
|
|
if (this.state.macros[id].file_name == "default") {
|
|
this.showNoGcodeMessage = true;
|
|
return;
|
|
}
|
|
const file_name = this.state.macros[id].file_name;
|
|
try {
|
|
// Selecting a file on the server is a side effect of
|
|
// GET /api/file/<name>. The macro button used to mutate
|
|
// state.selected client-side and immediately call start, which
|
|
// raced the file fetch: if the server hadn't seen the new
|
|
// selection yet, mach.start() ran whichever file was selected
|
|
// last. Do it explicitly and await so start always sees the
|
|
// right file.
|
|
if (file_name != this.state.selected) {
|
|
this.state.selected = file_name;
|
|
// GET /api/file/<name> returns gcode text (not JSON), so use
|
|
// fetch directly. The server's FileHandler.get sets
|
|
// state.selected as a side effect; we await the response
|
|
// before starting so mach.start() reads the right file.
|
|
const resp = await fetch(
|
|
`/api/file/${encodeURIComponent(file_name)}`,
|
|
{ cache: "no-cache" }
|
|
);
|
|
if (!resp.ok) {
|
|
throw new Error(`file fetch failed: ${resp.status}`);
|
|
}
|
|
await resp.text();
|
|
}
|
|
this.load();
|
|
if (this.state.macros[id].alert == true) {
|
|
this.macrosLoading = true;
|
|
} else {
|
|
await this.start_pause();
|
|
}
|
|
} catch (error) {
|
|
console.warn("Error running macro: ", error);
|
|
}
|
|
},
|
|
},
|
|
};
|