iobroker.mywebui 1.42.14 → 1.42.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/io-package.json
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 3D Drive Engine for mywebui 3D Screen Editor
|
|
3
|
+
*
|
|
4
|
+
* Kinematics system inspired by realvirtual's architecture.
|
|
5
|
+
* Written from scratch - no code copied, same logic independently implemented.
|
|
6
|
+
*
|
|
7
|
+
* Concepts:
|
|
8
|
+
* Drive - moves a Three.js Object3D along an axis (linear mm or rotation deg)
|
|
9
|
+
* SignalBus - subscribes to ioBroker signals via iobrokerHandler
|
|
10
|
+
* DriveCylinder - extends/retracts between MinPos/MaxPos on ioBroker bool signals
|
|
11
|
+
* DriveSimple - jogs continuously Forward/Backward on ioBroker bool signals
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { iobrokerHandler } from '../common/IobrokerHandler.js';
|
|
15
|
+
|
|
16
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const MM_TO_M = 0.001;
|
|
19
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
20
|
+
|
|
21
|
+
// ── Drive directions ─────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export const DriveDirection = {
|
|
24
|
+
LinearX: 'LinearX',
|
|
25
|
+
LinearY: 'LinearY',
|
|
26
|
+
LinearZ: 'LinearZ',
|
|
27
|
+
RotationX: 'RotationX',
|
|
28
|
+
RotationY: 'RotationY',
|
|
29
|
+
RotationZ: 'RotationZ',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function isRotation(dir) {
|
|
33
|
+
return dir === DriveDirection.RotationX ||
|
|
34
|
+
dir === DriveDirection.RotationY ||
|
|
35
|
+
dir === DriveDirection.RotationZ;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Signal Bus ───────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* SignalBus - bridges ioBroker states to the drive engine.
|
|
42
|
+
* Subscribes to ioBroker signals and dispatches value changes to listeners.
|
|
43
|
+
* ioBrokerHandler.subscribeState is used for live updates.
|
|
44
|
+
*/
|
|
45
|
+
export class SignalBus {
|
|
46
|
+
constructor() {
|
|
47
|
+
/** signalPath → current value */
|
|
48
|
+
this._values = new Map();
|
|
49
|
+
/** signalPath → Set<callback> */
|
|
50
|
+
this._listeners = new Map();
|
|
51
|
+
/** signalPath → unsubscribe function */
|
|
52
|
+
this._unsubs = new Map();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Subscribe to an ioBroker signal. Returns unsubscribe function. */
|
|
56
|
+
subscribe(signalPath, cb) {
|
|
57
|
+
if (!signalPath) return () => {};
|
|
58
|
+
|
|
59
|
+
// Register listener
|
|
60
|
+
if (!this._listeners.has(signalPath)) {
|
|
61
|
+
this._listeners.set(signalPath, new Set());
|
|
62
|
+
}
|
|
63
|
+
this._listeners.get(signalPath).add(cb);
|
|
64
|
+
|
|
65
|
+
// Subscribe to ioBroker if not already
|
|
66
|
+
if (!this._unsubs.has(signalPath)) {
|
|
67
|
+
const unsub = iobrokerHandler.subscribe(signalPath, (id, state) => {
|
|
68
|
+
const val = state?.val ?? false;
|
|
69
|
+
this._values.set(signalPath, val);
|
|
70
|
+
const subs = this._listeners.get(signalPath);
|
|
71
|
+
if (subs) for (const fn of subs) fn(val);
|
|
72
|
+
});
|
|
73
|
+
this._unsubs.set(signalPath, unsub);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Fire immediately with current value if known
|
|
77
|
+
if (this._values.has(signalPath)) {
|
|
78
|
+
cb(this._values.get(signalPath));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
const subs = this._listeners.get(signalPath);
|
|
83
|
+
if (subs) {
|
|
84
|
+
subs.delete(cb);
|
|
85
|
+
if (subs.size === 0) {
|
|
86
|
+
this._listeners.delete(signalPath);
|
|
87
|
+
const unsub = this._unsubs.get(signalPath);
|
|
88
|
+
if (unsub) unsub();
|
|
89
|
+
this._unsubs.delete(signalPath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Get current value of a signal (or undefined) */
|
|
96
|
+
get(signalPath) {
|
|
97
|
+
return this._values.get(signalPath);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Write a value back to ioBroker (for feedback signals) */
|
|
101
|
+
set(signalPath, value) {
|
|
102
|
+
if (!signalPath) return;
|
|
103
|
+
iobrokerHandler.setState(signalPath, value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Dispose all subscriptions */
|
|
107
|
+
dispose() {
|
|
108
|
+
for (const unsub of this._unsubs.values()) {
|
|
109
|
+
try { unsub(); } catch (_) {}
|
|
110
|
+
}
|
|
111
|
+
this._values.clear();
|
|
112
|
+
this._listeners.clear();
|
|
113
|
+
this._unsubs.clear();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Drive ────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Drive - moves a Three.js Object3D along a single axis.
|
|
121
|
+
*
|
|
122
|
+
* Position units: mm for linear, degrees for rotation.
|
|
123
|
+
* Three.js units: meters for linear, radians for rotation.
|
|
124
|
+
*/
|
|
125
|
+
export class Drive {
|
|
126
|
+
/**
|
|
127
|
+
* @param {Object3D} node - Three.js object to animate
|
|
128
|
+
* @param {object} config - drive configuration from asset.drives[]
|
|
129
|
+
*/
|
|
130
|
+
constructor(node, config = {}) {
|
|
131
|
+
this.node = node;
|
|
132
|
+
this.name = node.name || 'Drive';
|
|
133
|
+
|
|
134
|
+
// Config (from asset JSON)
|
|
135
|
+
this.direction = config.direction ?? DriveDirection.LinearX;
|
|
136
|
+
this.reverseDir = config.reverseDir ?? false;
|
|
137
|
+
this.offset = config.offset ?? 0;
|
|
138
|
+
this.startPosition = config.startPosition ?? 0;
|
|
139
|
+
this.targetSpeed = config.targetSpeed ?? 100; // mm/s or deg/s
|
|
140
|
+
this.acceleration = config.acceleration ?? 0; // 0 = instant
|
|
141
|
+
this.useLimits = config.useLimits ?? false;
|
|
142
|
+
this.lowerLimit = config.lowerLimit ?? -180;
|
|
143
|
+
this.upperLimit = config.upperLimit ?? 180;
|
|
144
|
+
|
|
145
|
+
// Runtime state
|
|
146
|
+
this.currentPosition = this.startPosition;
|
|
147
|
+
this.targetPosition = this.startPosition;
|
|
148
|
+
this.currentSpeed = 0;
|
|
149
|
+
this.isRunning = false;
|
|
150
|
+
this.jogForward = false;
|
|
151
|
+
this.jogBackward = false;
|
|
152
|
+
|
|
153
|
+
// Base transform (rest position)
|
|
154
|
+
this._basePos = node.position.clone();
|
|
155
|
+
this._baseQuat = node.quaternion.clone();
|
|
156
|
+
|
|
157
|
+
this._isRotation = isRotation(this.direction);
|
|
158
|
+
|
|
159
|
+
// Callbacks
|
|
160
|
+
this.onAfterUpdate = null; // (drive) => void
|
|
161
|
+
|
|
162
|
+
// Apply start position immediately
|
|
163
|
+
this._applyToNode();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
get isAtTarget() {
|
|
167
|
+
return Math.abs(this.currentPosition - this.targetPosition) < 0.01;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Move to a specific position */
|
|
171
|
+
moveTo(pos, speed) {
|
|
172
|
+
this.targetPosition = pos;
|
|
173
|
+
if (speed !== undefined) this.targetSpeed = speed;
|
|
174
|
+
this.isRunning = true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
stop() {
|
|
178
|
+
this.isRunning = false;
|
|
179
|
+
this.currentSpeed = 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Called every frame from DriveEngine tick */
|
|
183
|
+
update(dt) {
|
|
184
|
+
if (this.jogForward || this.jogBackward) {
|
|
185
|
+
// Jog mode: continuous motion (conveyor-style)
|
|
186
|
+
this.currentSpeed = this.targetSpeed;
|
|
187
|
+
const dir = this.jogForward ? 1 : -1;
|
|
188
|
+
let next = this.currentPosition + dir * this.currentSpeed * dt;
|
|
189
|
+
if (this.useLimits) {
|
|
190
|
+
next = Math.max(this.lowerLimit, Math.min(this.upperLimit, next));
|
|
191
|
+
}
|
|
192
|
+
this.currentPosition = next;
|
|
193
|
+
this._applyToNode();
|
|
194
|
+
this.onAfterUpdate?.(this);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!this.isRunning) return;
|
|
199
|
+
|
|
200
|
+
const dist = this.targetPosition - this.currentPosition;
|
|
201
|
+
if (Math.abs(dist) < 0.01) {
|
|
202
|
+
this.currentPosition = this.targetPosition;
|
|
203
|
+
this.isRunning = false;
|
|
204
|
+
this.currentSpeed = 0;
|
|
205
|
+
this._applyToNode();
|
|
206
|
+
this.onAfterUpdate?.(this);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const dir = Math.sign(dist);
|
|
211
|
+
const spd = this.targetSpeed;
|
|
212
|
+
|
|
213
|
+
if (this.acceleration > 0) {
|
|
214
|
+
// Kinematic acceleration/deceleration
|
|
215
|
+
const stopDist = (this.currentSpeed * this.currentSpeed) / (2 * this.acceleration);
|
|
216
|
+
if (stopDist >= Math.abs(dist)) {
|
|
217
|
+
this.currentSpeed = Math.max(0, this.currentSpeed - this.acceleration * dt);
|
|
218
|
+
} else if (this.currentSpeed < spd) {
|
|
219
|
+
this.currentSpeed = Math.min(spd, this.currentSpeed + this.acceleration * dt);
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
this.currentSpeed = spd;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let next = this.currentPosition + dir * this.currentSpeed * dt;
|
|
226
|
+
// Clamp to target
|
|
227
|
+
if (dir > 0 && next > this.targetPosition) next = this.targetPosition;
|
|
228
|
+
if (dir < 0 && next < this.targetPosition) next = this.targetPosition;
|
|
229
|
+
// Apply limits
|
|
230
|
+
if (this.useLimits) {
|
|
231
|
+
next = Math.max(this.lowerLimit, Math.min(this.upperLimit, next));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this.currentPosition = next;
|
|
235
|
+
this._applyToNode();
|
|
236
|
+
this.onAfterUpdate?.(this);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
_applyToNode() {
|
|
240
|
+
const pos = this.currentPosition + this.offset;
|
|
241
|
+
const sign = this.reverseDir ? -1 : 1;
|
|
242
|
+
|
|
243
|
+
switch (this.direction) {
|
|
244
|
+
case DriveDirection.LinearX:
|
|
245
|
+
this.node.position.x = this._basePos.x + sign * pos * MM_TO_M;
|
|
246
|
+
break;
|
|
247
|
+
case DriveDirection.LinearY:
|
|
248
|
+
this.node.position.y = this._basePos.y + sign * pos * MM_TO_M;
|
|
249
|
+
break;
|
|
250
|
+
case DriveDirection.LinearZ:
|
|
251
|
+
this.node.position.z = this._basePos.z + sign * pos * MM_TO_M;
|
|
252
|
+
break;
|
|
253
|
+
case DriveDirection.RotationX: {
|
|
254
|
+
const rad = sign * pos * DEG_TO_RAD;
|
|
255
|
+
this.node.quaternion.copy(this._baseQuat);
|
|
256
|
+
this.node.rotateX(rad);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
case DriveDirection.RotationY: {
|
|
260
|
+
const rad = sign * pos * DEG_TO_RAD;
|
|
261
|
+
this.node.quaternion.copy(this._baseQuat);
|
|
262
|
+
this.node.rotateY(rad);
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
case DriveDirection.RotationZ: {
|
|
266
|
+
const rad = sign * pos * DEG_TO_RAD;
|
|
267
|
+
this.node.quaternion.copy(this._baseQuat);
|
|
268
|
+
this.node.rotateZ(rad);
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Serialize current config back to JSON */
|
|
275
|
+
toConfig() {
|
|
276
|
+
return {
|
|
277
|
+
direction: this.direction,
|
|
278
|
+
reverseDir: this.reverseDir,
|
|
279
|
+
offset: this.offset,
|
|
280
|
+
startPosition: this.startPosition,
|
|
281
|
+
targetSpeed: this.targetSpeed,
|
|
282
|
+
acceleration: this.acceleration,
|
|
283
|
+
useLimits: this.useLimits,
|
|
284
|
+
lowerLimit: this.lowerLimit,
|
|
285
|
+
upperLimit: this.upperLimit,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── DriveCylinder ─────────────────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* DriveCylinder - pneumatic/hydraulic cylinder automation.
|
|
294
|
+
*
|
|
295
|
+
* Listens to two ioBroker signals (Out/In).
|
|
296
|
+
* Out=true → moves to MaxPos at stroke/TimeOut speed
|
|
297
|
+
* In=true → moves to MinPos at stroke/TimeIn speed
|
|
298
|
+
* Writes feedback signals: IsOut, IsIn, IsMovingOut, IsMovingIn
|
|
299
|
+
*/
|
|
300
|
+
export class DriveCylinder {
|
|
301
|
+
/**
|
|
302
|
+
* @param {Drive} drive - the underlying drive
|
|
303
|
+
* @param {SignalBus} bus - signal bus
|
|
304
|
+
* @param {object} config - cylinder config
|
|
305
|
+
*/
|
|
306
|
+
constructor(drive, bus, config = {}) {
|
|
307
|
+
this.drive = drive;
|
|
308
|
+
this.bus = bus;
|
|
309
|
+
this.name = drive.name;
|
|
310
|
+
|
|
311
|
+
this.minPos = config.minPos ?? 0;
|
|
312
|
+
this.maxPos = config.maxPos ?? 100;
|
|
313
|
+
this.timeOut = config.timeOut ?? 1; // seconds to extend
|
|
314
|
+
this.timeIn = config.timeIn ?? 1; // seconds to retract
|
|
315
|
+
this.oneBit = config.oneBit ?? false;
|
|
316
|
+
this.invertLogic = config.invertLogic ?? false;
|
|
317
|
+
|
|
318
|
+
// ioBroker signal paths
|
|
319
|
+
this.signalOut = config.signalOut ?? null;
|
|
320
|
+
this.signalIn = config.signalIn ?? null;
|
|
321
|
+
this.signalIsOut = config.signalIsOut ?? null;
|
|
322
|
+
this.signalIsIn = config.signalIsIn ?? null;
|
|
323
|
+
this.signalMovingOut = config.signalMovingOut ?? null;
|
|
324
|
+
this.signalMovingIn = config.signalMovingIn ?? null;
|
|
325
|
+
|
|
326
|
+
this._unsubs = [];
|
|
327
|
+
this._init();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
_init() {
|
|
331
|
+
const { drive, bus } = this;
|
|
332
|
+
const stroke = Math.abs(this.maxPos - this.minPos);
|
|
333
|
+
|
|
334
|
+
// Set initial position
|
|
335
|
+
drive.currentPosition = this.minPos;
|
|
336
|
+
drive._applyToNode();
|
|
337
|
+
|
|
338
|
+
if (this.signalOut) {
|
|
339
|
+
const unsub = bus.subscribe(this.signalOut, (val) => {
|
|
340
|
+
let v = Boolean(val);
|
|
341
|
+
if (this.invertLogic) v = !v;
|
|
342
|
+
if (this.oneBit) {
|
|
343
|
+
drive.targetSpeed = stroke / (v ? this.timeOut : this.timeIn);
|
|
344
|
+
drive.moveTo(v ? this.maxPos : this.minPos);
|
|
345
|
+
} else if (v) {
|
|
346
|
+
drive.targetSpeed = stroke / this.timeOut;
|
|
347
|
+
drive.moveTo(this.maxPos);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
this._unsubs.push(unsub);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (this.signalIn && !this.oneBit) {
|
|
354
|
+
const unsub = bus.subscribe(this.signalIn, (val) => {
|
|
355
|
+
let v = Boolean(val);
|
|
356
|
+
if (this.invertLogic) v = !v;
|
|
357
|
+
if (v) {
|
|
358
|
+
drive.targetSpeed = stroke / this.timeIn;
|
|
359
|
+
drive.moveTo(this.minPos);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
this._unsubs.push(unsub);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Feedback signals via onAfterUpdate
|
|
366
|
+
if (this.signalIsOut || this.signalIsIn || this.signalMovingOut || this.signalMovingIn) {
|
|
367
|
+
let prevIsOut = false, prevIsIn = true;
|
|
368
|
+
drive.onAfterUpdate = (d) => {
|
|
369
|
+
const atMax = Math.abs(d.currentPosition - this.maxPos) < 0.01;
|
|
370
|
+
const atMin = Math.abs(d.currentPosition - this.minPos) < 0.01;
|
|
371
|
+
const movingOut = d.isRunning && d.targetPosition === this.maxPos;
|
|
372
|
+
const movingIn = d.isRunning && d.targetPosition === this.minPos;
|
|
373
|
+
|
|
374
|
+
if (atMax !== prevIsOut) {
|
|
375
|
+
if (this.signalIsOut) bus.set(this.signalIsOut, atMax);
|
|
376
|
+
if (this.signalIsOut) bus.set(this.signalIsOut + '_max', atMax);
|
|
377
|
+
prevIsOut = atMax;
|
|
378
|
+
}
|
|
379
|
+
if (atMin !== prevIsIn) {
|
|
380
|
+
if (this.signalIsIn) bus.set(this.signalIsIn, atMin);
|
|
381
|
+
prevIsIn = atMin;
|
|
382
|
+
}
|
|
383
|
+
if (this.signalMovingOut) bus.set(this.signalMovingOut, movingOut);
|
|
384
|
+
if (this.signalMovingIn) bus.set(this.signalMovingIn, movingIn);
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
dispose() {
|
|
390
|
+
for (const fn of this._unsubs) fn();
|
|
391
|
+
this._unsubs = [];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
toConfig() {
|
|
395
|
+
return {
|
|
396
|
+
type: 'cylinder',
|
|
397
|
+
minPos: this.minPos,
|
|
398
|
+
maxPos: this.maxPos,
|
|
399
|
+
timeOut: this.timeOut,
|
|
400
|
+
timeIn: this.timeIn,
|
|
401
|
+
oneBit: this.oneBit,
|
|
402
|
+
invertLogic: this.invertLogic,
|
|
403
|
+
signalOut: this.signalOut,
|
|
404
|
+
signalIn: this.signalIn,
|
|
405
|
+
signalIsOut: this.signalIsOut,
|
|
406
|
+
signalIsIn: this.signalIsIn,
|
|
407
|
+
signalMovingOut: this.signalMovingOut,
|
|
408
|
+
signalMovingIn: this.signalMovingIn,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── DriveSimple ───────────────────────────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* DriveSimple - continuous jog motion (conveyor belt, rotating drum).
|
|
417
|
+
* Forward=true → jog forward, Backward=true → jog backward
|
|
418
|
+
*/
|
|
419
|
+
export class DriveSimple {
|
|
420
|
+
constructor(drive, bus, config = {}) {
|
|
421
|
+
this.drive = drive;
|
|
422
|
+
this.bus = bus;
|
|
423
|
+
this.signalForward = config.signalForward ?? null;
|
|
424
|
+
this.signalBackward = config.signalBackward ?? null;
|
|
425
|
+
this._unsubs = [];
|
|
426
|
+
this._init();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
_init() {
|
|
430
|
+
const { drive, bus } = this;
|
|
431
|
+
if (this.signalForward) {
|
|
432
|
+
const unsub = bus.subscribe(this.signalForward, (v) => {
|
|
433
|
+
drive.jogForward = Boolean(v);
|
|
434
|
+
if (drive.jogForward) drive.jogBackward = false;
|
|
435
|
+
});
|
|
436
|
+
this._unsubs.push(unsub);
|
|
437
|
+
}
|
|
438
|
+
if (this.signalBackward) {
|
|
439
|
+
const unsub = bus.subscribe(this.signalBackward, (v) => {
|
|
440
|
+
drive.jogBackward = Boolean(v);
|
|
441
|
+
if (drive.jogBackward) drive.jogForward = false;
|
|
442
|
+
});
|
|
443
|
+
this._unsubs.push(unsub);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
dispose() {
|
|
448
|
+
for (const fn of this._unsubs) fn();
|
|
449
|
+
this._unsubs = [];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
toConfig() {
|
|
453
|
+
return {
|
|
454
|
+
type: 'simple',
|
|
455
|
+
signalForward: this.signalForward,
|
|
456
|
+
signalBackward: this.signalBackward,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ── DriveEngine ───────────────────────────────────────────────────────────────
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* DriveEngine - main animation/kinematics engine.
|
|
465
|
+
*
|
|
466
|
+
* Manages all drives and ticks them on each animation frame.
|
|
467
|
+
* Integrates with the 3D editor's animation loop.
|
|
468
|
+
*
|
|
469
|
+
* Usage:
|
|
470
|
+
* const engine = new DriveEngine();
|
|
471
|
+
* engine.createFromAssets(sceneData.assets, threeObjects);
|
|
472
|
+
* // In animation loop:
|
|
473
|
+
* engine.tick(dt);
|
|
474
|
+
*/
|
|
475
|
+
export class DriveEngine {
|
|
476
|
+
constructor() {
|
|
477
|
+
this.bus = new SignalBus();
|
|
478
|
+
this.drives = []; // Drive[]
|
|
479
|
+
this.controllers = []; // DriveCylinder | DriveSimple
|
|
480
|
+
this._lastTime = null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Create drives from sceneData assets.
|
|
485
|
+
* Each asset can have an array `drives` in its config:
|
|
486
|
+
*
|
|
487
|
+
* asset.drives = [{
|
|
488
|
+
* direction: 'LinearY',
|
|
489
|
+
* targetSpeed: 50,
|
|
490
|
+
* type: 'cylinder',
|
|
491
|
+
* minPos: 0, maxPos: 200,
|
|
492
|
+
* timeOut: 1.5, timeIn: 1.0,
|
|
493
|
+
* signalOut: 'mywebui.0.device.cylinder_out',
|
|
494
|
+
* signalIn: 'mywebui.0.device.cylinder_in',
|
|
495
|
+
* signalIsOut: 'mywebui.0.device.cylinder_isout',
|
|
496
|
+
* }]
|
|
497
|
+
*/
|
|
498
|
+
createFromAssets(assets, threeObjects) {
|
|
499
|
+
this.dispose();
|
|
500
|
+
|
|
501
|
+
for (const asset of assets) {
|
|
502
|
+
if (!asset.drives || asset.drives.length === 0) continue;
|
|
503
|
+
|
|
504
|
+
// Find matching Three.js object
|
|
505
|
+
const obj = threeObjects.find(o => o.userData.assetData?.id === asset.id);
|
|
506
|
+
if (!obj) continue;
|
|
507
|
+
|
|
508
|
+
for (const driveCfg of asset.drives) {
|
|
509
|
+
const drive = new Drive(obj, driveCfg);
|
|
510
|
+
this.drives.push(drive);
|
|
511
|
+
|
|
512
|
+
if (driveCfg.type === 'cylinder') {
|
|
513
|
+
const ctrl = new DriveCylinder(drive, this.bus, driveCfg);
|
|
514
|
+
this.controllers.push(ctrl);
|
|
515
|
+
} else if (driveCfg.type === 'simple') {
|
|
516
|
+
const ctrl = new DriveSimple(drive, this.bus, driveCfg);
|
|
517
|
+
this.controllers.push(ctrl);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Tick all drives. Call this in the Three.js animation loop.
|
|
525
|
+
* @param {number} dt - delta time in seconds
|
|
526
|
+
*/
|
|
527
|
+
tick(dt) {
|
|
528
|
+
for (const drive of this.drives) {
|
|
529
|
+
drive.update(dt);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Called from Three.js requestAnimationFrame - auto-computes dt */
|
|
534
|
+
tickFromRAF(timestamp) {
|
|
535
|
+
if (this._lastTime === null) { this._lastTime = timestamp; return; }
|
|
536
|
+
const dt = Math.min((timestamp - this._lastTime) / 1000, 0.1); // cap at 100ms
|
|
537
|
+
this._lastTime = timestamp;
|
|
538
|
+
this.tick(dt);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Dispose all subscriptions and clear drives */
|
|
542
|
+
dispose() {
|
|
543
|
+
for (const ctrl of this.controllers) ctrl.dispose();
|
|
544
|
+
this.controllers = [];
|
|
545
|
+
this.drives = [];
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** Full dispose including signal bus */
|
|
549
|
+
destroy() {
|
|
550
|
+
this.dispose();
|
|
551
|
+
this.bus.dispose();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Get drive by asset id and drive index */
|
|
555
|
+
getDrive(assetId, driveIndex = 0) {
|
|
556
|
+
let count = 0;
|
|
557
|
+
for (const drive of this.drives) {
|
|
558
|
+
if (drive.node.userData.assetData?.id === assetId) {
|
|
559
|
+
if (count === driveIndex) return drive;
|
|
560
|
+
count++;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
}
|