@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.
Files changed (82) hide show
  1. package/README.md +160 -0
  2. package/dist/bim/index.d.ts +366 -0
  3. package/dist/bim/index.d.ts.map +1 -0
  4. package/dist/bim/index.js +60 -0
  5. package/dist/bim/index.js.map +1 -0
  6. package/dist/cad/index.d.ts +329 -0
  7. package/dist/cad/index.d.ts.map +1 -0
  8. package/dist/cad/index.js +124 -0
  9. package/dist/cad/index.js.map +1 -0
  10. package/dist/ecad/index.d.ts +316 -0
  11. package/dist/ecad/index.d.ts.map +1 -0
  12. package/dist/ecad/index.js +11 -0
  13. package/dist/ecad/index.js.map +1 -0
  14. package/dist/export/index.d.ts +93 -0
  15. package/dist/export/index.d.ts.map +1 -0
  16. package/dist/export/index.js +522 -0
  17. package/dist/export/index.js.map +1 -0
  18. package/dist/floor-plan/index.d.ts +248 -0
  19. package/dist/floor-plan/index.d.ts.map +1 -0
  20. package/dist/floor-plan/index.js +228 -0
  21. package/dist/floor-plan/index.js.map +1 -0
  22. package/dist/grid/index.d.ts +160 -0
  23. package/dist/grid/index.d.ts.map +1 -0
  24. package/dist/grid/index.js +319 -0
  25. package/dist/grid/index.js.map +1 -0
  26. package/dist/import/import.worker.d.ts +28 -0
  27. package/dist/import/import.worker.d.ts.map +1 -0
  28. package/dist/import/import.worker.js +52 -0
  29. package/dist/import/import.worker.js.map +1 -0
  30. package/dist/import/index.d.ts +111 -0
  31. package/dist/import/index.d.ts.map +1 -0
  32. package/dist/import/index.js +1092 -0
  33. package/dist/import/index.js.map +1 -0
  34. package/dist/import/workerImport.d.ts +56 -0
  35. package/dist/import/workerImport.d.ts.map +1 -0
  36. package/dist/import/workerImport.js +207 -0
  37. package/dist/import/workerImport.js.map +1 -0
  38. package/dist/index.d.ts +29 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +86 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/interior/index.d.ts +241 -0
  43. package/dist/interior/index.d.ts.map +1 -0
  44. package/dist/interior/index.js +101 -0
  45. package/dist/interior/index.js.map +1 -0
  46. package/dist/measurement/index.d.ts +186 -0
  47. package/dist/measurement/index.d.ts.map +1 -0
  48. package/dist/measurement/index.js +400 -0
  49. package/dist/measurement/index.js.map +1 -0
  50. package/dist/objects/index.d.ts +288 -0
  51. package/dist/objects/index.d.ts.map +1 -0
  52. package/dist/objects/index.js +463 -0
  53. package/dist/objects/index.js.map +1 -0
  54. package/dist/serialization/index.d.ts +421 -0
  55. package/dist/serialization/index.d.ts.map +1 -0
  56. package/dist/serialization/index.js +412 -0
  57. package/dist/serialization/index.js.map +1 -0
  58. package/dist/topology/index.d.ts +252 -0
  59. package/dist/topology/index.d.ts.map +1 -0
  60. package/dist/topology/index.js +525 -0
  61. package/dist/topology/index.js.map +1 -0
  62. package/dist/transitions/index.d.ts +141 -0
  63. package/dist/transitions/index.d.ts.map +1 -0
  64. package/dist/transitions/index.js +160 -0
  65. package/dist/transitions/index.js.map +1 -0
  66. package/dist/unified/index.d.ts +225 -0
  67. package/dist/unified/index.d.ts.map +1 -0
  68. package/dist/unified/index.js +474 -0
  69. package/dist/unified/index.js.map +1 -0
  70. package/dist/virtualization/QuadTree.d.ts +68 -0
  71. package/dist/virtualization/QuadTree.d.ts.map +1 -0
  72. package/dist/virtualization/QuadTree.js +228 -0
  73. package/dist/virtualization/QuadTree.js.map +1 -0
  74. package/dist/virtualization/ViewportCulling.d.ts +92 -0
  75. package/dist/virtualization/ViewportCulling.d.ts.map +1 -0
  76. package/dist/virtualization/ViewportCulling.js +123 -0
  77. package/dist/virtualization/ViewportCulling.js.map +1 -0
  78. package/dist/virtualization/index.d.ts +9 -0
  79. package/dist/virtualization/index.d.ts.map +1 -0
  80. package/dist/virtualization/index.js +9 -0
  81. package/dist/virtualization/index.js.map +1 -0
  82. 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