@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.
@@ -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);
@@ -30381,6 +30388,11 @@ var SceneClearingUtility = /*#__PURE__*/function () {
30381
30388
  this.sceneViewer.currentSceneData.connections = [];
30382
30389
  }
30383
30390
 
30391
+ // Clear the JSON data mirror so getAvailableConnections() returns nothing
30392
+ if (this.sceneViewer.currentSceneData && this.sceneViewer.currentSceneData.scene) {
30393
+ this.sceneViewer.currentSceneData.scene.children = [];
30394
+ }
30395
+
30384
30396
  // Reset component counter for CentralPlant
30385
30397
  if (this.sceneViewer.centralPlant && this.sceneViewer.centralPlant.componentCounter !== undefined) {
30386
30398
  this.sceneViewer.centralPlant.componentCounter = 0;
@@ -31123,123 +31135,52 @@ var SceneOperationsManager = /*#__PURE__*/function () {
31123
31135
  }
31124
31136
 
31125
31137
  /**
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];
31138
+ * Sync world-space positions and isDeclared flags for gateways and connectors
31139
+ * into the scene JSON data so the pathfinder can read them.
31140
+ *
31141
+ * Bounding boxes for components and segments are intentionally NOT computed here.
31142
+ * They are computed (with matrix-hash caching) by
31143
+ * PathfindingManager._enrichSceneDataWithBoundingBoxes(), which runs after GLB
31144
+ * models are fully loaded and therefore produces correct values.
31145
+ */
31146
+ }, {
31147
+ key: "_syncPositionsForPathfinding",
31148
+ value: function _syncPositionsForPathfinding(data) {
31149
+ var scene = this.sceneViewer.scene;
31150
+ var worldPos = new THREE__namespace.Vector3();
31151
+ var syncPosition = function syncPosition(jsonObject) {
31152
+ var _jsonObject$userData;
31153
+ var object = scene.getObjectByProperty('uuid', jsonObject.uuid) || scene.getObjectByProperty('uuid', (_jsonObject$userData = jsonObject.userData) === null || _jsonObject$userData === void 0 ? void 0 : _jsonObject$userData.originalUuid);
31154
+ if (!object) return;
31155
+ object.getWorldPosition(worldPos);
31156
+ var pos = [worldPos.x, worldPos.y, worldPos.z];
31157
+ jsonObject.userData.position = pos;
31158
+ object.userData.position = pos;
31159
+ };
31160
+ data.scene.children.forEach(function (jsonObject) {
31161
+ var _jsonObject$userData2;
31162
+ var type = (_jsonObject$userData2 = jsonObject.userData) === null || _jsonObject$userData2 === void 0 ? void 0 : _jsonObject$userData2.objectType;
31163
+ if (type === 'gateway') {
31164
+ syncPosition(jsonObject);
31165
+ if (jsonObject.userData.isDeclared === undefined) {
31166
+ jsonObject.userData.isDeclared = true;
31167
+ }
31168
+ } else if (type === 'connector') {
31169
+ syncPosition(jsonObject);
31170
+ } else if (type === 'segment-connector') {
31171
+ syncPosition(jsonObject);
31172
+ if (jsonObject.userData.isDeclared === undefined) {
31173
+ jsonObject.userData.isDeclared = true;
31174
+ }
31175
+ } else if (type === 'component' && Array.isArray(jsonObject.children)) {
31176
+ // Connectors are injected as JSON children by _injectConnectorChildrenFromDictionary
31177
+ // and their Three.js objects exist in the scene, created recursively by createSceneObject
31178
+ jsonObject.children.forEach(function (childJson) {
31179
+ var _childJson$userData;
31180
+ if (((_childJson$userData = childJson.userData) === null || _childJson$userData === void 0 ? void 0 : _childJson$userData.objectType) === 'connector') {
31181
+ syncPosition(childJson);
31241
31182
  }
31242
- }
31183
+ });
31243
31184
  }
31244
31185
  });
31245
31186
  }
@@ -31404,10 +31345,10 @@ var SceneOperationsManager = /*#__PURE__*/function () {
31404
31345
  var componentsProcessed = 0;
31405
31346
  var connectorsInjected = 0;
31406
31347
  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);
31348
+ var _child$userData4, _child$userData5, _child$userData6;
31349
+ 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
31350
  // Only process components with libraryId
31410
- if (childType === 'component' && (_child$userData7 = child.userData) !== null && _child$userData7 !== void 0 && _child$userData7.libraryId) {
31351
+ if (childType === 'component' && (_child$userData6 = child.userData) !== null && _child$userData6 !== void 0 && _child$userData6.libraryId) {
31411
31352
  var libraryId = child.userData.libraryId;
31412
31353
  var dictEntry = componentDictionary[libraryId];
31413
31354
 
@@ -31564,23 +31505,25 @@ var SceneOperationsManager = /*#__PURE__*/function () {
31564
31505
  geometries = this.createSceneGeometries(data, componentDictionary); // Create basic objects and track GLB replacements
31565
31506
  libraryObjectsToReplace = [];
31566
31507
  data.scene.children.forEach(function (child, index) {
31567
- var _child$userData8, _child$userData9;
31508
+ var _child$userData7, _child$userData8;
31568
31509
  var createdObject = _this4.createSceneObject(child, geometries, materials, componentDictionary);
31569
31510
  _this4.sceneViewer.scene.add(createdObject);
31570
31511
 
31571
31512
  // 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;
31513
+ 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]) {
31514
+ var _child$userData9;
31574
31515
  libraryObjectsToReplace.push({
31575
31516
  basicObject: createdObject,
31576
31517
  jsonData: child,
31577
- componentData: componentDictionary[(_child$userData0 = child.userData) === null || _child$userData0 === void 0 ? void 0 : _child$userData0.libraryId]
31518
+ componentData: componentDictionary[(_child$userData9 = child.userData) === null || _child$userData9 === void 0 ? void 0 : _child$userData9.libraryId]
31578
31519
  });
31579
31520
  }
31580
31521
  });
31581
31522
 
31582
- // Compute bounding boxes for pathfinding
31583
- this.computeWorldBoundingBoxes(data);
31523
+ // Sync gateway/connector world positions into JSON before pathfinding.
31524
+ // Bounding boxes are computed later by PathfindingManager._enrichSceneDataWithBoundingBoxes
31525
+ // (after GLB models are loaded), so no bbox work is done here.
31526
+ this._syncPositionsForPathfinding(data);
31584
31527
  this._saveOriginalWorldMatrices(this.sceneViewer.scene);
31585
31528
  return _context6.a(2, {
31586
31529
  componentDictionary: componentDictionary,
@@ -31726,8 +31669,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
31726
31669
  var instanceBehaviors = [];
31727
31670
  if (Array.isArray(data === null || data === void 0 || (_data$scene3 = data.scene) === null || _data$scene3 === void 0 ? void 0 : _data$scene3.children)) {
31728
31671
  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;
31672
+ var _child$userData0, _compData$defaultBeha;
31673
+ var libraryId = (_child$userData0 = child.userData) === null || _child$userData0 === void 0 ? void 0 : _child$userData0.libraryId;
31731
31674
  if (!libraryId) return;
31732
31675
  var instanceUuid = child.uuid;
31733
31676
  // Skip instances whose defaults were already resolved by Step A
@@ -31923,8 +31866,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
31923
31866
  key: "_saveOriginalWorldMatrices",
31924
31867
  value: function _saveOriginalWorldMatrices(scene) {
31925
31868
  scene.traverse(function (object) {
31926
- var _object$userData2;
31927
- if ((_object$userData2 = object.userData) !== null && _object$userData2 !== void 0 && _object$userData2.direction) {
31869
+ var _object$userData;
31870
+ if ((_object$userData = object.userData) !== null && _object$userData !== void 0 && _object$userData.direction) {
31928
31871
  var originalMatrix = new THREE__namespace.Matrix4();
31929
31872
  originalMatrix.copy(object.matrixWorld);
31930
31873
  }
@@ -32128,8 +32071,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
32128
32071
  // Process children (connectors, etc.) if they exist
32129
32072
  if (componentModel.children && componentModel.children.length > 0) {
32130
32073
  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);
32074
+ var _child$userData1, _child$userData10;
32075
+ 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
32076
  if (childType === 'connector') {
32134
32077
  var _child$geometry;
32135
32078
  var childBoundingBox = new THREE__namespace.Box3().setFromObject(child);
@@ -32214,8 +32157,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
32214
32157
  if (segment.children && segment.children.length > 0) {
32215
32158
  var childrenToRemove = _toConsumableArray(segment.children);
32216
32159
  childrenToRemove.forEach(function (child) {
32217
- var _child$userData12;
32218
- if ((_child$userData12 = child.userData) !== null && _child$userData12 !== void 0 && _child$userData12.isPipeElbow) {
32160
+ var _child$userData11;
32161
+ if ((_child$userData11 = child.userData) !== null && _child$userData11 !== void 0 && _child$userData11.isPipeElbow) {
32219
32162
  console.log("\uD83D\uDDD1\uFE0F Removing elbow child from segment before manualization: ".concat(child.uuid));
32220
32163
  segment.remove(child);
32221
32164
  if (child.geometry) child.geometry.dispose();
@@ -35485,6 +35428,18 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
35485
35428
  // Map of viewport instances by viewType or custom key
35486
35429
  _this2.viewports = new Map();
35487
35430
 
35431
+ // Per-refresh-cycle bbox cache: keyed by object.uuid, cleared each refresh()
35432
+ // so each component bbox is computed once per cycle regardless of viewport count
35433
+ _this2._bboxCache = new Map();
35434
+
35435
+ // Per-refresh-cycle component list cache: eliminates redundant scene traversals
35436
+ // when all 3 viewports render in the same cycle
35437
+ _this2._componentListCache = null;
35438
+
35439
+ // rAF debounce flag — prevents multiple same-frame refresh() calls from
35440
+ // stacking up independent renderComponents() runs
35441
+ _this2._refreshPending = false;
35442
+
35488
35443
  // Event listener reference for cleanup
35489
35444
  _this2._objectTransformedListener = null;
35490
35445
  console.log('🔲 Viewport2DManager initialized (multi-instance support)');
@@ -35506,7 +35461,6 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
35506
35461
 
35507
35462
  // Listen for object transformations to refresh all viewports
35508
35463
  this._objectTransformedListener = function (eventData) {
35509
- console.log('🔲 Viewport2DManager detected object transformation, refreshing all viewports');
35510
35464
  _this3.refresh(); // Refresh all viewports
35511
35465
  };
35512
35466
 
@@ -35572,6 +35526,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
35572
35526
  case 3:
35573
35527
  // Create new viewport instance
35574
35528
  viewport = new Viewport2DInstance(this.sceneViewer, this.Konva, viewType, container);
35529
+ viewport._instanceKey = key;
35575
35530
  this.viewports.set(key, viewport);
35576
35531
 
35577
35532
  // Initialize the stage for this viewport
@@ -35749,9 +35704,9 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
35749
35704
  viewport.stage.width(width);
35750
35705
  viewport.stage.height(height);
35751
35706
 
35752
- // Redraw content
35707
+ // Redraw grid immediately; schedule debounced component render
35753
35708
  this.drawGrid(viewport);
35754
- this.renderComponents(viewport);
35709
+ this.refresh(viewport._instanceKey);
35755
35710
  }
35756
35711
  }
35757
35712
 
@@ -35934,7 +35889,6 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
35934
35889
  worldDepth = _this$getComponentDim.worldDepth,
35935
35890
  worldHeight = _this$getComponentDim.worldHeight,
35936
35891
  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
35892
 
35939
35893
  // Project 3D bbox center to 2D based on view type
35940
35894
  var _this$project3DTo2D = this.project3DTo2D(viewport, bboxCenter, worldWidth, worldDepth, worldHeight),
@@ -36001,21 +35955,45 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36001
35955
  }, {
36002
35956
  key: "_getOrComputeWorldBoundingBox",
36003
35957
  value: function _getOrComputeWorldBoundingBox(object) {
36004
- // computeFilteredBoundingBox with no exclusions = full object bbox
35958
+ var _object$userData, _object$userData2;
35959
+ // Fast path: offset the stored world bbox by the position delta since load time.
35960
+ // Translation only shifts the bbox center — extents stay identical — so this is O(1)
35961
+ // instead of O(meshes × vertices) from a full geometry traversal.
35962
+ var stored = (_object$userData = object.userData) === null || _object$userData === void 0 ? void 0 : _object$userData.worldBoundingBox;
35963
+ var basePos = (_object$userData2 = object.userData) === null || _object$userData2 === void 0 ? void 0 : _object$userData2._wbbBasePosition;
35964
+ if (stored && basePos) {
35965
+ var dx = object.position.x - basePos.x;
35966
+ var dy = object.position.y - basePos.y;
35967
+ var dz = object.position.z - basePos.z;
35968
+ if (dx === 0 && dy === 0 && dz === 0) return stored;
35969
+ return {
35970
+ min: [stored.min[0] + dx, stored.min[1] + dy, stored.min[2] + dz],
35971
+ max: [stored.max[0] + dx, stored.max[1] + dy, stored.max[2] + dz]
35972
+ };
35973
+ }
35974
+
35975
+ // Slow path: full vertex-accurate traversal (fallback when userData not populated)
35976
+ if (this._bboxCache.has(object.uuid)) {
35977
+ return this._bboxCache.get(object.uuid);
35978
+ }
36005
35979
  var box = computeFilteredBoundingBox(object, []);
35980
+ var result;
36006
35981
  if (box.isEmpty()) {
36007
35982
  // Object has no geometry; fall back to a point at world position
36008
35983
  var wp = new THREE__namespace.Vector3();
36009
35984
  object.getWorldPosition(wp);
36010
- return {
35985
+ result = {
36011
35986
  min: [wp.x, wp.y, wp.z],
36012
35987
  max: [wp.x, wp.y, wp.z]
36013
35988
  };
35989
+ } else {
35990
+ result = {
35991
+ min: [box.min.x, box.min.y, box.min.z],
35992
+ max: [box.max.x, box.max.y, box.max.z]
35993
+ };
36014
35994
  }
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
- };
35995
+ this._bboxCache.set(object.uuid, result);
35996
+ return result;
36019
35997
  }
36020
35998
 
36021
35999
  /**
@@ -36024,11 +36002,12 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36024
36002
  }, {
36025
36003
  key: "getComponentDimensions",
36026
36004
  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);
36005
+ var _component$getWorldPo;
36006
+ // Always recompute from the live Three.js object so that the rect reflects
36007
+ // the current world position after translate/drag operations.
36008
+ // userData.worldBoundingBox is a load-time snapshot and goes stale whenever
36009
+ // the object moves, so we cannot rely on it here.
36010
+ var wbb = this._getOrComputeWorldBoundingBox(component);
36032
36011
  if (wbb !== null && wbb !== void 0 && wbb.min && wbb !== null && wbb !== void 0 && wbb.max) {
36033
36012
  var _wbb$min = _slicedToArray(wbb.min, 3),
36034
36013
  minX = _wbb$min[0],
@@ -36139,7 +36118,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36139
36118
  rect.stroke('#007bff');
36140
36119
  rect.strokeWidth(3);
36141
36120
  viewport.stage.container().style.cursor = 'grab';
36142
- viewport.componentLayer.draw();
36121
+ viewport.componentLayer.batchDraw();
36143
36122
  }
36144
36123
  });
36145
36124
  rect.on('mouseleave', function () {
@@ -36148,7 +36127,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36148
36127
  rect.stroke(colors.stroke);
36149
36128
  rect.strokeWidth(2);
36150
36129
  viewport.stage.container().style.cursor = 'default';
36151
- viewport.componentLayer.draw();
36130
+ viewport.componentLayer.batchDraw();
36152
36131
  }
36153
36132
  });
36154
36133
 
@@ -36192,7 +36171,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36192
36171
  // Snap to grid
36193
36172
  var snappedPos = _this6.snapScreenToGrid(viewport, currentPos.x, currentPos.y, scale, worldOriginX, worldOriginY);
36194
36173
  componentGroup.position(snappedPos);
36195
- viewport.componentLayer.draw();
36174
+ viewport.componentLayer.batchDraw();
36196
36175
  });
36197
36176
 
36198
36177
  // DRAG END
@@ -36365,18 +36344,20 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36365
36344
  }, {
36366
36345
  key: "getSceneComponents",
36367
36346
  value: function getSceneComponents() {
36347
+ if (this._componentListCache) return this._componentListCache;
36368
36348
  if (!this.sceneViewer || !this.sceneViewer.scene) {
36369
36349
  return [];
36370
36350
  }
36371
36351
  var components = [];
36372
36352
  this.sceneViewer.scene.traverse(function (object) {
36373
- var _object$userData, _object$userData2;
36353
+ var _object$userData3, _object$userData4;
36374
36354
  // Only match the ROOT component object — must have both objectType:'component'
36375
36355
  // 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) {
36356
+ 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
36357
  components.push(object);
36378
36358
  }
36379
36359
  });
36360
+ this._componentListCache = components;
36380
36361
  return components;
36381
36362
  }
36382
36363
 
@@ -36482,35 +36463,45 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36482
36463
  }
36483
36464
 
36484
36465
  /**
36485
- * Refresh a specific viewport or all viewports
36466
+ * Refresh a specific viewport or all viewports.
36467
+ * Debounced via requestAnimationFrame so multiple calls within the same
36468
+ * frame (e.g. from Viewport2D mount + refreshAll2DViews) collapse into one.
36486
36469
  * @param {string} key - Optional viewport key. If not provided, refreshes all viewports
36487
36470
  */
36488
36471
  }, {
36489
36472
  key: "refresh",
36490
36473
  value: function refresh() {
36474
+ var _this7 = this;
36491
36475
  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);
36476
+ if (this._refreshPending) return;
36477
+ this._refreshPending = true;
36478
+ requestAnimationFrame(function () {
36479
+ _this7._refreshPending = false;
36480
+ // Clear per-cycle caches so each component is measured/traversed once per paint
36481
+ _this7._bboxCache.clear();
36482
+ _this7._componentListCache = null;
36483
+ if (key) {
36484
+ var viewport = _this7.viewports.get(key);
36485
+ if (viewport && viewport.isReady) {
36486
+ _this7.renderComponents(viewport);
36487
+ }
36488
+ } else {
36489
+ var _iterator = _createForOfIteratorHelper(_this7.viewports.values()),
36490
+ _step;
36491
+ try {
36492
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
36493
+ var _viewport = _step.value;
36494
+ if (_viewport.isReady) {
36495
+ _this7.renderComponents(_viewport);
36496
+ }
36506
36497
  }
36498
+ } catch (err) {
36499
+ _iterator.e(err);
36500
+ } finally {
36501
+ _iterator.f();
36507
36502
  }
36508
- } catch (err) {
36509
- _iterator.e(err);
36510
- } finally {
36511
- _iterator.f();
36512
36503
  }
36513
- }
36504
+ });
36514
36505
  }
36515
36506
 
36516
36507
  /**
@@ -37830,7 +37821,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
37830
37821
  * Initialize the CentralPlant manager
37831
37822
  *
37832
37823
  * @constructor
37833
- * @version 0.3.5
37824
+ * @version 0.3.7
37834
37825
  * @updated 2025-10-22
37835
37826
  *
37836
37827
  * @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.7
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