Grotti 3.7.0: Treiber für Kobra 4 und Marlin, smartere Dateien-Auswahl und einstellbare Etiketten.
Senden und Abbruch laufen über den Treiber, Orca-Doppelzeilen und tote Ideen-API fallen weg, Drucker folgen der Datei, Etikettenformat liegt in den Einstellungen.
This commit is contained in:
1 parent
5081212212
commit
db6a85a703
54 files changed
+3379
-384
No files matched your search
+199
-58
@@ -32,6 +32,9 @@
|
||||
<option value="">Alle Maschinen</option>
|
||||
<option value="kobra_3">Kobra 3</option>
|
||||
<option value="kobra_s1">Kobra S1</option>
|
||||
<option value="kobra_4">Kobra 4</option>
|
||||
<option value="anycubic_i3">Anycubic i3</option>
|
||||
<option value="tenlog">Tenlog</option>
|
||||
</select>
|
||||
<select id="file-filter-project">
|
||||
<option value="">Alle Projekte</option>
|
||||
@@ -116,7 +119,15 @@
|
||||
<div class="opt-body">
|
||||
<div id="loaded-spools" class="loaded-spools"></div>
|
||||
<p class="tiny">Verbrauch geht von der Rolle am Drucker. Wechsel unter <a href="/material">Material</a>.</p>
|
||||
<label class="check"><input id="send-bed-level" type="checkbox"> Bettnetz vor dem Druck messen</label>
|
||||
<label class="check" id="send-bed-level-wrap"><input id="send-bed-level" type="checkbox"> Bettnetz vor dem Druck messen</label>
|
||||
<label class="check" id="send-marlin-wrap" hidden>
|
||||
Start über USB
|
||||
<select id="send-marlin-mode">
|
||||
<option value="stream">Strom (Zeile für Zeile)</option>
|
||||
<option value="sd">SD-Karte</option>
|
||||
</select>
|
||||
</label>
|
||||
<p class="tiny" id="send-marlin-hint" hidden>„Wenn frei“ startet immer als Strom.</p>
|
||||
<label class="check"><input id="send-override" type="checkbox"> Auch senden, wenn die Maschine nicht passt</label>
|
||||
<label for="send-project">Projekt</label>
|
||||
<select id="send-project"><option value="">keins</option></select>
|
||||
@@ -168,7 +179,12 @@
|
||||
<script src="{{ url_for('static', filename='gcode-viewer.js') }}?v=10"></script>
|
||||
<script src="{{ url_for('static', filename='sweep-viewer.js') }}?v=1"></script>
|
||||
<script>
|
||||
const families = { kobra_s1: "Kobra S1", kobra_3: "Kobra 3" };
|
||||
const families = {
|
||||
kobra_s1: "Kobra S1", kobra_3: "Kobra 3", kobra_4: "Kobra 4",
|
||||
anycubic_i3: "Anycubic i3", tenlog: "Tenlog",
|
||||
};
|
||||
const MOONRAKER_FAMILIES = new Set(["kobra_3", "kobra_s1"]);
|
||||
const MARLIN_FAMILIES = new Set(["anycubic_i3", "tenlog"]);
|
||||
const ABLAUF_KEY = "grotti-ablauf-v2";
|
||||
const SEND_KEY = "grotti-send-v1";
|
||||
const LIB_KEY = "grotti-lib-v1";
|
||||
@@ -359,40 +375,8 @@ function spoolWarnText() {
|
||||
if (!file || need <= 0) return "";
|
||||
const maps = collectAceMaps();
|
||||
const hosts = selectedHosts();
|
||||
const fileTypes = ((file.filaments || []).map((f) => (f.type || "").toLowerCase()).filter(Boolean));
|
||||
const fileProfiles = ((file.filaments || []).map((f) => (f.profile || "").toLowerCase()).filter(Boolean));
|
||||
const PROFILE_VARIANTS = ["meta", "matte", "matt", "silk", "plus", "hf", "cf"];
|
||||
function profileHasVariant(want) {
|
||||
return PROFILE_VARIANTS.some((v) => new RegExp("\\b" + v + "\\b", "i").test(want || ""));
|
||||
}
|
||||
function profileBaseType(want) {
|
||||
const m = String(want || "").toLowerCase().match(/\b(pla|petg|abs|asa|tpu|pa|pc|hips|pva)\b/);
|
||||
return m ? m[1] : "";
|
||||
}
|
||||
function profileOk(spoolRows) {
|
||||
if (!fileProfiles.length) return true;
|
||||
const names = spoolRows.map((s) => (s.name || "").toLowerCase()).filter(Boolean);
|
||||
const types = spoolRows.map((s) => (s.material_type || "").toLowerCase()).filter(Boolean);
|
||||
if (!names.length && !types.length) return true;
|
||||
return fileProfiles.every((want) => {
|
||||
if (names.some((n) => {
|
||||
if (n === want) return true;
|
||||
if (!n.startsWith(want) || /[a-z0-9]/i.test(n.charAt(want.length) || "")) return false;
|
||||
const rest = n.slice(want.length).replace(/^[\s·\-_|/]+/, "");
|
||||
const first = (rest.split(/\s+/)[0] || "").toLowerCase();
|
||||
return !PROFILE_VARIANTS.includes(first);
|
||||
})) return true;
|
||||
// Basis-Profil (nur „… PLA“): Typ reicht, Marke egal — keine Meta/Matte-Spule
|
||||
if (profileHasVariant(want)) return false;
|
||||
const kind = profileBaseType(want);
|
||||
if (!kind) return true;
|
||||
return spoolRows.some((s) => {
|
||||
const t = (s.material_type || "").toLowerCase();
|
||||
const n = (s.name || "").toLowerCase();
|
||||
return t === kind && !profileHasVariant(n);
|
||||
});
|
||||
});
|
||||
}
|
||||
const fileTypes = fileTypesOf(file);
|
||||
const fileProfiles = fileProfilesOf(file);
|
||||
for (const host of hosts) {
|
||||
const name = (printers.find((p) => p.host === host) || {}).name || host;
|
||||
const mapping = maps[host] || [];
|
||||
@@ -414,7 +398,7 @@ function spoolWarnText() {
|
||||
});
|
||||
if (!any) return name + ": keine Grotti-Spule auf den gewählten ACE-Plätzen — Abzug entfällt.";
|
||||
if (left + 0.5 < need) return "ACE auf " + name + ": nur " + grams(left) + ", Datei braucht " + grams(need) + ".";
|
||||
if (!profileOk(mappedSpools)) {
|
||||
if (!profileOk(file, mappedSpools)) {
|
||||
return name + ": Sorte passt nicht (Datei " + fileProfiles.join("/") + ").";
|
||||
}
|
||||
if (!fileProfiles.length && fileTypes.length && types.length && !fileTypes.some((t) => types.includes(t))) {
|
||||
@@ -431,7 +415,7 @@ function spoolWarnText() {
|
||||
if (left + 0.5 < need) {
|
||||
return name + ": nur " + grams(left) + ", Datei braucht " + grams(need) + ".";
|
||||
}
|
||||
if (!profileOk(external)) {
|
||||
if (!profileOk(file, external)) {
|
||||
return name + ": Sorte passt nicht (Datei " + fileProfiles.join("/") + ").";
|
||||
}
|
||||
const types = external.map((s) => (s.material_type || "").toLowerCase()).filter(Boolean);
|
||||
@@ -986,13 +970,116 @@ function familyMismatch(p, file) {
|
||||
return !!(file && file.printer_family && p.family && file.printer_family !== p.family);
|
||||
}
|
||||
|
||||
function renderTargets() {
|
||||
const box = document.getElementById("targets");
|
||||
const PROFILE_VARIANTS = ["meta", "matte", "matt", "silk", "plus", "hf", "cf"];
|
||||
|
||||
function fileProfilesOf(file) {
|
||||
return ((file && file.filaments) || []).map((f) => (f.profile || "").toLowerCase()).filter(Boolean);
|
||||
}
|
||||
|
||||
function fileTypesOf(file) {
|
||||
return ((file && file.filaments) || []).map((f) => (f.type || "").toLowerCase()).filter(Boolean);
|
||||
}
|
||||
|
||||
function profileHasVariant(want) {
|
||||
return PROFILE_VARIANTS.some((v) => new RegExp("\\b" + v + "\\b", "i").test(want || ""));
|
||||
}
|
||||
|
||||
function profileBaseType(want) {
|
||||
const m = String(want || "").toLowerCase().match(/\b(pla|petg|abs|asa|tpu|pa|pc|hips|pva)\b/);
|
||||
return m ? m[1] : "";
|
||||
}
|
||||
|
||||
function profileOk(file, spoolRows) {
|
||||
const fileProfiles = fileProfilesOf(file);
|
||||
if (!fileProfiles.length) return true;
|
||||
const names = spoolRows.map((s) => (s.name || "").toLowerCase()).filter(Boolean);
|
||||
const types = spoolRows.map((s) => (s.material_type || "").toLowerCase()).filter(Boolean);
|
||||
if (!names.length && !types.length) return true;
|
||||
return fileProfiles.every((want) => {
|
||||
if (names.some((n) => {
|
||||
if (n === want) return true;
|
||||
if (!n.startsWith(want) || /[a-z0-9]/i.test(n.charAt(want.length) || "")) return false;
|
||||
const rest = n.slice(want.length).replace(/^[\s·\-_|/]+/, "");
|
||||
const first = (rest.split(/\s+/)[0] || "").toLowerCase();
|
||||
return !PROFILE_VARIANTS.includes(first);
|
||||
})) return true;
|
||||
if (profileHasVariant(want)) return false;
|
||||
const kind = profileBaseType(want);
|
||||
if (!kind) return true;
|
||||
return spoolRows.some((s) => {
|
||||
const t = (s.material_type || "").toLowerCase();
|
||||
const n = (s.name || "").toLowerCase();
|
||||
return t === kind && !profileHasVariant(n);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function materialFitsHost(file, host) {
|
||||
const profiles = fileProfilesOf(file);
|
||||
const types = fileTypesOf(file);
|
||||
if (!profiles.length && !types.length) return true;
|
||||
const onHost = spoolsOnHost(host);
|
||||
if (!onHost.length) return false;
|
||||
if (profiles.length) return profileOk(file, onHost);
|
||||
const spoolTypes = onHost.map((s) => (s.material_type || "").toLowerCase()).filter(Boolean);
|
||||
if (!spoolTypes.length) return false;
|
||||
return types.some((t) => spoolTypes.includes(t));
|
||||
}
|
||||
|
||||
function printerFitsFile(p, file, override) {
|
||||
if (!p || !file) return false;
|
||||
if (sel.tab === "ablauf" && p.family !== "kobra_3") return false;
|
||||
if (familyMismatch(p, file) && !override) return false;
|
||||
if (!materialFitsHost(file, p.host)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function printerPickScore(p, file) {
|
||||
let score = 0;
|
||||
if (canStartNow(p)) score += 100;
|
||||
else if (isFree(p)) score += 50;
|
||||
else if (p.needs_clear) score += 20;
|
||||
if (spoolsOnHost(p.host).length) score += 10;
|
||||
if (file && file.printer_family && p.family === file.printer_family) score += 5;
|
||||
return score;
|
||||
}
|
||||
|
||||
function suggestHostsForFile(file) {
|
||||
if (!file) return new Set();
|
||||
const override = !!(document.getElementById("send-override") || {}).checked;
|
||||
const kept = [...sel.hosts].filter((host) => {
|
||||
const p = printers.find((x) => x.host === host);
|
||||
return printerFitsFile(p, file, override);
|
||||
});
|
||||
if (kept.length) return new Set(kept);
|
||||
const candidates = printers.filter((p) => printerFitsFile(p, file, override));
|
||||
candidates.sort((a, b) => printerPickScore(b, file) - printerPickScore(a, file)
|
||||
|| String(a.name || "").localeCompare(String(b.name || ""), "de"));
|
||||
if (!candidates.length) return new Set();
|
||||
return new Set([candidates[0].host]);
|
||||
}
|
||||
|
||||
function noPrinterReason(file) {
|
||||
if (!file || !printers.length) return "";
|
||||
const override = !!(document.getElementById("send-override") || {}).checked;
|
||||
const familyOk = printers.filter((p) => {
|
||||
if (sel.tab === "ablauf" && p.family !== "kobra_3") return false;
|
||||
return !familyMismatch(p, file) || override;
|
||||
});
|
||||
if (!familyOk.length) {
|
||||
const want = families[file.printer_family] || file.printer_family || "diese Maschine";
|
||||
return "Kein Drucker für " + want + ".";
|
||||
}
|
||||
if (familyOk.some((p) => materialFitsHost(file, p.host))) return "";
|
||||
const need = fileProfilesOf(file).join("/") || fileTypesOf(file).join("/") || "Material";
|
||||
return "Keine passende Rolle am Drucker (" + need + "). Unter Material zuweisen.";
|
||||
}
|
||||
|
||||
function targetsHtml() {
|
||||
const file = selectedFile();
|
||||
const override = document.getElementById("send-override").checked;
|
||||
const nextMode = sel.tab === "send" && sel.mode === "next";
|
||||
box.classList.toggle("dimmed", nextMode);
|
||||
box.innerHTML = printers.map((p) => {
|
||||
return printers.map((p) => {
|
||||
const s = targetState(p);
|
||||
const mismatch = familyMismatch(p, file);
|
||||
const notK3 = sel.tab === "ablauf" && p.family !== "kobra_3";
|
||||
@@ -1005,6 +1092,17 @@ function renderTargets() {
|
||||
<span class="row2 ${s.bad || off ? "bad" : ""}">${escapeHtml(why)}</span>
|
||||
</button>`;
|
||||
}).join("") || '<div class="picked-empty">' + icon("printer") + '<span>Noch kein Drucker.</span><span class="tiny"><a href="/einstellungen">In Einstellungen anlegen</a></span></div>';
|
||||
}
|
||||
|
||||
function renderTargets() {
|
||||
const box = document.getElementById("targets");
|
||||
const nextMode = sel.tab === "send" && sel.mode === "next";
|
||||
box.classList.toggle("dimmed", nextMode);
|
||||
const html = targetsHtml();
|
||||
if (box._html !== html) {
|
||||
box._html = html;
|
||||
box.innerHTML = html;
|
||||
}
|
||||
document.getElementById("step-target").classList.toggle("ok", nextMode || selectedHosts().length > 0);
|
||||
}
|
||||
|
||||
@@ -1060,7 +1158,12 @@ function updateSendButton() {
|
||||
button.disabled = !!problem || !!sendPoll;
|
||||
hint.textContent = problem || note;
|
||||
hint.classList.toggle("bad", !!problem);
|
||||
targetHint.textContent = sel.mode === "next" && sel.tab === "send" ? "Ziel wählt Grotti nach Sorte." : chosen.length > 1 ? "Je Drucker ein eigener Auftrag." : "";
|
||||
let targetNote = "";
|
||||
if (sel.mode === "next" && sel.tab === "send") targetNote = "Ziel wählt Grotti nach Sorte.";
|
||||
else if (chosen.length > 1) targetNote = "Je Drucker ein eigener Auftrag.";
|
||||
else if (file && !chosen.length && sel.mode === "this") targetNote = noPrinterReason(file);
|
||||
targetHint.textContent = targetNote;
|
||||
targetHint.classList.toggle("bad", !!(file && !chosen.length && sel.mode === "this" && targetNote));
|
||||
}
|
||||
|
||||
function updateAblaufButton() {
|
||||
@@ -1082,12 +1185,33 @@ function updateAblaufButton() {
|
||||
hint.classList.toggle("bad", !!problem);
|
||||
}
|
||||
|
||||
function syncSendOptions() {
|
||||
const chosen = selectedPrinters();
|
||||
const moonraker = chosen.some((p) => MOONRAKER_FAMILIES.has(p.family));
|
||||
const marlin = chosen.some((p) => MARLIN_FAMILIES.has(p.family));
|
||||
const bedWrap = document.getElementById("send-bed-level-wrap");
|
||||
const marlinWrap = document.getElementById("send-marlin-wrap");
|
||||
const marlinHint = document.getElementById("send-marlin-hint");
|
||||
if (bedWrap) {
|
||||
bedWrap.hidden = chosen.length > 0 && !moonraker;
|
||||
if (bedWrap.hidden) document.getElementById("send-bed-level").checked = false;
|
||||
}
|
||||
if (marlinWrap) marlinWrap.hidden = !marlin;
|
||||
if (marlinHint) marlinHint.hidden = !marlin;
|
||||
}
|
||||
|
||||
function updateOptionsSummary() {
|
||||
syncSendOptions();
|
||||
const parts = [];
|
||||
const hosts = selectedHosts();
|
||||
const loaded = hosts.reduce((n, h) => n + spoolsOnHost(h).length, 0);
|
||||
if (loaded) parts.push(loaded + (loaded === 1 ? " Spule" : " Spulen"));
|
||||
if (document.getElementById("send-bed-level").checked) parts.push("Bettnetz");
|
||||
if (!document.getElementById("send-bed-level-wrap")?.hidden && document.getElementById("send-bed-level").checked) {
|
||||
parts.push("Bettnetz");
|
||||
}
|
||||
if (!document.getElementById("send-marlin-wrap")?.hidden) {
|
||||
parts.push(document.getElementById("send-marlin-mode").value === "sd" ? "SD-Karte" : "Strom");
|
||||
}
|
||||
if (document.getElementById("send-override").checked) parts.push("Maschine egal");
|
||||
const project = projects.find((p) => String(p.id) === document.getElementById("send-project")?.value);
|
||||
if (project) parts.push(project.name);
|
||||
@@ -1116,7 +1240,9 @@ function refreshFlow() {
|
||||
updateAblaufButton();
|
||||
}
|
||||
|
||||
function selectFile(id) {
|
||||
function selectFile(id, opts) {
|
||||
const keepHosts = !!(opts && opts.keepHosts);
|
||||
const fromUrlHost = !!(opts && opts.fromUrlHost);
|
||||
sel.fileId = id;
|
||||
const file = selectedFile();
|
||||
if (file && file.project_id && document.getElementById("send-project")) {
|
||||
@@ -1127,6 +1253,10 @@ function selectFile(id) {
|
||||
saveAblaufMemory();
|
||||
if (sel.tab === "ablauf") renderAblauf();
|
||||
}
|
||||
if (file && !keepHosts && !fromUrlHost && !(sel.tab === "send" && sel.mode === "next")) {
|
||||
sel.hosts = suggestHostsForFile(file);
|
||||
saveSendMemory();
|
||||
}
|
||||
document.querySelectorAll("#lib-list .lib-item").forEach((n) => n.classList.toggle("on", n.dataset.file === String(id)));
|
||||
refreshFlow();
|
||||
scheduleToolpath(file, 0, 0);
|
||||
@@ -1170,12 +1300,17 @@ async function load() {
|
||||
fillProjectFilter();
|
||||
fillMaterialFilter();
|
||||
fillFolderFilter();
|
||||
const deepProject = new URLSearchParams(location.search).get("project");
|
||||
const deepParams = new URLSearchParams(location.search);
|
||||
const deepProject = deepParams.get("project");
|
||||
if (deepProject) {
|
||||
const pf = document.getElementById("file-filter-project");
|
||||
if (pf && [...pf.options].some((o) => o.value === deepProject)) pf.value = deepProject;
|
||||
}
|
||||
releaseRootForProject();
|
||||
// Beim Laden: gemerkten Drucker nur behalten, wenn er zur Datei passt — außer Deep-Link ?host=
|
||||
if (selectedFile() && !deepParams.get("host") && !(sel.tab === "send" && sel.mode === "next")) {
|
||||
sel.hosts = suggestHostsForFile(selectedFile());
|
||||
}
|
||||
renderLibrary();
|
||||
renderTabs();
|
||||
refreshFlow();
|
||||
@@ -1323,18 +1458,22 @@ async function watchJobs(ids) {
|
||||
}
|
||||
|
||||
async function postSenden(hosts, whenFree) {
|
||||
return api("/senden", {
|
||||
method: "POST",
|
||||
json: {
|
||||
file_id: Number(sel.fileId),
|
||||
hosts,
|
||||
confirm: true,
|
||||
override_machine: document.getElementById("send-override").checked,
|
||||
bed_leveling: document.getElementById("send-bed-level").checked,
|
||||
ace_maps: collectAceMaps(),
|
||||
when_free: whenFree,
|
||||
},
|
||||
});
|
||||
syncSendOptions();
|
||||
const marlinOn = !document.getElementById("send-marlin-wrap")?.hidden;
|
||||
const payload = {
|
||||
file_id: Number(sel.fileId),
|
||||
hosts,
|
||||
confirm: true,
|
||||
override_machine: document.getElementById("send-override").checked,
|
||||
bed_leveling: !document.getElementById("send-bed-level-wrap")?.hidden
|
||||
&& document.getElementById("send-bed-level").checked,
|
||||
ace_maps: collectAceMaps(),
|
||||
when_free: whenFree,
|
||||
};
|
||||
if (marlinOn && !whenFree) {
|
||||
payload.marlin_mode = document.getElementById("send-marlin-mode").value || "stream";
|
||||
}
|
||||
return api("/senden", { method: "POST", json: payload });
|
||||
}
|
||||
|
||||
async function doSend() {
|
||||
@@ -1760,6 +1899,7 @@ document.querySelector(".tabs").addEventListener("click", (event) => {
|
||||
});
|
||||
|
||||
document.getElementById("send-bed-level").addEventListener("change", updateOptionsSummary);
|
||||
document.getElementById("send-marlin-mode")?.addEventListener("change", updateOptionsSummary);
|
||||
document.getElementById("send-override").addEventListener("change", () => { refreshFlow(); });
|
||||
document.getElementById("send-project")?.addEventListener("change", updateOptionsSummary);
|
||||
|
||||
@@ -1863,6 +2003,7 @@ document.getElementById("rescan").addEventListener("click", async () => {
|
||||
});
|
||||
|
||||
window.onQueueChanged = refreshPrinters;
|
||||
window.onDruckerLive = refreshPrinters;
|
||||
loadSendMemory();
|
||||
loadAblaufMemory();
|
||||
load().catch((error) => toast(error.message || String(error), "bad"));
|
||||
|
||||
Reference in new issue
Block a user