import type { CADElement } from '../types/cad.types'; /** * Dimension & Annotation Service — creates dimension, text, leader, revcloud elements. * Supports linear, angular, radial dimensions and multi-line text. */ export type DimensionType = 'linear' | 'angular' | 'radial'; export interface DimensionConfig { type: DimensionType; x1: number; y1: number; x2: number; y2: number; offsetX: number; offsetY: number; unit: 'm' | 'cm' | 'mm'; precision: number; } export interface TextConfig { text: string; fontSize: number; rotation: number; multiline: boolean; align: 'left' | 'center' | 'right'; } export const DEFAULT_TEXT: TextConfig = { text: '', fontSize: 14, rotation: 0, multiline: false, align: 'left', }; export interface LeaderConfig { x1: number; y1: number; x2: number; y2: number; text: string; fontSize: number; } export const DEFAULT_LEADER: LeaderConfig = { x1: 0, y1: 0, x2: 0, y2: 0, text: '', fontSize: 12, }; export interface RevCloudConfig { points: Array<{ x: number; y: number }>; arcHeight: number; fill: string; stroke: string; } export const DEFAULT_REVCLOUD: RevCloudConfig = { points: [], arcHeight: 8, fill: 'none', stroke: '#e0e0e0', }; export class DimensionService { private generateId(): string { return `el_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; } /** Create a linear dimension between two points */ createLinearDimension( x1: number, y1: number, x2: number, y2: number, layerId: string, config: Partial = {}, ): CADElement { const cfg = { type: 'linear' as DimensionType, x1, y1, x2, y2, offsetX: 0, offsetY: -20, unit: 'm' as const, precision: 2, ...config }; const dist = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); const mx = (x1 + x2) / 2 + cfg.offsetX; const my = (y1 + y2) / 2 + cfg.offsetY; const value = this.formatDistance(dist, cfg.unit, cfg.precision); return { id: this.generateId(), type: 'dimension', layerId, x: mx, y: my, width: dist, height: 20, properties: { x1: cfg.x1, y1: cfg.y1, x2: cfg.x2, y2: cfg.y2, offsetX: cfg.offsetX, offsetY: cfg.offsetY, dimType: 'linear', value, unit: cfg.unit, stroke: '#888', strokeWidth: 1, }, }; } /** Create an angular dimension between three points (vertex, p1, p2) */ createAngularDimension( vx: number, vy: number, x1: number, y1: number, x2: number, y2: number, layerId: string, config: Partial = {}, ): CADElement { const cfg = { type: 'angular' as DimensionType, x1: vx, y1: vy, x2, y2, offsetX: 0, offsetY: -30, unit: 'deg' as any, precision: 1, ...config }; const a1 = Math.atan2(y1 - vy, x1 - vx); const a2 = Math.atan2(y2 - vy, x2 - vx); let angle = Math.abs(a2 - a1) * 180 / Math.PI; if (angle > 180) angle = 360 - angle; const r = 30; const midAngle = (a1 + a2) / 2; const mx = vx + Math.cos(midAngle) * r; const my = vy + Math.sin(midAngle) * r; return { id: this.generateId(), type: 'dimension', layerId, x: mx, y: my, width: r * 2, height: r * 2, properties: { x1: vx, y1: vy, x2, y2, ax1: x1, ay1: y1, ax2: x2, ay2: y2, dimType: 'angular', value: `${angle.toFixed(cfg.precision)}°`, radius: r, stroke: '#888', strokeWidth: 1, }, }; } /** Create a radial dimension for a circle */ createRadialDimension( cx: number, cy: number, radius: number, layerId: string, config: Partial = {}, ): CADElement { const cfg = { type: 'radial' as DimensionType, x1: cx, y1: cy, x2: cx + radius, y2: cy, offsetX: 0, offsetY: 0, unit: 'm' as const, precision: 2, ...config }; const value = `R ${this.formatDistance(radius, cfg.unit, cfg.precision)}`; return { id: this.generateId(), type: 'dimension', layerId, x: cx + radius / 2, y: cy - 15, width: radius, height: 20, properties: { x1: cx, y1: cy, x2: cx + radius, y2: cy, dimType: 'radial', value, unit: cfg.unit, stroke: '#888', strokeWidth: 1, }, }; } /** Create a text element (single or multi-line) */ createText(x: number, y: number, layerId: string, config: Partial = {}): CADElement { const cfg = { ...DEFAULT_TEXT, ...config }; const lines = cfg.text.split('\n'); const height = lines.length * cfg.fontSize * 1.2; const width = Math.max(...lines.map(l => l.length * cfg.fontSize * 0.6), 50); return { id: this.generateId(), type: 'text', layerId, x, y, width, height, properties: { text: cfg.text, fontSize: cfg.fontSize, rotation: cfg.rotation, multiline: cfg.multiline, align: cfg.align, stroke: '#e0e0e0', }, }; } /** Create a leader element (arrow + line + text) */ createLeader(x1: number, y1: number, x2: number, y2: number, layerId: string, config: Partial = {}): CADElement { const cfg = { ...DEFAULT_LEADER, x1, y1, x2, y2, ...config }; return { id: this.generateId(), type: 'leader', layerId, x: cfg.x2, y: cfg.y2, width: Math.abs(cfg.x2 - cfg.x1), height: Math.abs(cfg.y2 - cfg.y1), properties: { x1: cfg.x1, y1: cfg.y1, x2: cfg.x2, y2: cfg.y2, text: cfg.text, fontSize: cfg.fontSize, stroke: '#e0e0e0', strokeWidth: 1, }, }; } /** Create a revision cloud from a polyline of points */ createRevCloud(points: Array<{ x: number; y: number }>, layerId: string, config: Partial = {}): CADElement { const cfg = { ...DEFAULT_REVCLOUD, points, ...config }; const xs = points.map(p => p.x); const ys = points.map(p => p.y); const minX = Math.min(...xs), maxX = Math.max(...xs); const minY = Math.min(...ys), maxY = Math.max(...ys); return { id: this.generateId(), type: 'revcloud', layerId, x: (minX + maxX) / 2, y: (minY + maxY) / 2, width: maxX - minX, height: maxY - minY, properties: { points: cfg.points, arcHeight: cfg.arcHeight, fill: cfg.fill, stroke: cfg.stroke, strokeWidth: 1.5, }, }; } /** Format a distance value with unit and precision */ formatDistance(dist: number, unit: string, precision: number): string { let val = dist; let suffix = ''; switch (unit) { case 'm': val = dist / 100; suffix = ' m'; break; case 'cm': val = dist / 10; suffix = ' cm'; break; case 'mm': val = dist; suffix = ' mm'; break; default: suffix = ''; } return `${val.toFixed(precision)}${suffix}`; } }