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