@2112-lab/central-plant 0.1.79 → 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.
@@ -30911,12 +30911,21 @@ var AnimationManager = /*#__PURE__*/function (_BaseDisposable) {
30911
30911
  if (sceneViewer.performanceMonitorManager) {
30912
30912
  sceneViewer.performanceMonitorManager.beginFrame();
30913
30913
  }
30914
+ try {
30915
+ // Update controls
30916
+ sceneViewer.controls.update();
30914
30917
 
30915
- // Update controls
30916
- sceneViewer.controls.update();
30917
-
30918
- // Render the scene
30919
- 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
+ }
30920
30929
 
30921
30930
  // Render tooltips if tooltipsManager exists
30922
30931
  if (sceneViewer.tooltipsManager) {
@@ -31298,7 +31307,10 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
31298
31307
  }
31299
31308
  this.dragData.previewObject = cachedModel.clone();
31300
31309
 
31301
- // 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);
31302
31314
  this._cloneMaterials(this.dragData.previewObject);
31303
31315
 
31304
31316
  // Store original colors BEFORE making transparent
@@ -31437,6 +31449,7 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
31437
31449
  return _context3.a(3, 11);
31438
31450
  case 9:
31439
31451
  deviceModel = cachedDevice.clone();
31452
+ this._cloneGeometries(deviceModel);
31440
31453
  this._cloneMaterials(deviceModel);
31441
31454
  this._storeOriginalColors(deviceModel);
31442
31455
  deviceModel.userData = {
@@ -31489,16 +31502,38 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
31489
31502
  if (Array.isArray(child.material)) {
31490
31503
  // Handle material arrays
31491
31504
  child.material = child.material.map(function (material) {
31492
- return material.clone();
31505
+ var cloned = material.clone();
31506
+ cloned.userData._isClonedMaterial = true;
31507
+ return cloned;
31493
31508
  });
31494
31509
  } else {
31495
31510
  // Handle single materials
31496
31511
  child.material = child.material.clone();
31512
+ child.material.userData._isClonedMaterial = true;
31497
31513
  }
31498
31514
  }
31499
31515
  });
31500
31516
  }
31501
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
+
31502
31537
  /**
31503
31538
  * Store original colors from all materials in an object hierarchy
31504
31539
  * @param {THREE.Object3D} object - The object to store colors from
@@ -31877,18 +31912,27 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
31877
31912
  if (this.dragData.previewObject) {
31878
31913
  this.sceneViewer.scene.remove(this.dragData.previewObject);
31879
31914
 
31880
- // 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.
31881
31919
  this.dragData.previewObject.traverse(function (child) {
31882
- if (child.geometry) {
31883
- child.geometry.dispose();
31884
- }
31885
- if (child.material) {
31886
- if (Array.isArray(child.material)) {
31887
- child.material.forEach(function (material) {
31888
- return material.dispose();
31889
- });
31890
- } else {
31891
- 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
+ }
31892
31936
  }
31893
31937
  }
31894
31938
  });
@@ -35308,8 +35352,21 @@ var CentralPlantInternals = /*#__PURE__*/function () {
35308
35352
  // Clone the cached model to create a new instance
35309
35353
  var componentModel = cachedModel;
35310
35354
 
35311
- // Set the component properties
35312
- 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
+
35313
35370
  // Set the component properties
35314
35371
  componentModel.uuid = componentId;
35315
35372
 
@@ -35666,7 +35723,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
35666
35723
  * Initialize the CentralPlant manager
35667
35724
  *
35668
35725
  * @constructor
35669
- * @version 0.1.79
35726
+ * @version 0.1.80
35670
35727
  * @updated 2025-10-22
35671
35728
  *
35672
35729
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -41274,6 +41331,19 @@ var Rendering3D = /*#__PURE__*/function (_BaseDisposable) {
41274
41331
  this.renderer.useLegacyLights = false;
41275
41332
  container.appendChild(this.renderer.domElement);
41276
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
+
41277
41347
  // Register resources for automatic cleanup
41278
41348
  this.registerDOMElement(this.renderer.domElement);
41279
41349
  this.registerDisposable(this.renderer, 'WebGLRenderer');
@@ -41484,6 +41554,18 @@ var Rendering3D = /*#__PURE__*/function (_BaseDisposable) {
41484
41554
  frameTime: 0
41485
41555
  };
41486
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
+
41487
41569
  // Clear post processing
41488
41570
  this.postProcessing = {
41489
41571
  composer: null,
@@ -19,7 +19,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
19
19
  * Initialize the CentralPlant manager
20
20
  *
21
21
  * @constructor
22
- * @version 0.1.79
22
+ * @version 0.1.80
23
23
  * @updated 2025-10-22
24
24
  *
25
25
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -931,8 +931,21 @@ var CentralPlantInternals = /*#__PURE__*/function () {
931
931
  // Clone the cached model to create a new instance
932
932
  var componentModel = cachedModel;
933
933
 
934
- // Set the component properties
935
- componentModel.uuid = componentId;
934
+ // Clone materials to isolate this component instance from the model cache.
935
+ // Without this, shared materials would cause visual side-effects when one
936
+ // component's material is modified (e.g., selection highlighting).
937
+ componentModel.traverse(function (child) {
938
+ if (child.isMesh && child.material) {
939
+ if (Array.isArray(child.material)) {
940
+ child.material = child.material.map(function (m) {
941
+ return m.clone();
942
+ });
943
+ } else {
944
+ child.material = child.material.clone();
945
+ }
946
+ }
947
+ });
948
+
936
949
  // Set the component properties
937
950
  componentModel.uuid = componentId;
938
951
 
@@ -252,7 +252,10 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
252
252
  }
253
253
  this.dragData.previewObject = cachedModel.clone();
254
254
 
255
- // Clone materials to avoid affecting other objects
255
+ // Clone geometries and materials to fully isolate the preview from the cache.
256
+ // Without this, disposing the preview would invalidate shared GPU buffers
257
+ // and cause the scene to go white.
258
+ this._cloneGeometries(this.dragData.previewObject);
256
259
  this._cloneMaterials(this.dragData.previewObject);
257
260
 
258
261
  // Store original colors BEFORE making transparent
@@ -391,6 +394,7 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
391
394
  return _context3.a(3, 11);
392
395
  case 9:
393
396
  deviceModel = cachedDevice.clone();
397
+ this._cloneGeometries(deviceModel);
394
398
  this._cloneMaterials(deviceModel);
395
399
  this._storeOriginalColors(deviceModel);
396
400
  deviceModel.userData = {
@@ -443,16 +447,38 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
443
447
  if (Array.isArray(child.material)) {
444
448
  // Handle material arrays
445
449
  child.material = child.material.map(function (material) {
446
- return material.clone();
450
+ var cloned = material.clone();
451
+ cloned.userData._isClonedMaterial = true;
452
+ return cloned;
447
453
  });
448
454
  } else {
449
455
  // Handle single materials
450
456
  child.material = child.material.clone();
457
+ child.material.userData._isClonedMaterial = true;
451
458
  }
452
459
  }
453
460
  });
454
461
  }
455
462
 
463
+ /**
464
+ * Clone all geometries in an object hierarchy to avoid shared geometry buffer issues.
465
+ * Three.js Object3D.clone() shares geometry references between the original and clone.
466
+ * Disposing shared geometry invalidates GPU buffers for ALL objects using that geometry,
467
+ * which causes the scene to go white. This method gives each mesh its own geometry copy.
468
+ * @param {THREE.Object3D} object - The object to clone geometries for
469
+ * @private
470
+ */
471
+ }, {
472
+ key: "_cloneGeometries",
473
+ value: function _cloneGeometries(object) {
474
+ object.traverse(function (child) {
475
+ if (child.isMesh && child.geometry) {
476
+ child.geometry = child.geometry.clone();
477
+ child.userData._isClonedGeometry = true;
478
+ }
479
+ });
480
+ }
481
+
456
482
  /**
457
483
  * Store original colors from all materials in an object hierarchy
458
484
  * @param {THREE.Object3D} object - The object to store colors from
@@ -831,18 +857,27 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
831
857
  if (this.dragData.previewObject) {
832
858
  this.sceneViewer.scene.remove(this.dragData.previewObject);
833
859
 
834
- // Dispose of geometry and materials properly
860
+ // Dispose of CLONED geometry and materials only.
861
+ // Both geometries and materials were cloned during preview creation
862
+ // via _cloneGeometries() and _cloneMaterials(), so they are safe to dispose.
863
+ // IMPORTANT: Never dispose geometry/materials that are shared with the model cache.
835
864
  this.dragData.previewObject.traverse(function (child) {
836
- if (child.geometry) {
837
- child.geometry.dispose();
838
- }
839
- if (child.material) {
840
- if (Array.isArray(child.material)) {
841
- child.material.forEach(function (material) {
842
- return material.dispose();
843
- });
844
- } else {
845
- child.material.dispose();
865
+ if (child.isMesh) {
866
+ // Only dispose geometry if it was cloned for this preview
867
+ if (child.geometry && child.userData._isClonedGeometry) {
868
+ child.geometry.dispose();
869
+ }
870
+ // Only dispose material if it was cloned for this preview
871
+ if (child.material) {
872
+ if (Array.isArray(child.material)) {
873
+ child.material.forEach(function (material) {
874
+ if (material.userData._isClonedMaterial) {
875
+ material.dispose();
876
+ }
877
+ });
878
+ } else if (child.material.userData._isClonedMaterial) {
879
+ child.material.dispose();
880
+ }
846
881
  }
847
882
  }
848
883
  });
@@ -62,12 +62,21 @@ var AnimationManager = /*#__PURE__*/function (_BaseDisposable) {
62
62
  if (sceneViewer.performanceMonitorManager) {
63
63
  sceneViewer.performanceMonitorManager.beginFrame();
64
64
  }
65
-
66
- // Update controls
67
- sceneViewer.controls.update();
68
-
69
- // Render the scene
70
- sceneViewer.renderer.render(sceneViewer.scene, sceneViewer.camera);
65
+ try {
66
+ // Update controls
67
+ sceneViewer.controls.update();
68
+
69
+ // Render the scene
70
+ sceneViewer.renderer.render(sceneViewer.scene, sceneViewer.camera);
71
+ } catch (renderError) {
72
+ // Catch WebGL or rendering errors to prevent the animation loop from
73
+ // producing a permanent white screen. Log once and continue so that
74
+ // subsequent frames can recover if the problematic object is removed.
75
+ if (!this._hasLoggedRenderError) {
76
+ console.error('❌ AnimationManager: Render error caught — scene may contain disposed resources:', renderError);
77
+ this._hasLoggedRenderError = true;
78
+ }
79
+ }
71
80
 
72
81
  // Render tooltips if tooltipsManager exists
73
82
  if (sceneViewer.tooltipsManager) {
@@ -480,6 +480,19 @@ var Rendering3D = /*#__PURE__*/function (_BaseDisposable) {
480
480
  this.renderer.useLegacyLights = false;
481
481
  container.appendChild(this.renderer.domElement);
482
482
 
483
+ // Register WebGL context lost/restored handlers for resilience.
484
+ // If the GPU context is lost (e.g., due to disposed buffers or driver issues),
485
+ // these handlers prevent a permanent white screen.
486
+ this._onContextLost = function (event) {
487
+ event.preventDefault();
488
+ console.warn('⚠️ WebGL context lost — rendering paused. The browser may restore it automatically.');
489
+ };
490
+ this._onContextRestored = function () {
491
+ console.log('✅ WebGL context restored — rendering will resume.');
492
+ };
493
+ this.renderer.domElement.addEventListener('webglcontextlost', this._onContextLost, false);
494
+ this.renderer.domElement.addEventListener('webglcontextrestored', this._onContextRestored, false);
495
+
483
496
  // Register resources for automatic cleanup
484
497
  this.registerDOMElement(this.renderer.domElement);
485
498
  this.registerDisposable(this.renderer, 'WebGLRenderer');
@@ -690,6 +703,18 @@ var Rendering3D = /*#__PURE__*/function (_BaseDisposable) {
690
703
  frameTime: 0
691
704
  };
692
705
 
706
+ // Remove WebGL context event listeners
707
+ if (this.renderer && this.renderer.domElement) {
708
+ if (this._onContextLost) {
709
+ this.renderer.domElement.removeEventListener('webglcontextlost', this._onContextLost, false);
710
+ }
711
+ if (this._onContextRestored) {
712
+ this.renderer.domElement.removeEventListener('webglcontextrestored', this._onContextRestored, false);
713
+ }
714
+ }
715
+ this._onContextLost = null;
716
+ this._onContextRestored = null;
717
+
693
718
  // Clear post processing
694
719
  this.postProcessing = {
695
720
  composer: null,
@@ -15,7 +15,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
15
15
  * Initialize the CentralPlant manager
16
16
  *
17
17
  * @constructor
18
- * @version 0.1.79
18
+ * @version 0.1.80
19
19
  * @updated 2025-10-22
20
20
  *
21
21
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -907,8 +907,21 @@ var CentralPlantInternals = /*#__PURE__*/function () {
907
907
  // Clone the cached model to create a new instance
908
908
  var componentModel = cachedModel;
909
909
 
910
- // Set the component properties
911
- componentModel.uuid = componentId;
910
+ // Clone materials to isolate this component instance from the model cache.
911
+ // Without this, shared materials would cause visual side-effects when one
912
+ // component's material is modified (e.g., selection highlighting).
913
+ componentModel.traverse(function (child) {
914
+ if (child.isMesh && child.material) {
915
+ if (Array.isArray(child.material)) {
916
+ child.material = child.material.map(function (m) {
917
+ return m.clone();
918
+ });
919
+ } else {
920
+ child.material = child.material.clone();
921
+ }
922
+ }
923
+ });
924
+
912
925
  // Set the component properties
913
926
  componentModel.uuid = componentId;
914
927
 
@@ -228,7 +228,10 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
228
228
  }
229
229
  this.dragData.previewObject = cachedModel.clone();
230
230
 
231
- // Clone materials to avoid affecting other objects
231
+ // Clone geometries and materials to fully isolate the preview from the cache.
232
+ // Without this, disposing the preview would invalidate shared GPU buffers
233
+ // and cause the scene to go white.
234
+ this._cloneGeometries(this.dragData.previewObject);
232
235
  this._cloneMaterials(this.dragData.previewObject);
233
236
 
234
237
  // Store original colors BEFORE making transparent
@@ -367,6 +370,7 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
367
370
  return _context3.a(3, 11);
368
371
  case 9:
369
372
  deviceModel = cachedDevice.clone();
373
+ this._cloneGeometries(deviceModel);
370
374
  this._cloneMaterials(deviceModel);
371
375
  this._storeOriginalColors(deviceModel);
372
376
  deviceModel.userData = {
@@ -419,16 +423,38 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
419
423
  if (Array.isArray(child.material)) {
420
424
  // Handle material arrays
421
425
  child.material = child.material.map(function (material) {
422
- return material.clone();
426
+ var cloned = material.clone();
427
+ cloned.userData._isClonedMaterial = true;
428
+ return cloned;
423
429
  });
424
430
  } else {
425
431
  // Handle single materials
426
432
  child.material = child.material.clone();
433
+ child.material.userData._isClonedMaterial = true;
427
434
  }
428
435
  }
429
436
  });
430
437
  }
431
438
 
439
+ /**
440
+ * Clone all geometries in an object hierarchy to avoid shared geometry buffer issues.
441
+ * Three.js Object3D.clone() shares geometry references between the original and clone.
442
+ * Disposing shared geometry invalidates GPU buffers for ALL objects using that geometry,
443
+ * which causes the scene to go white. This method gives each mesh its own geometry copy.
444
+ * @param {THREE.Object3D} object - The object to clone geometries for
445
+ * @private
446
+ */
447
+ }, {
448
+ key: "_cloneGeometries",
449
+ value: function _cloneGeometries(object) {
450
+ object.traverse(function (child) {
451
+ if (child.isMesh && child.geometry) {
452
+ child.geometry = child.geometry.clone();
453
+ child.userData._isClonedGeometry = true;
454
+ }
455
+ });
456
+ }
457
+
432
458
  /**
433
459
  * Store original colors from all materials in an object hierarchy
434
460
  * @param {THREE.Object3D} object - The object to store colors from
@@ -807,18 +833,27 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
807
833
  if (this.dragData.previewObject) {
808
834
  this.sceneViewer.scene.remove(this.dragData.previewObject);
809
835
 
810
- // Dispose of geometry and materials properly
836
+ // Dispose of CLONED geometry and materials only.
837
+ // Both geometries and materials were cloned during preview creation
838
+ // via _cloneGeometries() and _cloneMaterials(), so they are safe to dispose.
839
+ // IMPORTANT: Never dispose geometry/materials that are shared with the model cache.
811
840
  this.dragData.previewObject.traverse(function (child) {
812
- if (child.geometry) {
813
- child.geometry.dispose();
814
- }
815
- if (child.material) {
816
- if (Array.isArray(child.material)) {
817
- child.material.forEach(function (material) {
818
- return material.dispose();
819
- });
820
- } else {
821
- child.material.dispose();
841
+ if (child.isMesh) {
842
+ // Only dispose geometry if it was cloned for this preview
843
+ if (child.geometry && child.userData._isClonedGeometry) {
844
+ child.geometry.dispose();
845
+ }
846
+ // Only dispose material if it was cloned for this preview
847
+ if (child.material) {
848
+ if (Array.isArray(child.material)) {
849
+ child.material.forEach(function (material) {
850
+ if (material.userData._isClonedMaterial) {
851
+ material.dispose();
852
+ }
853
+ });
854
+ } else if (child.material.userData._isClonedMaterial) {
855
+ child.material.dispose();
856
+ }
822
857
  }
823
858
  }
824
859
  });
@@ -58,12 +58,21 @@ var AnimationManager = /*#__PURE__*/function (_BaseDisposable) {
58
58
  if (sceneViewer.performanceMonitorManager) {
59
59
  sceneViewer.performanceMonitorManager.beginFrame();
60
60
  }
61
-
62
- // Update controls
63
- sceneViewer.controls.update();
64
-
65
- // Render the scene
66
- sceneViewer.renderer.render(sceneViewer.scene, sceneViewer.camera);
61
+ try {
62
+ // Update controls
63
+ sceneViewer.controls.update();
64
+
65
+ // Render the scene
66
+ sceneViewer.renderer.render(sceneViewer.scene, sceneViewer.camera);
67
+ } catch (renderError) {
68
+ // Catch WebGL or rendering errors to prevent the animation loop from
69
+ // producing a permanent white screen. Log once and continue so that
70
+ // subsequent frames can recover if the problematic object is removed.
71
+ if (!this._hasLoggedRenderError) {
72
+ console.error('❌ AnimationManager: Render error caught — scene may contain disposed resources:', renderError);
73
+ this._hasLoggedRenderError = true;
74
+ }
75
+ }
67
76
 
68
77
  // Render tooltips if tooltipsManager exists
69
78
  if (sceneViewer.tooltipsManager) {
@@ -456,6 +456,19 @@ var Rendering3D = /*#__PURE__*/function (_BaseDisposable) {
456
456
  this.renderer.useLegacyLights = false;
457
457
  container.appendChild(this.renderer.domElement);
458
458
 
459
+ // Register WebGL context lost/restored handlers for resilience.
460
+ // If the GPU context is lost (e.g., due to disposed buffers or driver issues),
461
+ // these handlers prevent a permanent white screen.
462
+ this._onContextLost = function (event) {
463
+ event.preventDefault();
464
+ console.warn('⚠️ WebGL context lost — rendering paused. The browser may restore it automatically.');
465
+ };
466
+ this._onContextRestored = function () {
467
+ console.log('✅ WebGL context restored — rendering will resume.');
468
+ };
469
+ this.renderer.domElement.addEventListener('webglcontextlost', this._onContextLost, false);
470
+ this.renderer.domElement.addEventListener('webglcontextrestored', this._onContextRestored, false);
471
+
459
472
  // Register resources for automatic cleanup
460
473
  this.registerDOMElement(this.renderer.domElement);
461
474
  this.registerDisposable(this.renderer, 'WebGLRenderer');
@@ -666,6 +679,18 @@ var Rendering3D = /*#__PURE__*/function (_BaseDisposable) {
666
679
  frameTime: 0
667
680
  };
668
681
 
682
+ // Remove WebGL context event listeners
683
+ if (this.renderer && this.renderer.domElement) {
684
+ if (this._onContextLost) {
685
+ this.renderer.domElement.removeEventListener('webglcontextlost', this._onContextLost, false);
686
+ }
687
+ if (this._onContextRestored) {
688
+ this.renderer.domElement.removeEventListener('webglcontextrestored', this._onContextRestored, false);
689
+ }
690
+ }
691
+ this._onContextLost = null;
692
+ this._onContextRestored = null;
693
+
669
694
  // Clear post processing
670
695
  this.postProcessing = {
671
696
  composer: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2112-lab/central-plant",
3
- "version": "0.1.79",
3
+ "version": "0.1.80",
4
4
  "description": "Utility modules for the Central Plant Application",
5
5
  "main": "dist/bundle/index.js",
6
6
  "module": "dist/esm/index.js",