task(A4.11): documentService singleton + global v2 undo/redo shortcuts
This commit is contained in:
@@ -16,6 +16,7 @@ import { InteractionDispatcher } from '../interaction/dispatcher';
|
||||
import { logger } from '../utils/logger';
|
||||
import { CADDocument } from '../kernel/document/CADDocument';
|
||||
import { YjsDocument } from '../crdt/YjsDocument';
|
||||
import { setActiveDocument, getActiveDocument } from '../kernel/document/documentService';
|
||||
import { pluginRegistry } from '../plugins';
|
||||
|
||||
const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||
@@ -62,6 +63,8 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||
color: '#ffffff', lineType: 'solid', transparency: 0, sortOrder: 0, parentId: null,
|
||||
} as never);
|
||||
const v2Doc = new CADDocument(ydoc);
|
||||
// App-Level-Zugriffspunkt für HistoryPanel/Tastenkürzel (Task A4.11):
|
||||
setActiveDocument(v2Doc);
|
||||
// Committete V2-Elemente fließen über den etablierten Kanal in UI/State:
|
||||
v2Doc.onChanged(() => {
|
||||
for (const el of v2Doc.getAllElements()) {
|
||||
@@ -112,6 +115,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||
interaction.detach();
|
||||
dispatcherRef.current?.destroy();
|
||||
dispatcherRef.current = null;
|
||||
setActiveDocument(null);
|
||||
zoomPanRef.current = null;
|
||||
renderEngineRef.current = null;
|
||||
interactionRef.current = null;
|
||||
@@ -283,6 +287,26 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeTool]);
|
||||
|
||||
// Task A4.11: Globale Undo/Redo-Shortcuts wirken nur auf das V2-Dokument,
|
||||
// wenn es registriert ist (= Flag aktiv). Legacy-History bleibt unberührt.
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (!(e.ctrlKey || e.metaKey)) return;
|
||||
const doc = getActiveDocument();
|
||||
if (!doc) return; // Flag aus → Legacy-Verhalten unangetastet
|
||||
const key = e.key.toLowerCase();
|
||||
if (key === 'z' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (doc.undo()) logger.info('V2 undo');
|
||||
} else if (key === 'y' || (key === 'z' && e.shiftKey)) {
|
||||
e.preventDefault();
|
||||
if (doc.redo()) logger.info('V2 redo');
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, []);
|
||||
|
||||
// Sync selected template to interaction engine
|
||||
useEffect(() => {
|
||||
const interaction = interactionRef.current;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* documentService – Prozessweiter Zugriffspunkt auf das aktive CADDocument.
|
||||
*
|
||||
* Warum: CanvasArea besitzt die V2-Dokumentinstanz (Task A4.1), aber App-Level
|
||||
* UI (HistoryPanel, Tastenkürzel Strg+Z/Y) benötigt Zugriff OHNE prop-
|
||||
* threading durch die God-Component. Dieser Service ist die bewusste,
|
||||
* dokumentierte Ausnahme von der Reinheitsregel — nur EINE Instanz aktiv.
|
||||
*
|
||||
* Setzen/Entfernen übernimmt CanvasArea im Mount-/Cleanup-Zyklus.
|
||||
*/
|
||||
import type { CADDocument } from './CADDocument';
|
||||
|
||||
type Listener = () => void;
|
||||
|
||||
let activeDoc: CADDocument | null = null;
|
||||
const listeners = new Set<Listener>();
|
||||
|
||||
/** Registriert das aktive Dokument (CanvasArea-Mount). */
|
||||
export function setActiveDocument(doc: CADDocument | null): void {
|
||||
activeDoc = doc;
|
||||
listeners.forEach((l) => l());
|
||||
}
|
||||
|
||||
/** Aktives Dokument oder null (z.B. wenn Flag aus / Canvas unmounted). */
|
||||
export function getActiveDocument(): CADDocument | null {
|
||||
return activeDoc;
|
||||
}
|
||||
|
||||
/** Änderungsabonnement (für React-Hooks). Liefert Unsubscribe. */
|
||||
export function onActiveDocumentChanged(cb: Listener): () => void {
|
||||
listeners.add(cb);
|
||||
return () => listeners.delete(cb);
|
||||
}
|
||||
|
||||
/** Bequemlichkeit: globaler Undo am aktiven Dokument. false wenn kein Doc/Stack leer. */
|
||||
export function undoActive(): boolean {
|
||||
return activeDoc?.undo() ?? false;
|
||||
}
|
||||
|
||||
/** Bequemlichkeit: globales Redo am aktiven Dokument. */
|
||||
export function redoActive(): boolean {
|
||||
return activeDoc?.redo() ?? false;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* documentService Tests (Task A4.11).
|
||||
* Prüft Registrierung/Löschung des aktiven Dokuments und die
|
||||
* Undo/Redo-Delegation an CADDocument.
|
||||
*/
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { setActiveDocument, getActiveDocument, undoActive, redoActive } from '../src/kernel/document/documentService';
|
||||
import { CADDocument } from '../src/kernel/document/CADDocument';
|
||||
import { createInMemoryDoc } from './helpers/inMemoryDoc';
|
||||
import type { CADElement } from '../src/types/cad.types';
|
||||
|
||||
function makeEl(id: string): CADElement {
|
||||
return {
|
||||
id,
|
||||
type: 'rect',
|
||||
layerId: 'l',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
properties: {},
|
||||
} as unknown as CADElement;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
setActiveDocument(null);
|
||||
});
|
||||
|
||||
describe('documentService', () => {
|
||||
it('registriert und entregistriert das aktive Dokument', () => {
|
||||
const { ydoc } = createInMemoryDoc();
|
||||
const doc = new CADDocument(ydoc);
|
||||
setActiveDocument(doc);
|
||||
expect(getActiveDocument()).toBe(doc);
|
||||
setActiveDocument(null);
|
||||
expect(getActiveDocument()).toBeNull();
|
||||
});
|
||||
|
||||
it('undoActive delegiert an das registrierte Dokument', () => {
|
||||
const { ydoc } = createInMemoryDoc();
|
||||
const doc = new CADDocument(ydoc);
|
||||
doc.addElement(makeEl('x1'));
|
||||
setActiveDocument(doc);
|
||||
|
||||
expect(undoActive()).toBe(true); // Element wird entfernt
|
||||
expect(doc.getAllElements().length).toBe(0);
|
||||
expect(redoActive()).toBe(true); // wiederhergestellt
|
||||
expect(doc.getAllElements().length).toBe(1);
|
||||
});
|
||||
|
||||
it('undoActive liefert false ohne aktives Dokument', () => {
|
||||
setActiveDocument(null);
|
||||
expect(undoActive()).toBe(false);
|
||||
expect(redoActive()).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user