feat: initial commit web-cad-neu with docker-compose, frontend and backend

This commit is contained in:
2026-06-26 10:50:24 +02:00
commit 4ec76fe406
102 changed files with 25722 additions and 0 deletions
+214
View File
@@ -0,0 +1,214 @@
/**
* DXF Writer converts CADElement[] to DXF string
* Minimal DXF R12 format for compatibility
*/
import type { CADElement, CADLayer, ProjectData } from '../types/cad.types';
/**
* Generate a DXF R12 string from project data.
*/
export function writeDXF(data: ProjectData): string {
const lines: string[] = [];
const w = (code: number, value: string | number) => {
lines.push(String(code));
lines.push(String(value));
};
// Header
w(0, 'SECTION');
w(2, 'HEADER');
w(9, '$ACADVER'); w(1, 'AC1009'); // R12
w(9, '$INSBASE'); w(10, 0); w(20, 0); w(30, 0);
w(9, '$EXTMIN'); w(10, 0); w(20, 0);
w(9, '$EXTMAX'); w(10, 1000); w(20, 1000);
w(0, 'ENDSEC');
// Tables section
w(0, 'SECTION');
w(2, 'TABLES');
// Layer table
w(0, 'TABLE');
w(2, 'LAYER');
w(70, data.layers.length);
for (const layer of data.layers) {
w(0, 'LAYER');
w(2, layer.name);
w(70, 0);
w(62, hexToACI(layer.color));
w(6, lineTypeToDXF(layer.lineType));
}
w(0, 'ENDTAB');
w(0, 'ENDSEC');
// Entities section
w(0, 'SECTION');
w(2, 'ENTITIES');
for (const el of data.elements) {
const layerName = getLayerName(data.layers, el.layerId);
writeEntity(w, el, layerName);
}
w(0, 'ENDSEC');
// EOF
w(0, 'EOF');
return lines.join('\n');
}
function writeEntity(
w: (code: number, value: string | number) => void,
el: CADElement,
layerName: string,
): void {
const p = el.properties;
const stroke = (p.stroke as string) || '#ffffff';
const aci = hexToACI(stroke);
switch (el.type) {
case 'line': {
w(0, 'LINE');
w(8, layerName);
w(62, aci);
w(10, p.x1 ?? el.x); w(20, p.y1 ?? el.y); w(30, 0);
w(11, p.x2 ?? el.x + el.width); w(21, p.y2 ?? el.y + el.height); w(31, 0);
break;
}
case 'circle': {
const cx = el.x + el.width / 2;
const cy = el.y + el.height / 2;
const r = p.radius ?? el.width / 2;
w(0, 'CIRCLE');
w(8, layerName);
w(62, aci);
w(10, cx); w(20, cy); w(30, 0);
w(40, r);
break;
}
case 'arc': {
const cx = el.x + el.width / 2;
const cy = el.y + el.height / 2;
const r = p.radius ?? el.width / 2;
w(0, 'ARC');
w(8, layerName);
w(62, aci);
w(10, cx); w(20, cy); w(30, 0);
w(40, r);
w(50, radToDeg(p.startAngle ?? 0));
w(51, radToDeg(p.endAngle ?? 360));
break;
}
case 'rect': {
// Rectangle as LWPOLYLINE
w(0, 'LWPOLYLINE');
w(8, layerName);
w(62, aci);
w(90, 4);
w(70, 1); // closed
w(10, el.x); w(20, el.y);
w(10, el.x + el.width); w(20, el.y);
w(10, el.x + el.width); w(20, el.y + el.height);
w(10, el.x); w(20, el.y + el.height);
break;
}
case 'polygon':
case 'polyline': {
const pts = p.points ?? [];
w(0, 'LWPOLYLINE');
w(8, layerName);
w(62, aci);
w(90, pts.length);
w(70, el.type === 'polygon' ? 1 : 0);
for (const pt of pts) {
w(10, pt.x); w(20, pt.y);
}
break;
}
case 'text': {
w(0, 'TEXT');
w(8, layerName);
w(62, aci);
w(10, el.x); w(20, el.y); w(30, 0);
w(40, p.fontSize ?? 12);
w(1, p.text ?? '');
break;
}
case 'dimension': {
const pts = p.points ?? [];
if (pts.length >= 2) {
// Draw dimension as LINE + TEXT
w(0, 'LINE');
w(8, layerName);
w(62, aci);
w(10, pts[0].x); w(20, pts[0].y); w(30, 0);
w(11, pts[1].x); w(21, pts[1].y); w(31, 0);
const midX = (pts[0].x + pts[1].x) / 2;
const midY = (pts[0].y + pts[1].y) / 2;
const dist = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y);
w(0, 'TEXT');
w(8, layerName);
w(62, aci);
w(10, midX); w(20, midY); w(30, 0);
w(40, 12);
w(1, dist.toFixed(2));
}
break;
}
case 'block_instance': {
w(0, 'INSERT');
w(8, layerName);
w(62, aci);
w(2, p.blockId ?? 'BLOCK');
w(10, el.x); w(20, el.y); w(30, 0);
w(41, p.scale ?? 1);
w(42, p.scale ?? 1);
w(50, radToDeg(p.rotation ?? 0));
break;
}
default:
// For chair, seating-row, etc. — export as LWPOLYLINE bounding box
w(0, 'LWPOLYLINE');
w(8, layerName);
w(62, aci);
w(90, 4);
w(70, 1);
w(10, el.x); w(20, el.y);
w(10, el.x + el.width); w(20, el.y);
w(10, el.x + el.width); w(20, el.y + el.height);
w(10, el.x); w(20, el.y + el.height);
break;
}
}
function getLayerName(layers: CADLayer[], layerId: string): string {
return layers.find(l => l.id === layerId)?.name ?? '0';
}
function hexToACI(hex: string): number {
const h = hex.replace('#', '');
const r = parseInt(h.substring(0, 2), 16);
const g = parseInt(h.substring(2, 4), 16);
const b = parseInt(h.substring(4, 6), 16);
// Map common colors to ACI
if (r > 200 && g < 100 && b < 100) return 1; // red
if (r > 200 && g > 200 && b < 100) return 2; // yellow
if (r < 100 && g > 200 && b < 100) return 3; // green
if (r < 100 && g > 200 && b > 200) return 4; // cyan
if (r < 100 && g < 100 && b > 200) return 5; // blue
if (r > 200 && g < 100 && b > 200) return 6; // magenta
if (r > 200 && g > 200 && b > 200) return 7; // white
if (r < 100 && g < 100 && b < 100) return 8; // gray
return 7; // default white
}
function lineTypeToDXF(lt: string): string {
if (lt === 'dashed') return 'DASHED';
if (lt === 'dotted') return 'DOT';
return 'CONTINUOUS';
}
function radToDeg(rad: number): number {
return (rad * 180) / Math.PI;
}