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 { logger } from '../utils/logger';
|
||||||
import { CADDocument } from '../kernel/document/CADDocument';
|
import { CADDocument } from '../kernel/document/CADDocument';
|
||||||
import { YjsDocument } from '../crdt/YjsDocument';
|
import { YjsDocument } from '../crdt/YjsDocument';
|
||||||
|
import { setActiveDocument, getActiveDocument } from '../kernel/document/documentService';
|
||||||
import { pluginRegistry } from '../plugins';
|
import { pluginRegistry } from '../plugins';
|
||||||
|
|
||||||
const CanvasArea: React.FC<CanvasAreaProps> = ({
|
const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||||
@@ -62,6 +63,8 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
color: '#ffffff', lineType: 'solid', transparency: 0, sortOrder: 0, parentId: null,
|
color: '#ffffff', lineType: 'solid', transparency: 0, sortOrder: 0, parentId: null,
|
||||||
} as never);
|
} as never);
|
||||||
const v2Doc = new CADDocument(ydoc);
|
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:
|
// Committete V2-Elemente fließen über den etablierten Kanal in UI/State:
|
||||||
v2Doc.onChanged(() => {
|
v2Doc.onChanged(() => {
|
||||||
for (const el of v2Doc.getAllElements()) {
|
for (const el of v2Doc.getAllElements()) {
|
||||||
@@ -112,6 +115,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
interaction.detach();
|
interaction.detach();
|
||||||
dispatcherRef.current?.destroy();
|
dispatcherRef.current?.destroy();
|
||||||
dispatcherRef.current = null;
|
dispatcherRef.current = null;
|
||||||
|
setActiveDocument(null);
|
||||||
zoomPanRef.current = null;
|
zoomPanRef.current = null;
|
||||||
renderEngineRef.current = null;
|
renderEngineRef.current = null;
|
||||||
interactionRef.current = null;
|
interactionRef.current = null;
|
||||||
@@ -283,6 +287,26 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [activeTool]);
|
}, [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
|
// Sync selected template to interaction engine
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interaction = interactionRef.current;
|
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