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

212 lines
7.1 KiB
TypeScript
Raw Normal View History

/**
* Import Service handles DXF, SVG, PDF, JSON imports
*/
import { parseDXF, type DXFImportResult } from './dxfParser';
import type { CADElement, CADLayer, BlockDefinition, ProjectData } from '../types/cad.types';
export interface ImportResult {
success: boolean;
elements: CADElement[];
layers?: CADLayer[];
blocks?: BlockDefinition[];
warnings: string[];
error?: string;
}
let idCounter = 0;
const nextId = () => `imp-${Date.now()}-${idCounter++}`;
/**
* Import a file based on its extension.
*/
export async function importFile(file: File): Promise<ImportResult> {
const ext = file.name.split('.').pop()?.toLowerCase();
const text = await file.text();
switch (ext) {
case 'dxf':
return importDXF(text);
case 'svg':
return importSVG(text);
case 'json':
return importJSON(text);
case 'pdf':
return { success: false, elements: [], warnings: ['PDF import not yet supported'] };
default:
return { success: false, elements: [], warnings: [`Unsupported format: ${ext}`] };
}
}
/**
* Import DXF string.
*/
export function importDXF(dxfString: string): ImportResult {
try {
const result: DXFImportResult = parseDXF(dxfString);
return {
success: true,
elements: result.elements,
layers: result.layers,
warnings: result.warnings,
};
} catch (err) {
return {
success: false,
elements: [],
warnings: [],
error: `DXF parse error: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
/**
* Import SVG string — converts SVG elements to CAD elements.
*/
export function importSVG(svgString: string): ImportResult {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(svgString, 'image/svg+xml');
const svg = doc.documentElement;
const elements: CADElement[] = [];
const warnings: string[] = [];
// Parse SVG viewBox for coordinate mapping
const viewBox = svg.getAttribute('viewBox');
let vbX = 0, vbY = 0;
if (viewBox) {
const parts = viewBox.split(/[\s,]+/).map(Number);
vbX = parts[0] || 0;
vbY = parts[1] || 0;
}
// Process SVG elements
const processElement = (node: Element) => {
const tag = node.tagName.toLowerCase();
const stroke = node.getAttribute('stroke') || '#ffffff';
const strokeWidth = parseFloat(node.getAttribute('stroke-width') || '1');
const fill = node.getAttribute('fill') || 'none';
switch (tag) {
case 'line': {
const x1 = parseFloat(node.getAttribute('x1') || '0') - vbX;
const y1 = parseFloat(node.getAttribute('y1') || '0') - vbY;
const x2 = parseFloat(node.getAttribute('x2') || '0') - vbX;
const y2 = parseFloat(node.getAttribute('y2') || '0') - vbY;
elements.push({
id: nextId(), type: 'line', layerId: 'layer-0',
x: Math.min(x1, x2), y: Math.min(y1, y2),
width: Math.abs(x2 - x1), height: Math.abs(y2 - y1),
properties: { stroke, strokeWidth, x1, y1, x2, y2 },
});
break;
}
case 'rect': {
const x = parseFloat(node.getAttribute('x') || '0') - vbX;
const y = parseFloat(node.getAttribute('y') || '0') - vbY;
const w = parseFloat(node.getAttribute('width') || '0');
const h = parseFloat(node.getAttribute('height') || '0');
elements.push({
id: nextId(), type: 'rect', layerId: 'layer-0',
x, y, width: w, height: h,
properties: { stroke, strokeWidth, fill: fill !== 'none' ? fill : undefined },
});
break;
}
case 'circle': {
const cx = parseFloat(node.getAttribute('cx') || '0') - vbX;
const cy = parseFloat(node.getAttribute('cy') || '0') - vbY;
const r = parseFloat(node.getAttribute('r') || '0');
elements.push({
id: nextId(), type: 'circle', layerId: 'layer-0',
x: cx - r, y: cy - r, width: r * 2, height: r * 2,
properties: { stroke, strokeWidth, radius: r },
});
break;
}
case 'ellipse': {
const cx = parseFloat(node.getAttribute('cx') || '0') - vbX;
const cy = parseFloat(node.getAttribute('cy') || '0') - vbY;
const rx = parseFloat(node.getAttribute('rx') || '0');
const ry = parseFloat(node.getAttribute('ry') || '0');
elements.push({
id: nextId(), type: 'circle', layerId: 'layer-0',
x: cx - rx, y: cy - ry, width: rx * 2, height: ry * 2,
properties: { stroke, strokeWidth, radius: Math.max(rx, ry) },
});
break;
}
case 'polyline':
case 'polygon': {
const ptsStr = node.getAttribute('points') || '';
const pts = ptsStr.trim().split(/[\s,]+/).reduce((acc: Array<{x:number;y:number}>, val: string, idx: number) => {
if (idx % 2 === 0) acc.push({ x: parseFloat(val) - vbX, y: 0 });
else acc[acc.length - 1].y = parseFloat(val) - vbY;
return acc;
}, []);
if (pts.length >= 2) {
const minX = Math.min(...pts.map(p => p.x));
const minY = Math.min(...pts.map(p => p.y));
const maxX = Math.max(...pts.map(p => p.x));
const maxY = Math.max(...pts.map(p => p.y));
elements.push({
id: nextId(), type: tag === 'polygon' ? 'polygon' : 'polyline', layerId: 'layer-0',
x: minX, y: minY, width: maxX - minX, height: maxY - minY,
properties: { stroke, strokeWidth, points: pts },
});
}
break;
}
case 'text': {
const x = parseFloat(node.getAttribute('x') || '0') - vbX;
const y = parseFloat(node.getAttribute('y') || '0') - vbY;
const fontSize = parseFloat(node.getAttribute('font-size') || '12');
const text = node.textContent || '';
elements.push({
id: nextId(), type: 'text', layerId: 'layer-0',
x, y, width: 0, height: 0,
properties: { stroke, strokeWidth, text, fontSize },
});
break;
}
case 'g': {
// Process group children
Array.from(node.children).forEach(processElement);
break;
}
default:
warnings.push(`Unsupported SVG element: ${tag}`);
}
};
Array.from(svg.children).forEach(processElement);
return { success: true, elements, warnings };
} catch (err) {
return {
success: false, elements: [], warnings: [],
error: `SVG parse error: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
/**
* Import JSON project file.
*/
export function importJSON(jsonString: string): ImportResult {
try {
const data: ProjectData = JSON.parse(jsonString);
return {
success: true,
elements: data.elements || [],
layers: data.layers || [],
blocks: data.blocks || [],
warnings: [],
};
} catch (err) {
return {
success: false, elements: [], warnings: [],
error: `JSON parse error: ${err instanceof Error ? err.message : String(err)}`,
};
}
}