diff --git a/CHANGELOG.de.md b/CHANGELOG.de.md index 11c89ef..aaf3713 100644 --- a/CHANGELOG.de.md +++ b/CHANGELOG.de.md @@ -1,5 +1,29 @@ # Changelog +## [0.9.10] – 2026-05-17 + +> **Hinweis:** Mit diesem Release wird der Fokus von neuen Features auf +> **Stabilisierung und Bugfixing** verlagert. Die Kern-Workflows +> (Multi-Printer, Drucker hinzufügen/entfernen, Filament-Dialog, +> Skip-Objekte, Standalone-Binaries) sind funktional ausreichend — ab jetzt +> steht Robustheit vor neuen Features. Größere Feature-Wünsche (ACE Pro 2, +> Home-Assistant-Integration vervollständigen, …) bleiben vorerst im Backlog. + +### Neu +- **Objekte überspringen (vor und während des Drucks):** Aus dem AnycubicSlicerNext-Workbench-Bundle rekonstruiert — der Kobra X kann das nativ über sein Protokoll, der Anycubic-Slicer bietet es bloß nicht im UI an. Die Bridge schon, in zwei Varianten: + - **Vor dem Druck:** beim Starten eines Multi-Object-Drucks aus dem Browser-Tab hat der Filament-Dialog jetzt einen zusätzlichen Abschnitt „Objekte". Einzelne Objekte abwählen (oder Polygon direkt im Build-Plate-SVG anklicken) — sie werden vor dem Druck herausgenommen. + - **Während des Drucks:** neuer ✂-Button im Dashboard (nur während eines laufenden Drucks sichtbar). Öffnet einen Dialog mit derselben interaktiven SVG-Vorschau — Teil anklicken, bestätigen, der Drucker druckt es nicht weiter. Bereits übersprungene Teile bleiben ausgegraut, der Dialog aktualisiert sich live damit man sieht welche schon weg sind. +- **Filament-Dialog – farbige Kanal- und Slot-Marker (Issue #23):** Die GCode-Kanal-Nummer sitzt jetzt in einer farbigen Box links (Hintergrund = Kanal-Farbe, Auto-Kontrast-Text statt des alten kleinen Punktes), der zugewiesene AMS-Slot bekommt rechts neben dem Dropdown denselben Look — aktualisiert sich live wenn man die Auswahl ändert. Funktioniert mit 4 Kanälen; das Layout iteriert sauber für mehr, aber >4 echte Filament-Slots brauchen eine ACE-Pro-2-Box und sind ohne entsprechende Hardware nicht durchgängig testbar (geparkt als Feature-Request, Issues #22 und #23). + +### Intern +- Neue Helfer `kobrax_client.skip_objects(names)` / `query_skip_objects()`. +- Neue Endpunkte: `GET /kx/files/{id}/objects`, `POST /kx/skip`, `POST /kx/skip/query`, `GET /kx/skip/state`. +- SQLite-Schema: `gcode_files` bekommt die Spalten `objects_skip_parts` und `svg_image` (Auto-Migration auf bestehenden DBs). +- `_on_file` extrahiert die vom Drucker gelieferte Objektliste + SVG-Vorschau und speichert sie pro Datei. +- `_on_skip`-Callback verfolgt, welche Objekte der Drucker aktuell als übersprungen meldet. + +--- + ## [0.9.9] – 2026-05-14 ### Fixes diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f28954..a4b51e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## [0.9.10] – 2026-05-17 + +> **Heads-up:** with this release the focus shifts from new features to +> **stabilization and bug-fixing**. The core flows (multi-printer, add/remove, +> filament dialog, skip objects, standalone binaries) are feature-complete +> enough — from now on the priority is making them rock-solid before adding +> more on top. Bigger feature requests (ACE Pro 2, Home Assistant integration +> completeness, …) stay on the backlog for now. + +### New +- **Skip objects (pre-print and mid-print):** Reverse-engineered from the AnycubicSlicerNext Workbench bundle — the Kobra X actually supports object skipping over its native protocol, but the Anycubic slicer doesn't expose it. The bridge does now, in both flavors: + - **Pre-print:** when starting a multi-object print from the Browser tab, the filament dialog now has an additional "Objects" section. Uncheck individual objects (or click the polygon directly on the build-plate SVG preview) and they're stripped from the print before it starts. + - **Mid-print:** new ✂ button on the dashboard (only visible during an active print). Opens a dialog with the same interactive SVG preview — click a part to mark it for skipping, hit confirm, and the printer drops it from the rest of the run. Already-skipped parts stay greyed out and the dialog refreshes live so you can see which ones are gone. +- **Filament dialog – colored channel and slot markers (Issue #23):** the GCode channel number now sits in a colored box on the left (background = channel color, auto-contrast text instead of the old tiny dot), and the assigned AMS slot gets the same treatment on the right of the dropdown — updates live as you change the selection. Plays well with 4 channels; the layout iterates so more channels render correctly, but >4 actual filament slots still need an ACE Pro 2 hub to be testable end-to-end (parked as a feature request, Issues #22 and #23). + +### Internal +- New `kobrax_client.skip_objects(names)` / `query_skip_objects()` helpers. +- New endpoints: `GET /kx/files/{id}/objects`, `POST /kx/skip`, `POST /kx/skip/query`, `GET /kx/skip/state`. +- SQLite schema: `gcode_files` gained `objects_skip_parts` and `svg_image` columns (auto-migrates on existing DBs). +- `_on_file` now extracts the printer-provided object list + SVG preview and persists them per file. +- `_on_skip` callback tracks which objects the printer reports as currently skipped. + +--- + ## [0.9.9] – 2026-05-14 ### Fixes diff --git a/VERSION b/VERSION index 7e310ba..56f3151 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.9 +0.9.10 diff --git a/kobrax_client.py b/kobrax_client.py index afd9f6c..46cb099 100644 --- a/kobrax_client.py +++ b/kobrax_client.py @@ -458,6 +458,20 @@ class KobraXClient: def stop_print(self, taskid: str = "-1") -> dict | None: return self.publish("print", "stop", {"taskid": taskid}) + # -- Part-Skip ("Exclude Object") --------------------------------------- + + def query_skip_objects(self) -> dict | None: + """Fragt den Drucker nach der aktuellen Objekt-/Skip-Liste.""" + return self.publish("skip", "query_obj") + + def skip_objects(self, names: list[str]) -> dict | None: + """Überspringt die genannten Objekte – auch mid-print möglich. + + Namen entsprechen den EXCLUDE_OBJECT_DEFINE NAME=… Einträgen + im GCode-Header bzw. file_details.objects_skip_parts. + """ + return self.publish("skip", "start", {"objects_skip_parts": list(names)}) + # -- G-Code Upload ------------------------------------------------------- def upload_gcode(self, filepath: str, remote_filename: str | None = None, diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index 9cab173..3a9c73a 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -267,7 +267,9 @@ class GCodeStore: est_print_time_sec INTEGER, filament_used_mm REAL, layer_count INTEGER, - gcode_filaments TEXT + gcode_filaments TEXT, + objects_skip_parts TEXT, + svg_image TEXT ); CREATE TABLE IF NOT EXISTS print_jobs ( id TEXT PRIMARY KEY, @@ -287,6 +289,13 @@ class GCodeStore: self._conn.commit() except Exception: pass + # Migration: Spalten objects_skip_parts + svg_image (Part-Skip-Feature, v0.9.10) + for col, typ in (("objects_skip_parts", "TEXT"), ("svg_image", "TEXT")): + try: + self._conn.execute(f"ALTER TABLE gcode_files ADD COLUMN {col} {typ}") + self._conn.commit() + except Exception: + pass def save_file(self, file_id: str, filename: str, data: bytes, est_time_sec: int = 0, thumbnail_b64: str = "", @@ -330,6 +339,18 @@ class GCodeStore: ).fetchone() return dict(row) if row else None + def update_file_objects(self, filename: str, objects: list, svg: str = "") -> None: + """Speichert Objekt-Liste + optionales SVG zu einer Datei (matcht via filename).""" + if not filename: + return + with self._lock: + self._conn.execute( + "UPDATE gcode_files SET objects_skip_parts=?, svg_image=? " + "WHERE filename=?", + (json.dumps(objects), svg or "", filename), + ) + self._conn.commit() + def delete_file(self, file_id: str) -> bool: row = self.get_file(file_id) if not row: @@ -435,6 +456,9 @@ class KobraXBridge: self._thumbnail_b64: str = "" + # Part-Skip: zuletzt vom Drucker gemeldete Skip-Liste (v0.9.10) + self._skip_state: dict = {"objects": [], "skipped": [], "ts": 0} + # Register MQTT push callbacks client.callbacks["tempature/report"] = self._on_temp client.callbacks["print/report"] = self._on_print @@ -442,6 +466,7 @@ class KobraXBridge: client.callbacks["file/report"] = self._on_file client.callbacks["multiColorBox/report"] = self._on_multicolor_box client.callbacks["light/report"] = self._on_light + client.callbacks["skip/report"] = self._on_skip # ------------------------------------------------------------------------- # MQTT callbacks (called from reader thread) @@ -539,6 +564,23 @@ class KobraXBridge: self._state["print_speed_mode"] = int(speed_mode) self._push_status_update() + def _on_skip(self, payload: dict): + """skip/report-Callback (Part-Skip-Feature, v0.9.10). + + Drucker meldet hier IMMER die Liste der bereits geskippten Objekte + zurück (objects_skip_parts), egal ob auf query_obj oder nach skip/start. + Die Gesamt-Objektliste kommt aus file/report. + """ + d = payload.get("data") or {} + skipped = d.get("objects_skip_parts") or d.get("skipped") or d.get("skipped_parts") or [] + # Liste immer (auch leer) übernehmen – sonst bleibt sie auf alten Stand + self._skip_state = { + "skipped": list(skipped), + "ts": int(time.time()), + } + if payload.get("state") == "done" or payload.get("code") == 200: + log.info(f"Skip-Antwort: state={payload.get('state')} code={payload.get('code')} skipped={skipped}") + def _on_file(self, payload: dict): d = payload.get("data") or {} details = d.get("file_details") or {} @@ -546,6 +588,17 @@ class KobraXBridge: if thumb: self._thumbnail_b64 = thumb log.info(f"Vorschaubild empfangen: {len(thumb)} Zeichen base64") + # Part-Skip: Objekt-Liste + optionales SVG (v0.9.10) + objs = details.get("objects_skip_parts") or [] + svg = details.get("svg_image") or "" + if objs: + filename = d.get("filename") or details.get("filename") or self._last_uploaded_file + if filename: + try: + self._store.update_file_objects(filename, objs, svg) + log.info(f"Skip-Objekte für {filename}: {len(objs)} ({'mit SVG' if svg else 'ohne SVG'})") + except Exception as e: + log.warning(f"update_file_objects fehlgeschlagen: {e}") self._push_status_update() def _on_multicolor_box(self, payload: dict): @@ -805,6 +858,91 @@ class KobraXBridge: jobs = self._store.list_jobs(limit=limit, offset=offset) return self._json_cors({"result": jobs}) + async def handle_kx_file_objects(self, request): + """Liefert die Objekt-Liste + optionales SVG für eine Datei. + + GET /kx/files/{id}/objects → {"names": [...], "svg_b64": "..."} + Wenn Datei noch keine Objekte hat (alter Eintrag): file/fileDetails + beim Drucker abfragen und Antwort abwarten ist Aufgabe des Frontends + (Reload nach Upload). Hier nur Datenbankstand zurückgeben. + """ + fid = request.match_info.get("id", "") + f = self._store.get_file(fid) + if not f: + return self._json_cors({"error": "file not found"}, status=404) + try: + names = json.loads(f.get("objects_skip_parts") or "[]") + except Exception: + names = [] + return self._json_cors({ + "result": { + "names": names, + "svg_b64": f.get("svg_image") or "", + } + }) + + async def handle_kx_skip(self, request): + """Mid-Print Skip auslösen. + + POST /kx/skip body={"names": ["..", ".."]} + """ + try: + body = await request.json() + except Exception: + return self._json_cors({"error": "invalid json"}, status=400) + names = body.get("names") or [] + if not isinstance(names, list) or not all(isinstance(n, str) for n in names): + return self._json_cors({"error": "names must be list[str]"}, status=400) + try: + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, lambda: self.client.skip_objects(names)) + except Exception as e: + return self._json_cors({"error": str(e)}, status=502) + return self._json_cors({"result": "ok", "names": names}) + + async def handle_kx_skip_query(self, request): + """Druck-Objektliste vom Drucker neu abfragen. + + POST /kx/skip/query → triggert skip/query_obj, gibt zuletzt bekannten + Stand zurück (skip/report kommt async, Frontend pollt /kx/skip/state). + """ + try: + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, lambda: self.client.query_skip_objects()) + except Exception as e: + return self._json_cors({"error": str(e)}, status=502) + return self._json_cors({"result": self._skip_state}) + + async def handle_kx_skip_state(self, request): + """Aktueller Skip-State. + + Kombiniert: + - Gesamt-Objektliste: aus dem GCode-Store, gematcht über den aktuell + laufenden filename (file/report beim Druckstart hat die Liste gefüllt). + skip/query_obj liefert nämlich NUR die bereits geskippten zurück, + nicht die Gesamtliste. + - Geskippt: aus self._skip_state (von skip/report aktualisiert). + """ + filename = self._state.get("filename", "") + all_objects: list[str] = [] + svg = "" + if filename: + try: + f = self._store.get_file_by_name(filename) + if f: + all_objects = json.loads(f.get("objects_skip_parts") or "[]") + svg = f.get("svg_image") or "" + except Exception as e: + log.warning(f"skip_state lookup failed: {e}") + result = { + "objects": all_objects, + "skipped": list(self._skip_state.get("skipped", [])), + "svg_b64": svg, + "ts": self._skip_state.get("ts", 0), + "filename": filename, + } + return self._json_cors({"result": result}) + async def handle_kx_printers(self, request): # Aktive Drucker (mit IP) sammeln active = [(pid, br) for pid, br in self._all_bridges.items() @@ -848,6 +986,10 @@ class KobraXBridge: # filament_assignments: [{slot_index, material, color_hex}, …] assignments = body.get("filament_assignments") + # excluded_objects: ["name1","name2",...] – Pre-Print Skip (v0.9.10) + excluded_objects = body.get("excluded_objects") or [] + if not isinstance(excluded_objects, list): + excluded_objects = [] if assignments: ams_box_mapping = [ @@ -912,11 +1054,11 @@ class KobraXBridge: "ai_settings": {"status": 0, "count": 0, "type": 1}, "timelapse": {"status": 0, "count": 0, "type": 64}, "drying_settings": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0}, - "model_objects_skip_parts": [], + "model_objects_skip_parts": excluded_objects, }, } - log.info(f"KX-Store Druckstart: {filename} ams={len(ams_box_mapping)} slots assignments={bool(assignments)}") + log.info(f"KX-Store Druckstart: {filename} ams={len(ams_box_mapping)} slots assignments={bool(assignments)} excluded={len(excluded_objects)}") loop = asyncio.get_event_loop() result = await loop.run_in_executor( None, lambda: self.client.publish("print", "start", payload, timeout=15.0) @@ -1210,6 +1352,10 @@ class KobraXBridge: # Optionale Slot-Auswahl aus dem Filament-Dialog filament_assignments = body.get("filament_assignments") + # Pre-Print Skip (v0.9.10) + excluded_objects = body.get("excluded_objects") or [] + if not isinstance(excluded_objects, list): + excluded_objects = [] if filament_assignments is not None: ams_box_mapping = [ { @@ -1271,7 +1417,7 @@ class KobraXBridge: "ai_settings": {"status": 0, "count": 0, "type": 0}, "timelapse": {"status": 0, "count": 0, "type": 0}, "drying_settings": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0}, - "model_objects_skip_parts": [], + "model_objects_skip_parts": excluded_objects, }, } @@ -1423,6 +1569,7 @@ main{flex:1;overflow-y:auto;padding:20px} .btn-start{background:var(--ok);color:#0d2010} .btn-pause{background:var(--raised);color:var(--txt);border:1px solid var(--border)} .btn-resume{background:#0d2d1a;color:var(--ok);border:1px solid var(--ok)} +.btn-skip{background:var(--raised);color:var(--warn);border:1px solid var(--warn)} .btn-cancel{background:#2d0d0d;color:var(--err);border:1px solid var(--err)} .btn-accent{background:var(--accent);color:#001a24} .btn-sm{padding:7px 12px;font-size:12px} @@ -1817,6 +1964,7 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
+
@@ -2101,6 +2249,11 @@ var LANG_DE={ file_slots_btn:'🎨 Slots wählen', file_cancel_btn:'✕ Abbrechen', nav_printers:'Drucker', + skip_title:'✂ Objekte überspringen',skip_hint:'Objekte abwählen, die nicht weiter gedruckt werden sollen:', + skip_btn_label:'Objekte',skip_no_objects:'Keine Objekte in diesem Druck.', + skip_already:'übersprungen',skip_select_at_least_one:'Bitte mindestens ein Objekt wählen.', + skip_sending:'Sende …',skip_success:'Objekte werden übersprungen.', + fd_objects_hint:'Objekte überspringen (optional):', add_printer:'Drucker hinzufügen',apd_lbl_ip:'Drucker-IP',apd_lbl_name:'Name (optional)', apd_fetching:'Hole Daten vom Drucker…',apd_success:'Drucker hinzugefügt, Bridge startet neu…',apd_err_ip:'Bitte IP-Adresse eingeben', printers_remove:'Drucker entfernen',printers_remove_confirm:'Drucker "{name}" entfernen? Die Bridge startet neu.', @@ -2157,6 +2310,11 @@ var LANG_EN={ file_slots_btn:'🎨 Select Slots', file_cancel_btn:'✕ Cancel', nav_printers:'Printers', + skip_title:'✂ Skip objects',skip_hint:'Uncheck objects you no longer want to print:', + skip_btn_label:'Objects',skip_no_objects:'No objects in this print.', + skip_already:'skipped',skip_select_at_least_one:'Please pick at least one object.', + skip_sending:'Sending …',skip_success:'Objects will be skipped.', + fd_objects_hint:'Skip objects (optional):', add_printer:'Add printer',apd_lbl_ip:'Printer IP',apd_lbl_name:'Name (optional)', apd_fetching:'Fetching data from printer…',apd_success:'Printer added, bridge restarting…',apd_err_ip:'Please enter an IP address', printers_remove:'Remove printer',printers_remove_confirm:'Remove printer "{name}"? The bridge will restart.', @@ -2262,6 +2420,10 @@ function applyLang(){ setText('printers-panel-title','🖨 '+T.nav_printers); setText('add-printer-btn-label',T.add_printer); setText('apd-title',T.add_printer); + setText('skip-title',T.skip_title); + setText('skip-hint',T.skip_hint); + setText('d-btn-skip-label',T.skip_btn_label); + setText('fd-objects-hint',T.fd_objects_hint); setText('apd-lbl-ip',T.apd_lbl_ip); setText('apd-lbl-name',T.apd_lbl_name); setText('store-panel-title','🗂 '+T.panel_browser_title); @@ -2484,6 +2646,13 @@ function applyState(){ frb.style.display='flex'; }else{frb.style.display='none';} } + // skip-button (mid-print) – nur sichtbar wenn aktuell gedruckt wird + var skipBtn=document.getElementById('d-btn-skip'); + if(skipBtn){ + var printing=(s.print_state==='printing'||s.print_state==='paused'); + skipBtn.style.display=printing?'':'none'; + } + // header var b=document.getElementById('h-badge'); b.className='hbadge '+s.print_state; @@ -3106,6 +3275,32 @@ function startReadyFileWithSlots(){ } var _amsSlots=[]; +var _printObjects=[]; // [{name, skip}] für aktuell offenen Dialog +var _printObjectsSvg=''; // base64-SVG aus DB für Visualisierung + +// Hilfsfunktionen für farbige Kanal/Slot-Marker (Issue #23) +function _contrastText(hex){ + // Helle Farbe → dunkler Text, dunkle Farbe → heller Text + var c=(hex||'').replace('#',''); + if(c.length===3)c=c[0]+c[0]+c[1]+c[1]+c[2]+c[2]; + if(c.length<6)return '#fff'; + var r=parseInt(c.slice(0,2),16),g=parseInt(c.slice(2,4),16),b=parseInt(c.slice(4,6),16); + // YIQ-Helligkeit + var y=(r*299 + g*587 + b*114)/1000; + return y>=140?'#111':'#fff'; +} +function _updateSlotMarker(sel){ + var opt=sel.options[sel.selectedIndex]; + var color=opt&&opt.dataset.color?opt.dataset.color:'#888'; + var slotIdx=parseInt(opt.value); + var paintIdx=sel.dataset.paint; + var marker=document.querySelector('.fd-slot-marker[data-for-paint="'+paintIdx+'"]'); + if(marker){ + marker.style.background=color; + marker.style.color=_contrastText(color); + marker.textContent=(slotIdx+1); + } +} function openFilamentDialog(slots){ _amsSlots=slots.filter(function(s){return s.status==='loaded';}); @@ -3113,6 +3308,24 @@ function openFilamentDialog(slots){ var title=document.getElementById('fd-title'); var body=document.getElementById('fd-slots'); if(title)title.textContent='▶ '+_storeFilename; + // Objekt-Liste laden (nur Store-Modus: per File-ID; Banner-Modus hat keine ID) + _printObjects=[]; + _printObjectsSvg=''; + var objSection=document.getElementById('fd-objects-section'); + if(objSection)objSection.style.display='none'; + if(_storeFileId){ + fetch(_apiUrl('/kx/files/'+encodeURIComponent(_storeFileId)+'/objects')) + .then(function(r){return r.json()}) + .then(function(d){ + var names=(d.result&&d.result.names)||[]; + _printObjectsSvg=(d.result&&d.result.svg_b64)||''; + if(names.length>=2){ + _printObjects=names.map(function(n){return {name:n,skip:false};}); + renderObjectChecklist(); renderObjectSvg(); + if(objSection)objSection.style.display='block'; + } + }).catch(function(){}); + } // GCode-Kanäle: bevorzugt aus _gcodeFilaments, sonst aus belegten AMS-Slots ableiten var channels=_gcodeFilaments.length?_gcodeFilaments:_amsSlots.map(function(s,i){ @@ -3131,14 +3344,18 @@ function openFilamentDialog(slots){ var opts=compatible.map(function(s){ var sel=(s.slot_index===defaultSlot.slot_index)?'selected':''; return ''; + '● Slot '+(s.slot_index+1)+' · '+s.material+''; }).join(''); - return '
'+ - ''+ - 'Kanal '+(i+1)+''+ - ''+gc.material+''+ - ''+ - ''+ opts+''+ '
'; }).join(''); @@ -3174,12 +3391,14 @@ function confirmFilamentPrint(){ ams_color: hexToRgba(amsSlot.color_hex||'#ffffff'), }); }); + // Pre-Print Skip: Namen der abgehakten Objekte sammeln + var excludedObjects=_printObjects.filter(function(o){return o.skip;}).map(function(o){return o.name;}); closeFilamentDialog(); if(_filamentDialogMode==='banner'){ // Banner-Modus: normaler print/start mit Slot-Override var btn=document.getElementById('file-ready-btn'); if(btn){btn.disabled=true;btn.textContent='…';} - post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments}) + post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments,excluded_objects:excludedObjects}) .then(function(r){return r.json();}) .then(function(){ document.getElementById('file-ready-banner').style.display='none'; @@ -3194,7 +3413,7 @@ function confirmFilamentPrint(){ fetch(_apiUrl('/kx/print'),{ method:'POST', headers:{'Content-Type':'application/json'}, - body:JSON.stringify({file_id:_storeFileId,filament_assignments:assignments}) + body:JSON.stringify({file_id:_storeFileId,filament_assignments:assignments,excluded_objects:excludedObjects}) }).then(function(r){return r.json()}).then(function(d){ if(d.result==='ok'){clog('Druckstart: '+_storeFilename,'msg-ok');showPanel('dashboard');} else{clog('Druckfehler: '+(d.error||'?'),'msg-err');} @@ -3202,6 +3421,164 @@ function confirmFilamentPrint(){ } } +function renderObjectChecklist(){ + var box=document.getElementById('fd-objects'); + if(!box)return; + box.innerHTML=_printObjects.map(function(o,i){ + var label=o.name; + // Klipper-Namen sind oft "Datei.stl_id_N_copy_M" → schöner darstellen + var m=label.match(/^(.+)\.stl_id_(\d+)_copy_(\d+)$/); + if(m)label=m[1]+' #'+(parseInt(m[2])+1)+(m[3]!=='0'?' ('+(parseInt(m[3])+1)+')':''); + return ''; + }).join(''); +} +function _toggleObjectSkip(idx,val){ + if(_printObjects[idx])_printObjects[idx].skip=!!val; + renderObjectSvg(); +} +function renderObjectSvg(){ + var box=document.getElementById('fd-objects-svg'); + if(!box)return; + if(!_printObjectsSvg||!_printObjects.length){box.style.display='none';box.innerHTML='';return;} + box.style.display='block'; + var svg=''; try{ svg=atob(_printObjectsSvg);}catch(e){ box.style.display='none'; return; } + box.innerHTML=svg; + var svgEl=box.querySelector('svg'); + if(!svgEl)return; + svgEl.style.width='100%'; svgEl.style.maxHeight='200px'; svgEl.style.height='auto'; + _printObjects.forEach(function(o,i){ + var g=svgEl.querySelector('g[id="'+CSS.escape(o.name)+'"]'); + if(!g)return; + var path=g.querySelector('path'); + g.style.cursor='pointer'; + g.setAttribute('opacity', o.skip?'0.8':'0.35'); + if(path){ + path.setAttribute('fill', o.skip?'#ff5e5b':'#5fa7ff'); + path.setAttribute('fill-opacity', o.skip?'0.4':'0.18'); + } + g.onclick=function(){ + _printObjects[i].skip=!_printObjects[i].skip; + renderObjectChecklist(); renderObjectSvg(); + }; + }); +} + +// ── Mid-Print Skip ── +var _skipObjects=[]; // [{name, skipped, willSkip}] +var _skipSvg=''; +function openSkipDialog(){ + document.getElementById('skip-status').textContent=''; + document.getElementById('skip-confirm').disabled=false; + _refreshSkipDialog(); + document.getElementById('skip-dialog').classList.add('open'); +} +function _refreshSkipDialog(){ + // Erst aktueller State (mit DB-Objects + svg), dann query_obj für frischen skipped + fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){ + var s=d.result||{}; + _skipSvg=s.svg_b64||''; + _skipObjects=(s.objects||[]).map(function(n){ + return {name:n, skipped:(s.skipped||[]).indexOf(n)>=0, willSkip:false}; + }); + renderSkipList(); renderSkipSvg(); + }); + // Frisch nachfragen (skipped-Liste aktualisieren) + fetch(_apiUrl('/kx/skip/query'),{method:'POST'}).then(function(r){return r.json()}).then(function(){ + setTimeout(function(){ + fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){ + var s=d.result||{}; + var skipped=s.skipped||[]; + _skipObjects.forEach(function(o){ o.skipped=skipped.indexOf(o.name)>=0; if(o.skipped)o.willSkip=false; }); + renderSkipList(); renderSkipSvg(); + }); + }, 500); + }).catch(function(){}); +} +function closeSkipDialog(){ + document.getElementById('skip-dialog').classList.remove('open'); +} +function _shortLabel(name){ + var m=name.match(/^(.+)\.[sS][tT][lL]_id_(\d+)_copy_(\d+)$/); + if(!m)return name; + return m[1]+' #'+(parseInt(m[2])+1)+(m[3]!=='0'?' ('+(parseInt(m[3])+1)+')':''); +} +function renderSkipList(){ + var box=document.getElementById('skip-list'); + if(!box)return; + if(!_skipObjects.length){ + box.innerHTML='
'+(T.skip_no_objects||'Keine Objekte in diesem Druck.')+'
'; + return; + } + box.innerHTML=_skipObjects.map(function(o,i){ + var label=_shortLabel(o.name); + var dis=o.skipped?'disabled':''; + var note=o.skipped?''+(T.skip_already||'übersprungen')+'':''; + return ''; + }).join(''); +} +function renderSkipSvg(){ + var box=document.getElementById('skip-svg'); + if(!box)return; + if(!_skipSvg||!_skipObjects.length){box.style.display='none';box.innerHTML='';return;} + box.style.display='block'; + // SVG aus base64 dekodieren + var svg=''; + try{ svg=atob(_skipSvg); }catch(e){ box.style.display='none'; return; } + box.innerHTML=svg; + // Polygone interaktiv machen: jeder entspricht einem Objekt + var svgEl=box.querySelector('svg'); + if(!svgEl)return; + svgEl.style.width='100%'; svgEl.style.maxHeight='280px'; svgEl.style.height='auto'; + _skipObjects.forEach(function(o,i){ + var g=svgEl.querySelector('g[id="'+CSS.escape(o.name)+'"]'); + if(!g)return; + var path=g.querySelector('path'); + if(o.skipped){ + // bereits übersprungen → ausgegraut, kein Klick + g.setAttribute('opacity','0.25'); + if(path){path.setAttribute('fill','#888');path.setAttribute('fill-opacity','0.3');} + g.style.cursor='not-allowed'; + } else { + g.style.cursor='pointer'; + g.setAttribute('opacity', o.willSkip?'0.8':'0.35'); + if(path){ + path.setAttribute('fill', o.willSkip?'#ff5e5b':'#5fa7ff'); + path.setAttribute('fill-opacity', o.willSkip?'0.4':'0.18'); + } + g.onclick=function(){ + _skipObjects[i].willSkip=!_skipObjects[i].willSkip; + renderSkipList(); renderSkipSvg(); + }; + } + }); +} +function _toggleWillSkip(idx,val){ + if(_skipObjects[idx])_skipObjects[idx].willSkip=!!val; + renderSkipSvg(); +} +function confirmSkip(){ + var names=_skipObjects.filter(function(o){return o.willSkip;}).map(function(o){return o.name;}); + var st=document.getElementById('skip-status'); + var btn=document.getElementById('skip-confirm'); + if(!names.length){st.textContent=T.skip_select_at_least_one||'Bitte mindestens ein Objekt wählen.';st.style.color='var(--warn)';return;} + btn.disabled=true; st.textContent=T.skip_sending||'Sende …'; st.style.color='var(--txt2)'; + fetch(_apiUrl('/kx/skip'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({names:names})}) + .then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});}) + .then(function(res){ + if(!res.ok){st.textContent=(res.j&&res.j.error)||'Fehler';st.style.color='var(--err)';btn.disabled=false;return;} + st.textContent=T.skip_success||'Objekte werden übersprungen.';st.style.color='var(--ok)'; + // Dialog offen lassen + neu laden damit der "übersprungen"-Status erscheint + setTimeout(function(){ _refreshSkipDialog(); btn.disabled=false; st.textContent=''; }, 1500); + }) + .catch(function(e){st.textContent=''+e;st.style.color='var(--err)';btn.disabled=false;}); +} + function storeDelete(fileId){ if(!confirm(T.store_delete_confirm)) return; fetch(_apiUrl('/kx/files/'+fileId),{method:'DELETE'}).then(function(r){ @@ -3322,11 +3699,16 @@ function loadPrinterTab(){ -

GCode-Kanal → AMS-Slot zuweisen:

+

GCode-Kanal → AMS-Slot zuweisen:

+
- - + +
@@ -3348,6 +3730,23 @@ function loadPrinterTab(){ + +