187 lines
5.9 KiB
TypeScript
187 lines
5.9 KiB
TypeScript
|
|
/**
|
|||
|
|
* DXF Parser – converts DXF entities to CADElement[]
|
|||
|
|
* Uses dxf-parser library
|
|||
|
|
*/
|
|||
|
|
import DxfParser from 'dxf-parser';
|
|||
|
|
import type { CADElement, CADLayer, CADProperties, ElementType } from '../types/cad.types';
|
|||
|
|
|
|||
|
|
export interface DXFImportResult {
|
|||
|
|
elements: CADElement[];
|
|||
|
|
layers: CADLayer[];
|
|||
|
|
warnings: string[];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let idCounter = 0;
|
|||
|
|
const nextId = () => `dxf-${Date.now()}-${idCounter++}`;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Parse a DXF string into CAD elements and layers.
|
|||
|
|
*/
|
|||
|
|
export function parseDXF(dxfString: string): DXFImportResult {
|
|||
|
|
const parser = new DxfParser();
|
|||
|
|
const dxf = parser.parseSync(dxfString) as any;
|
|||
|
|
if (!dxf) return { elements: [], layers: [], warnings: ['DXF parse returned null'] };
|
|||
|
|
const warnings: string[] = [];
|
|||
|
|
const layerMap = new Map<string, CADLayer>();
|
|||
|
|
const elements: CADElement[] = [];
|
|||
|
|
|
|||
|
|
// Build layers from DXF tables
|
|||
|
|
if (dxf.tables?.layer?.layers) {
|
|||
|
|
let sortOrder = 0;
|
|||
|
|
for (const [layerName, layerData] of Object.entries(dxf.tables.layer.layers) as [string, any][]) {
|
|||
|
|
const layer: CADLayer = {
|
|||
|
|
id: `dxf-layer-${layerName}`,
|
|||
|
|
name: layerName,
|
|||
|
|
visible: true,
|
|||
|
|
locked: false,
|
|||
|
|
color: dxfColorToHex(layerData.color) ?? '#ffffff',
|
|||
|
|
lineType: mapLineType(layerData.lineType),
|
|||
|
|
transparency: 0,
|
|||
|
|
sortOrder: sortOrder++,
|
|||
|
|
parentId: null,
|
|||
|
|
};
|
|||
|
|
layerMap.set(layerName, layer);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Ensure a default layer exists
|
|||
|
|
if (layerMap.size === 0) {
|
|||
|
|
layerMap.set('0', {
|
|||
|
|
id: 'dxf-layer-0', name: '0', visible: true, locked: false,
|
|||
|
|
color: '#ffffff', lineType: 'solid', transparency: 0, sortOrder: 0, parentId: null,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Parse entities
|
|||
|
|
if (dxf.entities) {
|
|||
|
|
for (const entity of dxf.entities) {
|
|||
|
|
const el = entityToCADElement(entity, layerMap);
|
|||
|
|
if (el) {
|
|||
|
|
elements.push(el);
|
|||
|
|
} else {
|
|||
|
|
warnings.push(`Unsupported entity type: ${entity.type}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { elements, layers: Array.from(layerMap.values()), warnings };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function entityToCADElement(
|
|||
|
|
entity: any,
|
|||
|
|
layerMap: Map<string, CADLayer>,
|
|||
|
|
): CADElement | null {
|
|||
|
|
const layerName = entity.layer || '0';
|
|||
|
|
const layer = layerMap.get(layerName) ?? layerMap.get('0')!;
|
|||
|
|
const color = entity.color ? (dxfColorToHex(entity.color) ?? layer.color) : layer.color;
|
|||
|
|
const baseProps: CADProperties = {
|
|||
|
|
stroke: color,
|
|||
|
|
strokeWidth: 1,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
switch (entity.type) {
|
|||
|
|
case 'LINE': {
|
|||
|
|
const s = entity.vertices?.[0];
|
|||
|
|
const e = entity.vertices?.[1];
|
|||
|
|
if (!s || !e) return null;
|
|||
|
|
const minX = Math.min(s.x, e.x);
|
|||
|
|
const minY = Math.min(s.y, e.y);
|
|||
|
|
const maxX = Math.max(s.x, e.x);
|
|||
|
|
const maxY = Math.max(s.y, e.y);
|
|||
|
|
return {
|
|||
|
|
id: nextId(), type: 'line', layerId: layer.id,
|
|||
|
|
x: minX, y: minY, width: maxX - minX, height: maxY - minY,
|
|||
|
|
properties: { ...baseProps, x1: s.x, y1: s.y, x2: e.x, y2: e.y },
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
case 'CIRCLE': {
|
|||
|
|
const c = entity.center;
|
|||
|
|
const r = entity.radius;
|
|||
|
|
if (!c || r == null) return null;
|
|||
|
|
return {
|
|||
|
|
id: nextId(), type: 'circle', layerId: layer.id,
|
|||
|
|
x: c.x - r, y: c.y - r, width: r * 2, height: r * 2,
|
|||
|
|
properties: { ...baseProps, radius: r },
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
case 'ARC': {
|
|||
|
|
const c = entity.center;
|
|||
|
|
const r = entity.radius;
|
|||
|
|
if (!c || r == null) return null;
|
|||
|
|
return {
|
|||
|
|
id: nextId(), type: 'arc', layerId: layer.id,
|
|||
|
|
x: c.x - r, y: c.y - r, width: r * 2, height: r * 2,
|
|||
|
|
properties: { ...baseProps, radius: r, startAngle: entity.startAngle, endAngle: entity.endAngle },
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
case 'LWPOLYLINE':
|
|||
|
|
case 'POLYLINE': {
|
|||
|
|
const pts = (entity.vertices || []).map((v: any) => ({ x: v.x, y: v.y }));
|
|||
|
|
if (pts.length < 2) return null;
|
|||
|
|
const minX = Math.min(...pts.map((p: any) => p.x));
|
|||
|
|
const minY = Math.min(...pts.map((p: any) => p.y));
|
|||
|
|
const maxX = Math.max(...pts.map((p: any) => p.x));
|
|||
|
|
const maxY = Math.max(...pts.map((p: any) => p.y));
|
|||
|
|
const isClosed = entity.shape === true;
|
|||
|
|
const type: ElementType = isClosed ? 'polygon' : 'polyline';
|
|||
|
|
return {
|
|||
|
|
id: nextId(), type, layerId: layer.id,
|
|||
|
|
x: minX, y: minY, width: maxX - minX, height: maxY - minY,
|
|||
|
|
properties: { ...baseProps, points: pts },
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
case 'TEXT': {
|
|||
|
|
const s = entity.startPoint;
|
|||
|
|
if (!s) return null;
|
|||
|
|
return {
|
|||
|
|
id: nextId(), type: 'text', layerId: layer.id,
|
|||
|
|
x: s.x, y: s.y, width: 0, height: 0,
|
|||
|
|
properties: { ...baseProps, text: entity.text || '', fontSize: entity.height || 12 },
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
case 'INSERT': {
|
|||
|
|
// Block reference — store as block_instance
|
|||
|
|
const pos = entity.position;
|
|||
|
|
if (!pos) return null;
|
|||
|
|
return {
|
|||
|
|
id: nextId(), type: 'block_instance', layerId: layer.id,
|
|||
|
|
x: pos.x, y: pos.y, width: 0, height: 0,
|
|||
|
|
properties: { ...baseProps, blockId: entity.name, scale: entity.scale || 1, rotation: entity.rotation || 0 },
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
case 'DIMENSION': {
|
|||
|
|
// Basic dimension support
|
|||
|
|
const pts = entity.definitionPoint || entity.points;
|
|||
|
|
if (!pts) return null;
|
|||
|
|
return {
|
|||
|
|
id: nextId(), type: 'dimension', layerId: layer.id,
|
|||
|
|
x: 0, y: 0, width: 0, height: 0,
|
|||
|
|
properties: { ...baseProps, points: Array.isArray(pts) ? pts.map((p: any) => ({ x: p.x, y: p.y })) : [] },
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
default:
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* DXF ACI color to hex string
|
|||
|
|
*/
|
|||
|
|
function dxfColorToHex(aci: number | undefined): string | null {
|
|||
|
|
if (aci == null) return null;
|
|||
|
|
const colors: Record<number, string> = {
|
|||
|
|
0: '#ffffff', 1: '#ff0000', 2: '#ffff00', 3: '#00ff00',
|
|||
|
|
4: '#00ffff', 5: '#0000ff', 6: '#ff00ff', 7: '#ffffff',
|
|||
|
|
8: '#808080', 9: '#c0c0c0',
|
|||
|
|
};
|
|||
|
|
return colors[aci] ?? null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function mapLineType(lt: string | undefined): 'solid' | 'dashed' | 'dotted' {
|
|||
|
|
if (!lt) return 'solid';
|
|||
|
|
const u = lt.toUpperCase();
|
|||
|
|
if (u.includes('DASH')) return 'dashed';
|
|||
|
|
if (u.includes('DOT')) return 'dotted';
|
|||
|
|
return 'solid';
|
|||
|
|
}
|