@2112-lab/central-plant 0.1.78 → 0.1.80

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
  }();
@@ -31012,18 +30911,32 @@ var AnimationManager = /*#__PURE__*/function (_BaseDisposable) {
31012
30911
  if (sceneViewer.performanceMonitorManager) {
31013
30912
  sceneViewer.performanceMonitorManager.beginFrame();
31014
30913
  }
30914
+ try {
30915
+ // Update controls
30916
+ sceneViewer.controls.update();
31015
30917
 
31016
- // Update controls
31017
- sceneViewer.controls.update();
31018
-
31019
- // Render the scene
31020
- sceneViewer.renderer.render(sceneViewer.scene, sceneViewer.camera);
30918
+ // Render the scene
30919
+ sceneViewer.renderer.render(sceneViewer.scene, sceneViewer.camera);
30920
+ } catch (renderError) {
30921
+ // Catch WebGL or rendering errors to prevent the animation loop from
30922
+ // producing a permanent white screen. Log once and continue so that
30923
+ // subsequent frames can recover if the problematic object is removed.
30924
+ if (!this._hasLoggedRenderError) {
30925
+ console.error('❌ AnimationManager: Render error caught — scene may contain disposed resources:', renderError);
30926
+ this._hasLoggedRenderError = true;
30927
+ }
30928
+ }
31021
30929
 
31022
30930
  // Render tooltips if tooltipsManager exists
31023
30931
  if (sceneViewer.tooltipsManager) {
31024
30932
  sceneViewer.tooltipsManager.render();
31025
30933
  }
31026
30934
 
30935
+ // Update component tooltip screen position
30936
+ if (sceneViewer.componentTooltipManager) {
30937
+ sceneViewer.componentTooltipManager.update();
30938
+ }
30939
+
31027
30940
  // End performance monitoring frame
31028
30941
  if (sceneViewer.performanceMonitorManager) {
31029
30942
  sceneViewer.performanceMonitorManager.endFrame();
@@ -31394,7 +31307,10 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
31394
31307
  }
31395
31308
  this.dragData.previewObject = cachedModel.clone();
31396
31309
 
31397
- // Clone materials to avoid affecting other objects
31310
+ // Clone geometries and materials to fully isolate the preview from the cache.
31311
+ // Without this, disposing the preview would invalidate shared GPU buffers
31312
+ // and cause the scene to go white.
31313
+ this._cloneGeometries(this.dragData.previewObject);
31398
31314
  this._cloneMaterials(this.dragData.previewObject);
31399
31315
 
31400
31316
  // Store original colors BEFORE making transparent
@@ -31533,6 +31449,7 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
31533
31449
  return _context3.a(3, 11);
31534
31450
  case 9:
31535
31451
  deviceModel = cachedDevice.clone();
31452
+ this._cloneGeometries(deviceModel);
31536
31453
  this._cloneMaterials(deviceModel);
31537
31454
  this._storeOriginalColors(deviceModel);
31538
31455
  deviceModel.userData = {
@@ -31585,16 +31502,38 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
31585
31502
  if (Array.isArray(child.material)) {
31586
31503
  // Handle material arrays
31587
31504
  child.material = child.material.map(function (material) {
31588
- return material.clone();
31505
+ var cloned = material.clone();
31506
+ cloned.userData._isClonedMaterial = true;
31507
+ return cloned;
31589
31508
  });
31590
31509
  } else {
31591
31510
  // Handle single materials
31592
31511
  child.material = child.material.clone();
31512
+ child.material.userData._isClonedMaterial = true;
31593
31513
  }
31594
31514
  }
31595
31515
  });
31596
31516
  }
31597
31517
 
31518
+ /**
31519
+ * Clone all geometries in an object hierarchy to avoid shared geometry buffer issues.
31520
+ * Three.js Object3D.clone() shares geometry references between the original and clone.
31521
+ * Disposing shared geometry invalidates GPU buffers for ALL objects using that geometry,
31522
+ * which causes the scene to go white. This method gives each mesh its own geometry copy.
31523
+ * @param {THREE.Object3D} object - The object to clone geometries for
31524
+ * @private
31525
+ */
31526
+ }, {
31527
+ key: "_cloneGeometries",
31528
+ value: function _cloneGeometries(object) {
31529
+ object.traverse(function (child) {
31530
+ if (child.isMesh && child.geometry) {
31531
+ child.geometry = child.geometry.clone();
31532
+ child.userData._isClonedGeometry = true;
31533
+ }
31534
+ });
31535
+ }
31536
+
31598
31537
  /**
31599
31538
  * Store original colors from all materials in an object hierarchy
31600
31539
  * @param {THREE.Object3D} object - The object to store colors from
@@ -31973,18 +31912,27 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
31973
31912
  if (this.dragData.previewObject) {
31974
31913
  this.sceneViewer.scene.remove(this.dragData.previewObject);
31975
31914
 
31976
- // Dispose of geometry and materials properly
31915
+ // Dispose of CLONED geometry and materials only.
31916
+ // Both geometries and materials were cloned during preview creation
31917
+ // via _cloneGeometries() and _cloneMaterials(), so they are safe to dispose.
31918
+ // IMPORTANT: Never dispose geometry/materials that are shared with the model cache.
31977
31919
  this.dragData.previewObject.traverse(function (child) {
31978
- if (child.geometry) {
31979
- child.geometry.dispose();
31980
- }
31981
- if (child.material) {
31982
- if (Array.isArray(child.material)) {
31983
- child.material.forEach(function (material) {
31984
- return material.dispose();
31985
- });
31986
- } else {
31987
- child.material.dispose();
31920
+ if (child.isMesh) {
31921
+ // Only dispose geometry if it was cloned for this preview
31922
+ if (child.geometry && child.userData._isClonedGeometry) {
31923
+ child.geometry.dispose();
31924
+ }
31925
+ // Only dispose material if it was cloned for this preview
31926
+ if (child.material) {
31927
+ if (Array.isArray(child.material)) {
31928
+ child.material.forEach(function (material) {
31929
+ if (material.userData._isClonedMaterial) {
31930
+ material.dispose();
31931
+ }
31932
+ });
31933
+ } else if (child.material.userData._isClonedMaterial) {
31934
+ child.material.dispose();
31935
+ }
31988
31936
  }
31989
31937
  }
31990
31938
  });
@@ -33080,6 +33028,317 @@ var SceneTooltipsManager = /*#__PURE__*/function (_BaseDisposable) {
33080
33028
  }]);
33081
33029
  }(BaseDisposable);
33082
33030
 
33031
+ // ---------------------------------------------------------------------------
33032
+ // Inline styles (injected once into the document head)
33033
+ // ---------------------------------------------------------------------------
33034
+ 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";
33035
+ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
33036
+ /**
33037
+ * @param {Object} sceneViewer - The sceneViewer instance
33038
+ */
33039
+ function ComponentTooltipManager(sceneViewer) {
33040
+ var _this;
33041
+ _classCallCheck(this, ComponentTooltipManager);
33042
+ _this = _callSuper(this, ComponentTooltipManager);
33043
+ _this.sceneViewer = sceneViewer;
33044
+ _this.selectedObject = null;
33045
+ _this.tooltipEl = null;
33046
+ _this._styleInjected = false;
33047
+ _this._ioExpanded = false;
33048
+ _this._injectStyles();
33049
+ return _this;
33050
+ }
33051
+
33052
+ // -----------------------------------------------------------------------
33053
+ // Lifecycle
33054
+ // -----------------------------------------------------------------------
33055
+
33056
+ /**
33057
+ * Called automatically by BaseDisposable.dispose()
33058
+ * @override
33059
+ */
33060
+ _inherits(ComponentTooltipManager, _BaseDisposable);
33061
+ return _createClass(ComponentTooltipManager, [{
33062
+ key: "_doDispose",
33063
+ value: function _doDispose() {
33064
+ this.hide();
33065
+ this._removeStyleTag();
33066
+ this.nullifyProperties('sceneViewer', 'selectedObject', 'tooltipEl');
33067
+ }
33068
+
33069
+ // -----------------------------------------------------------------------
33070
+ // Public API
33071
+ // -----------------------------------------------------------------------
33072
+
33073
+ /**
33074
+ * Should be called when an object is selected or deselected.
33075
+ * @param {THREE.Object3D|null} object
33076
+ */
33077
+ }, {
33078
+ key: "onSelectionChanged",
33079
+ value: function onSelectionChanged(object) {
33080
+ var _object$userData;
33081
+ if (!object) {
33082
+ this.hide();
33083
+ return;
33084
+ }
33085
+ var objectType = (_object$userData = object.userData) === null || _object$userData === void 0 ? void 0 : _object$userData.objectType;
33086
+ if (objectType !== 'component') {
33087
+ this.hide();
33088
+ return;
33089
+ }
33090
+ this.selectedObject = object;
33091
+ this._ioExpanded = false;
33092
+ this._buildTooltip(object);
33093
+ }
33094
+
33095
+ /**
33096
+ * Update tooltip screen position. Call once per frame (from the animation loop).
33097
+ */
33098
+ }, {
33099
+ key: "update",
33100
+ value: function update() {
33101
+ if (!this.tooltipEl || !this.selectedObject) return;
33102
+ this._positionTooltip();
33103
+ }
33104
+
33105
+ /**
33106
+ * Remove the tooltip from the DOM.
33107
+ */
33108
+ }, {
33109
+ key: "hide",
33110
+ value: function hide() {
33111
+ if (this.tooltipEl) {
33112
+ this.tooltipEl.remove();
33113
+ this.tooltipEl = null;
33114
+ }
33115
+ this.selectedObject = null;
33116
+ this._ioExpanded = false;
33117
+ }
33118
+
33119
+ // -----------------------------------------------------------------------
33120
+ // Internal — build
33121
+ // -----------------------------------------------------------------------
33122
+
33123
+ /** Inject the stylesheet once into the document head */
33124
+ }, {
33125
+ key: "_injectStyles",
33126
+ value: function _injectStyles() {
33127
+ if (this._styleInjected) return;
33128
+ this._styleTag = document.createElement('style');
33129
+ this._styleTag.setAttribute('data-cp-tooltip', '');
33130
+ this._styleTag.textContent = TOOLTIP_STYLES;
33131
+ document.head.appendChild(this._styleTag);
33132
+ this._styleInjected = true;
33133
+ this.registerDOMElement(this._styleTag);
33134
+ }
33135
+
33136
+ /** Remove the injected stylesheet */
33137
+ }, {
33138
+ key: "_removeStyleTag",
33139
+ value: function _removeStyleTag() {
33140
+ if (this._styleTag && this._styleTag.parentNode) {
33141
+ this._styleTag.parentNode.removeChild(this._styleTag);
33142
+ }
33143
+ this._styleTag = null;
33144
+ this._styleInjected = false;
33145
+ }
33146
+
33147
+ /**
33148
+ * Gather I/O device children from a component's Three.js hierarchy.
33149
+ * @param {THREE.Object3D} object
33150
+ * @returns {{ label: string, deviceId: string }[]}
33151
+ */
33152
+ }, {
33153
+ key: "_getIODevices",
33154
+ value: function _getIODevices(object) {
33155
+ var devices = [];
33156
+ object.traverse(function (child) {
33157
+ var _child$userData;
33158
+ if (((_child$userData = child.userData) === null || _child$userData === void 0 ? void 0 : _child$userData.objectType) === 'io-device') {
33159
+ devices.push({
33160
+ label: child.userData.attachmentLabel || child.name || child.userData.deviceId || 'Unknown Device',
33161
+ deviceId: child.userData.deviceId || ''
33162
+ });
33163
+ }
33164
+ });
33165
+ return devices;
33166
+ }
33167
+
33168
+ /**
33169
+ * Build and show the tooltip for the given object.
33170
+ * @param {THREE.Object3D} object
33171
+ */
33172
+ }, {
33173
+ key: "_buildTooltip",
33174
+ value: function _buildTooltip(object) {
33175
+ var _this2 = this;
33176
+ // Remove any existing tooltip first
33177
+ this.hide();
33178
+ // Re-assign selected object since hide() clears it
33179
+ this.selectedObject = object;
33180
+ var container = this._getContainer();
33181
+ if (!container) return;
33182
+
33183
+ // Ensure the container supports absolute positioning
33184
+ var containerStyle = window.getComputedStyle(container);
33185
+ if (containerStyle.position === 'static') {
33186
+ container.style.position = 'relative';
33187
+ }
33188
+
33189
+ // Gather data — extract the friendly name, stripping the " (component-id)" suffix
33190
+ var rawName = object.name || '';
33191
+ var componentName = rawName.replace(/\s*\([^)]*\)\s*$/, '') || 'Unnamed Component';
33192
+ var devices = this._getIODevices(object);
33193
+ var isSmart = devices.length > 0;
33194
+
33195
+ // Root element
33196
+ var root = document.createElement('div');
33197
+ root.className = 'cp-tooltip';
33198
+
33199
+ // Card
33200
+ var card = document.createElement('div');
33201
+ card.className = 'cp-tooltip__card';
33202
+
33203
+ // Header
33204
+ var header = document.createElement('div');
33205
+ header.className = 'cp-tooltip__header';
33206
+ header.textContent = componentName;
33207
+ card.appendChild(header);
33208
+
33209
+ // I/O Devices section
33210
+ if (isSmart) {
33211
+ var ioSection = document.createElement('div');
33212
+ ioSection.className = 'cp-tooltip__io-section';
33213
+
33214
+ // Trigger row
33215
+ var trigger = document.createElement('div');
33216
+ trigger.className = 'cp-tooltip__io-trigger';
33217
+ var label = document.createElement('span');
33218
+ label.className = 'cp-tooltip__io-trigger-label';
33219
+ label.textContent = "I/O Devices (".concat(devices.length, ")");
33220
+ var arrow = document.createElement('span');
33221
+ arrow.className = 'cp-tooltip__io-arrow';
33222
+ arrow.textContent = '▶';
33223
+ trigger.appendChild(label);
33224
+ trigger.appendChild(arrow);
33225
+
33226
+ // Device list
33227
+ var list = document.createElement('div');
33228
+ list.className = 'cp-tooltip__device-list';
33229
+ devices.forEach(function (device) {
33230
+ var item = document.createElement('div');
33231
+ item.className = 'cp-tooltip__device-item';
33232
+ var dot = document.createElement('span');
33233
+ dot.className = 'cp-tooltip__device-dot';
33234
+ var name = document.createElement('span');
33235
+ name.className = 'cp-tooltip__device-name';
33236
+ name.textContent = device.label;
33237
+ item.appendChild(dot);
33238
+ item.appendChild(name);
33239
+ list.appendChild(item);
33240
+ });
33241
+ ioSection.appendChild(trigger);
33242
+ ioSection.appendChild(list);
33243
+
33244
+ // Hover expand/collapse
33245
+ trigger.addEventListener('mouseenter', function () {
33246
+ ioSection.classList.add('expanded');
33247
+ _this2._ioExpanded = true;
33248
+ });
33249
+ ioSection.addEventListener('mouseleave', function () {
33250
+ ioSection.classList.remove('expanded');
33251
+ _this2._ioExpanded = false;
33252
+ });
33253
+ card.appendChild(ioSection);
33254
+ } else {
33255
+ // Non-smart: show empty state
33256
+ var noDevices = document.createElement('div');
33257
+ noDevices.className = 'cp-tooltip__no-devices';
33258
+ noDevices.textContent = 'No I/O devices attached';
33259
+ card.appendChild(noDevices);
33260
+ }
33261
+ root.appendChild(card);
33262
+
33263
+ // Caret
33264
+ var caret = document.createElement('div');
33265
+ caret.className = 'cp-tooltip__caret';
33266
+ root.appendChild(caret);
33267
+
33268
+ // Prevent clicks on tooltip from propagating to the scene
33269
+ root.addEventListener('pointerdown', function (e) {
33270
+ return e.stopPropagation();
33271
+ });
33272
+ root.addEventListener('click', function (e) {
33273
+ return e.stopPropagation();
33274
+ });
33275
+
33276
+ // Add to the renderer container so coordinates are relative to the viewport
33277
+ container.appendChild(root);
33278
+ this.tooltipEl = root;
33279
+
33280
+ // Initial positioning
33281
+ this._positionTooltip();
33282
+ }
33283
+
33284
+ // -----------------------------------------------------------------------
33285
+ // Internal — positioning
33286
+ // -----------------------------------------------------------------------
33287
+
33288
+ /**
33289
+ * Project the selected object's world position to screen space and
33290
+ * position the tooltip element accordingly.
33291
+ */
33292
+ }, {
33293
+ key: "_positionTooltip",
33294
+ value: function _positionTooltip() {
33295
+ var _this$sceneViewer, _this$sceneViewer2;
33296
+ if (!this.tooltipEl || !this.selectedObject) return;
33297
+ var container = this._getContainer();
33298
+ var camera = (_this$sceneViewer = this.sceneViewer) === null || _this$sceneViewer === void 0 ? void 0 : _this$sceneViewer.camera;
33299
+ var renderer = (_this$sceneViewer2 = this.sceneViewer) === null || _this$sceneViewer2 === void 0 ? void 0 : _this$sceneViewer2.renderer;
33300
+ if (!container || !camera || !renderer) return;
33301
+
33302
+ // Compute bounding box to position above the component
33303
+ var box = new THREE__namespace.Box3().setFromObject(this.selectedObject);
33304
+ var center = box.getCenter(new THREE__namespace.Vector3());
33305
+ // Use top of bounding box (Z-up)
33306
+ var topZ = box.max.z;
33307
+ var worldPos = new THREE__namespace.Vector3(center.x, center.y, topZ);
33308
+
33309
+ // Project to NDC
33310
+ var ndc = worldPos.clone().project(camera);
33311
+
33312
+ // NDC to pixel coords relative to the container
33313
+ var rect = container.getBoundingClientRect();
33314
+ var x = (ndc.x + 1) / 2 * rect.width;
33315
+ var y = (-ndc.y + 1) / 2 * rect.height;
33316
+
33317
+ // Hide if behind camera
33318
+ if (ndc.z > 1) {
33319
+ this.tooltipEl.style.display = 'none';
33320
+ return;
33321
+ }
33322
+ this.tooltipEl.style.display = '';
33323
+ this.tooltipEl.style.left = "".concat(x, "px");
33324
+ this.tooltipEl.style.top = "".concat(y, "px");
33325
+ }
33326
+
33327
+ /**
33328
+ * Get the DOM element where the Three.js canvas currently lives.
33329
+ * In QuadViewport the canvas is moved out of the original hidden
33330
+ * container, so we must use the renderer's actual parent element.
33331
+ * @returns {HTMLElement|null}
33332
+ */
33333
+ }, {
33334
+ key: "_getContainer",
33335
+ value: function _getContainer() {
33336
+ var _this$sceneViewer3;
33337
+ 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;
33338
+ }
33339
+ }]);
33340
+ }(BaseDisposable);
33341
+
33083
33342
  /**
33084
33343
  * Viewport2DInstance
33085
33344
  * Represents a single 2D viewport with its own Konva stage and configuration
@@ -34338,6 +34597,11 @@ var CentralPlantInternals = /*#__PURE__*/function () {
34338
34597
  this.centralPlant.managers.tooltipsManager = new SceneTooltipsManager(this.centralPlant.sceneViewer.$refs.container, this.centralPlant.sceneViewer.camera, this.centralPlant.sceneViewer.scene);
34339
34598
  this.centralPlant.sceneViewer.tooltipsManager = this.centralPlant.managers.tooltipsManager;
34340
34599
  console.log('🔍 Tooltip manager initialized:', this.centralPlant.managers.tooltipsManager);
34600
+
34601
+ // Initialize the component tooltip manager (screen-space tooltip on selection)
34602
+ this.centralPlant.managers.componentTooltipManager = new ComponentTooltipManager(this.centralPlant.sceneViewer);
34603
+ this.centralPlant.sceneViewer.componentTooltipManager = this.centralPlant.managers.componentTooltipManager;
34604
+ console.log('🔍 Component tooltip manager initialized');
34341
34605
  }
34342
34606
  }
34343
34607
 
@@ -35088,8 +35352,21 @@ var CentralPlantInternals = /*#__PURE__*/function () {
35088
35352
  // Clone the cached model to create a new instance
35089
35353
  var componentModel = cachedModel;
35090
35354
 
35091
- // Set the component properties
35092
- componentModel.uuid = componentId;
35355
+ // Clone materials to isolate this component instance from the model cache.
35356
+ // Without this, shared materials would cause visual side-effects when one
35357
+ // component's material is modified (e.g., selection highlighting).
35358
+ componentModel.traverse(function (child) {
35359
+ if (child.isMesh && child.material) {
35360
+ if (Array.isArray(child.material)) {
35361
+ child.material = child.material.map(function (m) {
35362
+ return m.clone();
35363
+ });
35364
+ } else {
35365
+ child.material = child.material.clone();
35366
+ }
35367
+ }
35368
+ });
35369
+
35093
35370
  // Set the component properties
35094
35371
  componentModel.uuid = componentId;
35095
35372
 
@@ -35446,7 +35723,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
35446
35723
  * Initialize the CentralPlant manager
35447
35724
  *
35448
35725
  * @constructor
35449
- * @version 0.1.78
35726
+ * @version 0.1.80
35450
35727
  * @updated 2025-10-22
35451
35728
  *
35452
35729
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -37714,7 +37991,7 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
37714
37991
  this.centralPlant.attachToComponent();
37715
37992
 
37716
37993
  // 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
37994
+ managerKeys = ['threeJSResourceManager', 'performanceMonitorManager', 'settingsManager', 'sceneExportManager', 'componentManager', 'sceneInitializationManager', 'environmentManager', 'keyboardControlsManager', 'pathfindingManager', 'sceneOperationsManager', 'animationManager', 'cameraControlsManager', 'componentDragManager', 'tooltipsManager', 'componentTooltipManager']; // Populate our managers tracking object
37718
37995
  managerKeys.forEach(function (key) {
37719
37996
  if (_this2[key]) {
37720
37997
  _this2.managers[key] = _this2[key];
@@ -37992,6 +38269,10 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
37992
38269
  this.transformManager.on({
37993
38270
  onModeChange: this.onModeChange.bind(this),
37994
38271
  onSelectionChanged: function onSelectionChanged(object) {
38272
+ // Update the component tooltip on selection changes
38273
+ if (_this4.componentTooltipManager) {
38274
+ _this4.componentTooltipManager.onSelectionChanged(object);
38275
+ }
37995
38276
  if (object) {
37996
38277
  var _object$userData;
37997
38278
  // Object selected
@@ -38417,6 +38698,12 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
38417
38698
  this.tooltipsManager = null;
38418
38699
  }
38419
38700
 
38701
+ // Clean up component tooltip manager
38702
+ if (this.componentTooltipManager) {
38703
+ this.componentTooltipManager.dispose('ComponentTooltipManager');
38704
+ this.componentTooltipManager = null;
38705
+ }
38706
+
38420
38707
  // Fallback cleanup if Three.js resource manager not available
38421
38708
  if (!this.threeJSResourceManager) {
38422
38709
  // Fallback cleanup if disposal manager not available
@@ -41044,6 +41331,19 @@ var Rendering3D = /*#__PURE__*/function (_BaseDisposable) {
41044
41331
  this.renderer.useLegacyLights = false;
41045
41332
  container.appendChild(this.renderer.domElement);
41046
41333
 
41334
+ // Register WebGL context lost/restored handlers for resilience.
41335
+ // If the GPU context is lost (e.g., due to disposed buffers or driver issues),
41336
+ // these handlers prevent a permanent white screen.
41337
+ this._onContextLost = function (event) {
41338
+ event.preventDefault();
41339
+ console.warn('⚠️ WebGL context lost — rendering paused. The browser may restore it automatically.');
41340
+ };
41341
+ this._onContextRestored = function () {
41342
+ console.log('✅ WebGL context restored — rendering will resume.');
41343
+ };
41344
+ this.renderer.domElement.addEventListener('webglcontextlost', this._onContextLost, false);
41345
+ this.renderer.domElement.addEventListener('webglcontextrestored', this._onContextRestored, false);
41346
+
41047
41347
  // Register resources for automatic cleanup
41048
41348
  this.registerDOMElement(this.renderer.domElement);
41049
41349
  this.registerDisposable(this.renderer, 'WebGLRenderer');
@@ -41254,6 +41554,18 @@ var Rendering3D = /*#__PURE__*/function (_BaseDisposable) {
41254
41554
  frameTime: 0
41255
41555
  };
41256
41556
 
41557
+ // Remove WebGL context event listeners
41558
+ if (this.renderer && this.renderer.domElement) {
41559
+ if (this._onContextLost) {
41560
+ this.renderer.domElement.removeEventListener('webglcontextlost', this._onContextLost, false);
41561
+ }
41562
+ if (this._onContextRestored) {
41563
+ this.renderer.domElement.removeEventListener('webglcontextrestored', this._onContextRestored, false);
41564
+ }
41565
+ }
41566
+ this._onContextLost = null;
41567
+ this._onContextRestored = null;
41568
+
41257
41569
  // Clear post processing
41258
41570
  this.postProcessing = {
41259
41571
  composer: null,
@@ -41278,6 +41590,7 @@ exports.CentralPlant = CentralPlant;
41278
41590
  exports.ComponentDataManager = ComponentDataManager;
41279
41591
  exports.ComponentDragManager = ComponentDragManager;
41280
41592
  exports.ComponentManager = ComponentManager;
41593
+ exports.ComponentTooltipManager = ComponentTooltipManager;
41281
41594
  exports.EnvironmentManager = EnvironmentManager;
41282
41595
  exports.KeyboardControlsManager = KeyboardControlsManager;
41283
41596
  exports.ModelManager = ModelManager;