diff --git a/public/index.html b/public/index.html index 46c2dc4..25b30cd 100644 --- a/public/index.html +++ b/public/index.html @@ -181,10 +181,10 @@ elProgList.innerHTML = folderHtml + fileHtml; - // Einfachklick → auswählen; Doppelklick → Programm laden + // Klick → auswählen + bei Dateien sofort laden elProgList.querySelectorAll('.prog-item').forEach(el => { - el.addEventListener('click', () => setSelected(el.dataset.id, el.dataset.type)); - el.addEventListener('dblclick', () => { + el.addEventListener('click', () => { + setSelected(el.dataset.id, el.dataset.type); if (el.dataset.type === 'file') loadProgram(el.dataset.id); }); }); diff --git a/src/active/activeState.js b/src/active/activeState.js index 08555ff..27738c6 100644 --- a/src/active/activeState.js +++ b/src/active/activeState.js @@ -1,12 +1,10 @@ /** * Aktives Programm + Cursor — Single Source of Truth. * - * Der Cursor lebt zur Laufzeit als In-Memory-Index (schnelles Stepping ohne - * Datei-Neuschreiben). Beim Speichern wird er ins .json-Sidecar geschrieben; - * das .gcode bleibt reiner G-Code ohne '!'-Marker. - * - * Inhaltliche Änderungen (FPoint, Editieren) werden direkt persistiert (durables - * Teaching); reine Cursor-Bewegungen NICHT. + * Der Cursor lebt zur Laufzeit als In-Memory-Index. Bei jeder Cursor-Bewegung + * (Stepping) UND bei Inhaltänderungen (FPoint, Editieren) wird er als ';!'-Marker + * in der .gcode-Datei persistiert — so bleibt die Position nach einem Neustart + * erhalten und ist direkt im File sichtbar. */ const store = require('../store/fileStore'); const units = require('../gcode/units'); @@ -111,9 +109,9 @@ class ActiveState { return this.getState(); } - // ---- Stepping (reine Cursor-Bewegung, gibt die ausführbare Zeile zurück) ---- + // ---- Stepping (persistiert Cursor als ';!' im .gcode) ---- - _gotoIndex(index) { + async _gotoIndex(index) { if (!this.lines.length) throw new ApiError(409, 'EMPTY_PROGRAM', 'active program is empty'); if (index < 0 || index >= this.lines.length) { throw new ApiError( @@ -124,6 +122,7 @@ class ActiveState { } this.cursor = index; this._touch(); + await this._persist(); // Cursor als ';!' in .gcode schreiben return { cursor: this.cursor, line: units.toExecutable(this.lines[this.cursor]) }; } diff --git a/src/store/fileStore.js b/src/store/fileStore.js index da5ef79..ab1b701 100644 --- a/src/store/fileStore.js +++ b/src/store/fileStore.js @@ -1,8 +1,12 @@ /** * Datei-basierte Persistenz der Programme: - * . — G-Code (Grad), standardnah, Zeitstempel/Cursor im Kommentar + * . — G-Code (Grad), standardnah; Zeitstempel als ;-Kommentar, + * Cursor-Zeile zusätzlich mit '!' (z. B. ;1234567890!) * .json — Sidecar mit Metadaten (Name, Zeiten, lineCount, angleUnit) * + * Cursor-Primärquelle ist das ';!'-Marker im .gcode (lesbar, portierbar). + * Der Sidecar enthält cursor als Fallback und für schnellen Zugriff. + * * Nach außen werden Programme NUR über die id angesprochen — niemals über Pfade. * Storage-Details bleiben hier gekapselt (Konzept §8/§10). */ @@ -70,20 +74,21 @@ async function read(id) { } catch { /* Sidecar ist optional */ } - // Migration: alter '!'-Cursor-Marker in .gcode → Cursor liegt jetzt im .json. - let legacyCursor = null; + // Primärquelle: ';!'-Marker im .gcode (genau einer pro Datei). + // Fallback: cursor aus .json (ältere Dateien ohne Marker). + let fileCursor = null; const lines = splitLines(text).map((line, i) => { - if (units.hasCursorMarker(line)) { legacyCursor = i; return units.removeCursorMarker(line); } + if (units.hasCursorMarker(line)) { fileCursor = i; return units.removeCursorMarker(line); } return line; }); - const cursor = meta.cursor ?? legacyCursor ?? 0; + const cursor = fileCursor ?? meta.cursor ?? 0; return { id, name: meta.name || id, cursor, lines, meta }; } /** * Schreibt .gcode + .json. - * lines = saubere G-Code-Zeilen (Grad, ohne '!'-Cursor-Marker). - * cursor = Cursor-Index (landet im .json, nicht in .gcode). + * lines = saubere G-Code-Zeilen (Grad, ohne '!'-Marker) — der Marker wird hier gesetzt. + * cursor = Cursor-Index: die entsprechende Zeile bekommt im .gcode ein '!' angehängt. */ async function write(id, { name, lines, cursor = 0 }) { assertValidId(id); @@ -105,7 +110,11 @@ async function write(id, { name, lines, cursor = 0 }) { createdAt, updatedAt: now, }; - const body = lines.join('\n') + (lines.length ? '\n' : ''); + // Cursor-Zeile im .gcode mit ';!'-Marker — sichtbar in jedem Text-Editor. + const withCursor = lines.map((line, i) => + i === cursor && line.length > 0 ? units.addCursorMarker(line) : line + ); + const body = withCursor.join('\n') + (withCursor.length ? '\n' : ''); await fsp.writeFile(gcodePath(id), body, 'utf8'); await fsp.writeFile(jsonPath(id), JSON.stringify(meta, null, 2) + '\n', 'utf8'); log.info(`write ${gcodePath(id)} (${lines.length} Zeilen, cursor ${cursor})`); diff --git a/test/activeState.test.js b/test/activeState.test.js index f8c18dc..48d848f 100644 --- a/test/activeState.test.js +++ b/test/activeState.test.js @@ -52,7 +52,7 @@ test('Playback: stepping liefert driver-native (Radian) Zeilen, Grenzen werfen', await expect(a.next()).rejects.toThrow(); // über das Ende → CURSOR_OUT_OF_RANGE }); -test('Cursor liegt im .json-Sidecar, .gcode bleibt sauber (kein !-Marker)', async () => { +test('Cursor liegt als ;! in der .gcode-Datei, store.read() liefert saubere Zeilen', async () => { await store.write('cur_1', { name: 'Cur', lines: [ @@ -62,16 +62,20 @@ test('Cursor liegt im .json-Sidecar, .gcode bleibt sauber (kein !-Marker)', asyn }); const a = new ActiveState(); await a.load('cur_1'); - await a.next(); // cursor → 1 (kein Persist) - await a.appendLine('G4 P0.1'); // persistiert, cursor → 2 + await a.next(); // cursor → 1, persistiert als ;! im .gcode + await a.appendLine('G4 P0.1'); // cursor → 2, persistiert + // Rohe .gcode-Datei: genau eine Zeile mit '!', und zwar an Index 2 + const rawText = await fsp.readFile(path.join(tmp, 'cur_1.gcode'), 'utf8'); + const rawLines = rawText.split('\n').filter(Boolean); + const markedLines = rawLines.filter(units.hasCursorMarker); + expect(markedLines).toHaveLength(1); + expect(rawLines.indexOf(markedLines[0])).toBe(2); + + // store.read() liefert saubere Zeilen (kein '!') und korrekten Cursor const prog = await store.read('cur_1'); - // .gcode-Zeilen sind sauber — kein '!'-Marker - const marked = prog.lines.filter(units.hasCursorMarker); - expect(marked).toHaveLength(0); - // Cursor steht im .json-Sidecar expect(prog.cursor).toBe(2); - // Korrekte Zeile am Ende + expect(prog.lines.filter(units.hasCursorMarker)).toHaveLength(0); expect(units.splitComment(prog.lines[2]).code).toBe('G4 P0.1'); });