@2112-lab/central-plant 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29813,15 +29813,28 @@ var ModelManager = /*#__PURE__*/function () {
29813
29813
  _context5.n = 2;
29814
29814
  return Promise.all(glbLoadingPromises);
29815
29815
  case 2:
29816
- // Update world bounding boxes for loaded models
29816
+ // Update world bounding boxes for loaded models and propagate to the live Three.js objects
29817
29817
  libraryObjectsToReplace.forEach(function (_ref2) {
29818
- var _jsonData$userData2;
29819
29818
  var jsonData = _ref2.jsonData,
29820
29819
  glbModel = _ref2.glbModel;
29821
- if (glbModel && (_jsonData$userData2 = jsonData.userData) !== null && _jsonData$userData2 !== void 0 && _jsonData$userData2.worldBoundingBox) {
29822
- var worldBoundingBox = _this3._calculateWorldBoundingBox(glbModel);
29823
- jsonData.userData.worldBoundingBox = worldBoundingBox;
29824
- }
29820
+ if (!glbModel) return;
29821
+ // Use filtered bbox (excludes connectors + io-devices) so it matches
29822
+ // what pathfindingManager._enrichSceneDataWithBoundingBoxes produces
29823
+ var filteredBox = computeFilteredBoundingBox(glbModel, ['io-device', 'connector']);
29824
+ var worldBoundingBox = filteredBox.isEmpty() ? _this3._calculateWorldBoundingBox(glbModel) : {
29825
+ min: [filteredBox.min.x, filteredBox.min.y, filteredBox.min.z],
29826
+ max: [filteredBox.max.x, filteredBox.max.y, filteredBox.max.z]
29827
+ };
29828
+ // Update both the JSON data object AND the live scene object
29829
+ jsonData.userData.worldBoundingBox = worldBoundingBox;
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
+ };
29825
29838
  });
29826
29839
 
29827
29840
  // Dispatch completion event
@@ -30121,10 +30134,10 @@ var SceneClearingUtility = /*#__PURE__*/function () {
30121
30134
  throw new Error('Scene not available for clearing');
30122
30135
  case 1:
30123
30136
  componentsToRemove = [];
30124
- scene = this.sceneViewer.scene; // Collect only component objects
30137
+ scene = this.sceneViewer.scene; // Collect component, segment, and gateway objects
30125
30138
  scene.traverse(function (child) {
30126
30139
  if (child === scene) return;
30127
- 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');
30128
30141
  var isDirectChild = child.parent === scene;
30129
30142
  if (isComponent && isDirectChild) {
30130
30143
  componentsToRemove.push(child);
@@ -31117,123 +31130,52 @@ var SceneOperationsManager = /*#__PURE__*/function () {
31117
31130
  }
31118
31131
 
31119
31132
  /**
31120
- * Helper function to compute world bounding boxes
31121
- * For components: uses filtered bbox (excludes io-device and connector subtrees)
31122
- * For io-devices: computes separate bounding boxes and injects them as children
31123
- */
31124
- }, {
31125
- key: "computeWorldBoundingBoxes",
31126
- value: function computeWorldBoundingBoxes(data) {
31127
- var component = this.sceneViewer;
31128
- component.scene.traverse(function (object) {
31129
- if (object.isMesh) {
31130
- // Find the corresponding JSON object
31131
- var jsonObject = null;
31132
- var _findJsonObject = function findJsonObject(children) {
31133
- var _iterator = _createForOfIteratorHelper(children),
31134
- _step;
31135
- try {
31136
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
31137
- var _object$userData, _child$userData4;
31138
- var child = _step.value;
31139
- // Enhanced matching logic with hardcoded UUID priority
31140
-
31141
- // Strategy 1: Direct hardcoded UUID match (HIGHEST PRIORITY)
31142
- 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)) {
31143
- return child;
31144
- }
31145
-
31146
- // Recursively search children
31147
- if (child.children) {
31148
- var found = _findJsonObject(child.children);
31149
- if (found) return found;
31150
- }
31151
- }
31152
- } catch (err) {
31153
- _iterator.e(err);
31154
- } finally {
31155
- _iterator.f();
31156
- }
31157
- return null;
31158
- };
31159
- jsonObject = _findJsonObject(data.scene.children);
31160
- if (jsonObject) {
31161
- // Store in JSON userData for pathfinder (skip for gateways - they're just routing points)
31162
- if (!jsonObject.userData) jsonObject.userData = {};
31163
- if (jsonObject.userData.objectType === 'component') {
31164
- // For components: compute filtered bounding box (excludes io-device and connector subtrees)
31165
- var filteredBBox = computeFilteredBoundingBox(object, ['io-device', 'connector']);
31166
- jsonObject.userData.worldBoundingBox = {
31167
- min: filteredBBox.min.toArray(),
31168
- max: filteredBBox.max.toArray()
31169
- };
31170
- console.log("Added filtered world bounding box for component:", jsonObject.userData.worldBoundingBox);
31171
-
31172
- // Compute and inject separate io-device bounding boxes as children
31173
- var ioDeviceBBoxes = computeIODeviceBoundingBoxes(object);
31174
- if (ioDeviceBBoxes.length > 0) {
31175
- if (!jsonObject.children) jsonObject.children = [];
31176
- ioDeviceBBoxes.forEach(function (deviceBBox) {
31177
- var existingIndex = jsonObject.children.findIndex(function (c) {
31178
- return c.uuid === deviceBBox.uuid;
31179
- });
31180
- if (existingIndex >= 0) {
31181
- // Update existing entry
31182
- if (!jsonObject.children[existingIndex].userData) jsonObject.children[existingIndex].userData = {};
31183
- jsonObject.children[existingIndex].userData.objectType = 'io-device';
31184
- jsonObject.children[existingIndex].userData.worldBoundingBox = deviceBBox.worldBoundingBox;
31185
- } else {
31186
- // Create new entry
31187
- jsonObject.children.push({
31188
- uuid: deviceBBox.uuid,
31189
- userData: _objectSpread2(_objectSpread2({}, deviceBBox.userData), {}, {
31190
- worldBoundingBox: deviceBBox.worldBoundingBox
31191
- }),
31192
- children: []
31193
- });
31194
- }
31195
- });
31196
- console.log("\uD83D\uDCE6 Injected ".concat(ioDeviceBBoxes.length, " io-device bbox(es) for component ").concat(jsonObject.uuid));
31197
- }
31198
- } else if (jsonObject.userData.objectType !== 'gateway') {
31199
- // For non-component, non-gateway objects: standard bounding box
31200
- var boundingBox = new THREE__namespace.Box3().setFromObject(object);
31201
- jsonObject.userData.worldBoundingBox = {
31202
- min: boundingBox.min.toArray(),
31203
- max: boundingBox.max.toArray()
31204
- };
31205
- console.log("Added world bounding box:", jsonObject.userData.worldBoundingBox);
31206
- }
31207
-
31208
- // For gateways and connectors, ensure userData.position exists in scene data
31209
- // This is REQUIRED for pathfinder compatibility
31210
- if (jsonObject.userData.objectType === 'gateway' || jsonObject.userData.objectType === 'connector') {
31211
- // Use the object's world position (from Three.js mesh)
31212
- var worldPos = new THREE__namespace.Vector3();
31213
- object.getWorldPosition(worldPos);
31214
-
31215
- // ALWAYS update userData.position with world position
31216
- // This is critical for manual segment connectors which start with local positions
31217
- jsonObject.userData.position = [worldPos.x, worldPos.y, worldPos.z];
31218
- 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), "]"));
31219
-
31220
- // For gateways, ensure isDeclared flag is in scene data
31221
- if (jsonObject.userData.objectType === 'gateway') {
31222
- if (jsonObject.userData.isDeclared === undefined) {
31223
- jsonObject.userData.isDeclared = true;
31224
- }
31225
- }
31226
-
31227
- // For manual segment connectors, ensure isDeclared is set in scene data
31228
- if (jsonObject.userData.objectType === 'segment-connector' && jsonObject.userData.isDeclared === undefined) {
31229
- jsonObject.userData.isDeclared = true;
31230
- console.log("\u2705 Set isDeclared=true for manual segment connector in scene data: ".concat(jsonObject.uuid));
31231
- }
31232
-
31233
- // Also sync the mesh's userData.position (belt and suspenders approach)
31234
- 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);
31235
31177
  }
31236
- }
31178
+ });
31237
31179
  }
31238
31180
  });
31239
31181
  }
@@ -31398,10 +31340,10 @@ var SceneOperationsManager = /*#__PURE__*/function () {
31398
31340
  var componentsProcessed = 0;
31399
31341
  var connectorsInjected = 0;
31400
31342
  data.scene.children.forEach(function (child) {
31401
- var _child$userData5, _child$userData6, _child$userData7;
31402
- 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);
31403
31345
  // Only process components with libraryId
31404
- 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) {
31405
31347
  var libraryId = child.userData.libraryId;
31406
31348
  var dictEntry = componentDictionary[libraryId];
31407
31349
 
@@ -31558,23 +31500,25 @@ var SceneOperationsManager = /*#__PURE__*/function () {
31558
31500
  geometries = this.createSceneGeometries(data, componentDictionary); // Create basic objects and track GLB replacements
31559
31501
  libraryObjectsToReplace = [];
31560
31502
  data.scene.children.forEach(function (child, index) {
31561
- var _child$userData8, _child$userData9;
31503
+ var _child$userData7, _child$userData8;
31562
31504
  var createdObject = _this4.createSceneObject(child, geometries, materials, componentDictionary);
31563
31505
  _this4.sceneViewer.scene.add(createdObject);
31564
31506
 
31565
31507
  // Track objects that need GLB model replacement
31566
- 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]) {
31567
- 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;
31568
31510
  libraryObjectsToReplace.push({
31569
31511
  basicObject: createdObject,
31570
31512
  jsonData: child,
31571
- 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]
31572
31514
  });
31573
31515
  }
31574
31516
  });
31575
31517
 
31576
- // Compute bounding boxes for pathfinding
31577
- 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);
31578
31522
  this._saveOriginalWorldMatrices(this.sceneViewer.scene);
31579
31523
  return _context6.a(2, {
31580
31524
  componentDictionary: componentDictionary,
@@ -31720,8 +31664,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
31720
31664
  var instanceBehaviors = [];
31721
31665
  if (Array.isArray(data === null || data === void 0 || (_data$scene3 = data.scene) === null || _data$scene3 === void 0 ? void 0 : _data$scene3.children)) {
31722
31666
  data.scene.children.forEach(function (child) {
31723
- var _child$userData1, _compData$defaultBeha;
31724
- 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;
31725
31669
  if (!libraryId) return;
31726
31670
  var instanceUuid = child.uuid;
31727
31671
  // Skip instances whose defaults were already resolved by Step A
@@ -31917,8 +31861,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
31917
31861
  key: "_saveOriginalWorldMatrices",
31918
31862
  value: function _saveOriginalWorldMatrices(scene) {
31919
31863
  scene.traverse(function (object) {
31920
- var _object$userData2;
31921
- 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) {
31922
31866
  var originalMatrix = new THREE__namespace.Matrix4();
31923
31867
  originalMatrix.copy(object.matrixWorld);
31924
31868
  }
@@ -32122,8 +32066,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
32122
32066
  // Process children (connectors, etc.) if they exist
32123
32067
  if (componentModel.children && componentModel.children.length > 0) {
32124
32068
  componentModel.children.forEach(function (child) {
32125
- var _child$userData10, _child$userData11;
32126
- 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);
32127
32071
  if (childType === 'connector') {
32128
32072
  var _child$geometry;
32129
32073
  var childBoundingBox = new THREE__namespace.Box3().setFromObject(child);
@@ -32208,8 +32152,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
32208
32152
  if (segment.children && segment.children.length > 0) {
32209
32153
  var childrenToRemove = _toConsumableArray(segment.children);
32210
32154
  childrenToRemove.forEach(function (child) {
32211
- var _child$userData12;
32212
- 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) {
32213
32157
  console.log("\uD83D\uDDD1\uFE0F Removing elbow child from segment before manualization: ".concat(child.uuid));
32214
32158
  segment.remove(child);
32215
32159
  if (child.geometry) child.geometry.dispose();
@@ -35479,6 +35423,18 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
35479
35423
  // Map of viewport instances by viewType or custom key
35480
35424
  _this2.viewports = new Map();
35481
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
+
35482
35438
  // Event listener reference for cleanup
35483
35439
  _this2._objectTransformedListener = null;
35484
35440
  console.log('🔲 Viewport2DManager initialized (multi-instance support)');
@@ -35500,7 +35456,6 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
35500
35456
 
35501
35457
  // Listen for object transformations to refresh all viewports
35502
35458
  this._objectTransformedListener = function (eventData) {
35503
- console.log('🔲 Viewport2DManager detected object transformation, refreshing all viewports');
35504
35459
  _this3.refresh(); // Refresh all viewports
35505
35460
  };
35506
35461
 
@@ -35566,6 +35521,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
35566
35521
  case 3:
35567
35522
  // Create new viewport instance
35568
35523
  viewport = new Viewport2DInstance(this.sceneViewer, this.Konva, viewType, container);
35524
+ viewport._instanceKey = key;
35569
35525
  this.viewports.set(key, viewport);
35570
35526
 
35571
35527
  // Initialize the stage for this viewport
@@ -35743,9 +35699,9 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
35743
35699
  viewport.stage.width(width);
35744
35700
  viewport.stage.height(height);
35745
35701
 
35746
- // Redraw content
35702
+ // Redraw grid immediately; schedule debounced component render
35747
35703
  this.drawGrid(viewport);
35748
- this.renderComponents(viewport);
35704
+ this.refresh(viewport._instanceKey);
35749
35705
  }
35750
35706
  }
35751
35707
 
@@ -35922,32 +35878,19 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
35922
35878
  }, {
35923
35879
  key: "renderComponent",
35924
35880
  value: function renderComponent(viewport, component, centerX, centerY, scale) {
35925
- var _component$position$x, _component$position, _component$position$y, _component$position2, _component$position$z, _component$position3, _component$rotation$x, _component$rotation, _component$rotation$y, _component$rotation2, _component$rotation$z, _component$rotation3;
35926
- // Get component position and rotation
35927
- var pos3D = {
35928
- x: (_component$position$x = (_component$position = component.position) === null || _component$position === void 0 ? void 0 : _component$position.x) !== null && _component$position$x !== void 0 ? _component$position$x : 0,
35929
- y: (_component$position$y = (_component$position2 = component.position) === null || _component$position2 === void 0 ? void 0 : _component$position2.y) !== null && _component$position$y !== void 0 ? _component$position$y : 0,
35930
- z: (_component$position$z = (_component$position3 = component.position) === null || _component$position3 === void 0 ? void 0 : _component$position3.z) !== null && _component$position$z !== void 0 ? _component$position$z : 0
35931
- };
35932
- var rot3D = {
35933
- x: (_component$rotation$x = (_component$rotation = component.rotation) === null || _component$rotation === void 0 ? void 0 : _component$rotation.x) !== null && _component$rotation$x !== void 0 ? _component$rotation$x : 0,
35934
- y: (_component$rotation$y = (_component$rotation2 = component.rotation) === null || _component$rotation2 === void 0 ? void 0 : _component$rotation2.y) !== null && _component$rotation$y !== void 0 ? _component$rotation$y : 0,
35935
- z: (_component$rotation$z = (_component$rotation3 = component.rotation) === null || _component$rotation3 === void 0 ? void 0 : _component$rotation3.z) !== null && _component$rotation$z !== void 0 ? _component$rotation$z : 0
35936
- };
35937
-
35938
- // Get bounding box dimensions
35881
+ // Get world-space bounding box dimensions and center
35939
35882
  var _this$getComponentDim = this.getComponentDimensions(component),
35940
35883
  worldWidth = _this$getComponentDim.worldWidth,
35941
35884
  worldDepth = _this$getComponentDim.worldDepth,
35942
- worldHeight = _this$getComponentDim.worldHeight;
35885
+ worldHeight = _this$getComponentDim.worldHeight,
35886
+ bboxCenter = _this$getComponentDim.bboxCenter;
35943
35887
 
35944
- // Project 3D coordinates to 2D based on view type
35945
- var _this$project3DTo2D = this.project3DTo2D(viewport, pos3D, rot3D, worldWidth, worldDepth, worldHeight),
35888
+ // Project 3D bbox center to 2D based on view type
35889
+ var _this$project3DTo2D = this.project3DTo2D(viewport, bboxCenter, worldWidth, worldDepth, worldHeight),
35946
35890
  posX = _this$project3DTo2D.posX,
35947
35891
  posY = _this$project3DTo2D.posY,
35948
35892
  rectWidth = _this$project3DTo2D.rectWidth,
35949
35893
  rectHeight = _this$project3DTo2D.rectHeight;
35950
- _this$project3DTo2D.rotationDegrees;
35951
35894
  var screenX = centerX + posX * scale;
35952
35895
  var screenY = centerY - posY * scale; // Flip Y for screen coords
35953
35896
 
@@ -35991,105 +35934,156 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
35991
35934
  });
35992
35935
 
35993
35936
  // Add mouse event handlers
35994
- this.addComponentInteractions(viewport, rect, componentGroup, component, worldWidth, worldDepth, worldHeight);
35937
+ this.addComponentInteractions(viewport, rect, componentGroup, component, worldWidth, worldDepth, worldHeight, bboxCenter);
35995
35938
  componentGroup.add(rect);
35996
35939
  componentGroup.add(label);
35997
35940
  viewport.componentLayer.add(componentGroup);
35998
35941
  }
35999
35942
 
36000
35943
  /**
36001
- * Get component dimensions from various sources
35944
+ * Compute worldBoundingBox for a live Three.js Object3D using the same
35945
+ * vertex-accurate approach as computeFilteredBoundingBox (explicitly calls
35946
+ * geometry.computeBoundingBox() on each mesh before measuring).
35947
+ * @param {THREE.Object3D} object
35948
+ * @returns {{min: number[], max: number[]}}
35949
+ */
35950
+ }, {
35951
+ key: "_getOrComputeWorldBoundingBox",
35952
+ value: function _getOrComputeWorldBoundingBox(object) {
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
+ }
35974
+ var box = computeFilteredBoundingBox(object, []);
35975
+ var result;
35976
+ if (box.isEmpty()) {
35977
+ // Object has no geometry; fall back to a point at world position
35978
+ var wp = new THREE__namespace.Vector3();
35979
+ object.getWorldPosition(wp);
35980
+ result = {
35981
+ min: [wp.x, wp.y, wp.z],
35982
+ max: [wp.x, wp.y, wp.z]
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
+ };
35989
+ }
35990
+ this._bboxCache.set(object.uuid, result);
35991
+ return result;
35992
+ }
35993
+
35994
+ /**
35995
+ * Get component dimensions and world-space center from worldBoundingBox
36002
35996
  */
36003
35997
  }, {
36004
35998
  key: "getComponentDimensions",
36005
35999
  value: function getComponentDimensions(component) {
36006
- var _component$userData, _component$userData2, _component$geometry;
36007
- var worldWidth = 1,
36008
- worldHeight = 1,
36009
- worldDepth = 1;
36010
-
36011
- // Try adaptedBoundingBox first
36012
- if ((_component$userData = component.userData) !== null && _component$userData !== void 0 && _component$userData.adaptedBoundingBox) {
36013
- var bbox = component.userData.adaptedBoundingBox;
36014
- if (bbox.max && bbox.min) {
36015
- worldWidth = Math.abs(bbox.max.x - bbox.min.x);
36016
- worldDepth = Math.abs(bbox.max.y - bbox.min.y);
36017
- worldHeight = Math.abs(bbox.max.z - bbox.min.z);
36018
- }
36019
- }
36020
- // Fallback to dimensions from userData
36021
- else if ((_component$userData2 = component.userData) !== null && _component$userData2 !== void 0 && _component$userData2.dimensions) {
36022
- var dims = component.userData.dimensions;
36023
- worldWidth = Math.abs(dims.x);
36024
- worldDepth = Math.abs(dims.y);
36025
- worldHeight = Math.abs(dims.z);
36026
- }
36027
- // Last resort: geometry bounding box
36028
- else if ((_component$geometry = component.geometry) !== null && _component$geometry !== void 0 && _component$geometry.boundingBox) {
36029
- var _bbox = component.geometry.boundingBox;
36030
- worldWidth = Math.abs(_bbox.max.x - _bbox.min.x);
36031
- worldDepth = Math.abs(_bbox.max.y - _bbox.min.y);
36032
- worldHeight = Math.abs(_bbox.max.z - _bbox.min.z);
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);
36006
+ if (wbb !== null && wbb !== void 0 && wbb.min && wbb !== null && wbb !== void 0 && wbb.max) {
36007
+ var _wbb$min = _slicedToArray(wbb.min, 3),
36008
+ minX = _wbb$min[0],
36009
+ minY = _wbb$min[1],
36010
+ minZ = _wbb$min[2];
36011
+ var _wbb$max = _slicedToArray(wbb.max, 3),
36012
+ maxX = _wbb$max[0],
36013
+ maxY = _wbb$max[1],
36014
+ maxZ = _wbb$max[2];
36015
+ var cx = (minX + maxX) / 2;
36016
+ var cy = (minY + maxY) / 2;
36017
+ var cz = (minZ + maxZ) / 2;
36018
+ // Guard against Infinity/NaN from empty or degenerate boxes
36019
+ if (isFinite(cx) && isFinite(cy) && isFinite(cz)) {
36020
+ return {
36021
+ worldWidth: Math.max(maxX - minX, 0.01),
36022
+ worldDepth: Math.max(maxY - minY, 0.01),
36023
+ worldHeight: Math.max(maxZ - minZ, 0.01),
36024
+ bboxCenter: {
36025
+ x: cx,
36026
+ y: cy,
36027
+ z: cz
36028
+ }
36029
+ };
36030
+ }
36033
36031
  }
36032
+ // Fallback: world position of the object, unit dimensions
36033
+ var wp = new THREE__namespace.Vector3();
36034
+ (_component$getWorldPo = component.getWorldPosition) === null || _component$getWorldPo === void 0 || _component$getWorldPo.call(component, wp);
36034
36035
  return {
36035
- worldWidth: worldWidth,
36036
- worldDepth: worldDepth,
36037
- worldHeight: worldHeight
36036
+ worldWidth: 1,
36037
+ worldDepth: 1,
36038
+ worldHeight: 1,
36039
+ bboxCenter: {
36040
+ x: wp.x,
36041
+ y: wp.y,
36042
+ z: wp.z
36043
+ }
36038
36044
  };
36039
36045
  }
36040
36046
 
36041
36047
  /**
36042
- * Project 3D coordinates to 2D based on view type
36048
+ * Project world-space bbox center to 2D based on view type.
36049
+ * worldBoundingBox is an AABB so rotation is already encoded in the extents —
36050
+ * no separate rotation correction is needed.
36043
36051
  * @param {Viewport2DInstance} viewport - The viewport instance
36052
+ * @param {Object} bboxCenter - World-space center {x, y, z}
36053
+ * @param {number} worldWidth - X extent (max[0] - min[0])
36054
+ * @param {number} worldDepth - Y extent (max[1] - min[1])
36055
+ * @param {number} worldHeight - Z extent (max[2] - min[2])
36044
36056
  */
36045
36057
  }, {
36046
36058
  key: "project3DTo2D",
36047
- value: function project3DTo2D(viewport, pos3D, rot3D, worldWidth, worldDepth, worldHeight) {
36048
- var posX, posY, rectWidth, rectHeight;
36049
- var rotationAngle = rot3D.z;
36050
- var rotationDegrees = rotationAngle * 180 / Math.PI;
36059
+ value: function project3DTo2D(viewport, bboxCenter, worldWidth, worldDepth, worldHeight) {
36051
36060
  var scale = viewport.PIXELS_PER_UNIT;
36061
+ var posX, posY, rectWidth, rectHeight;
36052
36062
  switch (viewport.viewType) {
36053
36063
  case 'top':
36054
- // Top view: Looking down Z-axis, X-Y plane
36055
- posX = pos3D.x;
36056
- posY = pos3D.y;
36057
-
36058
- // Swap width and depth when rotated 90° or 270°
36059
- if (Math.abs(rotationDegrees) === 90 || Math.abs(rotationDegrees) === 270) {
36060
- rectWidth = worldDepth * scale;
36061
- rectHeight = worldWidth * scale;
36062
- } else {
36063
- rectWidth = worldWidth * scale;
36064
- rectHeight = worldDepth * scale;
36065
- }
36064
+ // Looking down Z-axis X/Y plane
36065
+ posX = bboxCenter.x;
36066
+ posY = bboxCenter.y;
36067
+ rectWidth = worldWidth * scale;
36068
+ rectHeight = worldDepth * scale;
36066
36069
  break;
36067
36070
  case 'front':
36068
- // Front view: Looking along Y-axis, X-Z plane
36069
- posX = pos3D.x;
36070
- posY = pos3D.z + worldHeight / 2; // Offset Z by half height
36071
-
36072
- if (Math.abs(rotationDegrees) === 90 || Math.abs(rotationDegrees) === 270) {
36073
- rectWidth = worldDepth * scale;
36074
- } else {
36075
- rectWidth = worldWidth * scale;
36076
- }
36071
+ // Looking along Y-axis X/Z plane
36072
+ posX = bboxCenter.x;
36073
+ posY = bboxCenter.z;
36074
+ rectWidth = worldWidth * scale;
36077
36075
  rectHeight = worldHeight * scale;
36078
36076
  break;
36079
36077
  case 'side':
36080
- // Side view: Looking along X-axis, Y-Z plane (flipped)
36081
- posX = -pos3D.y; // Flipped
36082
- posY = pos3D.z + worldHeight / 2;
36083
- if (Math.abs(rotationDegrees) === 90 || Math.abs(rotationDegrees) === 270) {
36084
- rectWidth = worldWidth * scale;
36085
- } else {
36086
- rectWidth = worldDepth * scale;
36087
- }
36078
+ // Looking along X-axis Y/Z plane (Y negated for left-right orientation)
36079
+ posX = -bboxCenter.y;
36080
+ posY = bboxCenter.z;
36081
+ rectWidth = worldDepth * scale;
36088
36082
  rectHeight = worldHeight * scale;
36089
36083
  break;
36090
36084
  default:
36091
- posX = pos3D.x;
36092
- posY = pos3D.y;
36085
+ posX = bboxCenter.x;
36086
+ posY = bboxCenter.y;
36093
36087
  rectWidth = worldWidth * scale;
36094
36088
  rectHeight = worldDepth * scale;
36095
36089
  }
@@ -36097,8 +36091,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36097
36091
  posX: posX,
36098
36092
  posY: posY,
36099
36093
  rectWidth: rectWidth,
36100
- rectHeight: rectHeight,
36101
- rotationDegrees: rotationDegrees
36094
+ rectHeight: rectHeight
36102
36095
  };
36103
36096
  }
36104
36097
 
@@ -36108,7 +36101,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36108
36101
  */
36109
36102
  }, {
36110
36103
  key: "addComponentInteractions",
36111
- value: function addComponentInteractions(viewport, rect, componentGroup, component, worldWidth, worldDepth, worldHeight) {
36104
+ value: function addComponentInteractions(viewport, rect, componentGroup, component, worldWidth, worldDepth, worldHeight, bboxCenter) {
36112
36105
  var _this6 = this;
36113
36106
  if (!this.Konva) return;
36114
36107
  var colors = this.generateComponentColor(component.id);
@@ -36120,7 +36113,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36120
36113
  rect.stroke('#007bff');
36121
36114
  rect.strokeWidth(3);
36122
36115
  viewport.stage.container().style.cursor = 'grab';
36123
- viewport.componentLayer.draw();
36116
+ viewport.componentLayer.batchDraw();
36124
36117
  }
36125
36118
  });
36126
36119
  rect.on('mouseleave', function () {
@@ -36129,7 +36122,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36129
36122
  rect.stroke(colors.stroke);
36130
36123
  rect.strokeWidth(2);
36131
36124
  viewport.stage.container().style.cursor = 'default';
36132
- viewport.componentLayer.draw();
36125
+ viewport.componentLayer.batchDraw();
36133
36126
  }
36134
36127
  });
36135
36128
 
@@ -36173,7 +36166,7 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36173
36166
  // Snap to grid
36174
36167
  var snappedPos = _this6.snapScreenToGrid(viewport, currentPos.x, currentPos.y, scale, worldOriginX, worldOriginY);
36175
36168
  componentGroup.position(snappedPos);
36176
- viewport.componentLayer.draw();
36169
+ viewport.componentLayer.batchDraw();
36177
36170
  });
36178
36171
 
36179
36172
  // DRAG END
@@ -36191,9 +36184,9 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36191
36184
  // Convert screen to world coordinates
36192
36185
  var worldCoords = _this6.screenToWorldCoords(viewport, finalPos.x, finalPos.y, scale, worldOriginX, worldOriginY);
36193
36186
 
36194
- // Calculate new position
36187
+ // Calculate new position: delta from old bbox center to new bbox center
36195
36188
  var currentPos = component.position;
36196
- var newPosition = _this6.worldCoordsToObjectPosition(viewport, worldCoords, currentPos, worldWidth, worldDepth, worldHeight);
36189
+ var newPosition = _this6.worldCoordsToObjectPosition(viewport, worldCoords, currentPos, bboxCenter);
36197
36190
 
36198
36191
  // Apply translation via centralPlant API
36199
36192
  var deltaX = newPosition.x - currentPos.x;
@@ -36296,37 +36289,45 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36296
36289
  }
36297
36290
 
36298
36291
  /**
36299
- * Convert world coordinates to 3D object position
36292
+ * Convert dragged 2D world coordinates to a new 3D object position.
36293
+ * coord1/coord2 represent where the bbox CENTER should be in the view's 2D plane;
36294
+ * we compute the delta from the old bbox center and apply it to the pivot position.
36300
36295
  * @param {Viewport2DInstance} viewport - The viewport instance
36296
+ * @param {{coord1, coord2}} worldCoords - Projected world coordinates from screen
36297
+ * @param {{x,y,z}} currentPosition - Current Three.js object position (pivot)
36298
+ * @param {{x,y,z}} bboxCenter - Old world-space bbox center
36301
36299
  */
36302
36300
  }, {
36303
36301
  key: "worldCoordsToObjectPosition",
36304
- value: function worldCoordsToObjectPosition(viewport, worldCoords, currentPosition, worldWidth, worldDepth, worldHeight) {
36302
+ value: function worldCoordsToObjectPosition(viewport, worldCoords, currentPosition, bboxCenter) {
36305
36303
  var coord1 = worldCoords.coord1,
36306
36304
  coord2 = worldCoords.coord2;
36307
36305
  switch (viewport.viewType) {
36308
36306
  case 'top':
36307
+ // coord1=X, coord2=Y in world space
36309
36308
  return {
36310
- x: coord1,
36311
- y: coord2,
36309
+ x: currentPosition.x + (coord1 - bboxCenter.x),
36310
+ y: currentPosition.y + (coord2 - bboxCenter.y),
36312
36311
  z: currentPosition.z
36313
36312
  };
36314
36313
  case 'front':
36314
+ // coord1=X, coord2=Z in world space
36315
36315
  return {
36316
- x: coord1,
36316
+ x: currentPosition.x + (coord1 - bboxCenter.x),
36317
36317
  y: currentPosition.y,
36318
- z: coord2 - worldHeight / 2
36318
+ z: currentPosition.z + (coord2 - bboxCenter.z)
36319
36319
  };
36320
36320
  case 'side':
36321
+ // coord1=-Y (negated), coord2=Z in world space
36321
36322
  return {
36322
36323
  x: currentPosition.x,
36323
- y: -coord1,
36324
- z: coord2 - worldHeight / 2
36324
+ y: currentPosition.y + (-coord1 - bboxCenter.y),
36325
+ z: currentPosition.z + (coord2 - bboxCenter.z)
36325
36326
  };
36326
36327
  default:
36327
36328
  return {
36328
- x: coord1,
36329
- y: coord2,
36329
+ x: currentPosition.x + (coord1 - bboxCenter.x),
36330
+ y: currentPosition.y + (coord2 - bboxCenter.y),
36330
36331
  z: currentPosition.z
36331
36332
  };
36332
36333
  }
@@ -36338,17 +36339,20 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36338
36339
  }, {
36339
36340
  key: "getSceneComponents",
36340
36341
  value: function getSceneComponents() {
36342
+ if (this._componentListCache) return this._componentListCache;
36341
36343
  if (!this.sceneViewer || !this.sceneViewer.scene) {
36342
36344
  return [];
36343
36345
  }
36344
36346
  var components = [];
36345
36347
  this.sceneViewer.scene.traverse(function (object) {
36346
- var _object$userData, _object$userData2;
36347
- var objectType = ((_object$userData = object.userData) === null || _object$userData === void 0 ? void 0 : _object$userData.objectType) || ((_object$userData2 = object.userData) === null || _object$userData2 === void 0 ? void 0 : _object$userData2.objectType);
36348
- if (object.userData && objectType === 'component') {
36348
+ var _object$userData3, _object$userData4;
36349
+ // Only match the ROOT component object must have both objectType:'component'
36350
+ // AND libraryId (inner GLB mesh nodes don't have 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) {
36349
36352
  components.push(object);
36350
36353
  }
36351
36354
  });
36355
+ this._componentListCache = components;
36352
36356
  return components;
36353
36357
  }
36354
36358
 
@@ -36454,35 +36458,45 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36454
36458
  }
36455
36459
 
36456
36460
  /**
36457
- * 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.
36458
36464
  * @param {string} key - Optional viewport key. If not provided, refreshes all viewports
36459
36465
  */
36460
36466
  }, {
36461
36467
  key: "refresh",
36462
36468
  value: function refresh() {
36469
+ var _this7 = this;
36463
36470
  var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
36464
- if (key) {
36465
- var viewport = this.viewports.get(key);
36466
- if (viewport && viewport.isReady) {
36467
- this.renderComponents(viewport);
36468
- }
36469
- } else {
36470
- // Refresh all viewports
36471
- var _iterator = _createForOfIteratorHelper(this.viewports.values()),
36472
- _step;
36473
- try {
36474
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
36475
- var _viewport = _step.value;
36476
- if (_viewport.isReady) {
36477
- 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
+ }
36478
36492
  }
36493
+ } catch (err) {
36494
+ _iterator.e(err);
36495
+ } finally {
36496
+ _iterator.f();
36479
36497
  }
36480
- } catch (err) {
36481
- _iterator.e(err);
36482
- } finally {
36483
- _iterator.f();
36484
36498
  }
36485
- }
36499
+ });
36486
36500
  }
36487
36501
 
36488
36502
  /**
@@ -37802,7 +37816,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
37802
37816
  * Initialize the CentralPlant manager
37803
37817
  *
37804
37818
  * @constructor
37805
- * @version 0.3.4
37819
+ * @version 0.3.6
37806
37820
  * @updated 2025-10-22
37807
37821
  *
37808
37822
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.