@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,90 @@
1
+ import { slicedToArray as _slicedToArray } from '../../_virtual/_rollupPluginBabelHelpers.js';
2
+ import { makeAddress } from './stateEngine.js';
3
+ import { getScopedAttachmentKey } from '../utils/behaviorDispatch.js';
4
+
5
+ /**
6
+ * Registry address for an I/O device data point.
7
+ * Shape: `${parentUuid}::${attachmentId}::${stateId}` (matches §17
8
+ * `pump-001::start-switch::mode`). The scoped key is `${parentUuid}::${attachmentId}`.
9
+ *
10
+ * @param {string} attachmentId
11
+ * @param {string} stateId
12
+ * @param {string} [parentUuid]
13
+ * @returns {string}
14
+ */
15
+ function ioAddress(attachmentId, stateId, parentUuid) {
16
+ return makeAddress(getScopedAttachmentKey(attachmentId, parentUuid), stateId);
17
+ }
18
+
19
+ /**
20
+ * Parse an I/O device address back into its parts. Returns null for
21
+ * plant/group/object/pipe addresses (2-part) — I/O device data points are 3-part
22
+ * because a device always has a parent component (§8; free-floating I/O rejected).
23
+ *
24
+ * @param {string} address
25
+ * @returns {{ parentUuid: string, attachmentId: string, stateId: string, scopedKey: string } | null}
26
+ */
27
+ function parseIoAddress(address) {
28
+ if (typeof address !== 'string') return null;
29
+ var parts = address.split('::');
30
+ if (parts.length !== 3) return null;
31
+ var _parts = _slicedToArray(parts, 3),
32
+ parentUuid = _parts[0],
33
+ attachmentId = _parts[1],
34
+ stateId = _parts[2];
35
+ return {
36
+ parentUuid: parentUuid,
37
+ attachmentId: attachmentId,
38
+ stateId: stateId,
39
+ scopedKey: "".concat(parentUuid, "::").concat(attachmentId)
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Read a State Engine declaration out of scene data, tolerating legacy scenes.
45
+ * v3.0 scenes carry `plant` / `groups` / `chains` / `stateRegistry`; older v2.3
46
+ * scenes have none of these and load with safe defaults (plant power on, no
47
+ * groups, no chains) so nothing crashes on unknown/missing fields (§15).
48
+ *
49
+ * @param {object} [sceneData]
50
+ * @returns {{ plant: object, groups: object, chains: object[], stateRegistry: object }}
51
+ */
52
+ function extractDeclaration() {
53
+ var sceneData = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
54
+ return {
55
+ plant: sceneData.plant || {
56
+ states: {
57
+ power: {
58
+ type: 'Boolean',
59
+ default: true,
60
+ gate: true
61
+ }
62
+ }
63
+ },
64
+ groups: sceneData.groups || {},
65
+ chains: Array.isArray(sceneData.chains) ? sceneData.chains : [],
66
+ stateRegistry: sceneData.stateRegistry || {}
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Merge a State Engine's current declaration (plant / groups / chains /
72
+ * stateRegistry) into an export object, bumping it to version 3.0. Mutates and
73
+ * returns `exportData`.
74
+ *
75
+ * @param {object} exportData - the scene export object being assembled
76
+ * @param {{ toDeclaration: () => object }} engine
77
+ * @returns {object} exportData
78
+ */
79
+ function applyDeclarationToExport(exportData, engine) {
80
+ if (!exportData || !engine || typeof engine.toDeclaration !== 'function') return exportData;
81
+ var declaration = engine.toDeclaration();
82
+ exportData.version = '3.0';
83
+ exportData.plant = declaration.plant;
84
+ exportData.groups = declaration.groups;
85
+ if (declaration.chains.length > 0) exportData.chains = declaration.chains;
86
+ exportData.stateRegistry = declaration.stateRegistry;
87
+ return exportData;
88
+ }
89
+
90
+ export { applyDeclarationToExport, extractDeclaration, ioAddress, parseIoAddress };
@@ -1,5 +1,7 @@
1
1
  export { default as CentralPlant } from './core/centralPlant.js';
2
2
  export { default as sceneViewer } from './core/sceneViewer.js';
3
+ export { StateEngine, makeAddress, offValue, valuesEqual } from './core/stateEngine.js';
4
+ export { applyDeclarationToExport, extractDeclaration, ioAddress, parseIoAddress } from './core/stateEngineScene.js';
3
5
  export { findObjectByHardcodedUuid, generateUniqueComponentId, generateUuidFromName, getHardcodedUuid } from './utils/nameUtils.js';
4
6
  export { FLOW_ATTRIBUTE_KEYS, PathData, createPathfindingRequest } from './core/pathfindingData.js';
5
7
  export { getObjectTypeName, getObjectsByType, isComponent, isComputed, isConnector, isDeclared, isExportable, isGateway, isSegment, markAsComputed, markAsDeclared, shouldRemoveOnRegeneration } from './utils/objectTypes.js';
@@ -140,6 +140,12 @@ var TransformControlsManager = /*#__PURE__*/function () {
140
140
  // Update bounding box if the transformed object is in current selection
141
141
  if (_this.selectedObjects.includes(eventData.object)) {
142
142
  console.log('🔲 Updating bounding boxes for selected objects after transform');
143
+ // Reposition the transform gizmo (and its selection group) to follow the
144
+ // object's new position. Programmatic transforms (e.g. the Transform tab's
145
+ // Translate/Rotate buttons) move the object directly without touching the
146
+ // multi-selection group the gizmo is attached to, so without this the widget
147
+ // would stay at the pre-transform centroid.
148
+ _this.syncSelectionToObjects();
143
149
  _this.updateBoundingBox();
144
150
  }
145
151
  };
@@ -1246,6 +1252,59 @@ var TransformControlsManager = /*#__PURE__*/function () {
1246
1252
  console.log("\uD83D\uDCE6 Multi-selection group created at centroid:", centroid.toArray());
1247
1253
  }
1248
1254
 
1255
+ /**
1256
+ * Re-sync the multi-selection group (and the attached transform gizmo) to the
1257
+ * current world positions of the selected objects, in place.
1258
+ *
1259
+ * Used after a *programmatic* transform (e.g. the Transform tab's Translate /
1260
+ * Rotate buttons), which mutates the object directly and does not move the
1261
+ * selection group the gizmo is attached to. Unlike a full updateMultiSelection()
1262
+ * this keeps the same group instance so the gizmo doesn't detach/re-attach.
1263
+ *
1264
+ * It also refreshes the cached original transforms so a subsequent gizmo drag
1265
+ * computes its delta from the object's new position rather than jumping.
1266
+ */
1267
+ }, {
1268
+ key: "syncSelectionToObjects",
1269
+ value: function syncSelectionToObjects() {
1270
+ var _this$transformContro;
1271
+ if (!this.multiSelectionGroup || this.selectedObjects.length === 0) {
1272
+ return;
1273
+ }
1274
+
1275
+ // Recompute the centroid from the objects' current world positions.
1276
+ var centroid = new THREE.Vector3();
1277
+ this.selectedObjects.forEach(function (obj) {
1278
+ obj.updateMatrixWorld(true);
1279
+ var worldPos = new THREE.Vector3();
1280
+ obj.getWorldPosition(worldPos);
1281
+ centroid.add(worldPos);
1282
+ });
1283
+ centroid.divideScalar(this.selectedObjects.length);
1284
+
1285
+ // Move the group (and thus the attached gizmo) to the new centroid. Reset any
1286
+ // rotation so the gizmo matches the freshly-created-group convention.
1287
+ this.multiSelectionGroup.position.copy(centroid);
1288
+ this.multiSelectionGroup.rotation.set(0, 0, 0);
1289
+ this.multiSelectionGroup.updateMatrixWorld(true);
1290
+
1291
+ // Refresh cached originals so applyRealtimeTransform's delta stays correct on
1292
+ // the next drag (its "original centroid" is derived from these values).
1293
+ this.selectedObjects.forEach(function (obj) {
1294
+ obj.userData._multiSelectOriginalPosition = obj.position.clone();
1295
+ obj.userData._multiSelectOriginalRotation = obj.rotation.clone();
1296
+ obj.userData._multiSelectOriginalScale = obj.scale.clone();
1297
+ var worldPos = new THREE.Vector3();
1298
+ obj.getWorldPosition(worldPos);
1299
+ obj.userData._multiSelectOriginalWorldPosition = worldPos;
1300
+ });
1301
+
1302
+ // Keep the gizmo's world matrix in sync with the repositioned group.
1303
+ if ((_this$transformContro = this.transformControls) !== null && _this$transformContro !== void 0 && _this$transformContro.object) {
1304
+ this.transformControls.updateMatrixWorld(true);
1305
+ }
1306
+ }
1307
+
1249
1308
  /**
1250
1309
  * Create a bounding box that encompasses all selected objects
1251
1310
  */
@@ -1525,10 +1584,10 @@ var TransformControlsManager = /*#__PURE__*/function () {
1525
1584
  }, {
1526
1585
  key: "toggleTransformMode",
1527
1586
  value: function toggleTransformMode() {
1528
- var _this$transformContro;
1587
+ var _this$transformContro2;
1529
1588
  var newMode = this.currentMode === 'translate' ? 'rotate' : 'translate';
1530
1589
  this.setMode(newMode);
1531
- if ((_this$transformContro = this.transformControls) !== null && _this$transformContro !== void 0 && _this$transformContro.object) {
1590
+ if ((_this$transformContro2 = this.transformControls) !== null && _this$transformContro2 !== void 0 && _this$transformContro2.object) {
1532
1591
  this.transformControls.updateMatrixWorld(true);
1533
1592
  }
1534
1593
  }
@@ -10,6 +10,9 @@ var TEMP_MIN = 0;
10
10
  /** Maximum temperature in the colour ramp (maps to pure red). */
11
11
  var TEMP_MAX = 100;
12
12
 
13
+ /** Colour a pipe is painted when its effective flow is gated off (§6.1). */
14
+ var PIPE_OFF_GRAY = '#3a3a3a';
15
+
13
16
  /**
14
17
  * Convert a temperature value to a CSS hex colour string using a blue→red HSL
15
18
  * ramp. Values are clamped to [TEMP_MIN, TEMP_MAX].
@@ -47,22 +50,78 @@ var PathFlowManager = /*#__PURE__*/function (_BaseDisposable) {
47
50
  * @type {Map<string, Record<string, any>>}
48
51
  */
49
52
  _this._derivedStore = new Map();
53
+
54
+ /**
55
+ * Paths whose effective flow is gated off (State Engine §6.1). While a path
56
+ * is in this set it is painted {@link PIPE_OFF_GRAY} and temperature updates
57
+ * are suppressed so they don't override the off-look.
58
+ * @type {Set<string>}
59
+ */
60
+ _this._flowGatedOff = new Set();
61
+
62
+ /**
63
+ * Baseline (on-state) colour snapshotted the moment a path is gated off, so
64
+ * it can be restored exactly when flow returns — including pipes with no
65
+ * declared flowTemperature, whose colour is the base pipe material.
66
+ * @type {Map<string, string>}
67
+ */
68
+ _this._flowBaseColor = new Map();
50
69
  return _this;
51
70
  }
52
71
 
53
- // ── Public API attribute write ──────────────────────────────────────────
72
+ // ── Flow gating (State Engine effective → 3D bridge, §6.1) ─────────────────
54
73
 
55
74
  /**
56
- * Set a declared (persistent) flow attribute on a path.
57
- * The value is stored in PathData and will be serialised with the scene.
58
- * Triggers a visual update.
75
+ * Gate a path's flow visual on/off from the State Engine's effective `flow`.
76
+ * Off paint the shared per-path material gray; on restore the temperature
77
+ * colour. The off state is sticky (see {@link _flowGatedOff}).
59
78
  *
60
- * @param {string} pathId - e.g. "PUMP-1-CONN-1-->CHILLER-1-CONN-2"
61
- * @param {string} key - One of FLOW_ATTRIBUTE_KEYS
62
- * @param {any} value
79
+ * @param {string} pathId - `${from}-->${to}`
80
+ * @param {boolean} enabled - Effective flow
63
81
  */
64
82
  _inherits(PathFlowManager, _BaseDisposable);
65
83
  return _createClass(PathFlowManager, [{
84
+ key: "setFlowEnabled",
85
+ value: function setFlowEnabled(pathId, enabled) {
86
+ var split = this._splitPathId(pathId);
87
+ if (!split) return;
88
+ var renderingManager = this._getRenderingManager();
89
+ if (enabled) {
90
+ this._flowGatedOff.delete(pathId);
91
+ // Prefer a temperature-derived colour if one is declared; otherwise
92
+ // restore the baseline snapshotted when the pipe was gated off (this is
93
+ // what fixes plain, non-temperature pipes getting stuck gray).
94
+ var temp = this.resolve(pathId, 'flowTemperature');
95
+ if (temp !== null && temp !== undefined) {
96
+ this.applyVisualizationForPath(pathId);
97
+ } else if (this._flowBaseColor.has(pathId)) {
98
+ renderingManager === null || renderingManager === void 0 || renderingManager.updatePathColor(split.from, split.to, this._flowBaseColor.get(pathId));
99
+ }
100
+ this._flowBaseColor.delete(pathId);
101
+ } else {
102
+ // Snapshot the current (on-state) colour once, before overpainting gray.
103
+ if (!this._flowGatedOff.has(pathId) && renderingManager) {
104
+ var _renderingManager$get;
105
+ var base = (_renderingManager$get = renderingManager.getPathColorHex) === null || _renderingManager$get === void 0 ? void 0 : _renderingManager$get.call(renderingManager, pathId);
106
+ if (base) this._flowBaseColor.set(pathId, base);
107
+ }
108
+ this._flowGatedOff.add(pathId);
109
+ renderingManager === null || renderingManager === void 0 || renderingManager.updatePathColor(split.from, split.to, PIPE_OFF_GRAY);
110
+ }
111
+ }
112
+
113
+ // ── Public API — attribute write ──────────────────────────────────────────
114
+
115
+ /**
116
+ * Set a declared (persistent) flow attribute on a path.
117
+ * The value is stored in PathData and will be serialised with the scene.
118
+ * Triggers a visual update.
119
+ *
120
+ * @param {string} pathId - e.g. "PUMP-1-CONN-1-->CHILLER-1-CONN-2"
121
+ * @param {string} key - One of FLOW_ATTRIBUTE_KEYS
122
+ * @param {any} value
123
+ */
124
+ }, {
66
125
  key: "setDeclared",
67
126
  value: function setDeclared(pathId, key, value) {
68
127
  var pd = this._getPathData(pathId);
@@ -168,6 +227,13 @@ var PathFlowManager = /*#__PURE__*/function (_BaseDisposable) {
168
227
  }, {
169
228
  key: "applyVisualizationForPath",
170
229
  value: function applyVisualizationForPath(pathId) {
230
+ // Effective flow gated off (§6.1) — keep the off-look, ignore temperature.
231
+ if (this._flowGatedOff.has(pathId)) {
232
+ var _this$_getRenderingMa;
233
+ var _split = this._splitPathId(pathId);
234
+ if (_split) (_this$_getRenderingMa = this._getRenderingManager()) === null || _this$_getRenderingMa === void 0 || _this$_getRenderingMa.updatePathColor(_split.from, _split.to, PIPE_OFF_GRAY);
235
+ return;
236
+ }
171
237
  var temp = this.resolve(pathId, 'flowTemperature');
172
238
  if (temp === null || temp === undefined) {
173
239
  return; // No temperature declared — leave default pipe color
@@ -175,17 +241,30 @@ var PathFlowManager = /*#__PURE__*/function (_BaseDisposable) {
175
241
  var color = temperatureToColor(temp);
176
242
  var renderingManager = this._getRenderingManager();
177
243
  if (!renderingManager) return;
244
+ var split = this._splitPathId(pathId);
245
+ if (!split) return;
246
+ renderingManager.updatePathColor(split.from, split.to, color);
247
+ console.log("\uD83C\uDF21\uFE0F PathFlowManager: \"".concat(pathId, "\" \u2192 flowTemperature ").concat(temp, " \u2192 ").concat(color));
248
+ }
178
249
 
179
- // pathId = "from-->to"; extract the two halves
250
+ /**
251
+ * Split a `${from}-->${to}` pathId into its two connector halves.
252
+ * @param {string} pathId
253
+ * @returns {{ from: string, to: string }|null} null if malformed
254
+ * @private
255
+ */
256
+ }, {
257
+ key: "_splitPathId",
258
+ value: function _splitPathId(pathId) {
180
259
  var sepIdx = pathId.indexOf('-->');
181
260
  if (sepIdx === -1) {
182
261
  console.warn("PathFlowManager: malformed pathId \"".concat(pathId, "\""));
183
- return;
262
+ return null;
184
263
  }
185
- var from = pathId.slice(0, sepIdx);
186
- var to = pathId.slice(sepIdx + 3);
187
- renderingManager.updatePathColor(from, to, color);
188
- console.log("\uD83C\uDF21\uFE0F PathFlowManager: \"".concat(pathId, "\" \u2192 flowTemperature ").concat(temp, " \u2192 ").concat(color));
264
+ return {
265
+ from: pathId.slice(0, sepIdx),
266
+ to: pathId.slice(sepIdx + 3)
267
+ };
189
268
  }
190
269
 
191
270
  /**
@@ -217,6 +296,8 @@ var PathFlowManager = /*#__PURE__*/function (_BaseDisposable) {
217
296
  key: "dispose",
218
297
  value: function dispose() {
219
298
  this._derivedStore.clear();
299
+ this._flowGatedOff.clear();
300
+ this._flowBaseColor.clear();
220
301
  this.sceneViewer = null;
221
302
  _superPropGet(PathFlowManager, "dispose", this, 3)([]);
222
303
  }
@@ -64,6 +64,21 @@ var PathRenderingManager = /*#__PURE__*/function (_BaseDisposable) {
64
64
  }
65
65
  }
66
66
 
67
+ /**
68
+ * Read the current color of a path's shared material as a `#rrggbb` string,
69
+ * or null if the path has no material. Used to snapshot a pipe's baseline
70
+ * colour before it is temporarily overpainted (e.g. flow gating).
71
+ *
72
+ * @param {string} pathId - "${from}-->${to}"
73
+ * @returns {string|null}
74
+ */
75
+ }, {
76
+ key: "getPathColorHex",
77
+ value: function getPathColorHex(pathId) {
78
+ var mat = this._pathMaterials.get(pathId);
79
+ return mat ? "#".concat(mat.color.getHexString()) : null;
80
+ }
81
+
67
82
  /**
68
83
  * Get path colors for visual distinction
69
84
  * @param {number} index - Path index
@@ -120,7 +120,6 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
120
120
  if (!ioDeviceObject || !this._stateAdapter) return;
121
121
  var ud = ioDeviceObject.userData;
122
122
  var attachmentId = ud === null || ud === void 0 ? void 0 : ud.attachmentId;
123
- var dataPoints = (ud === null || ud === void 0 ? void 0 : ud.dataPoints) || [];
124
123
  if (!attachmentId) return;
125
124
 
126
125
  // Walk up to find parent component UUID for scoped state/behavior handling
@@ -135,6 +134,13 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
135
134
  obj = obj.parent;
136
135
  }
137
136
 
137
+ // Derive the device's data points from its behaviorConfig (same resolver the
138
+ // drag path uses). `userData.dataPoints` is not populated for attached
139
+ // devices, so reading it directly would find nothing — resolve instead, and
140
+ // keep the legacy field only as a fallback.
141
+ var resolved = resolveDataPoints(parentUuid, attachmentId, ud, getIoBehaviorManager(this.sceneViewer), null);
142
+ var dataPoints = resolved !== null && resolved !== void 0 && resolved.length ? resolved : (ud === null || ud === void 0 ? void 0 : ud.dataPoints) || [];
143
+
138
144
  // Create a scoped attachment key to prevent state sharing between instances
139
145
  // of the same smart component that share the same attachmentId
140
146
  var scopedAttachmentId = getScopedAttachmentKey(attachmentId, parentUuid);