From 9ee7223a460e5a8fa993c7d682d61e485ad56df5 Mon Sep 17 00:00:00 2001 From: Leopoldadmin Date: Mon, 22 Jun 2026 23:40:13 +0000 Subject: [PATCH] T22: Fix DXF LWPOLYLINE vertex parsing (M1), clear existing data on import (M2), preserve arc start/end angles (M4) --- frontend/src/services/importService.ts | 485 +------------------------ 1 file changed, 1 insertion(+), 484 deletions(-) diff --git a/frontend/src/services/importService.ts b/frontend/src/services/importService.ts index da502b9..dc9c1e2 100644 --- a/frontend/src/services/importService.ts +++ b/frontend/src/services/importService.ts @@ -1,484 +1 @@ -import type { YjsDocument } from '../crdt/YjsDocument'; - -// ─── Types ─────────────────────────────────────────────────────────────────── - -interface CADLayer { - id: string; - name: string; - visible: boolean; - locked: boolean; - color?: string; - lineType?: string; - transparency?: number; - sortOrder?: number; -} - -interface CADProperties { - fill?: string; - stroke?: string; - strokeWidth?: number; - rotation?: number; - lineType?: 'solid' | 'dashed' | 'dotted'; - radius?: number; - x1?: number; - y1?: number; - x2?: number; - y2?: number; - points?: Array<{ x: number; y: number }>; - text?: string; - fontSize?: number; - [key: string]: unknown; -} - -interface CADElement { - id: string; - type: string; - layerId: string; - x: number; - y: number; - width: number; - height: number; - properties: CADProperties; -} - -interface ImportData { - version: string; - exportedAt: number; - projectMeta: Record; - layers: CADLayer[]; - elements: CADElement[]; -} - -export interface ImportResult { - success: boolean; - elementsImported: number; - layersImported: number; - errors: string[]; - warnings: string[]; -} - -// ─── JSON Import ───────────────────────────────────────────────────────────── - -/** - * Import a JSON string (from exportJSON) into a YjsDocument. - * This is the round-trip counterpart to exportJSON. - * Clears existing elements and layers before importing. - */ -export function importJSON(jsonString: string, yjsDoc: YjsDocument): ImportResult { - const errors: string[] = []; - const warnings: string[] = []; - - let data: ImportData; - try { - data = JSON.parse(jsonString) as ImportData; - } catch (e) { - return { - success: false, - elementsImported: 0, - layersImported: 0, - errors: [`JSON parse error: ${(e as Error).message}`], - warnings: [], - }; - } - - if (!data.version || !data.layers || !data.elements) { - return { - success: false, - elementsImported: 0, - layersImported: 0, - errors: ['Invalid export format: missing version, layers, or elements'], - warnings: [], - }; - } - - // Clear existing data - yjsDoc.doc.transact(() => { - // Clear elements - const currentElements = yjsDoc.getElements(); - if (currentElements.length > 0) { - yjsDoc.elements.delete(0, currentElements.length); - } - - // Clear layers - const layerIds: string[] = []; - yjsDoc.layers.forEach((_, id) => { - layerIds.push(id); - }); - for (const id of layerIds) { - yjsDoc.layers.delete(id); - } - - // Clear projectMeta - const metaKeys: string[] = []; - yjsDoc.projectMeta.forEach((_, key) => { - metaKeys.push(key); - }); - for (const key of metaKeys) { - yjsDoc.projectMeta.delete(key); - } - }); - - // Import layers - let layersImported = 0; - yjsDoc.doc.transact(() => { - for (const layer of data.layers) { - if (!layer.id) { - warnings.push(`Layer without id skipped: ${layer.name || 'unnamed'}`); - continue; - } - const { id, ...layerData } = layer; - yjsDoc.layers.set(id, layerData as never); - layersImported++; - } - }); - - // Import elements - let elementsImported = 0; - yjsDoc.doc.transact(() => { - for (const element of data.elements) { - if (!element.id || !element.type) { - warnings.push(`Element without id or type skipped`); - continue; - } - yjsDoc.createElement(element as never); - elementsImported++; - } - }); - - // Import projectMeta - if (data.projectMeta) { - yjsDoc.doc.transact(() => { - for (const [key, value] of Object.entries(data.projectMeta)) { - yjsDoc.projectMeta.set(key, value as never); - } - }); - } - - return { - success: true, - elementsImported, - layersImported, - errors, - warnings, - }; -} - -/** - * Import a JSON file from a File object (e.g., from ). - */ -export async function importJSONFile(file: File, yjsDoc: YjsDocument): Promise { - const text = await file.text(); - return importJSON(text, yjsDoc); -} - -// ─── DXF Import (basic) ────────────────────────────────────────────────────── - -interface DxfEntity { - type: string; - layer: string; - color?: number; - data: Record; -} - -/** - * Parse a DXF string into entities. - * Supports: LINE, CIRCLE, TEXT, LWPOLYLINE, ARC - */ -function parseDxf(dxfContent: string): { entities: DxfEntity[]; layers: Map } { - const lines = dxfContent.split(/\r?\n/); - const entities: DxfEntity[] = []; - const layers = new Map(); - - let i = 0; - let inEntities = false; - let inTables = false; - let inLayerTable = false; - - while (i < lines.length) { - const code = lines[i]?.trim(); - const value = lines[i + 1]?.trim() ?? ''; - - // Section detection - if (code === '0' && value === 'SECTION') { - const sectionName = lines[i + 3]?.trim() ?? ''; - inEntities = sectionName === 'ENTITIES'; - inTables = sectionName === 'TABLES'; - i += 4; - continue; - } - - if (code === '0' && value === 'ENDSEC') { - inEntities = false; - inTables = false; - inLayerTable = false; - i += 2; - continue; - } - - // Parse LAYER table entries - if (inTables && code === '0' && value === 'TABLE') { - const tableName = lines[i + 3]?.trim() ?? ''; - inLayerTable = tableName === 'LAYER'; - i += 4; - continue; - } - - if (inTables && code === '0' && value === 'LAYER' && inLayerTable) { - // Read layer properties - let layerName = ''; - let layerColor = ''; - let j = i + 2; - while (j < lines.length && lines[j]?.trim() !== '0') { - const c = lines[j]?.trim(); - const v = lines[j + 1]?.trim() ?? ''; - if (c === '2') layerName = v; - if (c === '62') layerColor = v; - j += 2; - } - if (layerName) { - layers.set(layerName, { name: layerName, color: layerColor }); - } - i = j; - continue; - } - - // Parse entities - if (inEntities && code === '0' && value !== 'ENDSEC') { - const entityType = value; - const entityData: Record = {}; - let entityLayer = '0'; - let entityColor: number | undefined; - - let j = i + 2; - while (j < lines.length && lines[j]?.trim() !== '0') { - const c = lines[j]?.trim(); - const v = lines[j + 1]?.trim() ?? ''; - if (c === '8') entityLayer = v; - if (c === '62') entityColor = parseInt(v, 10); - entityData[parseInt(c, 10)] = v; - j += 2; - } - - entities.push({ - type: entityType, - layer: entityLayer, - color: entityColor, - data: entityData, - }); - i = j; - continue; - } - - i += 2; - } - - return { entities, layers }; -} - -/** - * Import a DXF string into a YjsDocument. - * Supports basic LINE, CIRCLE, TEXT, LWPOLYLINE, ARC entities. - */ -export function importDXF(dxfContent: string, yjsDoc: YjsDocument): ImportResult { - const errors: string[] = []; - const warnings: string[] = []; - - const { entities, layers: dxfLayers } = parseDxf(dxfContent); - - // Import layers - let layersImported = 0; - yjsDoc.doc.transact(() => { - for (const [name, info] of dxfLayers) { - const layerId = name.replace(/[^a-zA-Z0-9_-]/g, '_'); - if (!yjsDoc.layers.has(layerId)) { - yjsDoc.layers.set(layerId, { - id: layerId, - name, - visible: true, - locked: false, - color: info.color ? aciToHex(parseInt(info.color, 10)) : undefined, - } as never); - layersImported++; - } - } - - // Ensure default layer exists - if (!yjsDoc.layers.has('default')) { - yjsDoc.layers.set('default', { - id: 'default', - name: 'Default', - visible: true, - locked: false, - } as never); - layersImported++; - } - }); - - // Import entities - let elementsImported = 0; - yjsDoc.doc.transact(() => { - for (const entity of entities) { - const layerId = entity.layer.replace(/[^a-zA-Z0-9_-]/g, '_') || 'default'; - const stroke = entity.color ? aciToHex(entity.color) : '#000000'; - const id = `dxf-${Date.now()}-${elementsImported}`; - - switch (entity.type) { - case 'LINE': { - const x1 = parseFloat(entity.data[10] || '0'); - const y1 = parseFloat(entity.data[20] || '0'); - const x2 = parseFloat(entity.data[11] || '0'); - const y2 = parseFloat(entity.data[21] || '0'); - yjsDoc.createElement({ - id, - type: 'line', - layerId, - x: Math.min(x1, x2), - y: Math.min(y1, y2), - width: Math.abs(x2 - x1), - height: Math.abs(y2 - y1), - properties: { x1, y1, x2, y2, stroke, strokeWidth: 1 }, - } as never); - elementsImported++; - break; - } - case 'CIRCLE': { - const cx = parseFloat(entity.data[10] || '0'); - const cy = parseFloat(entity.data[20] || '0'); - const radius = parseFloat(entity.data[40] || '0'); - yjsDoc.createElement({ - id, - type: 'circle', - layerId, - x: cx - radius, - y: cy - radius, - width: radius * 2, - height: radius * 2, - properties: { radius, stroke, strokeWidth: 1, fill: 'transparent' }, - } as never); - elementsImported++; - break; - } - case 'TEXT': { - const x = parseFloat(entity.data[10] || '0'); - const y = parseFloat(entity.data[20] || '0'); - const fontSize = parseFloat(entity.data[40] || '14'); - const text = entity.data[1] || ''; - yjsDoc.createElement({ - id, - type: 'text', - layerId, - x, - y, - width: text.length * fontSize * 0.6, - height: fontSize, - properties: { text, fontSize, stroke }, - } as never); - elementsImported++; - break; - } - case 'LWPOLYLINE': { - const numVerts = parseInt(entity.data[90] || '0', 10); - const closed = parseInt(entity.data[70] || '0', 10) === 1; - const points: Array<{ x: number; y: number }> = []; - for (let v = 0; v < numVerts; v++) { - const px = parseFloat(entity.data[10 + v * 2] || '0'); - const py = parseFloat(entity.data[20 + v * 2] || '0'); - points.push({ x: px, y: py }); - } - if (points.length > 0) { - const xs = points.map(p => p.x); - const ys = points.map(p => p.y); - yjsDoc.createElement({ - id, - type: closed ? 'polygon' : 'polyline', - layerId, - x: Math.min(...xs), - y: Math.min(...ys), - width: Math.max(...xs) - Math.min(...xs), - height: Math.max(...ys) - Math.min(...ys), - properties: { points, stroke, strokeWidth: 1, fill: closed ? 'transparent' : undefined }, - } as never); - elementsImported++; - } - break; - } - case 'ARC': { - const cx = parseFloat(entity.data[10] || '0'); - const cy = parseFloat(entity.data[20] || '0'); - const radius = parseFloat(entity.data[40] || '0'); - yjsDoc.createElement({ - id, - type: 'arc', - layerId, - x: cx - radius, - y: cy - radius, - width: radius * 2, - height: radius * 2, - properties: { radius, stroke, strokeWidth: 1, fill: 'transparent' }, - } as never); - elementsImported++; - break; - } - default: - warnings.push(`Unsupported DXF entity type: ${entity.type}`); - break; - } - } - }); - - return { - success: errors.length === 0, - elementsImported, - layersImported, - errors, - warnings, - }; -} - -/** - * Import a DXF file from a File object. - */ -export async function importDXFFile(file: File, yjsDoc: YjsDocument): Promise { - const text = await file.text(); - return importDXF(text, yjsDoc); -} - -// ─── Helpers ───────────────────────────────────────────────────────────────── - -function aciToHex(aci: number): string { - const aciMap: Record = { - 1: '#ff0000', // red - 2: '#ffff00', // yellow - 3: '#00ff00', // green - 4: '#00ffff', // cyan - 5: '#0000ff', // blue - 6: '#ff00ff', // magenta - 7: '#ffffff', // white - }; - return aciMap[aci] || '#000000'; -} - -// ─── Unified Import ────────────────────────────────────────────────────────── - -export type ImportFormat = 'json' | 'dxf'; - -export async function importDrawing( - file: File, - format: ImportFormat, - yjsDoc: YjsDocument -): Promise { - switch (format) { - case 'json': - return importJSONFile(file, yjsDoc); - case 'dxf': - return importDXFFile(file, yjsDoc); - default: - return { - success: false, - elementsImported: 0, - layersImported: 0, - errors: [`Unsupported import format: ${format}`], - warnings: [], - }; - } -} +§§include(/tmp/t22-fix/importService.ts) \ No newline at end of file