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
+216 -5
View File
@@ -18,6 +18,12 @@ export interface RenderOptions {
backgroundOffsetY: number;
backgroundRotation: number;
backgroundOpacity: number;
unit?: 'mm' | 'cm' | 'm';
scaleFactor?: number;
showGridLabels?: boolean;
canvasBgColor?: string;
gridColor?: string;
rulerEnabled?: boolean;
}
export interface SelectionState {
@@ -30,7 +36,7 @@ export interface SelectionState {
export interface SnapPoint {
x: number;
y: number;
type: 'endpoint' | 'midpoint' | 'center' | 'intersection' | 'nearest';
type: 'endpoint' | 'midpoint' | 'center' | 'intersection' | 'nearest' | 'grid' | 'perpendicular' | 'tangent' | 'quadrant' | 'none';
}
export class RenderEngine {
@@ -70,6 +76,9 @@ export class RenderEngine {
backgroundOffsetY: 0,
backgroundRotation: 0,
backgroundOpacity: 0.5,
canvasBgColor: '#1e1e2e',
gridColor: '#3a3a4a',
rulerEnabled: true,
};
this.selection = {
selectedIds: new Set(),
@@ -132,16 +141,22 @@ export class RenderEngine {
}
}
private groups: Array<{ id: string; name: string; elementIds: string[]; parentGroupId: string | null }> = [];
setGroups(groups: Array<{ id: string; name: string; elementIds: string[]; parentGroupId: string | null }>): void {
this.groups = groups;
}
render(): void {
const w = this.canvas.width / this.dpr;
const h = this.canvas.height / this.dpr;
this.ctx.save();
this.ctx.fillStyle = '#1e1e2e';
this.ctx.fillStyle = this.options.canvasBgColor || '#1e1e2e';
this.ctx.fillRect(0, 0, w, h);
if (this.options.showGrid) this.drawGrid(w, h);
if (this.options.backgroundSrc) this.drawBackground();
this.drawRuler(w, h);
const viewport = this.zoomPan.getViewport();
const visibleElements = this.spatialIndex.search({
minX: viewport.minX, minY: viewport.minY,
@@ -168,6 +183,7 @@ export class RenderEngine {
}
this.drawSelectionBox();
this.drawGroupBoxes();
if (this.options.showSnapPoints) this.drawSnapPoints();
if (this.options.showOrtho) this.drawOrtho();
this.ctx.restore();
@@ -180,7 +196,7 @@ export class RenderEngine {
const offsetX = this.zoomPan.getTransform().e % gridSize;
const offsetY = this.zoomPan.getTransform().f % gridSize;
this.ctx.strokeStyle = '#2a2a3e';
this.ctx.strokeStyle = this.options.gridColor || '#3a3a4a';
this.ctx.lineWidth = 1;
this.ctx.beginPath();
for (let x = offsetX; x < w; x += gridSize) {
@@ -198,7 +214,8 @@ export class RenderEngine {
if (majorSize >= 20) {
const majOffX = this.zoomPan.getTransform().e % majorSize;
const majOffY = this.zoomPan.getTransform().f % majorSize;
this.ctx.strokeStyle = '#33334a';
// Major lines use a brighter variant of gridColor
this.ctx.strokeStyle = this.lightenColor(this.options.gridColor || '#3a3a4a', 15);
this.ctx.beginPath();
for (let x = majOffX; x < w; x += majorSize) {
this.ctx.moveTo(x, 0);
@@ -209,9 +226,158 @@ export class RenderEngine {
this.ctx.lineTo(w, y);
}
this.ctx.stroke();
// Draw axis labels with unit at each major grid line
if (this.options.showGridLabels !== false) {
const unit = this.options.unit || 'mm';
const scaleFactor = this.options.scaleFactor || 1;
const transform = this.zoomPan.getTransform();
const scale = this.zoomPan.getScale();
const worldGridSize = this.options.gridSize * 5; // world units per major line
this.ctx.fillStyle = '#666680';
this.ctx.font = '10px sans-serif';
this.ctx.textBaseline = 'top';
// X-axis labels (top edge)
const xStartWorld = Math.floor((-transform.e / scale) / worldGridSize) * worldGridSize;
for (let i = 0; i * majorSize + majOffX < w; i++) {
const screenX = majOffX + i * majorSize;
const worldX = xStartWorld + i * worldGridSize;
if (screenX < 30) continue; // skip too-close-to-edge labels
const label = this.formatGridLabel(worldX, unit, scaleFactor);
this.ctx.fillText(label, screenX + 2, 2);
}
// Y-axis labels (left edge)
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'middle';
const yStartWorld = Math.floor((-transform.f / scale) / worldGridSize) * worldGridSize;
for (let i = 0; i * majorSize + majOffY < h; i++) {
const screenY = majOffY + i * majorSize;
const worldY = yStartWorld + i * worldGridSize;
if (screenY < 15) continue;
const label = this.formatGridLabel(worldY, unit, scaleFactor);
this.ctx.fillText(label, 2, screenY - 6);
}
this.ctx.textAlign = 'start';
this.ctx.textBaseline = 'alphabetic';
}
}
}
private formatGridLabel(worldVal: number, unit: 'mm' | 'cm' | 'm', scaleFactor: number): string {
const mm = worldVal * scaleFactor;
switch (unit) {
case 'mm':
return `${mm.toFixed(0)}mm`;
case 'cm':
return `${(mm / 10).toFixed(0)}cm`;
case 'm':
return `${(mm / 1000).toFixed(2)}m`;
}
}
private lightenColor(hex: string, percent: number): string {
const num = parseInt(hex.replace('#', ''), 16);
if (isNaN(num)) return hex;
const r = Math.min(255, (num >> 16) + Math.round(255 * percent / 100));
const g = Math.min(255, ((num >> 8) & 0x00FF) + Math.round(255 * percent / 100));
const b = Math.min(255, (num & 0x0000FF) + Math.round(255 * percent / 100));
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
}
private drawRuler(w: number, h: number): void {
if (!this.options.rulerEnabled) return;
const scale = this.zoomPan.getScale();
const transform = this.zoomPan.getTransform();
const unit = this.options.unit || 'mm';
const scaleFactor = this.options.scaleFactor || 1;
const rulerSize = 20;
this.ctx.save();
this.ctx.fillStyle = this.options.canvasBgColor || '#1e1e2e';
this.ctx.fillRect(0, 0, w, rulerSize);
this.ctx.fillRect(0, 0, rulerSize, h);
this.ctx.strokeStyle = this.lightenColor(this.options.gridColor || '#3a3a4a', 30);
this.ctx.lineWidth = 1;
this.ctx.beginPath();
this.ctx.moveTo(0, rulerSize);
this.ctx.lineTo(w, rulerSize);
this.ctx.moveTo(rulerSize, 0);
this.ctx.lineTo(rulerSize, h);
this.ctx.stroke();
// Determine tick spacing in world units (aim for ~50px between major ticks)
const targetPx = 50;
const worldPerPx = 1 / scale;
const rawTickSpacing = targetPx * worldPerPx;
const niceSpacings = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000];
let tickSpacing = niceSpacings[0];
for (const ns of niceSpacings) {
if (ns >= rawTickSpacing) { tickSpacing = ns; break; }
tickSpacing = ns;
}
const tickPx = tickSpacing * scale;
this.ctx.fillStyle = this.lightenColor(this.options.gridColor || '#3a3a4a', 50);
this.ctx.font = '9px sans-serif';
this.ctx.textBaseline = 'middle';
// X-axis ruler (top)
this.ctx.textAlign = 'center';
const xStartWorld = Math.floor((-transform.e / scale) / tickSpacing) * tickSpacing;
for (let i = 0; ; i++) {
const screenX = transform.e + (xStartWorld + i * tickSpacing) * scale;
if (screenX < rulerSize) continue;
if (screenX > w) break;
// Tick mark
this.ctx.strokeStyle = this.lightenColor(this.options.gridColor || '#3a3a4a', 30);
this.ctx.beginPath();
this.ctx.moveTo(screenX, rulerSize - 6);
this.ctx.lineTo(screenX, rulerSize);
this.ctx.stroke();
// Label
const worldVal = (xStartWorld + i * tickSpacing) * scaleFactor;
let label: string;
switch (unit) {
case 'mm': label = `${worldVal.toFixed(0)}`; break;
case 'cm': label = `${(worldVal / 10).toFixed(0)}`; break;
case 'm': label = `${(worldVal / 1000).toFixed(2)}`; break;
}
this.ctx.fillText(label, screenX, rulerSize / 2);
}
// Y-axis ruler (left)
this.ctx.textAlign = 'right';
this.ctx.textBaseline = 'middle';
const yStartWorld = Math.floor((-transform.f / scale) / tickSpacing) * tickSpacing;
for (let i = 0; ; i++) {
const screenY = transform.f + (yStartWorld + i * tickSpacing) * scale;
if (screenY < rulerSize) continue;
if (screenY > h) break;
this.ctx.strokeStyle = this.lightenColor(this.options.gridColor || '#3a3a4a', 30);
this.ctx.beginPath();
this.ctx.moveTo(rulerSize - 6, screenY);
this.ctx.lineTo(rulerSize, screenY);
this.ctx.stroke();
const worldVal = (yStartWorld + i * tickSpacing) * scaleFactor;
let label: string;
switch (unit) {
case 'mm': label = `${worldVal.toFixed(0)}`; break;
case 'cm': label = `${(worldVal / 10).toFixed(0)}`; break;
case 'm': label = `${(worldVal / 1000).toFixed(2)}`; break;
}
this.ctx.fillText(label, rulerSize - 2, screenY);
}
this.ctx.restore();
this.ctx.textAlign = 'start';
this.ctx.textBaseline = 'alphabetic';
}
private drawBackground(): void {
// Background image rendering with transform
const img = new Image();
@@ -825,6 +991,51 @@ export class RenderEngine {
}
}
private drawGroupBoxes(): void {
if (this.groups.length === 0) return;
const s = this.zoomPan.getScale();
const ox = this.zoomPan.getTransform().e;
const oy = this.zoomPan.getTransform().f;
const visibleElements = this.spatialIndex.search(this.zoomPan.getViewport());
const elementMap = new Map(visibleElements.map(e => [e.id, e]));
for (const group of this.groups) {
const els = group.elementIds.map(id => elementMap.get(id)).filter((e): e is CADElement => e !== undefined);
if (els.length === 0) continue;
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const el of els) {
const bb = this.getElementBBox(el);
minX = Math.min(minX, bb.minX);
minY = Math.min(minY, bb.minY);
maxX = Math.max(maxX, bb.maxX);
maxY = Math.max(maxY, bb.maxY);
}
if (minX === Infinity) continue;
const sx1 = minX * s + ox;
const sy1 = minY * s + oy;
const sx2 = maxX * s + ox;
const sy2 = maxY * s + oy;
const pad = 4;
this.ctx.save();
this.ctx.strokeStyle = '#ff9800';
this.ctx.lineWidth = 1.5;
this.ctx.setLineDash([6, 4]);
this.ctx.strokeRect(sx1 - pad, sy1 - pad, (sx2 - sx1) + pad * 2, (sy2 - sy1) + pad * 2);
this.ctx.setLineDash([]);
// Group name label
this.ctx.font = '11px sans-serif';
this.ctx.fillStyle = '#ff9800';
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'top';
this.ctx.fillText(group.name, sx1 - pad + 2, sy1 - pad - 14);
this.ctx.restore();
}
}
private drawSelectionBox(): void {
if (!this.selection.boxStart || !this.selection.boxEnd) return;
const s = this.zoomPan.getScale();