@volter/editor-model-play 0.5.196 → 0.5.197
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 +87 -16
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.197",
|
|
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.197"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"react": "^19.0.0",
|
package/src/play-script.ts
CHANGED
|
@@ -168,14 +168,46 @@ export interface ModelPlayContext {
|
|
|
168
168
|
* `null` fades it out. Actions run on the game's clock. Each change is an `action` entry in the
|
|
169
169
|
* play log; a name the armature lacks is `action-unknown` once and returns false.
|
|
170
170
|
*
|
|
171
|
+
* LAYERS: an action plays on the `base` layer, every bone, unless `layer` names another. A named
|
|
172
|
+
* layer plays from one bone down (`from`, given the first time) and takes those bones from the
|
|
173
|
+
* base, so the legs can run while the upper body does something else. Blender has no layers at
|
|
174
|
+
* run time; its NLA tracks are the nearest thing, and they are not read here.
|
|
175
|
+
*
|
|
171
176
|
* play.setAction('Hero', moving ? 'Run' : 'Idle');
|
|
177
|
+
* play.setAction('Hero', 'Wave', { layer: 'upper', from: 'spine_01' });
|
|
172
178
|
*/
|
|
173
179
|
setAction(object: THREE.Object3D | string, action: string | null, options?: ModelPlayActionOptions): boolean;
|
|
180
|
+
/**
|
|
181
|
+
* Play several actions on one layer at once, at weights (0 to 1), blended: poses between authored
|
|
182
|
+
* ones, such as aiming up, level and down by the aim's angle. An action left out fades to nothing.
|
|
183
|
+
* Weights take effect at once unless `fade` is given, so a script may set them every update.
|
|
184
|
+
* Returns false, with `action-unknown` in the log, as `setAction` does.
|
|
185
|
+
*
|
|
186
|
+
* const up = Math.max(0, pitch), down = Math.max(0, -pitch);
|
|
187
|
+
* play.blend('Hero', { Aim_Up: up, Aim_Level: 1 - up - down, Aim_Down: down }, { layer: 'upper', from: 'spine_01' });
|
|
188
|
+
*/
|
|
189
|
+
blend(object: THREE.Object3D | string, weights: Readonly<Record<string, number>>, options?: ModelPlayActionOptions): boolean;
|
|
190
|
+
/**
|
|
191
|
+
* Turn one bone of an object's armature toward a world point or an object, every update after
|
|
192
|
+
* its actions, until set to `null`. It is the game's own: Blender's constraints and IK are not run
|
|
193
|
+
* in a game (an action holds what they made); this aims at what exists only while the game runs.
|
|
194
|
+
* What points at it is the bone's forward: the way it faced when the character faced its
|
|
195
|
+
* armature's -Y (Blender's front) in the file, so a head looks with its face. `axis` names one
|
|
196
|
+
* of the bone's own axes instead (`y` runs along a Blender bone). `weight` is how far (1),
|
|
197
|
+
* `limit` the most it turns from the animated pose (75 degrees).
|
|
198
|
+
*
|
|
199
|
+
* play.lookAt('Hero', 'head', player);
|
|
200
|
+
*/
|
|
201
|
+
lookAt(object: THREE.Object3D | string, bone: string, target: THREE.Object3D | THREE.Vector3 | null, options?: ModelPlayLookAtOptions): boolean;
|
|
174
202
|
/** The actions an object's armature can play, by name (empty without an armature). */
|
|
175
203
|
actions(object: THREE.Object3D | string): readonly string[];
|
|
176
204
|
}
|
|
177
205
|
|
|
178
206
|
export interface ModelPlayActionOptions {
|
|
207
|
+
/** The layer: `base` (every bone, the default) or a name of the script's choosing. */
|
|
208
|
+
readonly layer?: string;
|
|
209
|
+
/** A named layer's first bone: it plays on that bone and every bone below it. */
|
|
210
|
+
readonly from?: string;
|
|
179
211
|
readonly loop?: boolean;
|
|
180
212
|
readonly fade?: number;
|
|
181
213
|
readonly speed?: number;
|
|
@@ -183,6 +215,17 @@ export interface ModelPlayActionOptions {
|
|
|
183
215
|
readonly restart?: boolean;
|
|
184
216
|
}
|
|
185
217
|
|
|
218
|
+
export interface ModelPlayLookAtOptions {
|
|
219
|
+
readonly weight?: number;
|
|
220
|
+
readonly axis?: 'forward' | 'x' | 'y' | 'z' | '-x' | '-y' | '-z';
|
|
221
|
+
readonly limit?: number;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Weights as a log shows them: two decimals. */
|
|
225
|
+
function rounded(weights: Readonly<Record<string, number>>): Record<string, number> {
|
|
226
|
+
return Object.fromEntries(Object.entries(weights).map(([name, weight]) => [name, Math.round(weight * 100) / 100]));
|
|
227
|
+
}
|
|
228
|
+
|
|
186
229
|
/** What the bot is handed before each `update` it drives. */
|
|
187
230
|
export interface ModelPlayAutoplayInput {
|
|
188
231
|
/** The behaviour driving (`play autoplay on <behaviour>`), for a controller shared by several. */
|
|
@@ -387,6 +430,21 @@ export function runPlayScript(options: {
|
|
|
387
430
|
options.container.style.opacity = '0';
|
|
388
431
|
/** One script's context: its log, tint, opacity and bot do nothing once that script is gone
|
|
389
432
|
* (replaced, failed, or the run stopped), so a stale timer cannot reach a later one. */
|
|
433
|
+
/** The object an animation call names, and the document's animation; `action-unknown` once
|
|
434
|
+
* per object and name when either is missing. */
|
|
435
|
+
const animated = (alive: Script, object: THREE.Object3D | string, what: string | null, call: string) => {
|
|
436
|
+
const name = typeof object === 'string' ? object : object.name;
|
|
437
|
+
const unknown = (why: string): false => {
|
|
438
|
+
const key = `${name}\u0000${call}\u0000${what}`;
|
|
439
|
+
if (!alive.unknown.has(key)) { alive.unknown.add(key); run.append('play', 'action-unknown', { object: name, call, action: what, why }); }
|
|
440
|
+
return false;
|
|
441
|
+
};
|
|
442
|
+
if (!alive.value) return { ok: false as const };
|
|
443
|
+
const target = typeof object === 'string' ? root.getObjectByName(object) ?? null : object;
|
|
444
|
+
if (!target) return { ok: unknown('the model has no such object') };
|
|
445
|
+
if (!options_.animation) return { ok: unknown('this document lends no animation') };
|
|
446
|
+
return { ok: true as const, target, animation: options_.animation, unknown };
|
|
447
|
+
};
|
|
390
448
|
const contextFor = (alive: Script): ModelPlayContext => ({
|
|
391
449
|
root,
|
|
392
450
|
find(name) {
|
|
@@ -413,27 +471,40 @@ export function runPlayScript(options: {
|
|
|
413
471
|
if (alive.value) alive.bot = behaviors;
|
|
414
472
|
},
|
|
415
473
|
setAction(object, action, options) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const target
|
|
419
|
-
const
|
|
420
|
-
|
|
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);
|
|
474
|
+
const found = animated(alive, object, action, 'setAction');
|
|
475
|
+
if (!found.ok) return false;
|
|
476
|
+
const { target, animation } = found;
|
|
477
|
+
const layer = options?.layer ?? 'base';
|
|
478
|
+
const before = animation.playing(target, layer);
|
|
427
479
|
if (action === null) {
|
|
428
|
-
|
|
429
|
-
if (before !== null) run.append('play', 'action', { object: target.name, action: null, from: before });
|
|
480
|
+
animation.stop(target, options?.fade, layer);
|
|
481
|
+
if (before !== null) run.append('play', 'action', { object: target.name, layer, action: null, from: before });
|
|
430
482
|
return true;
|
|
431
483
|
}
|
|
432
|
-
const answer =
|
|
433
|
-
if (!answer.ok) return unknown(answer.why);
|
|
434
|
-
if (before !== action || options?.restart) run.append('play', 'action', { armature: answer.armature, action, from: before });
|
|
484
|
+
const answer = animation.play(target, action, options);
|
|
485
|
+
if (!answer.ok) return found.unknown(answer.why);
|
|
486
|
+
if (before !== action || options?.restart) run.append('play', 'action', { armature: answer.armature, layer, action, from: before });
|
|
487
|
+
return true;
|
|
488
|
+
},
|
|
489
|
+
blend(object, weights, options) {
|
|
490
|
+
const found = animated(alive, object, Object.keys(weights).join('+'), 'blend');
|
|
491
|
+
if (!found.ok) return false;
|
|
492
|
+
const { target, animation } = found;
|
|
493
|
+
const layer = options?.layer ?? 'base';
|
|
494
|
+
const before = animation.playing(target, layer);
|
|
495
|
+
const answer = animation.blend(target, weights, options);
|
|
496
|
+
if (!answer.ok) return found.unknown(answer.why);
|
|
497
|
+
// Weights change every update; the log keeps only a change of the action that leads.
|
|
498
|
+
const now = animation.playing(target, layer);
|
|
499
|
+
if (before !== now) run.append('play', 'action', { armature: answer.armature, layer, action: now, from: before, blend: rounded(weights) });
|
|
435
500
|
return true;
|
|
436
501
|
},
|
|
502
|
+
lookAt(object, bone, at, options) {
|
|
503
|
+
const found = animated(alive, object, bone, 'lookAt');
|
|
504
|
+
if (!found.ok) return false;
|
|
505
|
+
const answer = found.animation.lookAt(found.target, bone, at, options);
|
|
506
|
+
return answer.ok || found.unknown(answer.why);
|
|
507
|
+
},
|
|
437
508
|
actions(object) {
|
|
438
509
|
const target = typeof object === 'string' ? root.getObjectByName(object) ?? null : object;
|
|
439
510
|
return target && options_.animation ? options_.animation.clips(target) : [];
|