240 lines
6.7 KiB
TypeScript
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}`;
|
|
}
|
|
}
|