merge: combine all features from both codebases - mobile dashboard + layer panel + notifications + shares + all fixes
This commit is contained in:
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user