Files
web-cad/frontend/src/services/dimensionService.ts
T

240 lines
6.7 KiB
TypeScript

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<DimensionConfig> = {},
): 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<DimensionConfig> = {},
): 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<DimensionConfig> = {},
): 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<TextConfig> = {}): 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<LeaderConfig> = {}): 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<RevCloudConfig> = {}): 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}`;
}
}