@2112-lab/central-plant 0.3.12 → 0.3.14

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.
@@ -35,7 +35,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
35
35
  * Initialize the CentralPlant manager
36
36
  *
37
37
  * @constructor
38
- * @version 0.3.12
38
+ * @version 0.3.14
39
39
  * @updated 2025-10-22
40
40
  *
41
41
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -7,6 +7,7 @@ var THREE = require('three');
7
7
  var ioDeviceUtils = require('../../utils/ioDeviceUtils.js');
8
8
  var modelPreloader = require('../../rendering/modelPreloader.js');
9
9
  var boundingBoxUtils = require('../../utils/boundingBoxUtils.js');
10
+ var viewport2DManager = require('./viewport2DManager.js');
10
11
 
11
12
  function _interopNamespace(e) {
12
13
  if (e && e.__esModule) return e;
@@ -585,6 +586,15 @@ var ModelManager = /*#__PURE__*/function () {
585
586
  var jsonData = _ref2.jsonData,
586
587
  glbModel = _ref2.glbModel;
587
588
  if (!glbModel) return;
589
+
590
+ // CRITICAL: Force matrix updates before computing bbox.
591
+ // After loadLibraryModel positions the model, the world matrices may not be
592
+ // invalidated yet. computeFilteredBoundingBox uses updateWorldMatrix(false, true)
593
+ // which only updates if matrixWorldNeedsUpdate is true. Force the update here
594
+ // to ensure the bbox is computed with correct world-space coordinates.
595
+ glbModel.updateMatrix();
596
+ glbModel.updateMatrixWorld(true);
597
+
588
598
  // Use filtered bbox (excludes connectors + io-devices) so it matches
589
599
  // what pathfindingManager._enrichSceneDataWithBoundingBoxes produces
590
600
  var filteredBox = boundingBoxUtils.computeFilteredBoundingBox(glbModel, ['io-device', 'connector']);
@@ -595,13 +605,9 @@ var ModelManager = /*#__PURE__*/function () {
595
605
  // Update both the JSON data object AND the live scene object
596
606
  jsonData.userData.worldBoundingBox = worldBoundingBox;
597
607
  glbModel.userData.worldBoundingBox = worldBoundingBox;
598
- // Snapshot the object's local position so viewport2DManager can compute
608
+ // Cache the object's position so viewport2DManager can compute
599
609
  // world-bbox updates via a fast O(1) position delta instead of re-traversing geometry
600
- glbModel.userData._wbbBasePosition = {
601
- x: glbModel.position.x,
602
- y: glbModel.position.y,
603
- z: glbModel.position.z
604
- };
610
+ viewport2DManager.cacheBasePosition(glbModel, worldBoundingBox);
605
611
  });
606
612
 
607
613
  // Dispatch completion event
@@ -27,6 +27,29 @@ function _interopNamespace(e) {
27
27
 
28
28
  var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
29
29
 
30
+ /**
31
+ * WeakMap cache for storing base position data per Object3D.
32
+ * Used by _getOrComputeWorldBoundingBox() for O(1) bbox offset calculations.
33
+ * WeakMap ensures automatic cleanup when objects are garbage collected.
34
+ * @type {WeakMap<THREE.Object3D, {x: number, y: number, z: number, worldBoundingBox: {min: number[], max: number[]}}>}
35
+ */
36
+ var _basePositionCache = new WeakMap();
37
+
38
+ /**
39
+ * Store base position and worldBoundingBox for an Object3D.
40
+ * Called by modelManager after GLB loading to enable fast bbox updates.
41
+ * @param {THREE.Object3D} object - The loaded GLB model
42
+ * @param {{min: number[], max: number[]}} worldBoundingBox - Computed world bounding box
43
+ */
44
+ function cacheBasePosition(object, worldBoundingBox) {
45
+ _basePositionCache.set(object, {
46
+ x: object.position.x,
47
+ y: object.position.y,
48
+ z: object.position.z,
49
+ worldBoundingBox: worldBoundingBox
50
+ });
51
+ }
52
+
30
53
  /**
31
54
  * Viewport2DInstance
32
55
  * Represents a single 2D viewport with its own Konva stage and configuration
@@ -123,6 +146,9 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
123
146
  // stacking up independent renderComponents() runs
124
147
  _this2._refreshPending = false;
125
148
 
149
+ // Set of viewport keys pending refresh (collects keys across multiple refresh() calls in same frame)
150
+ _this2._pendingRefreshKeys = new Set();
151
+
126
152
  // Event listener reference for cleanup
127
153
  _this2._objectTransformedListener = null;
128
154
  console.log('🔲 Viewport2DManager initialized (multi-instance support)');
@@ -212,8 +238,10 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
212
238
  viewport._instanceKey = key;
213
239
  this.viewports.set(key, viewport);
214
240
 
215
- // Initialize the stage for this viewport
216
- this.initializeStage(viewport);
241
+ // Initialize the stage for this viewport (waits for DOM layout to settle)
242
+ _context.n = 4;
243
+ return this.initializeStage(viewport);
244
+ case 4:
217
245
  return _context.a(2, viewport.isReady);
218
246
  }
219
247
  }, _callee, this);
@@ -307,50 +335,67 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
307
335
  /**
308
336
  * Initialize Konva stage for a specific viewport instance
309
337
  * @param {Viewport2DInstance} viewport - The viewport instance to initialize
338
+ * @returns {Promise<void>} Resolves when viewport is fully ready
310
339
  */
311
340
  )
312
341
  }, {
313
342
  key: "initializeStage",
314
343
  value: function initializeStage(viewport) {
315
- if (!this.Konva || !viewport.container) {
316
- console.error('❌ Cannot initialize stage: Konva or container missing');
317
- return;
318
- }
319
-
320
- // Get container dimensions
321
- var rect = viewport.container.getBoundingClientRect();
322
- var width = rect.width || 800;
323
- var height = rect.height || 600;
324
- console.log("\uD83D\uDCD0 Initializing Konva stage (".concat(viewport.viewType, "): ").concat(width, "x").concat(height));
325
-
326
- // Create Konva stage for this viewport
327
- viewport.stage = new this.Konva.Stage({
328
- container: viewport.container,
329
- width: width,
330
- height: height
331
- });
332
-
333
- // Create separate layers for grid and components
334
- viewport.gridLayer = new this.Konva.Layer();
335
- viewport.componentLayer = new this.Konva.Layer();
336
-
337
- // Add layers to stage in order (grid first, then components)
338
- viewport.stage.add(viewport.gridLayer);
339
- viewport.stage.add(viewport.componentLayer);
340
-
341
- // Setup resize handling
342
- this.setupResizeListener(viewport);
343
-
344
- // Setup zoom and pan handlers
345
- this.setupZoomAndPanHandlers(viewport);
344
+ var _this4 = this;
345
+ return new Promise(function (resolve) {
346
+ if (!_this4.Konva || !viewport.container) {
347
+ console.error('❌ Cannot initialize stage: Konva or container missing');
348
+ resolve();
349
+ return;
350
+ }
346
351
 
347
- // Draw initial content
348
- this.drawGrid(viewport);
349
- this.renderComponents(viewport);
352
+ // Get container dimensions
353
+ var rect = viewport.container.getBoundingClientRect();
354
+ var width = rect.width || 800;
355
+ var height = rect.height || 600;
356
+ console.log("\uD83D\uDCD0 Initializing Konva stage (".concat(viewport.viewType, "): ").concat(width, "x").concat(height));
357
+
358
+ // Create Konva stage for this viewport
359
+ viewport.stage = new _this4.Konva.Stage({
360
+ container: viewport.container,
361
+ width: width,
362
+ height: height
363
+ });
350
364
 
351
- // Mark as ready
352
- viewport.isReady = true;
353
- console.log("\u2705 Viewport2DManager stage initialized (".concat(viewport.viewType, ")"));
365
+ // Create separate layers for grid and components
366
+ viewport.gridLayer = new _this4.Konva.Layer();
367
+ viewport.componentLayer = new _this4.Konva.Layer();
368
+
369
+ // Add layers to stage in order (grid first, then components)
370
+ viewport.stage.add(viewport.gridLayer);
371
+ viewport.stage.add(viewport.componentLayer);
372
+
373
+ // Setup resize handling
374
+ _this4.setupResizeListener(viewport);
375
+
376
+ // Setup zoom and pan handlers
377
+ _this4.setupZoomAndPanHandlers(viewport);
378
+
379
+ // Draw initial grid (lightweight, safe to do immediately)
380
+ _this4.drawGrid(viewport);
381
+
382
+ // Defer component rendering to next frame to ensure DOM layout is finalized.
383
+ // Without this, getBoundingClientRect() may return stale/zero dimensions
384
+ // if the container hasn't been fully laid out by CSS yet.
385
+ requestAnimationFrame(function () {
386
+ // Re-check container dimensions after layout settles
387
+ var finalRect = viewport.container.getBoundingClientRect();
388
+ if (finalRect.width > 0 && finalRect.height > 0) {
389
+ viewport.stage.width(finalRect.width);
390
+ viewport.stage.height(finalRect.height);
391
+ _this4.drawGrid(viewport);
392
+ }
393
+ _this4.renderComponents(viewport);
394
+ viewport.isReady = true;
395
+ console.log("\u2705 Viewport2DManager stage initialized (".concat(viewport.viewType, ")"));
396
+ resolve();
397
+ });
398
+ });
354
399
  }
355
400
 
356
401
  /**
@@ -360,12 +405,12 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
360
405
  }, {
361
406
  key: "setupResizeListener",
362
407
  value: function setupResizeListener(viewport) {
363
- var _this4 = this;
408
+ var _this5 = this;
364
409
  if (typeof window === 'undefined' || !window.ResizeObserver || !viewport.container) {
365
410
  return;
366
411
  }
367
412
  viewport._resizeObserver = new ResizeObserver(function () {
368
- _this4.resizeStage(viewport);
413
+ _this5.resizeStage(viewport);
369
414
  });
370
415
  viewport._resizeObserver.observe(viewport.container);
371
416
  }
@@ -400,7 +445,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
400
445
  }, {
401
446
  key: "setupZoomAndPanHandlers",
402
447
  value: function setupZoomAndPanHandlers(viewport) {
403
- var _this5 = this;
448
+ var _this6 = this;
404
449
  if (!viewport.stage) return;
405
450
 
406
451
  // Mouse wheel zoom
@@ -426,7 +471,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
426
471
  y: pointer.y - mousePointTo.y * clampedScale
427
472
  };
428
473
  viewport.stage.position(newPos);
429
- _this5.drawGrid(viewport);
474
+ _this6.drawGrid(viewport);
430
475
  viewport.stage.batchDraw();
431
476
  });
432
477
 
@@ -446,7 +491,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
446
491
  viewport.stage.x(viewport.stage.x() + dx);
447
492
  viewport.stage.y(viewport.stage.y() + dy);
448
493
  viewport.lastPanPoint = pos;
449
- _this5.drawGrid(viewport);
494
+ _this6.drawGrid(viewport);
450
495
  viewport.stage.batchDraw();
451
496
  });
452
497
  viewport.stage.on('mouseup', function () {
@@ -538,7 +583,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
538
583
  }, {
539
584
  key: "renderComponents",
540
585
  value: function renderComponents(viewport) {
541
- var _this6 = this;
586
+ var _this7 = this;
542
587
  if (!viewport.componentLayer || !viewport.stage || !this.sceneViewer) return;
543
588
  viewport.componentLayer.destroyChildren();
544
589
 
@@ -549,19 +594,18 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
549
594
  return;
550
595
  }
551
596
 
552
- // Only log on significant changes
553
- if (components.length !== viewport._lastComponentCount) {
554
- console.log("\uD83D\uDCE6 Rendering ".concat(components.length, " components in 2D view (").concat(viewport.viewType, ")"));
555
- viewport._lastComponentCount = components.length;
556
- }
597
+ // Track render count for debugging
598
+ if (viewport._renderCount === undefined) viewport._renderCount = 0;
599
+ viewport._renderCount++;
557
600
  var width = viewport.stage.width();
558
601
  var height = viewport.stage.height();
559
602
  var centerX = width / 2;
560
603
  var centerY = height / 2;
561
604
  var scale = viewport.PIXELS_PER_UNIT;
605
+ console.log("\uD83C\uDFA8 RENDER #".concat(viewport._renderCount, " (").concat(viewport.viewType, "): ").concat(components.length, " components, stage=").concat(width, "x").concat(height, ", center=(").concat(centerX, ", ").concat(centerY, ")"));
562
606
  components.forEach(function (component) {
563
607
  try {
564
- _this6.renderComponent(viewport, component, centerX, centerY, scale);
608
+ _this7.renderComponent(viewport, component, centerX, centerY, scale);
565
609
  } catch (err) {
566
610
  console.warn('⚠️ Error rendering component in 2D:', component.name, err);
567
611
  }
@@ -592,6 +636,11 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
592
636
  var screenX = centerX + posX * scale;
593
637
  var screenY = centerY - posY * scale; // Flip Y for screen coords
594
638
 
639
+ // Debug: Log ALL component positions on first render only
640
+ if (viewport._renderCount === 1) {
641
+ console.log("\uD83D\uDD0D [".concat(viewport.viewType, "] ").concat(component.name, ": bbox.z=").concat(bboxCenter.z.toFixed(2), ", posY=").concat(posY.toFixed(2), ", screenY=").concat(screenY.toFixed(0), ", centerY=").concat(centerY.toFixed(0)));
642
+ }
643
+
595
644
  // Generate unique color for this component
596
645
  var colors = this.generateComponentColor(component.id);
597
646
 
@@ -649,16 +698,15 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
649
698
  }, {
650
699
  key: "_getOrComputeWorldBoundingBox",
651
700
  value: function _getOrComputeWorldBoundingBox(object) {
652
- var _object$userData, _object$userData2;
653
701
  // Fast path: offset the stored world bbox by the position delta since load time.
654
702
  // Translation only shifts the bbox center — extents stay identical — so this is O(1)
655
703
  // instead of O(meshes × vertices) from a full geometry traversal.
656
- var stored = (_object$userData = object.userData) === null || _object$userData === void 0 ? void 0 : _object$userData.worldBoundingBox;
657
- var basePos = (_object$userData2 = object.userData) === null || _object$userData2 === void 0 ? void 0 : _object$userData2._wbbBasePosition;
658
- if (stored && basePos) {
659
- var dx = object.position.x - basePos.x;
660
- var dy = object.position.y - basePos.y;
661
- var dz = object.position.z - basePos.z;
704
+ var cached = _basePositionCache.get(object);
705
+ if (cached) {
706
+ var dx = object.position.x - cached.x;
707
+ var dy = object.position.y - cached.y;
708
+ var dz = object.position.z - cached.z;
709
+ var stored = cached.worldBoundingBox;
662
710
  if (dx === 0 && dy === 0 && dz === 0) return stored;
663
711
  return {
664
712
  min: [stored.min[0] + dx, stored.min[1] + dy, stored.min[2] + dz],
@@ -670,7 +718,14 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
670
718
  if (this._bboxCache.has(object.uuid)) {
671
719
  return this._bboxCache.get(object.uuid);
672
720
  }
673
- var box = boundingBoxUtils.computeFilteredBoundingBox(object, []);
721
+
722
+ // Force matrix updates before computing bbox to ensure world-space accuracy
723
+ object.updateMatrix();
724
+ object.updateMatrixWorld(true);
725
+
726
+ // Exclude io-devices and connectors to match the stored worldBoundingBox
727
+ // computed in modelManager.replaceWithGLBModels()
728
+ var box = boundingBoxUtils.computeFilteredBoundingBox(object, ['io-device', 'connector']);
674
729
  var result;
675
730
  if (box.isEmpty()) {
676
731
  // Object has no geometry; fall back to a point at world position
@@ -801,7 +856,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
801
856
  }, {
802
857
  key: "addComponentInteractions",
803
858
  value: function addComponentInteractions(viewport, rect, componentGroup, component, worldWidth, worldDepth, worldHeight, bboxCenter, label) {
804
- var _this7 = this;
859
+ var _this8 = this;
805
860
  if (!this.Konva) return;
806
861
  var colors = this.generateComponentColor(component.id);
807
862
 
@@ -830,12 +885,12 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
830
885
  // CLICK EVENT
831
886
  rect.on('click', function () {
832
887
  if (!viewport.isDragging) {
833
- var _this7$sceneViewer;
888
+ var _this8$sceneViewer;
834
889
  console.log("\uD83C\uDFAF Component clicked: ".concat(component.name));
835
890
 
836
891
  // Use centralPlant API to select component
837
- if ((_this7$sceneViewer = _this7.sceneViewer) !== null && _this7$sceneViewer !== void 0 && _this7$sceneViewer.centralPlant && component.uuid) {
838
- _this7.sceneViewer.centralPlant.selectComponent(component.uuid);
892
+ if ((_this8$sceneViewer = _this8.sceneViewer) !== null && _this8$sceneViewer !== void 0 && _this8$sceneViewer.centralPlant && component.uuid) {
893
+ _this8.sceneViewer.centralPlant.selectComponent(component.uuid);
839
894
  }
840
895
  }
841
896
  });
@@ -865,7 +920,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
865
920
  var worldOriginY = stageHeight / 2;
866
921
 
867
922
  // Snap to grid
868
- var snappedPos = _this7.snapScreenToGrid(viewport, currentPos.x, currentPos.y, scale, worldOriginX, worldOriginY);
923
+ var snappedPos = _this8.snapScreenToGrid(viewport, currentPos.x, currentPos.y, scale, worldOriginX, worldOriginY);
869
924
  componentGroup.position(snappedPos);
870
925
  viewport.componentLayer.batchDraw();
871
926
  });
@@ -873,7 +928,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
873
928
  // DRAG END
874
929
  componentGroup.on('dragend', function () {
875
930
  setTimeout(function () {
876
- var _this7$sceneViewer2;
931
+ var _this8$sceneViewer2;
877
932
  viewport.isDragging = false;
878
933
  var finalPos = componentGroup.position();
879
934
  var stageWidth = viewport.stage.width();
@@ -883,17 +938,17 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
883
938
  var worldOriginY = stageHeight / 2;
884
939
 
885
940
  // Convert screen to world coordinates
886
- var worldCoords = _this7.screenToWorldCoords(viewport, finalPos.x, finalPos.y, scale, worldOriginX, worldOriginY);
941
+ var worldCoords = _this8.screenToWorldCoords(viewport, finalPos.x, finalPos.y, scale, worldOriginX, worldOriginY);
887
942
 
888
943
  // Calculate new position: delta from old bbox center to new bbox center
889
944
  var currentPos = component.position;
890
- var newPosition = _this7.worldCoordsToObjectPosition(viewport, worldCoords, currentPos, bboxCenter);
945
+ var newPosition = _this8.worldCoordsToObjectPosition(viewport, worldCoords, currentPos, bboxCenter);
891
946
 
892
947
  // Apply translation via centralPlant API
893
948
  var deltaX = newPosition.x - currentPos.x;
894
949
  var deltaY = newPosition.y - currentPos.y;
895
950
  var deltaZ = newPosition.z - currentPos.z;
896
- if ((_this7$sceneViewer2 = _this7.sceneViewer) !== null && _this7$sceneViewer2 !== void 0 && _this7$sceneViewer2.centralPlant && component.uuid) {
951
+ if ((_this8$sceneViewer2 = _this8.sceneViewer) !== null && _this8$sceneViewer2 !== void 0 && _this8$sceneViewer2.centralPlant && component.uuid) {
897
952
  var success = true;
898
953
 
899
954
  // Suppress per-axis path updates so the pathfinder only runs once,
@@ -901,32 +956,32 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
901
956
  // Running updatePaths() after each individual axis (the default
902
957
  // translateComponent behaviour) produces intermediate wrong results on
903
958
  // the first drag because no cached fingerprint exists yet to skip them.
904
- var wasAutoUpdate = _this7.sceneViewer.shouldUpdatePaths;
905
- _this7.sceneViewer.shouldUpdatePaths = false;
959
+ var wasAutoUpdate = _this8.sceneViewer.shouldUpdatePaths;
960
+ _this8.sceneViewer.shouldUpdatePaths = false;
906
961
  try {
907
962
  if (Math.abs(deltaX) > 0.01) {
908
- success = success && _this7.sceneViewer.centralPlant.translate(component.uuid, 'x', deltaX);
963
+ success = success && _this8.sceneViewer.centralPlant.translate(component.uuid, 'x', deltaX);
909
964
  }
910
965
  if (Math.abs(deltaY) > 0.01) {
911
- success = success && _this7.sceneViewer.centralPlant.translate(component.uuid, 'y', deltaY);
966
+ success = success && _this8.sceneViewer.centralPlant.translate(component.uuid, 'y', deltaY);
912
967
  }
913
968
  if (Math.abs(deltaZ) > 0.01) {
914
- success = success && _this7.sceneViewer.centralPlant.translate(component.uuid, 'z', deltaZ);
969
+ success = success && _this8.sceneViewer.centralPlant.translate(component.uuid, 'z', deltaZ);
915
970
  }
916
971
  } finally {
917
- _this7.sceneViewer.shouldUpdatePaths = wasAutoUpdate;
972
+ _this8.sceneViewer.shouldUpdatePaths = wasAutoUpdate;
918
973
  }
919
- if (!success && _this7.dragStartPosition) {
974
+ if (!success && _this8.dragStartPosition) {
920
975
  console.warn('⚠️ Failed to translate component, reverting position');
921
- componentGroup.position(_this7.dragStartPosition);
976
+ componentGroup.position(_this8.dragStartPosition);
922
977
  } else {
923
978
  console.log("\u2705 Component ".concat(component.name, " translated successfully in 2D viewport"));
924
979
 
925
980
  // Single path update with the final combined position
926
- if (wasAutoUpdate && _this7.sceneViewer) {
981
+ if (wasAutoUpdate && _this8.sceneViewer) {
927
982
  console.log('🔄 Auto-updating paths after 2D viewport translation...');
928
983
  try {
929
- _this7.sceneViewer.updatePaths();
984
+ _this8.sceneViewer.updatePaths();
930
985
  console.log('✅ Paths auto-updated successfully from 2D viewport');
931
986
  } catch (error) {
932
987
  console.error('❌ Error auto-updating paths from 2D viewport:', error);
@@ -1058,10 +1113,10 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
1058
1113
  }
1059
1114
  var components = [];
1060
1115
  this.sceneViewer.scene.traverse(function (object) {
1061
- var _object$userData3, _object$userData4;
1116
+ var _object$userData, _object$userData2;
1062
1117
  // Only match the ROOT component object — must have both objectType:'component'
1063
1118
  // AND libraryId (inner GLB mesh nodes don't have libraryId)
1064
- if (((_object$userData3 = object.userData) === null || _object$userData3 === void 0 ? void 0 : _object$userData3.objectType) === 'component' && (_object$userData4 = object.userData) !== null && _object$userData4 !== void 0 && _object$userData4.libraryId) {
1119
+ if (((_object$userData = object.userData) === null || _object$userData === void 0 ? void 0 : _object$userData.objectType) === 'component' && (_object$userData2 = object.userData) !== null && _object$userData2 !== void 0 && _object$userData2.libraryId) {
1065
1120
  components.push(object);
1066
1121
  }
1067
1122
  });
@@ -1179,28 +1234,39 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
1179
1234
  }, {
1180
1235
  key: "refresh",
1181
1236
  value: function refresh() {
1182
- var _this8 = this;
1237
+ var _this9 = this;
1183
1238
  var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
1239
+ // Collect viewport keys to refresh; null means "all viewports"
1240
+ if (key) {
1241
+ this._pendingRefreshKeys.add(key);
1242
+ } else {
1243
+ // null key means refresh all — mark with special sentinel
1244
+ this._pendingRefreshKeys.add('__ALL__');
1245
+ }
1246
+
1247
+ // If already scheduled, the rAF callback will process our newly-added key
1184
1248
  if (this._refreshPending) return;
1185
1249
  this._refreshPending = true;
1186
1250
  requestAnimationFrame(function () {
1187
- _this8._refreshPending = false;
1251
+ _this9._refreshPending = false;
1252
+
1188
1253
  // Clear per-cycle caches so each component is measured/traversed once per paint
1189
- _this8._bboxCache.clear();
1190
- _this8._componentListCache = null;
1191
- if (key) {
1192
- var viewport = _this8.viewports.get(key);
1193
- if (viewport && viewport.isReady) {
1194
- _this8.renderComponents(viewport);
1195
- }
1196
- } else {
1197
- var _iterator = _rollupPluginBabelHelpers.createForOfIteratorHelper(_this8.viewports.values()),
1254
+ _this9._bboxCache.clear();
1255
+ _this9._componentListCache = null;
1256
+
1257
+ // Check if we need to refresh all viewports
1258
+ var refreshAll = _this9._pendingRefreshKeys.has('__ALL__');
1259
+ var keysToRefresh = new Set(_this9._pendingRefreshKeys);
1260
+ _this9._pendingRefreshKeys.clear();
1261
+ if (refreshAll) {
1262
+ // Refresh all viewports
1263
+ var _iterator = _rollupPluginBabelHelpers.createForOfIteratorHelper(_this9.viewports.values()),
1198
1264
  _step;
1199
1265
  try {
1200
1266
  for (_iterator.s(); !(_step = _iterator.n()).done;) {
1201
- var _viewport = _step.value;
1202
- if (_viewport.isReady) {
1203
- _this8.renderComponents(_viewport);
1267
+ var viewport = _step.value;
1268
+ if (viewport.isReady) {
1269
+ _this9.renderComponents(viewport);
1204
1270
  }
1205
1271
  }
1206
1272
  } catch (err) {
@@ -1208,6 +1274,23 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
1208
1274
  } finally {
1209
1275
  _iterator.f();
1210
1276
  }
1277
+ } else {
1278
+ // Refresh only the specific viewports that were requested
1279
+ var _iterator2 = _rollupPluginBabelHelpers.createForOfIteratorHelper(keysToRefresh),
1280
+ _step2;
1281
+ try {
1282
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
1283
+ var viewportKey = _step2.value;
1284
+ var _viewport = _this9.viewports.get(viewportKey);
1285
+ if (_viewport && _viewport.isReady) {
1286
+ _this9.renderComponents(_viewport);
1287
+ }
1288
+ }
1289
+ } catch (err) {
1290
+ _iterator2.e(err);
1291
+ } finally {
1292
+ _iterator2.f();
1293
+ }
1211
1294
  }
1212
1295
  });
1213
1296
  }
@@ -1228,20 +1311,20 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
1228
1311
  }
1229
1312
 
1230
1313
  // Dispose all viewport instances
1231
- var _iterator2 = _rollupPluginBabelHelpers.createForOfIteratorHelper(this.viewports.entries()),
1232
- _step2;
1314
+ var _iterator3 = _rollupPluginBabelHelpers.createForOfIteratorHelper(this.viewports.entries()),
1315
+ _step3;
1233
1316
  try {
1234
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
1235
- var _step2$value = _rollupPluginBabelHelpers.slicedToArray(_step2.value, 2),
1236
- key = _step2$value[0],
1237
- viewport = _step2$value[1];
1317
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
1318
+ var _step3$value = _rollupPluginBabelHelpers.slicedToArray(_step3.value, 2),
1319
+ key = _step3$value[0],
1320
+ viewport = _step3$value[1];
1238
1321
  console.log("\uD83D\uDDD1\uFE0F Disposing viewport: ".concat(key));
1239
1322
  viewport.dispose();
1240
1323
  }
1241
1324
  } catch (err) {
1242
- _iterator2.e(err);
1325
+ _iterator3.e(err);
1243
1326
  } finally {
1244
- _iterator2.f();
1327
+ _iterator3.f();
1245
1328
  }
1246
1329
  this.viewports.clear();
1247
1330
  this.Konva = null;
@@ -1253,3 +1336,4 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
1253
1336
  }(baseDisposable.BaseDisposable);
1254
1337
 
1255
1338
  exports.Viewport2DManager = Viewport2DManager;
1339
+ exports.cacheBasePosition = cacheBasePosition;
@@ -119,7 +119,12 @@ function computeFilteredBoundingBox(object) {
119
119
 
120
120
  // Build a Set for O(1) lookups
121
121
  var excludeSet = new Set(excludeTypes);
122
- object.updateWorldMatrix(false, true);
122
+
123
+ // Force matrix updates to ensure world-space coordinates are accurate.
124
+ // Using force=true ensures matrices are updated even if matrixWorldNeedsUpdate is false,
125
+ // which can happen after positioning a model before the render loop runs.
126
+ object.updateMatrix();
127
+ object.updateMatrixWorld(true);
123
128
  object.traverse(function (child) {
124
129
  // Only process nodes with geometry (Mesh, SkinnedMesh, etc.)
125
130
  if (!child.geometry) return;
@@ -31,7 +31,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
31
31
  * Initialize the CentralPlant manager
32
32
  *
33
33
  * @constructor
34
- * @version 0.3.12
34
+ * @version 0.3.14
35
35
  * @updated 2025-10-22
36
36
  *
37
37
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -3,6 +3,7 @@ import * as THREE from 'three';
3
3
  import { attachIODevicesToComponent } from '../../utils/ioDeviceUtils.js';
4
4
  import modelPreloader from '../../rendering/modelPreloader.js';
5
5
  import { computeFilteredBoundingBox } from '../../utils/boundingBoxUtils.js';
6
+ import { cacheBasePosition } from './viewport2DManager.js';
6
7
 
7
8
  var ModelManager = /*#__PURE__*/function () {
8
9
  function ModelManager(sceneViewer) {
@@ -561,6 +562,15 @@ var ModelManager = /*#__PURE__*/function () {
561
562
  var jsonData = _ref2.jsonData,
562
563
  glbModel = _ref2.glbModel;
563
564
  if (!glbModel) return;
565
+
566
+ // CRITICAL: Force matrix updates before computing bbox.
567
+ // After loadLibraryModel positions the model, the world matrices may not be
568
+ // invalidated yet. computeFilteredBoundingBox uses updateWorldMatrix(false, true)
569
+ // which only updates if matrixWorldNeedsUpdate is true. Force the update here
570
+ // to ensure the bbox is computed with correct world-space coordinates.
571
+ glbModel.updateMatrix();
572
+ glbModel.updateMatrixWorld(true);
573
+
564
574
  // Use filtered bbox (excludes connectors + io-devices) so it matches
565
575
  // what pathfindingManager._enrichSceneDataWithBoundingBoxes produces
566
576
  var filteredBox = computeFilteredBoundingBox(glbModel, ['io-device', 'connector']);
@@ -571,13 +581,9 @@ var ModelManager = /*#__PURE__*/function () {
571
581
  // Update both the JSON data object AND the live scene object
572
582
  jsonData.userData.worldBoundingBox = worldBoundingBox;
573
583
  glbModel.userData.worldBoundingBox = worldBoundingBox;
574
- // Snapshot the object's local position so viewport2DManager can compute
584
+ // Cache the object's position so viewport2DManager can compute
575
585
  // world-bbox updates via a fast O(1) position delta instead of re-traversing geometry
576
- glbModel.userData._wbbBasePosition = {
577
- x: glbModel.position.x,
578
- y: glbModel.position.y,
579
- z: glbModel.position.z
580
- };
586
+ cacheBasePosition(glbModel, worldBoundingBox);
581
587
  });
582
588
 
583
589
  // Dispatch completion event