Default Log

This commit is contained in:
chk
2026-06-14 12:47:12 +02:00
parent 988e8ec752
commit e6abe047dc
3 changed files with 31 additions and 25 deletions

View File

@@ -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 * Der Cursor lebt zur Laufzeit als In-Memory-Index (schnelles Stepping ohne
* Datei-Neuschreiben). Beim Laden wird er aus dem '!'-Kommentar gelesen, beim * Datei-Neuschreiben). Beim Speichern wird er ins .json-Sidecar geschrieben;
* Speichern/Entladen als '!' in die Cursor-Zeile zurückgeschrieben. * das .gcode bleibt reiner G-Code ohne '!'-Marker.
* *
* Inhaltliche Änderungen (FPoint, Editieren) werden direkt persistiert (durables * Inhaltliche Änderungen (FPoint, Editieren) werden direkt persistiert (durables
* Teaching); reine Cursor-Bewegungen NICHT. * Teaching); reine Cursor-Bewegungen NICHT.
@@ -65,15 +65,10 @@ class ActiveState {
} }
const prog = await store.read(id); 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.programId = id;
this.name = prog.name; this.name = prog.name;
this.lines = lines; this.lines = prog.lines; // bereits sauber (kein '!'-Marker)
this.cursor = Math.min(cursor, Math.max(0, lines.length - 1)); this.cursor = Math.min(prog.cursor ?? 0, Math.max(0, prog.lines.length - 1));
this.playing = false; this.playing = false;
this._touch(); this._touch();
return this.getState(); return this.getState();
@@ -199,18 +194,13 @@ class ActiveState {
this._requireActive(); this._requireActive();
const id = store.slugify(name); const id = store.slugify(name);
if (!id) throw new ApiError(400, 'INVALID_NAME', `invalid name: ${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 }; 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() { async _persist() {
if (!this.programId) return; 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() { async _persistIfActive() {

View File

@@ -9,6 +9,7 @@
const fsp = require('fs/promises'); const fsp = require('fs/promises');
const path = require('path'); const path = require('path');
const cfg = require('../config'); const cfg = require('../config');
const units = require('../gcode/units');
const { ApiError } = require('../errors'); const { ApiError } = require('../errors');
const ID_RE = /^[a-z0-9_]+$/; const ID_RE = /^[a-z0-9_]+$/;
@@ -55,25 +56,35 @@ function splitLines(text) {
.filter((l) => l.trim().length > 0); .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) { async function read(id) {
assertValidId(id); assertValidId(id);
if (!(await exists(id))) { if (!(await exists(id))) {
throw new ApiError(404, 'PROGRAM_NOT_FOUND', `no program '${id}'`); throw new ApiError(404, 'PROGRAM_NOT_FOUND', `no program '${id}'`);
} }
const text = await fsp.readFile(gcodePath(id), 'utf8'); const text = await fsp.readFile(gcodePath(id), 'utf8');
const lines = splitLines(text);
let meta = {}; let meta = {};
try { try {
meta = JSON.parse(await fsp.readFile(jsonPath(id), 'utf8')); meta = JSON.parse(await fsp.readFile(jsonPath(id), 'utf8'));
} catch { } catch {
/* Sidecar ist optional */ /* 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); assertValidId(id);
await ensureDir(); await ensureDir();
const now = new Date().toISOString(); const now = new Date().toISOString();
@@ -88,6 +99,7 @@ async function write(id, { name, lines }) {
id, id,
name: name || id, name: name || id,
lineCount: lines.length, lineCount: lines.length,
cursor,
angleUnit: cfg.storeAngleUnit, angleUnit: cfg.storeAngleUnit,
createdAt, createdAt,
updatedAt: now, updatedAt: now,

View File

@@ -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 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', { await store.write('cur_1', {
name: 'Cur', name: 'Cur',
lines: [ 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 await a.appendLine('G4 P0.1'); // persistiert, cursor → 2
const prog = await store.read('cur_1'); const prog = await store.read('cur_1');
// .gcode-Zeilen sind sauber — kein '!'-Marker
const marked = prog.lines.filter(units.hasCursorMarker); const marked = prog.lines.filter(units.hasCursorMarker);
expect(marked).toHaveLength(1); expect(marked).toHaveLength(0);
expect(units.splitComment(marked[0]).code).toBe('G4 P0.1'); // 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 () => { test('Stepping ohne aktives Programm → NO_ACTIVE_PROGRAM', async () => {