@volter/editor-model-play 0.5.197 → 0.5.199
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 +64 -58
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.199",
|
|
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.199"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"react": "^19.0.0",
|
package/src/play-script.ts
CHANGED
|
@@ -160,70 +160,64 @@ 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
170
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
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.
|
|
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).
|
|
175
173
|
*
|
|
176
174
|
* play.setAction('Hero', moving ? 'Run' : 'Idle');
|
|
177
|
-
* play.setAction('Hero', 'Wave', { layer: 'upper', from: 'spine_01' });
|
|
178
175
|
*/
|
|
179
176
|
setAction(object: THREE.Object3D | string, action: string | null, options?: ModelPlayActionOptions): boolean;
|
|
180
177
|
/**
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
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).
|
|
185
184
|
*
|
|
186
|
-
*
|
|
187
|
-
* play.
|
|
185
|
+
* play.setTrack('Hero', 'Aim', { action: 'Aim_Upper' });
|
|
186
|
+
* play.setTrack('Hero', 'AimUp', { action: 'Aim_Up_Upper', influence: Math.max(0, pitch) });
|
|
188
187
|
*/
|
|
189
|
-
|
|
188
|
+
setTrack(object: THREE.Object3D | string, track: string, options: ModelPlayTrackOptions | null): boolean;
|
|
190
189
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
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).
|
|
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.
|
|
198
194
|
*
|
|
199
|
-
* play.
|
|
195
|
+
* play.setConstraint('Hero', 'Head', 'Look', { influence: alert ? 1 : 0 });
|
|
200
196
|
*/
|
|
201
|
-
|
|
197
|
+
setConstraint(object: THREE.Object3D | string, bone: string, constraint: string, options: ModelPlayConstraintOptions): boolean;
|
|
202
198
|
/** The actions an object's armature can play, by name (empty without an armature). */
|
|
203
199
|
actions(object: THREE.Object3D | string): readonly string[];
|
|
204
200
|
}
|
|
205
201
|
|
|
206
202
|
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;
|
|
211
203
|
readonly loop?: boolean;
|
|
212
204
|
readonly fade?: number;
|
|
213
205
|
readonly speed?: number;
|
|
214
|
-
/** Start again from the first frame when this
|
|
206
|
+
/** Start again from the first frame when this action is already playing. */
|
|
215
207
|
readonly restart?: boolean;
|
|
216
208
|
}
|
|
217
209
|
|
|
218
|
-
export interface
|
|
219
|
-
|
|
220
|
-
readonly
|
|
221
|
-
readonly
|
|
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;
|
|
222
215
|
}
|
|
223
216
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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;
|
|
227
221
|
}
|
|
228
222
|
|
|
229
223
|
/** What the bot is handed before each `update` it drives. */
|
|
@@ -445,6 +439,8 @@ export function runPlayScript(options: {
|
|
|
445
439
|
if (!options_.animation) return { ok: unknown('this document lends no animation') };
|
|
446
440
|
return { ok: true as const, target, animation: options_.animation, unknown };
|
|
447
441
|
};
|
|
442
|
+
/** What each NLA track was last set to, so the log says a change once. */
|
|
443
|
+
const tracks = new Map<string, string>();
|
|
448
444
|
const contextFor = (alive: Script): ModelPlayContext => ({
|
|
449
445
|
root,
|
|
450
446
|
find(name) {
|
|
@@ -474,36 +470,46 @@ export function runPlayScript(options: {
|
|
|
474
470
|
const found = animated(alive, object, action, 'setAction');
|
|
475
471
|
if (!found.ok) return false;
|
|
476
472
|
const { target, animation } = found;
|
|
477
|
-
const
|
|
478
|
-
const before = animation.playing(target, layer);
|
|
473
|
+
const before = animation.playing(target);
|
|
479
474
|
if (action === null) {
|
|
480
|
-
animation.stop(target, options?.fade
|
|
481
|
-
if (before !== null) run.append('play', 'action', { object: target.name,
|
|
475
|
+
animation.stop(target, options?.fade);
|
|
476
|
+
if (before !== null) run.append('play', 'action', { object: target.name, action: null, from: before });
|
|
482
477
|
return true;
|
|
483
478
|
}
|
|
484
479
|
const answer = animation.play(target, action, options);
|
|
485
480
|
if (!answer.ok) return found.unknown(answer.why);
|
|
486
|
-
if (before !== action || options?.restart) run.append('play', 'action', { armature: answer.armature,
|
|
481
|
+
if (before !== action || options?.restart) run.append('play', 'action', { armature: answer.armature, action, from: before });
|
|
487
482
|
return true;
|
|
488
483
|
},
|
|
489
|
-
|
|
490
|
-
const found = animated(alive, object,
|
|
484
|
+
setTrack(object, track, options) {
|
|
485
|
+
const found = animated(alive, object, track, 'setTrack');
|
|
491
486
|
if (!found.ok) return false;
|
|
492
|
-
const
|
|
493
|
-
const layer = options?.layer ?? 'base';
|
|
494
|
-
const before = animation.playing(target, layer);
|
|
495
|
-
const answer = animation.blend(target, weights, options);
|
|
487
|
+
const answer = found.animation.track(found.target, track, options);
|
|
496
488
|
if (!answer.ok) return found.unknown(answer.why);
|
|
497
|
-
//
|
|
498
|
-
const
|
|
499
|
-
|
|
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
|
+
}
|
|
500
496
|
return true;
|
|
501
497
|
},
|
|
502
|
-
|
|
503
|
-
const found = animated(alive, object, bone
|
|
498
|
+
setConstraint(object, bone, constraint, options) {
|
|
499
|
+
const found = animated(alive, object, `${bone}/${constraint}`, 'setConstraint');
|
|
504
500
|
if (!found.ok) return false;
|
|
505
|
-
|
|
506
|
-
|
|
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;
|
|
507
513
|
},
|
|
508
514
|
actions(object) {
|
|
509
515
|
const target = typeof object === 'string' ? root.getObjectByName(object) ?? null : object;
|