@2112-lab/central-plant 0.1.78 → 0.1.79

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.
@@ -24701,6 +24701,8 @@ class RGBELoader extends THREE.DataTextureLoader {
24701
24701
 
24702
24702
  }
24703
24703
 
24704
+ /** Maximum anisotropy to apply (caps GPU memory for mip storage) */
24705
+ var MAX_ANISOTROPY = 4;
24704
24706
  var TEXTURE_SETS = {
24705
24707
  // Light metallic texture using the gravel_embedded_concrete with metallic properties
24706
24708
  light_metal: {
@@ -24837,10 +24839,29 @@ function _loadTextureSet() {
24837
24839
  return Promise.all(promises);
24838
24840
  case 2:
24839
24841
  loadedTextures = _context2.v;
24840
- textureMap = Object.fromEntries(loadedTextures); // Apply texture settings
24841
- Object.values(textureMap).forEach(function (texture) {
24842
+ textureMap = Object.fromEntries(loadedTextures); // Apply texture settings with memory-conscious defaults
24843
+ Object.entries(textureMap).forEach(function (_ref4) {
24844
+ var _ref5 = _slicedToArray(_ref4, 2),
24845
+ type = _ref5[0],
24846
+ texture = _ref5[1];
24842
24847
  texture.wrapS = texture.wrapT = THREE__namespace.RepeatWrapping;
24843
24848
  texture.repeat.set(textureSet.repeat.x, textureSet.repeat.y);
24849
+
24850
+ // Correct colour-space: diffuse is sRGB, everything else is linear data
24851
+ if (type === 'diffuse') {
24852
+ texture.colorSpace = THREE__namespace.SRGBColorSpace;
24853
+ } else {
24854
+ texture.colorSpace = THREE__namespace.LinearSRGBColorSpace;
24855
+ }
24856
+
24857
+ // Roughness maps don't benefit much from mipmaps — skip them to save GPU memory
24858
+ if (type === 'roughness') {
24859
+ texture.generateMipmaps = false;
24860
+ texture.minFilter = THREE__namespace.LinearFilter;
24861
+ }
24862
+
24863
+ // Cap anisotropy to balance quality vs memory
24864
+ texture.anisotropy = MAX_ANISOTROPY;
24844
24865
  });
24845
24866
  console.log("Texture set ".concat(setName, " loaded successfully"));
24846
24867
  return _context2.a(2, {
@@ -24905,173 +24926,88 @@ var EnvironmentManager = /*#__PURE__*/function () {
24905
24926
  this.sceneViewer = sceneViewer;
24906
24927
  }
24907
24928
 
24929
+ // ──────────────────────────────────────────────
24930
+ // Skybox / environment map
24931
+ // ──────────────────────────────────────────────
24932
+
24908
24933
  /**
24909
- * Create skybox with HDR environment mapping
24934
+ * Create a lightweight procedural sky (default).
24935
+ *
24936
+ * Builds a gradient-sky sphere + hemisphere light, then bakes a small
24937
+ * PMREM environment map for PBR reflections. Total GPU cost is ~1-2 MB
24938
+ * compared to ~50-70 MB for the previous HDR pipeline.
24910
24939
  */
24911
24940
  return _createClass(EnvironmentManager, [{
24912
24941
  key: "createSkybox",
24913
- value: (function () {
24942
+ value: function () {
24914
24943
  var _createSkybox = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
24915
- var component, pmremGenerator, loaders, applyEnvironmentMap, _loop, _ret, _i, _loaders, ambientLight, skyScene;
24916
- return _regenerator().w(function (_context3) {
24917
- while (1) switch (_context3.n) {
24944
+ var component, _t;
24945
+ return _regenerator().w(function (_context) {
24946
+ while (1) switch (_context.n) {
24918
24947
  case 0:
24919
24948
  component = this.sceneViewer;
24920
- pmremGenerator = new THREE__namespace.PMREMGenerator(component.renderer);
24921
- pmremGenerator.compileEquirectangularShader();
24922
- loaders = [{
24923
- type: 'hdr',
24924
- loader: new RGBELoader(),
24925
- paths: ['/skyboxes/kloofendal_48d_partly_cloudy_puresky_2k.hdr', '/skyboxes/kloofendal_48d_partly_cloudy_puresky_1k.hdr']
24926
- }, {
24927
- type: 'jpeg',
24928
- loader: component.textureLoader,
24929
- paths: ['/skyboxes/sky_fallback.jpg']
24930
- }];
24931
- applyEnvironmentMap = function applyEnvironmentMap(envTexture, type) {
24932
- try {
24933
- var _component$scene$envi, _component$scene$back;
24934
- envTexture.mapping = THREE__namespace.EquirectangularReflectionMapping;
24935
- var processedEnvMap = pmremGenerator.fromEquirectangular(envTexture).texture;
24936
- if ((_component$scene$envi = component.scene.environment) !== null && _component$scene$envi !== void 0 && _component$scene$envi.dispose) component.scene.environment.dispose();
24937
- if ((_component$scene$back = component.scene.background) !== null && _component$scene$back !== void 0 && _component$scene$back.dispose) component.scene.background.dispose();
24938
- component.scene.environment = processedEnvMap;
24939
- component.scene.background = processedEnvMap;
24940
- envTexture.dispose();
24941
- console.log("\u2705 ".concat(type.toUpperCase(), " environment map applied successfully"));
24942
- return true;
24943
- } catch (error) {
24944
- console.warn("\u274C Failed to apply ".concat(type, " environment map:"), error);
24945
- return false;
24946
- }
24947
- };
24948
- _loop = /*#__PURE__*/_regenerator().m(function _loop() {
24949
- var _loaders$_i, type, loader, paths, _iterator, _step, _loop2, _ret2, _t2;
24950
- return _regenerator().w(function (_context2) {
24951
- while (1) switch (_context2.n) {
24952
- case 0:
24953
- _loaders$_i = _loaders[_i], type = _loaders$_i.type, loader = _loaders$_i.loader, paths = _loaders$_i.paths;
24954
- _iterator = _createForOfIteratorHelper(paths);
24955
- _context2.p = 1;
24956
- _loop2 = /*#__PURE__*/_regenerator().m(function _loop2() {
24957
- var path, texture, _t;
24958
- return _regenerator().w(function (_context) {
24959
- while (1) switch (_context.n) {
24960
- case 0:
24961
- path = _step.value;
24962
- _context.p = 1;
24963
- console.log("\uD83D\uDD04 Attempting to load ".concat(type.toUpperCase(), ": ").concat(path));
24964
- _context.n = 2;
24965
- return new Promise(function (resolve, reject) {
24966
- var timeout = setTimeout(function () {
24967
- return reject(new Error('Timeout'));
24968
- }, 10000);
24969
- loader.load(path, function (tex) {
24970
- clearTimeout(timeout);
24971
- resolve(tex);
24972
- }, undefined, function (err) {
24973
- clearTimeout(timeout);
24974
- reject(err);
24975
- });
24976
- });
24977
- case 2:
24978
- texture = _context.v;
24979
- if (!applyEnvironmentMap(texture, type)) {
24980
- _context.n = 3;
24981
- break;
24982
- }
24983
- pmremGenerator.dispose();
24984
- return _context.a(2, {
24985
- v: {
24986
- v: void 0
24987
- }
24988
- });
24989
- case 3:
24990
- _context.n = 5;
24991
- break;
24992
- case 4:
24993
- _context.p = 4;
24994
- _t = _context.v;
24995
- console.warn("\u26A0\uFE0F Failed to load ".concat(path, ":"), _t.message);
24996
- case 5:
24997
- return _context.a(2);
24998
- }
24999
- }, _loop2, null, [[1, 4]]);
25000
- });
25001
- _iterator.s();
25002
- case 2:
25003
- if ((_step = _iterator.n()).done) {
25004
- _context2.n = 5;
25005
- break;
25006
- }
25007
- return _context2.d(_regeneratorValues(_loop2()), 3);
25008
- case 3:
25009
- _ret2 = _context2.v;
25010
- if (!_ret2) {
25011
- _context2.n = 4;
25012
- break;
25013
- }
25014
- return _context2.a(2, _ret2.v);
25015
- case 4:
25016
- _context2.n = 2;
25017
- break;
25018
- case 5:
25019
- _context2.n = 7;
25020
- break;
25021
- case 6:
25022
- _context2.p = 6;
25023
- _t2 = _context2.v;
25024
- _iterator.e(_t2);
25025
- case 7:
25026
- _context2.p = 7;
25027
- _iterator.f();
25028
- return _context2.f(7);
25029
- case 8:
25030
- return _context2.a(2);
25031
- }
25032
- }, _loop, null, [[1, 6, 7, 8]]);
25033
- });
25034
- _i = 0, _loaders = loaders;
25035
- case 1:
25036
- if (!(_i < _loaders.length)) {
25037
- _context3.n = 4;
25038
- break;
25039
- }
25040
- return _context3.d(_regeneratorValues(_loop()), 2);
24949
+ _context.p = 1;
24950
+ _context.n = 2;
24951
+ return this._applyProceduralSky(component);
25041
24952
  case 2:
25042
- _ret = _context3.v;
25043
- if (!_ret) {
25044
- _context3.n = 3;
25045
- break;
25046
- }
25047
- return _context3.a(2, _ret.v);
25048
- case 3:
25049
- _i++;
25050
- _context3.n = 1;
24953
+ console.log('✅ Procedural sky environment applied');
24954
+ _context.n = 4;
25051
24955
  break;
24956
+ case 3:
24957
+ _context.p = 3;
24958
+ _t = _context.v;
24959
+ console.warn('⚠️ Procedural sky failed, applying plain color fallback:', _t);
24960
+ this.setColorBackground();
25052
24961
  case 4:
25053
- console.log('🎨 Using procedural sky fallback');
25054
- ambientLight = new THREE__namespace.AmbientLight(0xffffff, 0.6);
25055
- component.scene.add(ambientLight);
25056
- skyScene = new THREE__namespace.Scene(); // Create hemisphere light with Z-up orientation - sky color up, ground color down
25057
- skyScene.add(new THREE__namespace.HemisphereLight(0x87CEEB, 0x444477, 1));
25058
- component.scene.environment = pmremGenerator.fromScene(skyScene).texture;
25059
- component.scene.background = new THREE__namespace.Color(0x87CEEB);
25060
- pmremGenerator.dispose();
25061
- case 5:
25062
- return _context3.a(2);
24962
+ return _context.a(2);
25063
24963
  }
25064
- }, _callee, this);
24964
+ }, _callee, this, [[1, 3]]);
25065
24965
  }));
25066
24966
  function createSkybox() {
25067
24967
  return _createSkybox.apply(this, arguments);
25068
24968
  }
25069
24969
  return createSkybox;
25070
24970
  }()
24971
+ /**
24972
+ * Build a procedural sky environment.
24973
+ *
24974
+ * Uses two separate, lightweight resources:
24975
+ * - **Background**: Canvas-rendered gradient texture mapped as equirectangular
24976
+ * (512 × 256 px ≈ 0.5 MB GPU).
24977
+ * - **Environment**: PMREM baked from a HemisphereLight scene for PBR IBL
24978
+ * reflections (~1 MB GPU).
24979
+ *
24980
+ * Custom ShaderMaterials are intentionally avoided inside the PMREM pipeline
24981
+ * because `PMREMGenerator.fromScene()` uses its own cube-face cameras that
24982
+ * can produce black output with non-standard shaders.
24983
+ * @private
24984
+ */
24985
+ }, {
24986
+ key: "_applyProceduralSky",
24987
+ value: function _applyProceduralSky(component) {
24988
+ var _component$scene$envi, _component$scene$back;
24989
+ // Dispose of existing textures
24990
+ if ((_component$scene$envi = component.scene.environment) !== null && _component$scene$envi !== void 0 && _component$scene$envi.dispose) component.scene.environment.dispose();
24991
+ if ((_component$scene$back = component.scene.background) !== null && _component$scene$back !== void 0 && _component$scene$back.dispose) component.scene.background.dispose();
24992
+
24993
+ // ── 1. Solid-colour background ──────────────────────────────────────────
24994
+ // A plain THREE.Color is coordinate-system agnostic (no equirectangular
24995
+ // mapping that assumes Y-up), cheap (zero GPU textures), and looks clean.
24996
+ component.scene.background = new THREE__namespace.Color(0xbcddeb); // light sky blue
24997
+
24998
+ // ── 2. PMREM env-map from HemisphereLight for PBR IBL reflections ───────
24999
+ // Intensity must be high enough to light MeshPhysicalMaterial surfaces
25000
+ // that the directional lights don't reach evenly (wall backs, crevices).
25001
+ var pmremGenerator = new THREE__namespace.PMREMGenerator(component.renderer);
25002
+ var lightScene = new THREE__namespace.Scene();
25003
+ lightScene.add(new THREE__namespace.HemisphereLight(0xbcddeb, 0x444477, 3.0));
25004
+ component.scene.environment = pmremGenerator.fromScene(lightScene).texture;
25005
+ pmremGenerator.dispose();
25006
+ }
25007
+
25071
25008
  /**
25072
25009
  * Setup scene lighting
25073
25010
  */
25074
- )
25075
25011
  }, {
25076
25012
  key: "setupLighting",
25077
25013
  value: function setupLighting() {
@@ -25097,15 +25033,27 @@ var EnvironmentManager = /*#__PURE__*/function () {
25097
25033
  component.scene.add(sunLight);
25098
25034
  // component.scene.add(sunLight.target)
25099
25035
 
25100
- // // Ambient hemispheric light
25101
- // const ambientLight = new THREE.HemisphereLight(0xffffff, 0x444466, 0.35)
25102
- // component.scene.add(ambientLight)
25036
+ // Flat ambient light – ensures no surface is ever fully black regardless
25037
+ // of orientation. Low intensity so it doesn't wash out lit surfaces.
25038
+ var flatAmbient = new THREE__namespace.AmbientLight(0xffffff, 1.0);
25039
+ component.scene.add(flatAmbient);
25040
+
25041
+ // Hemispheric ambient – adds sky/ground colour tint on surfaces the
25042
+ // directional lights miss (essential without an HDR environment map).
25043
+ var ambientLight = new THREE__namespace.HemisphereLight(0xffffff, 0x444466, 1.0);
25044
+ component.scene.add(ambientLight);
25103
25045
 
25104
25046
  // Fill light (secondary directional light) - adjusted for Z-up coordinate system with flipped Y
25105
- var fillLight = new THREE__namespace.DirectionalLight(0xffffff, 2.0);
25047
+ var fillLight = new THREE__namespace.DirectionalLight(0xffffff, 3.0);
25106
25048
  fillLight.position.set(10, -10, 10); // X=-10, Y=-10 (flipped), Z=10 (up in Z-up system)
25107
25049
  component.scene.add(fillLight);
25108
25050
 
25051
+ // Back-fill light – illuminates rear-facing walls and equipment backs
25052
+ // that the sun and primary fill can't reach.
25053
+ var backFillLight = new THREE__namespace.DirectionalLight(0xddeeff, 3.0);
25054
+ backFillLight.position.set(-10, 10, 8);
25055
+ component.scene.add(backFillLight);
25056
+
25109
25057
  // // Rim light (for edge definition)
25110
25058
  // const rimLight = new THREE.DirectionalLight(0xffffff, 0.9)
25111
25059
  // rimLight.position.set(0, 0, -20)
@@ -25134,9 +25082,9 @@ var EnvironmentManager = /*#__PURE__*/function () {
25134
25082
  key: "addTexturedGround",
25135
25083
  value: (function () {
25136
25084
  var _addTexturedGround = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
25137
- var component, groundSize, groundGeometry, groundMaterial, ground, texturedMaterial, _t3;
25138
- return _regenerator().w(function (_context4) {
25139
- while (1) switch (_context4.n) {
25085
+ var component, groundSize, groundGeometry, groundMaterial, ground, texturedMaterial, _t2;
25086
+ return _regenerator().w(function (_context2) {
25087
+ while (1) switch (_context2.n) {
25140
25088
  case 0:
25141
25089
  component = this.sceneViewer;
25142
25090
  console.debug('Starting addTexturedGround...');
@@ -25155,23 +25103,23 @@ var EnvironmentManager = /*#__PURE__*/function () {
25155
25103
  ground.uuid = 'GROUND'; // Set UUID to match JSON data structure
25156
25104
  ground.userData.isBaseGround = true;
25157
25105
  component.scene.add(ground);
25158
- _context4.p = 1;
25106
+ _context2.p = 1;
25159
25107
  console.debug('Loading concrete texture set...');
25160
- _context4.n = 2;
25108
+ _context2.n = 2;
25161
25109
  return loadTextureSetAndCreateMaterial('gravel_embedded_concrete', component.textureLoader);
25162
25110
  case 2:
25163
- texturedMaterial = _context4.v;
25111
+ texturedMaterial = _context2.v;
25164
25112
  ground.material = texturedMaterial;
25165
25113
  ground.material.needsUpdate = true;
25166
25114
  console.log('✅ Ground material updated with textures');
25167
- _context4.n = 4;
25115
+ _context2.n = 4;
25168
25116
  break;
25169
25117
  case 3:
25170
- _context4.p = 3;
25171
- _t3 = _context4.v;
25172
- console.warn('Error loading ground textures:', _t3);
25118
+ _context2.p = 3;
25119
+ _t2 = _context2.v;
25120
+ console.warn('Error loading ground textures:', _t2);
25173
25121
  case 4:
25174
- return _context4.a(2);
25122
+ return _context2.a(2);
25175
25123
  }
25176
25124
  }, _callee2, this, [[1, 3]]);
25177
25125
  }));
@@ -25188,9 +25136,9 @@ var EnvironmentManager = /*#__PURE__*/function () {
25188
25136
  key: "addBrickWalls",
25189
25137
  value: (function () {
25190
25138
  var _addBrickWalls = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3() {
25191
- var component, wallHeight, wallThickness, groundSize, halfGroundSize, brickMaterial, createWalls, texturedBrickMaterial, _t4;
25192
- return _regenerator().w(function (_context5) {
25193
- while (1) switch (_context5.n) {
25139
+ var component, wallHeight, wallThickness, groundSize, halfGroundSize, brickMaterial, createWalls, texturedBrickMaterial, _t3;
25140
+ return _regenerator().w(function (_context3) {
25141
+ while (1) switch (_context3.n) {
25194
25142
  case 0:
25195
25143
  component = this.sceneViewer;
25196
25144
  console.debug('Starting addBrickWalls...');
@@ -25292,12 +25240,12 @@ var EnvironmentManager = /*#__PURE__*/function () {
25292
25240
  };
25293
25241
  createWalls(brickMaterial);
25294
25242
  console.debug('Basic brick walls created');
25295
- _context5.p = 1;
25243
+ _context3.p = 1;
25296
25244
  console.debug('Loading brick texture set...');
25297
- _context5.n = 2;
25245
+ _context3.n = 2;
25298
25246
  return loadTextureSetAndCreateMaterial('brick', component.textureLoader);
25299
25247
  case 2:
25300
- texturedBrickMaterial = _context5.v;
25248
+ texturedBrickMaterial = _context3.v;
25301
25249
  component.scene.traverse(function (object) {
25302
25250
  if (object.isMesh && object.userData.isBrickWall) {
25303
25251
  object.material = texturedBrickMaterial;
@@ -25305,14 +25253,14 @@ var EnvironmentManager = /*#__PURE__*/function () {
25305
25253
  }
25306
25254
  });
25307
25255
  console.log('✅ Brick walls updated with textures');
25308
- _context5.n = 4;
25256
+ _context3.n = 4;
25309
25257
  break;
25310
25258
  case 3:
25311
- _context5.p = 3;
25312
- _t4 = _context5.v;
25313
- console.warn('Error loading brick textures:', _t4);
25259
+ _context3.p = 3;
25260
+ _t3 = _context3.v;
25261
+ console.warn('Error loading brick textures:', _t3);
25314
25262
  case 4:
25315
- return _context5.a(2);
25263
+ return _context3.a(2);
25316
25264
  }
25317
25265
  }, _callee3, this, [[1, 3]]);
25318
25266
  }));
@@ -25361,23 +25309,23 @@ var EnvironmentManager = /*#__PURE__*/function () {
25361
25309
  key: "initializeEnvironment",
25362
25310
  value: (function () {
25363
25311
  var _initializeEnvironment = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4() {
25364
- return _regenerator().w(function (_context6) {
25365
- while (1) switch (_context6.n) {
25312
+ return _regenerator().w(function (_context4) {
25313
+ while (1) switch (_context4.n) {
25366
25314
  case 0:
25367
- _context6.n = 1;
25315
+ _context4.n = 1;
25368
25316
  return this.createSkybox();
25369
25317
  case 1:
25370
25318
  this.setupLighting();
25371
- _context6.n = 2;
25319
+ _context4.n = 2;
25372
25320
  return this.addTexturedGround();
25373
25321
  case 2:
25374
- _context6.n = 3;
25322
+ _context4.n = 3;
25375
25323
  return this.addBrickWalls();
25376
25324
  case 3:
25377
25325
  this.addHorizonFog();
25378
25326
  console.log('Environment initialization completed');
25379
25327
  case 4:
25380
- return _context6.a(2);
25328
+ return _context4.a(2);
25381
25329
  }
25382
25330
  }, _callee4, this);
25383
25331
  }));
@@ -25418,60 +25366,50 @@ var EnvironmentManager = /*#__PURE__*/function () {
25418
25366
 
25419
25367
  /**
25420
25368
  * Set the skybox type dynamically
25421
- * @param {string} skyboxType - 'HDR', 'Fallback', or 'Color'
25369
+ * @param {'Procedural'|'HDR'|'Color'} skyboxType
25422
25370
  */
25423
25371
  }, {
25424
25372
  key: "setSkyboxType",
25425
25373
  value: (function () {
25426
25374
  var _setSkyboxType = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5(skyboxType) {
25427
- var component, _component$scene$envi3, _component$scene$back3, _pmremGenerator, _t5, _t6;
25428
- return _regenerator().w(function (_context7) {
25429
- while (1) switch (_context7.n) {
25375
+ var component, _component$scene$envi3, _component$scene$back3, _t4, _t5;
25376
+ return _regenerator().w(function (_context5) {
25377
+ while (1) switch (_context5.n) {
25430
25378
  case 0:
25431
25379
  component = this.sceneViewer;
25432
- _context7.p = 1;
25380
+ _context5.p = 1;
25433
25381
  console.log("\uD83C\uDF0C Changing skybox type to: ".concat(skyboxType));
25434
25382
 
25435
25383
  // Dispose of current environment textures
25436
- if ((_component$scene$envi3 = component.scene.environment) !== null && _component$scene$envi3 !== void 0 && _component$scene$envi3.dispose) {
25437
- component.scene.environment.dispose();
25438
- }
25439
- if ((_component$scene$back3 = component.scene.background) !== null && _component$scene$back3 !== void 0 && _component$scene$back3.dispose) {
25440
- component.scene.background.dispose();
25441
- }
25442
- _pmremGenerator = new THREE__namespace.PMREMGenerator(component.renderer);
25443
- _pmremGenerator.compileEquirectangularShader();
25444
- _t5 = skyboxType;
25445
- _context7.n = _t5 === 'HDR' ? 2 : _t5 === 'Fallback' ? 4 : _t5 === 'Color' ? 6 : 7;
25384
+ if ((_component$scene$envi3 = component.scene.environment) !== null && _component$scene$envi3 !== void 0 && _component$scene$envi3.dispose) component.scene.environment.dispose();
25385
+ if ((_component$scene$back3 = component.scene.background) !== null && _component$scene$back3 !== void 0 && _component$scene$back3.dispose) component.scene.background.dispose();
25386
+ _t4 = skyboxType;
25387
+ _context5.n = _t4 === 'Procedural' ? 2 : _t4 === 'HDR' ? 3 : _t4 === 'Color' ? 5 : 6;
25446
25388
  break;
25447
25389
  case 2:
25448
- _context7.n = 3;
25449
- return this.loadHDRSkybox(_pmremGenerator);
25390
+ this._applyProceduralSky(component);
25391
+ return _context5.a(3, 7);
25450
25392
  case 3:
25451
- return _context7.a(3, 8);
25393
+ _context5.n = 4;
25394
+ return this._loadHDRSkybox(component);
25452
25395
  case 4:
25453
- _context7.n = 5;
25454
- return this.loadFallbackSkybox(_pmremGenerator);
25396
+ return _context5.a(3, 7);
25455
25397
  case 5:
25456
- return _context7.a(3, 8);
25398
+ this._setColorBackground(component);
25399
+ return _context5.a(3, 7);
25457
25400
  case 6:
25458
- this.setColorBackground();
25459
- return _context7.a(3, 8);
25401
+ console.warn("Unknown skybox type: ".concat(skyboxType, ", using Procedural"));
25402
+ this._applyProceduralSky(component);
25460
25403
  case 7:
25461
- console.warn("Unknown skybox type: ".concat(skyboxType, ", falling back to HDR"));
25462
- _context7.n = 8;
25463
- return this.loadHDRSkybox(_pmremGenerator);
25404
+ console.log("\u2705 Skybox type changed to: ".concat(skyboxType));
25405
+ return _context5.a(2, true);
25464
25406
  case 8:
25465
- _pmremGenerator.dispose();
25466
- console.log("Skybox type changed to: ".concat(skyboxType));
25467
- return _context7.a(2, true);
25468
- case 9:
25469
- _context7.p = 9;
25470
- _t6 = _context7.v;
25471
- console.error('❌ Error setting skybox type:', _t6);
25472
- return _context7.a(2, false);
25407
+ _context5.p = 8;
25408
+ _t5 = _context5.v;
25409
+ console.error('❌ Error setting skybox type:', _t5);
25410
+ return _context5.a(2, false);
25473
25411
  }
25474
- }, _callee5, this, [[1, 9]]);
25412
+ }, _callee5, this, [[1, 8]]);
25475
25413
  }));
25476
25414
  function setSkyboxType(_x) {
25477
25415
  return _setSkyboxType.apply(this, arguments);
@@ -25479,29 +25417,34 @@ var EnvironmentManager = /*#__PURE__*/function () {
25479
25417
  return setSkyboxType;
25480
25418
  }()
25481
25419
  /**
25482
- * Load HDR skybox specifically
25420
+ * Load HDR skybox on demand (high quality, high memory).
25421
+ * Tries 1k first to save memory; falls back to color on failure.
25422
+ * @private
25483
25423
  */
25484
25424
  )
25485
25425
  }, {
25486
- key: "loadHDRSkybox",
25426
+ key: "_loadHDRSkybox",
25487
25427
  value: (function () {
25488
- var _loadHDRSkybox = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee6(pmremGenerator) {
25489
- var component, rgbeLoader, hdrPaths, _loop3, _ret3, _i2, _hdrPaths;
25490
- return _regenerator().w(function (_context9) {
25491
- while (1) switch (_context9.n) {
25428
+ var _loadHDRSkybox2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee6(component) {
25429
+ var rgbeLoader, pmremGenerator, hdrPaths, _loop, _ret, _i, _hdrPaths;
25430
+ return _regenerator().w(function (_context7) {
25431
+ while (1) switch (_context7.n) {
25492
25432
  case 0:
25493
- component = this.sceneViewer;
25494
25433
  rgbeLoader = new RGBELoader();
25495
- hdrPaths = ['/skyboxes/kloofendal_48d_partly_cloudy_puresky_2k.hdr', '/skyboxes/kloofendal_48d_partly_cloudy_puresky_1k.hdr'];
25496
- _loop3 = /*#__PURE__*/_regenerator().m(function _loop3() {
25497
- var path, texture, processedEnvMap, _t7;
25498
- return _regenerator().w(function (_context8) {
25499
- while (1) switch (_context8.n) {
25434
+ pmremGenerator = new THREE__namespace.PMREMGenerator(component.renderer);
25435
+ pmremGenerator.compileEquirectangularShader();
25436
+
25437
+ // Prefer the 1k variant (~20 MB GPU) over 2k (~50 MB GPU)
25438
+ hdrPaths = ['/skyboxes/kloofendal_48d_partly_cloudy_puresky_1k.hdr', '/skyboxes/kloofendal_48d_partly_cloudy_puresky_2k.hdr'];
25439
+ _loop = /*#__PURE__*/_regenerator().m(function _loop() {
25440
+ var path, texture, processedEnvMap, _t6;
25441
+ return _regenerator().w(function (_context6) {
25442
+ while (1) switch (_context6.n) {
25500
25443
  case 0:
25501
- path = _hdrPaths[_i2];
25502
- _context8.p = 1;
25444
+ path = _hdrPaths[_i];
25445
+ _context6.p = 1;
25503
25446
  console.log("\uD83D\uDD04 Loading HDR: ".concat(path));
25504
- _context8.n = 2;
25447
+ _context6.n = 2;
25505
25448
  return new Promise(function (resolve, reject) {
25506
25449
  var timeout = setTimeout(function () {
25507
25450
  return reject(new Error('Timeout'));
@@ -25515,123 +25458,79 @@ var EnvironmentManager = /*#__PURE__*/function () {
25515
25458
  });
25516
25459
  });
25517
25460
  case 2:
25518
- texture = _context8.v;
25461
+ texture = _context6.v;
25519
25462
  texture.mapping = THREE__namespace.EquirectangularReflectionMapping;
25520
25463
  processedEnvMap = pmremGenerator.fromEquirectangular(texture).texture;
25521
25464
  component.scene.environment = processedEnvMap;
25522
25465
  component.scene.background = processedEnvMap;
25523
25466
  texture.dispose();
25467
+ pmremGenerator.dispose();
25524
25468
  console.log('✅ HDR skybox loaded successfully');
25525
- return _context8.a(2, {
25469
+ return _context6.a(2, {
25526
25470
  v: void 0
25527
25471
  });
25528
25472
  case 3:
25529
- _context8.p = 3;
25530
- _t7 = _context8.v;
25531
- console.warn("\u26A0\uFE0F Failed to load HDR skybox ".concat(path, ":"), _t7.message);
25473
+ _context6.p = 3;
25474
+ _t6 = _context6.v;
25475
+ console.warn("\u26A0\uFE0F Failed to load HDR skybox ".concat(path, ":"), _t6.message);
25532
25476
  case 4:
25533
- return _context8.a(2);
25477
+ return _context6.a(2);
25534
25478
  }
25535
- }, _loop3, null, [[1, 3]]);
25479
+ }, _loop, null, [[1, 3]]);
25536
25480
  });
25537
- _i2 = 0, _hdrPaths = hdrPaths;
25481
+ _i = 0, _hdrPaths = hdrPaths;
25538
25482
  case 1:
25539
- if (!(_i2 < _hdrPaths.length)) {
25540
- _context9.n = 4;
25483
+ if (!(_i < _hdrPaths.length)) {
25484
+ _context7.n = 4;
25541
25485
  break;
25542
25486
  }
25543
- return _context9.d(_regeneratorValues(_loop3()), 2);
25487
+ return _context7.d(_regeneratorValues(_loop()), 2);
25544
25488
  case 2:
25545
- _ret3 = _context9.v;
25546
- if (!_ret3) {
25547
- _context9.n = 3;
25489
+ _ret = _context7.v;
25490
+ if (!_ret) {
25491
+ _context7.n = 3;
25548
25492
  break;
25549
25493
  }
25550
- return _context9.a(2, _ret3.v);
25494
+ return _context7.a(2, _ret.v);
25551
25495
  case 3:
25552
- _i2++;
25553
- _context9.n = 1;
25496
+ _i++;
25497
+ _context7.n = 1;
25554
25498
  break;
25555
25499
  case 4:
25500
+ pmremGenerator.dispose();
25556
25501
  throw new Error('Failed to load any HDR skybox');
25557
25502
  case 5:
25558
- return _context9.a(2);
25503
+ return _context7.a(2);
25559
25504
  }
25560
- }, _callee6, this);
25505
+ }, _callee6);
25561
25506
  }));
25562
- function loadHDRSkybox(_x2) {
25563
- return _loadHDRSkybox.apply(this, arguments);
25507
+ function _loadHDRSkybox(_x2) {
25508
+ return _loadHDRSkybox2.apply(this, arguments);
25564
25509
  }
25565
- return loadHDRSkybox;
25510
+ return _loadHDRSkybox;
25566
25511
  }()
25567
25512
  /**
25568
- * Load fallback JPEG skybox
25513
+ * Set solid color background with basic ambient lighting.
25514
+ * @private
25569
25515
  */
25570
25516
  )
25571
25517
  }, {
25572
- key: "loadFallbackSkybox",
25573
- value: (function () {
25574
- var _loadFallbackSkybox = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7(pmremGenerator) {
25575
- var component, texture, processedEnvMap, _t8;
25576
- return _regenerator().w(function (_context0) {
25577
- while (1) switch (_context0.n) {
25578
- case 0:
25579
- component = this.sceneViewer;
25580
- _context0.p = 1;
25581
- console.log('🔄 Loading fallback skybox');
25582
- _context0.n = 2;
25583
- return new Promise(function (resolve, reject) {
25584
- var timeout = setTimeout(function () {
25585
- return reject(new Error('Timeout'));
25586
- }, 10000);
25587
- component.textureLoader.load('/skyboxes/sky_fallback.jpg', function (tex) {
25588
- clearTimeout(timeout);
25589
- resolve(tex);
25590
- }, undefined, function (err) {
25591
- clearTimeout(timeout);
25592
- reject(err);
25593
- });
25594
- });
25595
- case 2:
25596
- texture = _context0.v;
25597
- texture.mapping = THREE__namespace.EquirectangularReflectionMapping;
25598
- processedEnvMap = pmremGenerator.fromEquirectangular(texture).texture;
25599
- component.scene.environment = processedEnvMap;
25600
- component.scene.background = processedEnvMap;
25601
- texture.dispose();
25602
- console.log('✅ Fallback skybox loaded successfully');
25603
- _context0.n = 4;
25604
- break;
25605
- case 3:
25606
- _context0.p = 3;
25607
- _t8 = _context0.v;
25608
- console.warn('⚠️ Failed to load fallback skybox:', _t8.message);
25609
- throw _t8;
25610
- case 4:
25611
- return _context0.a(2);
25612
- }
25613
- }, _callee7, this, [[1, 3]]);
25614
- }));
25615
- function loadFallbackSkybox(_x3) {
25616
- return _loadFallbackSkybox.apply(this, arguments);
25617
- }
25618
- return loadFallbackSkybox;
25619
- }()
25518
+ key: "_setColorBackground",
25519
+ value: function _setColorBackground(component) {
25520
+ component.scene.environment = null;
25521
+ component.scene.background = new THREE__namespace.Color(0xffffff);
25522
+ var ambientLight = new THREE__namespace.AmbientLight(0xffffff, 0.6);
25523
+ component.scene.add(ambientLight);
25524
+ console.log('✅ Color background set successfully');
25525
+ }
25526
+
25620
25527
  /**
25621
- * Set solid color background
25528
+ * Public convenience alias for setColorBackground (backwards compat)
25622
25529
  */
25623
- )
25624
25530
  }, {
25625
25531
  key: "setColorBackground",
25626
25532
  value: function setColorBackground() {
25627
- var component = this.sceneViewer;
25628
- component.scene.environment = null;
25629
- component.scene.background = new THREE__namespace.Color(0xffffff); // White background
25630
-
25631
- // Add basic ambient lighting since we don't have environment lighting
25632
- var ambientLight = new THREE__namespace.AmbientLight(0xffffff, 0.6);
25633
- component.scene.add(ambientLight);
25634
- console.log('✅ Color background set successfully');
25533
+ this._setColorBackground(this.sceneViewer);
25635
25534
  }
25636
25535
  }]);
25637
25536
  }();
@@ -31024,6 +30923,11 @@ var AnimationManager = /*#__PURE__*/function (_BaseDisposable) {
31024
30923
  sceneViewer.tooltipsManager.render();
31025
30924
  }
31026
30925
 
30926
+ // Update component tooltip screen position
30927
+ if (sceneViewer.componentTooltipManager) {
30928
+ sceneViewer.componentTooltipManager.update();
30929
+ }
30930
+
31027
30931
  // End performance monitoring frame
31028
30932
  if (sceneViewer.performanceMonitorManager) {
31029
30933
  sceneViewer.performanceMonitorManager.endFrame();
@@ -33080,6 +32984,317 @@ var SceneTooltipsManager = /*#__PURE__*/function (_BaseDisposable) {
33080
32984
  }]);
33081
32985
  }(BaseDisposable);
33082
32986
 
32987
+ // ---------------------------------------------------------------------------
32988
+ // Inline styles (injected once into the document head)
32989
+ // ---------------------------------------------------------------------------
32990
+ var TOOLTIP_STYLES = "\n.cp-tooltip {\n position: absolute;\n pointer-events: auto;\n z-index: 10000;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n font-size: 13px;\n line-height: 1.4;\n user-select: none;\n min-width: 200px;\n max-width: 280px;\n transform: translate(-50%, -100%);\n margin-top: -12px;\n transition: opacity 0.15s ease;\n}\n\n.cp-tooltip__card {\n background: rgba(28, 32, 40, 0.95);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 10px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);\n overflow: hidden;\n}\n\n/* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__header {\n padding: 10px 14px;\n font-weight: 600;\n font-size: 14px;\n color: #e8ecf1;\n background: rgba(255, 255, 255, 0.04);\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 I/O Devices section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__io-section {\n position: relative;\n}\n\n.cp-tooltip__io-trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 14px;\n color: #a4adba;\n cursor: pointer;\n transition: background 0.12s ease, color 0.12s ease;\n}\n\n.cp-tooltip__io-trigger:hover {\n background: rgba(255, 255, 255, 0.06);\n color: #e8ecf1;\n}\n\n.cp-tooltip__io-trigger-label {\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.6px;\n font-weight: 500;\n}\n\n.cp-tooltip__io-arrow {\n font-size: 10px;\n transition: transform 0.2s ease;\n}\n\n.cp-tooltip__io-section.expanded .cp-tooltip__io-arrow {\n transform: rotate(90deg);\n}\n\n/* \u2500\u2500 Device list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__device-list {\n max-height: 0;\n overflow: hidden;\n transition: max-height 0.25s ease;\n}\n\n.cp-tooltip__io-section.expanded .cp-tooltip__device-list {\n max-height: 300px;\n}\n\n.cp-tooltip__device-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 14px 6px 20px;\n color: #c1c8d1;\n font-size: 12px;\n border-top: 1px solid rgba(255, 255, 255, 0.04);\n transition: background 0.1s ease;\n}\n\n.cp-tooltip__device-item:hover {\n background: rgba(255, 255, 255, 0.04);\n}\n\n.cp-tooltip__device-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: #4fc3f7;\n flex-shrink: 0;\n}\n\n.cp-tooltip__device-name {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 Empty state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__no-devices {\n padding: 8px 14px;\n color: #6b7280;\n font-size: 12px;\n font-style: italic;\n}\n\n/* \u2500\u2500 Caret arrow pointing down \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__caret {\n width: 0;\n height: 0;\n border-left: 7px solid transparent;\n border-right: 7px solid transparent;\n border-top: 7px solid rgba(28, 32, 40, 0.95);\n margin: 0 auto;\n position: relative;\n top: -1px;\n}\n";
32991
+ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
32992
+ /**
32993
+ * @param {Object} sceneViewer - The sceneViewer instance
32994
+ */
32995
+ function ComponentTooltipManager(sceneViewer) {
32996
+ var _this;
32997
+ _classCallCheck(this, ComponentTooltipManager);
32998
+ _this = _callSuper(this, ComponentTooltipManager);
32999
+ _this.sceneViewer = sceneViewer;
33000
+ _this.selectedObject = null;
33001
+ _this.tooltipEl = null;
33002
+ _this._styleInjected = false;
33003
+ _this._ioExpanded = false;
33004
+ _this._injectStyles();
33005
+ return _this;
33006
+ }
33007
+
33008
+ // -----------------------------------------------------------------------
33009
+ // Lifecycle
33010
+ // -----------------------------------------------------------------------
33011
+
33012
+ /**
33013
+ * Called automatically by BaseDisposable.dispose()
33014
+ * @override
33015
+ */
33016
+ _inherits(ComponentTooltipManager, _BaseDisposable);
33017
+ return _createClass(ComponentTooltipManager, [{
33018
+ key: "_doDispose",
33019
+ value: function _doDispose() {
33020
+ this.hide();
33021
+ this._removeStyleTag();
33022
+ this.nullifyProperties('sceneViewer', 'selectedObject', 'tooltipEl');
33023
+ }
33024
+
33025
+ // -----------------------------------------------------------------------
33026
+ // Public API
33027
+ // -----------------------------------------------------------------------
33028
+
33029
+ /**
33030
+ * Should be called when an object is selected or deselected.
33031
+ * @param {THREE.Object3D|null} object
33032
+ */
33033
+ }, {
33034
+ key: "onSelectionChanged",
33035
+ value: function onSelectionChanged(object) {
33036
+ var _object$userData;
33037
+ if (!object) {
33038
+ this.hide();
33039
+ return;
33040
+ }
33041
+ var objectType = (_object$userData = object.userData) === null || _object$userData === void 0 ? void 0 : _object$userData.objectType;
33042
+ if (objectType !== 'component') {
33043
+ this.hide();
33044
+ return;
33045
+ }
33046
+ this.selectedObject = object;
33047
+ this._ioExpanded = false;
33048
+ this._buildTooltip(object);
33049
+ }
33050
+
33051
+ /**
33052
+ * Update tooltip screen position. Call once per frame (from the animation loop).
33053
+ */
33054
+ }, {
33055
+ key: "update",
33056
+ value: function update() {
33057
+ if (!this.tooltipEl || !this.selectedObject) return;
33058
+ this._positionTooltip();
33059
+ }
33060
+
33061
+ /**
33062
+ * Remove the tooltip from the DOM.
33063
+ */
33064
+ }, {
33065
+ key: "hide",
33066
+ value: function hide() {
33067
+ if (this.tooltipEl) {
33068
+ this.tooltipEl.remove();
33069
+ this.tooltipEl = null;
33070
+ }
33071
+ this.selectedObject = null;
33072
+ this._ioExpanded = false;
33073
+ }
33074
+
33075
+ // -----------------------------------------------------------------------
33076
+ // Internal — build
33077
+ // -----------------------------------------------------------------------
33078
+
33079
+ /** Inject the stylesheet once into the document head */
33080
+ }, {
33081
+ key: "_injectStyles",
33082
+ value: function _injectStyles() {
33083
+ if (this._styleInjected) return;
33084
+ this._styleTag = document.createElement('style');
33085
+ this._styleTag.setAttribute('data-cp-tooltip', '');
33086
+ this._styleTag.textContent = TOOLTIP_STYLES;
33087
+ document.head.appendChild(this._styleTag);
33088
+ this._styleInjected = true;
33089
+ this.registerDOMElement(this._styleTag);
33090
+ }
33091
+
33092
+ /** Remove the injected stylesheet */
33093
+ }, {
33094
+ key: "_removeStyleTag",
33095
+ value: function _removeStyleTag() {
33096
+ if (this._styleTag && this._styleTag.parentNode) {
33097
+ this._styleTag.parentNode.removeChild(this._styleTag);
33098
+ }
33099
+ this._styleTag = null;
33100
+ this._styleInjected = false;
33101
+ }
33102
+
33103
+ /**
33104
+ * Gather I/O device children from a component's Three.js hierarchy.
33105
+ * @param {THREE.Object3D} object
33106
+ * @returns {{ label: string, deviceId: string }[]}
33107
+ */
33108
+ }, {
33109
+ key: "_getIODevices",
33110
+ value: function _getIODevices(object) {
33111
+ var devices = [];
33112
+ object.traverse(function (child) {
33113
+ var _child$userData;
33114
+ if (((_child$userData = child.userData) === null || _child$userData === void 0 ? void 0 : _child$userData.objectType) === 'io-device') {
33115
+ devices.push({
33116
+ label: child.userData.attachmentLabel || child.name || child.userData.deviceId || 'Unknown Device',
33117
+ deviceId: child.userData.deviceId || ''
33118
+ });
33119
+ }
33120
+ });
33121
+ return devices;
33122
+ }
33123
+
33124
+ /**
33125
+ * Build and show the tooltip for the given object.
33126
+ * @param {THREE.Object3D} object
33127
+ */
33128
+ }, {
33129
+ key: "_buildTooltip",
33130
+ value: function _buildTooltip(object) {
33131
+ var _this2 = this;
33132
+ // Remove any existing tooltip first
33133
+ this.hide();
33134
+ // Re-assign selected object since hide() clears it
33135
+ this.selectedObject = object;
33136
+ var container = this._getContainer();
33137
+ if (!container) return;
33138
+
33139
+ // Ensure the container supports absolute positioning
33140
+ var containerStyle = window.getComputedStyle(container);
33141
+ if (containerStyle.position === 'static') {
33142
+ container.style.position = 'relative';
33143
+ }
33144
+
33145
+ // Gather data — extract the friendly name, stripping the " (component-id)" suffix
33146
+ var rawName = object.name || '';
33147
+ var componentName = rawName.replace(/\s*\([^)]*\)\s*$/, '') || 'Unnamed Component';
33148
+ var devices = this._getIODevices(object);
33149
+ var isSmart = devices.length > 0;
33150
+
33151
+ // Root element
33152
+ var root = document.createElement('div');
33153
+ root.className = 'cp-tooltip';
33154
+
33155
+ // Card
33156
+ var card = document.createElement('div');
33157
+ card.className = 'cp-tooltip__card';
33158
+
33159
+ // Header
33160
+ var header = document.createElement('div');
33161
+ header.className = 'cp-tooltip__header';
33162
+ header.textContent = componentName;
33163
+ card.appendChild(header);
33164
+
33165
+ // I/O Devices section
33166
+ if (isSmart) {
33167
+ var ioSection = document.createElement('div');
33168
+ ioSection.className = 'cp-tooltip__io-section';
33169
+
33170
+ // Trigger row
33171
+ var trigger = document.createElement('div');
33172
+ trigger.className = 'cp-tooltip__io-trigger';
33173
+ var label = document.createElement('span');
33174
+ label.className = 'cp-tooltip__io-trigger-label';
33175
+ label.textContent = "I/O Devices (".concat(devices.length, ")");
33176
+ var arrow = document.createElement('span');
33177
+ arrow.className = 'cp-tooltip__io-arrow';
33178
+ arrow.textContent = '▶';
33179
+ trigger.appendChild(label);
33180
+ trigger.appendChild(arrow);
33181
+
33182
+ // Device list
33183
+ var list = document.createElement('div');
33184
+ list.className = 'cp-tooltip__device-list';
33185
+ devices.forEach(function (device) {
33186
+ var item = document.createElement('div');
33187
+ item.className = 'cp-tooltip__device-item';
33188
+ var dot = document.createElement('span');
33189
+ dot.className = 'cp-tooltip__device-dot';
33190
+ var name = document.createElement('span');
33191
+ name.className = 'cp-tooltip__device-name';
33192
+ name.textContent = device.label;
33193
+ item.appendChild(dot);
33194
+ item.appendChild(name);
33195
+ list.appendChild(item);
33196
+ });
33197
+ ioSection.appendChild(trigger);
33198
+ ioSection.appendChild(list);
33199
+
33200
+ // Hover expand/collapse
33201
+ trigger.addEventListener('mouseenter', function () {
33202
+ ioSection.classList.add('expanded');
33203
+ _this2._ioExpanded = true;
33204
+ });
33205
+ ioSection.addEventListener('mouseleave', function () {
33206
+ ioSection.classList.remove('expanded');
33207
+ _this2._ioExpanded = false;
33208
+ });
33209
+ card.appendChild(ioSection);
33210
+ } else {
33211
+ // Non-smart: show empty state
33212
+ var noDevices = document.createElement('div');
33213
+ noDevices.className = 'cp-tooltip__no-devices';
33214
+ noDevices.textContent = 'No I/O devices attached';
33215
+ card.appendChild(noDevices);
33216
+ }
33217
+ root.appendChild(card);
33218
+
33219
+ // Caret
33220
+ var caret = document.createElement('div');
33221
+ caret.className = 'cp-tooltip__caret';
33222
+ root.appendChild(caret);
33223
+
33224
+ // Prevent clicks on tooltip from propagating to the scene
33225
+ root.addEventListener('pointerdown', function (e) {
33226
+ return e.stopPropagation();
33227
+ });
33228
+ root.addEventListener('click', function (e) {
33229
+ return e.stopPropagation();
33230
+ });
33231
+
33232
+ // Add to the renderer container so coordinates are relative to the viewport
33233
+ container.appendChild(root);
33234
+ this.tooltipEl = root;
33235
+
33236
+ // Initial positioning
33237
+ this._positionTooltip();
33238
+ }
33239
+
33240
+ // -----------------------------------------------------------------------
33241
+ // Internal — positioning
33242
+ // -----------------------------------------------------------------------
33243
+
33244
+ /**
33245
+ * Project the selected object's world position to screen space and
33246
+ * position the tooltip element accordingly.
33247
+ */
33248
+ }, {
33249
+ key: "_positionTooltip",
33250
+ value: function _positionTooltip() {
33251
+ var _this$sceneViewer, _this$sceneViewer2;
33252
+ if (!this.tooltipEl || !this.selectedObject) return;
33253
+ var container = this._getContainer();
33254
+ var camera = (_this$sceneViewer = this.sceneViewer) === null || _this$sceneViewer === void 0 ? void 0 : _this$sceneViewer.camera;
33255
+ var renderer = (_this$sceneViewer2 = this.sceneViewer) === null || _this$sceneViewer2 === void 0 ? void 0 : _this$sceneViewer2.renderer;
33256
+ if (!container || !camera || !renderer) return;
33257
+
33258
+ // Compute bounding box to position above the component
33259
+ var box = new THREE__namespace.Box3().setFromObject(this.selectedObject);
33260
+ var center = box.getCenter(new THREE__namespace.Vector3());
33261
+ // Use top of bounding box (Z-up)
33262
+ var topZ = box.max.z;
33263
+ var worldPos = new THREE__namespace.Vector3(center.x, center.y, topZ);
33264
+
33265
+ // Project to NDC
33266
+ var ndc = worldPos.clone().project(camera);
33267
+
33268
+ // NDC to pixel coords relative to the container
33269
+ var rect = container.getBoundingClientRect();
33270
+ var x = (ndc.x + 1) / 2 * rect.width;
33271
+ var y = (-ndc.y + 1) / 2 * rect.height;
33272
+
33273
+ // Hide if behind camera
33274
+ if (ndc.z > 1) {
33275
+ this.tooltipEl.style.display = 'none';
33276
+ return;
33277
+ }
33278
+ this.tooltipEl.style.display = '';
33279
+ this.tooltipEl.style.left = "".concat(x, "px");
33280
+ this.tooltipEl.style.top = "".concat(y, "px");
33281
+ }
33282
+
33283
+ /**
33284
+ * Get the DOM element where the Three.js canvas currently lives.
33285
+ * In QuadViewport the canvas is moved out of the original hidden
33286
+ * container, so we must use the renderer's actual parent element.
33287
+ * @returns {HTMLElement|null}
33288
+ */
33289
+ }, {
33290
+ key: "_getContainer",
33291
+ value: function _getContainer() {
33292
+ var _this$sceneViewer3;
33293
+ return ((_this$sceneViewer3 = this.sceneViewer) === null || _this$sceneViewer3 === void 0 || (_this$sceneViewer3 = _this$sceneViewer3.renderer) === null || _this$sceneViewer3 === void 0 || (_this$sceneViewer3 = _this$sceneViewer3.domElement) === null || _this$sceneViewer3 === void 0 ? void 0 : _this$sceneViewer3.parentElement) || null;
33294
+ }
33295
+ }]);
33296
+ }(BaseDisposable);
33297
+
33083
33298
  /**
33084
33299
  * Viewport2DInstance
33085
33300
  * Represents a single 2D viewport with its own Konva stage and configuration
@@ -34338,6 +34553,11 @@ var CentralPlantInternals = /*#__PURE__*/function () {
34338
34553
  this.centralPlant.managers.tooltipsManager = new SceneTooltipsManager(this.centralPlant.sceneViewer.$refs.container, this.centralPlant.sceneViewer.camera, this.centralPlant.sceneViewer.scene);
34339
34554
  this.centralPlant.sceneViewer.tooltipsManager = this.centralPlant.managers.tooltipsManager;
34340
34555
  console.log('🔍 Tooltip manager initialized:', this.centralPlant.managers.tooltipsManager);
34556
+
34557
+ // Initialize the component tooltip manager (screen-space tooltip on selection)
34558
+ this.centralPlant.managers.componentTooltipManager = new ComponentTooltipManager(this.centralPlant.sceneViewer);
34559
+ this.centralPlant.sceneViewer.componentTooltipManager = this.centralPlant.managers.componentTooltipManager;
34560
+ console.log('🔍 Component tooltip manager initialized');
34341
34561
  }
34342
34562
  }
34343
34563
 
@@ -35446,7 +35666,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
35446
35666
  * Initialize the CentralPlant manager
35447
35667
  *
35448
35668
  * @constructor
35449
- * @version 0.1.78
35669
+ * @version 0.1.79
35450
35670
  * @updated 2025-10-22
35451
35671
  *
35452
35672
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -37714,7 +37934,7 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
37714
37934
  this.centralPlant.attachToComponent();
37715
37935
 
37716
37936
  // Sync our managers tracking object after attachment
37717
- managerKeys = ['threeJSResourceManager', 'performanceMonitorManager', 'settingsManager', 'sceneExportManager', 'componentManager', 'sceneInitializationManager', 'environmentManager', 'keyboardControlsManager', 'pathfindingManager', 'sceneOperationsManager', 'animationManager', 'cameraControlsManager', 'componentDragManager', 'tooltipsManager']; // Populate our managers tracking object
37937
+ managerKeys = ['threeJSResourceManager', 'performanceMonitorManager', 'settingsManager', 'sceneExportManager', 'componentManager', 'sceneInitializationManager', 'environmentManager', 'keyboardControlsManager', 'pathfindingManager', 'sceneOperationsManager', 'animationManager', 'cameraControlsManager', 'componentDragManager', 'tooltipsManager', 'componentTooltipManager']; // Populate our managers tracking object
37718
37938
  managerKeys.forEach(function (key) {
37719
37939
  if (_this2[key]) {
37720
37940
  _this2.managers[key] = _this2[key];
@@ -37992,6 +38212,10 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
37992
38212
  this.transformManager.on({
37993
38213
  onModeChange: this.onModeChange.bind(this),
37994
38214
  onSelectionChanged: function onSelectionChanged(object) {
38215
+ // Update the component tooltip on selection changes
38216
+ if (_this4.componentTooltipManager) {
38217
+ _this4.componentTooltipManager.onSelectionChanged(object);
38218
+ }
37995
38219
  if (object) {
37996
38220
  var _object$userData;
37997
38221
  // Object selected
@@ -38417,6 +38641,12 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
38417
38641
  this.tooltipsManager = null;
38418
38642
  }
38419
38643
 
38644
+ // Clean up component tooltip manager
38645
+ if (this.componentTooltipManager) {
38646
+ this.componentTooltipManager.dispose('ComponentTooltipManager');
38647
+ this.componentTooltipManager = null;
38648
+ }
38649
+
38420
38650
  // Fallback cleanup if Three.js resource manager not available
38421
38651
  if (!this.threeJSResourceManager) {
38422
38652
  // Fallback cleanup if disposal manager not available
@@ -41278,6 +41508,7 @@ exports.CentralPlant = CentralPlant;
41278
41508
  exports.ComponentDataManager = ComponentDataManager;
41279
41509
  exports.ComponentDragManager = ComponentDragManager;
41280
41510
  exports.ComponentManager = ComponentManager;
41511
+ exports.ComponentTooltipManager = ComponentTooltipManager;
41281
41512
  exports.EnvironmentManager = EnvironmentManager;
41282
41513
  exports.KeyboardControlsManager = KeyboardControlsManager;
41283
41514
  exports.ModelManager = ModelManager;