FileBrowser Cursor !
This commit is contained in:
@@ -181,10 +181,10 @@
|
|||||||
|
|
||||||
elProgList.innerHTML = folderHtml + fileHtml;
|
elProgList.innerHTML = folderHtml + fileHtml;
|
||||||
|
|
||||||
// Einfachklick → auswählen; Doppelklick → Programm laden
|
// Klick → auswählen + bei Dateien sofort laden
|
||||||
elProgList.querySelectorAll('.prog-item').forEach(el => {
|
elProgList.querySelectorAll('.prog-item').forEach(el => {
|
||||||
el.addEventListener('click', () => setSelected(el.dataset.id, el.dataset.type));
|
el.addEventListener('click', () => {
|
||||||
el.addEventListener('dblclick', () => {
|
setSelected(el.dataset.id, el.dataset.type);
|
||||||
if (el.dataset.type === 'file') loadProgram(el.dataset.id);
|
if (el.dataset.type === 'file') loadProgram(el.dataset.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Aktives Programm + Cursor — Single Source of Truth.
|
* 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. Bei jeder Cursor-Bewegung
|
||||||
* Datei-Neuschreiben). Beim Speichern wird er ins .json-Sidecar geschrieben;
|
* (Stepping) UND bei Inhaltänderungen (FPoint, Editieren) wird er als ';!'-Marker
|
||||||
* das .gcode bleibt reiner G-Code ohne '!'-Marker.
|
* in der .gcode-Datei persistiert — so bleibt die Position nach einem Neustart
|
||||||
*
|
* erhalten und ist direkt im File sichtbar.
|
||||||
* Inhaltliche Änderungen (FPoint, Editieren) werden direkt persistiert (durables
|
|
||||||
* Teaching); reine Cursor-Bewegungen NICHT.
|
|
||||||
*/
|
*/
|
||||||
const store = require('../store/fileStore');
|
const store = require('../store/fileStore');
|
||||||
const units = require('../gcode/units');
|
const units = require('../gcode/units');
|
||||||
@@ -111,9 +109,9 @@ class ActiveState {
|
|||||||
return this.getState();
|
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 (!this.lines.length) throw new ApiError(409, 'EMPTY_PROGRAM', 'active program is empty');
|
||||||
if (index < 0 || index >= this.lines.length) {
|
if (index < 0 || index >= this.lines.length) {
|
||||||
throw new ApiError(
|
throw new ApiError(
|
||||||
@@ -124,6 +122,7 @@ class ActiveState {
|
|||||||
}
|
}
|
||||||
this.cursor = index;
|
this.cursor = index;
|
||||||
this._touch();
|
this._touch();
|
||||||
|
await this._persist(); // Cursor als ';!' in .gcode schreiben
|
||||||
return { cursor: this.cursor, line: units.toExecutable(this.lines[this.cursor]) };
|
return { cursor: this.cursor, line: units.toExecutable(this.lines[this.cursor]) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* Datei-basierte Persistenz der Programme:
|
* Datei-basierte Persistenz der Programme:
|
||||||
* <id>.<ext> — G-Code (Grad), standardnah, Zeitstempel/Cursor im Kommentar
|
* <id>.<ext> — G-Code (Grad), standardnah; Zeitstempel als ;<epoch>-Kommentar,
|
||||||
|
* Cursor-Zeile zusätzlich mit '!' (z. B. ;1234567890!)
|
||||||
* <id>.json — Sidecar mit Metadaten (Name, Zeiten, lineCount, angleUnit)
|
* <id>.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.
|
* Nach außen werden Programme NUR über die id angesprochen — niemals über Pfade.
|
||||||
* Storage-Details bleiben hier gekapselt (Konzept §8/§10).
|
* Storage-Details bleiben hier gekapselt (Konzept §8/§10).
|
||||||
*/
|
*/
|
||||||
@@ -70,20 +74,21 @@ async function read(id) {
|
|||||||
} catch {
|
} catch {
|
||||||
/* Sidecar ist optional */
|
/* Sidecar ist optional */
|
||||||
}
|
}
|
||||||
// Migration: alter '!'-Cursor-Marker in .gcode → Cursor liegt jetzt im .json.
|
// Primärquelle: ';!'-Marker im .gcode (genau einer pro Datei).
|
||||||
let legacyCursor = null;
|
// Fallback: cursor aus .json (ältere Dateien ohne Marker).
|
||||||
|
let fileCursor = null;
|
||||||
const lines = splitLines(text).map((line, i) => {
|
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;
|
return line;
|
||||||
});
|
});
|
||||||
const cursor = meta.cursor ?? legacyCursor ?? 0;
|
const cursor = fileCursor ?? meta.cursor ?? 0;
|
||||||
return { id, name: meta.name || id, cursor, lines, meta };
|
return { id, name: meta.name || id, cursor, lines, meta };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schreibt .gcode + .json.
|
* Schreibt .gcode + .json.
|
||||||
* lines = saubere G-Code-Zeilen (Grad, ohne '!'-Cursor-Marker).
|
* lines = saubere G-Code-Zeilen (Grad, ohne '!'-Marker) — der Marker wird hier gesetzt.
|
||||||
* cursor = Cursor-Index (landet im .json, nicht in .gcode).
|
* cursor = Cursor-Index: die entsprechende Zeile bekommt im .gcode ein '!' angehängt.
|
||||||
*/
|
*/
|
||||||
async function write(id, { name, lines, cursor = 0 }) {
|
async function write(id, { name, lines, cursor = 0 }) {
|
||||||
assertValidId(id);
|
assertValidId(id);
|
||||||
@@ -105,7 +110,11 @@ async function write(id, { name, lines, cursor = 0 }) {
|
|||||||
createdAt,
|
createdAt,
|
||||||
updatedAt: now,
|
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(gcodePath(id), body, 'utf8');
|
||||||
await fsp.writeFile(jsonPath(id), JSON.stringify(meta, null, 2) + '\n', 'utf8');
|
await fsp.writeFile(jsonPath(id), JSON.stringify(meta, null, 2) + '\n', 'utf8');
|
||||||
log.info(`write ${gcodePath(id)} (${lines.length} Zeilen, cursor ${cursor})`);
|
log.info(`write ${gcodePath(id)} (${lines.length} Zeilen, cursor ${cursor})`);
|
||||||
|
|||||||
@@ -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
|
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', {
|
await store.write('cur_1', {
|
||||||
name: 'Cur',
|
name: 'Cur',
|
||||||
lines: [
|
lines: [
|
||||||
@@ -62,16 +62,20 @@ test('Cursor liegt im .json-Sidecar, .gcode bleibt sauber (kein !-Marker)', asyn
|
|||||||
});
|
});
|
||||||
const a = new ActiveState();
|
const a = new ActiveState();
|
||||||
await a.load('cur_1');
|
await a.load('cur_1');
|
||||||
await a.next(); // cursor → 1 (kein Persist)
|
await a.next(); // cursor → 1, persistiert als ;! im .gcode
|
||||||
await a.appendLine('G4 P0.1'); // persistiert, cursor → 2
|
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');
|
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);
|
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');
|
expect(units.splitComment(prog.lines[2]).code).toBe('G4 P0.1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user