@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.
- package/dist/bundle/index.js +1635 -32
- package/dist/cjs/src/core/centralPlant.js +80 -11
- package/dist/cjs/src/core/centralPlantInternals.js +4 -0
- package/dist/cjs/src/core/centralPlantValidator.js +10 -6
- package/dist/cjs/src/core/stateEngine.js +733 -0
- package/dist/cjs/src/core/stateEngineScene.js +97 -0
- package/dist/cjs/src/index.js +10 -0
- package/dist/cjs/src/managers/pathfinding/PathFlowManager.js +94 -13
- package/dist/cjs/src/managers/pathfinding/PathRenderingManager.js +15 -0
- package/dist/cjs/src/managers/scene/componentTooltipManager.js +7 -1
- package/dist/cjs/src/managers/scene/controlPanelManager.js +377 -0
- package/dist/cjs/src/managers/scene/sceneExportManager.js +10 -1
- package/dist/cjs/src/managers/state/EffectiveVisualManager.js +270 -0
- package/dist/esm/src/core/centralPlant.js +80 -11
- package/dist/esm/src/core/centralPlantInternals.js +4 -0
- package/dist/esm/src/core/centralPlantValidator.js +10 -6
- package/dist/esm/src/core/stateEngine.js +725 -0
- package/dist/esm/src/core/stateEngineScene.js +90 -0
- package/dist/esm/src/index.js +2 -0
- package/dist/esm/src/managers/pathfinding/PathFlowManager.js +94 -13
- package/dist/esm/src/managers/pathfinding/PathRenderingManager.js +15 -0
- package/dist/esm/src/managers/scene/componentTooltipManager.js +7 -1
- package/dist/esm/src/managers/scene/controlPanelManager.js +352 -0
- package/dist/esm/src/managers/scene/sceneExportManager.js +10 -1
- package/dist/esm/src/managers/state/EffectiveVisualManager.js +266 -0
- package/dist/index.d.ts +85 -0
- 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 };
|
package/dist/esm/src/index.js
CHANGED
|
@@ -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';
|
|
@@ -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
|
-
// ──
|
|
72
|
+
// ── Flow gating (State Engine effective → 3D bridge, §6.1) ─────────────────
|
|
54
73
|
|
|
55
74
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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
|
|
61
|
-
* @param {
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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);
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { inherits as _inherits, createClass as _createClass, classCallCheck as _classCallCheck, callSuper as _callSuper, asyncToGenerator as _asyncToGenerator, regenerator as _regenerator, createForOfIteratorHelper as _createForOfIteratorHelper } from '../../../_virtual/_rollupPluginBabelHelpers.js';
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
import { BaseDisposable } from '../../core/baseDisposable.js';
|
|
4
|
+
import modelPreloader from '../../rendering/modelPreloader.js';
|
|
5
|
+
import { attachIODevicesToComponent } from '../../utils/ioDeviceUtils.js';
|
|
6
|
+
import { registerBehaviorsForComponent } from '../../utils/behaviorRegistration.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* X offsets for `count` switches evenly spaced and centered on 0.
|
|
10
|
+
* @param {number} count
|
|
11
|
+
* @param {number} spacing
|
|
12
|
+
* @returns {number[]}
|
|
13
|
+
*/
|
|
14
|
+
function computeSwitchRowX(count, spacing) {
|
|
15
|
+
var start = -((count - 1) * spacing) / 2;
|
|
16
|
+
return Array.from({
|
|
17
|
+
length: count
|
|
18
|
+
}, function (_, i) {
|
|
19
|
+
return start + i * spacing;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
var ControlPanelManager = /*#__PURE__*/function (_BaseDisposable) {
|
|
23
|
+
/**
|
|
24
|
+
* @param {Object} sceneViewer - The central sceneViewer hub
|
|
25
|
+
*/
|
|
26
|
+
function ControlPanelManager(sceneViewer) {
|
|
27
|
+
var _this;
|
|
28
|
+
_classCallCheck(this, ControlPanelManager);
|
|
29
|
+
_this = _callSuper(this, ControlPanelManager);
|
|
30
|
+
_this.sceneViewer = sceneViewer;
|
|
31
|
+
_this._panel = null;
|
|
32
|
+
_this._labels = [];
|
|
33
|
+
_this._panelId = null;
|
|
34
|
+
_this._build = _this._build.bind(_this);
|
|
35
|
+
if (sceneViewer !== null && sceneViewer !== void 0 && sceneViewer.on) {
|
|
36
|
+
sceneViewer.on('scene-loaded', _this._build);
|
|
37
|
+
}
|
|
38
|
+
return _this;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build (or rebuild) the panel from the current scene's `controlPanel` spec.
|
|
43
|
+
* @private
|
|
44
|
+
*/
|
|
45
|
+
_inherits(ControlPanelManager, _BaseDisposable);
|
|
46
|
+
return _createClass(ControlPanelManager, [{
|
|
47
|
+
key: "_build",
|
|
48
|
+
value: (function () {
|
|
49
|
+
var _build2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
|
|
50
|
+
var _this$sceneViewer,
|
|
51
|
+
_centralPlant$importe,
|
|
52
|
+
_this$sceneViewer2,
|
|
53
|
+
_b$width,
|
|
54
|
+
_b$height,
|
|
55
|
+
_b$thickness,
|
|
56
|
+
_desk$tilt,
|
|
57
|
+
_desk$height,
|
|
58
|
+
_desk$legThickness,
|
|
59
|
+
_desk$legInset,
|
|
60
|
+
_spec$switchSpacing,
|
|
61
|
+
_this$sceneViewer3,
|
|
62
|
+
_spec$labelSize,
|
|
63
|
+
_this2 = this;
|
|
64
|
+
var centralPlant, spec, scene, panelId, panel, p, r, d2r, scale, b, width, depth, thickness, desk, tilt, deskHeight, legThickness, legInset, legColor, top, board, legMat, halfW, halfD, _i, _arr, ly, cornerZ, legH, yPos, _i2, _arr2, lx, leg, switches, spacing, xs, topZ, attachedDevices, componentData, switchScale, ioBehavMgr, engine, _iterator, _step, sw, current, lo, labelSize;
|
|
65
|
+
return _regenerator().w(function (_context) {
|
|
66
|
+
while (1) switch (_context.n) {
|
|
67
|
+
case 0:
|
|
68
|
+
this._teardown();
|
|
69
|
+
centralPlant = (_this$sceneViewer = this.sceneViewer) === null || _this$sceneViewer === void 0 ? void 0 : _this$sceneViewer.centralPlant;
|
|
70
|
+
spec = centralPlant === null || centralPlant === void 0 || (_centralPlant$importe = centralPlant.importedSceneData) === null || _centralPlant$importe === void 0 ? void 0 : _centralPlant$importe.controlPanel;
|
|
71
|
+
scene = (_this$sceneViewer2 = this.sceneViewer) === null || _this$sceneViewer2 === void 0 ? void 0 : _this$sceneViewer2.scene;
|
|
72
|
+
if (!(!spec || !scene)) {
|
|
73
|
+
_context.n = 1;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
return _context.a(2);
|
|
77
|
+
case 1:
|
|
78
|
+
panelId = spec.id || 'CONTROL-PANEL-1';
|
|
79
|
+
this._panelId = panelId;
|
|
80
|
+
panel = new THREE.Group();
|
|
81
|
+
panel.name = 'Control Panel';
|
|
82
|
+
panel.uuid = panelId;
|
|
83
|
+
panel.userData = {
|
|
84
|
+
objectType: 'component',
|
|
85
|
+
isControlPanel: true
|
|
86
|
+
};
|
|
87
|
+
p = spec.position || {};
|
|
88
|
+
panel.position.set(p.x || 0, p.y || 0, p.z || 0);
|
|
89
|
+
r = spec.rotation || {};
|
|
90
|
+
d2r = Math.PI / 180;
|
|
91
|
+
panel.rotation.set((r.x || 0) * d2r, (r.y || 0) * d2r, (r.z || 0) * d2r, 'XYZ');
|
|
92
|
+
scale = Number.isFinite(spec.scale) && spec.scale > 0 ? spec.scale : 1;
|
|
93
|
+
panel.scale.setScalar(scale);
|
|
94
|
+
|
|
95
|
+
// ── Drawing-desk geometry (Z-up scene) ───────────────────────────────────
|
|
96
|
+
// A tilted top surface (the drafting board) raised on legs. Switches +
|
|
97
|
+
// labels live on the tilted `top` group so they sit on the angled surface.
|
|
98
|
+
b = spec.board || {};
|
|
99
|
+
width = (_b$width = b.width) !== null && _b$width !== void 0 ? _b$width : 7; // x-extent of the top
|
|
100
|
+
depth = (_b$height = b.height) !== null && _b$height !== void 0 ? _b$height : 2; // y-extent of the top (spec key kept as `height`)
|
|
101
|
+
thickness = (_b$thickness = b.thickness) !== null && _b$thickness !== void 0 ? _b$thickness : 0.15;
|
|
102
|
+
desk = spec.desk || {};
|
|
103
|
+
tilt = ((_desk$tilt = desk.tilt) !== null && _desk$tilt !== void 0 ? _desk$tilt : 30) * d2r; // surface tilt; +raises the back (+y) edge
|
|
104
|
+
deskHeight = (_desk$height = desk.height) !== null && _desk$height !== void 0 ? _desk$height : 1.3; // world height of the top's center
|
|
105
|
+
legThickness = (_desk$legThickness = desk.legThickness) !== null && _desk$legThickness !== void 0 ? _desk$legThickness : 0.18;
|
|
106
|
+
legInset = (_desk$legInset = desk.legInset) !== null && _desk$legInset !== void 0 ? _desk$legInset : 0.35;
|
|
107
|
+
legColor = desk.legColor || '#22303c'; // Tilted top group: raised to deskHeight, rotated about X.
|
|
108
|
+
top = new THREE.Group();
|
|
109
|
+
top.position.set(0, 0, deskHeight);
|
|
110
|
+
top.rotation.set(tilt, 0, 0, 'XYZ');
|
|
111
|
+
panel.add(top);
|
|
112
|
+
board = new THREE.Mesh(new THREE.BoxGeometry(width, depth, thickness), new THREE.MeshStandardMaterial({
|
|
113
|
+
color: b.color || '#2c3e50',
|
|
114
|
+
roughness: 0.6,
|
|
115
|
+
metalness: 0.2
|
|
116
|
+
}));
|
|
117
|
+
board.userData = {
|
|
118
|
+
objectType: 'control-panel-board'
|
|
119
|
+
};
|
|
120
|
+
top.add(board);
|
|
121
|
+
|
|
122
|
+
// Legs: one per corner, from the ground up to the tilted top's underside.
|
|
123
|
+
legMat = new THREE.MeshStandardMaterial({
|
|
124
|
+
color: legColor,
|
|
125
|
+
roughness: 0.7,
|
|
126
|
+
metalness: 0.1
|
|
127
|
+
});
|
|
128
|
+
halfW = Math.max(0.1, width / 2 - legInset);
|
|
129
|
+
halfD = Math.max(0.1, depth / 2 - legInset);
|
|
130
|
+
for (_i = 0, _arr = [halfD, -halfD]; _i < _arr.length; _i++) {
|
|
131
|
+
ly = _arr[_i];
|
|
132
|
+
// Underside height at this front/back row after the tilt.
|
|
133
|
+
cornerZ = deskHeight + ly * Math.sin(tilt) - thickness / 2 * Math.cos(tilt);
|
|
134
|
+
legH = Math.max(0.1, cornerZ);
|
|
135
|
+
yPos = ly * Math.cos(tilt); // corner's y in panel space after tilt
|
|
136
|
+
for (_i2 = 0, _arr2 = [halfW, -halfW]; _i2 < _arr2.length; _i2++) {
|
|
137
|
+
lx = _arr2[_i2];
|
|
138
|
+
leg = new THREE.Mesh(new THREE.BoxGeometry(legThickness, legThickness, legH), legMat);
|
|
139
|
+
leg.position.set(lx, yPos, legH / 2);
|
|
140
|
+
panel.add(leg);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Switch layout: a row along X on the top face (+Z) of the board ───────
|
|
145
|
+
switches = Array.isArray(spec.switches) ? spec.switches : [];
|
|
146
|
+
spacing = (_spec$switchSpacing = spec.switchSpacing) !== null && _spec$switchSpacing !== void 0 ? _spec$switchSpacing : 1.3;
|
|
147
|
+
xs = computeSwitchRowX(switches.length, spacing);
|
|
148
|
+
topZ = thickness / 2;
|
|
149
|
+
attachedDevices = {};
|
|
150
|
+
switches.forEach(function (sw, i) {
|
|
151
|
+
attachedDevices[sw.attachmentId] = {
|
|
152
|
+
deviceId: spec.deviceId,
|
|
153
|
+
attachmentLabel: sw.label || sw.attachmentId,
|
|
154
|
+
attachmentPoint: {
|
|
155
|
+
position: {
|
|
156
|
+
x: xs[i],
|
|
157
|
+
y: 0,
|
|
158
|
+
z: topZ
|
|
159
|
+
},
|
|
160
|
+
rotation: spec.switchRotation || {
|
|
161
|
+
x: 0,
|
|
162
|
+
y: 0,
|
|
163
|
+
z: 0
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
componentData = {
|
|
169
|
+
isSmart: true,
|
|
170
|
+
attachedDevices: attachedDevices
|
|
171
|
+
}; // Clone + tag the switch devices onto the tilted top (reuses smart-component path).
|
|
172
|
+
_context.n = 2;
|
|
173
|
+
return attachIODevicesToComponent(top, componentData, modelPreloader, panelId);
|
|
174
|
+
case 2:
|
|
175
|
+
// Optional switch-only scale (attach path always leaves devices at 1:1).
|
|
176
|
+
switchScale = Number.isFinite(spec.switchScale) && spec.switchScale > 0 ? spec.switchScale : 1;
|
|
177
|
+
if (switchScale !== 1) {
|
|
178
|
+
top.traverse(function (obj) {
|
|
179
|
+
var _obj$userData;
|
|
180
|
+
if (((_obj$userData = obj.userData) === null || _obj$userData === void 0 ? void 0 : _obj$userData.objectType) === 'io-device') {
|
|
181
|
+
obj.scale.setScalar(switchScale);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Panel must be in the scene before behaviors register (default-state seeding
|
|
187
|
+
// scans the live scene graph for io-devices).
|
|
188
|
+
scene.add(panel);
|
|
189
|
+
this._panel = panel;
|
|
190
|
+
ioBehavMgr = (_this$sceneViewer3 = this.sceneViewer) === null || _this$sceneViewer3 === void 0 || (_this$sceneViewer3 = _this$sceneViewer3.managers) === null || _this$sceneViewer3 === void 0 ? void 0 : _this$sceneViewer3.ioBehaviorManager;
|
|
191
|
+
if (ioBehavMgr) {
|
|
192
|
+
registerBehaviorsForComponent(ioBehavMgr, panelId, componentData, panel, modelPreloader.componentDictionary, centralPlant);
|
|
193
|
+
|
|
194
|
+
// Drive each switch's mesh to its current engine value so the initial
|
|
195
|
+
// pose/colour reflects the state. `registerBehaviorsForComponent`'s
|
|
196
|
+
// default-seeding writes through the engine, which no-ops when the value
|
|
197
|
+
// already matches the seeded `stateRegistry` — leaving the button at its
|
|
198
|
+
// neutral GLB rest pose. Trigger the animation directly to avoid that.
|
|
199
|
+
engine = centralPlant === null || centralPlant === void 0 ? void 0 : centralPlant.stateEngine;
|
|
200
|
+
_iterator = _createForOfIteratorHelper(switches);
|
|
201
|
+
try {
|
|
202
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
203
|
+
sw = _step.value;
|
|
204
|
+
current = engine === null || engine === void 0 ? void 0 : engine.getDesired("".concat(panelId, "::").concat(sw.attachmentId, "::mode"));
|
|
205
|
+
ioBehavMgr.triggerState(sw.attachmentId, 'mode', current !== null && current !== void 0 ? current : true, panelId);
|
|
206
|
+
}
|
|
207
|
+
} catch (err) {
|
|
208
|
+
_iterator.e(err);
|
|
209
|
+
} finally {
|
|
210
|
+
_iterator.f();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Floating labels above each switch (on the tilted top) ────────────────
|
|
215
|
+
lo = spec.labelOffset || {
|
|
216
|
+
x: 0,
|
|
217
|
+
y: 0,
|
|
218
|
+
z: 0.7
|
|
219
|
+
};
|
|
220
|
+
labelSize = (_spec$labelSize = spec.labelSize) !== null && _spec$labelSize !== void 0 ? _spec$labelSize : 0.5;
|
|
221
|
+
switches.forEach(function (sw, i) {
|
|
222
|
+
var _lo$z;
|
|
223
|
+
var label = _this2._makeLabel(sw.label || sw.attachmentId, labelSize);
|
|
224
|
+
if (!label) return;
|
|
225
|
+
label.position.set(xs[i] + (lo.x || 0), lo.y || 0, topZ + ((_lo$z = lo.z) !== null && _lo$z !== void 0 ? _lo$z : 0.7));
|
|
226
|
+
top.add(label);
|
|
227
|
+
_this2._labels.push(label);
|
|
228
|
+
});
|
|
229
|
+
case 3:
|
|
230
|
+
return _context.a(2);
|
|
231
|
+
}
|
|
232
|
+
}, _callee, this);
|
|
233
|
+
}));
|
|
234
|
+
function _build() {
|
|
235
|
+
return _build2.apply(this, arguments);
|
|
236
|
+
}
|
|
237
|
+
return _build;
|
|
238
|
+
}()
|
|
239
|
+
/**
|
|
240
|
+
* Build one text label as a camera-facing `THREE.Sprite` (canvas texture),
|
|
241
|
+
* drawn by the main WebGL renderer alongside the scene — no dependency on the
|
|
242
|
+
* tooltip CSS2D renderer. Returns null when there is no DOM (headless tests).
|
|
243
|
+
* @param {string} text
|
|
244
|
+
* @param {number} worldHeight - label height in world units
|
|
245
|
+
* @returns {THREE.Sprite|null}
|
|
246
|
+
* @private
|
|
247
|
+
*/
|
|
248
|
+
)
|
|
249
|
+
}, {
|
|
250
|
+
key: "_makeLabel",
|
|
251
|
+
value: function _makeLabel(text, worldHeight) {
|
|
252
|
+
if (typeof document === 'undefined') return null;
|
|
253
|
+
var dpr = 4; // supersample for crisp text
|
|
254
|
+
var fontPx = 40;
|
|
255
|
+
var padX = 14;
|
|
256
|
+
var padY = 8;
|
|
257
|
+
var font = "600 ".concat(fontPx, "px -apple-system, \"Segoe UI\", Roboto, sans-serif");
|
|
258
|
+
var canvas = document.createElement('canvas');
|
|
259
|
+
var ctx = canvas.getContext('2d');
|
|
260
|
+
ctx.font = font;
|
|
261
|
+
var textW = ctx.measureText(text).width;
|
|
262
|
+
canvas.width = Math.ceil((textW + padX * 2) * dpr);
|
|
263
|
+
canvas.height = Math.ceil((fontPx + padY * 2) * dpr);
|
|
264
|
+
|
|
265
|
+
// Re-apply after the resize (which clears the canvas), scaled by dpr.
|
|
266
|
+
ctx.scale(dpr, dpr);
|
|
267
|
+
ctx.font = font;
|
|
268
|
+
var wCss = canvas.width / dpr;
|
|
269
|
+
var hCss = canvas.height / dpr;
|
|
270
|
+
ctx.fillStyle = 'rgba(20,28,38,0.86)';
|
|
271
|
+
this._roundRect(ctx, 0, 0, wCss, hCss, 8);
|
|
272
|
+
ctx.fill();
|
|
273
|
+
ctx.fillStyle = '#eaf2f8';
|
|
274
|
+
ctx.textAlign = 'center';
|
|
275
|
+
ctx.textBaseline = 'middle';
|
|
276
|
+
ctx.fillText(text, wCss / 2, hCss / 2);
|
|
277
|
+
var texture = new THREE.CanvasTexture(canvas);
|
|
278
|
+
texture.minFilter = THREE.LinearFilter;
|
|
279
|
+
texture.anisotropy = 4;
|
|
280
|
+
var material = new THREE.SpriteMaterial({
|
|
281
|
+
map: texture,
|
|
282
|
+
transparent: true,
|
|
283
|
+
depthTest: false,
|
|
284
|
+
// always readable, never occluded by the board/switch
|
|
285
|
+
depthWrite: false
|
|
286
|
+
});
|
|
287
|
+
var sprite = new THREE.Sprite(material);
|
|
288
|
+
var aspect = canvas.width / canvas.height;
|
|
289
|
+
sprite.scale.set(worldHeight * aspect, worldHeight, 1);
|
|
290
|
+
sprite.renderOrder = 999;
|
|
291
|
+
return sprite;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Trace a rounded-rect path (fill applied by caller). @private */
|
|
295
|
+
}, {
|
|
296
|
+
key: "_roundRect",
|
|
297
|
+
value: function _roundRect(ctx, x, y, w, h, r) {
|
|
298
|
+
var rr = Math.min(r, w / 2, h / 2);
|
|
299
|
+
ctx.beginPath();
|
|
300
|
+
ctx.moveTo(x + rr, y);
|
|
301
|
+
ctx.arcTo(x + w, y, x + w, y + h, rr);
|
|
302
|
+
ctx.arcTo(x + w, y + h, x, y + h, rr);
|
|
303
|
+
ctx.arcTo(x, y + h, x, y, rr);
|
|
304
|
+
ctx.arcTo(x, y, x + w, y, rr);
|
|
305
|
+
ctx.closePath();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Remove the panel, its labels, and its registered behaviors (scene reload).
|
|
310
|
+
* @private
|
|
311
|
+
*/
|
|
312
|
+
}, {
|
|
313
|
+
key: "_teardown",
|
|
314
|
+
value: function _teardown() {
|
|
315
|
+
this._labels = [];
|
|
316
|
+
if (this._panelId) {
|
|
317
|
+
var _this$sceneViewer4;
|
|
318
|
+
(_this$sceneViewer4 = this.sceneViewer) === null || _this$sceneViewer4 === void 0 || (_this$sceneViewer4 = _this$sceneViewer4.managers) === null || _this$sceneViewer4 === void 0 || (_this$sceneViewer4 = _this$sceneViewer4.ioBehaviorManager) === null || _this$sceneViewer4 === void 0 || _this$sceneViewer4.unloadForComponent(this._panelId);
|
|
319
|
+
}
|
|
320
|
+
if (this._panel) {
|
|
321
|
+
var _this$sceneViewer5;
|
|
322
|
+
(_this$sceneViewer5 = this.sceneViewer) === null || _this$sceneViewer5 === void 0 || (_this$sceneViewer5 = _this$sceneViewer5.scene) === null || _this$sceneViewer5 === void 0 || _this$sceneViewer5.remove(this._panel);
|
|
323
|
+
this._panel.traverse(function (obj) {
|
|
324
|
+
if (obj.isMesh || obj.isSprite) {
|
|
325
|
+
var _obj$geometry, _obj$geometry$dispose;
|
|
326
|
+
(_obj$geometry = obj.geometry) === null || _obj$geometry === void 0 || (_obj$geometry$dispose = _obj$geometry.dispose) === null || _obj$geometry$dispose === void 0 || _obj$geometry$dispose.call(_obj$geometry);
|
|
327
|
+
var mats = Array.isArray(obj.material) ? obj.material : [obj.material];
|
|
328
|
+
mats.forEach(function (m) {
|
|
329
|
+
var _m$map, _m$map$dispose, _m$dispose;
|
|
330
|
+
m === null || m === void 0 || (_m$map = m.map) === null || _m$map === void 0 || (_m$map$dispose = _m$map.dispose) === null || _m$map$dispose === void 0 || _m$map$dispose.call(_m$map); // sprite canvas texture
|
|
331
|
+
m === null || m === void 0 || (_m$dispose = m.dispose) === null || _m$dispose === void 0 || _m$dispose.call(m);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
this._panel = null;
|
|
336
|
+
}
|
|
337
|
+
this._panelId = null;
|
|
338
|
+
}
|
|
339
|
+
}, {
|
|
340
|
+
key: "_doDispose",
|
|
341
|
+
value: function _doDispose() {
|
|
342
|
+
var _this$sceneViewer6;
|
|
343
|
+
if ((_this$sceneViewer6 = this.sceneViewer) !== null && _this$sceneViewer6 !== void 0 && _this$sceneViewer6.off) {
|
|
344
|
+
this.sceneViewer.off('scene-loaded', this._build);
|
|
345
|
+
}
|
|
346
|
+
this._teardown();
|
|
347
|
+
this.sceneViewer = null;
|
|
348
|
+
}
|
|
349
|
+
}]);
|
|
350
|
+
}(BaseDisposable);
|
|
351
|
+
|
|
352
|
+
export { ControlPanelManager, computeSwitchRowX };
|