2026-06-26 10:50:24 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* DXF Parser – converts DXF entities to CADElement[]
|
2026-08-29 01:17:21 +02:00
|
|
|
|
* Uses dxf-parser library.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Task F5 erweitert die Entity-Abdeckung: ELLIPSE (Polyline-Approx), SPLINE
|
|
|
|
|
|
* (Grad<=3 → de-Boor-Approx), MTEXT (Formatcode-Cleanup), POINT, ATTDEF und
|
|
|
|
|
|
* INSERT mit MINSERT-Arrays. HATCH wird defensiv mapping-fähig gehalten
|
|
|
|
|
|
* (dxf-parser 1.1.x schickt echte HATCH-Entities noch nicht durch).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Neben `warnings` liefert parseDXF einen strukturierten Fehlerreport
|
|
|
|
|
|
* (`skippedEntities`: type + reason je übersprungener Entity).
|
2026-06-26 10:50:24 +02:00
|
|
|
|
*/
|
|
|
|
|
|
import DxfParser from 'dxf-parser';
|
|
|
|
|
|
import type { CADElement, CADLayer, CADProperties, ElementType } from '../types/cad.types';
|
|
|
|
|
|
|
2026-08-29 01:17:21 +02:00
|
|
|
|
export interface SkippedEntity {
|
|
|
|
|
|
type: string;
|
|
|
|
|
|
reason: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 10:50:24 +02:00
|
|
|
|
export interface DXFImportResult {
|
|
|
|
|
|
elements: CADElement[];
|
|
|
|
|
|
layers: CADLayer[];
|
|
|
|
|
|
warnings: string[];
|
2026-08-29 01:17:21 +02:00
|
|
|
|
skippedEntities: SkippedEntity[];
|
2026-06-26 10:50:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let idCounter = 0;
|
|
|
|
|
|
const nextId = () => `dxf-${Date.now()}-${idCounter++}`;
|
|
|
|
|
|
|
2026-08-29 01:17:21 +02:00
|
|
|
|
interface Pt { x: number; y: number }
|
|
|
|
|
|
|
|
|
|
|
|
function bboxOf(pts: Pt[]): { x: number; y: number; width: number; height: number } {
|
|
|
|
|
|
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));
|
|
|
|
|
|
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 10:50:24 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* 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;
|
2026-08-29 01:17:21 +02:00
|
|
|
|
const empty: DXFImportResult = { elements: [], layers: [], warnings: [], skippedEntities: [] };
|
|
|
|
|
|
if (!dxf) return { ...empty, warnings: ['DXF parse returned null'] };
|
2026-06-26 10:50:24 +02:00
|
|
|
|
const warnings: string[] = [];
|
2026-08-29 01:17:21 +02:00
|
|
|
|
const skippedEntities: SkippedEntity[] = [];
|
2026-06-26 10:50:24 +02:00
|
|
|
|
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,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-29 01:17:21 +02:00
|
|
|
|
// Parse entities (INSERT expanded for MINSERT arrays)
|
2026-06-26 10:50:24 +02:00
|
|
|
|
if (dxf.entities) {
|
|
|
|
|
|
for (const entity of dxf.entities) {
|
2026-08-29 01:17:21 +02:00
|
|
|
|
if (entity?.type === 'INSERT') {
|
|
|
|
|
|
elements.push(...expandInsert(entity, layerMap));
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-06-26 10:50:24 +02:00
|
|
|
|
const el = entityToCADElement(entity, layerMap);
|
|
|
|
|
|
if (el) {
|
|
|
|
|
|
elements.push(el);
|
|
|
|
|
|
} else {
|
2026-08-29 01:17:21 +02:00
|
|
|
|
const type = String(entity?.type ?? 'UNKNOWN');
|
|
|
|
|
|
const reason = skipReason(entity);
|
|
|
|
|
|
skippedEntities.push({ type, reason });
|
|
|
|
|
|
warnings.push(`Unsupported entity type: ${type} — ${reason}`);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-29 01:17:21 +02:00
|
|
|
|
return { elements, layers: Array.from(layerMap.values()), warnings, skippedEntities };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Grund, warum eine Entity übersprungen wurde (für den Fehlerreport). */
|
|
|
|
|
|
export function skipReason(entity: any): string {
|
|
|
|
|
|
switch (entity?.type) {
|
|
|
|
|
|
case 'ELLIPSE':
|
|
|
|
|
|
return 'Ellipse ohne center/majorAxisEndPoint nicht darstellbar';
|
|
|
|
|
|
case 'SPLINE':
|
|
|
|
|
|
if ((entity?.degreeOfSplineCurve ?? 0) > 3) return 'Spline-Grad > 3 (Approx limitiert auf Grad <= 3)';
|
|
|
|
|
|
return 'Spline ohne verwertbare Kontrollpunkte';
|
|
|
|
|
|
case 'LINE':
|
|
|
|
|
|
return 'Line unvollständig (Start-/Endpunkt fehlt)';
|
|
|
|
|
|
case 'HATCH':
|
|
|
|
|
|
return 'HATCH-Grenzen nicht lesbar';
|
|
|
|
|
|
case 'ATTDEF':
|
|
|
|
|
|
return 'ATTDEF ohne tag';
|
|
|
|
|
|
default:
|
|
|
|
|
|
return 'Nicht unterstützter Entity-Typ';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* INSERT → block_instance(s). MINSERT-Arrays (columnCount/rowCount > 1)
|
|
|
|
|
|
* erzeugen n-fache Instanzen mit Spacing-Offset.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function expandInsert(entity: any, layerMap?: Map<string, CADLayer>): CADElement[] {
|
|
|
|
|
|
const layerName = entity.layer || '0';
|
|
|
|
|
|
const layer = layerMap?.get(layerName) ?? layerMap?.get('0');
|
|
|
|
|
|
const layerId = layer?.id ?? `dxf-layer-${layerName}`;
|
|
|
|
|
|
const pos = entity.position;
|
|
|
|
|
|
if (!pos) return [];
|
|
|
|
|
|
const baseProps: CADProperties = {
|
|
|
|
|
|
stroke: entity.color ? (dxfColorToHex(entity.color) ?? layer?.color) : (layer?.color ?? '#ffffff'),
|
|
|
|
|
|
strokeWidth: 1,
|
|
|
|
|
|
blockId: entity.name,
|
|
|
|
|
|
scale: entity.scale || entity.xScale || 1,
|
|
|
|
|
|
scaleX: entity.xScale ?? 1,
|
|
|
|
|
|
scaleY: entity.yScale ?? 1,
|
|
|
|
|
|
rotation: entity.rotation || 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const cols = Math.max(1, Math.floor(entity.columnCount || 1));
|
|
|
|
|
|
const rows = Math.max(1, Math.floor(entity.rowCount || 1));
|
|
|
|
|
|
const colSpacing = entity.columnSpacing || 0;
|
|
|
|
|
|
const rowSpacing = entity.rowSpacing || 0;
|
|
|
|
|
|
const out: CADElement[] = [];
|
|
|
|
|
|
for (let r = 0; r < rows; r++) {
|
|
|
|
|
|
for (let c = 0; c < cols; c++) {
|
|
|
|
|
|
out.push({
|
|
|
|
|
|
id: nextId(), type: 'block_instance', layerId,
|
|
|
|
|
|
x: pos.x + c * colSpacing, y: pos.y + r * rowSpacing,
|
|
|
|
|
|
width: 0, height: 0,
|
|
|
|
|
|
properties: { ...baseProps },
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** ELLIPSE → Polyline-Approximation (64 Segmente bei Voll-Ellipse). */
|
|
|
|
|
|
function ellipseToPoints(entity: any): Pt[] | null {
|
|
|
|
|
|
const c = entity.center;
|
|
|
|
|
|
const major = entity.majorAxisEndPoint;
|
|
|
|
|
|
if (!c || !major) return null;
|
|
|
|
|
|
const a = Math.hypot(major.x ?? 0, major.y ?? 0);
|
|
|
|
|
|
if (!(a > 0)) return null;
|
|
|
|
|
|
const ratio = typeof entity.axisRatio === 'number' && entity.axisRatio > 0 ? entity.axisRatio : 1;
|
|
|
|
|
|
const b = a * ratio;
|
|
|
|
|
|
const phi = Math.atan2(major.y ?? 0, major.x ?? 0);
|
|
|
|
|
|
const cosP = Math.cos(phi), sinP = Math.sin(phi);
|
|
|
|
|
|
const FULL = Math.PI * 2;
|
|
|
|
|
|
let start = typeof entity.startAngle === 'number' ? entity.startAngle : 0;
|
|
|
|
|
|
let end = typeof entity.endAngle === 'number' ? entity.endAngle : start + FULL;
|
|
|
|
|
|
let span = end - start;
|
|
|
|
|
|
const closed = !(span > 0.000001) || Math.abs(span - FULL) < 1e-6;
|
|
|
|
|
|
if (span <= 0) { start = 0; span = FULL; }
|
|
|
|
|
|
const n = Math.max(2, Math.round(64 * (span / FULL)));
|
|
|
|
|
|
const pts: Pt[] = [];
|
|
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
|
|
|
|
const t = closed ? start + (i * span) / n : start + (i * span) / (n - 1);
|
|
|
|
|
|
const ct = Math.cos(t), st = Math.sin(t);
|
|
|
|
|
|
pts.push({
|
|
|
|
|
|
x: c.x + a * ct * cosP - b * st * sinP,
|
|
|
|
|
|
y: c.y + a * ct * sinP + b * st * cosP,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return pts;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Cox-de-Boor: Punkt auf B-Spline (Grad <= 3). */
|
|
|
|
|
|
function bsplinePoint(degree: number, knots: number[], ctrl: Pt[], t: number): Pt {
|
|
|
|
|
|
const n = ctrl.length - 1;
|
|
|
|
|
|
let k = degree;
|
|
|
|
|
|
for (let i = degree; i <= n; i++) {
|
|
|
|
|
|
if (t >= knots[i] && t <= knots[i + 1]) { k = i; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
const d: Pt[] = [];
|
|
|
|
|
|
for (let j = 0; j <= degree; j++) d.push({ ...ctrl[k - degree + j] });
|
|
|
|
|
|
for (let r = 1; r <= degree; r++) {
|
|
|
|
|
|
for (let j = degree; j >= r; j--) {
|
|
|
|
|
|
const i = k - degree + j;
|
|
|
|
|
|
const denom = knots[i + degree - r + 1] - knots[i];
|
|
|
|
|
|
const alpha = denom === 0 ? 0 : (t - knots[i]) / denom;
|
|
|
|
|
|
d[j] = {
|
|
|
|
|
|
x: (1 - alpha) * d[j - 1].x + alpha * d[j].x,
|
|
|
|
|
|
y: (1 - alpha) * d[j - 1].y + alpha * d[j].y,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return d[degree];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** SPLINE → Polyline-Approximation via de Boor (Grad <= 3). */
|
|
|
|
|
|
function splineToPoints(entity: any): Pt[] | null {
|
|
|
|
|
|
const degree = entity.degreeOfSplineCurve ?? 3;
|
|
|
|
|
|
const ctrl = (entity.controlPoints || []) as Pt[];
|
|
|
|
|
|
if (!Array.isArray(ctrl) || ctrl.length < degree + 1) return null;
|
|
|
|
|
|
const m = ctrl.length;
|
|
|
|
|
|
const total = m + degree + 1;
|
|
|
|
|
|
const knots: number[] = entity.knotValues && entity.knotValues.length === total
|
|
|
|
|
|
? entity.knotValues.slice()
|
|
|
|
|
|
: (() => {
|
|
|
|
|
|
const ks: number[] = [];
|
|
|
|
|
|
for (let j = 0; j < total; j++) {
|
|
|
|
|
|
if (j <= degree) ks.push(0);
|
|
|
|
|
|
else if (j >= total - 1 - degree) ks.push(1);
|
|
|
|
|
|
else ks.push((j - degree) / (total - 1 - 2 * degree));
|
|
|
|
|
|
}
|
|
|
|
|
|
return ks;
|
|
|
|
|
|
})();
|
|
|
|
|
|
const N = 40;
|
|
|
|
|
|
const t0 = knots[degree];
|
|
|
|
|
|
const t1 = knots[m];
|
|
|
|
|
|
const span = t1 - t0;
|
|
|
|
|
|
const pts: Pt[] = [];
|
|
|
|
|
|
for (let i = 0; i < N; i++) {
|
|
|
|
|
|
const t = t0 + (i * span) / (N - 1);
|
|
|
|
|
|
pts.push(bsplinePoint(degree, knots, ctrl, t));
|
|
|
|
|
|
}
|
|
|
|
|
|
return pts;
|
2026-06-26 10:50:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-29 01:17:21 +02:00
|
|
|
|
/** MTEXT-Formatcodes zerlegen: \P → Umbruch, Inline-Codes entfernen. */
|
|
|
|
|
|
function cleanMText(raw: string): string {
|
|
|
|
|
|
return raw
|
|
|
|
|
|
.replace(/\\P/gi, '\n')
|
|
|
|
|
|
.replace(/\\~/g, ' ')
|
|
|
|
|
|
.replace(/\\[A-Za-z][^;{}\\]{0,12};/g, '')
|
|
|
|
|
|
.replace(/[{}]/g, '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Convert a single DXF entity into a CADElement (exported for unit tests).
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function entityToCADElement(
|
2026-06-26 10:50:24 +02:00
|
|
|
|
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;
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: nextId(), type: 'line', layerId: layer.id,
|
2026-08-29 01:17:21 +02:00
|
|
|
|
x: Math.min(s.x, e.x), y: Math.min(s.y, e.y),
|
|
|
|
|
|
width: Math.abs(e.x - s.x), height: Math.abs(e.y - s.y),
|
2026-06-26 10:50:24 +02:00
|
|
|
|
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 },
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-08-29 01:17:21 +02:00
|
|
|
|
case 'ELLIPSE': {
|
|
|
|
|
|
const pts = ellipseToPoints(entity);
|
|
|
|
|
|
if (!pts || pts.length < 2) return null;
|
|
|
|
|
|
const bbox = bboxOf(pts);
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: nextId(), type: 'polyline', layerId: layer.id,
|
|
|
|
|
|
...bbox,
|
|
|
|
|
|
properties: { ...baseProps, points: pts },
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'SPLINE': {
|
|
|
|
|
|
const degree = entity.degreeOfSplineCurve ?? 3;
|
|
|
|
|
|
const ctrl = (entity.controlPoints || []) as Pt[];
|
|
|
|
|
|
if (degree < 1 || degree > 3) return null;
|
|
|
|
|
|
if (!Array.isArray(ctrl) || ctrl.length < degree + 1) return null;
|
|
|
|
|
|
const pts = splineToPoints(entity);
|
|
|
|
|
|
if (!pts || pts.length < 2) return null;
|
|
|
|
|
|
const bbox = bboxOf(pts);
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: nextId(), type: 'polyline', layerId: layer.id,
|
|
|
|
|
|
...bbox,
|
|
|
|
|
|
properties: { ...baseProps, points: pts },
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-06-26 10:50:24 +02:00
|
|
|
|
case 'LWPOLYLINE':
|
|
|
|
|
|
case 'POLYLINE': {
|
|
|
|
|
|
const pts = (entity.vertices || []).map((v: any) => ({ x: v.x, y: v.y }));
|
|
|
|
|
|
if (pts.length < 2) return null;
|
2026-08-29 01:17:21 +02:00
|
|
|
|
const bbox = bboxOf(pts);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
const isClosed = entity.shape === true;
|
|
|
|
|
|
const type: ElementType = isClosed ? 'polygon' : 'polyline';
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: nextId(), type, layerId: layer.id,
|
2026-08-29 01:17:21 +02:00
|
|
|
|
...bbox,
|
2026-06-26 10:50:24 +02:00
|
|
|
|
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 },
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-08-29 01:17:21 +02:00
|
|
|
|
case 'MTEXT': {
|
2026-06-26 10:50:24 +02:00
|
|
|
|
const pos = entity.position;
|
|
|
|
|
|
if (!pos) return null;
|
2026-08-29 01:17:21 +02:00
|
|
|
|
const text = cleanMText(String(entity.text ?? ''));
|
|
|
|
|
|
if (!text) return null;
|
2026-06-26 10:50:24 +02:00
|
|
|
|
return {
|
2026-08-29 01:17:21 +02:00
|
|
|
|
id: nextId(), type: 'text', layerId: layer.id,
|
|
|
|
|
|
x: pos.x, y: pos.y, width: entity.width || 0, height: entity.height || 0,
|
|
|
|
|
|
properties: { ...baseProps, text, fontSize: entity.height || 12, rotation: entity.rotation || 0 },
|
2026-06-26 10:50:24 +02:00
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-08-29 01:17:21 +02:00
|
|
|
|
case 'POINT': {
|
|
|
|
|
|
const pos = entity.position;
|
|
|
|
|
|
if (!pos) return null;
|
|
|
|
|
|
const r = 1.5;
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: nextId(), type: 'circle', layerId: layer.id,
|
|
|
|
|
|
x: pos.x - r, y: pos.y - r, width: r * 2, height: r * 2,
|
|
|
|
|
|
properties: { ...baseProps, radius: r, fill: color, point: true },
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'ATTDEF': {
|
|
|
|
|
|
const s = entity.startPoint;
|
|
|
|
|
|
if (!entity.tag || !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.tag}=${entity.text ?? ''}`,
|
|
|
|
|
|
fontSize: entity.textHeight || 12,
|
|
|
|
|
|
tag: entity.tag,
|
|
|
|
|
|
prompt: entity.prompt ?? '',
|
|
|
|
|
|
attdef: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'HATCH': {
|
|
|
|
|
|
const pts = (entity.boundaryPoints || []) as Pt[];
|
|
|
|
|
|
if (pts.length < 3) return null;
|
|
|
|
|
|
const bbox = bboxOf(pts);
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: nextId(), type: 'polygon', layerId: layer.id,
|
|
|
|
|
|
...bbox,
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
...baseProps,
|
|
|
|
|
|
points: pts,
|
|
|
|
|
|
hatchPattern: entity.patternName || 'SOLID',
|
|
|
|
|
|
fill: entity.isSolid ? color : undefined,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'INSERT': {
|
|
|
|
|
|
const [inst] = expandInsert(entity, layerMap);
|
|
|
|
|
|
return inst ?? null;
|
|
|
|
|
|
}
|
2026-06-26 10:50:24 +02:00
|
|
|
|
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';
|
|
|
|
|
|
}
|