@2112-lab/central-plant 0.3.57 → 0.3.59

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 (29) hide show
  1. package/dist/bundle/index.js +1696 -34
  2. package/dist/cjs/src/core/centralPlant.js +80 -11
  3. package/dist/cjs/src/core/centralPlantInternals.js +4 -0
  4. package/dist/cjs/src/core/centralPlantValidator.js +10 -6
  5. package/dist/cjs/src/core/stateEngine.js +733 -0
  6. package/dist/cjs/src/core/stateEngineScene.js +97 -0
  7. package/dist/cjs/src/index.js +10 -0
  8. package/dist/cjs/src/managers/controls/transformControlsManager.js +61 -2
  9. package/dist/cjs/src/managers/pathfinding/PathFlowManager.js +94 -13
  10. package/dist/cjs/src/managers/pathfinding/PathRenderingManager.js +15 -0
  11. package/dist/cjs/src/managers/scene/componentTooltipManager.js +7 -1
  12. package/dist/cjs/src/managers/scene/controlPanelManager.js +377 -0
  13. package/dist/cjs/src/managers/scene/sceneExportManager.js +10 -1
  14. package/dist/cjs/src/managers/state/EffectiveVisualManager.js +270 -0
  15. package/dist/esm/src/core/centralPlant.js +80 -11
  16. package/dist/esm/src/core/centralPlantInternals.js +4 -0
  17. package/dist/esm/src/core/centralPlantValidator.js +10 -6
  18. package/dist/esm/src/core/stateEngine.js +725 -0
  19. package/dist/esm/src/core/stateEngineScene.js +90 -0
  20. package/dist/esm/src/index.js +2 -0
  21. package/dist/esm/src/managers/controls/transformControlsManager.js +61 -2
  22. package/dist/esm/src/managers/pathfinding/PathFlowManager.js +94 -13
  23. package/dist/esm/src/managers/pathfinding/PathRenderingManager.js +15 -0
  24. package/dist/esm/src/managers/scene/componentTooltipManager.js +7 -1
  25. package/dist/esm/src/managers/scene/controlPanelManager.js +352 -0
  26. package/dist/esm/src/managers/scene/sceneExportManager.js +10 -1
  27. package/dist/esm/src/managers/state/EffectiveVisualManager.js +266 -0
  28. package/dist/index.d.ts +85 -0
  29. package/package.json +5 -2
@@ -0,0 +1,352 @@
1
+ import { inherits as _inherits, createClass as _createClass, classCallCheck as _classCallCheck, callSuper as _callSuper, asyncToGenerator as _asyncToGenerator, regenerator as _regenerator, createForOfIteratorHelper as _createForOfIteratorHelper } from '../../../_virtual/_rollupPluginBabelHelpers.js';
2
+ import * as THREE from 'three';
3
+ import { BaseDisposable } from '../../core/baseDisposable.js';
4
+ import modelPreloader from '../../rendering/modelPreloader.js';
5
+ import { attachIODevicesToComponent } from '../../utils/ioDeviceUtils.js';
6
+ import { registerBehaviorsForComponent } from '../../utils/behaviorRegistration.js';
7
+
8
+ /**
9
+ * X offsets for `count` switches evenly spaced and centered on 0.
10
+ * @param {number} count
11
+ * @param {number} spacing
12
+ * @returns {number[]}
13
+ */
14
+ function computeSwitchRowX(count, spacing) {
15
+ var start = -((count - 1) * spacing) / 2;
16
+ return Array.from({
17
+ length: count
18
+ }, function (_, i) {
19
+ return start + i * spacing;
20
+ });
21
+ }
22
+ var ControlPanelManager = /*#__PURE__*/function (_BaseDisposable) {
23
+ /**
24
+ * @param {Object} sceneViewer - The central sceneViewer hub
25
+ */
26
+ function ControlPanelManager(sceneViewer) {
27
+ var _this;
28
+ _classCallCheck(this, ControlPanelManager);
29
+ _this = _callSuper(this, ControlPanelManager);
30
+ _this.sceneViewer = sceneViewer;
31
+ _this._panel = null;
32
+ _this._labels = [];
33
+ _this._panelId = null;
34
+ _this._build = _this._build.bind(_this);
35
+ if (sceneViewer !== null && sceneViewer !== void 0 && sceneViewer.on) {
36
+ sceneViewer.on('scene-loaded', _this._build);
37
+ }
38
+ return _this;
39
+ }
40
+
41
+ /**
42
+ * Build (or rebuild) the panel from the current scene's `controlPanel` spec.
43
+ * @private
44
+ */
45
+ _inherits(ControlPanelManager, _BaseDisposable);
46
+ return _createClass(ControlPanelManager, [{
47
+ key: "_build",
48
+ value: (function () {
49
+ var _build2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
50
+ var _this$sceneViewer,
51
+ _centralPlant$importe,
52
+ _this$sceneViewer2,
53
+ _b$width,
54
+ _b$height,
55
+ _b$thickness,
56
+ _desk$tilt,
57
+ _desk$height,
58
+ _desk$legThickness,
59
+ _desk$legInset,
60
+ _spec$switchSpacing,
61
+ _this$sceneViewer3,
62
+ _spec$labelSize,
63
+ _this2 = this;
64
+ var centralPlant, spec, scene, panelId, panel, p, r, d2r, scale, b, width, depth, thickness, desk, tilt, deskHeight, legThickness, legInset, legColor, top, board, legMat, halfW, halfD, _i, _arr, ly, cornerZ, legH, yPos, _i2, _arr2, lx, leg, switches, spacing, xs, topZ, attachedDevices, componentData, switchScale, ioBehavMgr, engine, _iterator, _step, sw, current, lo, labelSize;
65
+ return _regenerator().w(function (_context) {
66
+ while (1) switch (_context.n) {
67
+ case 0:
68
+ this._teardown();
69
+ centralPlant = (_this$sceneViewer = this.sceneViewer) === null || _this$sceneViewer === void 0 ? void 0 : _this$sceneViewer.centralPlant;
70
+ spec = centralPlant === null || centralPlant === void 0 || (_centralPlant$importe = centralPlant.importedSceneData) === null || _centralPlant$importe === void 0 ? void 0 : _centralPlant$importe.controlPanel;
71
+ scene = (_this$sceneViewer2 = this.sceneViewer) === null || _this$sceneViewer2 === void 0 ? void 0 : _this$sceneViewer2.scene;
72
+ if (!(!spec || !scene)) {
73
+ _context.n = 1;
74
+ break;
75
+ }
76
+ return _context.a(2);
77
+ case 1:
78
+ panelId = spec.id || 'CONTROL-PANEL-1';
79
+ this._panelId = panelId;
80
+ panel = new THREE.Group();
81
+ panel.name = 'Control Panel';
82
+ panel.uuid = panelId;
83
+ panel.userData = {
84
+ objectType: 'component',
85
+ isControlPanel: true
86
+ };
87
+ p = spec.position || {};
88
+ panel.position.set(p.x || 0, p.y || 0, p.z || 0);
89
+ r = spec.rotation || {};
90
+ d2r = Math.PI / 180;
91
+ panel.rotation.set((r.x || 0) * d2r, (r.y || 0) * d2r, (r.z || 0) * d2r, 'XYZ');
92
+ scale = Number.isFinite(spec.scale) && spec.scale > 0 ? spec.scale : 1;
93
+ panel.scale.setScalar(scale);
94
+
95
+ // ── Drawing-desk geometry (Z-up scene) ───────────────────────────────────
96
+ // A tilted top surface (the drafting board) raised on legs. Switches +
97
+ // labels live on the tilted `top` group so they sit on the angled surface.
98
+ b = spec.board || {};
99
+ width = (_b$width = b.width) !== null && _b$width !== void 0 ? _b$width : 7; // x-extent of the top
100
+ depth = (_b$height = b.height) !== null && _b$height !== void 0 ? _b$height : 2; // y-extent of the top (spec key kept as `height`)
101
+ thickness = (_b$thickness = b.thickness) !== null && _b$thickness !== void 0 ? _b$thickness : 0.15;
102
+ desk = spec.desk || {};
103
+ tilt = ((_desk$tilt = desk.tilt) !== null && _desk$tilt !== void 0 ? _desk$tilt : 30) * d2r; // surface tilt; +raises the back (+y) edge
104
+ deskHeight = (_desk$height = desk.height) !== null && _desk$height !== void 0 ? _desk$height : 1.3; // world height of the top's center
105
+ legThickness = (_desk$legThickness = desk.legThickness) !== null && _desk$legThickness !== void 0 ? _desk$legThickness : 0.18;
106
+ legInset = (_desk$legInset = desk.legInset) !== null && _desk$legInset !== void 0 ? _desk$legInset : 0.35;
107
+ legColor = desk.legColor || '#22303c'; // Tilted top group: raised to deskHeight, rotated about X.
108
+ top = new THREE.Group();
109
+ top.position.set(0, 0, deskHeight);
110
+ top.rotation.set(tilt, 0, 0, 'XYZ');
111
+ panel.add(top);
112
+ board = new THREE.Mesh(new THREE.BoxGeometry(width, depth, thickness), new THREE.MeshStandardMaterial({
113
+ color: b.color || '#2c3e50',
114
+ roughness: 0.6,
115
+ metalness: 0.2
116
+ }));
117
+ board.userData = {
118
+ objectType: 'control-panel-board'
119
+ };
120
+ top.add(board);
121
+
122
+ // Legs: one per corner, from the ground up to the tilted top's underside.
123
+ legMat = new THREE.MeshStandardMaterial({
124
+ color: legColor,
125
+ roughness: 0.7,
126
+ metalness: 0.1
127
+ });
128
+ halfW = Math.max(0.1, width / 2 - legInset);
129
+ halfD = Math.max(0.1, depth / 2 - legInset);
130
+ for (_i = 0, _arr = [halfD, -halfD]; _i < _arr.length; _i++) {
131
+ ly = _arr[_i];
132
+ // Underside height at this front/back row after the tilt.
133
+ cornerZ = deskHeight + ly * Math.sin(tilt) - thickness / 2 * Math.cos(tilt);
134
+ legH = Math.max(0.1, cornerZ);
135
+ yPos = ly * Math.cos(tilt); // corner's y in panel space after tilt
136
+ for (_i2 = 0, _arr2 = [halfW, -halfW]; _i2 < _arr2.length; _i2++) {
137
+ lx = _arr2[_i2];
138
+ leg = new THREE.Mesh(new THREE.BoxGeometry(legThickness, legThickness, legH), legMat);
139
+ leg.position.set(lx, yPos, legH / 2);
140
+ panel.add(leg);
141
+ }
142
+ }
143
+
144
+ // ── Switch layout: a row along X on the top face (+Z) of the board ───────
145
+ switches = Array.isArray(spec.switches) ? spec.switches : [];
146
+ spacing = (_spec$switchSpacing = spec.switchSpacing) !== null && _spec$switchSpacing !== void 0 ? _spec$switchSpacing : 1.3;
147
+ xs = computeSwitchRowX(switches.length, spacing);
148
+ topZ = thickness / 2;
149
+ attachedDevices = {};
150
+ switches.forEach(function (sw, i) {
151
+ attachedDevices[sw.attachmentId] = {
152
+ deviceId: spec.deviceId,
153
+ attachmentLabel: sw.label || sw.attachmentId,
154
+ attachmentPoint: {
155
+ position: {
156
+ x: xs[i],
157
+ y: 0,
158
+ z: topZ
159
+ },
160
+ rotation: spec.switchRotation || {
161
+ x: 0,
162
+ y: 0,
163
+ z: 0
164
+ }
165
+ }
166
+ };
167
+ });
168
+ componentData = {
169
+ isSmart: true,
170
+ attachedDevices: attachedDevices
171
+ }; // Clone + tag the switch devices onto the tilted top (reuses smart-component path).
172
+ _context.n = 2;
173
+ return attachIODevicesToComponent(top, componentData, modelPreloader, panelId);
174
+ case 2:
175
+ // Optional switch-only scale (attach path always leaves devices at 1:1).
176
+ switchScale = Number.isFinite(spec.switchScale) && spec.switchScale > 0 ? spec.switchScale : 1;
177
+ if (switchScale !== 1) {
178
+ top.traverse(function (obj) {
179
+ var _obj$userData;
180
+ if (((_obj$userData = obj.userData) === null || _obj$userData === void 0 ? void 0 : _obj$userData.objectType) === 'io-device') {
181
+ obj.scale.setScalar(switchScale);
182
+ }
183
+ });
184
+ }
185
+
186
+ // Panel must be in the scene before behaviors register (default-state seeding
187
+ // scans the live scene graph for io-devices).
188
+ scene.add(panel);
189
+ this._panel = panel;
190
+ ioBehavMgr = (_this$sceneViewer3 = this.sceneViewer) === null || _this$sceneViewer3 === void 0 || (_this$sceneViewer3 = _this$sceneViewer3.managers) === null || _this$sceneViewer3 === void 0 ? void 0 : _this$sceneViewer3.ioBehaviorManager;
191
+ if (ioBehavMgr) {
192
+ registerBehaviorsForComponent(ioBehavMgr, panelId, componentData, panel, modelPreloader.componentDictionary, centralPlant);
193
+
194
+ // Drive each switch's mesh to its current engine value so the initial
195
+ // pose/colour reflects the state. `registerBehaviorsForComponent`'s
196
+ // default-seeding writes through the engine, which no-ops when the value
197
+ // already matches the seeded `stateRegistry` — leaving the button at its
198
+ // neutral GLB rest pose. Trigger the animation directly to avoid that.
199
+ engine = centralPlant === null || centralPlant === void 0 ? void 0 : centralPlant.stateEngine;
200
+ _iterator = _createForOfIteratorHelper(switches);
201
+ try {
202
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
203
+ sw = _step.value;
204
+ current = engine === null || engine === void 0 ? void 0 : engine.getDesired("".concat(panelId, "::").concat(sw.attachmentId, "::mode"));
205
+ ioBehavMgr.triggerState(sw.attachmentId, 'mode', current !== null && current !== void 0 ? current : true, panelId);
206
+ }
207
+ } catch (err) {
208
+ _iterator.e(err);
209
+ } finally {
210
+ _iterator.f();
211
+ }
212
+ }
213
+
214
+ // ── Floating labels above each switch (on the tilted top) ────────────────
215
+ lo = spec.labelOffset || {
216
+ x: 0,
217
+ y: 0,
218
+ z: 0.7
219
+ };
220
+ labelSize = (_spec$labelSize = spec.labelSize) !== null && _spec$labelSize !== void 0 ? _spec$labelSize : 0.5;
221
+ switches.forEach(function (sw, i) {
222
+ var _lo$z;
223
+ var label = _this2._makeLabel(sw.label || sw.attachmentId, labelSize);
224
+ if (!label) return;
225
+ label.position.set(xs[i] + (lo.x || 0), lo.y || 0, topZ + ((_lo$z = lo.z) !== null && _lo$z !== void 0 ? _lo$z : 0.7));
226
+ top.add(label);
227
+ _this2._labels.push(label);
228
+ });
229
+ case 3:
230
+ return _context.a(2);
231
+ }
232
+ }, _callee, this);
233
+ }));
234
+ function _build() {
235
+ return _build2.apply(this, arguments);
236
+ }
237
+ return _build;
238
+ }()
239
+ /**
240
+ * Build one text label as a camera-facing `THREE.Sprite` (canvas texture),
241
+ * drawn by the main WebGL renderer alongside the scene — no dependency on the
242
+ * tooltip CSS2D renderer. Returns null when there is no DOM (headless tests).
243
+ * @param {string} text
244
+ * @param {number} worldHeight - label height in world units
245
+ * @returns {THREE.Sprite|null}
246
+ * @private
247
+ */
248
+ )
249
+ }, {
250
+ key: "_makeLabel",
251
+ value: function _makeLabel(text, worldHeight) {
252
+ if (typeof document === 'undefined') return null;
253
+ var dpr = 4; // supersample for crisp text
254
+ var fontPx = 40;
255
+ var padX = 14;
256
+ var padY = 8;
257
+ var font = "600 ".concat(fontPx, "px -apple-system, \"Segoe UI\", Roboto, sans-serif");
258
+ var canvas = document.createElement('canvas');
259
+ var ctx = canvas.getContext('2d');
260
+ ctx.font = font;
261
+ var textW = ctx.measureText(text).width;
262
+ canvas.width = Math.ceil((textW + padX * 2) * dpr);
263
+ canvas.height = Math.ceil((fontPx + padY * 2) * dpr);
264
+
265
+ // Re-apply after the resize (which clears the canvas), scaled by dpr.
266
+ ctx.scale(dpr, dpr);
267
+ ctx.font = font;
268
+ var wCss = canvas.width / dpr;
269
+ var hCss = canvas.height / dpr;
270
+ ctx.fillStyle = 'rgba(20,28,38,0.86)';
271
+ this._roundRect(ctx, 0, 0, wCss, hCss, 8);
272
+ ctx.fill();
273
+ ctx.fillStyle = '#eaf2f8';
274
+ ctx.textAlign = 'center';
275
+ ctx.textBaseline = 'middle';
276
+ ctx.fillText(text, wCss / 2, hCss / 2);
277
+ var texture = new THREE.CanvasTexture(canvas);
278
+ texture.minFilter = THREE.LinearFilter;
279
+ texture.anisotropy = 4;
280
+ var material = new THREE.SpriteMaterial({
281
+ map: texture,
282
+ transparent: true,
283
+ depthTest: false,
284
+ // always readable, never occluded by the board/switch
285
+ depthWrite: false
286
+ });
287
+ var sprite = new THREE.Sprite(material);
288
+ var aspect = canvas.width / canvas.height;
289
+ sprite.scale.set(worldHeight * aspect, worldHeight, 1);
290
+ sprite.renderOrder = 999;
291
+ return sprite;
292
+ }
293
+
294
+ /** Trace a rounded-rect path (fill applied by caller). @private */
295
+ }, {
296
+ key: "_roundRect",
297
+ value: function _roundRect(ctx, x, y, w, h, r) {
298
+ var rr = Math.min(r, w / 2, h / 2);
299
+ ctx.beginPath();
300
+ ctx.moveTo(x + rr, y);
301
+ ctx.arcTo(x + w, y, x + w, y + h, rr);
302
+ ctx.arcTo(x + w, y + h, x, y + h, rr);
303
+ ctx.arcTo(x, y + h, x, y, rr);
304
+ ctx.arcTo(x, y, x + w, y, rr);
305
+ ctx.closePath();
306
+ }
307
+
308
+ /**
309
+ * Remove the panel, its labels, and its registered behaviors (scene reload).
310
+ * @private
311
+ */
312
+ }, {
313
+ key: "_teardown",
314
+ value: function _teardown() {
315
+ this._labels = [];
316
+ if (this._panelId) {
317
+ var _this$sceneViewer4;
318
+ (_this$sceneViewer4 = this.sceneViewer) === null || _this$sceneViewer4 === void 0 || (_this$sceneViewer4 = _this$sceneViewer4.managers) === null || _this$sceneViewer4 === void 0 || (_this$sceneViewer4 = _this$sceneViewer4.ioBehaviorManager) === null || _this$sceneViewer4 === void 0 || _this$sceneViewer4.unloadForComponent(this._panelId);
319
+ }
320
+ if (this._panel) {
321
+ var _this$sceneViewer5;
322
+ (_this$sceneViewer5 = this.sceneViewer) === null || _this$sceneViewer5 === void 0 || (_this$sceneViewer5 = _this$sceneViewer5.scene) === null || _this$sceneViewer5 === void 0 || _this$sceneViewer5.remove(this._panel);
323
+ this._panel.traverse(function (obj) {
324
+ if (obj.isMesh || obj.isSprite) {
325
+ var _obj$geometry, _obj$geometry$dispose;
326
+ (_obj$geometry = obj.geometry) === null || _obj$geometry === void 0 || (_obj$geometry$dispose = _obj$geometry.dispose) === null || _obj$geometry$dispose === void 0 || _obj$geometry$dispose.call(_obj$geometry);
327
+ var mats = Array.isArray(obj.material) ? obj.material : [obj.material];
328
+ mats.forEach(function (m) {
329
+ var _m$map, _m$map$dispose, _m$dispose;
330
+ m === null || m === void 0 || (_m$map = m.map) === null || _m$map === void 0 || (_m$map$dispose = _m$map.dispose) === null || _m$map$dispose === void 0 || _m$map$dispose.call(_m$map); // sprite canvas texture
331
+ m === null || m === void 0 || (_m$dispose = m.dispose) === null || _m$dispose === void 0 || _m$dispose.call(m);
332
+ });
333
+ }
334
+ });
335
+ this._panel = null;
336
+ }
337
+ this._panelId = null;
338
+ }
339
+ }, {
340
+ key: "_doDispose",
341
+ value: function _doDispose() {
342
+ var _this$sceneViewer6;
343
+ if ((_this$sceneViewer6 = this.sceneViewer) !== null && _this$sceneViewer6 !== void 0 && _this$sceneViewer6.off) {
344
+ this.sceneViewer.off('scene-loaded', this._build);
345
+ }
346
+ this._teardown();
347
+ this.sceneViewer = null;
348
+ }
349
+ }]);
350
+ }(BaseDisposable);
351
+
352
+ export { ControlPanelManager, computeSwitchRowX };
@@ -2,6 +2,7 @@ import { createClass as _createClass, classCallCheck as _classCallCheck, toConsu
2
2
  import 'three';
3
3
  import { GLTFExporter } from '../../../node_modules/three/examples/jsm/exporters/GLTFExporter.js';
4
4
  import { getHardcodedUuid } from '../../utils/nameUtils.js';
5
+ import { applyDeclarationToExport } from '../../core/stateEngineScene.js';
5
6
 
6
7
  var SceneExportManager = /*#__PURE__*/function () {
7
8
  function SceneExportManager(sceneViewer) {
@@ -56,7 +57,8 @@ var SceneExportManager = /*#__PURE__*/function () {
56
57
  key: "exportSceneData",
57
58
  value: function exportSceneData() {
58
59
  var _this = this,
59
- _this$sceneViewer$cur2;
60
+ _this$sceneViewer$cur2,
61
+ _this$sceneViewer$cen;
60
62
  console.log('📤 Starting scene export...');
61
63
  if (!this.sceneViewer.scene) {
62
64
  console.warn('⚠️ No scene available for export');
@@ -289,6 +291,13 @@ var SceneExportManager = /*#__PURE__*/function () {
289
291
  if (sceneBehaviors && sceneBehaviors.length > 0) {
290
292
  exportData.behaviors = sceneBehaviors;
291
293
  }
294
+
295
+ // Persist plant / groups / chains / stateRegistry from the State Engine and
296
+ // bump the format to 3.0 (Framework §12; no-op if no engine present).
297
+ var stateEngine = (_this$sceneViewer$cen = this.sceneViewer.centralPlant) === null || _this$sceneViewer$cen === void 0 ? void 0 : _this$sceneViewer$cen.stateEngine;
298
+ if (stateEngine) {
299
+ applyDeclarationToExport(exportData, stateEngine);
300
+ }
292
301
  console.log('✅ Scene export completed:', exportData);
293
302
  console.log("\uD83D\uDCCA Exported ".concat(sceneChildren.length, " components and ").concat(exportData.connections.length, " connections"));
294
303
  return exportData;
@@ -0,0 +1,266 @@
1
+ import { inherits as _inherits, createClass as _createClass, slicedToArray as _slicedToArray, createForOfIteratorHelper as _createForOfIteratorHelper, classCallCheck as _classCallCheck, callSuper as _callSuper } from '../../../_virtual/_rollupPluginBabelHelpers.js';
2
+ import { BaseDisposable } from '../../core/baseDisposable.js';
3
+ import { findObjectByHardcodedUuid } from '../../utils/nameUtils.js';
4
+
5
+ /** How far the albedo is pulled toward black for a gated-off mesh. */
6
+ var OFF_COLOR_SCALE = 0.2;
7
+ var EffectiveVisualManager = /*#__PURE__*/function (_BaseDisposable) {
8
+ /**
9
+ * @param {Object} sceneViewer - The central sceneViewer hub
10
+ */
11
+ function EffectiveVisualManager(sceneViewer) {
12
+ var _this;
13
+ _classCallCheck(this, EffectiveVisualManager);
14
+ _this = _callSuper(this, EffectiveVisualManager);
15
+ _this.sceneViewer = sceneViewer;
16
+
17
+ /**
18
+ * Materials this manager has gated (cloned + cached), for restore on dispose.
19
+ * @type {Set<THREE.Material>}
20
+ */
21
+ _this._gatedMaterials = new Set();
22
+
23
+ // Bound handlers so we can unsubscribe in _doDispose().
24
+ _this._onChange = _this._onChange.bind(_this);
25
+ _this._regateAll = _this._regateAll.bind(_this);
26
+ if (sceneViewer !== null && sceneViewer !== void 0 && sceneViewer.on) {
27
+ sceneViewer.on('engine-state-changed', _this._onChange);
28
+ sceneViewer.on('scene-loaded', _this._regateAll);
29
+ }
30
+ return _this;
31
+ }
32
+
33
+ // ── Engine change handling ──────────────────────────────────────────────────
34
+
35
+ /**
36
+ * React to one committed registry change. Only 2-part object / pipe addresses
37
+ * are gated; everything else is ignored.
38
+ * @param {{ address: string, desired: *, effective: * }} change
39
+ * @private
40
+ */
41
+ _inherits(EffectiveVisualManager, _BaseDisposable);
42
+ return _createClass(EffectiveVisualManager, [{
43
+ key: "_onChange",
44
+ value: function _onChange(change) {
45
+ if (!change || typeof change.address !== 'string') return;
46
+ var parts = change.address.split('::');
47
+ if (parts.length !== 2) return; // 3-part I/O is desired-driven; skip
48
+
49
+ var _parts = _slicedToArray(parts, 2),
50
+ scope = _parts[0],
51
+ attr = _parts[1];
52
+ if (scope === 'plant' || scope.startsWith('group:')) return; // members self-report
53
+
54
+ if (attr === 'power') {
55
+ this._gateEquipment(scope, change.effective);
56
+ } else if (attr === 'flow') {
57
+ this._gatePipe(scope, change.effective);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Re-gate the whole scene from current effective state. Used after scene load,
63
+ * where StateEngine.load() populates the registry without emitting per-address
64
+ * events (and the meshes have just been (re)built).
65
+ * @private
66
+ */
67
+ }, {
68
+ key: "_regateAll",
69
+ value: function _regateAll() {
70
+ var _this$sceneViewer;
71
+ var engine = (_this$sceneViewer = this.sceneViewer) === null || _this$sceneViewer === void 0 || (_this$sceneViewer = _this$sceneViewer.centralPlant) === null || _this$sceneViewer === void 0 ? void 0 : _this$sceneViewer.stateEngine;
72
+ if (!engine) return;
73
+ var registry = engine.toRegistry();
74
+ for (var _i = 0, _Object$keys = Object.keys(registry); _i < _Object$keys.length; _i++) {
75
+ var address = _Object$keys[_i];
76
+ var parts = address.split('::');
77
+ if (parts.length !== 2) continue;
78
+ var _parts2 = _slicedToArray(parts, 2),
79
+ scope = _parts2[0],
80
+ attr = _parts2[1];
81
+ if (scope === 'plant' || scope.startsWith('group:')) continue;
82
+ if (attr === 'power') {
83
+ this._gateEquipment(scope, engine.getEffective(address));
84
+ } else if (attr === 'flow') {
85
+ this._gatePipe(scope, engine.getEffective(address));
86
+ }
87
+ }
88
+ }
89
+
90
+ // ── Equipment gating ────────────────────────────────────────────────────────
91
+
92
+ /**
93
+ * Darken (off) or restore (on) every mesh of an equipment object.
94
+ * @param {string} objectId - Hardcoded object id (address scope)
95
+ * @param {boolean} on - Effective power
96
+ * @private
97
+ */
98
+ }, {
99
+ key: "_gateEquipment",
100
+ value: function _gateEquipment(objectId, on) {
101
+ var _this$sceneViewer2,
102
+ _this2 = this;
103
+ var scene = (_this$sceneViewer2 = this.sceneViewer) === null || _this$sceneViewer2 === void 0 ? void 0 : _this$sceneViewer2.scene;
104
+ if (!scene) return;
105
+ var obj = findObjectByHardcodedUuid(scene, objectId);
106
+ if (!obj || typeof obj.traverse !== 'function') return;
107
+ obj.traverse(function (mesh) {
108
+ if (!mesh.isMesh && !mesh.material) return;
109
+ // Leave attached I/O control panels (switches) lit and operable even when
110
+ // their host is powered off — you must be able to see and click the switch
111
+ // that turns it back on. (traverse can't prune, so guard per mesh.)
112
+ if (_this2._isUnderIoDevice(mesh, obj)) return;
113
+ var materials = _this2._prepareMeshMaterials(mesh);
114
+ var _iterator = _createForOfIteratorHelper(materials),
115
+ _step;
116
+ try {
117
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
118
+ var material = _step.value;
119
+ _this2._applyGate(material, on);
120
+ }
121
+ } catch (err) {
122
+ _iterator.e(err);
123
+ } finally {
124
+ _iterator.f();
125
+ }
126
+ });
127
+ }
128
+
129
+ /**
130
+ * True if `mesh` is an io-device or nested under one, walking up to (but not
131
+ * past) the equipment root `stopAt`.
132
+ * @param {THREE.Object3D} mesh
133
+ * @param {THREE.Object3D} stopAt - equipment root to stop the upward walk at
134
+ * @returns {boolean}
135
+ * @private
136
+ */
137
+ }, {
138
+ key: "_isUnderIoDevice",
139
+ value: function _isUnderIoDevice(mesh, stopAt) {
140
+ var node = mesh;
141
+ while (node) {
142
+ var _node$userData;
143
+ if (((_node$userData = node.userData) === null || _node$userData === void 0 ? void 0 : _node$userData.objectType) === 'io-device') return true;
144
+ if (node === stopAt) break;
145
+ node = node.parent;
146
+ }
147
+ return false;
148
+ }
149
+
150
+ /**
151
+ * Ensure a mesh's material(s) are cloned-on-write and have their baseline
152
+ * cached, then return the material list to gate.
153
+ * @param {THREE.Mesh} mesh
154
+ * @returns {THREE.Material[]}
155
+ * @private
156
+ */
157
+ }, {
158
+ key: "_prepareMeshMaterials",
159
+ value: function _prepareMeshMaterials(mesh) {
160
+ if (!mesh.material) return [];
161
+
162
+ // Clone-on-write once per mesh so shared library materials aren't shared.
163
+ if (!mesh.userData._effectiveGateCloned) {
164
+ if (Array.isArray(mesh.material)) {
165
+ mesh.material = mesh.material.map(function (m) {
166
+ return m !== null && m !== void 0 && m.clone ? m.clone() : m;
167
+ });
168
+ } else if (mesh.material.clone) {
169
+ mesh.material = mesh.material.clone();
170
+ }
171
+ mesh.userData._effectiveGateCloned = true;
172
+ }
173
+ var materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
174
+ var _iterator2 = _createForOfIteratorHelper(materials),
175
+ _step2;
176
+ try {
177
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
178
+ var _material$userData, _material$color$clone, _material$color, _material$color$clone2, _material$emissive$cl, _material$emissive, _material$emissive$cl2;
179
+ var material = _step2.value;
180
+ if (!material || (_material$userData = material.userData) !== null && _material$userData !== void 0 && _material$userData._effectiveGate) continue;
181
+ material.userData = material.userData || {};
182
+ material.userData._effectiveGate = {
183
+ color: (_material$color$clone = (_material$color = material.color) === null || _material$color === void 0 || (_material$color$clone2 = _material$color.clone) === null || _material$color$clone2 === void 0 ? void 0 : _material$color$clone2.call(_material$color)) !== null && _material$color$clone !== void 0 ? _material$color$clone : null,
184
+ emissive: (_material$emissive$cl = (_material$emissive = material.emissive) === null || _material$emissive === void 0 || (_material$emissive$cl2 = _material$emissive.clone) === null || _material$emissive$cl2 === void 0 ? void 0 : _material$emissive$cl2.call(_material$emissive)) !== null && _material$emissive$cl !== void 0 ? _material$emissive$cl : null,
185
+ emissiveIntensity: typeof material.emissiveIntensity === 'number' ? material.emissiveIntensity : null
186
+ };
187
+ this._gatedMaterials.add(material);
188
+ }
189
+ } catch (err) {
190
+ _iterator2.e(err);
191
+ } finally {
192
+ _iterator2.f();
193
+ }
194
+ return materials.filter(Boolean);
195
+ }
196
+
197
+ /**
198
+ * Darken or restore a single material from its cached baseline.
199
+ * @param {THREE.Material} material
200
+ * @param {boolean} on
201
+ * @private
202
+ */
203
+ }, {
204
+ key: "_applyGate",
205
+ value: function _applyGate(material, on) {
206
+ var _material$userData2;
207
+ var base = (_material$userData2 = material.userData) === null || _material$userData2 === void 0 ? void 0 : _material$userData2._effectiveGate;
208
+ if (!base) return;
209
+ if (on) {
210
+ if (base.color && material.color) material.color.copy(base.color);
211
+ if (base.emissive && material.emissive) material.emissive.copy(base.emissive);
212
+ if (base.emissiveIntensity !== null) material.emissiveIntensity = base.emissiveIntensity;
213
+ } else {
214
+ if (base.color && material.color) material.color.copy(base.color).multiplyScalar(OFF_COLOR_SCALE);
215
+ if (material.emissive) material.emissive.setScalar(0);
216
+ if (typeof material.emissiveIntensity === 'number') material.emissiveIntensity = 0;
217
+ }
218
+ material.needsUpdate = true;
219
+ }
220
+
221
+ // ── Pipe flow gating ────────────────────────────────────────────────────────
222
+
223
+ /**
224
+ * Delegate pipe-flow gating to PathFlowManager (grays the shared per-path
225
+ * material when off, restores the temperature colour when on).
226
+ * @param {string} pathId - `${from}-->${to}`
227
+ * @param {boolean} on - Effective flow
228
+ * @private
229
+ */
230
+ }, {
231
+ key: "_gatePipe",
232
+ value: function _gatePipe(pathId, on) {
233
+ var _this$sceneViewer3, _pathFlowManager$setF;
234
+ var pathFlowManager = (_this$sceneViewer3 = this.sceneViewer) === null || _this$sceneViewer3 === void 0 || (_this$sceneViewer3 = _this$sceneViewer3.managers) === null || _this$sceneViewer3 === void 0 ? void 0 : _this$sceneViewer3.pathFlowManager;
235
+ pathFlowManager === null || pathFlowManager === void 0 || (_pathFlowManager$setF = pathFlowManager.setFlowEnabled) === null || _pathFlowManager$setF === void 0 || _pathFlowManager$setF.call(pathFlowManager, pathId, on);
236
+ }
237
+
238
+ // ── Lifecycle ───────────────────────────────────────────────────────────────
239
+ }, {
240
+ key: "_doDispose",
241
+ value: function _doDispose() {
242
+ var _this$sceneViewer4;
243
+ if ((_this$sceneViewer4 = this.sceneViewer) !== null && _this$sceneViewer4 !== void 0 && _this$sceneViewer4.off) {
244
+ this.sceneViewer.off('engine-state-changed', this._onChange);
245
+ this.sceneViewer.off('scene-loaded', this._regateAll);
246
+ }
247
+ // Restore any still-gated materials to their cached baseline.
248
+ var _iterator3 = _createForOfIteratorHelper(this._gatedMaterials),
249
+ _step3;
250
+ try {
251
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
252
+ var material = _step3.value;
253
+ this._applyGate(material, true);
254
+ }
255
+ } catch (err) {
256
+ _iterator3.e(err);
257
+ } finally {
258
+ _iterator3.f();
259
+ }
260
+ this._gatedMaterials.clear();
261
+ this.sceneViewer = null;
262
+ }
263
+ }]);
264
+ }(BaseDisposable);
265
+
266
+ export { EffectiveVisualManager };