@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.
@@ -1,5 +1,7 @@
1
- import { inherits as _inherits, createClass as _createClass, createForOfIteratorHelper as _createForOfIteratorHelper, slicedToArray as _slicedToArray, superPropGet as _superPropGet, classCallCheck as _classCallCheck, callSuper as _callSuper, asyncToGenerator as _asyncToGenerator, regenerator as _regenerator } from '../../../_virtual/_rollupPluginBabelHelpers.js';
1
+ import { inherits as _inherits, createClass as _createClass, slicedToArray as _slicedToArray, createForOfIteratorHelper as _createForOfIteratorHelper, superPropGet as _superPropGet, classCallCheck as _classCallCheck, callSuper as _callSuper, asyncToGenerator as _asyncToGenerator, regenerator as _regenerator } from '../../../_virtual/_rollupPluginBabelHelpers.js';
2
2
  import { BaseDisposable } from '../../core/baseDisposable.js';
3
+ import * as THREE from 'three';
4
+ import { computeFilteredBoundingBox } from '../../utils/boundingBoxUtils.js';
3
5
 
4
6
  /**
5
7
  * Viewport2DInstance
@@ -85,6 +87,18 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
85
87
  // Map of viewport instances by viewType or custom key
86
88
  _this2.viewports = new Map();
87
89
 
90
+ // Per-refresh-cycle bbox cache: keyed by object.uuid, cleared each refresh()
91
+ // so each component bbox is computed once per cycle regardless of viewport count
92
+ _this2._bboxCache = new Map();
93
+
94
+ // Per-refresh-cycle component list cache: eliminates redundant scene traversals
95
+ // when all 3 viewports render in the same cycle
96
+ _this2._componentListCache = null;
97
+
98
+ // rAF debounce flag — prevents multiple same-frame refresh() calls from
99
+ // stacking up independent renderComponents() runs
100
+ _this2._refreshPending = false;
101
+
88
102
  // Event listener reference for cleanup
89
103
  _this2._objectTransformedListener = null;
90
104
  console.log('🔲 Viewport2DManager initialized (multi-instance support)');
@@ -106,7 +120,6 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
106
120
 
107
121
  // Listen for object transformations to refresh all viewports
108
122
  this._objectTransformedListener = function (eventData) {
109
- console.log('🔲 Viewport2DManager detected object transformation, refreshing all viewports');
110
123
  _this3.refresh(); // Refresh all viewports
111
124
  };
112
125
 
@@ -172,6 +185,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
172
185
  case 3:
173
186
  // Create new viewport instance
174
187
  viewport = new Viewport2DInstance(this.sceneViewer, this.Konva, viewType, container);
188
+ viewport._instanceKey = key;
175
189
  this.viewports.set(key, viewport);
176
190
 
177
191
  // Initialize the stage for this viewport
@@ -349,9 +363,9 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
349
363
  viewport.stage.width(width);
350
364
  viewport.stage.height(height);
351
365
 
352
- // Redraw content
366
+ // Redraw grid immediately; schedule debounced component render
353
367
  this.drawGrid(viewport);
354
- this.renderComponents(viewport);
368
+ this.refresh(viewport._instanceKey);
355
369
  }
356
370
  }
357
371
 
@@ -528,32 +542,19 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
528
542
  }, {
529
543
  key: "renderComponent",
530
544
  value: function renderComponent(viewport, component, centerX, centerY, scale) {
531
- 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;
532
- // Get component position and rotation
533
- var pos3D = {
534
- 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,
535
- 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,
536
- 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
537
- };
538
- var rot3D = {
539
- 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,
540
- 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,
541
- 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
542
- };
543
-
544
- // Get bounding box dimensions
545
+ // Get world-space bounding box dimensions and center
545
546
  var _this$getComponentDim = this.getComponentDimensions(component),
546
547
  worldWidth = _this$getComponentDim.worldWidth,
547
548
  worldDepth = _this$getComponentDim.worldDepth,
548
- worldHeight = _this$getComponentDim.worldHeight;
549
+ worldHeight = _this$getComponentDim.worldHeight,
550
+ bboxCenter = _this$getComponentDim.bboxCenter;
549
551
 
550
- // Project 3D coordinates to 2D based on view type
551
- var _this$project3DTo2D = this.project3DTo2D(viewport, pos3D, rot3D, worldWidth, worldDepth, worldHeight),
552
+ // Project 3D bbox center to 2D based on view type
553
+ var _this$project3DTo2D = this.project3DTo2D(viewport, bboxCenter, worldWidth, worldDepth, worldHeight),
552
554
  posX = _this$project3DTo2D.posX,
553
555
  posY = _this$project3DTo2D.posY,
554
556
  rectWidth = _this$project3DTo2D.rectWidth,
555
557
  rectHeight = _this$project3DTo2D.rectHeight;
556
- _this$project3DTo2D.rotationDegrees;
557
558
  var screenX = centerX + posX * scale;
558
559
  var screenY = centerY - posY * scale; // Flip Y for screen coords
559
560
 
@@ -597,105 +598,156 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
597
598
  });
598
599
 
599
600
  // Add mouse event handlers
600
- this.addComponentInteractions(viewport, rect, componentGroup, component, worldWidth, worldDepth, worldHeight);
601
+ this.addComponentInteractions(viewport, rect, componentGroup, component, worldWidth, worldDepth, worldHeight, bboxCenter);
601
602
  componentGroup.add(rect);
602
603
  componentGroup.add(label);
603
604
  viewport.componentLayer.add(componentGroup);
604
605
  }
605
606
 
606
607
  /**
607
- * Get component dimensions from various sources
608
+ * Compute worldBoundingBox for a live Three.js Object3D using the same
609
+ * vertex-accurate approach as computeFilteredBoundingBox (explicitly calls
610
+ * geometry.computeBoundingBox() on each mesh before measuring).
611
+ * @param {THREE.Object3D} object
612
+ * @returns {{min: number[], max: number[]}}
613
+ */
614
+ }, {
615
+ key: "_getOrComputeWorldBoundingBox",
616
+ value: function _getOrComputeWorldBoundingBox(object) {
617
+ var _object$userData, _object$userData2;
618
+ // Fast path: offset the stored world bbox by the position delta since load time.
619
+ // Translation only shifts the bbox center — extents stay identical — so this is O(1)
620
+ // instead of O(meshes × vertices) from a full geometry traversal.
621
+ var stored = (_object$userData = object.userData) === null || _object$userData === void 0 ? void 0 : _object$userData.worldBoundingBox;
622
+ var basePos = (_object$userData2 = object.userData) === null || _object$userData2 === void 0 ? void 0 : _object$userData2._wbbBasePosition;
623
+ if (stored && basePos) {
624
+ var dx = object.position.x - basePos.x;
625
+ var dy = object.position.y - basePos.y;
626
+ var dz = object.position.z - basePos.z;
627
+ if (dx === 0 && dy === 0 && dz === 0) return stored;
628
+ return {
629
+ min: [stored.min[0] + dx, stored.min[1] + dy, stored.min[2] + dz],
630
+ max: [stored.max[0] + dx, stored.max[1] + dy, stored.max[2] + dz]
631
+ };
632
+ }
633
+
634
+ // Slow path: full vertex-accurate traversal (fallback when userData not populated)
635
+ if (this._bboxCache.has(object.uuid)) {
636
+ return this._bboxCache.get(object.uuid);
637
+ }
638
+ var box = computeFilteredBoundingBox(object, []);
639
+ var result;
640
+ if (box.isEmpty()) {
641
+ // Object has no geometry; fall back to a point at world position
642
+ var wp = new THREE.Vector3();
643
+ object.getWorldPosition(wp);
644
+ result = {
645
+ min: [wp.x, wp.y, wp.z],
646
+ max: [wp.x, wp.y, wp.z]
647
+ };
648
+ } else {
649
+ result = {
650
+ min: [box.min.x, box.min.y, box.min.z],
651
+ max: [box.max.x, box.max.y, box.max.z]
652
+ };
653
+ }
654
+ this._bboxCache.set(object.uuid, result);
655
+ return result;
656
+ }
657
+
658
+ /**
659
+ * Get component dimensions and world-space center from worldBoundingBox
608
660
  */
609
661
  }, {
610
662
  key: "getComponentDimensions",
611
663
  value: function getComponentDimensions(component) {
612
- var _component$userData, _component$userData2, _component$geometry;
613
- var worldWidth = 1,
614
- worldHeight = 1,
615
- worldDepth = 1;
616
-
617
- // Try adaptedBoundingBox first
618
- if ((_component$userData = component.userData) !== null && _component$userData !== void 0 && _component$userData.adaptedBoundingBox) {
619
- var bbox = component.userData.adaptedBoundingBox;
620
- if (bbox.max && bbox.min) {
621
- worldWidth = Math.abs(bbox.max.x - bbox.min.x);
622
- worldDepth = Math.abs(bbox.max.y - bbox.min.y);
623
- worldHeight = Math.abs(bbox.max.z - bbox.min.z);
664
+ var _component$getWorldPo;
665
+ // Always recompute from the live Three.js object so that the rect reflects
666
+ // the current world position after translate/drag operations.
667
+ // userData.worldBoundingBox is a load-time snapshot and goes stale whenever
668
+ // the object moves, so we cannot rely on it here.
669
+ var wbb = this._getOrComputeWorldBoundingBox(component);
670
+ if (wbb !== null && wbb !== void 0 && wbb.min && wbb !== null && wbb !== void 0 && wbb.max) {
671
+ var _wbb$min = _slicedToArray(wbb.min, 3),
672
+ minX = _wbb$min[0],
673
+ minY = _wbb$min[1],
674
+ minZ = _wbb$min[2];
675
+ var _wbb$max = _slicedToArray(wbb.max, 3),
676
+ maxX = _wbb$max[0],
677
+ maxY = _wbb$max[1],
678
+ maxZ = _wbb$max[2];
679
+ var cx = (minX + maxX) / 2;
680
+ var cy = (minY + maxY) / 2;
681
+ var cz = (minZ + maxZ) / 2;
682
+ // Guard against Infinity/NaN from empty or degenerate boxes
683
+ if (isFinite(cx) && isFinite(cy) && isFinite(cz)) {
684
+ return {
685
+ worldWidth: Math.max(maxX - minX, 0.01),
686
+ worldDepth: Math.max(maxY - minY, 0.01),
687
+ worldHeight: Math.max(maxZ - minZ, 0.01),
688
+ bboxCenter: {
689
+ x: cx,
690
+ y: cy,
691
+ z: cz
692
+ }
693
+ };
624
694
  }
625
695
  }
626
- // Fallback to dimensions from userData
627
- else if ((_component$userData2 = component.userData) !== null && _component$userData2 !== void 0 && _component$userData2.dimensions) {
628
- var dims = component.userData.dimensions;
629
- worldWidth = Math.abs(dims.x);
630
- worldDepth = Math.abs(dims.y);
631
- worldHeight = Math.abs(dims.z);
632
- }
633
- // Last resort: geometry bounding box
634
- else if ((_component$geometry = component.geometry) !== null && _component$geometry !== void 0 && _component$geometry.boundingBox) {
635
- var _bbox = component.geometry.boundingBox;
636
- worldWidth = Math.abs(_bbox.max.x - _bbox.min.x);
637
- worldDepth = Math.abs(_bbox.max.y - _bbox.min.y);
638
- worldHeight = Math.abs(_bbox.max.z - _bbox.min.z);
639
- }
696
+ // Fallback: world position of the object, unit dimensions
697
+ var wp = new THREE.Vector3();
698
+ (_component$getWorldPo = component.getWorldPosition) === null || _component$getWorldPo === void 0 || _component$getWorldPo.call(component, wp);
640
699
  return {
641
- worldWidth: worldWidth,
642
- worldDepth: worldDepth,
643
- worldHeight: worldHeight
700
+ worldWidth: 1,
701
+ worldDepth: 1,
702
+ worldHeight: 1,
703
+ bboxCenter: {
704
+ x: wp.x,
705
+ y: wp.y,
706
+ z: wp.z
707
+ }
644
708
  };
645
709
  }
646
710
 
647
711
  /**
648
- * Project 3D coordinates to 2D based on view type
712
+ * Project world-space bbox center to 2D based on view type.
713
+ * worldBoundingBox is an AABB so rotation is already encoded in the extents —
714
+ * no separate rotation correction is needed.
649
715
  * @param {Viewport2DInstance} viewport - The viewport instance
716
+ * @param {Object} bboxCenter - World-space center {x, y, z}
717
+ * @param {number} worldWidth - X extent (max[0] - min[0])
718
+ * @param {number} worldDepth - Y extent (max[1] - min[1])
719
+ * @param {number} worldHeight - Z extent (max[2] - min[2])
650
720
  */
651
721
  }, {
652
722
  key: "project3DTo2D",
653
- value: function project3DTo2D(viewport, pos3D, rot3D, worldWidth, worldDepth, worldHeight) {
654
- var posX, posY, rectWidth, rectHeight;
655
- var rotationAngle = rot3D.z;
656
- var rotationDegrees = rotationAngle * 180 / Math.PI;
723
+ value: function project3DTo2D(viewport, bboxCenter, worldWidth, worldDepth, worldHeight) {
657
724
  var scale = viewport.PIXELS_PER_UNIT;
725
+ var posX, posY, rectWidth, rectHeight;
658
726
  switch (viewport.viewType) {
659
727
  case 'top':
660
- // Top view: Looking down Z-axis, X-Y plane
661
- posX = pos3D.x;
662
- posY = pos3D.y;
663
-
664
- // Swap width and depth when rotated 90° or 270°
665
- if (Math.abs(rotationDegrees) === 90 || Math.abs(rotationDegrees) === 270) {
666
- rectWidth = worldDepth * scale;
667
- rectHeight = worldWidth * scale;
668
- } else {
669
- rectWidth = worldWidth * scale;
670
- rectHeight = worldDepth * scale;
671
- }
728
+ // Looking down Z-axis X/Y plane
729
+ posX = bboxCenter.x;
730
+ posY = bboxCenter.y;
731
+ rectWidth = worldWidth * scale;
732
+ rectHeight = worldDepth * scale;
672
733
  break;
673
734
  case 'front':
674
- // Front view: Looking along Y-axis, X-Z plane
675
- posX = pos3D.x;
676
- posY = pos3D.z + worldHeight / 2; // Offset Z by half height
677
-
678
- if (Math.abs(rotationDegrees) === 90 || Math.abs(rotationDegrees) === 270) {
679
- rectWidth = worldDepth * scale;
680
- } else {
681
- rectWidth = worldWidth * scale;
682
- }
735
+ // Looking along Y-axis X/Z plane
736
+ posX = bboxCenter.x;
737
+ posY = bboxCenter.z;
738
+ rectWidth = worldWidth * scale;
683
739
  rectHeight = worldHeight * scale;
684
740
  break;
685
741
  case 'side':
686
- // Side view: Looking along X-axis, Y-Z plane (flipped)
687
- posX = -pos3D.y; // Flipped
688
- posY = pos3D.z + worldHeight / 2;
689
- if (Math.abs(rotationDegrees) === 90 || Math.abs(rotationDegrees) === 270) {
690
- rectWidth = worldWidth * scale;
691
- } else {
692
- rectWidth = worldDepth * scale;
693
- }
742
+ // Looking along X-axis Y/Z plane (Y negated for left-right orientation)
743
+ posX = -bboxCenter.y;
744
+ posY = bboxCenter.z;
745
+ rectWidth = worldDepth * scale;
694
746
  rectHeight = worldHeight * scale;
695
747
  break;
696
748
  default:
697
- posX = pos3D.x;
698
- posY = pos3D.y;
749
+ posX = bboxCenter.x;
750
+ posY = bboxCenter.y;
699
751
  rectWidth = worldWidth * scale;
700
752
  rectHeight = worldDepth * scale;
701
753
  }
@@ -703,8 +755,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
703
755
  posX: posX,
704
756
  posY: posY,
705
757
  rectWidth: rectWidth,
706
- rectHeight: rectHeight,
707
- rotationDegrees: rotationDegrees
758
+ rectHeight: rectHeight
708
759
  };
709
760
  }
710
761
 
@@ -714,7 +765,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
714
765
  */
715
766
  }, {
716
767
  key: "addComponentInteractions",
717
- value: function addComponentInteractions(viewport, rect, componentGroup, component, worldWidth, worldDepth, worldHeight) {
768
+ value: function addComponentInteractions(viewport, rect, componentGroup, component, worldWidth, worldDepth, worldHeight, bboxCenter) {
718
769
  var _this6 = this;
719
770
  if (!this.Konva) return;
720
771
  var colors = this.generateComponentColor(component.id);
@@ -726,7 +777,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
726
777
  rect.stroke('#007bff');
727
778
  rect.strokeWidth(3);
728
779
  viewport.stage.container().style.cursor = 'grab';
729
- viewport.componentLayer.draw();
780
+ viewport.componentLayer.batchDraw();
730
781
  }
731
782
  });
732
783
  rect.on('mouseleave', function () {
@@ -735,7 +786,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
735
786
  rect.stroke(colors.stroke);
736
787
  rect.strokeWidth(2);
737
788
  viewport.stage.container().style.cursor = 'default';
738
- viewport.componentLayer.draw();
789
+ viewport.componentLayer.batchDraw();
739
790
  }
740
791
  });
741
792
 
@@ -779,7 +830,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
779
830
  // Snap to grid
780
831
  var snappedPos = _this6.snapScreenToGrid(viewport, currentPos.x, currentPos.y, scale, worldOriginX, worldOriginY);
781
832
  componentGroup.position(snappedPos);
782
- viewport.componentLayer.draw();
833
+ viewport.componentLayer.batchDraw();
783
834
  });
784
835
 
785
836
  // DRAG END
@@ -797,9 +848,9 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
797
848
  // Convert screen to world coordinates
798
849
  var worldCoords = _this6.screenToWorldCoords(viewport, finalPos.x, finalPos.y, scale, worldOriginX, worldOriginY);
799
850
 
800
- // Calculate new position
851
+ // Calculate new position: delta from old bbox center to new bbox center
801
852
  var currentPos = component.position;
802
- var newPosition = _this6.worldCoordsToObjectPosition(viewport, worldCoords, currentPos, worldWidth, worldDepth, worldHeight);
853
+ var newPosition = _this6.worldCoordsToObjectPosition(viewport, worldCoords, currentPos, bboxCenter);
803
854
 
804
855
  // Apply translation via centralPlant API
805
856
  var deltaX = newPosition.x - currentPos.x;
@@ -902,37 +953,45 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
902
953
  }
903
954
 
904
955
  /**
905
- * Convert world coordinates to 3D object position
956
+ * Convert dragged 2D world coordinates to a new 3D object position.
957
+ * coord1/coord2 represent where the bbox CENTER should be in the view's 2D plane;
958
+ * we compute the delta from the old bbox center and apply it to the pivot position.
906
959
  * @param {Viewport2DInstance} viewport - The viewport instance
960
+ * @param {{coord1, coord2}} worldCoords - Projected world coordinates from screen
961
+ * @param {{x,y,z}} currentPosition - Current Three.js object position (pivot)
962
+ * @param {{x,y,z}} bboxCenter - Old world-space bbox center
907
963
  */
908
964
  }, {
909
965
  key: "worldCoordsToObjectPosition",
910
- value: function worldCoordsToObjectPosition(viewport, worldCoords, currentPosition, worldWidth, worldDepth, worldHeight) {
966
+ value: function worldCoordsToObjectPosition(viewport, worldCoords, currentPosition, bboxCenter) {
911
967
  var coord1 = worldCoords.coord1,
912
968
  coord2 = worldCoords.coord2;
913
969
  switch (viewport.viewType) {
914
970
  case 'top':
971
+ // coord1=X, coord2=Y in world space
915
972
  return {
916
- x: coord1,
917
- y: coord2,
973
+ x: currentPosition.x + (coord1 - bboxCenter.x),
974
+ y: currentPosition.y + (coord2 - bboxCenter.y),
918
975
  z: currentPosition.z
919
976
  };
920
977
  case 'front':
978
+ // coord1=X, coord2=Z in world space
921
979
  return {
922
- x: coord1,
980
+ x: currentPosition.x + (coord1 - bboxCenter.x),
923
981
  y: currentPosition.y,
924
- z: coord2 - worldHeight / 2
982
+ z: currentPosition.z + (coord2 - bboxCenter.z)
925
983
  };
926
984
  case 'side':
985
+ // coord1=-Y (negated), coord2=Z in world space
927
986
  return {
928
987
  x: currentPosition.x,
929
- y: -coord1,
930
- z: coord2 - worldHeight / 2
988
+ y: currentPosition.y + (-coord1 - bboxCenter.y),
989
+ z: currentPosition.z + (coord2 - bboxCenter.z)
931
990
  };
932
991
  default:
933
992
  return {
934
- x: coord1,
935
- y: coord2,
993
+ x: currentPosition.x + (coord1 - bboxCenter.x),
994
+ y: currentPosition.y + (coord2 - bboxCenter.y),
936
995
  z: currentPosition.z
937
996
  };
938
997
  }
@@ -944,17 +1003,20 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
944
1003
  }, {
945
1004
  key: "getSceneComponents",
946
1005
  value: function getSceneComponents() {
1006
+ if (this._componentListCache) return this._componentListCache;
947
1007
  if (!this.sceneViewer || !this.sceneViewer.scene) {
948
1008
  return [];
949
1009
  }
950
1010
  var components = [];
951
1011
  this.sceneViewer.scene.traverse(function (object) {
952
- var _object$userData, _object$userData2;
953
- 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);
954
- if (object.userData && objectType === 'component') {
1012
+ var _object$userData3, _object$userData4;
1013
+ // Only match the ROOT component object must have both objectType:'component'
1014
+ // AND libraryId (inner GLB mesh nodes don't have libraryId)
1015
+ 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) {
955
1016
  components.push(object);
956
1017
  }
957
1018
  });
1019
+ this._componentListCache = components;
958
1020
  return components;
959
1021
  }
960
1022
 
@@ -1060,35 +1122,45 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
1060
1122
  }
1061
1123
 
1062
1124
  /**
1063
- * Refresh a specific viewport or all viewports
1125
+ * Refresh a specific viewport or all viewports.
1126
+ * Debounced via requestAnimationFrame so multiple calls within the same
1127
+ * frame (e.g. from Viewport2D mount + refreshAll2DViews) collapse into one.
1064
1128
  * @param {string} key - Optional viewport key. If not provided, refreshes all viewports
1065
1129
  */
1066
1130
  }, {
1067
1131
  key: "refresh",
1068
1132
  value: function refresh() {
1133
+ var _this7 = this;
1069
1134
  var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
1070
- if (key) {
1071
- var viewport = this.viewports.get(key);
1072
- if (viewport && viewport.isReady) {
1073
- this.renderComponents(viewport);
1074
- }
1075
- } else {
1076
- // Refresh all viewports
1077
- var _iterator = _createForOfIteratorHelper(this.viewports.values()),
1078
- _step;
1079
- try {
1080
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
1081
- var _viewport = _step.value;
1082
- if (_viewport.isReady) {
1083
- this.renderComponents(_viewport);
1135
+ if (this._refreshPending) return;
1136
+ this._refreshPending = true;
1137
+ requestAnimationFrame(function () {
1138
+ _this7._refreshPending = false;
1139
+ // Clear per-cycle caches so each component is measured/traversed once per paint
1140
+ _this7._bboxCache.clear();
1141
+ _this7._componentListCache = null;
1142
+ if (key) {
1143
+ var viewport = _this7.viewports.get(key);
1144
+ if (viewport && viewport.isReady) {
1145
+ _this7.renderComponents(viewport);
1146
+ }
1147
+ } else {
1148
+ var _iterator = _createForOfIteratorHelper(_this7.viewports.values()),
1149
+ _step;
1150
+ try {
1151
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
1152
+ var _viewport = _step.value;
1153
+ if (_viewport.isReady) {
1154
+ _this7.renderComponents(_viewport);
1155
+ }
1084
1156
  }
1157
+ } catch (err) {
1158
+ _iterator.e(err);
1159
+ } finally {
1160
+ _iterator.f();
1085
1161
  }
1086
- } catch (err) {
1087
- _iterator.e(err);
1088
- } finally {
1089
- _iterator.f();
1090
1162
  }
1091
- }
1163
+ });
1092
1164
  }
1093
1165
 
1094
1166
  /**
@@ -222,10 +222,10 @@ var SceneClearingUtility = /*#__PURE__*/function () {
222
222
  throw new Error('Scene not available for clearing');
223
223
  case 1:
224
224
  componentsToRemove = [];
225
- scene = this.sceneViewer.scene; // Collect only component objects
225
+ scene = this.sceneViewer.scene; // Collect component, segment, and gateway objects
226
226
  scene.traverse(function (child) {
227
227
  if (child === scene) return;
228
- var isComponent = child.userData && (child.userData.objectType === 'component' || child.userData.objectType === 'component');
228
+ var isComponent = child.userData && (child.userData.objectType === 'component' || child.userData.objectType === 'segment' || child.userData.objectType === 'gateway' || child.userData.objectType === 'connector');
229
229
  var isDirectChild = child.parent === scene;
230
230
  if (isComponent && isDirectChild) {
231
231
  componentsToRemove.push(child);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2112-lab/central-plant",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Utility modules for the Central Plant Application",
5
5
  "main": "dist/bundle/index.js",
6
6
  "module": "dist/esm/src/index.js",