@2112-lab/central-plant 0.3.58 → 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 (27) hide show
  1. package/dist/bundle/index.js +1635 -32
  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/pathfinding/PathFlowManager.js +94 -13
  9. package/dist/cjs/src/managers/pathfinding/PathRenderingManager.js +15 -0
  10. package/dist/cjs/src/managers/scene/componentTooltipManager.js +7 -1
  11. package/dist/cjs/src/managers/scene/controlPanelManager.js +377 -0
  12. package/dist/cjs/src/managers/scene/sceneExportManager.js +10 -1
  13. package/dist/cjs/src/managers/state/EffectiveVisualManager.js +270 -0
  14. package/dist/esm/src/core/centralPlant.js +80 -11
  15. package/dist/esm/src/core/centralPlantInternals.js +4 -0
  16. package/dist/esm/src/core/centralPlantValidator.js +10 -6
  17. package/dist/esm/src/core/stateEngine.js +725 -0
  18. package/dist/esm/src/core/stateEngineScene.js +90 -0
  19. package/dist/esm/src/index.js +2 -0
  20. package/dist/esm/src/managers/pathfinding/PathFlowManager.js +94 -13
  21. package/dist/esm/src/managers/pathfinding/PathRenderingManager.js +15 -0
  22. package/dist/esm/src/managers/scene/componentTooltipManager.js +7 -1
  23. package/dist/esm/src/managers/scene/controlPanelManager.js +352 -0
  24. package/dist/esm/src/managers/scene/sceneExportManager.js +10 -1
  25. package/dist/esm/src/managers/state/EffectiveVisualManager.js +266 -0
  26. package/dist/index.d.ts +85 -0
  27. package/package.json +5 -2
@@ -2143,20 +2143,24 @@ var CentralPlantValidator = /*#__PURE__*/function () {
2143
2143
  key: "validateImportedSceneData",
2144
2144
  value: function validateImportedSceneData(sceneData) {
2145
2145
  var results = [];
2146
- var hardcodedVersion = "2.3";
2147
2146
 
2148
- // Version validation - MUST be the value of hardcodedVersion
2149
- if (!sceneData.version || sceneData.version !== hardcodedVersion) {
2147
+ // Supported scene formats: 2.3 (legacy I/O behaviors) and 3.0 (State Engine:
2148
+ // adds plant/groups/chains/stateRegistry). Both load; older scenes keep working.
2149
+ var supportedVersions = ["2.3", "3.0"];
2150
+
2151
+ // Version validation - MUST be one of the supported versions
2152
+ if (!sceneData.version || !supportedVersions.includes(sceneData.version)) {
2150
2153
  var providedVersion = sceneData.version || 'unknown';
2151
- alert("The central-plant requires the input file to be version ".concat(hardcodedVersion));
2154
+ var supportedList = supportedVersions.join(' or ');
2155
+ alert("The central-plant requires the input file to be version ".concat(supportedList));
2152
2156
  results.push({
2153
2157
  isValid: false,
2154
2158
  severity: ValidationSeverity.ERROR,
2155
- message: "Invalid version: \"".concat(providedVersion, "\". Required version is \"").concat(hardcodedVersion, "\""),
2159
+ message: "Invalid version: \"".concat(providedVersion, "\". Supported versions are ").concat(supportedList),
2156
2160
  code: 'INVALID_VERSION',
2157
2161
  metadata: {
2158
2162
  providedVersion: providedVersion,
2159
- requiredVersion: hardcodedVersion
2163
+ supportedVersions: supportedVersions
2160
2164
  }
2161
2165
  });
2162
2166
  return this._combineValidationResults(results, 'scene_import_validation');
@@ -12114,6 +12118,813 @@ function generateUniqueComponentId(libraryId, scene) {
12114
12118
  return id;
12115
12119
  }
12116
12120
 
12121
+ /**
12122
+ * @file stateEngine.js
12123
+ * @description Portable State Engine core — registry, desired/effective resolution,
12124
+ * plant (and later group) gates, change notifications. No Vue/Three.js.
12125
+ *
12126
+ * Phase 1 scaffold per STATE_ENGINE_FRAMEWORK_0.3.md / milestones doc.
12127
+ */
12128
+
12129
+ /**
12130
+ * @typedef {'desired' | 'effective'} StateLayer
12131
+ */
12132
+
12133
+ /**
12134
+ * Build a scoped registry address.
12135
+ * @param {string} scope - e.g. "plant", "group:chiller-loop-a", "pump-001"
12136
+ * @param {string} attribute
12137
+ * @returns {string}
12138
+ */
12139
+ function makeAddress(scope, attribute) {
12140
+ return "".concat(scope, "::").concat(attribute);
12141
+ }
12142
+
12143
+ /**
12144
+ * Shallow equality for registry values (boolean, number, string, null).
12145
+ * @param {*} a
12146
+ * @param {*} b
12147
+ * @returns {boolean}
12148
+ */
12149
+ function valuesEqual(a, b) {
12150
+ return Object.is(a, b);
12151
+ }
12152
+
12153
+ /**
12154
+ * Safe-off value for a gated operational attribute: 0 for numbers, false for
12155
+ * booleans, otherwise the value unchanged.
12156
+ * @param {*} desired
12157
+ * @returns {*}
12158
+ */
12159
+ function offValue(desired) {
12160
+ if (typeof desired === 'number') return 0;
12161
+ if (typeof desired === 'boolean') return false;
12162
+ return desired;
12163
+ }
12164
+
12165
+ /**
12166
+ * Central Plant State Engine (logic only).
12167
+ *
12168
+ * Desired values are stored in the registry. Effective values are computed
12169
+ * from gates (plant / groups / object) without erasing desired.
12170
+ */
12171
+ var StateEngine = /*#__PURE__*/function () {
12172
+ /**
12173
+ * @param {object} [options]
12174
+ * @param {number} [options.maxChainDepth=8] - Ripple hop limit (Phase 3)
12175
+ */
12176
+ function StateEngine() {
12177
+ var _options$maxChainDept, _options$gatedOperati;
12178
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
12179
+ _classCallCheck(this, StateEngine);
12180
+ /** @type {Map<string, *>} address → desired value */
12181
+ this._desired = new Map();
12182
+
12183
+ /** @type {Map<string, Set<string>>} groupId → member object ids */
12184
+ this._groupMembers = new Map();
12185
+
12186
+ /** @type {Map<string, *>} groupId → group desired power (and later other attrs) */
12187
+ this._groupDesired = new Map();
12188
+
12189
+ /** @type {boolean} */
12190
+ this._plantPowerDesired = true;
12191
+
12192
+ /** @type {Set<(change: { address: string, desired: *, effective: * }) => void>} */
12193
+ this._listeners = new Set();
12194
+
12195
+ /** @type {number} */
12196
+ this.maxChainDepth = (_options$maxChainDept = options.maxChainDepth) !== null && _options$maxChainDept !== void 0 ? _options$maxChainDept : 8;
12197
+
12198
+ /**
12199
+ * Operational attributes that read a safe-off value when their object's
12200
+ * power is not effectively on (Framework §6/§10: fan level 5 + plant off → 0).
12201
+ * Passive/meta attributes stay ungated.
12202
+ * @type {Set<string>}
12203
+ */
12204
+ this._gatedOperationalAttributes = new Set((_options$gatedOperati = options.gatedOperationalAttributes) !== null && _options$gatedOperati !== void 0 ? _options$gatedOperati : ['fan-level', 'rpm', 'level', 'speed']);
12205
+
12206
+ /** @type {Array<object>} declared chains (Phase 3) */
12207
+ this._chains = [];
12208
+
12209
+ /** @type {boolean} */
12210
+ this._batching = false;
12211
+
12212
+ /** @type {Map<string, { address: string, desired: *, effective: * }>} */
12213
+ this._pendingNotifications = new Map();
12214
+ }
12215
+
12216
+ // ─── Load / reset ─────────────────────────────────────────────────────────
12217
+
12218
+ /**
12219
+ * Load plant, groups, and initial desired values from a scene-like declaration.
12220
+ * @param {object} declaration
12221
+ * @param {object} [declaration.plant]
12222
+ * @param {object} [declaration.groups]
12223
+ * @param {object} [declaration.stateRegistry]
12224
+ * @param {Array<object>} [declaration.chains]
12225
+ */
12226
+ return _createClass(StateEngine, [{
12227
+ key: "load",
12228
+ value: function load() {
12229
+ var _declaration$plant;
12230
+ var declaration = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
12231
+ this._desired.clear();
12232
+ this._groupMembers.clear();
12233
+ this._groupDesired.clear();
12234
+ this._chains = Array.isArray(declaration.chains) ? _toConsumableArray(declaration.chains) : [];
12235
+ var plantDefault = ((_declaration$plant = declaration.plant) === null || _declaration$plant === void 0 || (_declaration$plant = _declaration$plant.states) === null || _declaration$plant === void 0 || (_declaration$plant = _declaration$plant.power) === null || _declaration$plant === void 0 ? void 0 : _declaration$plant.default) !== undefined ? declaration.plant.states.power.default : true;
12236
+ this._plantPowerDesired = Boolean(plantDefault);
12237
+ this._desired.set(makeAddress('plant', 'power'), this._plantPowerDesired);
12238
+ var groups = declaration.groups || {};
12239
+ for (var _i = 0, _Object$entries = Object.entries(groups); _i < _Object$entries.length; _i++) {
12240
+ var _group$states;
12241
+ var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2),
12242
+ groupId = _Object$entries$_i[0],
12243
+ group = _Object$entries$_i[1];
12244
+ var members = Array.isArray(group.members) ? group.members : [];
12245
+ this._groupMembers.set(groupId, new Set(members));
12246
+ var groupPower = ((_group$states = group.states) === null || _group$states === void 0 || (_group$states = _group$states.power) === null || _group$states === void 0 ? void 0 : _group$states.default) !== undefined ? group.states.power.default : true;
12247
+ this._groupDesired.set(groupId, Boolean(groupPower));
12248
+ this._desired.set(makeAddress("group:".concat(groupId), 'power'), Boolean(groupPower));
12249
+ }
12250
+ var registry = declaration.stateRegistry || {};
12251
+ for (var _i2 = 0, _Object$entries2 = Object.entries(registry); _i2 < _Object$entries2.length; _i2++) {
12252
+ var _Object$entries2$_i = _slicedToArray(_Object$entries2[_i2], 2),
12253
+ address = _Object$entries2$_i[0],
12254
+ value = _Object$entries2$_i[1];
12255
+ this._desired.set(address, value);
12256
+ if (address === makeAddress('plant', 'power')) {
12257
+ this._plantPowerDesired = Boolean(value);
12258
+ }
12259
+ var groupMatch = /^group:([^:]+)::power$/.exec(address);
12260
+ if (groupMatch) {
12261
+ this._groupDesired.set(groupMatch[1], Boolean(value));
12262
+ }
12263
+ }
12264
+ }
12265
+ }, {
12266
+ key: "reset",
12267
+ value: function reset() {
12268
+ this._desired.clear();
12269
+ this._groupMembers.clear();
12270
+ this._groupDesired.clear();
12271
+ this._chains = [];
12272
+ this._plantPowerDesired = true;
12273
+ this._pendingNotifications.clear();
12274
+ this._batching = false;
12275
+ }
12276
+
12277
+ // ─── Desired / effective ──────────────────────────────────────────────────
12278
+
12279
+ /**
12280
+ * @param {string} address
12281
+ * @returns {*}
12282
+ */
12283
+ }, {
12284
+ key: "getDesired",
12285
+ value: function getDesired(address) {
12286
+ return this._desired.has(address) ? this._desired.get(address) : undefined;
12287
+ }
12288
+
12289
+ /**
12290
+ * Compute effective value for an address.
12291
+ * Power-style gates: plant AND all containing groups AND object desired.
12292
+ * Non-power attributes: return desired when power gates allow, else a safe off value.
12293
+ *
12294
+ * @param {string} address
12295
+ * @returns {*}
12296
+ */
12297
+ }, {
12298
+ key: "getEffective",
12299
+ value: function getEffective(address) {
12300
+ var desired = this.getDesired(address);
12301
+ if (desired === undefined) return undefined;
12302
+ if (address === makeAddress('plant', 'power')) {
12303
+ return Boolean(desired);
12304
+ }
12305
+ var groupPowerMatch = /^group:([^:]+)::power$/.exec(address);
12306
+ if (groupPowerMatch) {
12307
+ return this._plantPowerDesired && Boolean(desired);
12308
+ }
12309
+ var objectPowerMatch = /^([^:]+)::power$/.exec(address);
12310
+ if (objectPowerMatch && !address.startsWith('group:') && !address.startsWith('plant::')) {
12311
+ var objectId = objectPowerMatch[1];
12312
+ // Skip nested I/O addresses like pump-001::start-switch::mode
12313
+ if (address.split('::').length !== 2) {
12314
+ return desired;
12315
+ }
12316
+ return this._isPowerAllowedForObject(objectId) && Boolean(desired);
12317
+ }
12318
+
12319
+ // Flow and other attrs: when tied to an object id prefix, gate on that object's power path
12320
+ var parts = address.split('::');
12321
+ if (parts.length === 2) {
12322
+ var _parts = _slicedToArray(parts, 2),
12323
+ _objectId = _parts[0],
12324
+ attribute = _parts[1];
12325
+ if (attribute === 'flow') {
12326
+ var powerAllowed = this._isPowerAllowedForObject(_objectId);
12327
+ if (!powerAllowed) return false;
12328
+ return Boolean(desired);
12329
+ }
12330
+ // Operational values (fan level, rpm, …) collapse to a safe-off value when
12331
+ // the object's power is not effectively on; restored when power returns (§6, §10).
12332
+ if (this._gatedOperationalAttributes.has(attribute)) {
12333
+ if (!this._isPowerAllowedForObject(_objectId)) return offValue(desired);
12334
+ return desired;
12335
+ }
12336
+ }
12337
+ return desired;
12338
+ }
12339
+
12340
+ /**
12341
+ * @param {string} objectId
12342
+ * @returns {boolean}
12343
+ */
12344
+ }, {
12345
+ key: "_isPowerAllowedForObject",
12346
+ value: function _isPowerAllowedForObject(objectId) {
12347
+ if (!this._plantPowerDesired) return false;
12348
+ var _iterator = _createForOfIteratorHelper(this._groupMembers),
12349
+ _step;
12350
+ try {
12351
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
12352
+ var _step$value = _slicedToArray(_step.value, 2),
12353
+ groupId = _step$value[0],
12354
+ members = _step$value[1];
12355
+ if (members.has(objectId)) {
12356
+ var groupOn = this._groupDesired.get(groupId);
12357
+ if (!groupOn) return false;
12358
+ }
12359
+ }
12360
+ } catch (err) {
12361
+ _iterator.e(err);
12362
+ } finally {
12363
+ _iterator.f();
12364
+ }
12365
+ return true;
12366
+ }
12367
+
12368
+ /**
12369
+ * Groups that contain an object.
12370
+ * @param {string} objectId
12371
+ * @returns {string[]}
12372
+ */
12373
+ }, {
12374
+ key: "getGroupsForObject",
12375
+ value: function getGroupsForObject(objectId) {
12376
+ var result = [];
12377
+ var _iterator2 = _createForOfIteratorHelper(this._groupMembers),
12378
+ _step2;
12379
+ try {
12380
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
12381
+ var _step2$value = _slicedToArray(_step2.value, 2),
12382
+ groupId = _step2$value[0],
12383
+ members = _step2$value[1];
12384
+ if (members.has(objectId)) result.push(groupId);
12385
+ }
12386
+ } catch (err) {
12387
+ _iterator2.e(err);
12388
+ } finally {
12389
+ _iterator2.f();
12390
+ }
12391
+ return result;
12392
+ }
12393
+
12394
+ // ─── Writes ───────────────────────────────────────────────────────────────
12395
+
12396
+ /**
12397
+ * Set a desired value through the single write door and run one bounded
12398
+ * propagation pass: no-op stop, gate recompute, declared chains with depth
12399
+ * and visited-set guards, then batched notifications (Framework §11, §12).
12400
+ *
12401
+ * @param {string} address
12402
+ * @param {*} value
12403
+ * @returns {boolean} true if the desired value changed
12404
+ */
12405
+ }, {
12406
+ key: "setDesired",
12407
+ value: function setDesired(address, value) {
12408
+ this._beginBatch();
12409
+ var ctx = {
12410
+ visited: new Map()
12411
+ };
12412
+ var changed = this._writeHop(address, value, 0, ctx);
12413
+ this._endBatch();
12414
+ return changed;
12415
+ }
12416
+
12417
+ /**
12418
+ * One hop of a propagation pass: apply the write, then walk declared chains.
12419
+ * @param {string} address
12420
+ * @param {*} value
12421
+ * @param {number} depth
12422
+ * @param {{ visited: Map<string, *> }} ctx
12423
+ * @returns {boolean} whether this hop changed the desired value
12424
+ */
12425
+ }, {
12426
+ key: "_writeHop",
12427
+ value: function _writeHop(address, value, depth, ctx) {
12428
+ var result = this._applyWrite(address, value, ctx);
12429
+ if (!result.changed) return false;
12430
+ this._walkChains(result, depth, ctx);
12431
+ return true;
12432
+ }
12433
+
12434
+ /**
12435
+ * Atomic write door: no-op stop (§11.1), visited-set cycle guard (§11.5),
12436
+ * store desired, update gate mirrors, compute which effectives changed, and
12437
+ * queue notifications for changed addresses only.
12438
+ *
12439
+ * @param {string} address
12440
+ * @param {*} value
12441
+ * @param {{ visited: Map<string, *> }} ctx
12442
+ * @returns {{ changed: boolean, desiredChanged?: string, effectiveChanged?: Set<string> }}
12443
+ */
12444
+ }, {
12445
+ key: "_applyWrite",
12446
+ value: function _applyWrite(address, value, ctx) {
12447
+ var previous = this.getDesired(address);
12448
+ if (previous !== undefined && valuesEqual(previous, value)) {
12449
+ return {
12450
+ changed: false
12451
+ }; // §11.1 no-op stop
12452
+ }
12453
+
12454
+ // §11.5 visited-set: writing the same pair a different value this pass = cycle conflict
12455
+ if (ctx.visited.has(address) && !valuesEqual(ctx.visited.get(address), value)) {
12456
+ console.warn("[StateEngine] cycle conflict: ".concat(address, " rewritten to a different value in one pass; stopping branch"));
12457
+ return {
12458
+ changed: false
12459
+ };
12460
+ }
12461
+ ctx.visited.set(address, value);
12462
+
12463
+ // Snapshot effective for every address this write might gate, before mutating.
12464
+ var affected = this._affectedByWrite(address);
12465
+ var before = new Map();
12466
+ var _iterator3 = _createForOfIteratorHelper(affected),
12467
+ _step3;
12468
+ try {
12469
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
12470
+ var a = _step3.value;
12471
+ before.set(a, this.getEffective(a));
12472
+ }
12473
+
12474
+ // Store desired + keep plant/group gate mirrors in sync.
12475
+ } catch (err) {
12476
+ _iterator3.e(err);
12477
+ } finally {
12478
+ _iterator3.f();
12479
+ }
12480
+ this._desired.set(address, value);
12481
+ if (address === makeAddress('plant', 'power')) {
12482
+ this._plantPowerDesired = Boolean(value);
12483
+ }
12484
+ var groupMatch = /^group:([^:]+)::power$/.exec(address);
12485
+ if (groupMatch) {
12486
+ this._groupDesired.set(groupMatch[1], Boolean(value));
12487
+ }
12488
+
12489
+ // Which effectives actually changed (§11.4 dedupe: notify once, changed only).
12490
+ var effectiveChanged = new Set();
12491
+ var _iterator4 = _createForOfIteratorHelper(affected),
12492
+ _step4;
12493
+ try {
12494
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
12495
+ var _a = _step4.value;
12496
+ if (!valuesEqual(before.get(_a), this.getEffective(_a))) effectiveChanged.add(_a);
12497
+ }
12498
+ } catch (err) {
12499
+ _iterator4.e(err);
12500
+ } finally {
12501
+ _iterator4.f();
12502
+ }
12503
+ this._queueNotification(address); // desired changed
12504
+ var _iterator5 = _createForOfIteratorHelper(effectiveChanged),
12505
+ _step5;
12506
+ try {
12507
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
12508
+ var _a2 = _step5.value;
12509
+ if (_a2 !== address) this._queueNotification(_a2);
12510
+ }
12511
+ } catch (err) {
12512
+ _iterator5.e(err);
12513
+ } finally {
12514
+ _iterator5.f();
12515
+ }
12516
+ return {
12517
+ changed: true,
12518
+ desiredChanged: address,
12519
+ effectiveChanged: effectiveChanged
12520
+ };
12521
+ }
12522
+
12523
+ /**
12524
+ * Addresses whose effective value a write to `address` could change: the
12525
+ * address itself, plus gate dependents (all keys for plant power, member
12526
+ * keys for group power).
12527
+ * @param {string} address
12528
+ * @returns {string[]}
12529
+ */
12530
+ }, {
12531
+ key: "_affectedByWrite",
12532
+ value: function _affectedByWrite(address) {
12533
+ var affected = new Set([address]);
12534
+ if (address === makeAddress('plant', 'power')) {
12535
+ var _iterator6 = _createForOfIteratorHelper(this._desired.keys()),
12536
+ _step6;
12537
+ try {
12538
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
12539
+ var key = _step6.value;
12540
+ affected.add(key);
12541
+ }
12542
+ } catch (err) {
12543
+ _iterator6.e(err);
12544
+ } finally {
12545
+ _iterator6.f();
12546
+ }
12547
+ return _toConsumableArray(affected);
12548
+ }
12549
+ var groupMatch = /^group:([^:]+)::power$/.exec(address);
12550
+ if (groupMatch) {
12551
+ var members = this._groupMembers.get(groupMatch[1]);
12552
+ if (members) {
12553
+ var _iterator7 = _createForOfIteratorHelper(members),
12554
+ _step7;
12555
+ try {
12556
+ for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
12557
+ var memberId = _step7.value;
12558
+ var _iterator8 = _createForOfIteratorHelper(this._desired.keys()),
12559
+ _step8;
12560
+ try {
12561
+ for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
12562
+ var _key = _step8.value;
12563
+ if (_key.startsWith("".concat(memberId, "::"))) affected.add(_key);
12564
+ }
12565
+ } catch (err) {
12566
+ _iterator8.e(err);
12567
+ } finally {
12568
+ _iterator8.f();
12569
+ }
12570
+ }
12571
+ } catch (err) {
12572
+ _iterator7.e(err);
12573
+ } finally {
12574
+ _iterator7.f();
12575
+ }
12576
+ }
12577
+ }
12578
+ return _toConsumableArray(affected);
12579
+ }
12580
+
12581
+ /**
12582
+ * Walk declared chains triggered by this write (§10, §11.3). Desired-source
12583
+ * chains fire when the trigger's desired changed; effective-source chains fire
12584
+ * when the trigger's effective changed (so plant/group-off ripples through
12585
+ * `pump effective → pipe flow`, §17). Only `copy` is supported.
12586
+ *
12587
+ * @param {{ desiredChanged?: string, effectiveChanged?: Set<string> }} result
12588
+ * @param {number} depth
12589
+ * @param {{ visited: Map<string, *> }} ctx
12590
+ */
12591
+ }, {
12592
+ key: "_walkChains",
12593
+ value: function _walkChains(result, depth, ctx) {
12594
+ if (this._chains.length === 0) return;
12595
+ var _iterator9 = _createForOfIteratorHelper(this._chains),
12596
+ _step9;
12597
+ try {
12598
+ for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {
12599
+ var _result$effectiveChan;
12600
+ var chain = _step9.value;
12601
+ if (chain.action && chain.action !== 'copy') continue;
12602
+ var trigger = makeAddress(chain.when.address, chain.when.attribute);
12603
+ var useEffective = chain.when.source === 'effective';
12604
+ var fires = useEffective ? (_result$effectiveChan = result.effectiveChanged) === null || _result$effectiveChan === void 0 ? void 0 : _result$effectiveChan.has(trigger) : result.desiredChanged === trigger;
12605
+ if (!fires) continue;
12606
+ if (depth + 1 > this.maxChainDepth) {
12607
+ var _chain$id;
12608
+ console.warn("[StateEngine] ripple depth limit (".concat(this.maxChainDepth, ") reached at chain ") + "\"".concat((_chain$id = chain.id) !== null && _chain$id !== void 0 ? _chain$id : '<unnamed>', "\" (").concat(trigger, " \u2192 ").concat(makeAddress(chain.set.address, chain.set.attribute), "); stopping path"));
12609
+ continue;
12610
+ }
12611
+ var nextValue = useEffective ? this.getEffective(trigger) : this.getDesired(trigger);
12612
+ var target = makeAddress(chain.set.address, chain.set.attribute);
12613
+ this._writeHop(target, nextValue, depth + 1, ctx);
12614
+ }
12615
+ } catch (err) {
12616
+ _iterator9.e(err);
12617
+ } finally {
12618
+ _iterator9.f();
12619
+ }
12620
+ }
12621
+
12622
+ // ─── Pipe lifecycle (Framework §9, §15) ─────────────────────────────────────
12623
+
12624
+ /**
12625
+ * Register a pipe/path in the registry so its flow state exists and is tied to
12626
+ * pipe lifecycle (no orphan keys). Keyed on the pipe/path id, never on segment
12627
+ * declared/computed status (§9). No-op if already registered.
12628
+ * @param {string} pipeId
12629
+ * @param {object} [options]
12630
+ * @param {boolean} [options.defaultFlow=false]
12631
+ * @returns {string} the flow address
12632
+ */
12633
+ }, {
12634
+ key: "registerPipe",
12635
+ value: function registerPipe(pipeId) {
12636
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
12637
+ var address = makeAddress(pipeId, 'flow');
12638
+ if (!this._desired.has(address)) {
12639
+ var _options$defaultFlow;
12640
+ this._beginBatch();
12641
+ this._desired.set(address, Boolean((_options$defaultFlow = options.defaultFlow) !== null && _options$defaultFlow !== void 0 ? _options$defaultFlow : false));
12642
+ this._queueNotification(address);
12643
+ this._endBatch();
12644
+ }
12645
+ return address;
12646
+ }
12647
+
12648
+ /**
12649
+ * Remove a pipe and all of its registry entries when the pipe is destroyed, so
12650
+ * keys do not linger (§15). Emits a final notification (desired/effective
12651
+ * undefined) per removed key.
12652
+ * @param {string} pipeId
12653
+ * @returns {number} number of registry keys removed
12654
+ */
12655
+ }, {
12656
+ key: "unregisterPipe",
12657
+ value: function unregisterPipe(pipeId) {
12658
+ var prefix = "".concat(pipeId, "::");
12659
+ var removed = _toConsumableArray(this._desired.keys()).filter(function (k) {
12660
+ return k.startsWith(prefix);
12661
+ });
12662
+ if (removed.length === 0) return 0;
12663
+ this._beginBatch();
12664
+ var _iterator0 = _createForOfIteratorHelper(removed),
12665
+ _step0;
12666
+ try {
12667
+ for (_iterator0.s(); !(_step0 = _iterator0.n()).done;) {
12668
+ var key = _step0.value;
12669
+ this._desired.delete(key);
12670
+ }
12671
+ } catch (err) {
12672
+ _iterator0.e(err);
12673
+ } finally {
12674
+ _iterator0.f();
12675
+ }
12676
+ var _iterator1 = _createForOfIteratorHelper(removed),
12677
+ _step1;
12678
+ try {
12679
+ for (_iterator1.s(); !(_step1 = _iterator1.n()).done;) {
12680
+ var _key2 = _step1.value;
12681
+ this._queueNotification(_key2);
12682
+ }
12683
+ } catch (err) {
12684
+ _iterator1.e(err);
12685
+ } finally {
12686
+ _iterator1.f();
12687
+ }
12688
+ this._endBatch();
12689
+ return removed.length;
12690
+ }
12691
+ }, {
12692
+ key: "_beginBatch",
12693
+ value: function _beginBatch() {
12694
+ this._batching = true;
12695
+ this._pendingNotifications.clear();
12696
+ }
12697
+ }, {
12698
+ key: "_queueNotification",
12699
+ value: function _queueNotification(address) {
12700
+ var change = {
12701
+ address: address,
12702
+ desired: this.getDesired(address),
12703
+ effective: this.getEffective(address)
12704
+ };
12705
+ if (this._batching) {
12706
+ this._pendingNotifications.set(address, change);
12707
+ } else {
12708
+ this._emit(change);
12709
+ }
12710
+ }
12711
+ }, {
12712
+ key: "_endBatch",
12713
+ value: function _endBatch() {
12714
+ this._batching = false;
12715
+ var changes = _toConsumableArray(this._pendingNotifications.values());
12716
+ this._pendingNotifications.clear();
12717
+ var _iterator10 = _createForOfIteratorHelper(changes),
12718
+ _step10;
12719
+ try {
12720
+ for (_iterator10.s(); !(_step10 = _iterator10.n()).done;) {
12721
+ var change = _step10.value;
12722
+ this._emit(change);
12723
+ }
12724
+ } catch (err) {
12725
+ _iterator10.e(err);
12726
+ } finally {
12727
+ _iterator10.f();
12728
+ }
12729
+ }
12730
+ }, {
12731
+ key: "_emit",
12732
+ value: function _emit(change) {
12733
+ var _iterator11 = _createForOfIteratorHelper(this._listeners),
12734
+ _step11;
12735
+ try {
12736
+ for (_iterator11.s(); !(_step11 = _iterator11.n()).done;) {
12737
+ var listener = _step11.value;
12738
+ try {
12739
+ listener(change);
12740
+ } catch (err) {
12741
+ console.error('[StateEngine] listener error:', err);
12742
+ }
12743
+ }
12744
+ } catch (err) {
12745
+ _iterator11.e(err);
12746
+ } finally {
12747
+ _iterator11.f();
12748
+ }
12749
+ }
12750
+
12751
+ /**
12752
+ * @param {(change: { address: string, desired: *, effective: * }) => void} listener
12753
+ * @returns {() => void} unsubscribe
12754
+ */
12755
+ }, {
12756
+ key: "subscribe",
12757
+ value: function subscribe(listener) {
12758
+ var _this = this;
12759
+ this._listeners.add(listener);
12760
+ return function () {
12761
+ return _this._listeners.delete(listener);
12762
+ };
12763
+ }
12764
+
12765
+ /**
12766
+ * Snapshot of all desired values (for save / debug).
12767
+ * @returns {Record<string, *>}
12768
+ */
12769
+ }, {
12770
+ key: "toRegistry",
12771
+ value: function toRegistry() {
12772
+ var out = {};
12773
+ var _iterator12 = _createForOfIteratorHelper(this._desired),
12774
+ _step12;
12775
+ try {
12776
+ for (_iterator12.s(); !(_step12 = _iterator12.n()).done;) {
12777
+ var _step12$value = _slicedToArray(_step12.value, 2),
12778
+ k = _step12$value[0],
12779
+ v = _step12$value[1];
12780
+ out[k] = v;
12781
+ }
12782
+ } catch (err) {
12783
+ _iterator12.e(err);
12784
+ } finally {
12785
+ _iterator12.f();
12786
+ }
12787
+ return out;
12788
+ }
12789
+
12790
+ /**
12791
+ * Reconstruct the §17 scene declaration (plant / groups / chains /
12792
+ * stateRegistry) from current engine state, for scene save. Round-trips with
12793
+ * {@link StateEngine#load}.
12794
+ * @returns {{ version: string, plant: object, groups: object, chains: object[], stateRegistry: Record<string, *> }}
12795
+ */
12796
+ }, {
12797
+ key: "toDeclaration",
12798
+ value: function toDeclaration() {
12799
+ var groups = {};
12800
+ var _iterator13 = _createForOfIteratorHelper(this._groupMembers),
12801
+ _step13;
12802
+ try {
12803
+ for (_iterator13.s(); !(_step13 = _iterator13.n()).done;) {
12804
+ var _this$_groupDesired$g;
12805
+ var _step13$value = _slicedToArray(_step13.value, 2),
12806
+ groupId = _step13$value[0],
12807
+ members = _step13$value[1];
12808
+ groups[groupId] = {
12809
+ members: _toConsumableArray(members),
12810
+ states: {
12811
+ power: {
12812
+ type: 'Boolean',
12813
+ default: (_this$_groupDesired$g = this._groupDesired.get(groupId)) !== null && _this$_groupDesired$g !== void 0 ? _this$_groupDesired$g : true,
12814
+ gate: true
12815
+ }
12816
+ }
12817
+ };
12818
+ }
12819
+ } catch (err) {
12820
+ _iterator13.e(err);
12821
+ } finally {
12822
+ _iterator13.f();
12823
+ }
12824
+ return {
12825
+ version: '3.0',
12826
+ plant: {
12827
+ states: {
12828
+ power: {
12829
+ type: 'Boolean',
12830
+ default: this._plantPowerDesired,
12831
+ gate: true
12832
+ }
12833
+ }
12834
+ },
12835
+ groups: groups,
12836
+ chains: _toConsumableArray(this._chains),
12837
+ stateRegistry: this.toRegistry()
12838
+ };
12839
+ }
12840
+ }]);
12841
+ }();
12842
+
12843
+ /**
12844
+ * Registry address for an I/O device data point.
12845
+ * Shape: `${parentUuid}::${attachmentId}::${stateId}` (matches §17
12846
+ * `pump-001::start-switch::mode`). The scoped key is `${parentUuid}::${attachmentId}`.
12847
+ *
12848
+ * @param {string} attachmentId
12849
+ * @param {string} stateId
12850
+ * @param {string} [parentUuid]
12851
+ * @returns {string}
12852
+ */
12853
+ function ioAddress(attachmentId, stateId, parentUuid) {
12854
+ return makeAddress(getScopedAttachmentKey(attachmentId, parentUuid), stateId);
12855
+ }
12856
+
12857
+ /**
12858
+ * Parse an I/O device address back into its parts. Returns null for
12859
+ * plant/group/object/pipe addresses (2-part) — I/O device data points are 3-part
12860
+ * because a device always has a parent component (§8; free-floating I/O rejected).
12861
+ *
12862
+ * @param {string} address
12863
+ * @returns {{ parentUuid: string, attachmentId: string, stateId: string, scopedKey: string } | null}
12864
+ */
12865
+ function parseIoAddress(address) {
12866
+ if (typeof address !== 'string') return null;
12867
+ var parts = address.split('::');
12868
+ if (parts.length !== 3) return null;
12869
+ var _parts = _slicedToArray(parts, 3),
12870
+ parentUuid = _parts[0],
12871
+ attachmentId = _parts[1],
12872
+ stateId = _parts[2];
12873
+ return {
12874
+ parentUuid: parentUuid,
12875
+ attachmentId: attachmentId,
12876
+ stateId: stateId,
12877
+ scopedKey: "".concat(parentUuid, "::").concat(attachmentId)
12878
+ };
12879
+ }
12880
+
12881
+ /**
12882
+ * Read a State Engine declaration out of scene data, tolerating legacy scenes.
12883
+ * v3.0 scenes carry `plant` / `groups` / `chains` / `stateRegistry`; older v2.3
12884
+ * scenes have none of these and load with safe defaults (plant power on, no
12885
+ * groups, no chains) so nothing crashes on unknown/missing fields (§15).
12886
+ *
12887
+ * @param {object} [sceneData]
12888
+ * @returns {{ plant: object, groups: object, chains: object[], stateRegistry: object }}
12889
+ */
12890
+ function extractDeclaration() {
12891
+ var sceneData = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
12892
+ return {
12893
+ plant: sceneData.plant || {
12894
+ states: {
12895
+ power: {
12896
+ type: 'Boolean',
12897
+ default: true,
12898
+ gate: true
12899
+ }
12900
+ }
12901
+ },
12902
+ groups: sceneData.groups || {},
12903
+ chains: Array.isArray(sceneData.chains) ? sceneData.chains : [],
12904
+ stateRegistry: sceneData.stateRegistry || {}
12905
+ };
12906
+ }
12907
+
12908
+ /**
12909
+ * Merge a State Engine's current declaration (plant / groups / chains /
12910
+ * stateRegistry) into an export object, bumping it to version 3.0. Mutates and
12911
+ * returns `exportData`.
12912
+ *
12913
+ * @param {object} exportData - the scene export object being assembled
12914
+ * @param {{ toDeclaration: () => object }} engine
12915
+ * @returns {object} exportData
12916
+ */
12917
+ function applyDeclarationToExport(exportData, engine) {
12918
+ if (!exportData || !engine || typeof engine.toDeclaration !== 'function') return exportData;
12919
+ var declaration = engine.toDeclaration();
12920
+ exportData.version = '3.0';
12921
+ exportData.plant = declaration.plant;
12922
+ exportData.groups = declaration.groups;
12923
+ if (declaration.chains.length > 0) exportData.chains = declaration.chains;
12924
+ exportData.stateRegistry = declaration.stateRegistry;
12925
+ return exportData;
12926
+ }
12927
+
12117
12928
  var SceneExportManager = /*#__PURE__*/function () {
12118
12929
  function SceneExportManager(sceneViewer) {
12119
12930
  _classCallCheck(this, SceneExportManager);
@@ -12167,7 +12978,8 @@ var SceneExportManager = /*#__PURE__*/function () {
12167
12978
  key: "exportSceneData",
12168
12979
  value: function exportSceneData() {
12169
12980
  var _this = this,
12170
- _this$sceneViewer$cur2;
12981
+ _this$sceneViewer$cur2,
12982
+ _this$sceneViewer$cen;
12171
12983
  console.log('📤 Starting scene export...');
12172
12984
  if (!this.sceneViewer.scene) {
12173
12985
  console.warn('⚠️ No scene available for export');
@@ -12400,6 +13212,13 @@ var SceneExportManager = /*#__PURE__*/function () {
12400
13212
  if (sceneBehaviors && sceneBehaviors.length > 0) {
12401
13213
  exportData.behaviors = sceneBehaviors;
12402
13214
  }
13215
+
13216
+ // Persist plant / groups / chains / stateRegistry from the State Engine and
13217
+ // bump the format to 3.0 (Framework §12; no-op if no engine present).
13218
+ var stateEngine = (_this$sceneViewer$cen = this.sceneViewer.centralPlant) === null || _this$sceneViewer$cen === void 0 ? void 0 : _this$sceneViewer$cen.stateEngine;
13219
+ if (stateEngine) {
13220
+ applyDeclarationToExport(exportData, stateEngine);
13221
+ }
12403
13222
  console.log('✅ Scene export completed:', exportData);
12404
13223
  console.log("\uD83D\uDCCA Exported ".concat(sceneChildren.length, " components and ").concat(exportData.connections.length, " connections"));
12405
13224
  return exportData;
@@ -28239,6 +29058,21 @@ var PathRenderingManager = /*#__PURE__*/function (_BaseDisposable) {
28239
29058
  }
28240
29059
  }
28241
29060
 
29061
+ /**
29062
+ * Read the current color of a path's shared material as a `#rrggbb` string,
29063
+ * or null if the path has no material. Used to snapshot a pipe's baseline
29064
+ * colour before it is temporarily overpainted (e.g. flow gating).
29065
+ *
29066
+ * @param {string} pathId - "${from}-->${to}"
29067
+ * @returns {string|null}
29068
+ */
29069
+ }, {
29070
+ key: "getPathColorHex",
29071
+ value: function getPathColorHex(pathId) {
29072
+ var mat = this._pathMaterials.get(pathId);
29073
+ return mat ? "#".concat(mat.color.getHexString()) : null;
29074
+ }
29075
+
28242
29076
  /**
28243
29077
  * Get path colors for visual distinction
28244
29078
  * @param {number} index - Path index
@@ -30770,6 +31604,9 @@ var TEMP_MIN = 0;
30770
31604
  /** Maximum temperature in the colour ramp (maps to pure red). */
30771
31605
  var TEMP_MAX = 100;
30772
31606
 
31607
+ /** Colour a pipe is painted when its effective flow is gated off (§6.1). */
31608
+ var PIPE_OFF_GRAY = '#3a3a3a';
31609
+
30773
31610
  /**
30774
31611
  * Convert a temperature value to a CSS hex colour string using a blue→red HSL
30775
31612
  * ramp. Values are clamped to [TEMP_MIN, TEMP_MAX].
@@ -30807,22 +31644,78 @@ var PathFlowManager = /*#__PURE__*/function (_BaseDisposable) {
30807
31644
  * @type {Map<string, Record<string, any>>}
30808
31645
  */
30809
31646
  _this._derivedStore = new Map();
31647
+
31648
+ /**
31649
+ * Paths whose effective flow is gated off (State Engine §6.1). While a path
31650
+ * is in this set it is painted {@link PIPE_OFF_GRAY} and temperature updates
31651
+ * are suppressed so they don't override the off-look.
31652
+ * @type {Set<string>}
31653
+ */
31654
+ _this._flowGatedOff = new Set();
31655
+
31656
+ /**
31657
+ * Baseline (on-state) colour snapshotted the moment a path is gated off, so
31658
+ * it can be restored exactly when flow returns — including pipes with no
31659
+ * declared flowTemperature, whose colour is the base pipe material.
31660
+ * @type {Map<string, string>}
31661
+ */
31662
+ _this._flowBaseColor = new Map();
30810
31663
  return _this;
30811
31664
  }
30812
31665
 
30813
- // ── Public API attribute write ──────────────────────────────────────────
31666
+ // ── Flow gating (State Engine effective → 3D bridge, §6.1) ─────────────────
30814
31667
 
30815
31668
  /**
30816
- * Set a declared (persistent) flow attribute on a path.
30817
- * The value is stored in PathData and will be serialised with the scene.
30818
- * Triggers a visual update.
31669
+ * Gate a path's flow visual on/off from the State Engine's effective `flow`.
31670
+ * Off paint the shared per-path material gray; on restore the temperature
31671
+ * colour. The off state is sticky (see {@link _flowGatedOff}).
30819
31672
  *
30820
- * @param {string} pathId - e.g. "PUMP-1-CONN-1-->CHILLER-1-CONN-2"
30821
- * @param {string} key - One of FLOW_ATTRIBUTE_KEYS
30822
- * @param {any} value
31673
+ * @param {string} pathId - `${from}-->${to}`
31674
+ * @param {boolean} enabled - Effective flow
30823
31675
  */
30824
31676
  _inherits(PathFlowManager, _BaseDisposable);
30825
31677
  return _createClass(PathFlowManager, [{
31678
+ key: "setFlowEnabled",
31679
+ value: function setFlowEnabled(pathId, enabled) {
31680
+ var split = this._splitPathId(pathId);
31681
+ if (!split) return;
31682
+ var renderingManager = this._getRenderingManager();
31683
+ if (enabled) {
31684
+ this._flowGatedOff.delete(pathId);
31685
+ // Prefer a temperature-derived colour if one is declared; otherwise
31686
+ // restore the baseline snapshotted when the pipe was gated off (this is
31687
+ // what fixes plain, non-temperature pipes getting stuck gray).
31688
+ var temp = this.resolve(pathId, 'flowTemperature');
31689
+ if (temp !== null && temp !== undefined) {
31690
+ this.applyVisualizationForPath(pathId);
31691
+ } else if (this._flowBaseColor.has(pathId)) {
31692
+ renderingManager === null || renderingManager === void 0 || renderingManager.updatePathColor(split.from, split.to, this._flowBaseColor.get(pathId));
31693
+ }
31694
+ this._flowBaseColor.delete(pathId);
31695
+ } else {
31696
+ // Snapshot the current (on-state) colour once, before overpainting gray.
31697
+ if (!this._flowGatedOff.has(pathId) && renderingManager) {
31698
+ var _renderingManager$get;
31699
+ var base = (_renderingManager$get = renderingManager.getPathColorHex) === null || _renderingManager$get === void 0 ? void 0 : _renderingManager$get.call(renderingManager, pathId);
31700
+ if (base) this._flowBaseColor.set(pathId, base);
31701
+ }
31702
+ this._flowGatedOff.add(pathId);
31703
+ renderingManager === null || renderingManager === void 0 || renderingManager.updatePathColor(split.from, split.to, PIPE_OFF_GRAY);
31704
+ }
31705
+ }
31706
+
31707
+ // ── Public API — attribute write ──────────────────────────────────────────
31708
+
31709
+ /**
31710
+ * Set a declared (persistent) flow attribute on a path.
31711
+ * The value is stored in PathData and will be serialised with the scene.
31712
+ * Triggers a visual update.
31713
+ *
31714
+ * @param {string} pathId - e.g. "PUMP-1-CONN-1-->CHILLER-1-CONN-2"
31715
+ * @param {string} key - One of FLOW_ATTRIBUTE_KEYS
31716
+ * @param {any} value
31717
+ */
31718
+ }, {
30826
31719
  key: "setDeclared",
30827
31720
  value: function setDeclared(pathId, key, value) {
30828
31721
  var pd = this._getPathData(pathId);
@@ -30928,6 +31821,13 @@ var PathFlowManager = /*#__PURE__*/function (_BaseDisposable) {
30928
31821
  }, {
30929
31822
  key: "applyVisualizationForPath",
30930
31823
  value: function applyVisualizationForPath(pathId) {
31824
+ // Effective flow gated off (§6.1) — keep the off-look, ignore temperature.
31825
+ if (this._flowGatedOff.has(pathId)) {
31826
+ var _this$_getRenderingMa;
31827
+ var _split = this._splitPathId(pathId);
31828
+ if (_split) (_this$_getRenderingMa = this._getRenderingManager()) === null || _this$_getRenderingMa === void 0 || _this$_getRenderingMa.updatePathColor(_split.from, _split.to, PIPE_OFF_GRAY);
31829
+ return;
31830
+ }
30931
31831
  var temp = this.resolve(pathId, 'flowTemperature');
30932
31832
  if (temp === null || temp === undefined) {
30933
31833
  return; // No temperature declared — leave default pipe color
@@ -30935,17 +31835,30 @@ var PathFlowManager = /*#__PURE__*/function (_BaseDisposable) {
30935
31835
  var color = temperatureToColor(temp);
30936
31836
  var renderingManager = this._getRenderingManager();
30937
31837
  if (!renderingManager) return;
31838
+ var split = this._splitPathId(pathId);
31839
+ if (!split) return;
31840
+ renderingManager.updatePathColor(split.from, split.to, color);
31841
+ console.log("\uD83C\uDF21\uFE0F PathFlowManager: \"".concat(pathId, "\" \u2192 flowTemperature ").concat(temp, " \u2192 ").concat(color));
31842
+ }
30938
31843
 
30939
- // pathId = "from-->to"; extract the two halves
31844
+ /**
31845
+ * Split a `${from}-->${to}` pathId into its two connector halves.
31846
+ * @param {string} pathId
31847
+ * @returns {{ from: string, to: string }|null} null if malformed
31848
+ * @private
31849
+ */
31850
+ }, {
31851
+ key: "_splitPathId",
31852
+ value: function _splitPathId(pathId) {
30940
31853
  var sepIdx = pathId.indexOf('-->');
30941
31854
  if (sepIdx === -1) {
30942
31855
  console.warn("PathFlowManager: malformed pathId \"".concat(pathId, "\""));
30943
- return;
31856
+ return null;
30944
31857
  }
30945
- var from = pathId.slice(0, sepIdx);
30946
- var to = pathId.slice(sepIdx + 3);
30947
- renderingManager.updatePathColor(from, to, color);
30948
- console.log("\uD83C\uDF21\uFE0F PathFlowManager: \"".concat(pathId, "\" \u2192 flowTemperature ").concat(temp, " \u2192 ").concat(color));
31858
+ return {
31859
+ from: pathId.slice(0, sepIdx),
31860
+ to: pathId.slice(sepIdx + 3)
31861
+ };
30949
31862
  }
30950
31863
 
30951
31864
  /**
@@ -30977,6 +31890,8 @@ var PathFlowManager = /*#__PURE__*/function (_BaseDisposable) {
30977
31890
  key: "dispose",
30978
31891
  value: function dispose() {
30979
31892
  this._derivedStore.clear();
31893
+ this._flowGatedOff.clear();
31894
+ this._flowBaseColor.clear();
30980
31895
  this.sceneViewer = null;
30981
31896
  _superPropGet(PathFlowManager, "dispose", this, 3)([]);
30982
31897
  }
@@ -37899,7 +38814,6 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
37899
38814
  if (!ioDeviceObject || !this._stateAdapter) return;
37900
38815
  var ud = ioDeviceObject.userData;
37901
38816
  var attachmentId = ud === null || ud === void 0 ? void 0 : ud.attachmentId;
37902
- var dataPoints = (ud === null || ud === void 0 ? void 0 : ud.dataPoints) || [];
37903
38817
  if (!attachmentId) return;
37904
38818
 
37905
38819
  // Walk up to find parent component UUID for scoped state/behavior handling
@@ -37914,6 +38828,13 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
37914
38828
  obj = obj.parent;
37915
38829
  }
37916
38830
 
38831
+ // Derive the device's data points from its behaviorConfig (same resolver the
38832
+ // drag path uses). `userData.dataPoints` is not populated for attached
38833
+ // devices, so reading it directly would find nothing — resolve instead, and
38834
+ // keep the legacy field only as a fallback.
38835
+ var resolved = resolveDataPoints(parentUuid, attachmentId, ud, getIoBehaviorManager(this.sceneViewer), null);
38836
+ var dataPoints = resolved !== null && resolved !== void 0 && resolved.length ? resolved : (ud === null || ud === void 0 ? void 0 : ud.dataPoints) || [];
38837
+
37917
38838
  // Create a scoped attachment key to prevent state sharing between instances
37918
38839
  // of the same smart component that share the same attachmentId
37919
38840
  var scopedAttachmentId = getScopedAttachmentKey(attachmentId, parentUuid);
@@ -40328,6 +41249,611 @@ var IoOutlineManager = /*#__PURE__*/function (_BaseDisposable) {
40328
41249
  }]);
40329
41250
  }(BaseDisposable);
40330
41251
 
41252
+ /** How far the albedo is pulled toward black for a gated-off mesh. */
41253
+ var OFF_COLOR_SCALE = 0.2;
41254
+ var EffectiveVisualManager = /*#__PURE__*/function (_BaseDisposable) {
41255
+ /**
41256
+ * @param {Object} sceneViewer - The central sceneViewer hub
41257
+ */
41258
+ function EffectiveVisualManager(sceneViewer) {
41259
+ var _this;
41260
+ _classCallCheck(this, EffectiveVisualManager);
41261
+ _this = _callSuper(this, EffectiveVisualManager);
41262
+ _this.sceneViewer = sceneViewer;
41263
+
41264
+ /**
41265
+ * Materials this manager has gated (cloned + cached), for restore on dispose.
41266
+ * @type {Set<THREE.Material>}
41267
+ */
41268
+ _this._gatedMaterials = new Set();
41269
+
41270
+ // Bound handlers so we can unsubscribe in _doDispose().
41271
+ _this._onChange = _this._onChange.bind(_this);
41272
+ _this._regateAll = _this._regateAll.bind(_this);
41273
+ if (sceneViewer !== null && sceneViewer !== void 0 && sceneViewer.on) {
41274
+ sceneViewer.on('engine-state-changed', _this._onChange);
41275
+ sceneViewer.on('scene-loaded', _this._regateAll);
41276
+ }
41277
+ return _this;
41278
+ }
41279
+
41280
+ // ── Engine change handling ──────────────────────────────────────────────────
41281
+
41282
+ /**
41283
+ * React to one committed registry change. Only 2-part object / pipe addresses
41284
+ * are gated; everything else is ignored.
41285
+ * @param {{ address: string, desired: *, effective: * }} change
41286
+ * @private
41287
+ */
41288
+ _inherits(EffectiveVisualManager, _BaseDisposable);
41289
+ return _createClass(EffectiveVisualManager, [{
41290
+ key: "_onChange",
41291
+ value: function _onChange(change) {
41292
+ if (!change || typeof change.address !== 'string') return;
41293
+ var parts = change.address.split('::');
41294
+ if (parts.length !== 2) return; // 3-part I/O is desired-driven; skip
41295
+
41296
+ var _parts = _slicedToArray(parts, 2),
41297
+ scope = _parts[0],
41298
+ attr = _parts[1];
41299
+ if (scope === 'plant' || scope.startsWith('group:')) return; // members self-report
41300
+
41301
+ if (attr === 'power') {
41302
+ this._gateEquipment(scope, change.effective);
41303
+ } else if (attr === 'flow') {
41304
+ this._gatePipe(scope, change.effective);
41305
+ }
41306
+ }
41307
+
41308
+ /**
41309
+ * Re-gate the whole scene from current effective state. Used after scene load,
41310
+ * where StateEngine.load() populates the registry without emitting per-address
41311
+ * events (and the meshes have just been (re)built).
41312
+ * @private
41313
+ */
41314
+ }, {
41315
+ key: "_regateAll",
41316
+ value: function _regateAll() {
41317
+ var _this$sceneViewer;
41318
+ 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;
41319
+ if (!engine) return;
41320
+ var registry = engine.toRegistry();
41321
+ for (var _i = 0, _Object$keys = Object.keys(registry); _i < _Object$keys.length; _i++) {
41322
+ var address = _Object$keys[_i];
41323
+ var parts = address.split('::');
41324
+ if (parts.length !== 2) continue;
41325
+ var _parts2 = _slicedToArray(parts, 2),
41326
+ scope = _parts2[0],
41327
+ attr = _parts2[1];
41328
+ if (scope === 'plant' || scope.startsWith('group:')) continue;
41329
+ if (attr === 'power') {
41330
+ this._gateEquipment(scope, engine.getEffective(address));
41331
+ } else if (attr === 'flow') {
41332
+ this._gatePipe(scope, engine.getEffective(address));
41333
+ }
41334
+ }
41335
+ }
41336
+
41337
+ // ── Equipment gating ────────────────────────────────────────────────────────
41338
+
41339
+ /**
41340
+ * Darken (off) or restore (on) every mesh of an equipment object.
41341
+ * @param {string} objectId - Hardcoded object id (address scope)
41342
+ * @param {boolean} on - Effective power
41343
+ * @private
41344
+ */
41345
+ }, {
41346
+ key: "_gateEquipment",
41347
+ value: function _gateEquipment(objectId, on) {
41348
+ var _this$sceneViewer2,
41349
+ _this2 = this;
41350
+ var scene = (_this$sceneViewer2 = this.sceneViewer) === null || _this$sceneViewer2 === void 0 ? void 0 : _this$sceneViewer2.scene;
41351
+ if (!scene) return;
41352
+ var obj = findObjectByHardcodedUuid(scene, objectId);
41353
+ if (!obj || typeof obj.traverse !== 'function') return;
41354
+ obj.traverse(function (mesh) {
41355
+ if (!mesh.isMesh && !mesh.material) return;
41356
+ // Leave attached I/O control panels (switches) lit and operable even when
41357
+ // their host is powered off — you must be able to see and click the switch
41358
+ // that turns it back on. (traverse can't prune, so guard per mesh.)
41359
+ if (_this2._isUnderIoDevice(mesh, obj)) return;
41360
+ var materials = _this2._prepareMeshMaterials(mesh);
41361
+ var _iterator = _createForOfIteratorHelper(materials),
41362
+ _step;
41363
+ try {
41364
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
41365
+ var material = _step.value;
41366
+ _this2._applyGate(material, on);
41367
+ }
41368
+ } catch (err) {
41369
+ _iterator.e(err);
41370
+ } finally {
41371
+ _iterator.f();
41372
+ }
41373
+ });
41374
+ }
41375
+
41376
+ /**
41377
+ * True if `mesh` is an io-device or nested under one, walking up to (but not
41378
+ * past) the equipment root `stopAt`.
41379
+ * @param {THREE.Object3D} mesh
41380
+ * @param {THREE.Object3D} stopAt - equipment root to stop the upward walk at
41381
+ * @returns {boolean}
41382
+ * @private
41383
+ */
41384
+ }, {
41385
+ key: "_isUnderIoDevice",
41386
+ value: function _isUnderIoDevice(mesh, stopAt) {
41387
+ var node = mesh;
41388
+ while (node) {
41389
+ var _node$userData;
41390
+ if (((_node$userData = node.userData) === null || _node$userData === void 0 ? void 0 : _node$userData.objectType) === 'io-device') return true;
41391
+ if (node === stopAt) break;
41392
+ node = node.parent;
41393
+ }
41394
+ return false;
41395
+ }
41396
+
41397
+ /**
41398
+ * Ensure a mesh's material(s) are cloned-on-write and have their baseline
41399
+ * cached, then return the material list to gate.
41400
+ * @param {THREE.Mesh} mesh
41401
+ * @returns {THREE.Material[]}
41402
+ * @private
41403
+ */
41404
+ }, {
41405
+ key: "_prepareMeshMaterials",
41406
+ value: function _prepareMeshMaterials(mesh) {
41407
+ if (!mesh.material) return [];
41408
+
41409
+ // Clone-on-write once per mesh so shared library materials aren't shared.
41410
+ if (!mesh.userData._effectiveGateCloned) {
41411
+ if (Array.isArray(mesh.material)) {
41412
+ mesh.material = mesh.material.map(function (m) {
41413
+ return m !== null && m !== void 0 && m.clone ? m.clone() : m;
41414
+ });
41415
+ } else if (mesh.material.clone) {
41416
+ mesh.material = mesh.material.clone();
41417
+ }
41418
+ mesh.userData._effectiveGateCloned = true;
41419
+ }
41420
+ var materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
41421
+ var _iterator2 = _createForOfIteratorHelper(materials),
41422
+ _step2;
41423
+ try {
41424
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
41425
+ var _material$userData, _material$color$clone, _material$color, _material$color$clone2, _material$emissive$cl, _material$emissive, _material$emissive$cl2;
41426
+ var material = _step2.value;
41427
+ if (!material || (_material$userData = material.userData) !== null && _material$userData !== void 0 && _material$userData._effectiveGate) continue;
41428
+ material.userData = material.userData || {};
41429
+ material.userData._effectiveGate = {
41430
+ 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,
41431
+ 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,
41432
+ emissiveIntensity: typeof material.emissiveIntensity === 'number' ? material.emissiveIntensity : null
41433
+ };
41434
+ this._gatedMaterials.add(material);
41435
+ }
41436
+ } catch (err) {
41437
+ _iterator2.e(err);
41438
+ } finally {
41439
+ _iterator2.f();
41440
+ }
41441
+ return materials.filter(Boolean);
41442
+ }
41443
+
41444
+ /**
41445
+ * Darken or restore a single material from its cached baseline.
41446
+ * @param {THREE.Material} material
41447
+ * @param {boolean} on
41448
+ * @private
41449
+ */
41450
+ }, {
41451
+ key: "_applyGate",
41452
+ value: function _applyGate(material, on) {
41453
+ var _material$userData2;
41454
+ var base = (_material$userData2 = material.userData) === null || _material$userData2 === void 0 ? void 0 : _material$userData2._effectiveGate;
41455
+ if (!base) return;
41456
+ if (on) {
41457
+ if (base.color && material.color) material.color.copy(base.color);
41458
+ if (base.emissive && material.emissive) material.emissive.copy(base.emissive);
41459
+ if (base.emissiveIntensity !== null) material.emissiveIntensity = base.emissiveIntensity;
41460
+ } else {
41461
+ if (base.color && material.color) material.color.copy(base.color).multiplyScalar(OFF_COLOR_SCALE);
41462
+ if (material.emissive) material.emissive.setScalar(0);
41463
+ if (typeof material.emissiveIntensity === 'number') material.emissiveIntensity = 0;
41464
+ }
41465
+ material.needsUpdate = true;
41466
+ }
41467
+
41468
+ // ── Pipe flow gating ────────────────────────────────────────────────────────
41469
+
41470
+ /**
41471
+ * Delegate pipe-flow gating to PathFlowManager (grays the shared per-path
41472
+ * material when off, restores the temperature colour when on).
41473
+ * @param {string} pathId - `${from}-->${to}`
41474
+ * @param {boolean} on - Effective flow
41475
+ * @private
41476
+ */
41477
+ }, {
41478
+ key: "_gatePipe",
41479
+ value: function _gatePipe(pathId, on) {
41480
+ var _this$sceneViewer3, _pathFlowManager$setF;
41481
+ 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;
41482
+ pathFlowManager === null || pathFlowManager === void 0 || (_pathFlowManager$setF = pathFlowManager.setFlowEnabled) === null || _pathFlowManager$setF === void 0 || _pathFlowManager$setF.call(pathFlowManager, pathId, on);
41483
+ }
41484
+
41485
+ // ── Lifecycle ───────────────────────────────────────────────────────────────
41486
+ }, {
41487
+ key: "_doDispose",
41488
+ value: function _doDispose() {
41489
+ var _this$sceneViewer4;
41490
+ if ((_this$sceneViewer4 = this.sceneViewer) !== null && _this$sceneViewer4 !== void 0 && _this$sceneViewer4.off) {
41491
+ this.sceneViewer.off('engine-state-changed', this._onChange);
41492
+ this.sceneViewer.off('scene-loaded', this._regateAll);
41493
+ }
41494
+ // Restore any still-gated materials to their cached baseline.
41495
+ var _iterator3 = _createForOfIteratorHelper(this._gatedMaterials),
41496
+ _step3;
41497
+ try {
41498
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
41499
+ var material = _step3.value;
41500
+ this._applyGate(material, true);
41501
+ }
41502
+ } catch (err) {
41503
+ _iterator3.e(err);
41504
+ } finally {
41505
+ _iterator3.f();
41506
+ }
41507
+ this._gatedMaterials.clear();
41508
+ this.sceneViewer = null;
41509
+ }
41510
+ }]);
41511
+ }(BaseDisposable);
41512
+
41513
+ /**
41514
+ * X offsets for `count` switches evenly spaced and centered on 0.
41515
+ * @param {number} count
41516
+ * @param {number} spacing
41517
+ * @returns {number[]}
41518
+ */
41519
+ function computeSwitchRowX(count, spacing) {
41520
+ var start = -((count - 1) * spacing) / 2;
41521
+ return Array.from({
41522
+ length: count
41523
+ }, function (_, i) {
41524
+ return start + i * spacing;
41525
+ });
41526
+ }
41527
+ var ControlPanelManager = /*#__PURE__*/function (_BaseDisposable) {
41528
+ /**
41529
+ * @param {Object} sceneViewer - The central sceneViewer hub
41530
+ */
41531
+ function ControlPanelManager(sceneViewer) {
41532
+ var _this;
41533
+ _classCallCheck(this, ControlPanelManager);
41534
+ _this = _callSuper(this, ControlPanelManager);
41535
+ _this.sceneViewer = sceneViewer;
41536
+ _this._panel = null;
41537
+ _this._labels = [];
41538
+ _this._panelId = null;
41539
+ _this._build = _this._build.bind(_this);
41540
+ if (sceneViewer !== null && sceneViewer !== void 0 && sceneViewer.on) {
41541
+ sceneViewer.on('scene-loaded', _this._build);
41542
+ }
41543
+ return _this;
41544
+ }
41545
+
41546
+ /**
41547
+ * Build (or rebuild) the panel from the current scene's `controlPanel` spec.
41548
+ * @private
41549
+ */
41550
+ _inherits(ControlPanelManager, _BaseDisposable);
41551
+ return _createClass(ControlPanelManager, [{
41552
+ key: "_build",
41553
+ value: (function () {
41554
+ var _build2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
41555
+ var _this$sceneViewer,
41556
+ _centralPlant$importe,
41557
+ _this$sceneViewer2,
41558
+ _b$width,
41559
+ _b$height,
41560
+ _b$thickness,
41561
+ _desk$tilt,
41562
+ _desk$height,
41563
+ _desk$legThickness,
41564
+ _desk$legInset,
41565
+ _spec$switchSpacing,
41566
+ _this$sceneViewer3,
41567
+ _spec$labelSize,
41568
+ _this2 = this;
41569
+ 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;
41570
+ return _regenerator().w(function (_context) {
41571
+ while (1) switch (_context.n) {
41572
+ case 0:
41573
+ this._teardown();
41574
+ centralPlant = (_this$sceneViewer = this.sceneViewer) === null || _this$sceneViewer === void 0 ? void 0 : _this$sceneViewer.centralPlant;
41575
+ spec = centralPlant === null || centralPlant === void 0 || (_centralPlant$importe = centralPlant.importedSceneData) === null || _centralPlant$importe === void 0 ? void 0 : _centralPlant$importe.controlPanel;
41576
+ scene = (_this$sceneViewer2 = this.sceneViewer) === null || _this$sceneViewer2 === void 0 ? void 0 : _this$sceneViewer2.scene;
41577
+ if (!(!spec || !scene)) {
41578
+ _context.n = 1;
41579
+ break;
41580
+ }
41581
+ return _context.a(2);
41582
+ case 1:
41583
+ panelId = spec.id || 'CONTROL-PANEL-1';
41584
+ this._panelId = panelId;
41585
+ panel = new THREE__namespace.Group();
41586
+ panel.name = 'Control Panel';
41587
+ panel.uuid = panelId;
41588
+ panel.userData = {
41589
+ objectType: 'component',
41590
+ isControlPanel: true
41591
+ };
41592
+ p = spec.position || {};
41593
+ panel.position.set(p.x || 0, p.y || 0, p.z || 0);
41594
+ r = spec.rotation || {};
41595
+ d2r = Math.PI / 180;
41596
+ panel.rotation.set((r.x || 0) * d2r, (r.y || 0) * d2r, (r.z || 0) * d2r, 'XYZ');
41597
+ scale = Number.isFinite(spec.scale) && spec.scale > 0 ? spec.scale : 1;
41598
+ panel.scale.setScalar(scale);
41599
+
41600
+ // ── Drawing-desk geometry (Z-up scene) ───────────────────────────────────
41601
+ // A tilted top surface (the drafting board) raised on legs. Switches +
41602
+ // labels live on the tilted `top` group so they sit on the angled surface.
41603
+ b = spec.board || {};
41604
+ width = (_b$width = b.width) !== null && _b$width !== void 0 ? _b$width : 7; // x-extent of the top
41605
+ depth = (_b$height = b.height) !== null && _b$height !== void 0 ? _b$height : 2; // y-extent of the top (spec key kept as `height`)
41606
+ thickness = (_b$thickness = b.thickness) !== null && _b$thickness !== void 0 ? _b$thickness : 0.15;
41607
+ desk = spec.desk || {};
41608
+ tilt = ((_desk$tilt = desk.tilt) !== null && _desk$tilt !== void 0 ? _desk$tilt : 30) * d2r; // surface tilt; +raises the back (+y) edge
41609
+ deskHeight = (_desk$height = desk.height) !== null && _desk$height !== void 0 ? _desk$height : 1.3; // world height of the top's center
41610
+ legThickness = (_desk$legThickness = desk.legThickness) !== null && _desk$legThickness !== void 0 ? _desk$legThickness : 0.18;
41611
+ legInset = (_desk$legInset = desk.legInset) !== null && _desk$legInset !== void 0 ? _desk$legInset : 0.35;
41612
+ legColor = desk.legColor || '#22303c'; // Tilted top group: raised to deskHeight, rotated about X.
41613
+ top = new THREE__namespace.Group();
41614
+ top.position.set(0, 0, deskHeight);
41615
+ top.rotation.set(tilt, 0, 0, 'XYZ');
41616
+ panel.add(top);
41617
+ board = new THREE__namespace.Mesh(new THREE__namespace.BoxGeometry(width, depth, thickness), new THREE__namespace.MeshStandardMaterial({
41618
+ color: b.color || '#2c3e50',
41619
+ roughness: 0.6,
41620
+ metalness: 0.2
41621
+ }));
41622
+ board.userData = {
41623
+ objectType: 'control-panel-board'
41624
+ };
41625
+ top.add(board);
41626
+
41627
+ // Legs: one per corner, from the ground up to the tilted top's underside.
41628
+ legMat = new THREE__namespace.MeshStandardMaterial({
41629
+ color: legColor,
41630
+ roughness: 0.7,
41631
+ metalness: 0.1
41632
+ });
41633
+ halfW = Math.max(0.1, width / 2 - legInset);
41634
+ halfD = Math.max(0.1, depth / 2 - legInset);
41635
+ for (_i = 0, _arr = [halfD, -halfD]; _i < _arr.length; _i++) {
41636
+ ly = _arr[_i];
41637
+ // Underside height at this front/back row after the tilt.
41638
+ cornerZ = deskHeight + ly * Math.sin(tilt) - thickness / 2 * Math.cos(tilt);
41639
+ legH = Math.max(0.1, cornerZ);
41640
+ yPos = ly * Math.cos(tilt); // corner's y in panel space after tilt
41641
+ for (_i2 = 0, _arr2 = [halfW, -halfW]; _i2 < _arr2.length; _i2++) {
41642
+ lx = _arr2[_i2];
41643
+ leg = new THREE__namespace.Mesh(new THREE__namespace.BoxGeometry(legThickness, legThickness, legH), legMat);
41644
+ leg.position.set(lx, yPos, legH / 2);
41645
+ panel.add(leg);
41646
+ }
41647
+ }
41648
+
41649
+ // ── Switch layout: a row along X on the top face (+Z) of the board ───────
41650
+ switches = Array.isArray(spec.switches) ? spec.switches : [];
41651
+ spacing = (_spec$switchSpacing = spec.switchSpacing) !== null && _spec$switchSpacing !== void 0 ? _spec$switchSpacing : 1.3;
41652
+ xs = computeSwitchRowX(switches.length, spacing);
41653
+ topZ = thickness / 2;
41654
+ attachedDevices = {};
41655
+ switches.forEach(function (sw, i) {
41656
+ attachedDevices[sw.attachmentId] = {
41657
+ deviceId: spec.deviceId,
41658
+ attachmentLabel: sw.label || sw.attachmentId,
41659
+ attachmentPoint: {
41660
+ position: {
41661
+ x: xs[i],
41662
+ y: 0,
41663
+ z: topZ
41664
+ },
41665
+ rotation: spec.switchRotation || {
41666
+ x: 0,
41667
+ y: 0,
41668
+ z: 0
41669
+ }
41670
+ }
41671
+ };
41672
+ });
41673
+ componentData = {
41674
+ isSmart: true,
41675
+ attachedDevices: attachedDevices
41676
+ }; // Clone + tag the switch devices onto the tilted top (reuses smart-component path).
41677
+ _context.n = 2;
41678
+ return attachIODevicesToComponent(top, componentData, modelPreloader, panelId);
41679
+ case 2:
41680
+ // Optional switch-only scale (attach path always leaves devices at 1:1).
41681
+ switchScale = Number.isFinite(spec.switchScale) && spec.switchScale > 0 ? spec.switchScale : 1;
41682
+ if (switchScale !== 1) {
41683
+ top.traverse(function (obj) {
41684
+ var _obj$userData;
41685
+ if (((_obj$userData = obj.userData) === null || _obj$userData === void 0 ? void 0 : _obj$userData.objectType) === 'io-device') {
41686
+ obj.scale.setScalar(switchScale);
41687
+ }
41688
+ });
41689
+ }
41690
+
41691
+ // Panel must be in the scene before behaviors register (default-state seeding
41692
+ // scans the live scene graph for io-devices).
41693
+ scene.add(panel);
41694
+ this._panel = panel;
41695
+ 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;
41696
+ if (ioBehavMgr) {
41697
+ registerBehaviorsForComponent(ioBehavMgr, panelId, componentData, panel, modelPreloader.componentDictionary, centralPlant);
41698
+
41699
+ // Drive each switch's mesh to its current engine value so the initial
41700
+ // pose/colour reflects the state. `registerBehaviorsForComponent`'s
41701
+ // default-seeding writes through the engine, which no-ops when the value
41702
+ // already matches the seeded `stateRegistry` — leaving the button at its
41703
+ // neutral GLB rest pose. Trigger the animation directly to avoid that.
41704
+ engine = centralPlant === null || centralPlant === void 0 ? void 0 : centralPlant.stateEngine;
41705
+ _iterator = _createForOfIteratorHelper(switches);
41706
+ try {
41707
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
41708
+ sw = _step.value;
41709
+ current = engine === null || engine === void 0 ? void 0 : engine.getDesired("".concat(panelId, "::").concat(sw.attachmentId, "::mode"));
41710
+ ioBehavMgr.triggerState(sw.attachmentId, 'mode', current !== null && current !== void 0 ? current : true, panelId);
41711
+ }
41712
+ } catch (err) {
41713
+ _iterator.e(err);
41714
+ } finally {
41715
+ _iterator.f();
41716
+ }
41717
+ }
41718
+
41719
+ // ── Floating labels above each switch (on the tilted top) ────────────────
41720
+ lo = spec.labelOffset || {
41721
+ x: 0,
41722
+ y: 0,
41723
+ z: 0.7
41724
+ };
41725
+ labelSize = (_spec$labelSize = spec.labelSize) !== null && _spec$labelSize !== void 0 ? _spec$labelSize : 0.5;
41726
+ switches.forEach(function (sw, i) {
41727
+ var _lo$z;
41728
+ var label = _this2._makeLabel(sw.label || sw.attachmentId, labelSize);
41729
+ if (!label) return;
41730
+ 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));
41731
+ top.add(label);
41732
+ _this2._labels.push(label);
41733
+ });
41734
+ case 3:
41735
+ return _context.a(2);
41736
+ }
41737
+ }, _callee, this);
41738
+ }));
41739
+ function _build() {
41740
+ return _build2.apply(this, arguments);
41741
+ }
41742
+ return _build;
41743
+ }()
41744
+ /**
41745
+ * Build one text label as a camera-facing `THREE.Sprite` (canvas texture),
41746
+ * drawn by the main WebGL renderer alongside the scene — no dependency on the
41747
+ * tooltip CSS2D renderer. Returns null when there is no DOM (headless tests).
41748
+ * @param {string} text
41749
+ * @param {number} worldHeight - label height in world units
41750
+ * @returns {THREE.Sprite|null}
41751
+ * @private
41752
+ */
41753
+ )
41754
+ }, {
41755
+ key: "_makeLabel",
41756
+ value: function _makeLabel(text, worldHeight) {
41757
+ if (typeof document === 'undefined') return null;
41758
+ var dpr = 4; // supersample for crisp text
41759
+ var fontPx = 40;
41760
+ var padX = 14;
41761
+ var padY = 8;
41762
+ var font = "600 ".concat(fontPx, "px -apple-system, \"Segoe UI\", Roboto, sans-serif");
41763
+ var canvas = document.createElement('canvas');
41764
+ var ctx = canvas.getContext('2d');
41765
+ ctx.font = font;
41766
+ var textW = ctx.measureText(text).width;
41767
+ canvas.width = Math.ceil((textW + padX * 2) * dpr);
41768
+ canvas.height = Math.ceil((fontPx + padY * 2) * dpr);
41769
+
41770
+ // Re-apply after the resize (which clears the canvas), scaled by dpr.
41771
+ ctx.scale(dpr, dpr);
41772
+ ctx.font = font;
41773
+ var wCss = canvas.width / dpr;
41774
+ var hCss = canvas.height / dpr;
41775
+ ctx.fillStyle = 'rgba(20,28,38,0.86)';
41776
+ this._roundRect(ctx, 0, 0, wCss, hCss, 8);
41777
+ ctx.fill();
41778
+ ctx.fillStyle = '#eaf2f8';
41779
+ ctx.textAlign = 'center';
41780
+ ctx.textBaseline = 'middle';
41781
+ ctx.fillText(text, wCss / 2, hCss / 2);
41782
+ var texture = new THREE__namespace.CanvasTexture(canvas);
41783
+ texture.minFilter = THREE__namespace.LinearFilter;
41784
+ texture.anisotropy = 4;
41785
+ var material = new THREE__namespace.SpriteMaterial({
41786
+ map: texture,
41787
+ transparent: true,
41788
+ depthTest: false,
41789
+ // always readable, never occluded by the board/switch
41790
+ depthWrite: false
41791
+ });
41792
+ var sprite = new THREE__namespace.Sprite(material);
41793
+ var aspect = canvas.width / canvas.height;
41794
+ sprite.scale.set(worldHeight * aspect, worldHeight, 1);
41795
+ sprite.renderOrder = 999;
41796
+ return sprite;
41797
+ }
41798
+
41799
+ /** Trace a rounded-rect path (fill applied by caller). @private */
41800
+ }, {
41801
+ key: "_roundRect",
41802
+ value: function _roundRect(ctx, x, y, w, h, r) {
41803
+ var rr = Math.min(r, w / 2, h / 2);
41804
+ ctx.beginPath();
41805
+ ctx.moveTo(x + rr, y);
41806
+ ctx.arcTo(x + w, y, x + w, y + h, rr);
41807
+ ctx.arcTo(x + w, y + h, x, y + h, rr);
41808
+ ctx.arcTo(x, y + h, x, y, rr);
41809
+ ctx.arcTo(x, y, x + w, y, rr);
41810
+ ctx.closePath();
41811
+ }
41812
+
41813
+ /**
41814
+ * Remove the panel, its labels, and its registered behaviors (scene reload).
41815
+ * @private
41816
+ */
41817
+ }, {
41818
+ key: "_teardown",
41819
+ value: function _teardown() {
41820
+ this._labels = [];
41821
+ if (this._panelId) {
41822
+ var _this$sceneViewer4;
41823
+ (_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);
41824
+ }
41825
+ if (this._panel) {
41826
+ var _this$sceneViewer5;
41827
+ (_this$sceneViewer5 = this.sceneViewer) === null || _this$sceneViewer5 === void 0 || (_this$sceneViewer5 = _this$sceneViewer5.scene) === null || _this$sceneViewer5 === void 0 || _this$sceneViewer5.remove(this._panel);
41828
+ this._panel.traverse(function (obj) {
41829
+ if (obj.isMesh || obj.isSprite) {
41830
+ var _obj$geometry, _obj$geometry$dispose;
41831
+ (_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);
41832
+ var mats = Array.isArray(obj.material) ? obj.material : [obj.material];
41833
+ mats.forEach(function (m) {
41834
+ var _m$map, _m$map$dispose, _m$dispose;
41835
+ 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
41836
+ m === null || m === void 0 || (_m$dispose = m.dispose) === null || _m$dispose === void 0 || _m$dispose.call(m);
41837
+ });
41838
+ }
41839
+ });
41840
+ this._panel = null;
41841
+ }
41842
+ this._panelId = null;
41843
+ }
41844
+ }, {
41845
+ key: "_doDispose",
41846
+ value: function _doDispose() {
41847
+ var _this$sceneViewer6;
41848
+ if ((_this$sceneViewer6 = this.sceneViewer) !== null && _this$sceneViewer6 !== void 0 && _this$sceneViewer6.off) {
41849
+ this.sceneViewer.off('scene-loaded', this._build);
41850
+ }
41851
+ this._teardown();
41852
+ this.sceneViewer = null;
41853
+ }
41854
+ }]);
41855
+ }(BaseDisposable);
41856
+
40331
41857
  // ─────────────────────────────────────────────────────────────────────────────
40332
41858
  // Flow-direction helpers (module-level)
40333
41859
  // ─────────────────────────────────────────────────────────────────────────────
@@ -40451,6 +41977,8 @@ var CentralPlantInternals = /*#__PURE__*/function () {
40451
41977
  this.centralPlant.managers.viewport2DManager = new Viewport2DManager(this.centralPlant.sceneViewer);
40452
41978
  this.centralPlant.managers.ioBehaviorManager = new IoBehaviorManager(this.centralPlant.sceneViewer);
40453
41979
  this.centralPlant.managers.ioOutlineManager = new IoOutlineManager(this.centralPlant.sceneViewer);
41980
+ this.centralPlant.managers.effectiveVisualManager = new EffectiveVisualManager(this.centralPlant.sceneViewer);
41981
+ this.centralPlant.managers.controlPanelManager = new ControlPanelManager(this.centralPlant.sceneViewer);
40454
41982
 
40455
41983
  // All managers are now stored in the managers collection and will be attached via attachToComponent()
40456
41984
  }
@@ -41884,7 +43412,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
41884
43412
  * Initialize the CentralPlant manager
41885
43413
  *
41886
43414
  * @constructor
41887
- * @version 0.3.58
43415
+ * @version 0.3.59
41888
43416
  * @updated 2025-10-22
41889
43417
  *
41890
43418
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -41923,6 +43451,14 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
41923
43451
  // Initialize internals handler
41924
43452
  _this.internals = new CentralPlantInternals(_this);
41925
43453
 
43454
+ // Portable State Engine: single source of truth for desired/effective state
43455
+ // (plant/group/object/pipe). Owned here; the write door and scene load/save
43456
+ // route through it. The 3D layer/store mirror its output (Framework §12–§13).
43457
+ _this.stateEngine = new StateEngine();
43458
+ _this._unsubscribeStateEngine = _this.stateEngine.subscribe(function (change) {
43459
+ return _this._onEngineStateChange(change);
43460
+ });
43461
+
41926
43462
  // Optional cache primer function (dependency injection)
41927
43463
  _this.cachePrimer = null;
41928
43464
 
@@ -42099,6 +43635,16 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
42099
43635
  timestamp: new Date().toISOString()
42100
43636
  });
42101
43637
 
43638
+ // Load plant / groups / chains / stateRegistry into the State Engine. v3.0
43639
+ // scenes carry these; legacy v2.3 scenes load with safe defaults (§15).
43640
+ if (this.stateEngine) {
43641
+ try {
43642
+ this.stateEngine.load(extractDeclaration(sceneData));
43643
+ } catch (err) {
43644
+ console.warn('⚠️ setImportedSceneData(): stateEngine.load() failed:', err);
43645
+ }
43646
+ }
43647
+
42102
43648
  // Scene behaviors loaded
42103
43649
 
42104
43650
  // Cross-component behaviors are registered by sceneOperationsManager.loadSceneData()
@@ -43047,12 +44593,32 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
43047
44593
  }
43048
44594
 
43049
44595
  /**
43050
- * Internal single path for I/O state changes: persist, animate, link, emit.
44596
+ * Internal single path for I/O state changes. Routes the write through the
44597
+ * State Engine (the one write door, Framework §12); the engine subscription
44598
+ * ({@link CentralPlant#_onEngineStateChange}) mirrors to the state adapter,
44599
+ * animates meshes, and emits. Falls back to a direct write only if the engine
44600
+ * is unavailable.
43051
44601
  * @private
43052
44602
  */
43053
44603
  }, {
43054
44604
  key: "_dispatchIoState",
43055
44605
  value: function _dispatchIoState(attachmentId, stateId, value, parentUuid) {
44606
+ if (this.stateEngine) {
44607
+ this.stateEngine.setDesired(ioAddress(attachmentId, stateId, parentUuid), value);
44608
+ return;
44609
+ }
44610
+ this._dispatchIoStateDirect(attachmentId, stateId, value, parentUuid);
44611
+ }
44612
+
44613
+ /**
44614
+ * Persist to the state adapter, animate meshes, and emit for one I/O data
44615
+ * point. This is the mirror side of the engine (driven from desired, since I/O
44616
+ * data points are ungated) and the legacy fallback when no engine is present.
44617
+ * @private
44618
+ */
44619
+ }, {
44620
+ key: "_dispatchIoStateDirect",
44621
+ value: function _dispatchIoStateDirect(attachmentId, stateId, value, parentUuid) {
43056
44622
  var _this$managers, _this$sceneViewer5, _this$sceneViewer6, _this$managers2, _this$sceneViewer7, _this$sceneViewer8, _this$sceneViewer9;
43057
44623
  var tooltipMgr = ((_this$managers = this.managers) === null || _this$managers === void 0 ? void 0 : _this$managers.componentTooltipManager) || ((_this$sceneViewer5 = this.sceneViewer) === null || _this$sceneViewer5 === void 0 ? void 0 : _this$sceneViewer5.componentTooltipManager) || ((_this$sceneViewer6 = this.sceneViewer) === null || _this$sceneViewer6 === void 0 || (_this$sceneViewer6 = _this$sceneViewer6.managers) === null || _this$sceneViewer6 === void 0 ? void 0 : _this$sceneViewer6.componentTooltipManager);
43058
44624
  var stateAdapter = tooltipMgr === null || tooltipMgr === void 0 ? void 0 : tooltipMgr._stateAdapter;
@@ -43076,6 +44642,25 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
43076
44642
  });
43077
44643
  }
43078
44644
 
44645
+ /**
44646
+ * State Engine subscriber: fan a committed registry change out to the 3D/store
44647
+ * layer. I/O device data points (3-part addresses) mirror to the adapter and
44648
+ * animate; every change (incl. plant/group/object/pipe effective) is surfaced
44649
+ * as `engine-state-changed` for gating-aware visual subscribers (§13).
44650
+ * @private
44651
+ * @param {{ address: string, desired: *, effective: * }} change
44652
+ */
44653
+ }, {
44654
+ key: "_onEngineStateChange",
44655
+ value: function _onEngineStateChange(change) {
44656
+ var _this$sceneViewer0;
44657
+ var io = parseIoAddress(change.address);
44658
+ if (io) {
44659
+ this._dispatchIoStateDirect(io.attachmentId, io.stateId, change.desired, io.parentUuid);
44660
+ }
44661
+ (_this$sceneViewer0 = this.sceneViewer) === null || _this$sceneViewer0 === void 0 || _this$sceneViewer0.emit('engine-state-changed', change);
44662
+ }
44663
+
43079
44664
  /**
43080
44665
  * Configure the state adapter on both tooltip and behavior managers.
43081
44666
  * @param {{ getState: Function, setState: Function }} stateAdapter
@@ -43083,16 +44668,16 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
43083
44668
  }, {
43084
44669
  key: "configureStateAdapter",
43085
44670
  value: function configureStateAdapter(stateAdapter) {
43086
- var _this$managers3, _this$sceneViewer0, _this$managers4, _this$sceneViewer10, _this$sceneViewer11;
43087
- var tooltipMgr = ((_this$managers3 = this.managers) === null || _this$managers3 === void 0 ? void 0 : _this$managers3.componentTooltipManager) || ((_this$sceneViewer0 = this.sceneViewer) === null || _this$sceneViewer0 === void 0 ? void 0 : _this$sceneViewer0.componentTooltipManager);
44671
+ var _this$managers3, _this$sceneViewer1, _this$managers4, _this$sceneViewer11, _this$sceneViewer12;
44672
+ var tooltipMgr = ((_this$managers3 = this.managers) === null || _this$managers3 === void 0 ? void 0 : _this$managers3.componentTooltipManager) || ((_this$sceneViewer1 = this.sceneViewer) === null || _this$sceneViewer1 === void 0 ? void 0 : _this$sceneViewer1.componentTooltipManager);
43088
44673
  if (tooltipMgr !== null && tooltipMgr !== void 0 && tooltipMgr.configure) {
43089
- var _this$sceneViewer1;
44674
+ var _this$sceneViewer10;
43090
44675
  tooltipMgr.configure(stateAdapter);
43091
- if ((_this$sceneViewer1 = this.sceneViewer) !== null && _this$sceneViewer1 !== void 0 && _this$sceneViewer1.managers) {
44676
+ if ((_this$sceneViewer10 = this.sceneViewer) !== null && _this$sceneViewer10 !== void 0 && _this$sceneViewer10.managers) {
43092
44677
  this.sceneViewer.managers.componentTooltipManager = tooltipMgr;
43093
44678
  }
43094
44679
  }
43095
- var ioBehavMgr = ((_this$managers4 = this.managers) === null || _this$managers4 === void 0 ? void 0 : _this$managers4.ioBehaviorManager) || ((_this$sceneViewer10 = this.sceneViewer) === null || _this$sceneViewer10 === void 0 || (_this$sceneViewer10 = _this$sceneViewer10.managers) === null || _this$sceneViewer10 === void 0 ? void 0 : _this$sceneViewer10.ioBehaviorManager) || ((_this$sceneViewer11 = this.sceneViewer) === null || _this$sceneViewer11 === void 0 ? void 0 : _this$sceneViewer11.ioBehaviorManager);
44680
+ var ioBehavMgr = ((_this$managers4 = this.managers) === null || _this$managers4 === void 0 ? void 0 : _this$managers4.ioBehaviorManager) || ((_this$sceneViewer11 = this.sceneViewer) === null || _this$sceneViewer11 === void 0 || (_this$sceneViewer11 = _this$sceneViewer11.managers) === null || _this$sceneViewer11 === void 0 ? void 0 : _this$sceneViewer11.ioBehaviorManager) || ((_this$sceneViewer12 = this.sceneViewer) === null || _this$sceneViewer12 === void 0 ? void 0 : _this$sceneViewer12.ioBehaviorManager);
43096
44681
  if (ioBehavMgr !== null && ioBehavMgr !== void 0 && ioBehavMgr.configure) {
43097
44682
  ioBehavMgr.configure(stateAdapter);
43098
44683
  }
@@ -43127,8 +44712,8 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
43127
44712
  }, {
43128
44713
  key: "getSceneAttachments",
43129
44714
  value: function getSceneAttachments() {
43130
- var _this$sceneViewer12;
43131
- var scene = (_this$sceneViewer12 = this.sceneViewer) === null || _this$sceneViewer12 === void 0 ? void 0 : _this$sceneViewer12.scene;
44715
+ var _this$sceneViewer13;
44716
+ var scene = (_this$sceneViewer13 = this.sceneViewer) === null || _this$sceneViewer13 === void 0 ? void 0 : _this$sceneViewer13.scene;
43132
44717
  if (!scene) return [];
43133
44718
  var results = [];
43134
44719
  scene.traverse(function (obj) {
@@ -44909,12 +46494,14 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
44909
46494
  }, {
44910
46495
  key: "clearScene",
44911
46496
  value: function clearScene() {
44912
- var _this$sceneViewer13;
46497
+ var _this$sceneViewer14, _this$stateEngine;
44913
46498
  this.importedSceneData = null;
44914
- var ioBehavMgr = (_this$sceneViewer13 = this.sceneViewer) === null || _this$sceneViewer13 === void 0 || (_this$sceneViewer13 = _this$sceneViewer13.managers) === null || _this$sceneViewer13 === void 0 ? void 0 : _this$sceneViewer13.ioBehaviorManager;
46499
+ var ioBehavMgr = (_this$sceneViewer14 = this.sceneViewer) === null || _this$sceneViewer14 === void 0 || (_this$sceneViewer14 = _this$sceneViewer14.managers) === null || _this$sceneViewer14 === void 0 ? void 0 : _this$sceneViewer14.ioBehaviorManager;
44915
46500
  if (ioBehavMgr) {
44916
46501
  ioBehavMgr.setCrossComponentBehaviors([]);
44917
46502
  }
46503
+ // Reset the registry so pipe/object keys from the old scene don't linger (§15).
46504
+ (_this$stateEngine = this.stateEngine) === null || _this$stateEngine === void 0 || _this$stateEngine.reset();
44918
46505
  if (this.sceneViewer && this.sceneViewer.scene) {
44919
46506
  this.sceneViewer.scene.clear();
44920
46507
  }
@@ -44965,8 +46552,16 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
44965
46552
  }, {
44966
46553
  key: "_doDispose",
44967
46554
  value: function _doDispose() {
46555
+ var _this$stateEngine2;
44968
46556
  console.log('🧹 Disposing CentralPlant using enhanced disposal patterns...');
44969
46557
 
46558
+ // Tear down the State Engine subscription and registry.
46559
+ if (this._unsubscribeStateEngine) {
46560
+ this._unsubscribeStateEngine();
46561
+ this._unsubscribeStateEngine = null;
46562
+ }
46563
+ (_this$stateEngine2 = this.stateEngine) === null || _this$stateEngine2 === void 0 || _this$stateEngine2.reset();
46564
+
44970
46565
  // Use batch disposal for all managers
44971
46566
  var managers = Object.values(this.managers).filter(function (manager) {
44972
46567
  return manager && typeof manager.dispose === 'function';
@@ -50840,8 +52435,10 @@ exports.SceneInitializationManager = SceneInitializationManager;
50840
52435
  exports.SceneOperationsManager = SceneOperationsManager;
50841
52436
  exports.SceneTooltipsManager = SceneTooltipsManager;
50842
52437
  exports.SnapshotManager = SnapshotManager;
52438
+ exports.StateEngine = StateEngine;
50843
52439
  exports.ThreeJSResourceManager = ThreeJSResourceManager;
50844
52440
  exports.applyCrossComponentBehaviors = applyCrossComponentBehaviors;
52441
+ exports.applyDeclarationToExport = applyDeclarationToExport;
50845
52442
  exports.applyDefaultIoDeviceStates = applyDefaultIoDeviceStates;
50846
52443
  exports.applyModelRootTranslation = applyModelRootTranslation;
50847
52444
  exports.applyModelRootTranslationFromWorldBase = applyModelRootTranslationFromWorldBase;
@@ -50855,6 +52452,7 @@ exports.clearS3Cache = clearS3Cache;
50855
52452
  exports.collectSceneLibraryIds = collectSceneLibraryIds;
50856
52453
  exports.createPathfindingRequest = createPathfindingRequest;
50857
52454
  exports.createTransformControls = createTransformControls;
52455
+ exports.extractDeclaration = extractDeclaration;
50858
52456
  exports.findObjectByHardcodedUuid = findObjectByHardcodedUuid;
50859
52457
  exports.formatCacheExpiry = formatCacheExpiry;
50860
52458
  exports.generateUniqueComponentId = generateUniqueComponentId;
@@ -50880,6 +52478,7 @@ exports.getObjectsByType = getObjectsByType;
50880
52478
  exports.getScopedAttachmentKey = getScopedAttachmentKey;
50881
52479
  exports.getThumbnailKey = getThumbnailKey;
50882
52480
  exports.getUserOnlyCacheStats = getUserOnlyCacheStats;
52481
+ exports.ioAddress = ioAddress;
50883
52482
  exports.isCached = isCached;
50884
52483
  exports.isComponent = isComponent;
50885
52484
  exports.isComputed = isComputed;
@@ -50891,14 +52490,17 @@ exports.isSegment = isSegment;
50891
52490
  exports.isThumbnailCached = isThumbnailCached;
50892
52491
  exports.loadCrossComponentBehaviors = loadCrossComponentBehaviors;
50893
52492
  exports.loadTextureSetAndCreateMaterial = loadTextureSetAndCreateMaterial;
52493
+ exports.makeAddress = makeAddress;
50894
52494
  exports.markAsComputed = markAsComputed;
50895
52495
  exports.markAsDeclared = markAsDeclared;
50896
52496
  exports.measureS3Transfer = measureS3Transfer;
50897
52497
  exports.modelOffsetToWorldDelta = modelOffsetToWorldDelta;
50898
52498
  exports.modelPreloader = modelPreloader;
50899
52499
  exports.normalizeBehavior = normalizeBehavior;
52500
+ exports.offValue = offValue;
50900
52501
  exports.parseCrossBehavior = parseCrossBehavior;
50901
52502
  exports.parseIntraBehavior = parseIntraBehavior;
52503
+ exports.parseIoAddress = parseIoAddress;
50902
52504
  exports.preloadLocalFiles = preloadLocalFiles;
50903
52505
  exports.preloadS3Objects = preloadS3Objects;
50904
52506
  exports.refreshSceneIntraBehaviors = refreshSceneIntraBehaviors;
@@ -50915,3 +52517,4 @@ exports.scanSceneIoEndpoints = scanSceneIoEndpoints;
50915
52517
  exports.sceneViewer = sceneViewer;
50916
52518
  exports.shouldRemoveOnRegeneration = shouldRemoveOnRegeneration;
50917
52519
  exports.switchCachePartition = switchCachePartition;
52520
+ exports.valuesEqual = valuesEqual;