@2112-lab/central-plant 0.3.4 → 0.3.6

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.
@@ -4,6 +4,8 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var _rollupPluginBabelHelpers = require('../../../_virtual/_rollupPluginBabelHelpers.js');
6
6
  var baseDisposable = require('../../core/baseDisposable.js');
7
+ var THREE = require('three');
8
+ var boundingBoxUtils = require('../../utils/boundingBoxUtils.js');
7
9
 
8
10
  function _interopNamespace(e) {
9
11
  if (e && e.__esModule) return e;
@@ -23,6 +25,8 @@ function _interopNamespace(e) {
23
25
  return Object.freeze(n);
24
26
  }
25
27
 
28
+ var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
29
+
26
30
  /**
27
31
  * Viewport2DInstance
28
32
  * Represents a single 2D viewport with its own Konva stage and configuration
@@ -107,6 +111,18 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
107
111
  // Map of viewport instances by viewType or custom key
108
112
  _this2.viewports = new Map();
109
113
 
114
+ // Per-refresh-cycle bbox cache: keyed by object.uuid, cleared each refresh()
115
+ // so each component bbox is computed once per cycle regardless of viewport count
116
+ _this2._bboxCache = new Map();
117
+
118
+ // Per-refresh-cycle component list cache: eliminates redundant scene traversals
119
+ // when all 3 viewports render in the same cycle
120
+ _this2._componentListCache = null;
121
+
122
+ // rAF debounce flag — prevents multiple same-frame refresh() calls from
123
+ // stacking up independent renderComponents() runs
124
+ _this2._refreshPending = false;
125
+
110
126
  // Event listener reference for cleanup
111
127
  _this2._objectTransformedListener = null;
112
128
  console.log('🔲 Viewport2DManager initialized (multi-instance support)');
@@ -128,7 +144,6 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
128
144
 
129
145
  // Listen for object transformations to refresh all viewports
130
146
  this._objectTransformedListener = function (eventData) {
131
- console.log('🔲 Viewport2DManager detected object transformation, refreshing all viewports');
132
147
  _this3.refresh(); // Refresh all viewports
133
148
  };
134
149
 
@@ -194,6 +209,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
194
209
  case 3:
195
210
  // Create new viewport instance
196
211
  viewport = new Viewport2DInstance(this.sceneViewer, this.Konva, viewType, container);
212
+ viewport._instanceKey = key;
197
213
  this.viewports.set(key, viewport);
198
214
 
199
215
  // Initialize the stage for this viewport
@@ -371,9 +387,9 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
371
387
  viewport.stage.width(width);
372
388
  viewport.stage.height(height);
373
389
 
374
- // Redraw content
390
+ // Redraw grid immediately; schedule debounced component render
375
391
  this.drawGrid(viewport);
376
- this.renderComponents(viewport);
392
+ this.refresh(viewport._instanceKey);
377
393
  }
378
394
  }
379
395
 
@@ -550,32 +566,19 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
550
566
  }, {
551
567
  key: "renderComponent",
552
568
  value: function renderComponent(viewport, component, centerX, centerY, scale) {
553
- var _component$position$x, _component$position, _component$position$y, _component$position2, _component$position$z, _component$position3, _component$rotation$x, _component$rotation, _component$rotation$y, _component$rotation2, _component$rotation$z, _component$rotation3;
554
- // Get component position and rotation
555
- var pos3D = {
556
- x: (_component$position$x = (_component$position = component.position) === null || _component$position === void 0 ? void 0 : _component$position.x) !== null && _component$position$x !== void 0 ? _component$position$x : 0,
557
- y: (_component$position$y = (_component$position2 = component.position) === null || _component$position2 === void 0 ? void 0 : _component$position2.y) !== null && _component$position$y !== void 0 ? _component$position$y : 0,
558
- z: (_component$position$z = (_component$position3 = component.position) === null || _component$position3 === void 0 ? void 0 : _component$position3.z) !== null && _component$position$z !== void 0 ? _component$position$z : 0
559
- };
560
- var rot3D = {
561
- x: (_component$rotation$x = (_component$rotation = component.rotation) === null || _component$rotation === void 0 ? void 0 : _component$rotation.x) !== null && _component$rotation$x !== void 0 ? _component$rotation$x : 0,
562
- y: (_component$rotation$y = (_component$rotation2 = component.rotation) === null || _component$rotation2 === void 0 ? void 0 : _component$rotation2.y) !== null && _component$rotation$y !== void 0 ? _component$rotation$y : 0,
563
- z: (_component$rotation$z = (_component$rotation3 = component.rotation) === null || _component$rotation3 === void 0 ? void 0 : _component$rotation3.z) !== null && _component$rotation$z !== void 0 ? _component$rotation$z : 0
564
- };
565
-
566
- // Get bounding box dimensions
569
+ // Get world-space bounding box dimensions and center
567
570
  var _this$getComponentDim = this.getComponentDimensions(component),
568
571
  worldWidth = _this$getComponentDim.worldWidth,
569
572
  worldDepth = _this$getComponentDim.worldDepth,
570
- worldHeight = _this$getComponentDim.worldHeight;
573
+ worldHeight = _this$getComponentDim.worldHeight,
574
+ bboxCenter = _this$getComponentDim.bboxCenter;
571
575
 
572
- // Project 3D coordinates to 2D based on view type
573
- var _this$project3DTo2D = this.project3DTo2D(viewport, pos3D, rot3D, worldWidth, worldDepth, worldHeight),
576
+ // Project 3D bbox center to 2D based on view type
577
+ var _this$project3DTo2D = this.project3DTo2D(viewport, bboxCenter, worldWidth, worldDepth, worldHeight),
574
578
  posX = _this$project3DTo2D.posX,
575
579
  posY = _this$project3DTo2D.posY,
576
580
  rectWidth = _this$project3DTo2D.rectWidth,
577
581
  rectHeight = _this$project3DTo2D.rectHeight;
578
- _this$project3DTo2D.rotationDegrees;
579
582
  var screenX = centerX + posX * scale;
580
583
  var screenY = centerY - posY * scale; // Flip Y for screen coords
581
584
 
@@ -619,105 +622,156 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
619
622
  });
620
623
 
621
624
  // Add mouse event handlers
622
- this.addComponentInteractions(viewport, rect, componentGroup, component, worldWidth, worldDepth, worldHeight);
625
+ this.addComponentInteractions(viewport, rect, componentGroup, component, worldWidth, worldDepth, worldHeight, bboxCenter);
623
626
  componentGroup.add(rect);
624
627
  componentGroup.add(label);
625
628
  viewport.componentLayer.add(componentGroup);
626
629
  }
627
630
 
628
631
  /**
629
- * Get component dimensions from various sources
632
+ * Compute worldBoundingBox for a live Three.js Object3D using the same
633
+ * vertex-accurate approach as computeFilteredBoundingBox (explicitly calls
634
+ * geometry.computeBoundingBox() on each mesh before measuring).
635
+ * @param {THREE.Object3D} object
636
+ * @returns {{min: number[], max: number[]}}
637
+ */
638
+ }, {
639
+ key: "_getOrComputeWorldBoundingBox",
640
+ value: function _getOrComputeWorldBoundingBox(object) {
641
+ var _object$userData, _object$userData2;
642
+ // Fast path: offset the stored world bbox by the position delta since load time.
643
+ // Translation only shifts the bbox center — extents stay identical — so this is O(1)
644
+ // instead of O(meshes × vertices) from a full geometry traversal.
645
+ var stored = (_object$userData = object.userData) === null || _object$userData === void 0 ? void 0 : _object$userData.worldBoundingBox;
646
+ var basePos = (_object$userData2 = object.userData) === null || _object$userData2 === void 0 ? void 0 : _object$userData2._wbbBasePosition;
647
+ if (stored && basePos) {
648
+ var dx = object.position.x - basePos.x;
649
+ var dy = object.position.y - basePos.y;
650
+ var dz = object.position.z - basePos.z;
651
+ if (dx === 0 && dy === 0 && dz === 0) return stored;
652
+ return {
653
+ min: [stored.min[0] + dx, stored.min[1] + dy, stored.min[2] + dz],
654
+ max: [stored.max[0] + dx, stored.max[1] + dy, stored.max[2] + dz]
655
+ };
656
+ }
657
+
658
+ // Slow path: full vertex-accurate traversal (fallback when userData not populated)
659
+ if (this._bboxCache.has(object.uuid)) {
660
+ return this._bboxCache.get(object.uuid);
661
+ }
662
+ var box = boundingBoxUtils.computeFilteredBoundingBox(object, []);
663
+ var result;
664
+ if (box.isEmpty()) {
665
+ // Object has no geometry; fall back to a point at world position
666
+ var wp = new THREE__namespace.Vector3();
667
+ object.getWorldPosition(wp);
668
+ result = {
669
+ min: [wp.x, wp.y, wp.z],
670
+ max: [wp.x, wp.y, wp.z]
671
+ };
672
+ } else {
673
+ result = {
674
+ min: [box.min.x, box.min.y, box.min.z],
675
+ max: [box.max.x, box.max.y, box.max.z]
676
+ };
677
+ }
678
+ this._bboxCache.set(object.uuid, result);
679
+ return result;
680
+ }
681
+
682
+ /**
683
+ * Get component dimensions and world-space center from worldBoundingBox
630
684
  */
631
685
  }, {
632
686
  key: "getComponentDimensions",
633
687
  value: function getComponentDimensions(component) {
634
- var _component$userData, _component$userData2, _component$geometry;
635
- var worldWidth = 1,
636
- worldHeight = 1,
637
- worldDepth = 1;
638
-
639
- // Try adaptedBoundingBox first
640
- if ((_component$userData = component.userData) !== null && _component$userData !== void 0 && _component$userData.adaptedBoundingBox) {
641
- var bbox = component.userData.adaptedBoundingBox;
642
- if (bbox.max && bbox.min) {
643
- worldWidth = Math.abs(bbox.max.x - bbox.min.x);
644
- worldDepth = Math.abs(bbox.max.y - bbox.min.y);
645
- worldHeight = Math.abs(bbox.max.z - bbox.min.z);
688
+ var _component$getWorldPo;
689
+ // Always recompute from the live Three.js object so that the rect reflects
690
+ // the current world position after translate/drag operations.
691
+ // userData.worldBoundingBox is a load-time snapshot and goes stale whenever
692
+ // the object moves, so we cannot rely on it here.
693
+ var wbb = this._getOrComputeWorldBoundingBox(component);
694
+ if (wbb !== null && wbb !== void 0 && wbb.min && wbb !== null && wbb !== void 0 && wbb.max) {
695
+ var _wbb$min = _rollupPluginBabelHelpers.slicedToArray(wbb.min, 3),
696
+ minX = _wbb$min[0],
697
+ minY = _wbb$min[1],
698
+ minZ = _wbb$min[2];
699
+ var _wbb$max = _rollupPluginBabelHelpers.slicedToArray(wbb.max, 3),
700
+ maxX = _wbb$max[0],
701
+ maxY = _wbb$max[1],
702
+ maxZ = _wbb$max[2];
703
+ var cx = (minX + maxX) / 2;
704
+ var cy = (minY + maxY) / 2;
705
+ var cz = (minZ + maxZ) / 2;
706
+ // Guard against Infinity/NaN from empty or degenerate boxes
707
+ if (isFinite(cx) && isFinite(cy) && isFinite(cz)) {
708
+ return {
709
+ worldWidth: Math.max(maxX - minX, 0.01),
710
+ worldDepth: Math.max(maxY - minY, 0.01),
711
+ worldHeight: Math.max(maxZ - minZ, 0.01),
712
+ bboxCenter: {
713
+ x: cx,
714
+ y: cy,
715
+ z: cz
716
+ }
717
+ };
646
718
  }
647
719
  }
648
- // Fallback to dimensions from userData
649
- else if ((_component$userData2 = component.userData) !== null && _component$userData2 !== void 0 && _component$userData2.dimensions) {
650
- var dims = component.userData.dimensions;
651
- worldWidth = Math.abs(dims.x);
652
- worldDepth = Math.abs(dims.y);
653
- worldHeight = Math.abs(dims.z);
654
- }
655
- // Last resort: geometry bounding box
656
- else if ((_component$geometry = component.geometry) !== null && _component$geometry !== void 0 && _component$geometry.boundingBox) {
657
- var _bbox = component.geometry.boundingBox;
658
- worldWidth = Math.abs(_bbox.max.x - _bbox.min.x);
659
- worldDepth = Math.abs(_bbox.max.y - _bbox.min.y);
660
- worldHeight = Math.abs(_bbox.max.z - _bbox.min.z);
661
- }
720
+ // Fallback: world position of the object, unit dimensions
721
+ var wp = new THREE__namespace.Vector3();
722
+ (_component$getWorldPo = component.getWorldPosition) === null || _component$getWorldPo === void 0 || _component$getWorldPo.call(component, wp);
662
723
  return {
663
- worldWidth: worldWidth,
664
- worldDepth: worldDepth,
665
- worldHeight: worldHeight
724
+ worldWidth: 1,
725
+ worldDepth: 1,
726
+ worldHeight: 1,
727
+ bboxCenter: {
728
+ x: wp.x,
729
+ y: wp.y,
730
+ z: wp.z
731
+ }
666
732
  };
667
733
  }
668
734
 
669
735
  /**
670
- * Project 3D coordinates to 2D based on view type
736
+ * Project world-space bbox center to 2D based on view type.
737
+ * worldBoundingBox is an AABB so rotation is already encoded in the extents —
738
+ * no separate rotation correction is needed.
671
739
  * @param {Viewport2DInstance} viewport - The viewport instance
740
+ * @param {Object} bboxCenter - World-space center {x, y, z}
741
+ * @param {number} worldWidth - X extent (max[0] - min[0])
742
+ * @param {number} worldDepth - Y extent (max[1] - min[1])
743
+ * @param {number} worldHeight - Z extent (max[2] - min[2])
672
744
  */
673
745
  }, {
674
746
  key: "project3DTo2D",
675
- value: function project3DTo2D(viewport, pos3D, rot3D, worldWidth, worldDepth, worldHeight) {
676
- var posX, posY, rectWidth, rectHeight;
677
- var rotationAngle = rot3D.z;
678
- var rotationDegrees = rotationAngle * 180 / Math.PI;
747
+ value: function project3DTo2D(viewport, bboxCenter, worldWidth, worldDepth, worldHeight) {
679
748
  var scale = viewport.PIXELS_PER_UNIT;
749
+ var posX, posY, rectWidth, rectHeight;
680
750
  switch (viewport.viewType) {
681
751
  case 'top':
682
- // Top view: Looking down Z-axis, X-Y plane
683
- posX = pos3D.x;
684
- posY = pos3D.y;
685
-
686
- // Swap width and depth when rotated 90° or 270°
687
- if (Math.abs(rotationDegrees) === 90 || Math.abs(rotationDegrees) === 270) {
688
- rectWidth = worldDepth * scale;
689
- rectHeight = worldWidth * scale;
690
- } else {
691
- rectWidth = worldWidth * scale;
692
- rectHeight = worldDepth * scale;
693
- }
752
+ // Looking down Z-axis X/Y plane
753
+ posX = bboxCenter.x;
754
+ posY = bboxCenter.y;
755
+ rectWidth = worldWidth * scale;
756
+ rectHeight = worldDepth * scale;
694
757
  break;
695
758
  case 'front':
696
- // Front view: Looking along Y-axis, X-Z plane
697
- posX = pos3D.x;
698
- posY = pos3D.z + worldHeight / 2; // Offset Z by half height
699
-
700
- if (Math.abs(rotationDegrees) === 90 || Math.abs(rotationDegrees) === 270) {
701
- rectWidth = worldDepth * scale;
702
- } else {
703
- rectWidth = worldWidth * scale;
704
- }
759
+ // Looking along Y-axis X/Z plane
760
+ posX = bboxCenter.x;
761
+ posY = bboxCenter.z;
762
+ rectWidth = worldWidth * scale;
705
763
  rectHeight = worldHeight * scale;
706
764
  break;
707
765
  case 'side':
708
- // Side view: Looking along X-axis, Y-Z plane (flipped)
709
- posX = -pos3D.y; // Flipped
710
- posY = pos3D.z + worldHeight / 2;
711
- if (Math.abs(rotationDegrees) === 90 || Math.abs(rotationDegrees) === 270) {
712
- rectWidth = worldWidth * scale;
713
- } else {
714
- rectWidth = worldDepth * scale;
715
- }
766
+ // Looking along X-axis Y/Z plane (Y negated for left-right orientation)
767
+ posX = -bboxCenter.y;
768
+ posY = bboxCenter.z;
769
+ rectWidth = worldDepth * scale;
716
770
  rectHeight = worldHeight * scale;
717
771
  break;
718
772
  default:
719
- posX = pos3D.x;
720
- posY = pos3D.y;
773
+ posX = bboxCenter.x;
774
+ posY = bboxCenter.y;
721
775
  rectWidth = worldWidth * scale;
722
776
  rectHeight = worldDepth * scale;
723
777
  }
@@ -725,8 +779,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
725
779
  posX: posX,
726
780
  posY: posY,
727
781
  rectWidth: rectWidth,
728
- rectHeight: rectHeight,
729
- rotationDegrees: rotationDegrees
782
+ rectHeight: rectHeight
730
783
  };
731
784
  }
732
785
 
@@ -736,7 +789,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
736
789
  */
737
790
  }, {
738
791
  key: "addComponentInteractions",
739
- value: function addComponentInteractions(viewport, rect, componentGroup, component, worldWidth, worldDepth, worldHeight) {
792
+ value: function addComponentInteractions(viewport, rect, componentGroup, component, worldWidth, worldDepth, worldHeight, bboxCenter) {
740
793
  var _this6 = this;
741
794
  if (!this.Konva) return;
742
795
  var colors = this.generateComponentColor(component.id);
@@ -748,7 +801,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
748
801
  rect.stroke('#007bff');
749
802
  rect.strokeWidth(3);
750
803
  viewport.stage.container().style.cursor = 'grab';
751
- viewport.componentLayer.draw();
804
+ viewport.componentLayer.batchDraw();
752
805
  }
753
806
  });
754
807
  rect.on('mouseleave', function () {
@@ -757,7 +810,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
757
810
  rect.stroke(colors.stroke);
758
811
  rect.strokeWidth(2);
759
812
  viewport.stage.container().style.cursor = 'default';
760
- viewport.componentLayer.draw();
813
+ viewport.componentLayer.batchDraw();
761
814
  }
762
815
  });
763
816
 
@@ -801,7 +854,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
801
854
  // Snap to grid
802
855
  var snappedPos = _this6.snapScreenToGrid(viewport, currentPos.x, currentPos.y, scale, worldOriginX, worldOriginY);
803
856
  componentGroup.position(snappedPos);
804
- viewport.componentLayer.draw();
857
+ viewport.componentLayer.batchDraw();
805
858
  });
806
859
 
807
860
  // DRAG END
@@ -819,9 +872,9 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
819
872
  // Convert screen to world coordinates
820
873
  var worldCoords = _this6.screenToWorldCoords(viewport, finalPos.x, finalPos.y, scale, worldOriginX, worldOriginY);
821
874
 
822
- // Calculate new position
875
+ // Calculate new position: delta from old bbox center to new bbox center
823
876
  var currentPos = component.position;
824
- var newPosition = _this6.worldCoordsToObjectPosition(viewport, worldCoords, currentPos, worldWidth, worldDepth, worldHeight);
877
+ var newPosition = _this6.worldCoordsToObjectPosition(viewport, worldCoords, currentPos, bboxCenter);
825
878
 
826
879
  // Apply translation via centralPlant API
827
880
  var deltaX = newPosition.x - currentPos.x;
@@ -924,37 +977,45 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
924
977
  }
925
978
 
926
979
  /**
927
- * Convert world coordinates to 3D object position
980
+ * Convert dragged 2D world coordinates to a new 3D object position.
981
+ * coord1/coord2 represent where the bbox CENTER should be in the view's 2D plane;
982
+ * we compute the delta from the old bbox center and apply it to the pivot position.
928
983
  * @param {Viewport2DInstance} viewport - The viewport instance
984
+ * @param {{coord1, coord2}} worldCoords - Projected world coordinates from screen
985
+ * @param {{x,y,z}} currentPosition - Current Three.js object position (pivot)
986
+ * @param {{x,y,z}} bboxCenter - Old world-space bbox center
929
987
  */
930
988
  }, {
931
989
  key: "worldCoordsToObjectPosition",
932
- value: function worldCoordsToObjectPosition(viewport, worldCoords, currentPosition, worldWidth, worldDepth, worldHeight) {
990
+ value: function worldCoordsToObjectPosition(viewport, worldCoords, currentPosition, bboxCenter) {
933
991
  var coord1 = worldCoords.coord1,
934
992
  coord2 = worldCoords.coord2;
935
993
  switch (viewport.viewType) {
936
994
  case 'top':
995
+ // coord1=X, coord2=Y in world space
937
996
  return {
938
- x: coord1,
939
- y: coord2,
997
+ x: currentPosition.x + (coord1 - bboxCenter.x),
998
+ y: currentPosition.y + (coord2 - bboxCenter.y),
940
999
  z: currentPosition.z
941
1000
  };
942
1001
  case 'front':
1002
+ // coord1=X, coord2=Z in world space
943
1003
  return {
944
- x: coord1,
1004
+ x: currentPosition.x + (coord1 - bboxCenter.x),
945
1005
  y: currentPosition.y,
946
- z: coord2 - worldHeight / 2
1006
+ z: currentPosition.z + (coord2 - bboxCenter.z)
947
1007
  };
948
1008
  case 'side':
1009
+ // coord1=-Y (negated), coord2=Z in world space
949
1010
  return {
950
1011
  x: currentPosition.x,
951
- y: -coord1,
952
- z: coord2 - worldHeight / 2
1012
+ y: currentPosition.y + (-coord1 - bboxCenter.y),
1013
+ z: currentPosition.z + (coord2 - bboxCenter.z)
953
1014
  };
954
1015
  default:
955
1016
  return {
956
- x: coord1,
957
- y: coord2,
1017
+ x: currentPosition.x + (coord1 - bboxCenter.x),
1018
+ y: currentPosition.y + (coord2 - bboxCenter.y),
958
1019
  z: currentPosition.z
959
1020
  };
960
1021
  }
@@ -966,17 +1027,20 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
966
1027
  }, {
967
1028
  key: "getSceneComponents",
968
1029
  value: function getSceneComponents() {
1030
+ if (this._componentListCache) return this._componentListCache;
969
1031
  if (!this.sceneViewer || !this.sceneViewer.scene) {
970
1032
  return [];
971
1033
  }
972
1034
  var components = [];
973
1035
  this.sceneViewer.scene.traverse(function (object) {
974
- var _object$userData, _object$userData2;
975
- var objectType = ((_object$userData = object.userData) === null || _object$userData === void 0 ? void 0 : _object$userData.objectType) || ((_object$userData2 = object.userData) === null || _object$userData2 === void 0 ? void 0 : _object$userData2.objectType);
976
- if (object.userData && objectType === 'component') {
1036
+ var _object$userData3, _object$userData4;
1037
+ // Only match the ROOT component object must have both objectType:'component'
1038
+ // AND libraryId (inner GLB mesh nodes don't have libraryId)
1039
+ 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) {
977
1040
  components.push(object);
978
1041
  }
979
1042
  });
1043
+ this._componentListCache = components;
980
1044
  return components;
981
1045
  }
982
1046
 
@@ -1082,35 +1146,45 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
1082
1146
  }
1083
1147
 
1084
1148
  /**
1085
- * Refresh a specific viewport or all viewports
1149
+ * Refresh a specific viewport or all viewports.
1150
+ * Debounced via requestAnimationFrame so multiple calls within the same
1151
+ * frame (e.g. from Viewport2D mount + refreshAll2DViews) collapse into one.
1086
1152
  * @param {string} key - Optional viewport key. If not provided, refreshes all viewports
1087
1153
  */
1088
1154
  }, {
1089
1155
  key: "refresh",
1090
1156
  value: function refresh() {
1157
+ var _this7 = this;
1091
1158
  var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
1092
- if (key) {
1093
- var viewport = this.viewports.get(key);
1094
- if (viewport && viewport.isReady) {
1095
- this.renderComponents(viewport);
1096
- }
1097
- } else {
1098
- // Refresh all viewports
1099
- var _iterator = _rollupPluginBabelHelpers.createForOfIteratorHelper(this.viewports.values()),
1100
- _step;
1101
- try {
1102
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
1103
- var _viewport = _step.value;
1104
- if (_viewport.isReady) {
1105
- this.renderComponents(_viewport);
1159
+ if (this._refreshPending) return;
1160
+ this._refreshPending = true;
1161
+ requestAnimationFrame(function () {
1162
+ _this7._refreshPending = false;
1163
+ // Clear per-cycle caches so each component is measured/traversed once per paint
1164
+ _this7._bboxCache.clear();
1165
+ _this7._componentListCache = null;
1166
+ if (key) {
1167
+ var viewport = _this7.viewports.get(key);
1168
+ if (viewport && viewport.isReady) {
1169
+ _this7.renderComponents(viewport);
1170
+ }
1171
+ } else {
1172
+ var _iterator = _rollupPluginBabelHelpers.createForOfIteratorHelper(_this7.viewports.values()),
1173
+ _step;
1174
+ try {
1175
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
1176
+ var _viewport = _step.value;
1177
+ if (_viewport.isReady) {
1178
+ _this7.renderComponents(_viewport);
1179
+ }
1106
1180
  }
1181
+ } catch (err) {
1182
+ _iterator.e(err);
1183
+ } finally {
1184
+ _iterator.f();
1107
1185
  }
1108
- } catch (err) {
1109
- _iterator.e(err);
1110
- } finally {
1111
- _iterator.f();
1112
1186
  }
1113
- }
1187
+ });
1114
1188
  }
1115
1189
 
1116
1190
  /**
@@ -226,10 +226,10 @@ var SceneClearingUtility = /*#__PURE__*/function () {
226
226
  throw new Error('Scene not available for clearing');
227
227
  case 1:
228
228
  componentsToRemove = [];
229
- scene = this.sceneViewer.scene; // Collect only component objects
229
+ scene = this.sceneViewer.scene; // Collect component, segment, and gateway objects
230
230
  scene.traverse(function (child) {
231
231
  if (child === scene) return;
232
- var isComponent = child.userData && (child.userData.objectType === 'component' || child.userData.objectType === 'component');
232
+ var isComponent = child.userData && (child.userData.objectType === 'component' || child.userData.objectType === 'segment' || child.userData.objectType === 'gateway' || child.userData.objectType === 'connector');
233
233
  var isDirectChild = child.parent === scene;
234
234
  if (isComponent && isDirectChild) {
235
235
  componentsToRemove.push(child);
@@ -31,7 +31,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
31
31
  * Initialize the CentralPlant manager
32
32
  *
33
33
  * @constructor
34
- * @version 0.3.4
34
+ * @version 0.3.6
35
35
  * @updated 2025-10-22
36
36
  *
37
37
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -2,6 +2,7 @@ import { createClass as _createClass, objectSpread2 as _objectSpread2, toConsuma
2
2
  import * as THREE from 'three';
3
3
  import { attachIODevicesToComponent } from '../../utils/ioDeviceUtils.js';
4
4
  import modelPreloader from '../../rendering/modelPreloader.js';
5
+ import { computeFilteredBoundingBox } from '../../utils/boundingBoxUtils.js';
5
6
 
6
7
  var ModelManager = /*#__PURE__*/function () {
7
8
  function ModelManager(sceneViewer) {
@@ -555,15 +556,28 @@ var ModelManager = /*#__PURE__*/function () {
555
556
  _context5.n = 2;
556
557
  return Promise.all(glbLoadingPromises);
557
558
  case 2:
558
- // Update world bounding boxes for loaded models
559
+ // Update world bounding boxes for loaded models and propagate to the live Three.js objects
559
560
  libraryObjectsToReplace.forEach(function (_ref2) {
560
- var _jsonData$userData2;
561
561
  var jsonData = _ref2.jsonData,
562
562
  glbModel = _ref2.glbModel;
563
- if (glbModel && (_jsonData$userData2 = jsonData.userData) !== null && _jsonData$userData2 !== void 0 && _jsonData$userData2.worldBoundingBox) {
564
- var worldBoundingBox = _this3._calculateWorldBoundingBox(glbModel);
565
- jsonData.userData.worldBoundingBox = worldBoundingBox;
566
- }
563
+ if (!glbModel) return;
564
+ // Use filtered bbox (excludes connectors + io-devices) so it matches
565
+ // what pathfindingManager._enrichSceneDataWithBoundingBoxes produces
566
+ var filteredBox = computeFilteredBoundingBox(glbModel, ['io-device', 'connector']);
567
+ var worldBoundingBox = filteredBox.isEmpty() ? _this3._calculateWorldBoundingBox(glbModel) : {
568
+ min: [filteredBox.min.x, filteredBox.min.y, filteredBox.min.z],
569
+ max: [filteredBox.max.x, filteredBox.max.y, filteredBox.max.z]
570
+ };
571
+ // Update both the JSON data object AND the live scene object
572
+ jsonData.userData.worldBoundingBox = worldBoundingBox;
573
+ glbModel.userData.worldBoundingBox = worldBoundingBox;
574
+ // Snapshot the object's local position so viewport2DManager can compute
575
+ // 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
+ };
567
581
  });
568
582
 
569
583
  // Dispatch completion event