@2112-lab/central-plant 0.3.5 → 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.
@@ -29828,6 +29828,13 @@ var ModelManager = /*#__PURE__*/function () {
29828
29828
  // Update both the JSON data object AND the live scene object
29829
29829
  jsonData.userData.worldBoundingBox = worldBoundingBox;
29830
29830
  glbModel.userData.worldBoundingBox = worldBoundingBox;
29831
+ // Snapshot the object's local position so viewport2DManager can compute
29832
+ // world-bbox updates via a fast O(1) position delta instead of re-traversing geometry
29833
+ glbModel.userData._wbbBasePosition = {
29834
+ x: glbModel.position.x,
29835
+ y: glbModel.position.y,
29836
+ z: glbModel.position.z
29837
+ };
29831
29838
  });
29832
29839
 
29833
29840
  // Dispatch completion event
@@ -30127,10 +30134,10 @@ var SceneClearingUtility = /*#__PURE__*/function () {
30127
30134
  throw new Error('Scene not available for clearing');
30128
30135
  case 1:
30129
30136
  componentsToRemove = [];
30130
- scene = this.sceneViewer.scene; // Collect only component objects
30137
+ scene = this.sceneViewer.scene; // Collect component, segment, and gateway objects
30131
30138
  scene.traverse(function (child) {
30132
30139
  if (child === scene) return;
30133
- var isComponent = child.userData && (child.userData.objectType === 'component' || child.userData.objectType === 'component');
30140
+ var isComponent = child.userData && (child.userData.objectType === 'component' || child.userData.objectType === 'segment' || child.userData.objectType === 'gateway' || child.userData.objectType === 'connector');
30134
30141
  var isDirectChild = child.parent === scene;
30135
30142
  if (isComponent && isDirectChild) {
30136
30143
  componentsToRemove.push(child);
@@ -31123,123 +31130,52 @@ var SceneOperationsManager = /*#__PURE__*/function () {
31123
31130
  }
31124
31131
 
31125
31132
  /**
31126
- * Helper function to compute world bounding boxes
31127
- * For components: uses filtered bbox (excludes io-device and connector subtrees)
31128
- * For io-devices: computes separate bounding boxes and injects them as children
31129
- */
31130
- }, {
31131
- key: "computeWorldBoundingBoxes",
31132
- value: function computeWorldBoundingBoxes(data) {
31133
- var component = this.sceneViewer;
31134
- component.scene.traverse(function (object) {
31135
- if (object.isMesh) {
31136
- // Find the corresponding JSON object
31137
- var jsonObject = null;
31138
- var _findJsonObject = function findJsonObject(children) {
31139
- var _iterator = _createForOfIteratorHelper(children),
31140
- _step;
31141
- try {
31142
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
31143
- var _object$userData, _child$userData4;
31144
- var child = _step.value;
31145
- // Enhanced matching logic with hardcoded UUID priority
31146
-
31147
- // Strategy 1: Direct hardcoded UUID match (HIGHEST PRIORITY)
31148
- 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)) {
31149
- return child;
31150
- }
31151
-
31152
- // Recursively search children
31153
- if (child.children) {
31154
- var found = _findJsonObject(child.children);
31155
- if (found) return found;
31156
- }
31157
- }
31158
- } catch (err) {
31159
- _iterator.e(err);
31160
- } finally {
31161
- _iterator.f();
31162
- }
31163
- return null;
31164
- };
31165
- jsonObject = _findJsonObject(data.scene.children);
31166
- if (jsonObject) {
31167
- // Store in JSON userData for pathfinder (skip for gateways - they're just routing points)
31168
- if (!jsonObject.userData) jsonObject.userData = {};
31169
- if (jsonObject.userData.objectType === 'component') {
31170
- // For components: compute filtered bounding box (excludes io-device and connector subtrees)
31171
- var filteredBBox = computeFilteredBoundingBox(object, ['io-device', 'connector']);
31172
- jsonObject.userData.worldBoundingBox = {
31173
- min: filteredBBox.min.toArray(),
31174
- max: filteredBBox.max.toArray()
31175
- };
31176
- console.log("Added filtered world bounding box for component:", jsonObject.userData.worldBoundingBox);
31177
-
31178
- // Compute and inject separate io-device bounding boxes as children
31179
- var ioDeviceBBoxes = computeIODeviceBoundingBoxes(object);
31180
- if (ioDeviceBBoxes.length > 0) {
31181
- if (!jsonObject.children) jsonObject.children = [];
31182
- ioDeviceBBoxes.forEach(function (deviceBBox) {
31183
- var existingIndex = jsonObject.children.findIndex(function (c) {
31184
- return c.uuid === deviceBBox.uuid;
31185
- });
31186
- if (existingIndex >= 0) {
31187
- // Update existing entry
31188
- if (!jsonObject.children[existingIndex].userData) jsonObject.children[existingIndex].userData = {};
31189
- jsonObject.children[existingIndex].userData.objectType = 'io-device';
31190
- jsonObject.children[existingIndex].userData.worldBoundingBox = deviceBBox.worldBoundingBox;
31191
- } else {
31192
- // Create new entry
31193
- jsonObject.children.push({
31194
- uuid: deviceBBox.uuid,
31195
- userData: _objectSpread2(_objectSpread2({}, deviceBBox.userData), {}, {
31196
- worldBoundingBox: deviceBBox.worldBoundingBox
31197
- }),
31198
- children: []
31199
- });
31200
- }
31201
- });
31202
- console.log("\uD83D\uDCE6 Injected ".concat(ioDeviceBBoxes.length, " io-device bbox(es) for component ").concat(jsonObject.uuid));
31203
- }
31204
- } else if (jsonObject.userData.objectType !== 'gateway') {
31205
- // For non-component, non-gateway objects: standard bounding box
31206
- var boundingBox = new THREE__namespace.Box3().setFromObject(object);
31207
- jsonObject.userData.worldBoundingBox = {
31208
- min: boundingBox.min.toArray(),
31209
- max: boundingBox.max.toArray()
31210
- };
31211
- console.log("Added world bounding box:", jsonObject.userData.worldBoundingBox);
31212
- }
31213
-
31214
- // For gateways and connectors, ensure userData.position exists in scene data
31215
- // This is REQUIRED for pathfinder compatibility
31216
- if (jsonObject.userData.objectType === 'gateway' || jsonObject.userData.objectType === 'connector') {
31217
- // Use the object's world position (from Three.js mesh)
31218
- var worldPos = new THREE__namespace.Vector3();
31219
- object.getWorldPosition(worldPos);
31220
-
31221
- // ALWAYS update userData.position with world position
31222
- // This is critical for manual segment connectors which start with local positions
31223
- jsonObject.userData.position = [worldPos.x, worldPos.y, worldPos.z];
31224
- 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), "]"));
31225
-
31226
- // For gateways, ensure isDeclared flag is in scene data
31227
- if (jsonObject.userData.objectType === 'gateway') {
31228
- if (jsonObject.userData.isDeclared === undefined) {
31229
- jsonObject.userData.isDeclared = true;
31230
- }
31231
- }
31232
-
31233
- // For manual segment connectors, ensure isDeclared is set in scene data
31234
- if (jsonObject.userData.objectType === 'segment-connector' && jsonObject.userData.isDeclared === undefined) {
31235
- jsonObject.userData.isDeclared = true;
31236
- console.log("\u2705 Set isDeclared=true for manual segment connector in scene data: ".concat(jsonObject.uuid));
31237
- }
31238
-
31239
- // Also sync the mesh's userData.position (belt and suspenders approach)
31240
- object.userData.position = [worldPos.x, worldPos.y, worldPos.z];
31133
+ * Sync world-space positions and isDeclared flags for gateways and connectors
31134
+ * into the scene JSON data so the pathfinder can read them.
31135
+ *
31136
+ * Bounding boxes for components and segments are intentionally NOT computed here.
31137
+ * They are computed (with matrix-hash caching) by
31138
+ * PathfindingManager._enrichSceneDataWithBoundingBoxes(), which runs after GLB
31139
+ * models are fully loaded and therefore produces correct values.
31140
+ */
31141
+ }, {
31142
+ key: "_syncPositionsForPathfinding",
31143
+ value: function _syncPositionsForPathfinding(data) {
31144
+ var scene = this.sceneViewer.scene;
31145
+ var worldPos = new THREE__namespace.Vector3();
31146
+ var syncPosition = function syncPosition(jsonObject) {
31147
+ var _jsonObject$userData;
31148
+ var object = scene.getObjectByProperty('uuid', jsonObject.uuid) || scene.getObjectByProperty('uuid', (_jsonObject$userData = jsonObject.userData) === null || _jsonObject$userData === void 0 ? void 0 : _jsonObject$userData.originalUuid);
31149
+ if (!object) return;
31150
+ object.getWorldPosition(worldPos);
31151
+ var pos = [worldPos.x, worldPos.y, worldPos.z];
31152
+ jsonObject.userData.position = pos;
31153
+ object.userData.position = pos;
31154
+ };
31155
+ data.scene.children.forEach(function (jsonObject) {
31156
+ var _jsonObject$userData2;
31157
+ var type = (_jsonObject$userData2 = jsonObject.userData) === null || _jsonObject$userData2 === void 0 ? void 0 : _jsonObject$userData2.objectType;
31158
+ if (type === 'gateway') {
31159
+ syncPosition(jsonObject);
31160
+ if (jsonObject.userData.isDeclared === undefined) {
31161
+ jsonObject.userData.isDeclared = true;
31162
+ }
31163
+ } else if (type === 'connector') {
31164
+ syncPosition(jsonObject);
31165
+ } else if (type === 'segment-connector') {
31166
+ syncPosition(jsonObject);
31167
+ if (jsonObject.userData.isDeclared === undefined) {
31168
+ jsonObject.userData.isDeclared = true;
31169
+ }
31170
+ } else if (type === 'component' && Array.isArray(jsonObject.children)) {
31171
+ // Connectors are injected as JSON children by _injectConnectorChildrenFromDictionary
31172
+ // and their Three.js objects exist in the scene, created recursively by createSceneObject
31173
+ jsonObject.children.forEach(function (childJson) {
31174
+ var _childJson$userData;
31175
+ if (((_childJson$userData = childJson.userData) === null || _childJson$userData === void 0 ? void 0 : _childJson$userData.objectType) === 'connector') {
31176
+ syncPosition(childJson);
31241
31177
  }
31242
- }
31178
+ });
31243
31179
  }
31244
31180
  });
31245
31181
  }
@@ -31404,10 +31340,10 @@ var SceneOperationsManager = /*#__PURE__*/function () {
31404
31340
  var componentsProcessed = 0;
31405
31341
  var connectorsInjected = 0;
31406
31342
  data.scene.children.forEach(function (child) {
31407
- var _child$userData5, _child$userData6, _child$userData7;
31408
- 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);
31343
+ var _child$userData4, _child$userData5, _child$userData6;
31344
+ 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);
31409
31345
  // Only process components with libraryId
31410
- if (childType === 'component' && (_child$userData7 = child.userData) !== null && _child$userData7 !== void 0 && _child$userData7.libraryId) {
31346
+ if (childType === 'component' && (_child$userData6 = child.userData) !== null && _child$userData6 !== void 0 && _child$userData6.libraryId) {
31411
31347
  var libraryId = child.userData.libraryId;
31412
31348
  var dictEntry = componentDictionary[libraryId];
31413
31349
 
@@ -31564,23 +31500,25 @@ var SceneOperationsManager = /*#__PURE__*/function () {
31564
31500
  geometries = this.createSceneGeometries(data, componentDictionary); // Create basic objects and track GLB replacements
31565
31501
  libraryObjectsToReplace = [];
31566
31502
  data.scene.children.forEach(function (child, index) {
31567
- var _child$userData8, _child$userData9;
31503
+ var _child$userData7, _child$userData8;
31568
31504
  var createdObject = _this4.createSceneObject(child, geometries, materials, componentDictionary);
31569
31505
  _this4.sceneViewer.scene.add(createdObject);
31570
31506
 
31571
31507
  // Track objects that need GLB model replacement
31572
- 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]) {
31573
- var _child$userData0;
31508
+ 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]) {
31509
+ var _child$userData9;
31574
31510
  libraryObjectsToReplace.push({
31575
31511
  basicObject: createdObject,
31576
31512
  jsonData: child,
31577
- componentData: componentDictionary[(_child$userData0 = child.userData) === null || _child$userData0 === void 0 ? void 0 : _child$userData0.libraryId]
31513
+ componentData: componentDictionary[(_child$userData9 = child.userData) === null || _child$userData9 === void 0 ? void 0 : _child$userData9.libraryId]
31578
31514
  });
31579
31515
  }
31580
31516
  });
31581
31517
 
31582
- // Compute bounding boxes for pathfinding
31583
- this.computeWorldBoundingBoxes(data);
31518
+ // Sync gateway/connector world positions into JSON before pathfinding.
31519
+ // Bounding boxes are computed later by PathfindingManager._enrichSceneDataWithBoundingBoxes
31520
+ // (after GLB models are loaded), so no bbox work is done here.
31521
+ this._syncPositionsForPathfinding(data);
31584
31522
  this._saveOriginalWorldMatrices(this.sceneViewer.scene);
31585
31523
  return _context6.a(2, {
31586
31524
  componentDictionary: componentDictionary,
@@ -31726,8 +31664,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
31726
31664
  var instanceBehaviors = [];
31727
31665
  if (Array.isArray(data === null || data === void 0 || (_data$scene3 = data.scene) === null || _data$scene3 === void 0 ? void 0 : _data$scene3.children)) {
31728
31666
  data.scene.children.forEach(function (child) {
31729
- var _child$userData1, _compData$defaultBeha;
31730
- var libraryId = (_child$userData1 = child.userData) === null || _child$userData1 === void 0 ? void 0 : _child$userData1.libraryId;
31667
+ var _child$userData0, _compData$defaultBeha;
31668
+ var libraryId = (_child$userData0 = child.userData) === null || _child$userData0 === void 0 ? void 0 : _child$userData0.libraryId;
31731
31669
  if (!libraryId) return;
31732
31670
  var instanceUuid = child.uuid;
31733
31671
  // Skip instances whose defaults were already resolved by Step A
@@ -31923,8 +31861,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
31923
31861
  key: "_saveOriginalWorldMatrices",
31924
31862
  value: function _saveOriginalWorldMatrices(scene) {
31925
31863
  scene.traverse(function (object) {
31926
- var _object$userData2;
31927
- if ((_object$userData2 = object.userData) !== null && _object$userData2 !== void 0 && _object$userData2.direction) {
31864
+ var _object$userData;
31865
+ if ((_object$userData = object.userData) !== null && _object$userData !== void 0 && _object$userData.direction) {
31928
31866
  var originalMatrix = new THREE__namespace.Matrix4();
31929
31867
  originalMatrix.copy(object.matrixWorld);
31930
31868
  }
@@ -32128,8 +32066,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
32128
32066
  // Process children (connectors, etc.) if they exist
32129
32067
  if (componentModel.children && componentModel.children.length > 0) {
32130
32068
  componentModel.children.forEach(function (child) {
32131
- var _child$userData10, _child$userData11;
32132
- 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);
32069
+ var _child$userData1, _child$userData10;
32070
+ 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);
32133
32071
  if (childType === 'connector') {
32134
32072
  var _child$geometry;
32135
32073
  var childBoundingBox = new THREE__namespace.Box3().setFromObject(child);
@@ -32214,8 +32152,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
32214
32152
  if (segment.children && segment.children.length > 0) {
32215
32153
  var childrenToRemove = _toConsumableArray(segment.children);
32216
32154
  childrenToRemove.forEach(function (child) {
32217
- var _child$userData12;
32218
- if ((_child$userData12 = child.userData) !== null && _child$userData12 !== void 0 && _child$userData12.isPipeElbow) {
32155
+ var _child$userData11;
32156
+ if ((_child$userData11 = child.userData) !== null && _child$userData11 !== void 0 && _child$userData11.isPipeElbow) {
32219
32157
  console.log("\uD83D\uDDD1\uFE0F Removing elbow child from segment before manualization: ".concat(child.uuid));
32220
32158
  segment.remove(child);
32221
32159
  if (child.geometry) child.geometry.dispose();
@@ -35485,6 +35423,18 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
35485
35423
  // Map of viewport instances by viewType or custom key
35486
35424
  _this2.viewports = new Map();
35487
35425
 
35426
+ // Per-refresh-cycle bbox cache: keyed by object.uuid, cleared each refresh()
35427
+ // so each component bbox is computed once per cycle regardless of viewport count
35428
+ _this2._bboxCache = new Map();
35429
+
35430
+ // Per-refresh-cycle component list cache: eliminates redundant scene traversals
35431
+ // when all 3 viewports render in the same cycle
35432
+ _this2._componentListCache = null;
35433
+
35434
+ // rAF debounce flag — prevents multiple same-frame refresh() calls from
35435
+ // stacking up independent renderComponents() runs
35436
+ _this2._refreshPending = false;
35437
+
35488
35438
  // Event listener reference for cleanup
35489
35439
  _this2._objectTransformedListener = null;
35490
35440
  console.log('🔲 Viewport2DManager initialized (multi-instance support)');
@@ -35506,7 +35456,6 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
35506
35456
 
35507
35457
  // Listen for object transformations to refresh all viewports
35508
35458
  this._objectTransformedListener = function (eventData) {
35509
- console.log('🔲 Viewport2DManager detected object transformation, refreshing all viewports');
35510
35459
  _this3.refresh(); // Refresh all viewports
35511
35460
  };
35512
35461
 
@@ -35572,6 +35521,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
35572
35521
  case 3:
35573
35522
  // Create new viewport instance
35574
35523
  viewport = new Viewport2DInstance(this.sceneViewer, this.Konva, viewType, container);
35524
+ viewport._instanceKey = key;
35575
35525
  this.viewports.set(key, viewport);
35576
35526
 
35577
35527
  // Initialize the stage for this viewport
@@ -35749,9 +35699,9 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
35749
35699
  viewport.stage.width(width);
35750
35700
  viewport.stage.height(height);
35751
35701
 
35752
- // Redraw content
35702
+ // Redraw grid immediately; schedule debounced component render
35753
35703
  this.drawGrid(viewport);
35754
- this.renderComponents(viewport);
35704
+ this.refresh(viewport._instanceKey);
35755
35705
  }
35756
35706
  }
35757
35707
 
@@ -35934,7 +35884,6 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
35934
35884
  worldDepth = _this$getComponentDim.worldDepth,
35935
35885
  worldHeight = _this$getComponentDim.worldHeight,
35936
35886
  bboxCenter = _this$getComponentDim.bboxCenter;
35937
- 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), ")"));
35938
35887
 
35939
35888
  // Project 3D bbox center to 2D based on view type
35940
35889
  var _this$project3DTo2D = this.project3DTo2D(viewport, bboxCenter, worldWidth, worldDepth, worldHeight),
@@ -36001,21 +35950,45 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36001
35950
  }, {
36002
35951
  key: "_getOrComputeWorldBoundingBox",
36003
35952
  value: function _getOrComputeWorldBoundingBox(object) {
36004
- // computeFilteredBoundingBox with no exclusions = full object bbox
35953
+ var _object$userData, _object$userData2;
35954
+ // Fast path: offset the stored world bbox by the position delta since load time.
35955
+ // Translation only shifts the bbox center — extents stay identical — so this is O(1)
35956
+ // instead of O(meshes × vertices) from a full geometry traversal.
35957
+ var stored = (_object$userData = object.userData) === null || _object$userData === void 0 ? void 0 : _object$userData.worldBoundingBox;
35958
+ var basePos = (_object$userData2 = object.userData) === null || _object$userData2 === void 0 ? void 0 : _object$userData2._wbbBasePosition;
35959
+ if (stored && basePos) {
35960
+ var dx = object.position.x - basePos.x;
35961
+ var dy = object.position.y - basePos.y;
35962
+ var dz = object.position.z - basePos.z;
35963
+ if (dx === 0 && dy === 0 && dz === 0) return stored;
35964
+ return {
35965
+ min: [stored.min[0] + dx, stored.min[1] + dy, stored.min[2] + dz],
35966
+ max: [stored.max[0] + dx, stored.max[1] + dy, stored.max[2] + dz]
35967
+ };
35968
+ }
35969
+
35970
+ // Slow path: full vertex-accurate traversal (fallback when userData not populated)
35971
+ if (this._bboxCache.has(object.uuid)) {
35972
+ return this._bboxCache.get(object.uuid);
35973
+ }
36005
35974
  var box = computeFilteredBoundingBox(object, []);
35975
+ var result;
36006
35976
  if (box.isEmpty()) {
36007
35977
  // Object has no geometry; fall back to a point at world position
36008
35978
  var wp = new THREE__namespace.Vector3();
36009
35979
  object.getWorldPosition(wp);
36010
- return {
35980
+ result = {
36011
35981
  min: [wp.x, wp.y, wp.z],
36012
35982
  max: [wp.x, wp.y, wp.z]
36013
35983
  };
35984
+ } else {
35985
+ result = {
35986
+ min: [box.min.x, box.min.y, box.min.z],
35987
+ max: [box.max.x, box.max.y, box.max.z]
35988
+ };
36014
35989
  }
36015
- return {
36016
- min: [box.min.x, box.min.y, box.min.z],
36017
- max: [box.max.x, box.max.y, box.max.z]
36018
- };
35990
+ this._bboxCache.set(object.uuid, result);
35991
+ return result;
36019
35992
  }
36020
35993
 
36021
35994
  /**
@@ -36024,11 +35997,12 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36024
35997
  }, {
36025
35998
  key: "getComponentDimensions",
36026
35999
  value: function getComponentDimensions(component) {
36027
- var _component$userData$w, _component$userData, _component$getWorldPo;
36028
- // Prefer worldBoundingBox already stored on the object set after GLB loading
36029
- // by modelManager.replaceWithGLBModels using computeFilteredBoundingBox.
36030
- // Fall back to computing live if not yet available (e.g. first render before GLB load).
36031
- 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);
36000
+ var _component$getWorldPo;
36001
+ // Always recompute from the live Three.js object so that the rect reflects
36002
+ // the current world position after translate/drag operations.
36003
+ // userData.worldBoundingBox is a load-time snapshot and goes stale whenever
36004
+ // the object moves, so we cannot rely on it here.
36005
+ var wbb = this._getOrComputeWorldBoundingBox(component);
36032
36006
  if (wbb !== null && wbb !== void 0 && wbb.min && wbb !== null && wbb !== void 0 && wbb.max) {
36033
36007
  var _wbb$min = _slicedToArray(wbb.min, 3),
36034
36008
  minX = _wbb$min[0],
@@ -36139,7 +36113,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36139
36113
  rect.stroke('#007bff');
36140
36114
  rect.strokeWidth(3);
36141
36115
  viewport.stage.container().style.cursor = 'grab';
36142
- viewport.componentLayer.draw();
36116
+ viewport.componentLayer.batchDraw();
36143
36117
  }
36144
36118
  });
36145
36119
  rect.on('mouseleave', function () {
@@ -36148,7 +36122,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36148
36122
  rect.stroke(colors.stroke);
36149
36123
  rect.strokeWidth(2);
36150
36124
  viewport.stage.container().style.cursor = 'default';
36151
- viewport.componentLayer.draw();
36125
+ viewport.componentLayer.batchDraw();
36152
36126
  }
36153
36127
  });
36154
36128
 
@@ -36192,7 +36166,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36192
36166
  // Snap to grid
36193
36167
  var snappedPos = _this6.snapScreenToGrid(viewport, currentPos.x, currentPos.y, scale, worldOriginX, worldOriginY);
36194
36168
  componentGroup.position(snappedPos);
36195
- viewport.componentLayer.draw();
36169
+ viewport.componentLayer.batchDraw();
36196
36170
  });
36197
36171
 
36198
36172
  // DRAG END
@@ -36365,18 +36339,20 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36365
36339
  }, {
36366
36340
  key: "getSceneComponents",
36367
36341
  value: function getSceneComponents() {
36342
+ if (this._componentListCache) return this._componentListCache;
36368
36343
  if (!this.sceneViewer || !this.sceneViewer.scene) {
36369
36344
  return [];
36370
36345
  }
36371
36346
  var components = [];
36372
36347
  this.sceneViewer.scene.traverse(function (object) {
36373
- var _object$userData, _object$userData2;
36348
+ var _object$userData3, _object$userData4;
36374
36349
  // Only match the ROOT component object — must have both objectType:'component'
36375
36350
  // AND libraryId (inner GLB mesh nodes don't have libraryId)
36376
- 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) {
36351
+ 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) {
36377
36352
  components.push(object);
36378
36353
  }
36379
36354
  });
36355
+ this._componentListCache = components;
36380
36356
  return components;
36381
36357
  }
36382
36358
 
@@ -36482,35 +36458,45 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36482
36458
  }
36483
36459
 
36484
36460
  /**
36485
- * Refresh a specific viewport or all viewports
36461
+ * Refresh a specific viewport or all viewports.
36462
+ * Debounced via requestAnimationFrame so multiple calls within the same
36463
+ * frame (e.g. from Viewport2D mount + refreshAll2DViews) collapse into one.
36486
36464
  * @param {string} key - Optional viewport key. If not provided, refreshes all viewports
36487
36465
  */
36488
36466
  }, {
36489
36467
  key: "refresh",
36490
36468
  value: function refresh() {
36469
+ var _this7 = this;
36491
36470
  var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
36492
- if (key) {
36493
- var viewport = this.viewports.get(key);
36494
- if (viewport && viewport.isReady) {
36495
- this.renderComponents(viewport);
36496
- }
36497
- } else {
36498
- // Refresh all viewports
36499
- var _iterator = _createForOfIteratorHelper(this.viewports.values()),
36500
- _step;
36501
- try {
36502
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
36503
- var _viewport = _step.value;
36504
- if (_viewport.isReady) {
36505
- this.renderComponents(_viewport);
36471
+ if (this._refreshPending) return;
36472
+ this._refreshPending = true;
36473
+ requestAnimationFrame(function () {
36474
+ _this7._refreshPending = false;
36475
+ // Clear per-cycle caches so each component is measured/traversed once per paint
36476
+ _this7._bboxCache.clear();
36477
+ _this7._componentListCache = null;
36478
+ if (key) {
36479
+ var viewport = _this7.viewports.get(key);
36480
+ if (viewport && viewport.isReady) {
36481
+ _this7.renderComponents(viewport);
36482
+ }
36483
+ } else {
36484
+ var _iterator = _createForOfIteratorHelper(_this7.viewports.values()),
36485
+ _step;
36486
+ try {
36487
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
36488
+ var _viewport = _step.value;
36489
+ if (_viewport.isReady) {
36490
+ _this7.renderComponents(_viewport);
36491
+ }
36506
36492
  }
36493
+ } catch (err) {
36494
+ _iterator.e(err);
36495
+ } finally {
36496
+ _iterator.f();
36507
36497
  }
36508
- } catch (err) {
36509
- _iterator.e(err);
36510
- } finally {
36511
- _iterator.f();
36512
36498
  }
36513
- }
36499
+ });
36514
36500
  }
36515
36501
 
36516
36502
  /**
@@ -37830,7 +37816,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
37830
37816
  * Initialize the CentralPlant manager
37831
37817
  *
37832
37818
  * @constructor
37833
- * @version 0.3.5
37819
+ * @version 0.3.6
37834
37820
  * @updated 2025-10-22
37835
37821
  *
37836
37822
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -35,7 +35,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
35
35
  * Initialize the CentralPlant manager
36
36
  *
37
37
  * @constructor
38
- * @version 0.3.5
38
+ * @version 0.3.6
39
39
  * @updated 2025-10-22
40
40
  *
41
41
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -595,6 +595,13 @@ var ModelManager = /*#__PURE__*/function () {
595
595
  // Update both the JSON data object AND the live scene object
596
596
  jsonData.userData.worldBoundingBox = worldBoundingBox;
597
597
  glbModel.userData.worldBoundingBox = worldBoundingBox;
598
+ // Snapshot the object's local position so viewport2DManager can compute
599
+ // 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
+ };
598
605
  });
599
606
 
600
607
  // Dispatch completion event