@2112-lab/central-plant 0.3.27 → 0.3.29

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.
@@ -0,0 +1,547 @@
1
+ import { inherits as _inherits, createClass as _createClass, createForOfIteratorHelper as _createForOfIteratorHelper, slicedToArray as _slicedToArray, toConsumableArray as _toConsumableArray, superPropGet as _superPropGet, classCallCheck as _classCallCheck, callSuper as _callSuper } from '../../../_virtual/_rollupPluginBabelHelpers.js';
2
+ import * as THREE from 'three';
3
+ import { BaseDisposable } from '../../core/baseDisposable.js';
4
+
5
+ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
6
+ function IoBehaviorManager(sceneViewer) {
7
+ var _this;
8
+ _classCallCheck(this, IoBehaviorManager);
9
+ _this = _callSuper(this, IoBehaviorManager);
10
+ _this.sceneViewer = sceneViewer;
11
+
12
+ /**
13
+ * Map: `${parentUuid}::${attachmentId}` → Array<{
14
+ * anim: Object,
15
+ * mesh: THREE.Object3D,
16
+ * origPos: THREE.Vector3,
17
+ * origRot: THREE.Euler
18
+ * }>
19
+ */
20
+ _this._entries = new Map();
21
+ return _this;
22
+ }
23
+
24
+ // ─────────────────────────────────────────────────────────────────────────
25
+ // PUBLIC API
26
+ // ─────────────────────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Register animation entries for one attached I/O device.
30
+ * Should be called after the device GLB has been merged into the scene
31
+ * so that mesh references are live.
32
+ *
33
+ * @param {string} attachmentId - The attachment key (e.g. 'attch-switch-01')
34
+ * @param {Object|Array} behaviorConfig - Serialized config; either the full object
35
+ * { behaviors: [...] } or a plain array of behavior entries.
36
+ * @param {THREE.Object3D} deviceModelRoot - The device's root Object3D as added to the scene
37
+ * @param {string} parentUuid - UUID of the host component Object3D
38
+ */
39
+ _inherits(IoBehaviorManager, _BaseDisposable);
40
+ return _createClass(IoBehaviorManager, [{
41
+ key: "loadBehaviors",
42
+ value: function loadBehaviors(attachmentId, behaviorConfig, deviceModelRoot, parentUuid) {
43
+ var _behaviorConfig$behav;
44
+ if (!behaviorConfig || !deviceModelRoot) return;
45
+ var anims = Array.isArray(behaviorConfig) ? behaviorConfig : (_behaviorConfig$behav = behaviorConfig.behaviors) !== null && _behaviorConfig$behav !== void 0 ? _behaviorConfig$behav : [];
46
+ if (!anims.length) return;
47
+ var key = this._key(parentUuid, attachmentId);
48
+ var entries = [];
49
+ var _iterator = _createForOfIteratorHelper(anims),
50
+ _step;
51
+ try {
52
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
53
+ var anim = _step.value;
54
+ var mesh = this._resolveMesh(anim, deviceModelRoot);
55
+ if (!mesh) {
56
+ console.warn("[IoBehaviorManager] Could not find mesh for animation \"".concat(anim.name || anim.stateVariable, "\" (uuid: ").concat(anim.meshUuid, ", name: \"").concat(anim.meshName, "\")"));
57
+ continue;
58
+ }
59
+ entries.push({
60
+ anim: anim,
61
+ mesh: mesh,
62
+ origPos: mesh.position.clone(),
63
+ origRot: mesh.rotation.clone()
64
+ });
65
+ }
66
+ } catch (err) {
67
+ _iterator.e(err);
68
+ } finally {
69
+ _iterator.f();
70
+ }
71
+ if (entries.length) {
72
+ this._entries.set(key, entries);
73
+ console.log("[IoBehaviorManager] Loaded ".concat(entries.length, " animation(s) for attachment \"").concat(attachmentId, "\" (parent: ").concat(parentUuid, ") \u2014 stateVariables: ").concat(entries.map(function (e) {
74
+ return e.anim.stateVariable;
75
+ }).join(', ')));
76
+ } else {
77
+ console.warn("[IoBehaviorManager] No mesh entries resolved for attachment \"".concat(attachmentId, "\" \u2014 behaviorConfig had ").concat(anims.length, " entries but none matched a mesh"));
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Apply animations triggered by an IO device state change.
83
+ * Should be called in parallel with BehaviorManager.triggerState().
84
+ *
85
+ * @param {string} attachmentId - Raw attachment key (not scoped)
86
+ * @param {string} dataPointId - The data point / state variable id that changed
87
+ * @param {*} value - New state value
88
+ * @param {string} parentUuid - UUID of the host component
89
+ */
90
+ }, {
91
+ key: "triggerState",
92
+ value: function triggerState(attachmentId, dataPointId, value, parentUuid) {
93
+ var key = this._key(parentUuid, attachmentId);
94
+ var entries = this._entries.get(key);
95
+ if (!(entries !== null && entries !== void 0 && entries.length)) return;
96
+ var _iterator2 = _createForOfIteratorHelper(entries),
97
+ _step2;
98
+ try {
99
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
100
+ var entry = _step2.value;
101
+ if (entry.anim.stateVariable !== dataPointId) continue;
102
+ this._applyAnimation(entry, value);
103
+ }
104
+ } catch (err) {
105
+ _iterator2.e(err);
106
+ } finally {
107
+ _iterator2.f();
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Return tooltip-compatible data point definitions derived from the loaded
113
+ * animation entries for a given attachment. Used by componentTooltipManager
114
+ * to replace the static ioConfig.states[] snapshot with the richer animation
115
+ * state definitions created in the Animate window.
116
+ *
117
+ * One dp object is emitted per unique stateVariable; multiple mesh entries
118
+ * that share the same stateVariable are collapsed into one.
119
+ *
120
+ * Returned dp shape (matches what _buildDataPointRow / _buildInputControl expect):
121
+ * {
122
+ * id: string, // stateVariable name
123
+ * name: string, // human-readable label (anim.name or stateVariable)
124
+ * stateType: string, // normalised to 'binary' | 'enum' | 'number'
125
+ * stateConfig: Object, // { onLabel?, offLabel?, options?, min?, max?, unit? }
126
+ * defaultValue: any, // sensible default for the stateType
127
+ * direction: 'input' // all animation-driven states are interactive
128
+ * }
129
+ *
130
+ * @param {string} parentUuid
131
+ * @param {string} attachmentId
132
+ * @returns {Object[]} Array of dp objects, or empty array if none loaded.
133
+ */
134
+ }, {
135
+ key: "getAnimationDataPoints",
136
+ value: function getAnimationDataPoints(parentUuid, attachmentId) {
137
+ var key = this._key(parentUuid, attachmentId);
138
+ var entries = this._entries.get(key);
139
+ if (!(entries !== null && entries !== void 0 && entries.length)) return [];
140
+
141
+ // Collapse multiple mesh entries that share the same stateVariable
142
+ var seen = new Map(); // stateVariable → anim
143
+ var _iterator3 = _createForOfIteratorHelper(entries),
144
+ _step3;
145
+ try {
146
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
147
+ var anim = _step3.value.anim;
148
+ if (!seen.has(anim.stateVariable)) {
149
+ seen.set(anim.stateVariable, anim);
150
+ }
151
+ }
152
+ } catch (err) {
153
+ _iterator3.e(err);
154
+ } finally {
155
+ _iterator3.f();
156
+ }
157
+ var dps = [];
158
+ var _iterator4 = _createForOfIteratorHelper(seen),
159
+ _step4;
160
+ try {
161
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
162
+ var _step4$value = _slicedToArray(_step4.value, 2),
163
+ stateVar = _step4$value[0],
164
+ _anim = _step4$value[1];
165
+ // Normalise stateType from AnimateDevicesDialog variants
166
+ var stateType = void 0;
167
+ var raw = (_anim.stateType || '').toLowerCase();
168
+ if (raw === 'binary' || raw === 'boolean') {
169
+ stateType = 'binary';
170
+ } else if (raw === 'enum') {
171
+ stateType = 'enum';
172
+ } else {
173
+ // 'continuous', 'range', 'number', or anything else → numeric slider
174
+ stateType = 'number';
175
+ }
176
+
177
+ // Derive stateConfig from mappings
178
+ var stateConfig = {};
179
+ var mappingValues = (_anim.mappings || []).map(function (m) {
180
+ return m.stateValue;
181
+ });
182
+ if (stateType === 'enum') {
183
+ stateConfig.options = _toConsumableArray(new Set(mappingValues.map(String)));
184
+ } else if (stateType === 'number') {
185
+ var nums = mappingValues.map(Number).filter(function (n) {
186
+ return !isNaN(n);
187
+ });
188
+ if (nums.length) {
189
+ stateConfig.min = Math.min.apply(Math, _toConsumableArray(nums));
190
+ stateConfig.max = Math.max.apply(Math, _toConsumableArray(nums));
191
+ }
192
+ }
193
+
194
+ // Sensible default values
195
+ var defaultValue = void 0;
196
+ if (stateType === 'binary') {
197
+ defaultValue = 0;
198
+ } else if (stateType === 'enum') {
199
+ var _stateConfig$options$, _stateConfig$options;
200
+ defaultValue = (_stateConfig$options$ = (_stateConfig$options = stateConfig.options) === null || _stateConfig$options === void 0 ? void 0 : _stateConfig$options[0]) !== null && _stateConfig$options$ !== void 0 ? _stateConfig$options$ : '';
201
+ } else {
202
+ var _stateConfig$min;
203
+ defaultValue = (_stateConfig$min = stateConfig.min) !== null && _stateConfig$min !== void 0 ? _stateConfig$min : 0;
204
+ }
205
+ dps.push({
206
+ id: stateVar,
207
+ name: _anim.name || stateVar,
208
+ stateType: stateType,
209
+ stateConfig: stateConfig,
210
+ defaultValue: defaultValue,
211
+ direction: 'input'
212
+ });
213
+ }
214
+ } catch (err) {
215
+ _iterator4.e(err);
216
+ } finally {
217
+ _iterator4.f();
218
+ }
219
+ return dps;
220
+ }
221
+
222
+ /**
223
+ * Return the Three.js mesh objects that are animated for a given attachment.
224
+ * Used by IoOutlineManager to include animated meshes in the outline.
225
+ *
226
+ * @param {string} parentUuid
227
+ * @param {string} attachmentId
228
+ * @returns {THREE.Object3D[]}
229
+ */
230
+ }, {
231
+ key: "getAnimatedMeshes",
232
+ value: function getAnimatedMeshes(parentUuid, attachmentId) {
233
+ var key = this._key(parentUuid, attachmentId);
234
+ var entries = this._entries.get(key);
235
+ if (!(entries !== null && entries !== void 0 && entries.length)) return [];
236
+ // Deduplicate — multiple animations can target the same mesh
237
+ return _toConsumableArray(new Set(entries.map(function (e) {
238
+ return e.mesh;
239
+ })));
240
+ }
241
+
242
+ /**
243
+ * Remove all animation entries associated with a given host component.
244
+ * Call when a component is removed from the scene.
245
+ *
246
+ * @param {string} parentUuid
247
+ */
248
+ }, {
249
+ key: "unloadForComponent",
250
+ value: function unloadForComponent(parentUuid) {
251
+ var prefix = "".concat(parentUuid, "::");
252
+ var _iterator5 = _createForOfIteratorHelper(this._entries.keys()),
253
+ _step5;
254
+ try {
255
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
256
+ var key = _step5.value;
257
+ if (key.startsWith(prefix)) {
258
+ this._entries.delete(key);
259
+ }
260
+ }
261
+ } catch (err) {
262
+ _iterator5.e(err);
263
+ } finally {
264
+ _iterator5.f();
265
+ }
266
+ }
267
+ }, {
268
+ key: "dispose",
269
+ value: function dispose() {
270
+ this._entries.clear();
271
+ _superPropGet(IoBehaviorManager, "dispose", this, 3)([]);
272
+ }
273
+
274
+ // ─────────────────────────────────────────────────────────────────────────
275
+ // PRIVATE HELPERS
276
+ // ─────────────────────────────────────────────────────────────────────────
277
+ }, {
278
+ key: "_key",
279
+ value: function _key(parentUuid, attachmentId) {
280
+ return "".concat(parentUuid, "::").concat(attachmentId);
281
+ }
282
+
283
+ /**
284
+ * Find the mesh inside `root` using UUID first, then name as fallback.
285
+ * GLTFLoader assigns fresh UUIDs on every load, so name is the reliable key.
286
+ * @param {Object} anim
287
+ * @param {THREE.Object3D} root
288
+ * @returns {THREE.Object3D|null}
289
+ */
290
+ }, {
291
+ key: "_resolveMesh",
292
+ value: function _resolveMesh(anim, root) {
293
+ var found = null;
294
+ root.traverse(function (obj) {
295
+ if (found) return;
296
+
297
+ // Prefer UUID match (works when the device was cloned from a cached instance
298
+ // whose UUIDs were preserved)
299
+ if (anim.meshUuid && obj.uuid === anim.meshUuid) {
300
+ found = obj;
301
+ return;
302
+ }
303
+
304
+ // Reliable fallback: mesh name
305
+ if (anim.meshName && obj.name === anim.meshName) {
306
+ found = obj;
307
+ }
308
+ });
309
+ return found;
310
+ }
311
+
312
+ /**
313
+ * Resolve which mapping row to use and apply all declared transform types.
314
+ * @param {{ anim, mesh, origPos, origRot }} entry
315
+ * @param {*} value - Current state value
316
+ */
317
+ }, {
318
+ key: "_applyAnimation",
319
+ value: function _applyAnimation(entry, value) {
320
+ var anim = entry.anim,
321
+ mesh = entry.mesh,
322
+ origPos = entry.origPos,
323
+ origRot = entry.origRot;
324
+ var mapping = this._resolveMapping(anim, value);
325
+ if (!mapping) return;
326
+ var types = anim.transformTypes || [];
327
+ var _iterator6 = _createForOfIteratorHelper(types),
328
+ _step6;
329
+ try {
330
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
331
+ var type = _step6.value;
332
+ if (type === 'translation') {
333
+ this._applyTranslation(mesh, origPos, mapping.transform);
334
+ } else if (type === 'rotation') {
335
+ this._applyRotation(mesh, origPos, origRot, anim, mapping.rotationTransform);
336
+ } else if (type === 'color') {
337
+ this._applyColor(mesh, mapping.colorTransform);
338
+ }
339
+ }
340
+ } catch (err) {
341
+ _iterator6.e(err);
342
+ } finally {
343
+ _iterator6.f();
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Find the mapping row that matches `value` for the given animation entry.
349
+ *
350
+ * - binary / enum: find where mapping.stateValue == value (loose equality so
351
+ * JSON-deserialised "true" matches boolean true)
352
+ * - continuous: linear interpolation between ordered anchor points
353
+ *
354
+ * @returns {Object|null} The matched/interpolated mapping row, or null.
355
+ */
356
+ }, {
357
+ key: "_resolveMapping",
358
+ value: function _resolveMapping(anim, value) {
359
+ var mappings = anim.mappings;
360
+ if (!(mappings !== null && mappings !== void 0 && mappings.length)) return null;
361
+
362
+ // Normalise stateType variants saved by AnimateDevicesDialog
363
+ // ('boolean' → 'binary', 'range' → 'continuous')
364
+ var raw = (anim.stateType || '').toLowerCase();
365
+ var stateType;
366
+ if (raw === 'binary' || raw === 'boolean') {
367
+ stateType = 'binary';
368
+ } else if (raw === 'continuous' || raw === 'range') {
369
+ stateType = 'continuous';
370
+ } else {
371
+ stateType = raw;
372
+ }
373
+ if (stateType === 'binary' || stateType === 'enum') {
374
+ var _mappings$find;
375
+ // eslint-disable-next-line eqeqeq
376
+ return (_mappings$find = mappings.find(function (m) {
377
+ return m.stateValue == value;
378
+ })) !== null && _mappings$find !== void 0 ? _mappings$find : null;
379
+ }
380
+ if (stateType === 'continuous') {
381
+ var num = Number(value);
382
+ if (isNaN(num)) return null;
383
+
384
+ // Sort anchors by numeric stateValue
385
+ var sorted = _toConsumableArray(mappings).filter(function (m) {
386
+ return m.stateValue != null && !isNaN(Number(m.stateValue));
387
+ }).sort(function (a, b) {
388
+ return Number(a.stateValue) - Number(b.stateValue);
389
+ });
390
+ if (!sorted.length) return null;
391
+ if (sorted.length === 1) return sorted[0];
392
+
393
+ // Clamp to range
394
+ if (num <= Number(sorted[0].stateValue)) return sorted[0];
395
+ if (num >= Number(sorted[sorted.length - 1].stateValue)) return sorted[sorted.length - 1];
396
+
397
+ // Find surrounding anchors and interpolate
398
+ for (var i = 0; i < sorted.length - 1; i++) {
399
+ var lo = sorted[i];
400
+ var hi = sorted[i + 1];
401
+ var loVal = Number(lo.stateValue);
402
+ var hiVal = Number(hi.stateValue);
403
+ if (num >= loVal && num <= hiVal) {
404
+ var t = (num - loVal) / (hiVal - loVal);
405
+ return this._lerpMapping(lo, hi, t);
406
+ }
407
+ }
408
+ }
409
+ return null;
410
+ }
411
+
412
+ /**
413
+ * Linear-interpolate between two mapping rows.
414
+ * @param {Object} lo - lower anchor mapping
415
+ * @param {Object} hi - upper anchor mapping
416
+ * @param {number} t - 0..1
417
+ */
418
+ }, {
419
+ key: "_lerpMapping",
420
+ value: function _lerpMapping(lo, hi, t) {
421
+ var _lo$transform$x, _lo$transform, _hi$transform$x, _hi$transform, _lo$transform$y, _lo$transform2, _hi$transform$y, _hi$transform2, _lo$transform$z, _lo$transform3, _hi$transform$z, _hi$transform3;
422
+ var lerp = function lerp(a, b) {
423
+ return a + (b - a) * t;
424
+ };
425
+ var transform = {
426
+ x: lerp((_lo$transform$x = (_lo$transform = lo.transform) === null || _lo$transform === void 0 ? void 0 : _lo$transform.x) !== null && _lo$transform$x !== void 0 ? _lo$transform$x : 0, (_hi$transform$x = (_hi$transform = hi.transform) === null || _hi$transform === void 0 ? void 0 : _hi$transform.x) !== null && _hi$transform$x !== void 0 ? _hi$transform$x : 0),
427
+ y: lerp((_lo$transform$y = (_lo$transform2 = lo.transform) === null || _lo$transform2 === void 0 ? void 0 : _lo$transform2.y) !== null && _lo$transform$y !== void 0 ? _lo$transform$y : 0, (_hi$transform$y = (_hi$transform2 = hi.transform) === null || _hi$transform2 === void 0 ? void 0 : _hi$transform2.y) !== null && _hi$transform$y !== void 0 ? _hi$transform$y : 0),
428
+ z: lerp((_lo$transform$z = (_lo$transform3 = lo.transform) === null || _lo$transform3 === void 0 ? void 0 : _lo$transform3.z) !== null && _lo$transform$z !== void 0 ? _lo$transform$z : 0, (_hi$transform$z = (_hi$transform3 = hi.transform) === null || _hi$transform3 === void 0 ? void 0 : _hi$transform3.z) !== null && _hi$transform$z !== void 0 ? _hi$transform$z : 0)
429
+ };
430
+ var rotationTransform = lerp(typeof lo.rotationTransform === 'number' ? lo.rotationTransform : 0, typeof hi.rotationTransform === 'number' ? hi.rotationTransform : 0);
431
+
432
+ // Color interpolation: convert hex → RGB components → lerp → back to hex
433
+ var colorTransform = this._lerpHex(lo.colorTransform, hi.colorTransform, t);
434
+ return {
435
+ stateValue: null,
436
+ transform: transform,
437
+ rotationTransform: rotationTransform,
438
+ colorTransform: colorTransform
439
+ };
440
+ }
441
+
442
+ /**
443
+ * Interpolate between two hex colour strings.
444
+ */
445
+ }, {
446
+ key: "_lerpHex",
447
+ value: function _lerpHex(hexA, hexB, t) {
448
+ try {
449
+ var ca = new THREE.Color(hexA);
450
+ var cb = new THREE.Color(hexB);
451
+ ca.lerp(cb, t);
452
+ return "#".concat(ca.getHexString());
453
+ } catch (_unused) {
454
+ return hexA !== null && hexA !== void 0 ? hexA : '#ffffff';
455
+ }
456
+ }
457
+
458
+ // ─────────────────────────────────────────────────────────────────────────
459
+ // TRANSFORM APPLIERS
460
+ // ─────────────────────────────────────────────────────────────────────────
461
+
462
+ /**
463
+ * Apply a position delta relative to the mesh's original position.
464
+ * @param {THREE.Object3D} mesh
465
+ * @param {THREE.Vector3} origPos
466
+ * @param {{ x, y, z }} transform - Deltas
467
+ */
468
+ }, {
469
+ key: "_applyTranslation",
470
+ value: function _applyTranslation(mesh, origPos, transform) {
471
+ var _transform$x, _transform$y, _transform$z;
472
+ if (!transform) return;
473
+ // X and Y are negated to match the sign convention used in the AnimateDevicesDialog
474
+ // preview (_syncViewerTransform negates x and y before calling setMeshPreviewOffset).
475
+ // Z is added directly (no negation) — matching the dialog's z handling.
476
+ mesh.position.set(origPos.x - ((_transform$x = transform.x) !== null && _transform$x !== void 0 ? _transform$x : 0), origPos.y - ((_transform$y = transform.y) !== null && _transform$y !== void 0 ? _transform$y : 0), origPos.z + ((_transform$z = transform.z) !== null && _transform$z !== void 0 ? _transform$z : 0));
477
+ }
478
+
479
+ /**
480
+ * Apply a rotation around an arbitrary pivot point in device-local space,
481
+ * optionally also displacing the mesh position to simulate orbital motion.
482
+ *
483
+ * Math (all in device-local space):
484
+ * pivot = rotAxisOffset
485
+ * delta = origPos - pivot
486
+ * newDelta = rotate(delta, angle, axis)
487
+ * newPos = pivot + newDelta
488
+ * newRot[axis] = origRot[axis] + angle
489
+ *
490
+ * @param {THREE.Object3D} mesh
491
+ * @param {THREE.Vector3} origPos
492
+ * @param {THREE.Euler} origRot
493
+ * @param {Object} anim
494
+ * @param {number} angleDeg - Degrees
495
+ */
496
+ }, {
497
+ key: "_applyRotation",
498
+ value: function _applyRotation(mesh, origPos, origRot, anim, angleDeg) {
499
+ var _anim$rotAxis, _anim$rotAxisOffset, _off$x, _off$y, _off$z;
500
+ var angle = THREE.MathUtils.degToRad(typeof angleDeg === 'number' ? angleDeg : 0);
501
+ var axis = ((_anim$rotAxis = anim.rotAxis) !== null && _anim$rotAxis !== void 0 ? _anim$rotAxis : 'x').toLowerCase();
502
+ var off = (_anim$rotAxisOffset = anim.rotAxisOffset) !== null && _anim$rotAxisOffset !== void 0 ? _anim$rotAxisOffset : {
503
+ x: 0,
504
+ y: 0,
505
+ z: 0
506
+ };
507
+
508
+ // Unit vector for the chosen axis
509
+ var axisVec = new THREE.Vector3(axis === 'x' ? 1 : 0, axis === 'y' ? 1 : 0, axis === 'z' ? 1 : 0);
510
+ var pivot = new THREE.Vector3((_off$x = off.x) !== null && _off$x !== void 0 ? _off$x : 0, (_off$y = off.y) !== null && _off$y !== void 0 ? _off$y : 0, (_off$z = off.z) !== null && _off$z !== void 0 ? _off$z : 0);
511
+ var delta = origPos.clone().sub(pivot);
512
+ delta.applyAxisAngle(axisVec, angle);
513
+ mesh.position.copy(pivot).add(delta);
514
+ mesh.rotation[axis] = origRot[axis] + angle;
515
+ }
516
+
517
+ /**
518
+ * Apply a colour to all Mesh descendants of `mesh`.
519
+ * Creates a cloned material per mesh on first call to avoid shared-material
520
+ * cross-contamination between different device instances.
521
+ *
522
+ * @param {THREE.Object3D} mesh
523
+ * @param {string} colorHex - e.g. '#ff0000'
524
+ */
525
+ }, {
526
+ key: "_applyColor",
527
+ value: function _applyColor(mesh, colorHex) {
528
+ if (!colorHex) return;
529
+ mesh.traverse(function (obj) {
530
+ if (!obj.isMesh || !obj.material) return;
531
+
532
+ // Ensure this mesh has its own material instance
533
+ if (!obj.userData._materialCloned) {
534
+ obj.material = obj.material.clone();
535
+ obj.userData._materialCloned = true;
536
+ }
537
+ try {
538
+ obj.material.color.set(colorHex);
539
+ } catch (e) {
540
+ // Material may not have a color property (e.g. MeshDepthMaterial)
541
+ }
542
+ });
543
+ }
544
+ }]);
545
+ }(BaseDisposable);
546
+
547
+ export { IoBehaviorManager };
@@ -105,7 +105,7 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
105
105
  }, {
106
106
  key: "toggleIODeviceBinaryState",
107
107
  value: function toggleIODeviceBinaryState(ioDeviceObject) {
108
- var _ref, _this$sceneViewer, _this$sceneViewer2;
108
+ var _ref, _this$sceneViewer;
109
109
  if (!ioDeviceObject || !this._stateAdapter) return;
110
110
  var ud = ioDeviceObject.userData;
111
111
  var attachmentId = ud === null || ud === void 0 ? void 0 : ud.attachmentId;
@@ -139,8 +139,7 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
139
139
  var currentVal = (_ref = storedVal !== null && storedVal !== void 0 ? storedVal : binaryState.defaultValue) !== null && _ref !== void 0 ? _ref : false;
140
140
  var newVal = !Boolean(currentVal);
141
141
  this._stateAdapter.setState(scopedAttachmentId, dpId, newVal);
142
- (_this$sceneViewer = this.sceneViewer) === null || _this$sceneViewer === void 0 || (_this$sceneViewer = _this$sceneViewer.managers) === null || _this$sceneViewer === void 0 || (_this$sceneViewer = _this$sceneViewer.behaviorManager) === null || _this$sceneViewer === void 0 || _this$sceneViewer.triggerState(attachmentId, dpId, newVal, parentUuid);
143
- (_this$sceneViewer2 = this.sceneViewer) === null || _this$sceneViewer2 === void 0 || (_this$sceneViewer2 = _this$sceneViewer2.managers) === null || _this$sceneViewer2 === void 0 || (_this$sceneViewer2 = _this$sceneViewer2.ioAnimationManager) === null || _this$sceneViewer2 === void 0 || _this$sceneViewer2.triggerState(attachmentId, dpId, newVal, parentUuid);
142
+ (_this$sceneViewer = this.sceneViewer) === null || _this$sceneViewer === void 0 || (_this$sceneViewer = _this$sceneViewer.managers) === null || _this$sceneViewer === void 0 || (_this$sceneViewer = _this$sceneViewer.ioBehaviorManager) === null || _this$sceneViewer === void 0 || _this$sceneViewer.triggerState(attachmentId, dpId, newVal, parentUuid);
144
143
  console.log("\uD83D\uDD04 [IODevice] Toggled ".concat(scopedAttachmentId, ".").concat(dpId, ": ").concat(currentVal, " \u2192 ").concat(newVal));
145
144
  }
146
145
 
@@ -156,7 +155,7 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
156
155
  }, {
157
156
  key: "startIODeviceDrag",
158
157
  value: function startIODeviceDrag(ioDeviceObject) {
159
- var _this$sceneViewer3,
158
+ var _this$sceneViewer2,
160
159
  _this2 = this;
161
160
  if (!ioDeviceObject || !this._stateAdapter) return;
162
161
  var ud = ioDeviceObject.userData;
@@ -173,8 +172,8 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
173
172
  obj = obj.parent;
174
173
  }
175
174
  var scopedAttachmentId = this._getScopedAttachmentKey(attachmentId, parentUuid);
176
- var ioAnimMgr = (_this$sceneViewer3 = this.sceneViewer) === null || _this$sceneViewer3 === void 0 || (_this$sceneViewer3 = _this$sceneViewer3.managers) === null || _this$sceneViewer3 === void 0 ? void 0 : _this$sceneViewer3.ioAnimationManager;
177
- var dataPoints = ((ioAnimMgr === null || ioAnimMgr === void 0 ? void 0 : ioAnimMgr.getAnimationDataPoints(parentUuid, attachmentId)) || []).concat((ud === null || ud === void 0 ? void 0 : ud.dataPoints) || [])
175
+ var ioBehavMgr = (_this$sceneViewer2 = this.sceneViewer) === null || _this$sceneViewer2 === void 0 || (_this$sceneViewer2 = _this$sceneViewer2.managers) === null || _this$sceneViewer2 === void 0 ? void 0 : _this$sceneViewer2.ioBehaviorManager;
176
+ var dataPoints = ((ioBehavMgr === null || ioBehavMgr === void 0 ? void 0 : ioBehavMgr.getAnimationDataPoints(parentUuid, attachmentId)) || []).concat((ud === null || ud === void 0 ? void 0 : ud.dataPoints) || [])
178
177
  // deduplicate by id
179
178
  .filter(function (dp, i, arr) {
180
179
  return arr.findIndex(function (d) {
@@ -296,15 +295,14 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
296
295
  }, {
297
296
  key: "_applyDpState",
298
297
  value: function _applyDpState(_ref2, newVal) {
299
- var _this$_stateAdapter, _this$sceneViewer4, _this$sceneViewer5;
298
+ var _this$_stateAdapter, _this$sceneViewer3;
300
299
  var scopedAttachmentId = _ref2.scopedAttachmentId,
301
300
  attachmentId = _ref2.attachmentId,
302
301
  parentUuid = _ref2.parentUuid,
303
302
  dp = _ref2.dp;
304
303
  var dpId = dp.id;
305
304
  (_this$_stateAdapter = this._stateAdapter) === null || _this$_stateAdapter === void 0 || _this$_stateAdapter.setState(scopedAttachmentId, dpId, newVal);
306
- (_this$sceneViewer4 = this.sceneViewer) === null || _this$sceneViewer4 === void 0 || (_this$sceneViewer4 = _this$sceneViewer4.managers) === null || _this$sceneViewer4 === void 0 || (_this$sceneViewer4 = _this$sceneViewer4.behaviorManager) === null || _this$sceneViewer4 === void 0 || _this$sceneViewer4.triggerState(attachmentId, dpId, newVal, parentUuid);
307
- (_this$sceneViewer5 = this.sceneViewer) === null || _this$sceneViewer5 === void 0 || (_this$sceneViewer5 = _this$sceneViewer5.managers) === null || _this$sceneViewer5 === void 0 || (_this$sceneViewer5 = _this$sceneViewer5.ioAnimationManager) === null || _this$sceneViewer5 === void 0 || _this$sceneViewer5.triggerState(attachmentId, dpId, newVal, parentUuid);
305
+ (_this$sceneViewer3 = this.sceneViewer) === null || _this$sceneViewer3 === void 0 || (_this$sceneViewer3 = _this$sceneViewer3.managers) === null || _this$sceneViewer3 === void 0 || (_this$sceneViewer3 = _this$sceneViewer3.ioBehaviorManager) === null || _this$sceneViewer3 === void 0 || _this$sceneViewer3.triggerState(attachmentId, dpId, newVal, parentUuid);
308
306
  }
309
307
 
310
308
  /**
@@ -429,11 +427,11 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
429
427
  var _this3$sceneViewer$ma, _this3$sceneViewer;
430
428
  var attachmentId = child.userData.attachmentId || '';
431
429
 
432
- // Use only data points from the animate window (animationConfig).
430
+ // Use only data points from the animate window (behaviorConfig).
433
431
  // The static ioConfig.states[] snapshot on userData is intentionally ignored.
434
- var dataPoints = (_this3$sceneViewer$ma = (_this3$sceneViewer = _this3.sceneViewer) === null || _this3$sceneViewer === void 0 || (_this3$sceneViewer = _this3$sceneViewer.managers) === null || _this3$sceneViewer === void 0 || (_this3$sceneViewer = _this3$sceneViewer.ioAnimationManager) === null || _this3$sceneViewer === void 0 ? void 0 : _this3$sceneViewer.getAnimationDataPoints(parentUuid, attachmentId)) !== null && _this3$sceneViewer$ma !== void 0 ? _this3$sceneViewer$ma : [];
432
+ var dataPoints = (_this3$sceneViewer$ma = (_this3$sceneViewer = _this3.sceneViewer) === null || _this3$sceneViewer === void 0 || (_this3$sceneViewer = _this3$sceneViewer.managers) === null || _this3$sceneViewer === void 0 || (_this3$sceneViewer = _this3$sceneViewer.ioBehaviorManager) === null || _this3$sceneViewer === void 0 ? void 0 : _this3$sceneViewer.getAnimationDataPoints(parentUuid, attachmentId)) !== null && _this3$sceneViewer$ma !== void 0 ? _this3$sceneViewer$ma : [];
435
433
 
436
- // When data points come from animationConfig they already carry direction:'input'.
434
+ // When data points come from behaviorConfig they already carry direction:'input'.
437
435
  // Pass null so _buildDataPointRow uses the per-dp direction instead of the
438
436
  // device-level ioDirection (which may be 'output' and would hide controls).
439
437
  var deviceDirection = dataPoints.length > 0 ? null : child.userData.ioDirection || 'output';
@@ -587,11 +585,11 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
587
585
  }, {
588
586
  key: "_positionTooltip",
589
587
  value: function _positionTooltip() {
590
- var _this$sceneViewer6, _this$sceneViewer7;
588
+ var _this$sceneViewer4, _this$sceneViewer5;
591
589
  if (!this.tooltipEl || !this.selectedObject) return;
592
590
  var container = this._getContainer();
593
- var camera = (_this$sceneViewer6 = this.sceneViewer) === null || _this$sceneViewer6 === void 0 ? void 0 : _this$sceneViewer6.camera;
594
- var renderer = (_this$sceneViewer7 = this.sceneViewer) === null || _this$sceneViewer7 === void 0 ? void 0 : _this$sceneViewer7.renderer;
591
+ var camera = (_this$sceneViewer4 = this.sceneViewer) === null || _this$sceneViewer4 === void 0 ? void 0 : _this$sceneViewer4.camera;
592
+ var renderer = (_this$sceneViewer5 = this.sceneViewer) === null || _this$sceneViewer5 === void 0 ? void 0 : _this$sceneViewer5.renderer;
595
593
  if (!container || !camera || !renderer) return;
596
594
 
597
595
  // Compute bounding box to position above the component
@@ -628,8 +626,8 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
628
626
  }, {
629
627
  key: "_getContainer",
630
628
  value: function _getContainer() {
631
- var _this$sceneViewer8;
632
- return ((_this$sceneViewer8 = this.sceneViewer) === null || _this$sceneViewer8 === void 0 || (_this$sceneViewer8 = _this$sceneViewer8.renderer) === null || _this$sceneViewer8 === void 0 || (_this$sceneViewer8 = _this$sceneViewer8.domElement) === null || _this$sceneViewer8 === void 0 ? void 0 : _this$sceneViewer8.parentElement) || null;
629
+ var _this$sceneViewer6;
630
+ return ((_this$sceneViewer6 = this.sceneViewer) === null || _this$sceneViewer6 === void 0 || (_this$sceneViewer6 = _this$sceneViewer6.renderer) === null || _this$sceneViewer6 === void 0 || (_this$sceneViewer6 = _this$sceneViewer6.domElement) === null || _this$sceneViewer6 === void 0 ? void 0 : _this$sceneViewer6.parentElement) || null;
633
631
  }
634
632
 
635
633
  // -----------------------------------------------------------------------
@@ -669,15 +667,10 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
669
667
  var currentVal = (_ref3 = (_this$_stateAdapter$g = (_this$_stateAdapter2 = this._stateAdapter) === null || _this$_stateAdapter2 === void 0 ? void 0 : _this$_stateAdapter2.getState(scopedAttachmentId, dpId)) !== null && _this$_stateAdapter$g !== void 0 ? _this$_stateAdapter$g : dp.defaultValue) !== null && _ref3 !== void 0 ? _ref3 : null;
670
668
  if (isInput) {
671
669
  var ctrl = this._buildInputControl(dp, currentVal, function (newVal) {
672
- var _this5$_stateAdapter, _this5$selectedObject, _this5$sceneViewer, _this5$sceneViewer2;
670
+ var _this5$_stateAdapter, _this5$selectedObject, _this5$sceneViewer;
673
671
  (_this5$_stateAdapter = _this5._stateAdapter) === null || _this5$_stateAdapter === void 0 || _this5$_stateAdapter.setState(scopedAttachmentId, dpId, newVal);
674
- // Also fire BehaviorManager so any wired behaviors react immediately.
675
- // Pass the parent component UUID so behaviors scoped to a specific instance
676
- // don't bleed across clones that share the same attachmentId.
677
- // Use originalAttachmentId for behavior triggering as behaviors are keyed by original ID
678
672
  var parentUuid = ((_this5$selectedObject = _this5.selectedObject) === null || _this5$selectedObject === void 0 ? void 0 : _this5$selectedObject.uuid) || null;
679
- (_this5$sceneViewer = _this5.sceneViewer) === null || _this5$sceneViewer === void 0 || (_this5$sceneViewer = _this5$sceneViewer.managers) === null || _this5$sceneViewer === void 0 || (_this5$sceneViewer = _this5$sceneViewer.behaviorManager) === null || _this5$sceneViewer === void 0 || _this5$sceneViewer.triggerState(originalAttachmentId || scopedAttachmentId, dpId, newVal, parentUuid);
680
- (_this5$sceneViewer2 = _this5.sceneViewer) === null || _this5$sceneViewer2 === void 0 || (_this5$sceneViewer2 = _this5$sceneViewer2.managers) === null || _this5$sceneViewer2 === void 0 || (_this5$sceneViewer2 = _this5$sceneViewer2.ioAnimationManager) === null || _this5$sceneViewer2 === void 0 || _this5$sceneViewer2.triggerState(originalAttachmentId || scopedAttachmentId, dpId, newVal, parentUuid);
673
+ (_this5$sceneViewer = _this5.sceneViewer) === null || _this5$sceneViewer === void 0 || (_this5$sceneViewer = _this5$sceneViewer.managers) === null || _this5$sceneViewer === void 0 || (_this5$sceneViewer = _this5$sceneViewer.ioBehaviorManager) === null || _this5$sceneViewer === void 0 || _this5$sceneViewer.triggerState(originalAttachmentId || scopedAttachmentId, dpId, newVal, parentUuid);
681
674
  });
682
675
  row.appendChild(ctrl);
683
676
  this._stateElements.set(key, {