task(CAD-14): phase 1 - tool exclusivity, V2 dispatcher exclusive for V2 tools (was: both engines had same tool = multi elements per click), selection handles now render in pixi mode via overlay pass, E2E proof test added

This commit is contained in:
Agent Zero
2026-09-16 18:03:08 +02:00
parent 4e6edb51ed
commit 02ed6a5ff0
4 changed files with 103 additions and 20 deletions
+32 -11
View File
@@ -57,6 +57,12 @@ export class RenderEngine {
private dpr = 1;
/** CAD-13-fix: true = Legacy-Canvas zeichnet KEINE Elemente (Pixi-Overlay zeichnet sie). Grid/Hintergrund bleiben. */
skipElementRendering = false;
/** CAD-14: Elemente-Spiegel fuer Auswahl-Griffe im Pixi-Modus (RenderEngine zeichnet Elemente nicht, aber die Griffe). */
private overlayElements: CADElement[] = [];
/** CAD-14: Setzt die Elemente, deren Auswahl-Griffe im Overlay gezeichnet werden. */
setOverlayElements(els: CADElement[]): void {
this.overlayElements = els;
}
constructor(
canvas: HTMLCanvasElement,
@@ -164,19 +170,19 @@ export class RenderEngine {
if (this.options.backgroundSrc) this.drawBackground();
this.drawRuler(w, h);
// CAD-13-fix: Wenn Pixi-Renderer aktiv ist (skipElementRendering=true),
// zeichnet das Legacy-Canvas KEINE Elemente mehr (sonst doppelt) —
// nur Grid/Hintergrund/Lineal bleiben hier.
if (this.skipElementRendering) {
this.ctx.restore();
return;
}
// CAD-14: skipElementRendering = Pixi rendert die Elemente. Das Legacy-
// Canvas zeichnet KEINE Elemente mehr — ABER weiterhin alle Overlays
// (Vorschau, Auswahl-Griffe, Auswahl-Rechteck, Snap-Punkte, Ortho),
// sonst waere Auswahl im Pixi-Modus unsichtbar.
const skipElements = this.skipElementRendering;
const viewport = this.zoomPan.getViewport();
const visibleElements = this.spatialIndex.search({
minX: viewport.minX, minY: viewport.minY,
maxX: viewport.maxX, maxY: viewport.maxY,
});
const visibleElements = skipElements
? []
: this.spatialIndex.search({
minX: viewport.minX, minY: viewport.minY,
maxX: viewport.maxX, maxY: viewport.maxY,
});
const layers = this.layerManager.getLayers();
for (const layer of layers) {
@@ -187,6 +193,21 @@ export class RenderEngine {
}
}
// CAD-14: Auswahl-Griffe auch fuer Pixi-gerenderte Elemente weiterhin
// auf dem Overlay-Canvas zeigen (Elemente selbst kommen von Pixi).
if (skipElements && this.selection.selectedIds.size > 0) {
const hs = this.zoomPan.getScale();
const hox = this.zoomPan.getTransform().e;
const hoy = this.zoomPan.getTransform().f;
const hsx = (val: number) => val * hs + hox;
const hsy = (val: number) => val * hs + hoy;
for (const el of this.overlayElements) {
if (this.selection.selectedIds.has(el.id)) {
this.drawSelectionHandles(el, hsx, hsy, hs);
}
}
}
// Draw preview element with dashed style
if (this.previewElement) {
this.ctx.save();
+8 -6
View File
@@ -304,6 +304,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
interaction.setElements(elements);
spatialIndex.clear();
spatialIndex.bulkInsert(elements);
renderEngine.setOverlayElements(elements);
renderEngine.render();
// CAD-13-fix: Bestands-Elemente (aus DB geladen) müssen in Pixi gespiegelt
@@ -447,12 +448,13 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
useEffect(() => {
const interaction = interactionRef.current;
if (!interaction) return;
interaction.setTool(activeTool as ToolType);
// Task A4.1: V2-Werkzeuge zusätzlich an den Dispatcher routen (Flag aktiv).
// Legacy bleibt parallel aktiv für noch nicht migrierte Tools.
dispatcherRef.current?.setActive(
pluginRegistry.getToolV2(activeTool) ? activeTool : null,
);
// CAD-14: EXKLUSIVITAET — V2-Werkzeuge laufen AUSSCHLIESSLICH ueber den
// InteractionDispatcher. Die Legacy-Engine bekommt sie nicht mehr
// (vorher hatten BEIDE dasselbe Tool aktiv => jeder Klick erzeugte
// mehrfach Elemente). Nur Tools ohne V2-Entsprechung laufen legacy.
const isV2 = !!pluginRegistry.getToolV2(activeTool);
interaction.setTool(isV2 ? null : (activeTool as ToolType));
dispatcherRef.current?.setActive(isV2 ? activeTool : null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTool]);
+3 -3
View File
@@ -14,7 +14,7 @@ import { formatDistance, type UnitType } from '../utils/format';
export type ToolPhase = 'idle' | 'drawing' | 'modifying' | 'panning' | 'selecting';
export interface ToolState {
activeTool: ToolType;
activeTool: ToolType | null;
phase: ToolPhase;
points: Array<{ x: number; y: number }>; // accumulated world points for current operation
previewElement: CADElement | null; // temporary element being drawn
@@ -138,13 +138,13 @@ export class InteractionEngine {
this.selectedTemplate = templateName;
}
setTool(tool: ToolType): void {
setTool(tool: ToolType | null): void {
this.state.activeTool = tool;
this.state.phase = 'idle';
this.state.points = [];
this.state.previewElement = null;
const drawingTools: ToolType[] = ['line', 'rect', 'circle', 'arc', 'polyline', 'polygon', 'revcloud', 'text', 'dimension', 'leader', 'chair', 'seating-row', 'seating-block', 'table', 'stage', 'seating-template', 'hatch'];
if (drawingTools.includes(tool)) {
if (tool && drawingTools.includes(tool)) {
this.selectionEngine.clearSelection();
this.emitSelectionChange();
}