@volter/editor-model-play 0.5.196 → 0.5.198
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/package.json +2 -2
- package/src/play-script.ts +99 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/editor-model-play",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.198",
|
|
4
4
|
"author": "Volter AI, Inc.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
]
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@volter/editor-sdk": "0.5.
|
|
31
|
+
"@volter/editor-sdk": "0.5.198"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"react": "^19.0.0",
|
package/src/play-script.ts
CHANGED
|
@@ -160,17 +160,41 @@ export interface ModelPlayContext {
|
|
|
160
160
|
autoplay(bot: ModelPlayAutoplayController | Readonly<Record<string, ModelPlayAutoplayController>> | null): void;
|
|
161
161
|
/**
|
|
162
162
|
* Set the action an object's armature plays, as Blender's `animation_data.action` does: any
|
|
163
|
-
* action in the file
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* `
|
|
169
|
-
*
|
|
163
|
+
* action in the file, by name. `object` is the armature, its skinned mesh, or an object above or
|
|
164
|
+
* below them, or its name. Each armature starts a run playing the action the file assigns it.
|
|
165
|
+
* Setting another crossfades over `fade` seconds (0.2), repeats unless `loop: false` (which
|
|
166
|
+
* holds the last frame), and plays at `speed`; setting the action already playing does nothing,
|
|
167
|
+
* so a script may set it every update from the character's state. `null` fades it out. Each
|
|
168
|
+
* change is an `action` entry in the play log; a name the file lacks is `action-unknown` once
|
|
169
|
+
* and returns false.
|
|
170
|
+
*
|
|
171
|
+
* The armature plays as Blender plays it: its NLA tracks, then this action over them, then its
|
|
172
|
+
* bone constraints, all as the file sets them (and as the Timeline shows them).
|
|
170
173
|
*
|
|
171
174
|
* play.setAction('Hero', moving ? 'Run' : 'Idle');
|
|
172
175
|
*/
|
|
173
176
|
setAction(object: THREE.Object3D | string, action: string | null, options?: ModelPlayActionOptions): boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Set one of the armature's NLA tracks, by name: the `action` it plays from now (a track the
|
|
179
|
+
* file lacks is made, over the file's), its `influence` (0 to 1, reached over `fade`), or
|
|
180
|
+
* `mute`. `null` gives the track back to the file. As in Blender, an action changes only the
|
|
181
|
+
* bones it keys: an action keyed on the upper body, on a track over a running action, is an
|
|
182
|
+
* upper body that aims while the legs run. Influences set every update blend poses between
|
|
183
|
+
* authored ones (aim up, level and down by the aim's angle).
|
|
184
|
+
*
|
|
185
|
+
* play.setTrack('Hero', 'Aim', { action: 'Aim_Upper' });
|
|
186
|
+
* play.setTrack('Hero', 'AimUp', { action: 'Aim_Up_Upper', influence: Math.max(0, pitch) });
|
|
187
|
+
*/
|
|
188
|
+
setTrack(object: THREE.Object3D | string, track: string, options: ModelPlayTrackOptions | null): boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Set one bone constraint the file gives the armature, by bone and constraint name: its
|
|
191
|
+
* `influence`, or the `target` object it aims at. A constraint aims at the game's copy of its
|
|
192
|
+
* file target, so moving that object in the game moves the aim with no call at all. A game
|
|
193
|
+
* plays Damped Track; another type is refused with the reason.
|
|
194
|
+
*
|
|
195
|
+
* play.setConstraint('Hero', 'Head', 'Look', { influence: alert ? 1 : 0 });
|
|
196
|
+
*/
|
|
197
|
+
setConstraint(object: THREE.Object3D | string, bone: string, constraint: string, options: ModelPlayConstraintOptions): boolean;
|
|
174
198
|
/** The actions an object's armature can play, by name (empty without an armature). */
|
|
175
199
|
actions(object: THREE.Object3D | string): readonly string[];
|
|
176
200
|
}
|
|
@@ -179,10 +203,23 @@ export interface ModelPlayActionOptions {
|
|
|
179
203
|
readonly loop?: boolean;
|
|
180
204
|
readonly fade?: number;
|
|
181
205
|
readonly speed?: number;
|
|
182
|
-
/** Start again from the first frame when this
|
|
206
|
+
/** Start again from the first frame when this action is already playing. */
|
|
183
207
|
readonly restart?: boolean;
|
|
184
208
|
}
|
|
185
209
|
|
|
210
|
+
export interface ModelPlayTrackOptions extends ModelPlayActionOptions {
|
|
211
|
+
/** The action the track plays from now; absent, what the file gives it. */
|
|
212
|
+
readonly action?: string;
|
|
213
|
+
readonly influence?: number;
|
|
214
|
+
readonly mute?: boolean;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface ModelPlayConstraintOptions {
|
|
218
|
+
readonly influence?: number;
|
|
219
|
+
/** The object it aims at (or its name) instead of the file's target. */
|
|
220
|
+
readonly target?: THREE.Object3D | string | null;
|
|
221
|
+
}
|
|
222
|
+
|
|
186
223
|
/** What the bot is handed before each `update` it drives. */
|
|
187
224
|
export interface ModelPlayAutoplayInput {
|
|
188
225
|
/** The behaviour driving (`play autoplay on <behaviour>`), for a controller shared by several. */
|
|
@@ -387,6 +424,23 @@ export function runPlayScript(options: {
|
|
|
387
424
|
options.container.style.opacity = '0';
|
|
388
425
|
/** One script's context: its log, tint, opacity and bot do nothing once that script is gone
|
|
389
426
|
* (replaced, failed, or the run stopped), so a stale timer cannot reach a later one. */
|
|
427
|
+
/** The object an animation call names, and the document's animation; `action-unknown` once
|
|
428
|
+
* per object and name when either is missing. */
|
|
429
|
+
const animated = (alive: Script, object: THREE.Object3D | string, what: string | null, call: string) => {
|
|
430
|
+
const name = typeof object === 'string' ? object : object.name;
|
|
431
|
+
const unknown = (why: string): false => {
|
|
432
|
+
const key = `${name}\u0000${call}\u0000${what}`;
|
|
433
|
+
if (!alive.unknown.has(key)) { alive.unknown.add(key); run.append('play', 'action-unknown', { object: name, call, action: what, why }); }
|
|
434
|
+
return false;
|
|
435
|
+
};
|
|
436
|
+
if (!alive.value) return { ok: false as const };
|
|
437
|
+
const target = typeof object === 'string' ? root.getObjectByName(object) ?? null : object;
|
|
438
|
+
if (!target) return { ok: unknown('the model has no such object') };
|
|
439
|
+
if (!options_.animation) return { ok: unknown('this document lends no animation') };
|
|
440
|
+
return { ok: true as const, target, animation: options_.animation, unknown };
|
|
441
|
+
};
|
|
442
|
+
/** What each NLA track was last set to, so the log says a change once. */
|
|
443
|
+
const tracks = new Map<string, string>();
|
|
390
444
|
const contextFor = (alive: Script): ModelPlayContext => ({
|
|
391
445
|
root,
|
|
392
446
|
find(name) {
|
|
@@ -413,27 +467,50 @@ export function runPlayScript(options: {
|
|
|
413
467
|
if (alive.value) alive.bot = behaviors;
|
|
414
468
|
},
|
|
415
469
|
setAction(object, action, options) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const target
|
|
419
|
-
const
|
|
420
|
-
const key = `${name}\u0000${action}`;
|
|
421
|
-
if (!alive.unknown.has(key)) { alive.unknown.add(key); run.append('play', 'action-unknown', { object: name, action, why }); }
|
|
422
|
-
return false;
|
|
423
|
-
};
|
|
424
|
-
if (!target) return unknown('the model has no such object');
|
|
425
|
-
if (!options_.animation) return unknown('this document lends no animation');
|
|
426
|
-
const before = options_.animation.playing(target);
|
|
470
|
+
const found = animated(alive, object, action, 'setAction');
|
|
471
|
+
if (!found.ok) return false;
|
|
472
|
+
const { target, animation } = found;
|
|
473
|
+
const before = animation.playing(target);
|
|
427
474
|
if (action === null) {
|
|
428
|
-
|
|
475
|
+
animation.stop(target, options?.fade);
|
|
429
476
|
if (before !== null) run.append('play', 'action', { object: target.name, action: null, from: before });
|
|
430
477
|
return true;
|
|
431
478
|
}
|
|
432
|
-
const answer =
|
|
433
|
-
if (!answer.ok) return unknown(answer.why);
|
|
479
|
+
const answer = animation.play(target, action, options);
|
|
480
|
+
if (!answer.ok) return found.unknown(answer.why);
|
|
434
481
|
if (before !== action || options?.restart) run.append('play', 'action', { armature: answer.armature, action, from: before });
|
|
435
482
|
return true;
|
|
436
483
|
},
|
|
484
|
+
setTrack(object, track, options) {
|
|
485
|
+
const found = animated(alive, object, track, 'setTrack');
|
|
486
|
+
if (!found.ok) return false;
|
|
487
|
+
const answer = found.animation.track(found.target, track, options);
|
|
488
|
+
if (!answer.ok) return found.unknown(answer.why);
|
|
489
|
+
// Influences change every update; the log keeps what the track plays and whether it is muted.
|
|
490
|
+
const key = `${answer.armature}${track}`;
|
|
491
|
+
const now = options === null ? 'file' : JSON.stringify([options.action ?? null, options.mute ?? false]);
|
|
492
|
+
if (tracks.get(key) !== now) {
|
|
493
|
+
tracks.set(key, now);
|
|
494
|
+
run.append('play', 'track', { armature: answer.armature, track, ...(options === null ? { file: true } : { action: options.action ?? null, mute: options.mute ?? false }) });
|
|
495
|
+
}
|
|
496
|
+
return true;
|
|
497
|
+
},
|
|
498
|
+
setConstraint(object, bone, constraint, options) {
|
|
499
|
+
const found = animated(alive, object, `${bone}/${constraint}`, 'setConstraint');
|
|
500
|
+
if (!found.ok) return false;
|
|
501
|
+
let aim: THREE.Object3D | null | undefined = undefined;
|
|
502
|
+
if (typeof options.target === 'string') {
|
|
503
|
+
aim = root.getObjectByName(options.target) ?? null;
|
|
504
|
+
if (!aim) return found.unknown(`the model has no object "${options.target}" to aim at`);
|
|
505
|
+
} else aim = options.target;
|
|
506
|
+
const answer = found.animation.constraint(found.target, bone, constraint, {
|
|
507
|
+
...(options.influence === undefined ? {} : { influence: options.influence }),
|
|
508
|
+
...(aim === undefined ? {} : { target: aim }),
|
|
509
|
+
});
|
|
510
|
+
if (!answer.ok) return found.unknown(answer.why);
|
|
511
|
+
if (aim !== undefined) run.append('play', 'constraint', { armature: answer.armature, bone, constraint, target: aim?.name ?? null });
|
|
512
|
+
return true;
|
|
513
|
+
},
|
|
437
514
|
actions(object) {
|
|
438
515
|
const target = typeof object === 'string' ? root.getObjectByName(object) ?? null : object;
|
|
439
516
|
return target && options_.animation ? options_.animation.clips(target) : [];
|