feat: initial commit web-cad-neu with docker-compose, frontend and backend
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* 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)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user