merge: combine all features from both codebases - mobile dashboard + layer panel + notifications + shares + all fixes

This commit is contained in:
Leopoldadmin
2026-07-04 16:43:55 +02:00
parent 0606dbb501
commit be62470b53
79 changed files with 9562 additions and 3132 deletions
+149 -45
View File
@@ -5,9 +5,11 @@ import { SnapEngine } from '../canvas/SnapEngine';
import { SelectionEngine } from '../canvas/SelectionEngine';
import { SpatialIndex } from '../canvas/SpatialIndex';
import { LayerManager } from '../canvas/LayerManager';
import { GroupManager } from '../tools/modification/GroupTool';
import { moveElement, rotateElement, scaleElement, mirrorElement, offsetElement, trimElement, extendElement, filletElements, distance, angleBetween } from '../tools/modification/geometry';
import { SeatingService, SEATING_TEMPLATES } from '../services/seatingService';
import { DimensionService } from '../services/dimensionService';
import { formatDistance, type UnitType } from '../utils/format';
export type ToolPhase = 'idle' | 'drawing' | 'modifying' | 'panning' | 'selecting';
@@ -42,6 +44,7 @@ export class InteractionEngine {
private config: InteractionConfig;
private selectedTemplate: string | null = null;
private allElements: CADElement[] = [];
private groupManager: GroupManager | null = null;
private isMouseDown = false;
private isRightDown = false;
private lastMouseScreen = { x: 0, y: 0 };
@@ -59,6 +62,8 @@ export class InteractionEngine {
private boundMouseMove: EventListener;
private boundMouseUp: EventListener;
private boundPointerCancel: EventListener;
private unit: UnitType = 'mm';
private scaleFactor: number = 1;
constructor(
canvas: HTMLCanvasElement,
@@ -104,6 +109,10 @@ export class InteractionEngine {
this.snapEngine.setElements(elements);
}
setGroupManager(gm: GroupManager | null): void {
this.groupManager = gm;
}
setSnapEnabled(enabled: boolean): void {
this.state.snapEnabled = enabled;
}
@@ -112,6 +121,15 @@ export class InteractionEngine {
this.state.ortho = enabled;
}
setUnits(unit: UnitType, scaleFactor: number): void {
this.unit = unit;
this.scaleFactor = scaleFactor;
}
setGridSize(gridSize: number): void {
this.snapEngine.setConfig({ gridSpacing: gridSize });
}
setPolarEnabled(enabled: boolean): void {
this.snapEngine.setConfig({ polarEnabled: enabled });
}
@@ -125,7 +143,8 @@ export class InteractionEngine {
this.state.phase = 'idle';
this.state.points = [];
this.state.previewElement = null;
if (tool !== 'select' && tool !== 'pan') {
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)) {
this.selectionEngine.clearSelection();
this.emitSelectionChange();
}
@@ -274,9 +293,6 @@ export class InteractionEngine {
case 'hatch':
this.handleHatchDown(final);
break;
case 'zoom-win':
this.handleZoomWinDown(final);
break;
case 'measure':
this.handleMeasureDown(final);
break;
@@ -361,9 +377,26 @@ export class InteractionEngine {
return;
}
// Move/Copy/Rotate/Scale: confirm on mouseUp (press-drag-release)
if (this.state.phase === 'modifying' && this.state.points.length >= 1) {
const tool = this.state.activeTool;
if (tool === 'move' || tool === 'copy' || tool === 'rotate' || tool === 'scale') {
const raw = this.getWorldCoords(e);
const snapped = this.applySnap(raw.x, raw.y);
const final = this.applyOrtho(snapped.x, snapped.y);
this.confirmModify(final);
return;
}
}
if (this.state.activeTool === 'select' && this.selectionEngine.isBoxSelecting()) {
this.selectionEngine.finishBoxSelect(this.allElements);
this.emitSelectionChange();
if (this.selectionEngine.hasBoxMoved()) {
this.selectionEngine.finishBoxSelect(this.allElements);
this.emitSelectionChange();
} else {
// No drag movement — cancel box select (it was just a click, already handled by clickSelect)
this.selectionEngine.cancelBoxSelect();
}
this.requestRender();
return;
}
@@ -436,6 +469,22 @@ export class InteractionEngine {
return;
}
// G = Group selected elements
if (e.key === 'g' && !e.ctrlKey && !e.metaKey && this.state.activeTool === 'select') {
const ids = Array.from(this.selectionEngine.getSelectedIds());
if (ids.length >= 2) {
this.onCommandTrigger?.('group');
}
return;
}
// Ctrl+G = Ungroup
if ((e.ctrlKey || e.metaKey) && e.key === 'g' && this.state.activeTool === 'select') {
e.preventDefault();
this.onCommandTrigger?.('ungroup');
return;
}
// Ctrl+A = select all
if ((e.ctrlKey || e.metaKey) && e.key === this.config.selectAllKey) {
e.preventDefault();
@@ -488,9 +537,42 @@ export class InteractionEngine {
this.selectionEngine.setOptions({ additive, subtractive });
if (!additive && !subtractive) {
// Start potential box select
this.selectionEngine.startBoxSelect(world.x, world.y);
// Hit test first: if an object is clicked, select it directly
const hit = this.renderEngine.hitTest(world.x, world.y, 5);
if (hit) {
// Group-aware click selection: if clicked element is in a group, select all group members
if (this.groupManager) {
const groupElements = this.groupManager.getGroupElements(hit.id);
if (groupElements.length > 1) {
this.selectionEngine.selectByIds(groupElements, false);
this.emitSelectionChange();
return;
}
}
this.selectionEngine.clickSelect(world.x, world.y, this.allElements);
this.emitSelectionChange();
} else {
// No object hit: start potential box select for drag
this.selectionEngine.startBoxSelect(world.x, world.y);
}
} else {
// Group-aware click selection: if clicked element is in a group, select all group members
const hit = this.renderEngine.hitTest(world.x, world.y, 5);
if (hit && this.groupManager) {
const groupElements = this.groupManager.getGroupElements(hit.id);
if (groupElements.length > 1) {
if (subtractive) {
// Remove all group elements from selection
const currentIds = Array.from(this.selectionEngine.getSelectedIds());
const filtered = currentIds.filter(id => !groupElements.includes(id));
this.selectionEngine.selectByIds(filtered, false);
} else {
this.selectionEngine.selectByIds(groupElements, !additive ? false : true);
}
this.emitSelectionChange();
return;
}
}
this.selectionEngine.clickSelect(world.x, world.y, this.allElements);
this.emitSelectionChange();
}
@@ -584,25 +666,6 @@ export class InteractionEngine {
this.requestRender();
}
private handleZoomWinDown(pt: { x: number; y: number }): void {
// Zoom window: two clicks define the zoom area
if (this.state.points.length === 0) {
this.state.points.push(pt);
this.state.phase = 'drawing';
this.onCommandTrigger?.('zoom: click opposite corner');
} else {
const first = this.state.points[0];
const minX = Math.min(first.x, pt.x);
const minY = Math.min(first.y, pt.y);
const maxX = Math.max(first.x, pt.x);
const maxY = Math.max(first.y, pt.y);
this.zoomPan.zoomToRect({ minX, minY, maxX, maxY });
this.state.points = [];
this.state.phase = 'idle';
this.requestRender();
}
}
private handleMeasureDown(pt: { x: number; y: number }): void {
// Measure: two clicks, display distance
if (this.state.points.length === 0) {
@@ -611,9 +674,10 @@ export class InteractionEngine {
this.onCommandTrigger?.('measure: click end point');
} else {
const first = this.state.points[0];
const dist = Math.sqrt((pt.x - first.x) ** 2 + (pt.y - first.y) ** 2);
const rawDist = Math.sqrt((pt.x - first.x) ** 2 + (pt.y - first.y) ** 2);
const angle = (Math.atan2(pt.y - first.y, pt.x - first.x) * 180) / Math.PI;
this.onCommandTrigger?.(`distance: ${dist.toFixed(2)} angle: ${angle.toFixed(1)}°`);
const distStr = formatDistance(rawDist, this.unit, this.scaleFactor);
this.onCommandTrigger?.(`distance: ${distStr} angle: ${angle.toFixed(1)}°`);
this.state.points = [];
this.state.phase = 'idle';
this.requestRender();
@@ -704,8 +768,26 @@ export class InteractionEngine {
return;
}
// Move/Copy/Rotate/Scale/Mirror: require existing selection
// Second click confirms the modification
// Move/Copy/Rotate/Scale: press-drag-release (kein click-click)
if (tool === 'move' || tool === 'copy' || tool === 'rotate' || tool === 'scale') {
if (this.state.phase === 'modifying' && this.state.points.length >= 1) {
// Zweiter Klick — sollte nicht passieren bei drag-release, aber falls doch: bestätigen
this.confirmModify(pt);
return;
}
const selected = this.selectionEngine.getSelectedElements(this.allElements);
if (selected.length === 0) {
this.onCommandTrigger?.('select-first');
return;
}
this.modifySelected = selected;
this.state.phase = 'modifying';
this.state.points.push(pt); // Start-Punkt für drag
this.notifyToolStateChanged();
return;
}
// Mirror: click-click (zwei Punkte für die Spiegelungsachse)
if (this.state.phase === 'modifying' && this.state.points.length >= 1) {
this.confirmModify(pt);
return;
@@ -734,13 +816,17 @@ export class InteractionEngine {
// Show preview of first selected element moved
this.state.previewElement = moveElement(this.modifySelected[0], dx, dy);
} else if (tool === 'rotate') {
const angle = angleBetween(base, pt);
this.state.previewElement = rotateElement(this.modifySelected[0], base.x, base.y, angle);
const objCenter = { x: this.modifySelected[0].x, y: this.modifySelected[0].y };
const startAngle = angleBetween(objCenter, base);
const currentAngle = angleBetween(objCenter, pt);
const rotationDelta = currentAngle - startAngle;
this.state.previewElement = rotateElement(this.modifySelected[0], objCenter.x, objCenter.y, rotationDelta);
} else if (tool === 'scale') {
const dist = distance(base, pt);
const refDist = distance(base, { x: this.modifySelected[0].x, y: this.modifySelected[0].y });
const factor = refDist > 0 ? dist / refDist : 1;
this.state.previewElement = scaleElement(this.modifySelected[0], base.x, base.y, factor, factor);
const objCenter = { x: this.modifySelected[0].x, y: this.modifySelected[0].y };
const startDist = distance(objCenter, base);
const currentDist = distance(objCenter, pt);
const factor = startDist > 0 ? currentDist / startDist : 1;
this.state.previewElement = scaleElement(this.modifySelected[0], objCenter.x, objCenter.y, factor, factor);
} else if (tool === 'mirror') {
// Mirror preview: axis from base point to current mouse position
this.state.previewElement = mirrorElement(this.modifySelected[0], base.x, base.y, pt.x, pt.y);
@@ -755,7 +841,22 @@ export class InteractionEngine {
if (tool === 'move') {
const dx = pt.x - base.x;
const dy = pt.y - base.y;
const modified = this.modifySelected.map(el => moveElement(el, dx, dy));
// Group-aware: expand modifySelected to include group members
let toMove = [...this.modifySelected];
if (this.groupManager) {
const additionalGroupEls: CADElement[] = [];
for (const el of this.modifySelected) {
const groupEls = this.groupManager.getGroupElements(el.id);
for (const gid of groupEls) {
if (!toMove.some(e => e.id === gid)) {
const found = this.allElements.find(e => e.id === gid);
if (found) additionalGroupEls.push(found);
}
}
}
toMove = [...toMove, ...additionalGroupEls];
}
const modified = toMove.map(el => moveElement(el, dx, dy));
this.onElementsModified?.(modified);
} else if (tool === 'copy') {
const dx = pt.x - base.x;
@@ -765,14 +866,18 @@ export class InteractionEngine {
this.onElementCreated?.({ ...copy, id: this.generateId() });
}
} else if (tool === 'rotate') {
const angle = angleBetween(base, pt);
const modified = this.modifySelected.map(el => rotateElement(el, base.x, base.y, angle));
const objCenter = { x: this.modifySelected[0].x, y: this.modifySelected[0].y };
const startAngle = angleBetween(objCenter, base);
const endAngle = angleBetween(objCenter, pt);
const rotationDelta = endAngle - startAngle;
const modified = this.modifySelected.map(el => rotateElement(el, objCenter.x, objCenter.y, rotationDelta));
this.onElementsModified?.(modified);
} else if (tool === 'scale') {
const dist = distance(base, pt);
const refDist = distance(base, { x: this.modifySelected[0].x, y: this.modifySelected[0].y });
const factor = refDist > 0 ? dist / refDist : 1;
const modified = this.modifySelected.map(el => scaleElement(el, base.x, base.y, factor, factor));
const objCenter = { x: this.modifySelected[0].x, y: this.modifySelected[0].y };
const startDist = distance(objCenter, base);
const endDist = distance(objCenter, pt);
const factor = startDist > 0 ? endDist / startDist : 1;
const modified = this.modifySelected.map(el => scaleElement(el, objCenter.x, objCenter.y, factor, factor));
this.onElementsModified?.(modified);
} else if (tool === 'mirror') {
// Mirror axis: base point (first click) to current point (second click)
@@ -957,7 +1062,6 @@ export class InteractionEngine {
};
break;
}
case 'zoom-win':
case 'measure': {
// Show a dashed line preview from first point to cursor
this.state.previewElement = {