@volter/blender-engine 0.1.132 → 0.1.133
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/browser/protocol.ts
CHANGED
|
@@ -78,7 +78,10 @@ export type WorkerRequest =
|
|
|
78
78
|
* bone, the LOCAL transform at every integer frame of the action's range,
|
|
79
79
|
* plus the scene's frame range/fps and the summary row's key columns.
|
|
80
80
|
* READ ONLY: keying, moving a key and setting a range are edits. */
|
|
81
|
-
| { id: number; op: 'action-clip'; object?: string; bake?: boolean }
|
|
81
|
+
| { id: number; op: 'action-clip'; object?: string; bake?: boolean; action?: string }
|
|
82
|
+
/** EVERY ARMATURE AND THE ACTIONS THAT CAN PLAY ON IT (`session.py`'s `rna_armature_actions`):
|
|
83
|
+
* each action that animates one of its pose bones, assigned or not. READ ONLY. */
|
|
84
|
+
| { id: number; op: 'armature-actions' }
|
|
82
85
|
/** ONE RESTRICTION COLUMN, written (`session.py`'s `outliner_set`). A column
|
|
83
86
|
* Blender draws on no row of that type is refused by name. */
|
|
84
87
|
| { id: number; op: 'outliner-set'; path: string; column: string; value: boolean }
|
package/browser/runtime.ts
CHANGED
|
@@ -423,15 +423,22 @@ export class BlenderRuntime {
|
|
|
423
423
|
* false` answers the header and the summary row's key columns without the
|
|
424
424
|
* sampled tracks, which is what a Timeline that only needs to DRAW asks
|
|
425
425
|
* for. */
|
|
426
|
-
async actionClip(options?: { object?: string; bake?: boolean }): Promise<BlenderActionClip> {
|
|
426
|
+
async actionClip(options?: { object?: string; bake?: boolean; action?: string }): Promise<BlenderActionClip> {
|
|
427
427
|
await this.#ready();
|
|
428
428
|
return (await this.#request({
|
|
429
429
|
op: 'action-clip',
|
|
430
430
|
...(options?.object === undefined ? {} : { object: options.object }),
|
|
431
431
|
...(options?.bake === undefined ? {} : { bake: options.bake }),
|
|
432
|
+
...(options?.action === undefined ? {} : { action: options.action }),
|
|
432
433
|
})) as BlenderActionClip;
|
|
433
434
|
}
|
|
434
435
|
|
|
436
|
+
/** Every armature and the actions that can play on it, assigned or not (a game's clip library). */
|
|
437
|
+
async armatureActions(): Promise<{ armatures: { armature: string; current: string | null; actions: string[]; keys?: string }[]; scene?: { frameCurrent: number; frameStart: number; frameEnd: number; fps: number } }> {
|
|
438
|
+
await this.#ready();
|
|
439
|
+
return (await this.#request({ op: 'armature-actions' })) as { armatures: { armature: string; current: string | null; actions: string[]; keys?: string }[]; scene?: { frameCurrent: number; frameStart: number; frameEnd: number; fps: number } };
|
|
440
|
+
}
|
|
441
|
+
|
|
435
442
|
/** Write ONE restriction column (the eye, the render camera, a collection's
|
|
436
443
|
* Exclude). A column Blender draws on no row of that type is refused by
|
|
437
444
|
* name rather than written somewhere near it. */
|
package/browser/session.py
CHANGED
|
@@ -4841,7 +4841,51 @@ def _summary_objects():
|
|
|
4841
4841
|
return out
|
|
4842
4842
|
|
|
4843
4843
|
|
|
4844
|
-
def
|
|
4844
|
+
def rna_armature_actions():
|
|
4845
|
+
"""EVERY ARMATURE AND THE ACTIONS THAT CAN PLAY ON IT -- what a game picks its clips from.
|
|
4846
|
+
|
|
4847
|
+
An action can play on an armature when it animates at least one of that armature's pose bones
|
|
4848
|
+
(`pose.bones["<name>"]...` on a bone the armature has). Read off each action's F-Curves through
|
|
4849
|
+
`_action_channelbag`, every channelbag, so an action assigned to nothing (a clip library: Idle,
|
|
4850
|
+
Run, Attack beside each other in the file) is offered as well as the one each armature plays now.
|
|
4851
|
+
READ ONLY."""
|
|
4852
|
+
out = []
|
|
4853
|
+
for arm in bpy.data.objects:
|
|
4854
|
+
if arm.type != "ARMATURE" or arm.pose is None:
|
|
4855
|
+
continue
|
|
4856
|
+
bones = {pchan.name for pchan in arm.pose.bones}
|
|
4857
|
+
current = arm.animation_data.action.name if (arm.animation_data and arm.animation_data.action) else None
|
|
4858
|
+
names = []
|
|
4859
|
+
keys, total = 0, 0.0
|
|
4860
|
+
for action in bpy.data.actions:
|
|
4861
|
+
curves = []
|
|
4862
|
+
legacy = getattr(action, "fcurves", None)
|
|
4863
|
+
if legacy is not None:
|
|
4864
|
+
curves = list(legacy)
|
|
4865
|
+
else:
|
|
4866
|
+
for layer in getattr(action, "layers", ()):
|
|
4867
|
+
for strip in getattr(layer, "strips", ()):
|
|
4868
|
+
for bag in (getattr(strip, "channelbags", None) or ()):
|
|
4869
|
+
curves.extend(bag.fcurves)
|
|
4870
|
+
if any((m := _BONE_PATH.match(fc.data_path)) is not None and m.group(1) in bones for fc in curves):
|
|
4871
|
+
names.append(action.name)
|
|
4872
|
+
# A CHEAP SIGNATURE of the keys, so a reader can tell a key edit from no change
|
|
4873
|
+
# without baking: the count and the sum of every key's frame and value.
|
|
4874
|
+
for fc in curves:
|
|
4875
|
+
for key in fc.keyframe_points:
|
|
4876
|
+
keys += 1
|
|
4877
|
+
total += float(key.co[0]) * 7.0 + float(key.co[1])
|
|
4878
|
+
out.append({"armature": arm.name, "current": current, "actions": sorted(names),
|
|
4879
|
+
"keys": "%d:%.6f" % (keys, total)})
|
|
4880
|
+
scene = bpy.context.scene
|
|
4881
|
+
# THE SCENE'S CLOCK TOO: a reader that skips a re-bind on an unchanged listing must still see
|
|
4882
|
+
# the playhead and range move when an agent sets them.
|
|
4883
|
+
return {"armatures": out, "scene": {"frameCurrent": int(scene.frame_current),
|
|
4884
|
+
"frameStart": int(scene.frame_start), "frameEnd": int(scene.frame_end),
|
|
4885
|
+
"fps": float(scene.render.fps) / float(scene.render.fps_base or 1.0)}}
|
|
4886
|
+
|
|
4887
|
+
|
|
4888
|
+
def rna_action_clip(object_name=None, bake=True, action_name=None):
|
|
4845
4889
|
"""ONE ACTION AS A THREE.JS CLIP: per bone, the LOCAL transform it has at
|
|
4846
4890
|
every integer frame of the action's own range.
|
|
4847
4891
|
|
|
@@ -4910,13 +4954,32 @@ def rna_action_clip(object_name=None, bake=True):
|
|
|
4910
4954
|
# when the subject below turns out to be nothing at all.
|
|
4911
4955
|
"summary": _summary_objects(),
|
|
4912
4956
|
}
|
|
4913
|
-
|
|
4957
|
+
adt = arm_obj.animation_data if arm_obj is not None else None
|
|
4958
|
+
assigned = adt.action if adt is not None else None
|
|
4959
|
+
if arm_obj is None or (assigned is None and not action_name):
|
|
4914
4960
|
header["reason"] = ("Nothing here carries an action: the Timeline draws the scene range "
|
|
4915
4961
|
"and an empty summary row, which is Blender's own empty state.")
|
|
4916
4962
|
return header
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4963
|
+
action = assigned
|
|
4964
|
+
slot = getattr(adt, "action_slot", None) if adt is not None else None
|
|
4965
|
+
if action_name and (assigned is None or action_name != assigned.name):
|
|
4966
|
+
# A NAMED ACTION, assigned or not (a game's clip library: Idle, Run, Attack side by side):
|
|
4967
|
+
# baked against this armature WITHOUT assigning it, so the file is untouched. Its curves
|
|
4968
|
+
# are its first channelbag's (no slot of this object chooses among them).
|
|
4969
|
+
action = bpy.data.actions.get(action_name)
|
|
4970
|
+
if action is None:
|
|
4971
|
+
raise ValueError("The engine holds no action named %r" % (action_name,))
|
|
4972
|
+
# THE SLOT FOR THIS ARMATURE, as Blender would pick it on assignment: the one whose
|
|
4973
|
+
# identifier names this object (`OB<name>`), else the only object slot. A layered action
|
|
4974
|
+
# with several object slots and none for this armature has no right answer; its first
|
|
4975
|
+
# object slot is used and said in the reason.
|
|
4976
|
+
slots = [one for one in getattr(action, "slots", ()) if getattr(one, "target_id_type", "OBJECT") == "OBJECT"]
|
|
4977
|
+
slot = next((one for one in slots if getattr(one, "identifier", "") == "OB" + arm_obj.name), None)
|
|
4978
|
+
if slot is None and slots:
|
|
4979
|
+
slot = slots[0]
|
|
4980
|
+
if len(slots) > 1:
|
|
4981
|
+
header["reason"] = ("%r has %d object slots and none named for %r; the first, %r, is played."
|
|
4982
|
+
% (action.name, len(slots), arm_obj.name, getattr(slot, "identifier", "?")))
|
|
4920
4983
|
curves, shape = _action_channelbag(action, getattr(slot, "handle", None))
|
|
4921
4984
|
header.update({
|
|
4922
4985
|
"action": action.name,
|
|
@@ -5451,7 +5514,9 @@ def _dispatch(request):
|
|
|
5451
5514
|
if op == "rig":
|
|
5452
5515
|
return rna_rig(request.get("object"))
|
|
5453
5516
|
if op == "action-clip":
|
|
5454
|
-
return rna_action_clip(request.get("object"), request.get("bake", True))
|
|
5517
|
+
return rna_action_clip(request.get("object"), request.get("bake", True), request.get("action"))
|
|
5518
|
+
if op == "armature-actions":
|
|
5519
|
+
return rna_armature_actions()
|
|
5455
5520
|
if op == "outliner":
|
|
5456
5521
|
return rna_outliner(request.get("selected"))
|
|
5457
5522
|
if op == "outliner-set":
|
|
@@ -277,7 +277,7 @@ export function graphAttributeName(name: string): string {
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
export function geometryFromDrawArrays(
|
|
280
|
-
arrays: Omit<DrawArrays, 'hash' | 'sourceVertex'> & { sourceVertex?: Uint32Array },
|
|
280
|
+
arrays: Omit<DrawArrays, 'hash' | 'sourceVertex'> & { sourceVertex?: Uint32Array | undefined },
|
|
281
281
|
): THREE.BufferGeometry {
|
|
282
282
|
const geometry = new THREE.BufferGeometry();
|
|
283
283
|
geometry.setAttribute('position', new THREE.BufferAttribute(arrays.positions, 3));
|
|
@@ -147,6 +147,8 @@ const drawArraysSchema = z
|
|
|
147
147
|
})
|
|
148
148
|
.strict(),
|
|
149
149
|
),
|
|
150
|
+
/** The draw's vertex map (`DrawArrays.sourceVertex`), kept by a copy that plays (skins bind over it). */
|
|
151
|
+
sourceVertex: z.instanceof(Uint32Array).optional(),
|
|
150
152
|
hash: z.string(),
|
|
151
153
|
})
|
|
152
154
|
.strict();
|
|
@@ -2248,7 +2250,7 @@ export class BlenderRuntimeView {
|
|
|
2248
2250
|
/** Everything this view holds, as ONE self-contained frame another view can be built from:
|
|
2249
2251
|
* every mesh's resident geometry and every resident image, with no reference to what a
|
|
2250
2252
|
* receiver already holds. */
|
|
2251
|
-
private fullFrame(): Frame {
|
|
2253
|
+
private fullFrame(options: { sourceVertices?: boolean } = {}): Frame {
|
|
2252
2254
|
const source = this.frame;
|
|
2253
2255
|
if (!source) throw new Error('Blender capture requires a presented frame');
|
|
2254
2256
|
// Inspection overlays are not render content. In particular weight-paint
|
|
@@ -2290,6 +2292,12 @@ export class BlenderRuntimeView {
|
|
|
2290
2292
|
count: group.count,
|
|
2291
2293
|
materialIndex: group.materialIndex ?? 0,
|
|
2292
2294
|
})),
|
|
2295
|
+
// A COPY THAT PLAYS keeps the draw's vertex map: Play binds skins over it (a Blender
|
|
2296
|
+
// vertex's weights expand onto its drawn vertices through it), where a render snapshot
|
|
2297
|
+
// needs none.
|
|
2298
|
+
...(options.sourceVertices && geometry.getAttribute('blenderVertex')
|
|
2299
|
+
? { sourceVertex: new Uint32Array((geometry.getAttribute('blenderVertex') as THREE.BufferAttribute).array) }
|
|
2300
|
+
: {}),
|
|
2293
2301
|
};
|
|
2294
2302
|
}
|
|
2295
2303
|
for (const [id, { signature }] of this.volumes)
|
|
@@ -2368,7 +2376,7 @@ export class BlenderRuntimeView {
|
|
|
2368
2376
|
*/
|
|
2369
2377
|
detach(): { readonly view: BlenderRuntimeView; dispose(): void } {
|
|
2370
2378
|
const detached = new BlenderRuntimeView();
|
|
2371
|
-
if (this.frame) detached.applyFrame(this.fullFrame());
|
|
2379
|
+
if (this.frame) detached.applyFrame(this.fullFrame({ sourceVertices: true }));
|
|
2372
2380
|
let disposed = false;
|
|
2373
2381
|
return {
|
|
2374
2382
|
view: detached,
|
package/browser/worker.ts
CHANGED
|
@@ -806,7 +806,10 @@ async function handle(request: WorkerRequest): Promise<unknown> {
|
|
|
806
806
|
op: 'action-clip',
|
|
807
807
|
...(request.object === undefined ? {} : { object: request.object }),
|
|
808
808
|
...(request.bake === undefined ? {} : { bake: request.bake }),
|
|
809
|
+
...(request.action === undefined ? {} : { action: request.action }),
|
|
809
810
|
});
|
|
811
|
+
case 'armature-actions':
|
|
812
|
+
return ask({ op: 'armature-actions' });
|
|
810
813
|
case 'outliner-set':
|
|
811
814
|
return ask({
|
|
812
815
|
op: 'outliner-set',
|