@2112-lab/central-plant 0.3.26 → 0.3.28

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.
Files changed (28) hide show
  1. package/dist/bundle/index.js +922 -974
  2. package/dist/cjs/src/core/centralPlant.js +8 -115
  3. package/dist/cjs/src/core/centralPlantInternals.js +23 -17
  4. package/dist/cjs/src/core/sceneViewer.js +55 -2
  5. package/dist/cjs/src/index.js +0 -2
  6. package/dist/cjs/src/managers/behaviors/IoAnimationManager.js +24 -1
  7. package/dist/cjs/src/managers/behaviors/IoOutlineManager.js +258 -0
  8. package/dist/cjs/src/managers/controls/transformControlsManager.js +319 -43
  9. package/dist/cjs/src/managers/scene/animationManager.js +9 -2
  10. package/dist/cjs/src/managers/scene/componentTooltipManager.js +190 -34
  11. package/dist/cjs/src/managers/scene/modelManager.js +15 -1
  12. package/dist/cjs/src/managers/scene/sceneExportManager.js +3 -29
  13. package/dist/cjs/src/managers/scene/sceneOperationsManager.js +12 -289
  14. package/dist/cjs/src/utils/boundingBoxUtils.js +38 -40
  15. package/dist/esm/src/core/centralPlant.js +8 -115
  16. package/dist/esm/src/core/centralPlantInternals.js +23 -17
  17. package/dist/esm/src/core/sceneViewer.js +55 -2
  18. package/dist/esm/src/index.js +0 -1
  19. package/dist/esm/src/managers/behaviors/IoAnimationManager.js +24 -1
  20. package/dist/esm/src/managers/behaviors/IoOutlineManager.js +234 -0
  21. package/dist/esm/src/managers/controls/transformControlsManager.js +319 -43
  22. package/dist/esm/src/managers/scene/animationManager.js +9 -2
  23. package/dist/esm/src/managers/scene/componentTooltipManager.js +191 -35
  24. package/dist/esm/src/managers/scene/modelManager.js +16 -2
  25. package/dist/esm/src/managers/scene/sceneExportManager.js +4 -30
  26. package/dist/esm/src/managers/scene/sceneOperationsManager.js +12 -289
  27. package/dist/esm/src/utils/boundingBoxUtils.js +39 -42
  28. package/package.json +1 -1
@@ -15,7 +15,6 @@ import { EnvironmentManager } from '../managers/environment/environmentManager.j
15
15
  import { KeyboardControlsManager } from '../managers/controls/keyboardControlsManager.js';
16
16
  import { PathfindingManager } from '../managers/pathfinding/pathfindingManager.js';
17
17
  import { PathFlowManager } from '../managers/pathfinding/PathFlowManager.js';
18
- import { BehaviorManager } from '../managers/behaviors/BehaviorManager.js';
19
18
  import { SceneOperationsManager } from '../managers/scene/sceneOperationsManager.js';
20
19
  import { AnimationManager } from '../managers/scene/animationManager.js';
21
20
  import { CameraControlsManager } from '../managers/controls/cameraControlsManager.js';
@@ -24,8 +23,10 @@ import { SceneTooltipsManager } from '../managers/scene/sceneTooltipsManager.js'
24
23
  import { ComponentTooltipManager } from '../managers/scene/componentTooltipManager.js';
25
24
  import { Viewport2DManager } from '../managers/scene/viewport2DManager.js';
26
25
  import { IoAnimationManager } from '../managers/behaviors/IoAnimationManager.js';
26
+ import { IoOutlineManager } from '../managers/behaviors/IoOutlineManager.js';
27
27
  import { generateUuidFromName, getHardcodedUuid, findObjectByHardcodedUuid, generateUniqueComponentId } from '../utils/nameUtils.js';
28
28
  import { attachIODevicesToComponent } from '../utils/ioDeviceUtils.js';
29
+ import { computeFilteredBoundingBoxCached } from '../utils/boundingBoxUtils.js';
29
30
  import modelPreloader from '../rendering/modelPreloader.js';
30
31
 
31
32
  // ─────────────────────────────────────────────────────────────────────────────
@@ -143,13 +144,13 @@ var CentralPlantInternals = /*#__PURE__*/function () {
143
144
  this.centralPlant.managers.keyboardControlsManager = new KeyboardControlsManager(this.centralPlant.sceneViewer);
144
145
  this.centralPlant.managers.pathfindingManager = new PathfindingManager(this.centralPlant.sceneViewer);
145
146
  this.centralPlant.managers.pathFlowManager = new PathFlowManager(this.centralPlant.sceneViewer);
146
- this.centralPlant.managers.behaviorManager = new BehaviorManager(this.centralPlant.sceneViewer);
147
147
  this.centralPlant.managers.sceneOperationsManager = new SceneOperationsManager(this.centralPlant.sceneViewer);
148
148
  this.centralPlant.managers.animationManager = new AnimationManager(this.centralPlant.sceneViewer);
149
149
  this.centralPlant.managers.cameraControlsManager = new CameraControlsManager(this.centralPlant.sceneViewer);
150
150
  this.centralPlant.managers.componentDragManager = new ComponentDragManager(this.centralPlant.sceneViewer);
151
151
  this.centralPlant.managers.viewport2DManager = new Viewport2DManager(this.centralPlant.sceneViewer);
152
152
  this.centralPlant.managers.ioAnimationManager = new IoAnimationManager(this.centralPlant.sceneViewer);
153
+ this.centralPlant.managers.ioOutlineManager = new IoOutlineManager(this.centralPlant.sceneViewer);
153
154
 
154
155
  // All managers are now stored in the managers collection and will be attached via attachToComponent()
155
156
  }
@@ -920,7 +921,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
920
921
  return false;
921
922
  }
922
923
  try {
923
- var _componentData$childr, _componentData$childr2, _this$centralPlant$sc7, _componentData$childr3, _componentData$defaul;
924
+ var _componentData$childr, _componentData$childr2, _this$centralPlant$sc7, _componentData$childr3;
924
925
  // Generate a unique component ID if not provided
925
926
  var componentId = options.customId || this.generateUniqueComponentId(libraryId);
926
927
 
@@ -1152,20 +1153,25 @@ var CentralPlantInternals = /*#__PURE__*/function () {
1152
1153
  }
1153
1154
  }
1154
1155
 
1155
- // Register default behaviors for smart components so the BehaviorManager
1156
- // responds to tooltip-driven state changes immediately after drop.
1157
- // (The scene-load path uses _processBehaviors instead, which runs on loadSceneData.)
1158
- if ((_componentData$defaul = componentData.defaultBehaviors) !== null && _componentData$defaul !== void 0 && _componentData$defaul.length) {
1159
- var _this$centralPlant$sc9, _som$registerBehavior;
1160
- var som = (_this$centralPlant$sc9 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc9 === void 0 ? void 0 : _this$centralPlant$sc9.sceneOperationsManager;
1161
- som === null || som === void 0 || (_som$registerBehavior = som.registerBehaviorsForComponentInstance) === null || _som$registerBehavior === void 0 || _som$registerBehavior.call(som, componentData, componentId);
1162
- }
1163
-
1164
1156
  // Notify the component manager about the new component
1165
1157
  if (componentManager.registerComponent) {
1166
1158
  componentManager.registerComponent(componentModel);
1167
1159
  }
1168
1160
 
1161
+ // Pre-warm the filtered bounding-box cache for smart components so the
1162
+ // first selection is instant. Deferred to idle time so it does not
1163
+ // block the current frame.
1164
+ if (componentData.isSmart) {
1165
+ var warmFn = function warmFn() {
1166
+ return computeFilteredBoundingBoxCached(componentModel, ['io-device', 'connector']);
1167
+ };
1168
+ if (typeof requestIdleCallback !== 'undefined') {
1169
+ requestIdleCallback(warmFn);
1170
+ } else {
1171
+ setTimeout(warmFn, 0);
1172
+ }
1173
+ }
1174
+
1169
1175
  // EMIT COMPONENT ADDED EVENT
1170
1176
  // This allows UI components (like SceneHierarchy) to update reactively
1171
1177
  if (this.centralPlant.sceneViewer.emit) {
@@ -1219,18 +1225,18 @@ var CentralPlantInternals = /*#__PURE__*/function () {
1219
1225
  }, {
1220
1226
  key: "deleteComponent",
1221
1227
  value: function deleteComponent(componentId) {
1222
- var _this$centralPlant$sc0;
1228
+ var _this$centralPlant$sc9;
1223
1229
  // Check if component manager is available
1224
- var componentManager = (_this$centralPlant$sc0 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc0 === void 0 ? void 0 : _this$centralPlant$sc0.componentManager;
1230
+ var componentManager = (_this$centralPlant$sc9 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc9 === void 0 ? void 0 : _this$centralPlant$sc9.componentManager;
1225
1231
  if (!componentManager) {
1226
1232
  console.error('❌ deleteComponent(): Component manager not available');
1227
1233
  return false;
1228
1234
  }
1229
1235
  try {
1230
- var _this$centralPlant$sc1, _this$centralPlant$sc10, _sceneData$scene2, _sceneData$scene3;
1236
+ var _this$centralPlant$sc0, _this$centralPlant$sc1, _sceneData$scene2, _sceneData$scene3;
1231
1237
  console.log("\uD83D\uDDD1\uFE0F deleteComponent(): Deleting component ".concat(componentId));
1232
- var threeScene = (_this$centralPlant$sc1 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc1 === void 0 ? void 0 : _this$centralPlant$sc1.scene;
1233
- var sceneData = (_this$centralPlant$sc10 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc10 === void 0 ? void 0 : _this$centralPlant$sc10.currentSceneData;
1238
+ var threeScene = (_this$centralPlant$sc0 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc0 === void 0 ? void 0 : _this$centralPlant$sc0.scene;
1239
+ var sceneData = (_this$centralPlant$sc1 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc1 === void 0 ? void 0 : _this$centralPlant$sc1.currentSceneData;
1234
1240
 
1235
1241
  // Step 1: Resolve the actual Three.js UUID from componentId.
1236
1242
  // The UI emits object.name (e.g. "Pump (PUMP-1)") as the selection ID, but
@@ -98,7 +98,7 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
98
98
  this.centralPlant.attachToComponent();
99
99
 
100
100
  // Sync our managers tracking object after attachment
101
- managerKeys = ['threeJSResourceManager', 'performanceMonitorManager', 'settingsManager', 'sceneExportManager', 'componentManager', 'sceneInitializationManager', 'environmentManager', 'keyboardControlsManager', 'pathfindingManager', 'pathFlowManager', 'behaviorManager', 'ioAnimationManager', 'sceneOperationsManager', 'animationManager', 'cameraControlsManager', 'componentDragManager', 'tooltipsManager', 'componentTooltipManager']; // Populate our managers tracking object
101
+ managerKeys = ['threeJSResourceManager', 'performanceMonitorManager', 'settingsManager', 'sceneExportManager', 'componentManager', 'sceneInitializationManager', 'environmentManager', 'keyboardControlsManager', 'pathfindingManager', 'pathFlowManager', 'ioAnimationManager', 'ioOutlineManager', 'sceneOperationsManager', 'animationManager', 'cameraControlsManager', 'componentDragManager', 'tooltipsManager', 'componentTooltipManager']; // Populate our managers tracking object
102
102
  managerKeys.forEach(function (key) {
103
103
  if (_this2[key]) {
104
104
  _this2.managers[key] = _this2[key];
@@ -286,7 +286,21 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
286
286
  }
287
287
 
288
288
  // Update camera aspect ratio
289
- this.camera.aspect = width / height;
289
+ if (this.camera.isPerspectiveCamera) {
290
+ this.camera.aspect = width / height;
291
+ } else if (this.camera.isOrthographicCamera) {
292
+ var _this$camera$userData;
293
+ var aspect = width / height;
294
+ var extents = (_this$camera$userData = this.camera.userData) === null || _this$camera$userData === void 0 ? void 0 : _this$camera$userData._orthoHalfExtents;
295
+ if (extents) {
296
+ var frustumHalfH = Math.max(extents.halfH, extents.halfW / aspect);
297
+ var frustumHalfW = frustumHalfH * aspect;
298
+ this.camera.left = -frustumHalfW;
299
+ this.camera.right = frustumHalfW;
300
+ this.camera.top = frustumHalfH;
301
+ this.camera.bottom = -frustumHalfH;
302
+ }
303
+ }
290
304
  this.camera.updateProjectionMatrix();
291
305
 
292
306
  // Update renderer size (updateStyle=true to sync canvas CSS)
@@ -413,6 +427,45 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
413
427
  if (_this4.componentTooltipManager) {
414
428
  _this4.componentTooltipManager.toggleIODeviceBinaryState(ioDeviceObject);
415
429
  }
430
+ },
431
+ onIODeviceDrag: function onIODeviceDrag(ioDeviceObject, signedDelta, isStart) {
432
+ if (isStart) {
433
+ var _ioDeviceObject$userD, _this4$managers$ioAni, _this4$managers, _this4$managers2;
434
+ // Resolve parentUuid by walking up to the host component.
435
+ // Use userData.originalUuid (the custom componentId) because that
436
+ // is what IoAnimationManager uses as the map key — NOT obj.uuid.
437
+ var parentUuid = null;
438
+ var obj = ioDeviceObject.parent;
439
+ while (obj) {
440
+ var _obj$userData;
441
+ if (((_obj$userData = obj.userData) === null || _obj$userData === void 0 ? void 0 : _obj$userData.objectType) === 'component') {
442
+ parentUuid = obj.userData.originalUuid || obj.uuid;
443
+ break;
444
+ }
445
+ obj = obj.parent;
446
+ }
447
+ var attachmentId = (_ioDeviceObject$userD = ioDeviceObject.userData) === null || _ioDeviceObject$userD === void 0 ? void 0 : _ioDeviceObject$userD.attachmentId;
448
+ // When animated meshes are available, outline ONLY them so their
449
+ // silhouette is isolated and the outline ring is visible around
450
+ // them specifically (not swallowed by the larger device body).
451
+ // Fall back to the whole device group when none are registered.
452
+ var animatedMeshes = attachmentId && parentUuid ? (_this4$managers$ioAni = (_this4$managers = _this4.managers) === null || _this4$managers === void 0 || (_this4$managers = _this4$managers.ioAnimationManager) === null || _this4$managers === void 0 ? void 0 : _this4$managers.getAnimatedMeshes(parentUuid, attachmentId)) !== null && _this4$managers$ioAni !== void 0 ? _this4$managers$ioAni : [] : [];
453
+ var targets = animatedMeshes.length > 0 ? animatedMeshes : [ioDeviceObject];
454
+ (_this4$managers2 = _this4.managers) === null || _this4$managers2 === void 0 || (_this4$managers2 = _this4$managers2.ioOutlineManager) === null || _this4$managers2 === void 0 || _this4$managers2.setTargets(targets);
455
+ }
456
+ if (!_this4.componentTooltipManager) return;
457
+ if (isStart) {
458
+ _this4.componentTooltipManager.startIODeviceDrag(ioDeviceObject);
459
+ } else {
460
+ _this4.componentTooltipManager.updateIODeviceDrag(signedDelta);
461
+ }
462
+ },
463
+ onIODeviceDragEnd: function onIODeviceDragEnd(ioDeviceObject) {
464
+ var _this4$managers3;
465
+ (_this4$managers3 = _this4.managers) === null || _this4$managers3 === void 0 || (_this4$managers3 = _this4$managers3.ioOutlineManager) === null || _this4$managers3 === void 0 || _this4$managers3.setTargets([]);
466
+ if (_this4.componentTooltipManager) {
467
+ _this4.componentTooltipManager.endIODeviceDrag();
468
+ }
416
469
  }
417
470
  });
418
471
 
@@ -9,7 +9,6 @@ export { SceneExportManager } from './managers/scene/sceneExportManager.js';
9
9
  export { SceneTooltipsManager } from './managers/scene/sceneTooltipsManager.js';
10
10
  export { ComponentTooltipManager } from './managers/scene/componentTooltipManager.js';
11
11
  export { SceneHierarchyManager } from './managers/scene/sceneHierarchyManager.js';
12
- export { BehaviorManager } from './managers/behaviors/BehaviorManager.js';
13
12
  export { IoAnimationManager } from './managers/behaviors/IoAnimationManager.js';
14
13
  export { ComponentManager } from './managers/components/componentManager.js';
15
14
  export { AnimationManager } from './managers/scene/animationManager.js';
@@ -219,6 +219,26 @@ var IoAnimationManager = /*#__PURE__*/function (_BaseDisposable) {
219
219
  return dps;
220
220
  }
221
221
 
222
+ /**
223
+ * Return the Three.js mesh objects that are animated for a given attachment.
224
+ * Used by IoOutlineManager to include animated meshes in the outline.
225
+ *
226
+ * @param {string} parentUuid
227
+ * @param {string} attachmentId
228
+ * @returns {THREE.Object3D[]}
229
+ */
230
+ }, {
231
+ key: "getAnimatedMeshes",
232
+ value: function getAnimatedMeshes(parentUuid, attachmentId) {
233
+ var key = this._key(parentUuid, attachmentId);
234
+ var entries = this._entries.get(key);
235
+ if (!(entries !== null && entries !== void 0 && entries.length)) return [];
236
+ // Deduplicate — multiple animations can target the same mesh
237
+ return _toConsumableArray(new Set(entries.map(function (e) {
238
+ return e.mesh;
239
+ })));
240
+ }
241
+
222
242
  /**
223
243
  * Remove all animation entries associated with a given host component.
224
244
  * Call when a component is removed from the scene.
@@ -450,7 +470,10 @@ var IoAnimationManager = /*#__PURE__*/function (_BaseDisposable) {
450
470
  value: function _applyTranslation(mesh, origPos, transform) {
451
471
  var _transform$x, _transform$y, _transform$z;
452
472
  if (!transform) return;
453
- mesh.position.set(origPos.x + ((_transform$x = transform.x) !== null && _transform$x !== void 0 ? _transform$x : 0), origPos.y + ((_transform$y = transform.y) !== null && _transform$y !== void 0 ? _transform$y : 0), origPos.z + ((_transform$z = transform.z) !== null && _transform$z !== void 0 ? _transform$z : 0));
473
+ // X and Y are negated to match the sign convention used in the AnimateDevicesDialog
474
+ // preview (_syncViewerTransform negates x and y before calling setMeshPreviewOffset).
475
+ // Z is added directly (no negation) — matching the dialog's z handling.
476
+ mesh.position.set(origPos.x - ((_transform$x = transform.x) !== null && _transform$x !== void 0 ? _transform$x : 0), origPos.y - ((_transform$y = transform.y) !== null && _transform$y !== void 0 ? _transform$y : 0), origPos.z + ((_transform$z = transform.z) !== null && _transform$z !== void 0 ? _transform$z : 0));
454
477
  }
455
478
 
456
479
  /**
@@ -0,0 +1,234 @@
1
+ import { inherits as _inherits, createClass as _createClass, superPropGet as _superPropGet, createForOfIteratorHelper as _createForOfIteratorHelper, classCallCheck as _classCallCheck, callSuper as _callSuper } from '../../../_virtual/_rollupPluginBabelHelpers.js';
2
+ import * as THREE from 'three';
3
+ import { BaseDisposable } from '../../core/baseDisposable.js';
4
+
5
+ /**
6
+ * Three.js layer reserved exclusively for the outline mask pass.
7
+ * Layer 20 is high enough to be well clear of typical app layer usage.
8
+ */
9
+ var OUTLINE_LAYER = 20;
10
+
11
+ // ── Shaders ──────────────────────────────────────────────────────────────────
12
+
13
+ var VERT_SHADER = /* glsl */"\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position.xy, 0.0, 1.0);\n}\n";
14
+
15
+ /**
16
+ * Screen-space outline shader.
17
+ *
18
+ * Samples in a circular kernel to find the nearest filled (silhouette) pixel.
19
+ * Applies a smooth alpha falloff at the outer edge using smoothstep so the
20
+ * outline fades cleanly rather than cutting off hard.
21
+ *
22
+ * RADIUS — maximum distance in pixels from the silhouette edge to draw.
23
+ * SAMPLES — kernel half-extent; must be an integer ≥ ceil(RADIUS).
24
+ */
25
+ var FRAG_SHADER = /* glsl */"\nuniform sampler2D tMask;\nuniform vec2 uInvSize;\nvarying vec2 vUv;\n\nconst float RADIUS = 3.0; // total outline width in pixels\nconst int SAMPLES = 4; // kernel half-extent (\u2265 ceil(RADIUS))\n\nvoid main() {\n float center = texture2D(tMask, vUv).r;\n\n // Pixels inside the silhouette: the main render already drew them.\n if (center > 0.5) discard;\n\n // Find the closest filled neighbour within the circular kernel.\n float minDist = 99.0;\n for (int x = -SAMPLES; x <= SAMPLES; x++) {\n for (int y = -SAMPLES; y <= SAMPLES; y++) {\n float dist = length(vec2(float(x), float(y)));\n if (dist > RADIUS + 1.0) continue; // skip corners outside circle\n vec2 offset = vec2(float(x), float(y)) * uInvSize;\n if (texture2D(tMask, vUv + offset).r > 0.5) {\n minDist = min(minDist, dist);\n }\n }\n }\n\n if (minDist > RADIUS + 0.5) discard;\n\n // Smooth alpha: full opacity near the edge, fades to 0 at RADIUS pixels out.\n float alpha = 1.0 - smoothstep(RADIUS - 1.0, RADIUS + 0.5, minDist);\n gl_FragColor = vec4(1.0, 1.0, 1.0, alpha);\n}\n";
26
+ var IoOutlineManager = /*#__PURE__*/function (_BaseDisposable) {
27
+ function IoOutlineManager(sceneViewer) {
28
+ var _this;
29
+ _classCallCheck(this, IoOutlineManager);
30
+ _this = _callSuper(this, IoOutlineManager);
31
+ _this.sceneViewer = sceneViewer;
32
+ _this._maskTarget = null;
33
+ _this._maskMat = null;
34
+ _this._overlayScene = null;
35
+ _this._overlayCamera = null;
36
+ _this._overlayMat = null;
37
+
38
+ /**
39
+ * Meshes that have been moved onto OUTLINE_LAYER, stored with their
40
+ * original layers.mask so they can be restored on clearance.
41
+ * @type {{ mesh: THREE.Mesh, originalMask: number }[]}
42
+ */
43
+ _this._layeredMeshes = [];
44
+
45
+ /** @type {boolean} Whether an outline is currently active. */
46
+ _this.isActive = false;
47
+ return _this;
48
+ }
49
+
50
+ // ─────────────────────────────────────────────────────────────────────────
51
+ // PUBLIC API
52
+ // ─────────────────────────────────────────────────────────────────────────
53
+
54
+ /**
55
+ * Set the objects to outline. Pass an empty array (or call with no args)
56
+ * to remove the outline.
57
+ * @param {THREE.Object3D[]} objects
58
+ */
59
+ _inherits(IoOutlineManager, _BaseDisposable);
60
+ return _createClass(IoOutlineManager, [{
61
+ key: "setTargets",
62
+ value: function setTargets() {
63
+ var _this2 = this;
64
+ var objects = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
65
+ this._clearLayeredMeshes();
66
+ if (!objects.length) {
67
+ this.isActive = false;
68
+ return;
69
+ }
70
+ objects.forEach(function (obj) {
71
+ obj.traverse(function (child) {
72
+ if (!child.isMesh) return;
73
+ var originalMask = child.layers.mask;
74
+ child.layers.enable(OUTLINE_LAYER);
75
+ _this2._layeredMeshes.push({
76
+ mesh: child,
77
+ originalMask: originalMask
78
+ });
79
+ });
80
+ });
81
+ this.isActive = this._layeredMeshes.length > 0;
82
+ }
83
+
84
+ /**
85
+ * Render one frame: main scene → mask pass → outline composite.
86
+ * Must be called instead of renderer.render() when isActive is true.
87
+ */
88
+ }, {
89
+ key: "render",
90
+ value: function render() {
91
+ var sv = this.sceneViewer;
92
+ var renderer = sv.renderer,
93
+ scene = sv.scene,
94
+ camera = sv.camera;
95
+ if (!renderer || !scene || !camera) return;
96
+
97
+ // ── Step 1: Normal scene render — sky, transparency, all unaffected ──
98
+ renderer.render(scene, camera);
99
+ if (!this.isActive) return;
100
+ this._ensureResources();
101
+ if (!this._maskTarget) return;
102
+
103
+ // Stash renderer / scene / camera state we are about to mutate
104
+ var prevBg = scene.background;
105
+ var prevOverride = scene.overrideMaterial;
106
+ var prevLayerMask = camera.layers.mask;
107
+ var prevAutoClear = renderer.autoClear;
108
+ var prevClearClr = renderer.getClearColor(new THREE.Color());
109
+ var prevClearA = renderer.getClearAlpha();
110
+
111
+ // ── Step 2: Render the device silhouette into the private mask target ──
112
+ scene.background = null; // transparent clear
113
+ scene.overrideMaterial = this._maskMat; // flat white for all geometry
114
+ camera.layers.set(OUTLINE_LAYER); // only see the device meshes
115
+ renderer.setClearColor(0x000000, 0);
116
+ renderer.autoClear = true;
117
+ renderer.setRenderTarget(this._maskTarget);
118
+ renderer.render(scene, camera);
119
+ renderer.setRenderTarget(null);
120
+
121
+ // Restore mutated state
122
+ scene.background = prevBg;
123
+ scene.overrideMaterial = prevOverride;
124
+ camera.layers.mask = prevLayerMask;
125
+ renderer.setClearColor(prevClearClr, prevClearA);
126
+
127
+ // ── Step 3: Composite the outline on top without clearing the framebuffer
128
+ renderer.autoClear = false;
129
+ renderer.render(this._overlayScene, this._overlayCamera);
130
+ renderer.autoClear = prevAutoClear;
131
+ }
132
+
133
+ /**
134
+ * Call when the canvas is resized to keep the mask target and shader in sync.
135
+ * @param {number} width
136
+ * @param {number} height
137
+ */
138
+ }, {
139
+ key: "setSize",
140
+ value: function setSize(width, height) {
141
+ if (this._maskTarget) this._maskTarget.setSize(width, height);
142
+ if (this._overlayMat) {
143
+ this._overlayMat.uniforms.uInvSize.value.set(1 / width, 1 / height);
144
+ }
145
+ }
146
+ }, {
147
+ key: "dispose",
148
+ value: function dispose() {
149
+ this._clearLayeredMeshes();
150
+ if (this._maskTarget) {
151
+ this._maskTarget.dispose();
152
+ this._maskTarget = null;
153
+ }
154
+ if (this._maskMat) {
155
+ this._maskMat.dispose();
156
+ this._maskMat = null;
157
+ }
158
+ if (this._overlayMat) {
159
+ this._overlayMat.dispose();
160
+ this._overlayMat = null;
161
+ }
162
+ _superPropGet(IoOutlineManager, "dispose", this, 3)([]);
163
+ }
164
+
165
+ // ─────────────────────────────────────────────────────────────────────────
166
+ // PRIVATE
167
+ // ─────────────────────────────────────────────────────────────────────────
168
+
169
+ /** Lazy-initialise GPU resources on first use. */
170
+ }, {
171
+ key: "_ensureResources",
172
+ value: function _ensureResources() {
173
+ if (this._maskTarget) return;
174
+ var renderer = this.sceneViewer.renderer;
175
+ if (!renderer) return;
176
+ var size = renderer.getSize(new THREE.Vector2());
177
+ var w = size.x;
178
+ var h = size.y;
179
+
180
+ // Private render target — receives the flat white device silhouette
181
+ this._maskTarget = new THREE.WebGLRenderTarget(w, h);
182
+
183
+ // Flat white material used during the mask pass
184
+ this._maskMat = new THREE.MeshBasicMaterial({
185
+ color: 0xffffff
186
+ });
187
+
188
+ // Screen-space overlay: reads the mask and draws only the outline pixels
189
+ this._overlayMat = new THREE.ShaderMaterial({
190
+ uniforms: {
191
+ tMask: {
192
+ value: this._maskTarget.texture
193
+ },
194
+ uInvSize: {
195
+ value: new THREE.Vector2(1 / w, 1 / h)
196
+ }
197
+ },
198
+ vertexShader: VERT_SHADER,
199
+ fragmentShader: FRAG_SHADER,
200
+ transparent: true,
201
+ depthTest: false,
202
+ depthWrite: false
203
+ });
204
+ var quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this._overlayMat);
205
+ quad.frustumCulled = false;
206
+ this._overlayScene = new THREE.Scene();
207
+ this._overlayScene.add(quad);
208
+ this._overlayCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
209
+ }
210
+
211
+ /** Remove OUTLINE_LAYER from all tracked meshes and clear the list. */
212
+ }, {
213
+ key: "_clearLayeredMeshes",
214
+ value: function _clearLayeredMeshes() {
215
+ var _iterator = _createForOfIteratorHelper(this._layeredMeshes),
216
+ _step;
217
+ try {
218
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
219
+ var _step$value = _step.value,
220
+ mesh = _step$value.mesh,
221
+ originalMask = _step$value.originalMask;
222
+ mesh.layers.mask = originalMask;
223
+ }
224
+ } catch (err) {
225
+ _iterator.e(err);
226
+ } finally {
227
+ _iterator.f();
228
+ }
229
+ this._layeredMeshes = [];
230
+ }
231
+ }]);
232
+ }(BaseDisposable);
233
+
234
+ export { IoOutlineManager };