diff --git a/src/active/activeState.js b/src/active/activeState.js index fb5cbef..588654f 100644 --- a/src/active/activeState.js +++ b/src/active/activeState.js @@ -1,9 +1,9 @@ /** - * Aktives Programm + Cursor — Single Source of Truth (doc/draft_filehandeling.md §9). + * Aktives Programm + Cursor — Single Source of Truth. * * Der Cursor lebt zur Laufzeit als In-Memory-Index (schnelles Stepping ohne - * Datei-Neuschreiben). Beim Laden wird er aus dem '!'-Kommentar gelesen, beim - * Speichern/Entladen als '!' in die Cursor-Zeile zurückgeschrieben. + * 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. @@ -65,15 +65,10 @@ class ActiveState { } const prog = await store.read(id); - let cursor = 0; - const lines = prog.lines.map((line, i) => { - if (units.hasCursorMarker(line)) cursor = i; - return units.removeCursorMarker(line); - }); this.programId = id; this.name = prog.name; - this.lines = lines; - this.cursor = Math.min(cursor, Math.max(0, lines.length - 1)); + this.lines = prog.lines; // bereits sauber (kein '!'-Marker) + this.cursor = Math.min(prog.cursor ?? 0, Math.max(0, prog.lines.length - 1)); this.playing = false; this._touch(); return this.getState(); @@ -199,18 +194,13 @@ class ActiveState { this._requireActive(); const id = store.slugify(name); if (!id) throw new ApiError(400, 'INVALID_NAME', `invalid name: ${name}`); - const meta = await store.write(id, { name, lines: this._linesWithCursor() }); + const meta = await store.write(id, { name, lines: this.lines, cursor: this.cursor }); return { id: meta.id, lineCount: meta.lineCount }; } - /** In-Memory-Zeilen mit '!' an der Cursor-Position (für die Persistenz). */ - _linesWithCursor() { - return this.lines.map((line, i) => (i === this.cursor ? units.addCursorMarker(line) : line)); - } - async _persist() { if (!this.programId) return; - await store.write(this.programId, { name: this.name, lines: this._linesWithCursor() }); + await store.write(this.programId, { name: this.name, lines: this.lines, cursor: this.cursor }); } async _persistIfActive() { diff --git a/src/store/fileStore.js b/src/store/fileStore.js index c345b0a..8f05128 100644 --- a/src/store/fileStore.js +++ b/src/store/fileStore.js @@ -9,6 +9,7 @@ const fsp = require('fs/promises'); const path = require('path'); const cfg = require('../config'); +const units = require('../gcode/units'); const { ApiError } = require('../errors'); const ID_RE = /^[a-z0-9_]+$/; @@ -55,25 +56,35 @@ function splitLines(text) { .filter((l) => l.trim().length > 0); } -/** Liest ein Programm: { id, name, lines (Grad, mit Kommentaren), meta }. */ +/** Liest ein Programm: { id, name, cursor, lines (Grad, sauber ohne '!'), meta }. */ async function read(id) { assertValidId(id); if (!(await exists(id))) { throw new ApiError(404, 'PROGRAM_NOT_FOUND', `no program '${id}'`); } const text = await fsp.readFile(gcodePath(id), 'utf8'); - const lines = splitLines(text); let meta = {}; try { meta = JSON.parse(await fsp.readFile(jsonPath(id), 'utf8')); } catch { /* Sidecar ist optional */ } - return { id, name: meta.name || id, lines, meta }; + // Migration: alter '!'-Cursor-Marker in .gcode → Cursor liegt jetzt im .json. + let legacyCursor = null; + const lines = splitLines(text).map((line, i) => { + if (units.hasCursorMarker(line)) { legacyCursor = i; return units.removeCursorMarker(line); } + return line; + }); + const cursor = meta.cursor ?? legacyCursor ?? 0; + return { id, name: meta.name || id, cursor, lines, meta }; } -/** Schreibt .gcode + .json. lines = gespeicherte Zeilen (Grad, inkl. Kommentar/Cursor). */ -async function write(id, { name, lines }) { +/** + * Schreibt .gcode + .json. + * lines = saubere G-Code-Zeilen (Grad, ohne '!'-Cursor-Marker). + * cursor = Cursor-Index (landet im .json, nicht in .gcode). + */ +async function write(id, { name, lines, cursor = 0 }) { assertValidId(id); await ensureDir(); const now = new Date().toISOString(); @@ -88,6 +99,7 @@ async function write(id, { name, lines }) { id, name: name || id, lineCount: lines.length, + cursor, angleUnit: cfg.storeAngleUnit, createdAt, updatedAt: now, diff --git a/test/activeState.test.js b/test/activeState.test.js index b2c66ff..916b7ea 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', expect(() => a.next()).toThrow(); // über das Ende → CURSOR_OUT_OF_RANGE }); -test('Cursor wird beim Speichern als !-Kommentar abgelegt (genau eine Zeile)', async () => { +test('Cursor liegt im .json-Sidecar, .gcode bleibt sauber (kein !-Marker)', async () => { await store.write('cur_1', { name: 'Cur', lines: [ @@ -66,9 +66,13 @@ test('Cursor wird beim Speichern als !-Kommentar abgelegt (genau eine Zeile)', a await a.appendLine('G4 P0.1'); // persistiert, cursor → 2 const prog = await store.read('cur_1'); + // .gcode-Zeilen sind sauber — kein '!'-Marker const marked = prog.lines.filter(units.hasCursorMarker); - expect(marked).toHaveLength(1); - expect(units.splitComment(marked[0]).code).toBe('G4 P0.1'); + expect(marked).toHaveLength(0); + // Cursor steht im .json-Sidecar + expect(prog.cursor).toBe(2); + // Korrekte Zeile am Ende + expect(units.splitComment(prog.lines[2]).code).toBe('G4 P0.1'); }); test('Stepping ohne aktives Programm → NO_ACTIVE_PROGRAM', async () => {