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:
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* CAD-14 Beweis-Test: Der Workflow, den der Nutzer jeden Tag macht.
|
||||||
|
* 1. Werkzeug waehlen (Sidebar), 2. zeichnen (2 Klicks),
|
||||||
|
* 3. EXAKT EIN Element an der geklickten Position erwarten.
|
||||||
|
* ROT-Zustand (jetzt): 2 Elemente (Legacy + V2 je eins) = Doppelung belegt.
|
||||||
|
* GRUEN nach Schnitt: 1 Element, Position = Klickposition.
|
||||||
|
*/
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
const BASE_URL = process.env.FRONTEND_URL || 'https://web-cad-neu.server.media-on.de';
|
||||||
|
|
||||||
|
interface VerifyWindow {
|
||||||
|
__v2Debug?: { info: () => { cachedElements: number; ready: boolean } | null };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('zeichnen erzeugt GENAU EIN Element an der Klick-Position', async ({ page }) => {
|
||||||
|
await page.goto(BASE_URL);
|
||||||
|
await page.fill('input[type=email]', 'admin@media-on.de');
|
||||||
|
await page.fill('input[type=password]', 'WebCAD2026!');
|
||||||
|
await page.click('button[type=submit]');
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// Neues leeres Projekt anlegen (damit der Count deterministisch ist)
|
||||||
|
const newBtn = page.locator('button:has-text("Neu"), [title*="Neues Projekt"]').first();
|
||||||
|
const cards = await page.$$('.dashboard-project-card, [class*=project-card]');
|
||||||
|
if (cards.length === 0) { await newBtn.click(); } // Dashboard-Variante
|
||||||
|
// Falls Dashboard: einfach das erste Projekt oeffnen und Delta messen
|
||||||
|
if (cards.length > 0) { await cards[0].click(); }
|
||||||
|
await page.waitForTimeout(2500);
|
||||||
|
|
||||||
|
const verify = () => page.evaluate(() => {
|
||||||
|
const w = window as unknown as VerifyWindow;
|
||||||
|
return w.__v2Debug?.info?.() ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const before = await verify();
|
||||||
|
const beforeCount = before?.cachedElements ?? -1;
|
||||||
|
expect(beforeCount, 'Pixi muss ready sein').toBeGreaterThanOrEqual(0);
|
||||||
|
|
||||||
|
// Linie-Werkzeug aus der SIDEBAR (der richtige Ort)
|
||||||
|
await page.locator('[data-tool="line"]').first().click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// Auf die Zeichenflaeche klicken: zwei Klicks = eine Linie
|
||||||
|
const canvas = page.locator('canvas').last();
|
||||||
|
const box = await canvas.boundingBox();
|
||||||
|
if (!box) throw new Error('Kein Canvas gefunden');
|
||||||
|
const cx = box.x + box.width / 2;
|
||||||
|
const cy = box.y + box.height / 2;
|
||||||
|
await page.mouse.click(cx, cy);
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.mouse.click(cx + 120, cy + 60);
|
||||||
|
await page.waitForTimeout(600);
|
||||||
|
|
||||||
|
const after = await verify();
|
||||||
|
const afterCount = after?.cachedElements ?? -1;
|
||||||
|
|
||||||
|
// ABNAHME: genau +1 Element (jetzt faehlt der Test, weil 2 entstehen)
|
||||||
|
expect(afterCount - beforeCount, `Erwartet genau 1 neues Element, got ${afterCount - beforeCount}`).toBe(1);
|
||||||
|
});
|
||||||
@@ -57,6 +57,12 @@ export class RenderEngine {
|
|||||||
private dpr = 1;
|
private dpr = 1;
|
||||||
/** CAD-13-fix: true = Legacy-Canvas zeichnet KEINE Elemente (Pixi-Overlay zeichnet sie). Grid/Hintergrund bleiben. */
|
/** CAD-13-fix: true = Legacy-Canvas zeichnet KEINE Elemente (Pixi-Overlay zeichnet sie). Grid/Hintergrund bleiben. */
|
||||||
skipElementRendering = false;
|
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(
|
constructor(
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
@@ -164,16 +170,16 @@ export class RenderEngine {
|
|||||||
if (this.options.backgroundSrc) this.drawBackground();
|
if (this.options.backgroundSrc) this.drawBackground();
|
||||||
this.drawRuler(w, h);
|
this.drawRuler(w, h);
|
||||||
|
|
||||||
// CAD-13-fix: Wenn Pixi-Renderer aktiv ist (skipElementRendering=true),
|
// CAD-14: skipElementRendering = Pixi rendert die Elemente. Das Legacy-
|
||||||
// zeichnet das Legacy-Canvas KEINE Elemente mehr (sonst doppelt) —
|
// Canvas zeichnet KEINE Elemente mehr — ABER weiterhin alle Overlays
|
||||||
// nur Grid/Hintergrund/Lineal bleiben hier.
|
// (Vorschau, Auswahl-Griffe, Auswahl-Rechteck, Snap-Punkte, Ortho),
|
||||||
if (this.skipElementRendering) {
|
// sonst waere Auswahl im Pixi-Modus unsichtbar.
|
||||||
this.ctx.restore();
|
const skipElements = this.skipElementRendering;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewport = this.zoomPan.getViewport();
|
const viewport = this.zoomPan.getViewport();
|
||||||
const visibleElements = this.spatialIndex.search({
|
const visibleElements = skipElements
|
||||||
|
? []
|
||||||
|
: this.spatialIndex.search({
|
||||||
minX: viewport.minX, minY: viewport.minY,
|
minX: viewport.minX, minY: viewport.minY,
|
||||||
maxX: viewport.maxX, maxY: viewport.maxY,
|
maxX: viewport.maxX, maxY: viewport.maxY,
|
||||||
});
|
});
|
||||||
@@ -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
|
// Draw preview element with dashed style
|
||||||
if (this.previewElement) {
|
if (this.previewElement) {
|
||||||
this.ctx.save();
|
this.ctx.save();
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
interaction.setElements(elements);
|
interaction.setElements(elements);
|
||||||
spatialIndex.clear();
|
spatialIndex.clear();
|
||||||
spatialIndex.bulkInsert(elements);
|
spatialIndex.bulkInsert(elements);
|
||||||
|
renderEngine.setOverlayElements(elements);
|
||||||
renderEngine.render();
|
renderEngine.render();
|
||||||
|
|
||||||
// CAD-13-fix: Bestands-Elemente (aus DB geladen) müssen in Pixi gespiegelt
|
// CAD-13-fix: Bestands-Elemente (aus DB geladen) müssen in Pixi gespiegelt
|
||||||
@@ -447,12 +448,13 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interaction = interactionRef.current;
|
const interaction = interactionRef.current;
|
||||||
if (!interaction) return;
|
if (!interaction) return;
|
||||||
interaction.setTool(activeTool as ToolType);
|
// CAD-14: EXKLUSIVITAET — V2-Werkzeuge laufen AUSSCHLIESSLICH ueber den
|
||||||
// Task A4.1: V2-Werkzeuge zusätzlich an den Dispatcher routen (Flag aktiv).
|
// InteractionDispatcher. Die Legacy-Engine bekommt sie nicht mehr
|
||||||
// Legacy bleibt parallel aktiv für noch nicht migrierte Tools.
|
// (vorher hatten BEIDE dasselbe Tool aktiv => jeder Klick erzeugte
|
||||||
dispatcherRef.current?.setActive(
|
// mehrfach Elemente). Nur Tools ohne V2-Entsprechung laufen legacy.
|
||||||
pluginRegistry.getToolV2(activeTool) ? activeTool : null,
|
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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [activeTool]);
|
}, [activeTool]);
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { formatDistance, type UnitType } from '../utils/format';
|
|||||||
export type ToolPhase = 'idle' | 'drawing' | 'modifying' | 'panning' | 'selecting';
|
export type ToolPhase = 'idle' | 'drawing' | 'modifying' | 'panning' | 'selecting';
|
||||||
|
|
||||||
export interface ToolState {
|
export interface ToolState {
|
||||||
activeTool: ToolType;
|
activeTool: ToolType | null;
|
||||||
phase: ToolPhase;
|
phase: ToolPhase;
|
||||||
points: Array<{ x: number; y: number }>; // accumulated world points for current operation
|
points: Array<{ x: number; y: number }>; // accumulated world points for current operation
|
||||||
previewElement: CADElement | null; // temporary element being drawn
|
previewElement: CADElement | null; // temporary element being drawn
|
||||||
@@ -138,13 +138,13 @@ export class InteractionEngine {
|
|||||||
this.selectedTemplate = templateName;
|
this.selectedTemplate = templateName;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTool(tool: ToolType): void {
|
setTool(tool: ToolType | null): void {
|
||||||
this.state.activeTool = tool;
|
this.state.activeTool = tool;
|
||||||
this.state.phase = 'idle';
|
this.state.phase = 'idle';
|
||||||
this.state.points = [];
|
this.state.points = [];
|
||||||
this.state.previewElement = null;
|
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'];
|
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.selectionEngine.clearSelection();
|
||||||
this.emitSelectionChange();
|
this.emitSelectionChange();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user