@2112-lab/central-plant 0.3.28 → 0.3.30

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,672 @@
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
+
74
+ // Capture the device root's world orientation once so each entry can
75
+ // convert the configured axis from device-local space to world space.
76
+ var deviceWorldQuat = new THREE__namespace.Quaternion();
77
+ deviceModelRoot.getWorldQuaternion(deviceWorldQuat);
78
+
79
+ // Compute the model's native max dimension so rotAxisOffset values (stored
80
+ // in dialog-viewer-world units, where the model is normalised to 1 unit)
81
+ // can be scaled to runtime-world units. viewerMaxDim = 1 / ns_viewer.
82
+ var _deviceBox = new THREE__namespace.Box3().setFromObject(deviceModelRoot);
83
+ var _deviceSize = new THREE__namespace.Vector3();
84
+ _deviceBox.getSize(_deviceSize);
85
+ var viewerMaxDim = Math.max(_deviceSize.x, _deviceSize.y, _deviceSize.z) || 1;
86
+ var _iterator = _rollupPluginBabelHelpers.createForOfIteratorHelper(anims),
87
+ _step;
88
+ try {
89
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
90
+ var anim = _step.value;
91
+ var mesh = this._resolveMesh(anim, deviceModelRoot);
92
+ if (!mesh) {
93
+ console.warn("[IoBehaviorManager] Could not find mesh for animation \"".concat(anim.name || anim.stateVariable, "\" (uuid: ").concat(anim.meshUuid, ", name: \"").concat(anim.meshName, "\")"));
94
+ continue;
95
+ }
96
+ var worldPos = new THREE__namespace.Vector3();
97
+ mesh.getWorldPosition(worldPos);
98
+ var worldQuat = new THREE__namespace.Quaternion();
99
+ mesh.getWorldQuaternion(worldQuat);
100
+ var box = new THREE__namespace.Box3().setFromObject(mesh);
101
+ var worldCenter = new THREE__namespace.Vector3();
102
+ if (!box.isEmpty()) box.getCenter(worldCenter);else worldCenter.copy(worldPos);
103
+ entries.push({
104
+ anim: anim,
105
+ mesh: mesh,
106
+ origPos: mesh.position.clone(),
107
+ origRot: mesh.rotation.clone(),
108
+ origWorldPos: worldPos,
109
+ origWorldQuat: worldQuat,
110
+ origWorldCenter: worldCenter,
111
+ deviceWorldQuat: deviceWorldQuat.clone(),
112
+ viewerMaxDim: viewerMaxDim
113
+ });
114
+ }
115
+ } catch (err) {
116
+ _iterator.e(err);
117
+ } finally {
118
+ _iterator.f();
119
+ }
120
+ if (entries.length) {
121
+ this._entries.set(key, entries);
122
+ console.log("[IoBehaviorManager] Loaded ".concat(entries.length, " animation(s) for attachment \"").concat(attachmentId, "\" (parent: ").concat(parentUuid, ") \u2014 stateVariables: ").concat(entries.map(function (e) {
123
+ return e.anim.stateVariable;
124
+ }).join(', ')));
125
+ } else {
126
+ console.warn("[IoBehaviorManager] No mesh entries resolved for attachment \"".concat(attachmentId, "\" \u2014 behaviorConfig had ").concat(anims.length, " entries but none matched a mesh"));
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Apply animations triggered by an IO device state change.
132
+ * Should be called in parallel with BehaviorManager.triggerState().
133
+ *
134
+ * @param {string} attachmentId - Raw attachment key (not scoped)
135
+ * @param {string} dataPointId - The data point / state variable id that changed
136
+ * @param {*} value - New state value
137
+ * @param {string} parentUuid - UUID of the host component
138
+ */
139
+ }, {
140
+ key: "triggerState",
141
+ value: function triggerState(attachmentId, dataPointId, value, parentUuid) {
142
+ var key = this._key(parentUuid, attachmentId);
143
+ var entries = this._entries.get(key);
144
+ if (!(entries !== null && entries !== void 0 && entries.length)) return;
145
+ var _iterator2 = _rollupPluginBabelHelpers.createForOfIteratorHelper(entries),
146
+ _step2;
147
+ try {
148
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
149
+ var entry = _step2.value;
150
+ if (entry.anim.stateVariable !== dataPointId) continue;
151
+ this._applyAnimation(entry, value);
152
+ }
153
+ } catch (err) {
154
+ _iterator2.e(err);
155
+ } finally {
156
+ _iterator2.f();
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Return tooltip-compatible data point definitions derived from the loaded
162
+ * animation entries for a given attachment. Used by componentTooltipManager
163
+ * to replace the static ioConfig.states[] snapshot with the richer animation
164
+ * state definitions created in the Animate window.
165
+ *
166
+ * One dp object is emitted per unique stateVariable; multiple mesh entries
167
+ * that share the same stateVariable are collapsed into one.
168
+ *
169
+ * Returned dp shape (matches what _buildDataPointRow / _buildInputControl expect):
170
+ * {
171
+ * id: string, // stateVariable name
172
+ * name: string, // human-readable label (anim.name or stateVariable)
173
+ * stateType: string, // normalised to 'binary' | 'enum' | 'number'
174
+ * stateConfig: Object, // { onLabel?, offLabel?, options?, min?, max?, unit? }
175
+ * defaultValue: any, // sensible default for the stateType
176
+ * direction: 'input' // all animation-driven states are interactive
177
+ * }
178
+ *
179
+ * @param {string} parentUuid
180
+ * @param {string} attachmentId
181
+ * @returns {Object[]} Array of dp objects, or empty array if none loaded.
182
+ */
183
+ }, {
184
+ key: "getAnimationDataPoints",
185
+ value: function getAnimationDataPoints(parentUuid, attachmentId) {
186
+ var _this2 = this;
187
+ var hitMesh = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
188
+ var key = this._key(parentUuid, attachmentId);
189
+ var entries = this._entries.get(key);
190
+ if (!(entries !== null && entries !== void 0 && entries.length)) return [];
191
+
192
+ // When a specific mesh was clicked, filter to only animations whose target
193
+ // mesh is the clicked mesh or an ancestor of it (e.g. the clicked primitive
194
+ // is inside a group that is the animation target).
195
+ var filtered = entries;
196
+ if (hitMesh) {
197
+ var matching = entries.filter(function (e) {
198
+ return _this2._isMeshOrDescendant(hitMesh, e.mesh);
199
+ });
200
+ if (matching.length > 0) filtered = matching;
201
+ }
202
+
203
+ // Collapse multiple mesh entries that share the same stateVariable
204
+ var seen = new Map(); // stateVariable → anim
205
+ var _iterator3 = _rollupPluginBabelHelpers.createForOfIteratorHelper(filtered),
206
+ _step3;
207
+ try {
208
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
209
+ var anim = _step3.value.anim;
210
+ if (!seen.has(anim.stateVariable)) {
211
+ seen.set(anim.stateVariable, anim);
212
+ }
213
+ }
214
+ } catch (err) {
215
+ _iterator3.e(err);
216
+ } finally {
217
+ _iterator3.f();
218
+ }
219
+ var dps = [];
220
+ var _iterator4 = _rollupPluginBabelHelpers.createForOfIteratorHelper(seen),
221
+ _step4;
222
+ try {
223
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
224
+ var _step4$value = _rollupPluginBabelHelpers.slicedToArray(_step4.value, 2),
225
+ stateVar = _step4$value[0],
226
+ _anim = _step4$value[1];
227
+ // Normalise stateType from AnimateDevicesDialog variants
228
+ var stateType = void 0;
229
+ var raw = (_anim.stateType || '').toLowerCase();
230
+ if (raw === 'binary' || raw === 'boolean') {
231
+ stateType = 'binary';
232
+ } else if (raw === 'enum') {
233
+ stateType = 'enum';
234
+ } else {
235
+ // 'continuous', 'range', 'number', or anything else → numeric slider
236
+ stateType = 'number';
237
+ }
238
+
239
+ // Derive stateConfig from mappings
240
+ var stateConfig = {};
241
+ var mappingValues = (_anim.mappings || []).map(function (m) {
242
+ return m.stateValue;
243
+ });
244
+ if (stateType === 'enum') {
245
+ stateConfig.options = _rollupPluginBabelHelpers.toConsumableArray(new Set(mappingValues.map(String)));
246
+ } else if (stateType === 'number') {
247
+ var nums = mappingValues.map(Number).filter(function (n) {
248
+ return !isNaN(n);
249
+ });
250
+ if (nums.length) {
251
+ stateConfig.min = Math.min.apply(Math, _rollupPluginBabelHelpers.toConsumableArray(nums));
252
+ stateConfig.max = Math.max.apply(Math, _rollupPluginBabelHelpers.toConsumableArray(nums));
253
+ }
254
+ }
255
+
256
+ // Sensible default values
257
+ var defaultValue = void 0;
258
+ if (stateType === 'binary') {
259
+ defaultValue = 0;
260
+ } else if (stateType === 'enum') {
261
+ var _stateConfig$options$, _stateConfig$options;
262
+ defaultValue = (_stateConfig$options$ = (_stateConfig$options = stateConfig.options) === null || _stateConfig$options === void 0 ? void 0 : _stateConfig$options[0]) !== null && _stateConfig$options$ !== void 0 ? _stateConfig$options$ : '';
263
+ } else {
264
+ var _stateConfig$min;
265
+ defaultValue = (_stateConfig$min = stateConfig.min) !== null && _stateConfig$min !== void 0 ? _stateConfig$min : 0;
266
+ }
267
+ dps.push({
268
+ id: stateVar,
269
+ name: _anim.name || stateVar,
270
+ stateType: stateType,
271
+ stateConfig: stateConfig,
272
+ defaultValue: defaultValue,
273
+ direction: 'input'
274
+ });
275
+ }
276
+ } catch (err) {
277
+ _iterator4.e(err);
278
+ } finally {
279
+ _iterator4.f();
280
+ }
281
+ return dps;
282
+ }
283
+
284
+ /**
285
+ * Return the Three.js mesh objects that are animated for a given attachment.
286
+ * Used by IoOutlineManager to include animated meshes in the outline.
287
+ *
288
+ * @param {string} parentUuid
289
+ * @param {string} attachmentId
290
+ * @returns {THREE.Object3D[]}
291
+ */
292
+ }, {
293
+ key: "getAnimatedMeshes",
294
+ value: function getAnimatedMeshes(parentUuid, attachmentId) {
295
+ var key = this._key(parentUuid, attachmentId);
296
+ var entries = this._entries.get(key);
297
+ if (!(entries !== null && entries !== void 0 && entries.length)) return [];
298
+ // Deduplicate — multiple animations can target the same mesh
299
+ return _rollupPluginBabelHelpers.toConsumableArray(new Set(entries.map(function (e) {
300
+ return e.mesh;
301
+ })));
302
+ }
303
+
304
+ /**
305
+ * Remove all animation entries associated with a given host component.
306
+ * Call when a component is removed from the scene.
307
+ *
308
+ * @param {string} parentUuid
309
+ */
310
+ }, {
311
+ key: "unloadForComponent",
312
+ value: function unloadForComponent(parentUuid) {
313
+ var prefix = "".concat(parentUuid, "::");
314
+ var _iterator5 = _rollupPluginBabelHelpers.createForOfIteratorHelper(this._entries.keys()),
315
+ _step5;
316
+ try {
317
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
318
+ var key = _step5.value;
319
+ if (key.startsWith(prefix)) {
320
+ this._entries.delete(key);
321
+ }
322
+ }
323
+ } catch (err) {
324
+ _iterator5.e(err);
325
+ } finally {
326
+ _iterator5.f();
327
+ }
328
+ }
329
+ }, {
330
+ key: "dispose",
331
+ value: function dispose() {
332
+ this._entries.clear();
333
+ _rollupPluginBabelHelpers.superPropGet(IoBehaviorManager, "dispose", this, 3)([]);
334
+ }
335
+
336
+ // ─────────────────────────────────────────────────────────────────────────
337
+ // PRIVATE HELPERS
338
+ // ─────────────────────────────────────────────────────────────────────────
339
+ }, {
340
+ key: "_key",
341
+ value: function _key(parentUuid, attachmentId) {
342
+ return "".concat(parentUuid, "::").concat(attachmentId);
343
+ }
344
+
345
+ /**
346
+ * Returns true if `candidate` is `ancestor` or any descendant of `ancestor`.
347
+ * @param {THREE.Object3D} candidate
348
+ * @param {THREE.Object3D} ancestor
349
+ */
350
+ }, {
351
+ key: "_isMeshOrDescendant",
352
+ value: function _isMeshOrDescendant(candidate, ancestor) {
353
+ var obj = candidate;
354
+ while (obj) {
355
+ if (obj === ancestor) return true;
356
+ obj = obj.parent;
357
+ }
358
+ return false;
359
+ }
360
+
361
+ /**
362
+ * Find the mesh inside `root` using UUID first, then name as fallback.
363
+ * GLTFLoader assigns fresh UUIDs on every load, so name is the reliable key.
364
+ * @param {Object} anim
365
+ * @param {THREE.Object3D} root
366
+ * @returns {THREE.Object3D|null}
367
+ */
368
+ }, {
369
+ key: "_resolveMesh",
370
+ value: function _resolveMesh(anim, root) {
371
+ var found = null;
372
+ root.traverse(function (obj) {
373
+ if (found) return;
374
+
375
+ // Prefer UUID match (works when the device was cloned from a cached instance
376
+ // whose UUIDs were preserved)
377
+ if (anim.meshUuid && obj.uuid === anim.meshUuid) {
378
+ found = obj;
379
+ return;
380
+ }
381
+
382
+ // Reliable fallback: mesh name
383
+ if (anim.meshName && obj.name === anim.meshName) {
384
+ found = obj;
385
+ }
386
+ });
387
+ return found;
388
+ }
389
+
390
+ /**
391
+ * Resolve which mapping row to use and apply all declared transform types.
392
+ * @param {{ anim, mesh, origPos, origRot }} entry
393
+ * @param {*} value - Current state value
394
+ */
395
+ }, {
396
+ key: "_applyAnimation",
397
+ value: function _applyAnimation(entry, value) {
398
+ var anim = entry.anim,
399
+ mesh = entry.mesh,
400
+ origPos = entry.origPos,
401
+ origRot = entry.origRot,
402
+ origWorldPos = entry.origWorldPos,
403
+ origWorldQuat = entry.origWorldQuat,
404
+ origWorldCenter = entry.origWorldCenter,
405
+ deviceWorldQuat = entry.deviceWorldQuat,
406
+ viewerMaxDim = entry.viewerMaxDim;
407
+ var mapping = this._resolveMapping(anim, value);
408
+ if (!mapping) return;
409
+ var types = anim.transformTypes || [];
410
+ var _iterator6 = _rollupPluginBabelHelpers.createForOfIteratorHelper(types),
411
+ _step6;
412
+ try {
413
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
414
+ var type = _step6.value;
415
+ if (type === 'translation') {
416
+ this._applyTranslation(mesh, origPos, mapping.transform);
417
+ } else if (type === 'rotation') {
418
+ this._applyRotation(mesh, origPos, origRot, anim, mapping.rotationTransform, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim);
419
+ } else if (type === 'color') {
420
+ this._applyColor(mesh, mapping.colorTransform);
421
+ }
422
+ }
423
+ } catch (err) {
424
+ _iterator6.e(err);
425
+ } finally {
426
+ _iterator6.f();
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Find the mapping row that matches `value` for the given animation entry.
432
+ *
433
+ * - binary / enum: find where mapping.stateValue == value (loose equality so
434
+ * JSON-deserialised "true" matches boolean true)
435
+ * - continuous: linear interpolation between ordered anchor points
436
+ *
437
+ * @returns {Object|null} The matched/interpolated mapping row, or null.
438
+ */
439
+ }, {
440
+ key: "_resolveMapping",
441
+ value: function _resolveMapping(anim, value) {
442
+ var mappings = anim.mappings;
443
+ if (!(mappings !== null && mappings !== void 0 && mappings.length)) return null;
444
+
445
+ // Normalise stateType variants saved by AnimateDevicesDialog
446
+ // ('boolean' → 'binary', 'range' → 'continuous')
447
+ var raw = (anim.stateType || '').toLowerCase();
448
+ var stateType;
449
+ if (raw === 'binary' || raw === 'boolean') {
450
+ stateType = 'binary';
451
+ } else if (raw === 'continuous' || raw === 'range') {
452
+ stateType = 'continuous';
453
+ } else {
454
+ stateType = raw;
455
+ }
456
+ if (stateType === 'binary' || stateType === 'enum') {
457
+ var _mappings$find;
458
+ // eslint-disable-next-line eqeqeq
459
+ return (_mappings$find = mappings.find(function (m) {
460
+ return m.stateValue == value;
461
+ })) !== null && _mappings$find !== void 0 ? _mappings$find : null;
462
+ }
463
+ if (stateType === 'continuous') {
464
+ var num = Number(value);
465
+ if (isNaN(num)) return null;
466
+
467
+ // Sort anchors by numeric stateValue
468
+ var sorted = _rollupPluginBabelHelpers.toConsumableArray(mappings).filter(function (m) {
469
+ return m.stateValue != null && !isNaN(Number(m.stateValue));
470
+ }).sort(function (a, b) {
471
+ return Number(a.stateValue) - Number(b.stateValue);
472
+ });
473
+ if (!sorted.length) return null;
474
+ if (sorted.length === 1) return sorted[0];
475
+
476
+ // Clamp to range
477
+ if (num <= Number(sorted[0].stateValue)) return sorted[0];
478
+ if (num >= Number(sorted[sorted.length - 1].stateValue)) return sorted[sorted.length - 1];
479
+
480
+ // Find surrounding anchors and interpolate
481
+ for (var i = 0; i < sorted.length - 1; i++) {
482
+ var lo = sorted[i];
483
+ var hi = sorted[i + 1];
484
+ var loVal = Number(lo.stateValue);
485
+ var hiVal = Number(hi.stateValue);
486
+ if (num >= loVal && num <= hiVal) {
487
+ var t = (num - loVal) / (hiVal - loVal);
488
+ return this._lerpMapping(lo, hi, t);
489
+ }
490
+ }
491
+ }
492
+ return null;
493
+ }
494
+
495
+ /**
496
+ * Linear-interpolate between two mapping rows.
497
+ * @param {Object} lo - lower anchor mapping
498
+ * @param {Object} hi - upper anchor mapping
499
+ * @param {number} t - 0..1
500
+ */
501
+ }, {
502
+ key: "_lerpMapping",
503
+ value: function _lerpMapping(lo, hi, t) {
504
+ 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;
505
+ var lerp = function lerp(a, b) {
506
+ return a + (b - a) * t;
507
+ };
508
+ var transform = {
509
+ 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),
510
+ 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),
511
+ 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)
512
+ };
513
+ var rotationTransform = lerp(typeof lo.rotationTransform === 'number' ? lo.rotationTransform : 0, typeof hi.rotationTransform === 'number' ? hi.rotationTransform : 0);
514
+
515
+ // Color interpolation: convert hex → RGB components → lerp → back to hex
516
+ var colorTransform = this._lerpHex(lo.colorTransform, hi.colorTransform, t);
517
+ return {
518
+ stateValue: null,
519
+ transform: transform,
520
+ rotationTransform: rotationTransform,
521
+ colorTransform: colorTransform
522
+ };
523
+ }
524
+
525
+ /**
526
+ * Interpolate between two hex colour strings.
527
+ */
528
+ }, {
529
+ key: "_lerpHex",
530
+ value: function _lerpHex(hexA, hexB, t) {
531
+ try {
532
+ var ca = new THREE__namespace.Color(hexA);
533
+ var cb = new THREE__namespace.Color(hexB);
534
+ ca.lerp(cb, t);
535
+ return "#".concat(ca.getHexString());
536
+ } catch (_unused) {
537
+ return hexA !== null && hexA !== void 0 ? hexA : '#ffffff';
538
+ }
539
+ }
540
+
541
+ // ─────────────────────────────────────────────────────────────────────────
542
+ // TRANSFORM APPLIERS
543
+ // ─────────────────────────────────────────────────────────────────────────
544
+
545
+ /**
546
+ * Apply a position delta relative to the mesh's original position.
547
+ * @param {THREE.Object3D} mesh
548
+ * @param {THREE.Vector3} origPos
549
+ * @param {{ x, y, z }} transform - Deltas
550
+ */
551
+ }, {
552
+ key: "_applyTranslation",
553
+ value: function _applyTranslation(mesh, origPos, transform) {
554
+ var _transform$x, _transform$y, _transform$z;
555
+ if (!transform) return;
556
+ // X and Y are negated to match the sign convention used in the AnimateDevicesDialog
557
+ // preview (_syncViewerTransform negates x and y before calling setMeshPreviewOffset).
558
+ // Z is added directly (no negation) — matching the dialog's z handling.
559
+ 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));
560
+ }
561
+
562
+ /**
563
+ * Apply a rotation around an arbitrary pivot point using world-space quaternion
564
+ * math matching the ReconstructionViewer's setMeshPreviewRotationAxis approach.
565
+ * This ensures the runtime axis/pivot matches what the user configured in the
566
+ * animation dialog, regardless of the device's parent transform in the scene.
567
+ *
568
+ * @param {THREE.Object3D} mesh
569
+ * @param {THREE.Vector3} origPos - local position at load time (unused, kept for signature compat)
570
+ * @param {THREE.Euler} origRot - local rotation at load time (unused)
571
+ * @param {Object} anim
572
+ * @param {number} angleDeg - Degrees
573
+ * @param {THREE.Vector3} origWorldPos - world position at load time
574
+ * @param {THREE.Quaternion} origWorldQuat - world quaternion at load time
575
+ * @param {THREE.Vector3} origWorldCenter - world bounding-box center at load time
576
+ */
577
+ }, {
578
+ key: "_applyRotation",
579
+ value: function _applyRotation(mesh, origPos, origRot, anim, angleDeg, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim) {
580
+ var _anim$rotAxis, _anim$rotAxisOffset;
581
+ var angle = THREE__namespace.MathUtils.degToRad(typeof angleDeg === 'number' ? angleDeg : 0);
582
+ var axis = ((_anim$rotAxis = anim.rotAxis) !== null && _anim$rotAxis !== void 0 ? _anim$rotAxis : 'x').toLowerCase();
583
+ var off = (_anim$rotAxisOffset = anim.rotAxisOffset) !== null && _anim$rotAxisOffset !== void 0 ? _anim$rotAxisOffset : {
584
+ x: 0,
585
+ y: 0,
586
+ z: 0
587
+ };
588
+
589
+ // Local axis in the device's coordinate system
590
+ var localAxisVec = new THREE__namespace.Vector3(axis === 'x' ? 1 : 0, axis === 'y' ? 1 : 0, axis === 'z' ? 1 : 0);
591
+ if (origWorldPos && origWorldQuat && origWorldCenter) {
592
+ // Transform the configured axis from device-local space into world space
593
+ // so that 'X' means the device's local X, not the world X.
594
+ var worldAxisVec = localAxisVec.clone().applyQuaternion(deviceWorldQuat || new THREE__namespace.Quaternion());
595
+
596
+ // rotAxisOffset is stored in dialog-viewer-world space where the model root
597
+ // has been rotated Rot_Z(π) by default (ReconstructionViewer's else branch).
598
+ // Translation uses the same convention and negates X/Y to compensate.
599
+ // For rotation offset we apply the same compensation mathematically:
600
+ // offset_runtime = deviceWorldQuat × Rot_Z(π) × offset_dialog × scale
601
+ // This maps from dialog world space (post Z+180° flip) → model local → world.
602
+ var scale = viewerMaxDim || 1;
603
+ var rotZ180 = new THREE__namespace.Quaternion().setFromAxisAngle(new THREE__namespace.Vector3(0, 0, 1), Math.PI);
604
+ var qCombined = (deviceWorldQuat || new THREE__namespace.Quaternion()).clone().multiply(rotZ180);
605
+ var offWorld = new THREE__namespace.Vector3((off.x || 0) * scale, (off.y || 0) * scale, (off.z || 0) * scale).applyQuaternion(qCombined);
606
+ var pivot = origWorldCenter.clone().add(offWorld);
607
+ var deltaQuat = new THREE__namespace.Quaternion().setFromAxisAngle(worldAxisVec, angle);
608
+
609
+ // Rotate world position around pivot
610
+ var offsetVec = origWorldPos.clone().sub(pivot);
611
+ offsetVec.applyQuaternion(deltaQuat);
612
+ var newWorldPos = pivot.clone().add(offsetVec);
613
+
614
+ // Compose world quaternion
615
+ var newWorldQuat = deltaQuat.clone().multiply(origWorldQuat);
616
+
617
+ // Convert world position → parent local space
618
+ if (mesh.parent) {
619
+ mesh.parent.worldToLocal(newWorldPos);
620
+ }
621
+ mesh.position.copy(newWorldPos);
622
+
623
+ // Convert quaternion → parent local space
624
+ if (mesh.parent) {
625
+ var parentWorldQuat = new THREE__namespace.Quaternion();
626
+ mesh.parent.getWorldQuaternion(parentWorldQuat);
627
+ parentWorldQuat.invert();
628
+ newWorldQuat.premultiply(parentWorldQuat);
629
+ }
630
+ mesh.quaternion.copy(newWorldQuat);
631
+ } else {
632
+ var _off$x, _off$y, _off$z;
633
+ // Fallback for entries loaded without world data
634
+ 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);
635
+ var delta = origPos.clone().sub(_pivot);
636
+ delta.applyAxisAngle(localAxisVec, angle);
637
+ mesh.position.copy(_pivot).add(delta);
638
+ mesh.rotation[axis] = origRot[axis] + angle;
639
+ }
640
+ }
641
+
642
+ /**
643
+ * Apply a colour to all Mesh descendants of `mesh`.
644
+ * Creates a cloned material per mesh on first call to avoid shared-material
645
+ * cross-contamination between different device instances.
646
+ *
647
+ * @param {THREE.Object3D} mesh
648
+ * @param {string} colorHex - e.g. '#ff0000'
649
+ */
650
+ }, {
651
+ key: "_applyColor",
652
+ value: function _applyColor(mesh, colorHex) {
653
+ if (!colorHex) return;
654
+ mesh.traverse(function (obj) {
655
+ if (!obj.isMesh || !obj.material) return;
656
+
657
+ // Ensure this mesh has its own material instance
658
+ if (!obj.userData._materialCloned) {
659
+ obj.material = obj.material.clone();
660
+ obj.userData._materialCloned = true;
661
+ }
662
+ try {
663
+ obj.material.color.set(colorHex);
664
+ } catch (e) {
665
+ // Material may not have a color property (e.g. MeshDepthMaterial)
666
+ }
667
+ });
668
+ }
669
+ }]);
670
+ }(baseDisposable.BaseDisposable);
671
+
672
+ exports.IoBehaviorManager = IoBehaviorManager;