@2112-lab/central-plant 0.3.5 → 0.3.7

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.
@@ -7,7 +7,6 @@ var THREE = require('three');
7
7
  var textureConfig = require('../environment/textureConfig.js');
8
8
  var modelManager = require('./modelManager.js');
9
9
  var sceneClearingUtility = require('../../utils/sceneClearingUtility.js');
10
- var boundingBoxUtils = require('../../utils/boundingBoxUtils.js');
11
10
 
12
11
  function _interopNamespace(e) {
13
12
  if (e && e.__esModule) return e;
@@ -647,123 +646,52 @@ var SceneOperationsManager = /*#__PURE__*/function () {
647
646
  }
648
647
 
649
648
  /**
650
- * Helper function to compute world bounding boxes
651
- * For components: uses filtered bbox (excludes io-device and connector subtrees)
652
- * For io-devices: computes separate bounding boxes and injects them as children
649
+ * Sync world-space positions and isDeclared flags for gateways and connectors
650
+ * into the scene JSON data so the pathfinder can read them.
651
+ *
652
+ * Bounding boxes for components and segments are intentionally NOT computed here.
653
+ * They are computed (with matrix-hash caching) by
654
+ * PathfindingManager._enrichSceneDataWithBoundingBoxes(), which runs after GLB
655
+ * models are fully loaded and therefore produces correct values.
653
656
  */
654
657
  }, {
655
- key: "computeWorldBoundingBoxes",
656
- value: function computeWorldBoundingBoxes(data) {
657
- var component = this.sceneViewer;
658
- component.scene.traverse(function (object) {
659
- if (object.isMesh) {
660
- // Find the corresponding JSON object
661
- var jsonObject = null;
662
- var _findJsonObject = function findJsonObject(children) {
663
- var _iterator = _rollupPluginBabelHelpers.createForOfIteratorHelper(children),
664
- _step;
665
- try {
666
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
667
- var _object$userData, _child$userData4;
668
- var child = _step.value;
669
- // Enhanced matching logic with hardcoded UUID priority
670
-
671
- // Strategy 1: Direct hardcoded UUID match (HIGHEST PRIORITY)
672
- if (child.uuid === object.uuid || child.uuid === ((_object$userData = object.userData) === null || _object$userData === void 0 ? void 0 : _object$userData.originalUuid) || object.uuid === ((_child$userData4 = child.userData) === null || _child$userData4 === void 0 ? void 0 : _child$userData4.originalUuid)) {
673
- return child;
674
- }
675
-
676
- // Recursively search children
677
- if (child.children) {
678
- var found = _findJsonObject(child.children);
679
- if (found) return found;
680
- }
681
- }
682
- } catch (err) {
683
- _iterator.e(err);
684
- } finally {
685
- _iterator.f();
686
- }
687
- return null;
688
- };
689
- jsonObject = _findJsonObject(data.scene.children);
690
- if (jsonObject) {
691
- // Store in JSON userData for pathfinder (skip for gateways - they're just routing points)
692
- if (!jsonObject.userData) jsonObject.userData = {};
693
- if (jsonObject.userData.objectType === 'component') {
694
- // For components: compute filtered bounding box (excludes io-device and connector subtrees)
695
- var filteredBBox = boundingBoxUtils.computeFilteredBoundingBox(object, ['io-device', 'connector']);
696
- jsonObject.userData.worldBoundingBox = {
697
- min: filteredBBox.min.toArray(),
698
- max: filteredBBox.max.toArray()
699
- };
700
- console.log("Added filtered world bounding box for component:", jsonObject.userData.worldBoundingBox);
701
-
702
- // Compute and inject separate io-device bounding boxes as children
703
- var ioDeviceBBoxes = boundingBoxUtils.computeIODeviceBoundingBoxes(object);
704
- if (ioDeviceBBoxes.length > 0) {
705
- if (!jsonObject.children) jsonObject.children = [];
706
- ioDeviceBBoxes.forEach(function (deviceBBox) {
707
- var existingIndex = jsonObject.children.findIndex(function (c) {
708
- return c.uuid === deviceBBox.uuid;
709
- });
710
- if (existingIndex >= 0) {
711
- // Update existing entry
712
- if (!jsonObject.children[existingIndex].userData) jsonObject.children[existingIndex].userData = {};
713
- jsonObject.children[existingIndex].userData.objectType = 'io-device';
714
- jsonObject.children[existingIndex].userData.worldBoundingBox = deviceBBox.worldBoundingBox;
715
- } else {
716
- // Create new entry
717
- jsonObject.children.push({
718
- uuid: deviceBBox.uuid,
719
- userData: _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, deviceBBox.userData), {}, {
720
- worldBoundingBox: deviceBBox.worldBoundingBox
721
- }),
722
- children: []
723
- });
724
- }
725
- });
726
- console.log("\uD83D\uDCE6 Injected ".concat(ioDeviceBBoxes.length, " io-device bbox(es) for component ").concat(jsonObject.uuid));
727
- }
728
- } else if (jsonObject.userData.objectType !== 'gateway') {
729
- // For non-component, non-gateway objects: standard bounding box
730
- var boundingBox = new THREE__namespace.Box3().setFromObject(object);
731
- jsonObject.userData.worldBoundingBox = {
732
- min: boundingBox.min.toArray(),
733
- max: boundingBox.max.toArray()
734
- };
735
- console.log("Added world bounding box:", jsonObject.userData.worldBoundingBox);
736
- }
737
-
738
- // For gateways and connectors, ensure userData.position exists in scene data
739
- // This is REQUIRED for pathfinder compatibility
740
- if (jsonObject.userData.objectType === 'gateway' || jsonObject.userData.objectType === 'connector') {
741
- // Use the object's world position (from Three.js mesh)
742
- var worldPos = new THREE__namespace.Vector3();
743
- object.getWorldPosition(worldPos);
744
-
745
- // ALWAYS update userData.position with world position
746
- // This is critical for manual segment connectors which start with local positions
747
- jsonObject.userData.position = [worldPos.x, worldPos.y, worldPos.z];
748
- console.log("\u2705 Set userData.position for ".concat(jsonObject.userData.objectType, " ").concat(jsonObject.uuid, ": [").concat(worldPos.x.toFixed(2), ", ").concat(worldPos.y.toFixed(2), ", ").concat(worldPos.z.toFixed(2), "]"));
749
-
750
- // For gateways, ensure isDeclared flag is in scene data
751
- if (jsonObject.userData.objectType === 'gateway') {
752
- if (jsonObject.userData.isDeclared === undefined) {
753
- jsonObject.userData.isDeclared = true;
754
- }
755
- }
756
-
757
- // For manual segment connectors, ensure isDeclared is set in scene data
758
- if (jsonObject.userData.objectType === 'segment-connector' && jsonObject.userData.isDeclared === undefined) {
759
- jsonObject.userData.isDeclared = true;
760
- console.log("\u2705 Set isDeclared=true for manual segment connector in scene data: ".concat(jsonObject.uuid));
761
- }
762
-
763
- // Also sync the mesh's userData.position (belt and suspenders approach)
764
- object.userData.position = [worldPos.x, worldPos.y, worldPos.z];
765
- }
658
+ key: "_syncPositionsForPathfinding",
659
+ value: function _syncPositionsForPathfinding(data) {
660
+ var scene = this.sceneViewer.scene;
661
+ var worldPos = new THREE__namespace.Vector3();
662
+ var syncPosition = function syncPosition(jsonObject) {
663
+ var _jsonObject$userData;
664
+ var object = scene.getObjectByProperty('uuid', jsonObject.uuid) || scene.getObjectByProperty('uuid', (_jsonObject$userData = jsonObject.userData) === null || _jsonObject$userData === void 0 ? void 0 : _jsonObject$userData.originalUuid);
665
+ if (!object) return;
666
+ object.getWorldPosition(worldPos);
667
+ var pos = [worldPos.x, worldPos.y, worldPos.z];
668
+ jsonObject.userData.position = pos;
669
+ object.userData.position = pos;
670
+ };
671
+ data.scene.children.forEach(function (jsonObject) {
672
+ var _jsonObject$userData2;
673
+ var type = (_jsonObject$userData2 = jsonObject.userData) === null || _jsonObject$userData2 === void 0 ? void 0 : _jsonObject$userData2.objectType;
674
+ if (type === 'gateway') {
675
+ syncPosition(jsonObject);
676
+ if (jsonObject.userData.isDeclared === undefined) {
677
+ jsonObject.userData.isDeclared = true;
766
678
  }
679
+ } else if (type === 'connector') {
680
+ syncPosition(jsonObject);
681
+ } else if (type === 'segment-connector') {
682
+ syncPosition(jsonObject);
683
+ if (jsonObject.userData.isDeclared === undefined) {
684
+ jsonObject.userData.isDeclared = true;
685
+ }
686
+ } else if (type === 'component' && Array.isArray(jsonObject.children)) {
687
+ // Connectors are injected as JSON children by _injectConnectorChildrenFromDictionary
688
+ // and their Three.js objects exist in the scene, created recursively by createSceneObject
689
+ jsonObject.children.forEach(function (childJson) {
690
+ var _childJson$userData;
691
+ if (((_childJson$userData = childJson.userData) === null || _childJson$userData === void 0 ? void 0 : _childJson$userData.objectType) === 'connector') {
692
+ syncPosition(childJson);
693
+ }
694
+ });
767
695
  }
768
696
  });
769
697
  }
@@ -928,10 +856,10 @@ var SceneOperationsManager = /*#__PURE__*/function () {
928
856
  var componentsProcessed = 0;
929
857
  var connectorsInjected = 0;
930
858
  data.scene.children.forEach(function (child) {
931
- var _child$userData5, _child$userData6, _child$userData7;
932
- var childType = ((_child$userData5 = child.userData) === null || _child$userData5 === void 0 ? void 0 : _child$userData5.objectType) || ((_child$userData6 = child.userData) === null || _child$userData6 === void 0 ? void 0 : _child$userData6.objectType);
859
+ var _child$userData4, _child$userData5, _child$userData6;
860
+ var childType = ((_child$userData4 = child.userData) === null || _child$userData4 === void 0 ? void 0 : _child$userData4.objectType) || ((_child$userData5 = child.userData) === null || _child$userData5 === void 0 ? void 0 : _child$userData5.objectType);
933
861
  // Only process components with libraryId
934
- if (childType === 'component' && (_child$userData7 = child.userData) !== null && _child$userData7 !== void 0 && _child$userData7.libraryId) {
862
+ if (childType === 'component' && (_child$userData6 = child.userData) !== null && _child$userData6 !== void 0 && _child$userData6.libraryId) {
935
863
  var libraryId = child.userData.libraryId;
936
864
  var dictEntry = componentDictionary[libraryId];
937
865
 
@@ -1088,23 +1016,25 @@ var SceneOperationsManager = /*#__PURE__*/function () {
1088
1016
  geometries = this.createSceneGeometries(data, componentDictionary); // Create basic objects and track GLB replacements
1089
1017
  libraryObjectsToReplace = [];
1090
1018
  data.scene.children.forEach(function (child, index) {
1091
- var _child$userData8, _child$userData9;
1019
+ var _child$userData7, _child$userData8;
1092
1020
  var createdObject = _this4.createSceneObject(child, geometries, materials, componentDictionary);
1093
1021
  _this4.sceneViewer.scene.add(createdObject);
1094
1022
 
1095
1023
  // Track objects that need GLB model replacement
1096
- if ((_child$userData8 = child.userData) !== null && _child$userData8 !== void 0 && _child$userData8.libraryId && componentDictionary[(_child$userData9 = child.userData) === null || _child$userData9 === void 0 ? void 0 : _child$userData9.libraryId]) {
1097
- var _child$userData0;
1024
+ if ((_child$userData7 = child.userData) !== null && _child$userData7 !== void 0 && _child$userData7.libraryId && componentDictionary[(_child$userData8 = child.userData) === null || _child$userData8 === void 0 ? void 0 : _child$userData8.libraryId]) {
1025
+ var _child$userData9;
1098
1026
  libraryObjectsToReplace.push({
1099
1027
  basicObject: createdObject,
1100
1028
  jsonData: child,
1101
- componentData: componentDictionary[(_child$userData0 = child.userData) === null || _child$userData0 === void 0 ? void 0 : _child$userData0.libraryId]
1029
+ componentData: componentDictionary[(_child$userData9 = child.userData) === null || _child$userData9 === void 0 ? void 0 : _child$userData9.libraryId]
1102
1030
  });
1103
1031
  }
1104
1032
  });
1105
1033
 
1106
- // Compute bounding boxes for pathfinding
1107
- this.computeWorldBoundingBoxes(data);
1034
+ // Sync gateway/connector world positions into JSON before pathfinding.
1035
+ // Bounding boxes are computed later by PathfindingManager._enrichSceneDataWithBoundingBoxes
1036
+ // (after GLB models are loaded), so no bbox work is done here.
1037
+ this._syncPositionsForPathfinding(data);
1108
1038
  this._saveOriginalWorldMatrices(this.sceneViewer.scene);
1109
1039
  return _context6.a(2, {
1110
1040
  componentDictionary: componentDictionary,
@@ -1250,8 +1180,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
1250
1180
  var instanceBehaviors = [];
1251
1181
  if (Array.isArray(data === null || data === void 0 || (_data$scene3 = data.scene) === null || _data$scene3 === void 0 ? void 0 : _data$scene3.children)) {
1252
1182
  data.scene.children.forEach(function (child) {
1253
- var _child$userData1, _compData$defaultBeha;
1254
- var libraryId = (_child$userData1 = child.userData) === null || _child$userData1 === void 0 ? void 0 : _child$userData1.libraryId;
1183
+ var _child$userData0, _compData$defaultBeha;
1184
+ var libraryId = (_child$userData0 = child.userData) === null || _child$userData0 === void 0 ? void 0 : _child$userData0.libraryId;
1255
1185
  if (!libraryId) return;
1256
1186
  var instanceUuid = child.uuid;
1257
1187
  // Skip instances whose defaults were already resolved by Step A
@@ -1447,8 +1377,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
1447
1377
  key: "_saveOriginalWorldMatrices",
1448
1378
  value: function _saveOriginalWorldMatrices(scene) {
1449
1379
  scene.traverse(function (object) {
1450
- var _object$userData2;
1451
- if ((_object$userData2 = object.userData) !== null && _object$userData2 !== void 0 && _object$userData2.direction) {
1380
+ var _object$userData;
1381
+ if ((_object$userData = object.userData) !== null && _object$userData !== void 0 && _object$userData.direction) {
1452
1382
  var originalMatrix = new THREE__namespace.Matrix4();
1453
1383
  originalMatrix.copy(object.matrixWorld);
1454
1384
  }
@@ -1652,8 +1582,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
1652
1582
  // Process children (connectors, etc.) if they exist
1653
1583
  if (componentModel.children && componentModel.children.length > 0) {
1654
1584
  componentModel.children.forEach(function (child) {
1655
- var _child$userData10, _child$userData11;
1656
- var childType = ((_child$userData10 = child.userData) === null || _child$userData10 === void 0 ? void 0 : _child$userData10.objectType) || ((_child$userData11 = child.userData) === null || _child$userData11 === void 0 ? void 0 : _child$userData11.objectType);
1585
+ var _child$userData1, _child$userData10;
1586
+ var childType = ((_child$userData1 = child.userData) === null || _child$userData1 === void 0 ? void 0 : _child$userData1.objectType) || ((_child$userData10 = child.userData) === null || _child$userData10 === void 0 ? void 0 : _child$userData10.objectType);
1657
1587
  if (childType === 'connector') {
1658
1588
  var _child$geometry;
1659
1589
  var childBoundingBox = new THREE__namespace.Box3().setFromObject(child);
@@ -1738,8 +1668,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
1738
1668
  if (segment.children && segment.children.length > 0) {
1739
1669
  var childrenToRemove = _rollupPluginBabelHelpers.toConsumableArray(segment.children);
1740
1670
  childrenToRemove.forEach(function (child) {
1741
- var _child$userData12;
1742
- if ((_child$userData12 = child.userData) !== null && _child$userData12 !== void 0 && _child$userData12.isPipeElbow) {
1671
+ var _child$userData11;
1672
+ if ((_child$userData11 = child.userData) !== null && _child$userData11 !== void 0 && _child$userData11.isPipeElbow) {
1743
1673
  console.log("\uD83D\uDDD1\uFE0F Removing elbow child from segment before manualization: ".concat(child.uuid));
1744
1674
  segment.remove(child);
1745
1675
  if (child.geometry) child.geometry.dispose();
@@ -111,6 +111,18 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
111
111
  // Map of viewport instances by viewType or custom key
112
112
  _this2.viewports = new Map();
113
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
+
114
126
  // Event listener reference for cleanup
115
127
  _this2._objectTransformedListener = null;
116
128
  console.log('🔲 Viewport2DManager initialized (multi-instance support)');
@@ -132,7 +144,6 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
132
144
 
133
145
  // Listen for object transformations to refresh all viewports
134
146
  this._objectTransformedListener = function (eventData) {
135
- console.log('🔲 Viewport2DManager detected object transformation, refreshing all viewports');
136
147
  _this3.refresh(); // Refresh all viewports
137
148
  };
138
149
 
@@ -198,6 +209,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
198
209
  case 3:
199
210
  // Create new viewport instance
200
211
  viewport = new Viewport2DInstance(this.sceneViewer, this.Konva, viewType, container);
212
+ viewport._instanceKey = key;
201
213
  this.viewports.set(key, viewport);
202
214
 
203
215
  // Initialize the stage for this viewport
@@ -375,9 +387,9 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
375
387
  viewport.stage.width(width);
376
388
  viewport.stage.height(height);
377
389
 
378
- // Redraw content
390
+ // Redraw grid immediately; schedule debounced component render
379
391
  this.drawGrid(viewport);
380
- this.renderComponents(viewport);
392
+ this.refresh(viewport._instanceKey);
381
393
  }
382
394
  }
383
395
 
@@ -560,7 +572,6 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
560
572
  worldDepth = _this$getComponentDim.worldDepth,
561
573
  worldHeight = _this$getComponentDim.worldHeight,
562
574
  bboxCenter = _this$getComponentDim.bboxCenter;
563
- console.log("[2D] ".concat(component.name, " | w=").concat(worldWidth.toFixed(3), " d=").concat(worldDepth.toFixed(3), " h=").concat(worldHeight.toFixed(3), " | center=(").concat(bboxCenter.x.toFixed(2), ",").concat(bboxCenter.y.toFixed(2), ",").concat(bboxCenter.z.toFixed(2), ")"));
564
575
 
565
576
  // Project 3D bbox center to 2D based on view type
566
577
  var _this$project3DTo2D = this.project3DTo2D(viewport, bboxCenter, worldWidth, worldDepth, worldHeight),
@@ -627,21 +638,45 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
627
638
  }, {
628
639
  key: "_getOrComputeWorldBoundingBox",
629
640
  value: function _getOrComputeWorldBoundingBox(object) {
630
- // computeFilteredBoundingBox with no exclusions = full object bbox
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
+ }
631
662
  var box = boundingBoxUtils.computeFilteredBoundingBox(object, []);
663
+ var result;
632
664
  if (box.isEmpty()) {
633
665
  // Object has no geometry; fall back to a point at world position
634
666
  var wp = new THREE__namespace.Vector3();
635
667
  object.getWorldPosition(wp);
636
- return {
668
+ result = {
637
669
  min: [wp.x, wp.y, wp.z],
638
670
  max: [wp.x, wp.y, wp.z]
639
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
+ };
640
677
  }
641
- return {
642
- min: [box.min.x, box.min.y, box.min.z],
643
- max: [box.max.x, box.max.y, box.max.z]
644
- };
678
+ this._bboxCache.set(object.uuid, result);
679
+ return result;
645
680
  }
646
681
 
647
682
  /**
@@ -650,11 +685,12 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
650
685
  }, {
651
686
  key: "getComponentDimensions",
652
687
  value: function getComponentDimensions(component) {
653
- var _component$userData$w, _component$userData, _component$getWorldPo;
654
- // Prefer worldBoundingBox already stored on the object set after GLB loading
655
- // by modelManager.replaceWithGLBModels using computeFilteredBoundingBox.
656
- // Fall back to computing live if not yet available (e.g. first render before GLB load).
657
- var wbb = (_component$userData$w = (_component$userData = component.userData) === null || _component$userData === void 0 ? void 0 : _component$userData.worldBoundingBox) !== null && _component$userData$w !== void 0 ? _component$userData$w : this._getOrComputeWorldBoundingBox(component);
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);
658
694
  if (wbb !== null && wbb !== void 0 && wbb.min && wbb !== null && wbb !== void 0 && wbb.max) {
659
695
  var _wbb$min = _rollupPluginBabelHelpers.slicedToArray(wbb.min, 3),
660
696
  minX = _wbb$min[0],
@@ -765,7 +801,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
765
801
  rect.stroke('#007bff');
766
802
  rect.strokeWidth(3);
767
803
  viewport.stage.container().style.cursor = 'grab';
768
- viewport.componentLayer.draw();
804
+ viewport.componentLayer.batchDraw();
769
805
  }
770
806
  });
771
807
  rect.on('mouseleave', function () {
@@ -774,7 +810,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
774
810
  rect.stroke(colors.stroke);
775
811
  rect.strokeWidth(2);
776
812
  viewport.stage.container().style.cursor = 'default';
777
- viewport.componentLayer.draw();
813
+ viewport.componentLayer.batchDraw();
778
814
  }
779
815
  });
780
816
 
@@ -818,7 +854,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
818
854
  // Snap to grid
819
855
  var snappedPos = _this6.snapScreenToGrid(viewport, currentPos.x, currentPos.y, scale, worldOriginX, worldOriginY);
820
856
  componentGroup.position(snappedPos);
821
- viewport.componentLayer.draw();
857
+ viewport.componentLayer.batchDraw();
822
858
  });
823
859
 
824
860
  // DRAG END
@@ -991,18 +1027,20 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
991
1027
  }, {
992
1028
  key: "getSceneComponents",
993
1029
  value: function getSceneComponents() {
1030
+ if (this._componentListCache) return this._componentListCache;
994
1031
  if (!this.sceneViewer || !this.sceneViewer.scene) {
995
1032
  return [];
996
1033
  }
997
1034
  var components = [];
998
1035
  this.sceneViewer.scene.traverse(function (object) {
999
- var _object$userData, _object$userData2;
1036
+ var _object$userData3, _object$userData4;
1000
1037
  // Only match the ROOT component object — must have both objectType:'component'
1001
1038
  // AND libraryId (inner GLB mesh nodes don't have libraryId)
1002
- 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) {
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) {
1003
1040
  components.push(object);
1004
1041
  }
1005
1042
  });
1043
+ this._componentListCache = components;
1006
1044
  return components;
1007
1045
  }
1008
1046
 
@@ -1108,35 +1146,45 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
1108
1146
  }
1109
1147
 
1110
1148
  /**
1111
- * 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.
1112
1152
  * @param {string} key - Optional viewport key. If not provided, refreshes all viewports
1113
1153
  */
1114
1154
  }, {
1115
1155
  key: "refresh",
1116
1156
  value: function refresh() {
1157
+ var _this7 = this;
1117
1158
  var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
1118
- if (key) {
1119
- var viewport = this.viewports.get(key);
1120
- if (viewport && viewport.isReady) {
1121
- this.renderComponents(viewport);
1122
- }
1123
- } else {
1124
- // Refresh all viewports
1125
- var _iterator = _rollupPluginBabelHelpers.createForOfIteratorHelper(this.viewports.values()),
1126
- _step;
1127
- try {
1128
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
1129
- var _viewport = _step.value;
1130
- if (_viewport.isReady) {
1131
- 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
+ }
1132
1180
  }
1181
+ } catch (err) {
1182
+ _iterator.e(err);
1183
+ } finally {
1184
+ _iterator.f();
1133
1185
  }
1134
- } catch (err) {
1135
- _iterator.e(err);
1136
- } finally {
1137
- _iterator.f();
1138
1186
  }
1139
- }
1187
+ });
1140
1188
  }
1141
1189
 
1142
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);
@@ -480,6 +480,11 @@ var SceneClearingUtility = /*#__PURE__*/function () {
480
480
  this.sceneViewer.currentSceneData.connections = [];
481
481
  }
482
482
 
483
+ // Clear the JSON data mirror so getAvailableConnections() returns nothing
484
+ if (this.sceneViewer.currentSceneData && this.sceneViewer.currentSceneData.scene) {
485
+ this.sceneViewer.currentSceneData.scene.children = [];
486
+ }
487
+
483
488
  // Reset component counter for CentralPlant
484
489
  if (this.sceneViewer.centralPlant && this.sceneViewer.centralPlant.componentCounter !== undefined) {
485
490
  this.sceneViewer.centralPlant.componentCounter = 0;
@@ -31,7 +31,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
31
31
  * Initialize the CentralPlant manager
32
32
  *
33
33
  * @constructor
34
- * @version 0.3.5
34
+ * @version 0.3.7
35
35
  * @updated 2025-10-22
36
36
  *
37
37
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -571,6 +571,13 @@ var ModelManager = /*#__PURE__*/function () {
571
571
  // Update both the JSON data object AND the live scene object
572
572
  jsonData.userData.worldBoundingBox = worldBoundingBox;
573
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
+ };
574
581
  });
575
582
 
576
583
  // Dispatch completion event