@nice2dev/spatial-core 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -0
- package/dist/bim/index.d.ts +366 -0
- package/dist/bim/index.d.ts.map +1 -0
- package/dist/bim/index.js +60 -0
- package/dist/bim/index.js.map +1 -0
- package/dist/cad/index.d.ts +329 -0
- package/dist/cad/index.d.ts.map +1 -0
- package/dist/cad/index.js +124 -0
- package/dist/cad/index.js.map +1 -0
- package/dist/ecad/index.d.ts +316 -0
- package/dist/ecad/index.d.ts.map +1 -0
- package/dist/ecad/index.js +11 -0
- package/dist/ecad/index.js.map +1 -0
- package/dist/export/index.d.ts +93 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +522 -0
- package/dist/export/index.js.map +1 -0
- package/dist/floor-plan/index.d.ts +248 -0
- package/dist/floor-plan/index.d.ts.map +1 -0
- package/dist/floor-plan/index.js +228 -0
- package/dist/floor-plan/index.js.map +1 -0
- package/dist/grid/index.d.ts +160 -0
- package/dist/grid/index.d.ts.map +1 -0
- package/dist/grid/index.js +319 -0
- package/dist/grid/index.js.map +1 -0
- package/dist/import/import.worker.d.ts +28 -0
- package/dist/import/import.worker.d.ts.map +1 -0
- package/dist/import/import.worker.js +52 -0
- package/dist/import/import.worker.js.map +1 -0
- package/dist/import/index.d.ts +111 -0
- package/dist/import/index.d.ts.map +1 -0
- package/dist/import/index.js +1092 -0
- package/dist/import/index.js.map +1 -0
- package/dist/import/workerImport.d.ts +56 -0
- package/dist/import/workerImport.d.ts.map +1 -0
- package/dist/import/workerImport.js +207 -0
- package/dist/import/workerImport.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/interior/index.d.ts +241 -0
- package/dist/interior/index.d.ts.map +1 -0
- package/dist/interior/index.js +101 -0
- package/dist/interior/index.js.map +1 -0
- package/dist/measurement/index.d.ts +186 -0
- package/dist/measurement/index.d.ts.map +1 -0
- package/dist/measurement/index.js +400 -0
- package/dist/measurement/index.js.map +1 -0
- package/dist/objects/index.d.ts +288 -0
- package/dist/objects/index.d.ts.map +1 -0
- package/dist/objects/index.js +463 -0
- package/dist/objects/index.js.map +1 -0
- package/dist/serialization/index.d.ts +421 -0
- package/dist/serialization/index.d.ts.map +1 -0
- package/dist/serialization/index.js +412 -0
- package/dist/serialization/index.js.map +1 -0
- package/dist/topology/index.d.ts +252 -0
- package/dist/topology/index.d.ts.map +1 -0
- package/dist/topology/index.js +525 -0
- package/dist/topology/index.js.map +1 -0
- package/dist/transitions/index.d.ts +141 -0
- package/dist/transitions/index.d.ts.map +1 -0
- package/dist/transitions/index.js +160 -0
- package/dist/transitions/index.js.map +1 -0
- package/dist/unified/index.d.ts +225 -0
- package/dist/unified/index.d.ts.map +1 -0
- package/dist/unified/index.js +474 -0
- package/dist/unified/index.js.map +1 -0
- package/dist/virtualization/QuadTree.d.ts +68 -0
- package/dist/virtualization/QuadTree.d.ts.map +1 -0
- package/dist/virtualization/QuadTree.js +228 -0
- package/dist/virtualization/QuadTree.js.map +1 -0
- package/dist/virtualization/ViewportCulling.d.ts +92 -0
- package/dist/virtualization/ViewportCulling.d.ts.map +1 -0
- package/dist/virtualization/ViewportCulling.js +123 -0
- package/dist/virtualization/ViewportCulling.js.map +1 -0
- package/dist/virtualization/index.d.ts +9 -0
- package/dist/virtualization/index.d.ts.map +1 -0
- package/dist/virtualization/index.js +9 -0
- package/dist/virtualization/index.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,1092 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import — Import external formats into FloorPlanDocument.
|
|
3
|
+
*
|
|
4
|
+
* Supported formats:
|
|
5
|
+
* - DXF (AutoCAD Drawing Exchange Format)
|
|
6
|
+
* - SVG (Scalable Vector Graphics)
|
|
7
|
+
* - GeoJSON (Geographic JSON)
|
|
8
|
+
* - IFC (Industry Foundation Classes) — basic support
|
|
9
|
+
*
|
|
10
|
+
* @module @nice2dev/spatial-core/import
|
|
11
|
+
*/
|
|
12
|
+
/** Parse DXF content string. */
|
|
13
|
+
function parseDXF(content) {
|
|
14
|
+
const entities = [];
|
|
15
|
+
const lines = content.split(/\r?\n/);
|
|
16
|
+
let i = 0;
|
|
17
|
+
let inEntities = false;
|
|
18
|
+
let currentEntity = null;
|
|
19
|
+
let vertices = [];
|
|
20
|
+
let currentX = null;
|
|
21
|
+
while (i < lines.length) {
|
|
22
|
+
const code = parseInt(lines[i]?.trim() ?? '', 10);
|
|
23
|
+
const value = lines[i + 1]?.trim() ?? '';
|
|
24
|
+
i += 2;
|
|
25
|
+
// Start of ENTITIES section
|
|
26
|
+
if (code === 2 && value === 'ENTITIES') {
|
|
27
|
+
inEntities = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
// End of ENTITIES section
|
|
31
|
+
if (code === 0 && value === 'ENDSEC' && inEntities) {
|
|
32
|
+
if (currentEntity?.type) {
|
|
33
|
+
if (vertices.length > 0) {
|
|
34
|
+
currentEntity.vertices = [...vertices];
|
|
35
|
+
}
|
|
36
|
+
entities.push(currentEntity);
|
|
37
|
+
}
|
|
38
|
+
inEntities = false;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (!inEntities) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// New entity
|
|
45
|
+
if (code === 0) {
|
|
46
|
+
// Save previous entity
|
|
47
|
+
if (currentEntity?.type) {
|
|
48
|
+
if (vertices.length > 0) {
|
|
49
|
+
currentEntity.vertices = [...vertices];
|
|
50
|
+
}
|
|
51
|
+
entities.push(currentEntity);
|
|
52
|
+
}
|
|
53
|
+
// Start new entity
|
|
54
|
+
currentEntity = { type: value, layer: '0' };
|
|
55
|
+
vertices = [];
|
|
56
|
+
currentX = null;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!currentEntity) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// Common codes
|
|
63
|
+
switch (code) {
|
|
64
|
+
case 8: // Layer
|
|
65
|
+
currentEntity.layer = value;
|
|
66
|
+
break;
|
|
67
|
+
case 5: // Handle
|
|
68
|
+
currentEntity.handle = value;
|
|
69
|
+
break;
|
|
70
|
+
case 10: // X coordinate (start or vertex)
|
|
71
|
+
currentX = parseFloat(value);
|
|
72
|
+
break;
|
|
73
|
+
case 20: // Y coordinate
|
|
74
|
+
if (currentX !== null) {
|
|
75
|
+
vertices.push({ x: currentX, y: parseFloat(value) });
|
|
76
|
+
if (!currentEntity.position) {
|
|
77
|
+
currentEntity.position = { x: currentX, y: parseFloat(value) };
|
|
78
|
+
}
|
|
79
|
+
currentX = null;
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
case 11: // X2 (end point for LINE)
|
|
83
|
+
currentX = parseFloat(value);
|
|
84
|
+
break;
|
|
85
|
+
case 21: // Y2
|
|
86
|
+
if (currentX !== null) {
|
|
87
|
+
vertices.push({ x: currentX, y: parseFloat(value) });
|
|
88
|
+
currentX = null;
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
case 40: // Radius (CIRCLE) or text height
|
|
92
|
+
if (currentEntity.type === 'CIRCLE' || currentEntity.type === 'ARC') {
|
|
93
|
+
currentEntity.radius = parseFloat(value);
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
case 50: // Start angle (ARC)
|
|
97
|
+
currentEntity.startAngle = parseFloat(value);
|
|
98
|
+
break;
|
|
99
|
+
case 51: // End angle (ARC)
|
|
100
|
+
currentEntity.endAngle = parseFloat(value);
|
|
101
|
+
break;
|
|
102
|
+
case 1: // Text content
|
|
103
|
+
currentEntity.text = value;
|
|
104
|
+
break;
|
|
105
|
+
case 2: // Block name (INSERT)
|
|
106
|
+
currentEntity.blockName = value;
|
|
107
|
+
break;
|
|
108
|
+
case 70: // Flags (polyline closed flag)
|
|
109
|
+
if (parseInt(value, 10) & 1) {
|
|
110
|
+
currentEntity.closed = true;
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return entities;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Import DXF file content.
|
|
119
|
+
*/
|
|
120
|
+
export function importDXF(content, options = {}) {
|
|
121
|
+
const startTime = Date.now();
|
|
122
|
+
const warnings = [];
|
|
123
|
+
const errors = [];
|
|
124
|
+
const scaleFactor = options.scaleFactor ?? 1;
|
|
125
|
+
const originOffset = options.originOffset ?? { x: 0, y: 0 };
|
|
126
|
+
try {
|
|
127
|
+
const entities = parseDXF(content);
|
|
128
|
+
const walls = [];
|
|
129
|
+
const zones = [];
|
|
130
|
+
const objects = [];
|
|
131
|
+
const layersFound = new Set();
|
|
132
|
+
let entitiesSkipped = 0;
|
|
133
|
+
for (const entity of entities) {
|
|
134
|
+
layersFound.add(entity.layer);
|
|
135
|
+
// Check layer filters
|
|
136
|
+
if (options.includeLayers && !options.includeLayers.includes(entity.layer)) {
|
|
137
|
+
entitiesSkipped++;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (options.excludeLayers?.includes(entity.layer)) {
|
|
141
|
+
entitiesSkipped++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
// Map layer name
|
|
145
|
+
const layerName = options.layerMappings?.[entity.layer] ?? entity.layer;
|
|
146
|
+
switch (entity.type) {
|
|
147
|
+
case 'LINE':
|
|
148
|
+
if (entity.vertices && entity.vertices.length >= 2) {
|
|
149
|
+
const start = applyTransform(entity.vertices[0], scaleFactor, originOffset);
|
|
150
|
+
const end = applyTransform(entity.vertices[1], scaleFactor, originOffset);
|
|
151
|
+
walls.push({ start, end, layer: layerName });
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
case 'LWPOLYLINE':
|
|
155
|
+
case 'POLYLINE':
|
|
156
|
+
if (entity.vertices && entity.vertices.length >= 2) {
|
|
157
|
+
const transformedVertices = entity.vertices.map((v) => applyTransform(v, scaleFactor, originOffset));
|
|
158
|
+
// Check if closed polyline should be a zone
|
|
159
|
+
if (entity.closed && options.convertClosedPolylinesToZones) {
|
|
160
|
+
const area = calculatePolygonArea(transformedVertices);
|
|
161
|
+
const minArea = options.minZoneArea ?? 0.1;
|
|
162
|
+
if (area >= minArea) {
|
|
163
|
+
zones.push({
|
|
164
|
+
name: layerName,
|
|
165
|
+
boundary: transformedVertices,
|
|
166
|
+
layer: layerName,
|
|
167
|
+
});
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Convert to walls
|
|
172
|
+
const count = entity.closed
|
|
173
|
+
? transformedVertices.length
|
|
174
|
+
: transformedVertices.length - 1;
|
|
175
|
+
for (let j = 0; j < count; j++) {
|
|
176
|
+
const start = transformedVertices[j];
|
|
177
|
+
const end = transformedVertices[(j + 1) % transformedVertices.length];
|
|
178
|
+
walls.push({ start, end, layer: layerName });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
case 'CIRCLE':
|
|
183
|
+
if (entity.position && entity.radius) {
|
|
184
|
+
const center = applyTransform(entity.position, scaleFactor, originOffset);
|
|
185
|
+
objects.push({
|
|
186
|
+
type: 'circle',
|
|
187
|
+
position: center,
|
|
188
|
+
layer: layerName,
|
|
189
|
+
metadata: { radius: entity.radius * scaleFactor },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
case 'INSERT':
|
|
194
|
+
if (entity.position && entity.blockName) {
|
|
195
|
+
const pos = applyTransform(entity.position, scaleFactor, originOffset);
|
|
196
|
+
objects.push({
|
|
197
|
+
type: 'block-reference',
|
|
198
|
+
position: pos,
|
|
199
|
+
layer: layerName,
|
|
200
|
+
metadata: { blockName: entity.blockName },
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
default:
|
|
205
|
+
entitiesSkipped++;
|
|
206
|
+
warnings.push({
|
|
207
|
+
code: 'UNSUPPORTED_ENTITY',
|
|
208
|
+
message: `Unsupported DXF entity type: ${entity.type}`,
|
|
209
|
+
context: { type: entity.type, layer: entity.layer },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Assemble document
|
|
214
|
+
const document = assembleDocument(walls, zones, objects, layersFound, {
|
|
215
|
+
name: 'Imported DXF',
|
|
216
|
+
sourceFormat: 'dxf',
|
|
217
|
+
units: options.defaultUnits ?? 'metric',
|
|
218
|
+
});
|
|
219
|
+
return {
|
|
220
|
+
success: true,
|
|
221
|
+
document,
|
|
222
|
+
warnings,
|
|
223
|
+
errors,
|
|
224
|
+
stats: {
|
|
225
|
+
wallsImported: walls.length,
|
|
226
|
+
zonesImported: zones.length,
|
|
227
|
+
objectsImported: objects.length,
|
|
228
|
+
layersFound: layersFound.size,
|
|
229
|
+
entitiesSkipped,
|
|
230
|
+
importTimeMs: Date.now() - startTime,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
errors.push({
|
|
236
|
+
code: 'PARSE_ERROR',
|
|
237
|
+
message: err instanceof Error ? err.message : 'Unknown error parsing DXF',
|
|
238
|
+
fatal: true,
|
|
239
|
+
});
|
|
240
|
+
return {
|
|
241
|
+
success: false,
|
|
242
|
+
warnings,
|
|
243
|
+
errors,
|
|
244
|
+
stats: {
|
|
245
|
+
wallsImported: 0,
|
|
246
|
+
zonesImported: 0,
|
|
247
|
+
objectsImported: 0,
|
|
248
|
+
layersFound: 0,
|
|
249
|
+
entitiesSkipped: 0,
|
|
250
|
+
importTimeMs: Date.now() - startTime,
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/* ================================================================
|
|
256
|
+
SVG IMPORT
|
|
257
|
+
================================================================ */
|
|
258
|
+
/** Parse SVG path d attribute to points. */
|
|
259
|
+
function parseSVGPath(d) {
|
|
260
|
+
const paths = [];
|
|
261
|
+
let currentPath = [];
|
|
262
|
+
let currentX = 0;
|
|
263
|
+
let currentY = 0;
|
|
264
|
+
let startX = 0;
|
|
265
|
+
let startY = 0;
|
|
266
|
+
// Tokenize path
|
|
267
|
+
const commands = d.match(/[MmLlHhVvCcSsQqTtAaZz]|[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/g) ?? [];
|
|
268
|
+
let i = 0;
|
|
269
|
+
while (i < commands.length) {
|
|
270
|
+
const cmd = commands[i];
|
|
271
|
+
i++;
|
|
272
|
+
const getNumber = () => {
|
|
273
|
+
const val = parseFloat(commands[i] ?? '0');
|
|
274
|
+
i++;
|
|
275
|
+
return val;
|
|
276
|
+
};
|
|
277
|
+
switch (cmd) {
|
|
278
|
+
case 'M': // Absolute moveto
|
|
279
|
+
if (currentPath.length > 0) {
|
|
280
|
+
paths.push(currentPath);
|
|
281
|
+
currentPath = [];
|
|
282
|
+
}
|
|
283
|
+
currentX = getNumber();
|
|
284
|
+
currentY = getNumber();
|
|
285
|
+
startX = currentX;
|
|
286
|
+
startY = currentY;
|
|
287
|
+
currentPath.push({ x: currentX, y: currentY });
|
|
288
|
+
break;
|
|
289
|
+
case 'm': // Relative moveto
|
|
290
|
+
if (currentPath.length > 0) {
|
|
291
|
+
paths.push(currentPath);
|
|
292
|
+
currentPath = [];
|
|
293
|
+
}
|
|
294
|
+
currentX += getNumber();
|
|
295
|
+
currentY += getNumber();
|
|
296
|
+
startX = currentX;
|
|
297
|
+
startY = currentY;
|
|
298
|
+
currentPath.push({ x: currentX, y: currentY });
|
|
299
|
+
break;
|
|
300
|
+
case 'L': // Absolute lineto
|
|
301
|
+
currentX = getNumber();
|
|
302
|
+
currentY = getNumber();
|
|
303
|
+
currentPath.push({ x: currentX, y: currentY });
|
|
304
|
+
break;
|
|
305
|
+
case 'l': // Relative lineto
|
|
306
|
+
currentX += getNumber();
|
|
307
|
+
currentY += getNumber();
|
|
308
|
+
currentPath.push({ x: currentX, y: currentY });
|
|
309
|
+
break;
|
|
310
|
+
case 'H': // Absolute horizontal line
|
|
311
|
+
currentX = getNumber();
|
|
312
|
+
currentPath.push({ x: currentX, y: currentY });
|
|
313
|
+
break;
|
|
314
|
+
case 'h': // Relative horizontal line
|
|
315
|
+
currentX += getNumber();
|
|
316
|
+
currentPath.push({ x: currentX, y: currentY });
|
|
317
|
+
break;
|
|
318
|
+
case 'V': // Absolute vertical line
|
|
319
|
+
currentY = getNumber();
|
|
320
|
+
currentPath.push({ x: currentX, y: currentY });
|
|
321
|
+
break;
|
|
322
|
+
case 'v': // Relative vertical line
|
|
323
|
+
currentY += getNumber();
|
|
324
|
+
currentPath.push({ x: currentX, y: currentY });
|
|
325
|
+
break;
|
|
326
|
+
case 'Z':
|
|
327
|
+
case 'z': // Close path
|
|
328
|
+
if (currentPath.length > 0) {
|
|
329
|
+
currentPath.push({ x: startX, y: startY });
|
|
330
|
+
}
|
|
331
|
+
currentX = startX;
|
|
332
|
+
currentY = startY;
|
|
333
|
+
break;
|
|
334
|
+
case 'C': // Cubic bezier (absolute) — simplified to endpoint
|
|
335
|
+
getNumber();
|
|
336
|
+
getNumber(); // Control point 1
|
|
337
|
+
getNumber();
|
|
338
|
+
getNumber(); // Control point 2
|
|
339
|
+
currentX = getNumber();
|
|
340
|
+
currentY = getNumber();
|
|
341
|
+
currentPath.push({ x: currentX, y: currentY });
|
|
342
|
+
break;
|
|
343
|
+
case 'c': // Cubic bezier (relative) — simplified to endpoint
|
|
344
|
+
getNumber();
|
|
345
|
+
getNumber(); // Control point 1
|
|
346
|
+
getNumber();
|
|
347
|
+
getNumber(); // Control point 2
|
|
348
|
+
currentX += getNumber();
|
|
349
|
+
currentY += getNumber();
|
|
350
|
+
currentPath.push({ x: currentX, y: currentY });
|
|
351
|
+
break;
|
|
352
|
+
case 'Q': // Quadratic bezier (absolute) — simplified to endpoint
|
|
353
|
+
getNumber();
|
|
354
|
+
getNumber(); // Control point
|
|
355
|
+
currentX = getNumber();
|
|
356
|
+
currentY = getNumber();
|
|
357
|
+
currentPath.push({ x: currentX, y: currentY });
|
|
358
|
+
break;
|
|
359
|
+
case 'q': // Quadratic bezier (relative) — simplified to endpoint
|
|
360
|
+
getNumber();
|
|
361
|
+
getNumber(); // Control point
|
|
362
|
+
currentX += getNumber();
|
|
363
|
+
currentY += getNumber();
|
|
364
|
+
currentPath.push({ x: currentX, y: currentY });
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (currentPath.length > 0) {
|
|
369
|
+
paths.push(currentPath);
|
|
370
|
+
}
|
|
371
|
+
return paths;
|
|
372
|
+
}
|
|
373
|
+
/** Parse SVG transform attribute. */
|
|
374
|
+
function parseSVGTransform(transform) {
|
|
375
|
+
let translateX = 0;
|
|
376
|
+
let translateY = 0;
|
|
377
|
+
let scale = 1;
|
|
378
|
+
// Parse translate
|
|
379
|
+
const translateMatch = transform.match(/translate\s*\(\s*([-\d.]+)(?:\s*,?\s*([-\d.]+))?\s*\)/);
|
|
380
|
+
if (translateMatch) {
|
|
381
|
+
translateX = parseFloat(translateMatch[1]);
|
|
382
|
+
translateY = parseFloat(translateMatch[2] ?? '0');
|
|
383
|
+
}
|
|
384
|
+
// Parse scale
|
|
385
|
+
const scaleMatch = transform.match(/scale\s*\(\s*([-\d.]+)(?:\s*,?\s*([-\d.]+))?\s*\)/);
|
|
386
|
+
if (scaleMatch) {
|
|
387
|
+
scale = parseFloat(scaleMatch[1]);
|
|
388
|
+
}
|
|
389
|
+
return { translate: { x: translateX, y: translateY }, scale };
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Import SVG file content.
|
|
393
|
+
*/
|
|
394
|
+
export function importSVG(content, options = {}) {
|
|
395
|
+
const startTime = Date.now();
|
|
396
|
+
const warnings = [];
|
|
397
|
+
const errors = [];
|
|
398
|
+
const scaleFactor = options.scaleFactor ?? 1;
|
|
399
|
+
const originOffset = options.originOffset ?? { x: 0, y: 0 };
|
|
400
|
+
try {
|
|
401
|
+
const walls = [];
|
|
402
|
+
const zones = [];
|
|
403
|
+
const objects = [];
|
|
404
|
+
const layersFound = new Set();
|
|
405
|
+
// Find all path elements
|
|
406
|
+
const pathRegex = /<path[^>]*\bd="([^"]+)"[^>]*>/gi;
|
|
407
|
+
let pathMatch;
|
|
408
|
+
while ((pathMatch = pathRegex.exec(content)) !== null) {
|
|
409
|
+
const d = pathMatch[1];
|
|
410
|
+
const fullTag = pathMatch[0];
|
|
411
|
+
// Get layer/id
|
|
412
|
+
const idMatch = fullTag.match(/\bid="([^"]+)"/);
|
|
413
|
+
const layerName = idMatch?.[1] ?? 'default';
|
|
414
|
+
layersFound.add(layerName);
|
|
415
|
+
// Get transform
|
|
416
|
+
const transformMatch = fullTag.match(/\btransform="([^"]+)"/);
|
|
417
|
+
const transform = transformMatch
|
|
418
|
+
? parseSVGTransform(transformMatch[1])
|
|
419
|
+
: { translate: { x: 0, y: 0 }, scale: 1 };
|
|
420
|
+
// Parse path
|
|
421
|
+
const subPaths = parseSVGPath(d);
|
|
422
|
+
for (const points of subPaths) {
|
|
423
|
+
if (points.length < 2) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const transformedPoints = points.map((p) => applyTransform({
|
|
427
|
+
x: p.x * transform.scale + transform.translate.x,
|
|
428
|
+
y: p.y * transform.scale + transform.translate.y,
|
|
429
|
+
}, scaleFactor, originOffset));
|
|
430
|
+
// Check if closed (becomes zone)
|
|
431
|
+
const isClosed = Math.abs(transformedPoints[0].x - transformedPoints[transformedPoints.length - 1].x) <
|
|
432
|
+
0.1 &&
|
|
433
|
+
Math.abs(transformedPoints[0].y - transformedPoints[transformedPoints.length - 1].y) <
|
|
434
|
+
0.1;
|
|
435
|
+
if (isClosed && options.convertClosedPolylinesToZones) {
|
|
436
|
+
const area = calculatePolygonArea(transformedPoints);
|
|
437
|
+
if (area >= (options.minZoneArea ?? 0.1)) {
|
|
438
|
+
zones.push({ name: layerName, boundary: transformedPoints, layer: layerName });
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Convert to walls
|
|
443
|
+
for (let j = 0; j < transformedPoints.length - 1; j++) {
|
|
444
|
+
walls.push({
|
|
445
|
+
start: transformedPoints[j],
|
|
446
|
+
end: transformedPoints[j + 1],
|
|
447
|
+
layer: layerName,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Find all line elements
|
|
453
|
+
const lineRegex = /<line[^>]*\bx1="([^"]+)"[^>]*\by1="([^"]+)"[^>]*\bx2="([^"]+)"[^>]*\by2="([^"]+)"[^>]*>/gi;
|
|
454
|
+
let lineMatch;
|
|
455
|
+
while ((lineMatch = lineRegex.exec(content)) !== null) {
|
|
456
|
+
const x1 = parseFloat(lineMatch[1]);
|
|
457
|
+
const y1 = parseFloat(lineMatch[2]);
|
|
458
|
+
const x2 = parseFloat(lineMatch[3]);
|
|
459
|
+
const y2 = parseFloat(lineMatch[4]);
|
|
460
|
+
const start = applyTransform({ x: x1, y: y1 }, scaleFactor, originOffset);
|
|
461
|
+
const end = applyTransform({ x: x2, y: y2 }, scaleFactor, originOffset);
|
|
462
|
+
walls.push({ start, end, layer: 'default' });
|
|
463
|
+
layersFound.add('default');
|
|
464
|
+
}
|
|
465
|
+
// Find all rect elements
|
|
466
|
+
const rectRegex = /<rect[^>]*\bx="([^"]+)"[^>]*\by="([^"]+)"[^>]*\bwidth="([^"]+)"[^>]*\bheight="([^"]+)"[^>]*>/gi;
|
|
467
|
+
let rectMatch;
|
|
468
|
+
while ((rectMatch = rectRegex.exec(content)) !== null) {
|
|
469
|
+
const x = parseFloat(rectMatch[1]);
|
|
470
|
+
const y = parseFloat(rectMatch[2]);
|
|
471
|
+
const width = parseFloat(rectMatch[3]);
|
|
472
|
+
const height = parseFloat(rectMatch[4]);
|
|
473
|
+
const rect = [
|
|
474
|
+
applyTransform({ x, y }, scaleFactor, originOffset),
|
|
475
|
+
applyTransform({ x: x + width, y }, scaleFactor, originOffset),
|
|
476
|
+
applyTransform({ x: x + width, y: y + height }, scaleFactor, originOffset),
|
|
477
|
+
applyTransform({ x, y: y + height }, scaleFactor, originOffset),
|
|
478
|
+
];
|
|
479
|
+
if (options.convertClosedPolylinesToZones) {
|
|
480
|
+
zones.push({ name: 'rect', boundary: rect, layer: 'default' });
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
walls.push({ start: rect[0], end: rect[1], layer: 'default' });
|
|
484
|
+
walls.push({ start: rect[1], end: rect[2], layer: 'default' });
|
|
485
|
+
walls.push({ start: rect[2], end: rect[3], layer: 'default' });
|
|
486
|
+
walls.push({ start: rect[3], end: rect[0], layer: 'default' });
|
|
487
|
+
}
|
|
488
|
+
layersFound.add('default');
|
|
489
|
+
}
|
|
490
|
+
// Find all circle elements
|
|
491
|
+
const circleRegex = /<circle[^>]*\bcx="([^"]+)"[^>]*\bcy="([^"]+)"[^>]*\br="([^"]+)"[^>]*>/gi;
|
|
492
|
+
let circleMatch;
|
|
493
|
+
while ((circleMatch = circleRegex.exec(content)) !== null) {
|
|
494
|
+
const cx = parseFloat(circleMatch[1]);
|
|
495
|
+
const cy = parseFloat(circleMatch[2]);
|
|
496
|
+
const r = parseFloat(circleMatch[3]);
|
|
497
|
+
const center = applyTransform({ x: cx, y: cy }, scaleFactor, originOffset);
|
|
498
|
+
objects.push({
|
|
499
|
+
type: 'circle',
|
|
500
|
+
position: center,
|
|
501
|
+
layer: 'default',
|
|
502
|
+
metadata: { radius: r * scaleFactor },
|
|
503
|
+
});
|
|
504
|
+
layersFound.add('default');
|
|
505
|
+
}
|
|
506
|
+
// Assemble document
|
|
507
|
+
const document = assembleDocument(walls, zones, objects, layersFound, {
|
|
508
|
+
name: 'Imported SVG',
|
|
509
|
+
sourceFormat: 'svg',
|
|
510
|
+
units: options.defaultUnits ?? 'metric',
|
|
511
|
+
});
|
|
512
|
+
return {
|
|
513
|
+
success: true,
|
|
514
|
+
document,
|
|
515
|
+
warnings,
|
|
516
|
+
errors,
|
|
517
|
+
stats: {
|
|
518
|
+
wallsImported: walls.length,
|
|
519
|
+
zonesImported: zones.length,
|
|
520
|
+
objectsImported: objects.length,
|
|
521
|
+
layersFound: layersFound.size,
|
|
522
|
+
entitiesSkipped: 0,
|
|
523
|
+
importTimeMs: Date.now() - startTime,
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
catch (err) {
|
|
528
|
+
errors.push({
|
|
529
|
+
code: 'PARSE_ERROR',
|
|
530
|
+
message: err instanceof Error ? err.message : 'Unknown error parsing SVG',
|
|
531
|
+
fatal: true,
|
|
532
|
+
});
|
|
533
|
+
return {
|
|
534
|
+
success: false,
|
|
535
|
+
warnings,
|
|
536
|
+
errors,
|
|
537
|
+
stats: {
|
|
538
|
+
wallsImported: 0,
|
|
539
|
+
zonesImported: 0,
|
|
540
|
+
objectsImported: 0,
|
|
541
|
+
layersFound: 0,
|
|
542
|
+
entitiesSkipped: 0,
|
|
543
|
+
importTimeMs: Date.now() - startTime,
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Import GeoJSON file content.
|
|
550
|
+
*/
|
|
551
|
+
export function importGeoJSON(content, options = {}) {
|
|
552
|
+
const startTime = Date.now();
|
|
553
|
+
const warnings = [];
|
|
554
|
+
const errors = [];
|
|
555
|
+
const scaleFactor = options.scaleFactor ?? 1;
|
|
556
|
+
const originOffset = options.originOffset ?? { x: 0, y: 0 };
|
|
557
|
+
try {
|
|
558
|
+
const geojson = JSON.parse(content);
|
|
559
|
+
const walls = [];
|
|
560
|
+
const zones = [];
|
|
561
|
+
const objects = [];
|
|
562
|
+
const layersFound = new Set();
|
|
563
|
+
let entitiesSkipped = 0;
|
|
564
|
+
const features = geojson.type === 'FeatureCollection' ? geojson.features : [geojson];
|
|
565
|
+
for (const feature of features) {
|
|
566
|
+
const layerName = feature.properties?.layer ?? 'default';
|
|
567
|
+
layersFound.add(layerName);
|
|
568
|
+
const geom = feature.geometry;
|
|
569
|
+
switch (geom.type) {
|
|
570
|
+
case 'Point': {
|
|
571
|
+
const coords = geom.coordinates;
|
|
572
|
+
const pos = applyTransform({ x: coords[0], y: coords[1] }, scaleFactor, originOffset);
|
|
573
|
+
objects.push({
|
|
574
|
+
type: 'marker',
|
|
575
|
+
position: pos,
|
|
576
|
+
layer: layerName,
|
|
577
|
+
metadata: { properties: feature.properties },
|
|
578
|
+
});
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
case 'LineString': {
|
|
582
|
+
const coords = geom.coordinates;
|
|
583
|
+
for (let i = 0; i < coords.length - 1; i++) {
|
|
584
|
+
const start = applyTransform({ x: coords[i][0], y: coords[i][1] }, scaleFactor, originOffset);
|
|
585
|
+
const end = applyTransform({ x: coords[i + 1][0], y: coords[i + 1][1] }, scaleFactor, originOffset);
|
|
586
|
+
walls.push({ start, end, layer: layerName });
|
|
587
|
+
}
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
case 'MultiLineString': {
|
|
591
|
+
const lines = geom.coordinates;
|
|
592
|
+
for (const line of lines) {
|
|
593
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
594
|
+
const start = applyTransform({ x: line[i][0], y: line[i][1] }, scaleFactor, originOffset);
|
|
595
|
+
const end = applyTransform({ x: line[i + 1][0], y: line[i + 1][1] }, scaleFactor, originOffset);
|
|
596
|
+
walls.push({ start, end, layer: layerName });
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
case 'Polygon': {
|
|
602
|
+
const rings = geom.coordinates;
|
|
603
|
+
const outerRing = rings[0];
|
|
604
|
+
const points = outerRing.map((c) => applyTransform({ x: c[0], y: c[1] }, scaleFactor, originOffset));
|
|
605
|
+
zones.push({ name: layerName, boundary: points, layer: layerName });
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
case 'MultiPolygon': {
|
|
609
|
+
const polygons = geom.coordinates;
|
|
610
|
+
for (const polygon of polygons) {
|
|
611
|
+
const outerRing = polygon[0];
|
|
612
|
+
const points = outerRing.map((c) => applyTransform({ x: c[0], y: c[1] }, scaleFactor, originOffset));
|
|
613
|
+
zones.push({ name: layerName, boundary: points, layer: layerName });
|
|
614
|
+
}
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
default:
|
|
618
|
+
entitiesSkipped++;
|
|
619
|
+
warnings.push({
|
|
620
|
+
code: 'UNSUPPORTED_GEOMETRY',
|
|
621
|
+
message: `Unsupported GeoJSON geometry: ${geom.type}`,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
// Assemble document
|
|
626
|
+
const document = assembleDocument(walls, zones, objects, layersFound, {
|
|
627
|
+
name: 'Imported GeoJSON',
|
|
628
|
+
sourceFormat: 'geojson',
|
|
629
|
+
units: options.defaultUnits ?? 'metric',
|
|
630
|
+
});
|
|
631
|
+
return {
|
|
632
|
+
success: true,
|
|
633
|
+
document,
|
|
634
|
+
warnings,
|
|
635
|
+
errors,
|
|
636
|
+
stats: {
|
|
637
|
+
wallsImported: walls.length,
|
|
638
|
+
zonesImported: zones.length,
|
|
639
|
+
objectsImported: objects.length,
|
|
640
|
+
layersFound: layersFound.size,
|
|
641
|
+
entitiesSkipped,
|
|
642
|
+
importTimeMs: Date.now() - startTime,
|
|
643
|
+
},
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
errors.push({
|
|
648
|
+
code: 'PARSE_ERROR',
|
|
649
|
+
message: err instanceof Error ? err.message : 'Unknown error parsing GeoJSON',
|
|
650
|
+
fatal: true,
|
|
651
|
+
});
|
|
652
|
+
return {
|
|
653
|
+
success: false,
|
|
654
|
+
warnings,
|
|
655
|
+
errors,
|
|
656
|
+
stats: {
|
|
657
|
+
wallsImported: 0,
|
|
658
|
+
zonesImported: 0,
|
|
659
|
+
objectsImported: 0,
|
|
660
|
+
layersFound: 0,
|
|
661
|
+
entitiesSkipped: 0,
|
|
662
|
+
importTimeMs: Date.now() - startTime,
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/* ================================================================
|
|
668
|
+
IFC IMPORT (Basic)
|
|
669
|
+
================================================================ */
|
|
670
|
+
/**
|
|
671
|
+
* Import IFC file content.
|
|
672
|
+
* Note: This is a very basic implementation that handles STEP-formatted IFC.
|
|
673
|
+
* For full IFC support, use a dedicated IFC library like web-ifc.
|
|
674
|
+
*/
|
|
675
|
+
export function importIFC(content, options = {}) {
|
|
676
|
+
const startTime = Date.now();
|
|
677
|
+
const warnings = [];
|
|
678
|
+
const errors = [];
|
|
679
|
+
warnings.push({
|
|
680
|
+
code: 'LIMITED_IFC_SUPPORT',
|
|
681
|
+
message: 'IFC import is limited. For full support, use a dedicated IFC library like web-ifc.',
|
|
682
|
+
});
|
|
683
|
+
try {
|
|
684
|
+
const walls = [];
|
|
685
|
+
const zones = [];
|
|
686
|
+
const objects = [];
|
|
687
|
+
const layersFound = new Set();
|
|
688
|
+
let entitiesSkipped = 0;
|
|
689
|
+
// Parse IFC entities (very simplified)
|
|
690
|
+
const lines = content.split(/\r?\n/);
|
|
691
|
+
const entities = new Map();
|
|
692
|
+
// First pass: collect all entities
|
|
693
|
+
for (const line of lines) {
|
|
694
|
+
const match = line.match(/^#(\d+)\s*=\s*(\w+)\s*\((.+)\)\s*;?$/);
|
|
695
|
+
if (match) {
|
|
696
|
+
entities.set(`#${match[1]}`, { type: match[2], params: match[3] });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// Look for IfcWallStandardCase and IfcSpace
|
|
700
|
+
for (const [id, entity] of entities) {
|
|
701
|
+
if (entity.type === 'IFCWALLSTANDARDCASE' || entity.type === 'IFCWALL') {
|
|
702
|
+
layersFound.add('Walls');
|
|
703
|
+
const nameMatch = entity.params.match(/'([^']+)'/);
|
|
704
|
+
const name = nameMatch?.[1] ?? 'Wall';
|
|
705
|
+
warnings.push({
|
|
706
|
+
code: 'IFC_WALL_SIMPLIFIED',
|
|
707
|
+
message: `Wall "${name}" (${id}) geometry simplified to placeholder`,
|
|
708
|
+
});
|
|
709
|
+
entitiesSkipped++;
|
|
710
|
+
}
|
|
711
|
+
else if (entity.type === 'IFCSPACE') {
|
|
712
|
+
layersFound.add('Spaces');
|
|
713
|
+
const nameMatch = entity.params.match(/'([^']+)'/);
|
|
714
|
+
const name = nameMatch?.[1] ?? 'Space';
|
|
715
|
+
warnings.push({
|
|
716
|
+
code: 'IFC_SPACE_SIMPLIFIED',
|
|
717
|
+
message: `Space "${name}" (${id}) geometry simplified to placeholder`,
|
|
718
|
+
});
|
|
719
|
+
entitiesSkipped++;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// Assemble document (mostly empty without proper IFC geometry parsing)
|
|
723
|
+
const document = assembleDocument(walls, zones, objects, layersFound, {
|
|
724
|
+
name: 'Imported IFC',
|
|
725
|
+
sourceFormat: 'ifc',
|
|
726
|
+
units: options.defaultUnits ?? 'metric',
|
|
727
|
+
});
|
|
728
|
+
return {
|
|
729
|
+
success: true,
|
|
730
|
+
document,
|
|
731
|
+
warnings,
|
|
732
|
+
errors,
|
|
733
|
+
stats: {
|
|
734
|
+
wallsImported: walls.length,
|
|
735
|
+
zonesImported: zones.length,
|
|
736
|
+
objectsImported: objects.length,
|
|
737
|
+
layersFound: layersFound.size,
|
|
738
|
+
entitiesSkipped,
|
|
739
|
+
importTimeMs: Date.now() - startTime,
|
|
740
|
+
},
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
errors.push({
|
|
745
|
+
code: 'PARSE_ERROR',
|
|
746
|
+
message: err instanceof Error ? err.message : 'Unknown error parsing IFC',
|
|
747
|
+
fatal: true,
|
|
748
|
+
});
|
|
749
|
+
return {
|
|
750
|
+
success: false,
|
|
751
|
+
warnings,
|
|
752
|
+
errors,
|
|
753
|
+
stats: {
|
|
754
|
+
wallsImported: 0,
|
|
755
|
+
zonesImported: 0,
|
|
756
|
+
objectsImported: 0,
|
|
757
|
+
layersFound: 0,
|
|
758
|
+
entitiesSkipped: 0,
|
|
759
|
+
importTimeMs: Date.now() - startTime,
|
|
760
|
+
},
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
/* ================================================================
|
|
765
|
+
UNIFIED IMPORT FUNCTION
|
|
766
|
+
================================================================ */
|
|
767
|
+
/**
|
|
768
|
+
* Import file content in specified format.
|
|
769
|
+
*/
|
|
770
|
+
export function importFile(content, format, options = {}) {
|
|
771
|
+
switch (format) {
|
|
772
|
+
case 'dxf':
|
|
773
|
+
return importDXF(content, options);
|
|
774
|
+
case 'svg':
|
|
775
|
+
return importSVG(content, options);
|
|
776
|
+
case 'geojson':
|
|
777
|
+
return importGeoJSON(content, options);
|
|
778
|
+
case 'ifc':
|
|
779
|
+
return importIFC(content, options);
|
|
780
|
+
default:
|
|
781
|
+
return {
|
|
782
|
+
success: false,
|
|
783
|
+
warnings: [],
|
|
784
|
+
errors: [
|
|
785
|
+
{
|
|
786
|
+
code: 'UNSUPPORTED_FORMAT',
|
|
787
|
+
message: `Unsupported import format: ${format}`,
|
|
788
|
+
fatal: true,
|
|
789
|
+
},
|
|
790
|
+
],
|
|
791
|
+
stats: {
|
|
792
|
+
wallsImported: 0,
|
|
793
|
+
zonesImported: 0,
|
|
794
|
+
objectsImported: 0,
|
|
795
|
+
layersFound: 0,
|
|
796
|
+
entitiesSkipped: 0,
|
|
797
|
+
importTimeMs: 0,
|
|
798
|
+
},
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Detect import format from file extension or content.
|
|
804
|
+
*/
|
|
805
|
+
export function detectFileImportFormat(filename, content) {
|
|
806
|
+
const ext = filename.toLowerCase().split('.').pop();
|
|
807
|
+
switch (ext) {
|
|
808
|
+
case 'dxf':
|
|
809
|
+
return 'dxf';
|
|
810
|
+
case 'svg':
|
|
811
|
+
return 'svg';
|
|
812
|
+
case 'geojson':
|
|
813
|
+
case 'json':
|
|
814
|
+
if (content?.includes('"type"') &&
|
|
815
|
+
(content.includes('"Feature"') || content.includes('"FeatureCollection"'))) {
|
|
816
|
+
return 'geojson';
|
|
817
|
+
}
|
|
818
|
+
return ext === 'geojson' ? 'geojson' : null;
|
|
819
|
+
case 'ifc':
|
|
820
|
+
return 'ifc';
|
|
821
|
+
default:
|
|
822
|
+
// Try content detection
|
|
823
|
+
if (content) {
|
|
824
|
+
if (content.includes('ISO-10303-21') || content.includes('IFCPROJECT')) {
|
|
825
|
+
return 'ifc';
|
|
826
|
+
}
|
|
827
|
+
if (content.includes('<?xml') && content.includes('<svg')) {
|
|
828
|
+
return 'svg';
|
|
829
|
+
}
|
|
830
|
+
if (content.includes('SECTION') && content.includes('ENTITIES')) {
|
|
831
|
+
return 'dxf';
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Import from file (browser File API).
|
|
839
|
+
*/
|
|
840
|
+
export async function importFromBrowserFile(file, options = {}) {
|
|
841
|
+
return new Promise((resolve) => {
|
|
842
|
+
const reader = new FileReader();
|
|
843
|
+
reader.onload = () => {
|
|
844
|
+
const content = reader.result;
|
|
845
|
+
const format = options.format ?? detectFileImportFormat(file.name, content);
|
|
846
|
+
if (!format) {
|
|
847
|
+
resolve({
|
|
848
|
+
success: false,
|
|
849
|
+
warnings: [],
|
|
850
|
+
errors: [
|
|
851
|
+
{
|
|
852
|
+
code: 'UNKNOWN_FORMAT',
|
|
853
|
+
message: `Unable to detect format for file: ${file.name}`,
|
|
854
|
+
fatal: true,
|
|
855
|
+
},
|
|
856
|
+
],
|
|
857
|
+
stats: {
|
|
858
|
+
wallsImported: 0,
|
|
859
|
+
zonesImported: 0,
|
|
860
|
+
objectsImported: 0,
|
|
861
|
+
layersFound: 0,
|
|
862
|
+
entitiesSkipped: 0,
|
|
863
|
+
importTimeMs: 0,
|
|
864
|
+
},
|
|
865
|
+
});
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
resolve(importFile(content, format, options));
|
|
869
|
+
};
|
|
870
|
+
reader.onerror = () => {
|
|
871
|
+
resolve({
|
|
872
|
+
success: false,
|
|
873
|
+
warnings: [],
|
|
874
|
+
errors: [
|
|
875
|
+
{
|
|
876
|
+
code: 'FILE_READ_ERROR',
|
|
877
|
+
message: 'Error reading file',
|
|
878
|
+
fatal: true,
|
|
879
|
+
},
|
|
880
|
+
],
|
|
881
|
+
stats: {
|
|
882
|
+
wallsImported: 0,
|
|
883
|
+
zonesImported: 0,
|
|
884
|
+
objectsImported: 0,
|
|
885
|
+
layersFound: 0,
|
|
886
|
+
entitiesSkipped: 0,
|
|
887
|
+
importTimeMs: 0,
|
|
888
|
+
},
|
|
889
|
+
});
|
|
890
|
+
};
|
|
891
|
+
reader.readAsText(file);
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
/* ================================================================
|
|
895
|
+
HELPER FUNCTIONS
|
|
896
|
+
================================================================ */
|
|
897
|
+
/** Generate unique ID. */
|
|
898
|
+
function generateId() {
|
|
899
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
900
|
+
}
|
|
901
|
+
/** Apply scale and offset transform. */
|
|
902
|
+
function applyTransform(point, scale, offset) {
|
|
903
|
+
return {
|
|
904
|
+
x: point.x * scale + offset.x,
|
|
905
|
+
y: point.y * scale + offset.y,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
/** Calculate polygon area using shoelace formula. */
|
|
909
|
+
function calculatePolygonArea(vertices) {
|
|
910
|
+
let area = 0;
|
|
911
|
+
const n = vertices.length;
|
|
912
|
+
for (let i = 0; i < n; i++) {
|
|
913
|
+
const j = (i + 1) % n;
|
|
914
|
+
area += vertices[i].x * vertices[j].y;
|
|
915
|
+
area -= vertices[j].x * vertices[i].y;
|
|
916
|
+
}
|
|
917
|
+
return Math.abs(area / 2);
|
|
918
|
+
}
|
|
919
|
+
/** Calculate bounding box of points. */
|
|
920
|
+
function calculateBounds(points) {
|
|
921
|
+
if (points.length === 0) {
|
|
922
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
923
|
+
}
|
|
924
|
+
let minX = Infinity, minY = Infinity;
|
|
925
|
+
let maxX = -Infinity, maxY = -Infinity;
|
|
926
|
+
for (const p of points) {
|
|
927
|
+
if (p.x < minX) {
|
|
928
|
+
minX = p.x;
|
|
929
|
+
}
|
|
930
|
+
if (p.y < minY) {
|
|
931
|
+
minY = p.y;
|
|
932
|
+
}
|
|
933
|
+
if (p.x > maxX) {
|
|
934
|
+
maxX = p.x;
|
|
935
|
+
}
|
|
936
|
+
if (p.y > maxY) {
|
|
937
|
+
maxY = p.y;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return {
|
|
941
|
+
x: minX,
|
|
942
|
+
y: minY,
|
|
943
|
+
width: maxX - minX,
|
|
944
|
+
height: maxY - minY,
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
/** Get a color for a layer name. */
|
|
948
|
+
function getLayerColor(layerName) {
|
|
949
|
+
let hash = 0;
|
|
950
|
+
for (let i = 0; i < layerName.length; i++) {
|
|
951
|
+
hash = layerName.charCodeAt(i) + ((hash << 5) - hash);
|
|
952
|
+
}
|
|
953
|
+
const hue = Math.abs(hash) % 360;
|
|
954
|
+
return `hsl(${hue}, 60%, 50%)`;
|
|
955
|
+
}
|
|
956
|
+
/** Create transform for position. */
|
|
957
|
+
function createTransform(position) {
|
|
958
|
+
return {
|
|
959
|
+
position,
|
|
960
|
+
rotation: 0,
|
|
961
|
+
scale: { x: 1, y: 1 },
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Assemble imported data into a FloorPlanDocument.
|
|
966
|
+
*/
|
|
967
|
+
function assembleDocument(walls, zones, objects, layersFound, docInfo) {
|
|
968
|
+
const now = new Date().toISOString();
|
|
969
|
+
const floorId = `floor-${generateId()}`;
|
|
970
|
+
// Calculate all points for bounds
|
|
971
|
+
const allPoints = [];
|
|
972
|
+
for (const w of walls) {
|
|
973
|
+
allPoints.push(w.start, w.end);
|
|
974
|
+
}
|
|
975
|
+
for (const z of zones) {
|
|
976
|
+
allPoints.push(...z.boundary);
|
|
977
|
+
}
|
|
978
|
+
for (const o of objects) {
|
|
979
|
+
allPoints.push(o.position);
|
|
980
|
+
}
|
|
981
|
+
const floorBounds = calculateBounds(allPoints);
|
|
982
|
+
// Create wall objects
|
|
983
|
+
const wallObjects = walls.map((w) => ({
|
|
984
|
+
id: `wall-${generateId()}`,
|
|
985
|
+
type: 'wall',
|
|
986
|
+
category: 'building',
|
|
987
|
+
name: 'Wall',
|
|
988
|
+
transform: createTransform(w.start),
|
|
989
|
+
bounds: calculateBounds([w.start, w.end]),
|
|
990
|
+
layer: w.layer,
|
|
991
|
+
locked: false,
|
|
992
|
+
visible: true,
|
|
993
|
+
metadata: {
|
|
994
|
+
end: w.end,
|
|
995
|
+
thickness: w.thickness ?? 0.15,
|
|
996
|
+
importedAt: now,
|
|
997
|
+
},
|
|
998
|
+
childIds: [],
|
|
999
|
+
}));
|
|
1000
|
+
// Create general objects
|
|
1001
|
+
const generalObjects = objects.map((o) => ({
|
|
1002
|
+
id: `obj-${generateId()}`,
|
|
1003
|
+
type: o.type,
|
|
1004
|
+
category: 'annotation',
|
|
1005
|
+
name: o.type,
|
|
1006
|
+
transform: createTransform(o.position),
|
|
1007
|
+
bounds: {
|
|
1008
|
+
x: o.position.x - 10,
|
|
1009
|
+
y: o.position.y - 10,
|
|
1010
|
+
width: 20,
|
|
1011
|
+
height: 20,
|
|
1012
|
+
},
|
|
1013
|
+
layer: o.layer,
|
|
1014
|
+
locked: false,
|
|
1015
|
+
visible: true,
|
|
1016
|
+
metadata: {
|
|
1017
|
+
...o.metadata,
|
|
1018
|
+
importedAt: now,
|
|
1019
|
+
},
|
|
1020
|
+
childIds: [],
|
|
1021
|
+
}));
|
|
1022
|
+
// Create layers
|
|
1023
|
+
const layers = Array.from(layersFound).map((name) => ({
|
|
1024
|
+
id: `layer-${generateId()}`,
|
|
1025
|
+
name,
|
|
1026
|
+
type: (name.toLowerCase().includes('wall') ? 'walls' : 'custom'),
|
|
1027
|
+
visible: true,
|
|
1028
|
+
locked: false,
|
|
1029
|
+
opacity: 1,
|
|
1030
|
+
color: getLayerColor(name),
|
|
1031
|
+
objects: [
|
|
1032
|
+
...wallObjects.filter((w) => w.layer === name),
|
|
1033
|
+
...generalObjects.filter((o) => o.layer === name),
|
|
1034
|
+
],
|
|
1035
|
+
}));
|
|
1036
|
+
// Create floor
|
|
1037
|
+
const floor = {
|
|
1038
|
+
id: floorId,
|
|
1039
|
+
name: 'Imported Floor',
|
|
1040
|
+
level: 0,
|
|
1041
|
+
elevation: 0,
|
|
1042
|
+
ceilingHeight: 2.7,
|
|
1043
|
+
bounds: floorBounds,
|
|
1044
|
+
layers,
|
|
1045
|
+
};
|
|
1046
|
+
// Create zones - stored in document metadata for future use
|
|
1047
|
+
// TODO: Add zones property to FloorPlanDocument when supported
|
|
1048
|
+
const documentZones = zones.map((z) => ({
|
|
1049
|
+
id: `zone-${generateId()}`,
|
|
1050
|
+
name: z.name,
|
|
1051
|
+
type: 'custom',
|
|
1052
|
+
boundary: z.boundary,
|
|
1053
|
+
floorId,
|
|
1054
|
+
roomIds: [],
|
|
1055
|
+
color: getLayerColor(z.name),
|
|
1056
|
+
}));
|
|
1057
|
+
// Create document
|
|
1058
|
+
const document = {
|
|
1059
|
+
$schema: 'https://nice2dev.com/schemas/nsp-v1.json',
|
|
1060
|
+
version: '1.0.0',
|
|
1061
|
+
type: 'floor-plan',
|
|
1062
|
+
metadata: {
|
|
1063
|
+
id: `doc-${generateId()}`,
|
|
1064
|
+
name: docInfo.name,
|
|
1065
|
+
description: `Imported from ${docInfo.sourceFormat.toUpperCase()}`,
|
|
1066
|
+
created: now,
|
|
1067
|
+
modified: now,
|
|
1068
|
+
tags: ['imported', docInfo.sourceFormat],
|
|
1069
|
+
},
|
|
1070
|
+
floors: [floor],
|
|
1071
|
+
settings: {
|
|
1072
|
+
units: docInfo.units,
|
|
1073
|
+
scale: 50,
|
|
1074
|
+
grid: {
|
|
1075
|
+
visible: true,
|
|
1076
|
+
size: 0.5,
|
|
1077
|
+
snapEnabled: true,
|
|
1078
|
+
},
|
|
1079
|
+
defaultWallHeight: 2.7,
|
|
1080
|
+
defaultWallThickness: 0.15,
|
|
1081
|
+
precision: 2,
|
|
1082
|
+
},
|
|
1083
|
+
};
|
|
1084
|
+
// Attach zones to metadata for later use
|
|
1085
|
+
document.zones = documentZones;
|
|
1086
|
+
return document;
|
|
1087
|
+
}
|
|
1088
|
+
/* ================================================================
|
|
1089
|
+
WEB WORKER IMPORT (re-exported)
|
|
1090
|
+
================================================================ */
|
|
1091
|
+
export { importFileInWorker, importFileAsync, isWorkerSupported, terminateImportWorker, createImportWorker, } from './workerImport';
|
|
1092
|
+
//# sourceMappingURL=index.js.map
|