@volter/blender-engine 0.1.134 → 0.1.135
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/rna.ts
CHANGED
|
@@ -528,22 +528,29 @@ export interface BlenderUvLayout {
|
|
|
528
528
|
}
|
|
529
529
|
|
|
530
530
|
/**
|
|
531
|
-
* ONE
|
|
532
|
-
* `
|
|
533
|
-
*
|
|
534
|
-
* `constant` says the door found every sample equal to the first and shipped
|
|
535
|
-
* two keys instead of N — which is most of a rig, most of the time.
|
|
531
|
+
* ONE F-CURVE of {@link BlenderActionClip}, whole: one component (`index`) of one of a bone's
|
|
532
|
+
* channels (`location`, `rotation_quaternion` as w,x,y,z, `rotation_euler`, `rotation_axis_angle`
|
|
533
|
+
* as angle,x,y,z, `scale`), with everything `fcurve_eval_keyframes` reads to evaluate it.
|
|
536
534
|
*/
|
|
537
|
-
export interface BlenderClipTrack {
|
|
535
|
+
export interface BlenderClipTrack extends BlenderClipCurve {
|
|
538
536
|
readonly bone: string;
|
|
539
|
-
readonly property: '
|
|
540
|
-
readonly
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
/**
|
|
546
|
-
readonly
|
|
537
|
+
readonly property: 'location' | 'rotation_quaternion' | 'rotation_euler' | 'rotation_axis_angle' | 'scale';
|
|
538
|
+
readonly index: number;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** An F-Curve's body: everything Blender evaluates it from, wherever it lives. */
|
|
542
|
+
export interface BlenderClipCurve {
|
|
543
|
+
/** `FCurve.extrapolation`: `CONSTANT` or `LINEAR`. */
|
|
544
|
+
readonly extrapolation: string;
|
|
545
|
+
/** Per key, how the segment after it interpolates: 0 constant, 1 linear, 2 Bezier. */
|
|
546
|
+
readonly interpolation: readonly number[];
|
|
547
|
+
/** Float32, six per key: `co`, `handle_left`, `handle_right` (frame, value each). */
|
|
548
|
+
readonly keysBase64: string;
|
|
549
|
+
/** A Cycles modifier: [mode before, cycles before, mode after, cycles after]; mode 0 none,
|
|
550
|
+
* 1 repeat, 2 repeat with offset, 3 mirror. */
|
|
551
|
+
readonly cycles?: readonly [number, number, number, number];
|
|
552
|
+
/** What this curve does in Blender that is not played (an easing type, another modifier). */
|
|
553
|
+
readonly unsupported?: readonly string[];
|
|
547
554
|
}
|
|
548
555
|
|
|
549
556
|
/** One column of the Timeline's summary row: a frame at which SOMETHING is
|
|
@@ -616,9 +623,18 @@ export interface BlenderActionClip {
|
|
|
616
623
|
readonly tracks: readonly BlenderClipTrack[];
|
|
617
624
|
readonly clipStart?: number;
|
|
618
625
|
readonly clipEnd?: number;
|
|
626
|
+
/** The range Blender places the action over NLA strips in: its keys, null on a side a Cycles
|
|
627
|
+
* modifier repeats into. */
|
|
628
|
+
readonly keysStart?: number | null;
|
|
629
|
+
readonly keysEnd?: number | null;
|
|
630
|
+
/** Blender repeats it past its range (Cycles modifiers on its curves, or `use_cyclic`). */
|
|
631
|
+
readonly cyclic?: boolean;
|
|
619
632
|
readonly duration?: number;
|
|
620
633
|
readonly sampled?: number;
|
|
621
634
|
/** Bones the action names that the armature does not have. */
|
|
622
635
|
readonly unplayedBones?: readonly string[];
|
|
636
|
+
/** What the action keys besides bone channels (the object's own transform, a property),
|
|
637
|
+
* which plays only in Blender. */
|
|
638
|
+
readonly unsupported?: readonly string[];
|
|
623
639
|
readonly reason?: string;
|
|
624
640
|
}
|
package/browser/session.py
CHANGED
|
@@ -1332,10 +1332,116 @@ def _armature_bones(obj, locked_groups):
|
|
|
1332
1332
|
# (`overlay_armature.cc:2059-2084`); it shades both the solid and
|
|
1333
1333
|
# the wire toward `bone_locked_weight`.
|
|
1334
1334
|
"lockedWeight": bone.name in locked_groups,
|
|
1335
|
+
# WHAT THE PRESENTER EVALUATES THE POSE FROM, the same inputs `BKE_pose_where_is` takes:
|
|
1336
|
+
# the rest matrix (`Bone.matrix_local`, armature space), the channels' own values (what
|
|
1337
|
+
# a channel no action animates keeps), and the bone's constraints.
|
|
1338
|
+
"rest": _matrix_rows(bone.matrix_local),
|
|
1339
|
+
"channels": _bone_channels(pchan),
|
|
1340
|
+
**_bone_inherit(bone),
|
|
1341
|
+
**_bone_constraints(pchan),
|
|
1335
1342
|
})
|
|
1336
1343
|
return bones
|
|
1337
1344
|
|
|
1338
1345
|
|
|
1346
|
+
def _bone_channels(pchan):
|
|
1347
|
+
"""A pose bone's channel VALUES as Blender stores them (`PoseBone.location`,
|
|
1348
|
+
`rotation_*`, `scale`), every rotation property and the mode that chooses among them, so the
|
|
1349
|
+
presenter blends them exactly as the NLA does, per channel, before building the matrix."""
|
|
1350
|
+
return {
|
|
1351
|
+
"mode": pchan.rotation_mode,
|
|
1352
|
+
"location": [float(v) for v in pchan.location],
|
|
1353
|
+
"rotation_quaternion": [float(v) for v in pchan.rotation_quaternion],
|
|
1354
|
+
"rotation_euler": [float(v) for v in pchan.rotation_euler],
|
|
1355
|
+
"rotation_axis_angle": [float(v) for v in pchan.rotation_axis_angle],
|
|
1356
|
+
"scale": [float(v) for v in pchan.scale],
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
|
|
1360
|
+
def _bone_inherit(bone):
|
|
1361
|
+
"""The parenting flags, only when one is not Blender's default (the presenter composes the
|
|
1362
|
+
default chain `parent @ rest @ basis`, and names a bone that asks for another)."""
|
|
1363
|
+
flags = []
|
|
1364
|
+
if not bone.use_inherit_rotation:
|
|
1365
|
+
flags.append("use_inherit_rotation=False")
|
|
1366
|
+
if bone.inherit_scale != "FULL":
|
|
1367
|
+
flags.append("inherit_scale=%s" % bone.inherit_scale)
|
|
1368
|
+
if not bone.use_local_location:
|
|
1369
|
+
flags.append("use_local_location=False")
|
|
1370
|
+
if getattr(bone, "use_relative_parent", False):
|
|
1371
|
+
flags.append("use_relative_parent=True")
|
|
1372
|
+
return {"inherit": flags} if flags else {}
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
def _bone_constraints(pchan):
|
|
1376
|
+
"""A pose bone's constraint stack, in order. Every constraint is named with its type; the ones
|
|
1377
|
+
the presenter evaluates (Damped Track) carry what it needs."""
|
|
1378
|
+
stack = []
|
|
1379
|
+
for con in pchan.constraints:
|
|
1380
|
+
entry = {"name": con.name, "type": con.type, "enabled": bool(con.enabled),
|
|
1381
|
+
"influence": float(con.influence)}
|
|
1382
|
+
target = getattr(con, "target", None)
|
|
1383
|
+
if target is not None:
|
|
1384
|
+
entry["target"] = target.name
|
|
1385
|
+
subtarget = getattr(con, "subtarget", "")
|
|
1386
|
+
if subtarget:
|
|
1387
|
+
entry["subtarget"] = subtarget
|
|
1388
|
+
if con.type == "DAMPED_TRACK":
|
|
1389
|
+
entry["trackAxis"] = con.track_axis
|
|
1390
|
+
entry["headTail"] = float(con.head_tail)
|
|
1391
|
+
stack.append(entry)
|
|
1392
|
+
return {"constraints": stack} if stack else {}
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
def _strip_influence_curve(strip):
|
|
1396
|
+
"""A strip's keyed influence (`NlaStrip.fcurves`, data path `influence`), whole, so the
|
|
1397
|
+
presenter evaluates it at every frame as `nlastrip_evaluate_controls` does."""
|
|
1398
|
+
if not strip.use_animated_influence:
|
|
1399
|
+
return {}
|
|
1400
|
+
for fcurve in strip.fcurves:
|
|
1401
|
+
if fcurve.data_path == "influence":
|
|
1402
|
+
return {"influenceCurve": _curve_body(fcurve)}
|
|
1403
|
+
return {}
|
|
1404
|
+
|
|
1405
|
+
|
|
1406
|
+
def _armature_animation(obj):
|
|
1407
|
+
"""THE ARMATURE'S ANIMATION STACK as Blender evaluates it (`BKE_animsys_evaluate_animdata`):
|
|
1408
|
+
its NLA tracks bottom to top, then the active action over them, each with its influence and
|
|
1409
|
+
blend type. Strip timing is Blender's own (`nlastrip_get_frame`), so it is sent whole."""
|
|
1410
|
+
adt = obj.animation_data
|
|
1411
|
+
if adt is None:
|
|
1412
|
+
return {}
|
|
1413
|
+
tracks = []
|
|
1414
|
+
for track in adt.nla_tracks:
|
|
1415
|
+
strips = []
|
|
1416
|
+
for strip in track.strips:
|
|
1417
|
+
strips.append({
|
|
1418
|
+
"name": strip.name, "type": strip.type,
|
|
1419
|
+
"action": strip.action.name if strip.action is not None else None,
|
|
1420
|
+
"start": float(strip.frame_start), "end": float(strip.frame_end),
|
|
1421
|
+
"actionStart": float(strip.action_frame_start), "actionEnd": float(strip.action_frame_end),
|
|
1422
|
+
"scale": float(strip.scale), "repeat": float(strip.repeat),
|
|
1423
|
+
"influence": float(strip.influence), "animatedInfluence": bool(strip.use_animated_influence),
|
|
1424
|
+
**_strip_influence_curve(strip),
|
|
1425
|
+
"animatedTime": bool(strip.use_animated_time),
|
|
1426
|
+
"blendIn": float(strip.blend_in), "blendOut": float(strip.blend_out),
|
|
1427
|
+
"blendType": strip.blend_type, "extrapolation": strip.extrapolation,
|
|
1428
|
+
"reversed": bool(strip.use_reverse), "mute": bool(strip.mute),
|
|
1429
|
+
})
|
|
1430
|
+
tracks.append({"name": track.name, "mute": bool(track.mute), "solo": bool(track.is_solo),
|
|
1431
|
+
"strips": strips})
|
|
1432
|
+
drivers = sorted({match.group(1) for match in (_BONE_PATH.match(fc.data_path) for fc in adt.drivers)
|
|
1433
|
+
if match is not None})
|
|
1434
|
+
return {"animation": {
|
|
1435
|
+
"useNla": bool(adt.use_nla),
|
|
1436
|
+
"influence": float(adt.action_influence),
|
|
1437
|
+
"blendType": adt.action_blend_type,
|
|
1438
|
+
"extrapolation": adt.action_extrapolation,
|
|
1439
|
+
"tweak": bool(adt.use_tweak_mode),
|
|
1440
|
+
"tracks": tracks,
|
|
1441
|
+
**({"drivers": drivers} if drivers else {}),
|
|
1442
|
+
}}
|
|
1443
|
+
|
|
1444
|
+
|
|
1339
1445
|
def _locked_weight_groups(view_layer):
|
|
1340
1446
|
"""The vertex groups `BONE_DRAW_LOCKED_WEIGHT` is read from.
|
|
1341
1447
|
|
|
@@ -1378,6 +1484,7 @@ def _armatures(view_layer):
|
|
|
1378
1484
|
"action": (obj.animation_data.action.name
|
|
1379
1485
|
if obj.animation_data is not None and obj.animation_data.action is not None
|
|
1380
1486
|
else None),
|
|
1487
|
+
**_armature_animation(obj),
|
|
1381
1488
|
}
|
|
1382
1489
|
return armatures
|
|
1383
1490
|
|
|
@@ -1651,6 +1758,9 @@ class Session:
|
|
|
1651
1758
|
# THE SCENE'S CLOCK beside the frame number: what a Timeline draws and a mixer maps time by.
|
|
1652
1759
|
frame["clock"] = {"start": int(scene.frame_start), "end": int(scene.frame_end),
|
|
1653
1760
|
"fps": float(scene.render.fps) / float(scene.render.fps_base or 1.0)}
|
|
1761
|
+
# ACTIONS NOTHING KEEPS: no user and no fake user, so the next save drops them (Blender's own
|
|
1762
|
+
# rule). A game that plays one is told before it vanishes.
|
|
1763
|
+
frame["unkeptActions"] = sorted(a.name for a in bpy.data.actions if a.users == 0 and not a.use_fake_user)
|
|
1654
1764
|
frame["weights"] = _weights(scene, view_layer, frame, self._known)
|
|
1655
1765
|
warnings = list(frame.get("warnings", ()))
|
|
1656
1766
|
warnings.extend(_WARNINGS)
|
|
@@ -3853,6 +3963,13 @@ def rna_context(object_name=None, collection_path=None):
|
|
|
3853
3963
|
# list longer than _OUTLINER_PAGE answers its first page and says how many more
|
|
3854
3964
|
# there are, so a scene of 20,000 objects cannot turn one read into a
|
|
3855
3965
|
# 20,000-row payload.
|
|
3966
|
+
#
|
|
3967
|
+
# AND THE READ COSTS WHAT IT SHOWS, not what the scene holds: an object's row
|
|
3968
|
+
# and its expansion are built only once the page has kept its place
|
|
3969
|
+
# (`rna_outliner`). The ids do not move for it. An id's ordinal is the number of
|
|
3970
|
+
# rows that claimed the same address before it in the WHOLE tree's order, kept
|
|
3971
|
+
# rows or not, and `_OutlinerScope` answers that count for a kept object's rows
|
|
3972
|
+
# from what every earlier object would have claimed.
|
|
3856
3973
|
|
|
3857
3974
|
_OUTLINER_PAGE = 64
|
|
3858
3975
|
|
|
@@ -4191,6 +4308,60 @@ def _outliner_object(obj, view_layer, seen, chosen, active):
|
|
|
4191
4308
|
return row
|
|
4192
4309
|
|
|
4193
4310
|
|
|
4311
|
+
def _outliner_preorder(row, out):
|
|
4312
|
+
"""A built row and everything under it, IN THE ORDER THEY CLAIMED THEIR IDS:
|
|
4313
|
+
every builder above makes a row before its children, and its children in
|
|
4314
|
+
list order, so the claim order is the tree's pre-order."""
|
|
4315
|
+
out.append(row)
|
|
4316
|
+
for child in row["children"]:
|
|
4317
|
+
_outliner_preorder(child, out)
|
|
4318
|
+
return out
|
|
4319
|
+
|
|
4320
|
+
|
|
4321
|
+
class _OutlinerScope:
|
|
4322
|
+
"""THE COUNTS ONE KEPT OBJECT'S ROWS CLAIM AGAINST, in `_outliner_unique`'s
|
|
4323
|
+
own `seen` shape (`get`, item assignment).
|
|
4324
|
+
|
|
4325
|
+
Every address an object's expansion claims is one of three kinds, and two
|
|
4326
|
+
of them never need the rows that came before to be built:
|
|
4327
|
+
|
|
4328
|
+
- under the OBJECT's own address (`.animation_data`, `.pose`, its pose
|
|
4329
|
+
bones, `.constraints`, `.modifiers`, `.particle_systems`,
|
|
4330
|
+
`.vertex_groups`): only that object's expansions claim it, so the
|
|
4331
|
+
count before is how many times the object was expanded before;
|
|
4332
|
+
- under its DATA's or the data's shape keys' address (the data row,
|
|
4333
|
+
key blocks, bones, bone collections): only expansions of that data
|
|
4334
|
+
claim it -- bones only outside Pose Mode -- so the count before is
|
|
4335
|
+
how many times the data was expanded before;
|
|
4336
|
+
- anything else (the object row itself, materials, a modifier's linked
|
|
4337
|
+
object or node group, an instanced collection) is shared with any
|
|
4338
|
+
row in the tree, and its count is the tree's own, in `seen`."""
|
|
4339
|
+
|
|
4340
|
+
def __init__(self, seen, scopes):
|
|
4341
|
+
self.seen = seen
|
|
4342
|
+
# (prefix, exact address, count before), most specific first.
|
|
4343
|
+
self.scopes = scopes
|
|
4344
|
+
self.local = {}
|
|
4345
|
+
|
|
4346
|
+
def _before(self, path):
|
|
4347
|
+
if path is not None:
|
|
4348
|
+
for prefix, address, before in self.scopes:
|
|
4349
|
+
if path == address or path.startswith(prefix):
|
|
4350
|
+
return before
|
|
4351
|
+
return None
|
|
4352
|
+
|
|
4353
|
+
def get(self, path, default=0):
|
|
4354
|
+
if path in self.local:
|
|
4355
|
+
return self.local[path]
|
|
4356
|
+
before = self._before(path)
|
|
4357
|
+
return self.seen.get(path, default) if before is None else before
|
|
4358
|
+
|
|
4359
|
+
def __setitem__(self, path, count):
|
|
4360
|
+
self.local[path] = count
|
|
4361
|
+
if self._before(path) is None:
|
|
4362
|
+
self.seen[path] = count
|
|
4363
|
+
|
|
4364
|
+
|
|
4194
4365
|
# ---- the node editor's tree ------------------------------------------------
|
|
4195
4366
|
#
|
|
4196
4367
|
# THE ONE READ A NODE VIEW NEEDS. The generic `rna_view` door answers every one
|
|
@@ -4897,8 +5068,12 @@ def rna_action_clip(object_name=None, bake=True, action_name=None):
|
|
|
4897
5068
|
adt = arm_obj.animation_data if arm_obj is not None else None
|
|
4898
5069
|
assigned = adt.action if adt is not None else None
|
|
4899
5070
|
if arm_obj is None or (assigned is None and not action_name):
|
|
4900
|
-
|
|
4901
|
-
|
|
5071
|
+
# NLA STRIPS PLAY WITHOUT AN ACTIVE ACTION, and the Timeline's summary row draws only the
|
|
5072
|
+
# active action's keys, as Blender's does; so the empty state is said only when nothing plays.
|
|
5073
|
+
strips = adt is not None and any(len(track.strips) for track in adt.nla_tracks)
|
|
5074
|
+
if not strips:
|
|
5075
|
+
header["reason"] = ("Nothing here carries an action: the Timeline draws the scene range "
|
|
5076
|
+
"and an empty summary row, which is Blender's own empty state.")
|
|
4902
5077
|
return header
|
|
4903
5078
|
action = assigned
|
|
4904
5079
|
slot = getattr(adt, "action_slot", None) if adt is not None else None
|
|
@@ -4937,16 +5112,28 @@ def rna_action_clip(object_name=None, bake=True, action_name=None):
|
|
|
4937
5112
|
if not bake:
|
|
4938
5113
|
return header
|
|
4939
5114
|
frames = sorted(columns)
|
|
4940
|
-
|
|
4941
|
-
|
|
5115
|
+
# THE ACTION'S OWN RANGE (`Action.frame_range`: its keys, or its manual range) with its keys:
|
|
5116
|
+
# what a strip or the action line plays, sampled through `FCurve.evaluate`, which applies the
|
|
5117
|
+
# curves' extrapolation and modifiers (a Cycles modifier repeats them) as Blender does.
|
|
5118
|
+
span = getattr(action, "frame_range", None)
|
|
5119
|
+
first = int(math.floor(min(frames[0], span[0] if span is not None else frames[0])))
|
|
5120
|
+
last = int(math.ceil(max(frames[-1], span[1] if span is not None else frames[-1])))
|
|
4942
5121
|
if last <= first:
|
|
4943
5122
|
last = first + 1
|
|
4944
5123
|
header["clipStart"] = first
|
|
4945
5124
|
header["clipEnd"] = last
|
|
5125
|
+
# THE RANGE BLENDER PLACES THE ACTIVE ACTION OVER over NLA strips (`calc_action_range` with
|
|
5126
|
+
# modifiers): its keys, opened to either side a Cycles modifier repeats into (null: unbounded).
|
|
5127
|
+
# A manual frame range does not enter it. Measured: a cycling action is not held at its end.
|
|
5128
|
+
header["keysStart"], header["keysEnd"] = _action_key_range(action, first, last)
|
|
4946
5129
|
channels = {}
|
|
5130
|
+
others = set()
|
|
4947
5131
|
for fcurve in curves:
|
|
4948
5132
|
match = _BONE_PATH.match(fcurve.data_path)
|
|
4949
5133
|
if match is None:
|
|
5134
|
+
# NOT A BONE'S CHANNEL: the armature object's own transform (root motion), a custom
|
|
5135
|
+
# property, a bone channel this door does not read. Named, so it is never dropped quietly.
|
|
5136
|
+
others.add(fcurve.data_path)
|
|
4950
5137
|
continue
|
|
4951
5138
|
bone, prop = match.group(1), match.group(2)
|
|
4952
5139
|
channels.setdefault((bone, prop), {})[int(fcurve.array_index)] = fcurve
|
|
@@ -4955,61 +5142,19 @@ def rna_action_clip(object_name=None, bake=True, action_name=None):
|
|
|
4955
5142
|
"door does not play (object transform, shape keys, a material). The "
|
|
4956
5143
|
"Timeline still draws its keys." % (action.name,))
|
|
4957
5144
|
return header
|
|
4958
|
-
rest_relative = {}
|
|
4959
|
-
for pchan in _pose_bones_parents_first(arm_obj):
|
|
4960
|
-
local = pchan.bone.matrix_local
|
|
4961
|
-
parent = pchan.parent
|
|
4962
|
-
rest_relative[pchan.name] = (parent.bone.matrix_local.inverted_safe() @ local
|
|
4963
|
-
if parent is not None else local.copy())
|
|
4964
|
-
times = [(frame - first) / fps for frame in range(first, last + 1)]
|
|
4965
5145
|
tracks = []
|
|
4966
5146
|
unplayed = []
|
|
4967
|
-
for bone in sorted(
|
|
4968
|
-
|
|
4969
|
-
if pchan is None:
|
|
5147
|
+
for (bone, prop) in sorted(channels):
|
|
5148
|
+
if arm_obj.pose.bones.get(bone) is None:
|
|
4970
5149
|
unplayed.append(bone)
|
|
4971
5150
|
continue
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
for frame in range(first, last + 1):
|
|
4977
|
-
location = _sample(channels.get((bone, "location")), pchan.location, frame)
|
|
4978
|
-
scale = _sample(channels.get((bone, "scale")), pchan.scale, frame, default=1.0)
|
|
4979
|
-
if rotation_mode == "QUATERNION":
|
|
4980
|
-
quat = mathutils.Quaternion(
|
|
4981
|
-
_sample(channels.get((bone, "rotation_quaternion")),
|
|
4982
|
-
pchan.rotation_quaternion, frame, size=4, default=None,
|
|
4983
|
-
identity=(1.0, 0.0, 0.0, 0.0)))
|
|
4984
|
-
elif rotation_mode == "AXIS_ANGLE":
|
|
4985
|
-
raw = _sample(channels.get((bone, "rotation_axis_angle")),
|
|
4986
|
-
pchan.rotation_axis_angle, frame, size=4, default=None,
|
|
4987
|
-
identity=(0.0, 0.0, 1.0, 0.0))
|
|
4988
|
-
quat = mathutils.Quaternion(raw[1:4], raw[0])
|
|
4989
|
-
else:
|
|
4990
|
-
quat = mathutils.Euler(
|
|
4991
|
-
_sample(channels.get((bone, "rotation_euler")), pchan.rotation_euler, frame),
|
|
4992
|
-
rotation_mode).to_quaternion()
|
|
4993
|
-
basis = mathutils.Matrix.LocRotScale(mathutils.Vector(location), quat,
|
|
4994
|
-
mathutils.Vector(scale))
|
|
4995
|
-
position, rotation, scaling = (rest @ basis).decompose()
|
|
4996
|
-
# QUATERNION CONTINUITY, and it is not cosmetic: three's
|
|
4997
|
-
# `QuaternionLinearInterpolant` takes the shorter arc between
|
|
4998
|
-
# NEIGHBOURING samples, so one sign flip in the middle of a bake
|
|
4999
|
-
# spins the bone the long way round for a frame.
|
|
5000
|
-
if previous is not None and rotation.dot(previous) < 0.0:
|
|
5001
|
-
rotation.negate()
|
|
5002
|
-
previous = rotation.copy()
|
|
5003
|
-
positions.extend((position.x, position.y, position.z))
|
|
5004
|
-
quaternions.extend((rotation.x, rotation.y, rotation.z, rotation.w))
|
|
5005
|
-
scales.extend((scaling.x, scaling.y, scaling.z))
|
|
5006
|
-
tracks.extend(_track(bone, "position", times, positions, 3))
|
|
5007
|
-
tracks.extend(_track(bone, "quaternion", times, quaternions, 4))
|
|
5008
|
-
tracks.extend(_track(bone, "scale", times, scales, 3))
|
|
5151
|
+
for index, fcurve in sorted(channels[(bone, prop)].items()):
|
|
5152
|
+
tracks.append(_curve(bone, prop, index, fcurve))
|
|
5153
|
+
if others:
|
|
5154
|
+
header["unsupported"] = ["curves on %s" % ", ".join(sorted(others))]
|
|
5009
5155
|
header.update({
|
|
5010
5156
|
"tracks": tracks,
|
|
5011
5157
|
"duration": (last - first) / fps,
|
|
5012
|
-
"sampled": len(times),
|
|
5013
5158
|
"unplayedBones": unplayed,
|
|
5014
5159
|
})
|
|
5015
5160
|
return header
|
|
@@ -5022,52 +5167,94 @@ _KEY_TYPE_RANK = {"JITTER": 1, "GENERATED": 2, "MOVING_HOLD": 3, "BREAKDOWN": 4,
|
|
|
5022
5167
|
"KEYFRAME": 5, "EXTREME": 6}
|
|
5023
5168
|
|
|
5024
5169
|
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
is one, and the pose channel's CURRENT value where there is not -- which is
|
|
5028
|
-
what Blender does too (an unkeyed component simply keeps its value)."""
|
|
5029
|
-
fallback = list(identity) if identity is not None else [
|
|
5030
|
-
(current[i] if current is not None and i < len(current) else default) for i in range(size)]
|
|
5031
|
-
if identity is not None and current is not None and group is not None:
|
|
5032
|
-
fallback = [float(v) for v in current]
|
|
5033
|
-
if group is None:
|
|
5034
|
-
return fallback
|
|
5035
|
-
out = list(fallback)
|
|
5036
|
-
for index, fcurve in group.items():
|
|
5037
|
-
if 0 <= index < len(out):
|
|
5038
|
-
out[index] = float(fcurve.evaluate(frame))
|
|
5039
|
-
return out
|
|
5040
|
-
|
|
5170
|
+
_INTERPOLATION = {"CONSTANT": 0, "LINEAR": 1, "BEZIER": 2}
|
|
5171
|
+
_CYCLE_MODE = {"NONE": 0, "REPEAT": 1, "REPEAT_OFFSET": 2, "MIRROR": 3}
|
|
5041
5172
|
|
|
5042
|
-
def _track(bone, prop, times, values, stride):
|
|
5043
|
-
"""One three.js keyframe track, or two keys when nothing moves.
|
|
5044
|
-
|
|
5045
|
-
The arrays cross as base64 Float32 (`timeBase64`, `valueBase64`), the shape
|
|
5046
|
-
every large payload in this file takes."""
|
|
5047
|
-
count = len(times)
|
|
5048
|
-
constant = True
|
|
5049
|
-
for i in range(1, count):
|
|
5050
|
-
for c in range(stride):
|
|
5051
|
-
if abs(values[i * stride + c] - values[c]) > 1e-6:
|
|
5052
|
-
constant = False
|
|
5053
|
-
break
|
|
5054
|
-
if not constant:
|
|
5055
|
-
break
|
|
5056
|
-
if constant:
|
|
5057
|
-
if count == 0:
|
|
5058
|
-
return []
|
|
5059
|
-
times = [times[0], times[-1]]
|
|
5060
|
-
values = values[0:stride] * 2
|
|
5061
|
-
return [{
|
|
5062
|
-
"bone": bone,
|
|
5063
|
-
"property": prop,
|
|
5064
|
-
"stride": stride,
|
|
5065
|
-
"count": len(times),
|
|
5066
|
-
"constant": constant,
|
|
5067
|
-
"timeBase64": base64.b64encode(array.array("f", times).tobytes()).decode("ascii"),
|
|
5068
|
-
"valueBase64": base64.b64encode(array.array("f", values).tobytes()).decode("ascii"),
|
|
5069
|
-
}]
|
|
5070
5173
|
|
|
5174
|
+
def _action_key_range(action, first, last):
|
|
5175
|
+
"""`Action::get_frame_range_of_keys(true)` (`action.cc`, `get_frame_range_of_fcurves`): every
|
|
5176
|
+
F-Curve of the WHOLE action (every layer, strip and slot) widens the range by its keys, and only
|
|
5177
|
+
its LAST modifier counts: Limits widens it by its set X limits, Cycles opens each side its mode
|
|
5178
|
+
repeats into, any other type opens both sides. Mute is not read. None is an open side."""
|
|
5179
|
+
low, high = None, None
|
|
5180
|
+
opened_low = opened_high = False
|
|
5181
|
+
curves = list(getattr(action, "fcurves", None) or ())
|
|
5182
|
+
for layer in getattr(action, "layers", ()):
|
|
5183
|
+
for strip in getattr(layer, "strips", ()):
|
|
5184
|
+
for bag in (getattr(strip, "channelbags", None) or ()):
|
|
5185
|
+
curves.extend(bag.fcurves)
|
|
5186
|
+
# A curve's keys and its last modifier count independently: a keyless curve carrying a
|
|
5187
|
+
# Generator or Noise still opens the range, as in Blender.
|
|
5188
|
+
limits = []
|
|
5189
|
+
for fcurve in curves:
|
|
5190
|
+
points = fcurve.keyframe_points
|
|
5191
|
+
if len(points):
|
|
5192
|
+
frames = [float(key.co[0]) for key in points]
|
|
5193
|
+
low = min(frames) if low is None else min(low, min(frames))
|
|
5194
|
+
high = max(frames) if high is None else max(high, max(frames))
|
|
5195
|
+
if not len(fcurve.modifiers):
|
|
5196
|
+
continue
|
|
5197
|
+
modifier = fcurve.modifiers[-1]
|
|
5198
|
+
if modifier.type == "LIMITS":
|
|
5199
|
+
limits.append(modifier)
|
|
5200
|
+
elif modifier.type == "CYCLES":
|
|
5201
|
+
opened_low = opened_low or modifier.mode_before != "NONE"
|
|
5202
|
+
opened_high = opened_high or modifier.mode_after != "NONE"
|
|
5203
|
+
else:
|
|
5204
|
+
opened_low = opened_high = True
|
|
5205
|
+
if low is None:
|
|
5206
|
+
low, high = first, last
|
|
5207
|
+
for modifier in limits:
|
|
5208
|
+
if modifier.use_min_x:
|
|
5209
|
+
low = min(low, float(modifier.min_x))
|
|
5210
|
+
if modifier.use_max_x:
|
|
5211
|
+
high = max(high, float(modifier.max_x))
|
|
5212
|
+
return (None if opened_low else low), (None if opened_high else high)
|
|
5213
|
+
|
|
5214
|
+
|
|
5215
|
+
def _curve(bone, prop, index, fcurve):
|
|
5216
|
+
"""One bone channel's F-Curve: its address and its body (`_curve_body`)."""
|
|
5217
|
+
return {"bone": bone, "property": prop, "index": index, **_curve_body(fcurve)}
|
|
5218
|
+
|
|
5219
|
+
|
|
5220
|
+
def _curve_body(fcurve):
|
|
5221
|
+
"""ONE F-CURVE, whole: its keys and handles, how each segment interpolates, its extrapolation
|
|
5222
|
+
and its Cycles modifier, which is everything `fcurve_eval_keyframes` and `fcm_cycles_time` read
|
|
5223
|
+
to evaluate it. The presenter evaluates it as they do, at any frame, fractional or not.
|
|
5224
|
+
|
|
5225
|
+
The keys cross as base64 Float32, six per key: `co`, `handle_left`, `handle_right`
|
|
5226
|
+
(frame, value each). A segment whose interpolation is one of the easing types (Back, Bounce,
|
|
5227
|
+
Elastic, Sine, ...) and any modifier other than Cycles are named rather than played."""
|
|
5228
|
+
keys = array.array("f")
|
|
5229
|
+
interpolation = []
|
|
5230
|
+
unsupported = []
|
|
5231
|
+
for key in fcurve.keyframe_points:
|
|
5232
|
+
keys.extend((key.co[0], key.co[1], key.handle_left[0], key.handle_left[1],
|
|
5233
|
+
key.handle_right[0], key.handle_right[1]))
|
|
5234
|
+
code = _INTERPOLATION.get(key.interpolation)
|
|
5235
|
+
if code is None:
|
|
5236
|
+
unsupported.append("%s interpolation" % key.interpolation.lower())
|
|
5237
|
+
code = 1
|
|
5238
|
+
interpolation.append(code)
|
|
5239
|
+
cycles = None
|
|
5240
|
+
for modifier in fcurve.modifiers:
|
|
5241
|
+
if modifier.mute:
|
|
5242
|
+
continue
|
|
5243
|
+
if modifier.type == "CYCLES" and not modifier.use_restricted_range and not modifier.use_influence:
|
|
5244
|
+
cycles = [_CYCLE_MODE.get(modifier.mode_before, 0), int(modifier.cycles_before),
|
|
5245
|
+
_CYCLE_MODE.get(modifier.mode_after, 0), int(modifier.cycles_after)]
|
|
5246
|
+
else:
|
|
5247
|
+
unsupported.append("%s modifier" % modifier.type.lower())
|
|
5248
|
+
out = {
|
|
5249
|
+
"extrapolation": fcurve.extrapolation,
|
|
5250
|
+
"interpolation": interpolation,
|
|
5251
|
+
"keysBase64": base64.b64encode(keys.tobytes()).decode("ascii"),
|
|
5252
|
+
}
|
|
5253
|
+
if cycles is not None:
|
|
5254
|
+
out["cycles"] = cycles
|
|
5255
|
+
if unsupported:
|
|
5256
|
+
out["unsupported"] = sorted(set(unsupported))
|
|
5257
|
+
return out
|
|
5071
5258
|
|
|
5072
5259
|
def rna_outliner(selected=None):
|
|
5073
5260
|
"""BLENDER'S VIEW LAYER TREE for the session's scene.
|
|
@@ -5092,10 +5279,16 @@ def rna_outliner(selected=None):
|
|
|
5092
5279
|
engine_active = view_layer.objects.active
|
|
5093
5280
|
active = engine_active.name if engine_active is not None else None
|
|
5094
5281
|
seen = {}
|
|
5095
|
-
# Every object
|
|
5282
|
+
# Every object's PLACE, with the row it sits under and the COLLECTION row it
|
|
5096
5283
|
# belongs to -- what `ObjectsChildrenBuilder`'s map holds, and what the
|
|
5097
|
-
# parent walk below moves.
|
|
5284
|
+
# parent walk below moves. A place is not a row yet: `{"place": obj,
|
|
5285
|
+
# "name", "children"}`, where `children` are the child objects the fold
|
|
5286
|
+
# hangs under it. The page decides which places become rows.
|
|
5098
5287
|
placements = {}
|
|
5288
|
+
# Every place, in the order the whole tree's rows claim their ids: each
|
|
5289
|
+
# collection's row, its child collections, then its objects, depth first.
|
|
5290
|
+
occurrences = []
|
|
5291
|
+
collections = []
|
|
5099
5292
|
|
|
5100
5293
|
def place(obj, row, collection_row):
|
|
5101
5294
|
placements.setdefault(obj.name, []).append([row, collection_row, collection_row])
|
|
@@ -5122,10 +5315,12 @@ def rna_outliner(selected=None):
|
|
|
5122
5315
|
# Fold parenting before paging: a parent may follow its children in
|
|
5123
5316
|
# collection order, and must not disappear behind the flat cap.
|
|
5124
5317
|
for obj in collection.objects:
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5318
|
+
spot = {"place": obj, "name": obj.name, "children": []}
|
|
5319
|
+
occurrences.append(spot)
|
|
5320
|
+
place(obj, spot, row)
|
|
5321
|
+
children.append(spot)
|
|
5128
5322
|
row["children"] = children
|
|
5323
|
+
collections.append(row)
|
|
5129
5324
|
return row
|
|
5130
5325
|
|
|
5131
5326
|
scene_collection = _outliner_row(
|
|
@@ -5138,10 +5333,12 @@ def rna_outliner(selected=None):
|
|
|
5138
5333
|
child.collection.name))
|
|
5139
5334
|
for child in root.children]
|
|
5140
5335
|
for obj in root.collection.objects:
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5336
|
+
spot = {"place": obj, "name": obj.name, "children": []}
|
|
5337
|
+
occurrences.append(spot)
|
|
5338
|
+
place(obj, spot, scene_collection)
|
|
5339
|
+
children.append(spot)
|
|
5144
5340
|
scene_collection["children"] = children
|
|
5341
|
+
collections.append(scene_collection)
|
|
5145
5342
|
|
|
5146
5343
|
# `ObjectsChildrenBuilder::make_object_parent_hierarchy_collections`, parents
|
|
5147
5344
|
# before children: a child object is MOVED out of its collection's row and
|
|
@@ -5159,8 +5356,15 @@ def rna_outliner(selected=None):
|
|
|
5159
5356
|
placed.add(obj.name)
|
|
5160
5357
|
ordered.append(obj)
|
|
5161
5358
|
|
|
5162
|
-
|
|
5163
|
-
|
|
5359
|
+
# The placed object itself, not `bpy.data.objects[name]`: a lookup by name is
|
|
5360
|
+
# a walk of the whole ID list, and one per object made this loop quadratic
|
|
5361
|
+
# (43 ms of a 1,490-object read).
|
|
5362
|
+
for entries in list(placements.values()):
|
|
5363
|
+
order(entries[0][0]["place"])
|
|
5364
|
+
# A move takes the place out of its collection's list; the lists are
|
|
5365
|
+
# filtered once below rather than searched once per move (`list.remove`).
|
|
5366
|
+
moved = set()
|
|
5367
|
+
duplicates = []
|
|
5164
5368
|
for obj in ordered:
|
|
5165
5369
|
if obj.parent is None:
|
|
5166
5370
|
continue
|
|
@@ -5169,24 +5373,25 @@ def rna_outliner(selected=None):
|
|
|
5169
5373
|
if not parents or not mine:
|
|
5170
5374
|
continue
|
|
5171
5375
|
for parent_row, _parent_owner, parent_collection in parents:
|
|
5172
|
-
|
|
5376
|
+
moved_here = False
|
|
5173
5377
|
for entry in mine:
|
|
5174
5378
|
row, owner, _collection = entry
|
|
5175
5379
|
if owner is not parent_collection:
|
|
5176
5380
|
continue
|
|
5177
|
-
|
|
5381
|
+
moved.add(id(row))
|
|
5178
5382
|
parent_row["children"].append(row)
|
|
5179
5383
|
entry[1] = parent_row
|
|
5180
|
-
|
|
5384
|
+
moved_here = True
|
|
5181
5385
|
break
|
|
5182
|
-
if
|
|
5386
|
+
if moved_here:
|
|
5183
5387
|
continue
|
|
5184
|
-
duplicate =
|
|
5185
|
-
|
|
5186
|
-
struct="Object", objectType=obj.type, object=obj.name,
|
|
5187
|
-
selected=obj.name in chosen, active=obj.name == active, notInCollection=True)
|
|
5388
|
+
duplicate = {"place": obj, "name": obj.name, "children": [], "duplicate": True}
|
|
5389
|
+
duplicates.append(duplicate)
|
|
5188
5390
|
parent_row["children"].append(duplicate)
|
|
5189
5391
|
mine.append([duplicate, parent_row, parent_collection])
|
|
5392
|
+
if moved:
|
|
5393
|
+
for row in collections:
|
|
5394
|
+
row["children"] = [child for child in row["children"] if id(child) not in moved]
|
|
5190
5395
|
|
|
5191
5396
|
# THE SELECTION KEEPS ITS ROWS PAST THE PAGE. A row is how the editor
|
|
5192
5397
|
# addresses an object -- the viewport's outline and gizmo, the Properties
|
|
@@ -5196,18 +5401,20 @@ def rna_outliner(selected=None):
|
|
|
5196
5401
|
# page of the selection, so selecting everything cannot turn one read back
|
|
5197
5402
|
# into the whole tree the page exists to keep out (Canyon Comet, 1,493
|
|
5198
5403
|
# objects selected: 1.77 MB a read uncapped, 142 KB capped).
|
|
5404
|
+
# The caller's choice comes before the rest of the engine's selection: a click
|
|
5405
|
+
# onto a selection already larger than a page keeps the object just clicked.
|
|
5199
5406
|
wanted = [active] if active is not None else []
|
|
5200
|
-
wanted += [o.name for o in view_layer.objects if o.select_get(view_layer=view_layer)]
|
|
5201
5407
|
wanted += sorted(chosen)
|
|
5408
|
+
wanted += [o.name for o in view_layer.objects if o.select_get(view_layer=view_layer)]
|
|
5202
5409
|
kept = set()
|
|
5203
5410
|
for name in wanted:
|
|
5204
|
-
if len(kept)
|
|
5411
|
+
if len(kept) >= _OUTLINER_PAGE:
|
|
5205
5412
|
break
|
|
5206
5413
|
kept.add(name)
|
|
5207
5414
|
holding = set()
|
|
5208
5415
|
|
|
5209
5416
|
def hold(row):
|
|
5210
|
-
inside =
|
|
5417
|
+
inside = "place" in row and row["name"] in kept
|
|
5211
5418
|
for child in row.get("children", []):
|
|
5212
5419
|
inside = hold(child) or inside
|
|
5213
5420
|
if inside:
|
|
@@ -5216,13 +5423,36 @@ def rna_outliner(selected=None):
|
|
|
5216
5423
|
|
|
5217
5424
|
hold(scene_collection)
|
|
5218
5425
|
|
|
5426
|
+
# A PLACE THE PAGE KEEPS BECOMES ITS ROW: the object's own row and the
|
|
5427
|
+
# expansion `TreeElementIDObject::expand` hangs under it, then the child
|
|
5428
|
+
# objects the fold put there, in that order. Its ids are claimed below, in
|
|
5429
|
+
# the whole tree's order; `claims` is the expansion's own claim order.
|
|
5430
|
+
scratch = {}
|
|
5431
|
+
|
|
5432
|
+
def kept_row(spot):
|
|
5433
|
+
if "place" not in spot:
|
|
5434
|
+
return spot
|
|
5435
|
+
obj = spot["place"]
|
|
5436
|
+
if spot.get("duplicate"):
|
|
5437
|
+
row = _outliner_row(
|
|
5438
|
+
_rna_address(obj), obj.name, "TSE_SOME_ID", _outliner_object_icon(obj), scratch,
|
|
5439
|
+
struct="Object", objectType=obj.type, object=obj.name,
|
|
5440
|
+
selected=obj.name in chosen, active=obj.name == active, notInCollection=True)
|
|
5441
|
+
spot["claims"] = [row]
|
|
5442
|
+
else:
|
|
5443
|
+
row = _outliner_object(obj, view_layer, scratch, chosen, active)
|
|
5444
|
+
spot["claims"] = _outliner_preorder(row, [])
|
|
5445
|
+
row["children"] = row["children"] + spot["children"]
|
|
5446
|
+
spot["row"] = row
|
|
5447
|
+
return row
|
|
5448
|
+
|
|
5219
5449
|
# Cap the hierarchy's visible child lists, not the collection's flat input.
|
|
5220
5450
|
# Every parent participates in folding even when its child list needs a page.
|
|
5221
5451
|
def page_tree(row):
|
|
5222
5452
|
members = row.get("children", [])
|
|
5223
5453
|
children, more = _outliner_page(members)
|
|
5224
5454
|
beyond = [child for child in members[len(children):] if id(child) in holding]
|
|
5225
|
-
children = children + beyond
|
|
5455
|
+
children = [kept_row(child) for child in children + beyond]
|
|
5226
5456
|
row["children"] = children
|
|
5227
5457
|
if more - len(beyond):
|
|
5228
5458
|
row["more"] = more - len(beyond)
|
|
@@ -5231,6 +5461,90 @@ def rna_outliner(selected=None):
|
|
|
5231
5461
|
|
|
5232
5462
|
page_tree(scene_collection)
|
|
5233
5463
|
|
|
5464
|
+
# THE IDS, CLAIMED IN THE WHOLE TREE'S ORDER as if every object had been
|
|
5465
|
+
# built: a kept object's rows claim through `_OutlinerScope`; an object the
|
|
5466
|
+
# page left behind adds only what it would have claimed that a later row
|
|
5467
|
+
# can share -- its own address, its materials, its modifiers' linked
|
|
5468
|
+
# datablocks, its instanced collection -- and one more expansion of itself
|
|
5469
|
+
# and of its data. Data with no address of its own claims its rows the
|
|
5470
|
+
# long way, every one in the shared count.
|
|
5471
|
+
addresses = {}
|
|
5472
|
+
|
|
5473
|
+
def address(datablock):
|
|
5474
|
+
key = datablock.as_pointer()
|
|
5475
|
+
if key not in addresses:
|
|
5476
|
+
addresses[key] = _rna_address(datablock)
|
|
5477
|
+
return addresses[key]
|
|
5478
|
+
|
|
5479
|
+
def bump(path):
|
|
5480
|
+
seen[path] = seen.get(path, 0) + 1
|
|
5481
|
+
|
|
5482
|
+
expanded = {}
|
|
5483
|
+
data_expanded = {}
|
|
5484
|
+
bones_expanded = {}
|
|
5485
|
+
for spot in occurrences:
|
|
5486
|
+
obj = spot["place"]
|
|
5487
|
+
object_key = obj.as_pointer()
|
|
5488
|
+
object_path = address(obj)
|
|
5489
|
+
data = obj.data
|
|
5490
|
+
exact = object_path is None
|
|
5491
|
+
scopes = [] if exact else [(object_path + ".", None, expanded.get(object_key, 0))]
|
|
5492
|
+
data_key = bones = None
|
|
5493
|
+
if data is not None:
|
|
5494
|
+
data_key = data.as_pointer()
|
|
5495
|
+
data_path = address(data)
|
|
5496
|
+
keys = getattr(data, "shape_keys", None)
|
|
5497
|
+
keys_path = address(keys) if keys is not None else None
|
|
5498
|
+
exact = exact or data_path is None or (keys is not None and keys_path is None)
|
|
5499
|
+
bones = isinstance(data, bpy.types.Armature) and obj.mode != "POSE"
|
|
5500
|
+
if not exact:
|
|
5501
|
+
before = data_expanded.get(data_key, 0)
|
|
5502
|
+
scopes += [(data_path + ".bones[", None, bones_expanded.get(data_key, 0)),
|
|
5503
|
+
(data_path + ".", data_path, before)]
|
|
5504
|
+
if keys_path is not None:
|
|
5505
|
+
scopes.append((keys_path + ".", keys_path, before))
|
|
5506
|
+
if exact:
|
|
5507
|
+
scopes = []
|
|
5508
|
+
row = spot.get("row")
|
|
5509
|
+
if row is not None or exact:
|
|
5510
|
+
# Built rows, or the long way for data the scopes cannot name.
|
|
5511
|
+
scope = _OutlinerScope(seen, scopes)
|
|
5512
|
+
if row is None:
|
|
5513
|
+
_outliner_object(obj, view_layer, scope, chosen, active)
|
|
5514
|
+
else:
|
|
5515
|
+
for claimed in spot["claims"]:
|
|
5516
|
+
claimed["id"] = _outliner_unique(claimed["path"], scope)
|
|
5517
|
+
else:
|
|
5518
|
+
bump(object_path)
|
|
5519
|
+
if data is not None:
|
|
5520
|
+
for material in getattr(data, "materials", ()) or ():
|
|
5521
|
+
if material is not None:
|
|
5522
|
+
bump(address(material))
|
|
5523
|
+
for slot in obj.material_slots:
|
|
5524
|
+
if slot.material is not None:
|
|
5525
|
+
bump(address(slot.material))
|
|
5526
|
+
for md in obj.modifiers:
|
|
5527
|
+
for member in ("object", "node_group"):
|
|
5528
|
+
linked = getattr(md, member, None)
|
|
5529
|
+
if linked is None:
|
|
5530
|
+
continue
|
|
5531
|
+
linked_path = address(linked)
|
|
5532
|
+
if linked_path:
|
|
5533
|
+
bump(linked_path)
|
|
5534
|
+
break
|
|
5535
|
+
if obj.instance_collection is not None and obj.instance_type == "COLLECTION":
|
|
5536
|
+
bump(address(obj.instance_collection))
|
|
5537
|
+
expanded[object_key] = expanded.get(object_key, 0) + 1
|
|
5538
|
+
if data_key is not None:
|
|
5539
|
+
data_expanded[data_key] = data_expanded.get(data_key, 0) + 1
|
|
5540
|
+
if bones:
|
|
5541
|
+
bones_expanded[data_key] = bones_expanded.get(data_key, 0) + 1
|
|
5542
|
+
# The fold's duplicates claim after every collection's objects, as they are made.
|
|
5543
|
+
for spot in duplicates:
|
|
5544
|
+
claimed = _outliner_unique(address(spot["place"]), seen)
|
|
5545
|
+
if "row" in spot:
|
|
5546
|
+
spot["row"]["id"] = claimed
|
|
5547
|
+
|
|
5234
5548
|
return {
|
|
5235
5549
|
"scene": scene_path,
|
|
5236
5550
|
"viewLayer": view_layer_path,
|
|
@@ -33,6 +33,7 @@ import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js
|
|
|
33
33
|
import { z } from 'zod';
|
|
34
34
|
|
|
35
35
|
const scalar = z.number().finite();
|
|
36
|
+
const matrixSchema = z.array(z.tuple([scalar, scalar, scalar, scalar])).length(4);
|
|
36
37
|
|
|
37
38
|
/** ONE BONE, as `session.py`'s `_armature_bones` answers it. */
|
|
38
39
|
export const boneSchema = z
|
|
@@ -49,9 +50,76 @@ export const boneSchema = z
|
|
|
49
50
|
select: z.boolean(),
|
|
50
51
|
active: z.boolean(),
|
|
51
52
|
lockedWeight: z.boolean(),
|
|
53
|
+
/** `Bone.matrix_local`, the rest matrix in the armature's space. */
|
|
54
|
+
rest: matrixSchema.optional(),
|
|
55
|
+
/** The channels' own values: what a channel no action animates keeps. */
|
|
56
|
+
channels: z.object({
|
|
57
|
+
mode: z.string(),
|
|
58
|
+
location: z.array(scalar).length(3),
|
|
59
|
+
rotation_quaternion: z.array(scalar).length(4),
|
|
60
|
+
rotation_euler: z.array(scalar).length(3),
|
|
61
|
+
rotation_axis_angle: z.array(scalar).length(4),
|
|
62
|
+
scale: z.array(scalar).length(3),
|
|
63
|
+
}).strict().optional(),
|
|
64
|
+
/** Parenting flags that are not Blender's default, by name. */
|
|
65
|
+
inherit: z.array(z.string()).optional(),
|
|
66
|
+
/** The bone's constraint stack, in order. */
|
|
67
|
+
constraints: z.array(z.object({
|
|
68
|
+
name: z.string(),
|
|
69
|
+
type: z.string(),
|
|
70
|
+
enabled: z.boolean(),
|
|
71
|
+
influence: scalar,
|
|
72
|
+
target: z.string().optional(),
|
|
73
|
+
subtarget: z.string().optional(),
|
|
74
|
+
trackAxis: z.string().optional(),
|
|
75
|
+
headTail: scalar.optional(),
|
|
76
|
+
}).strict()).optional(),
|
|
52
77
|
})
|
|
53
78
|
.strict();
|
|
54
79
|
|
|
80
|
+
/** One NLA strip, as `session.py`'s `_armature_animation` answers it. */
|
|
81
|
+
export const nlaStripSchema = z.object({
|
|
82
|
+
name: z.string(),
|
|
83
|
+
type: z.string(),
|
|
84
|
+
action: z.string().nullable(),
|
|
85
|
+
start: scalar,
|
|
86
|
+
end: scalar,
|
|
87
|
+
actionStart: scalar,
|
|
88
|
+
actionEnd: scalar,
|
|
89
|
+
scale: scalar,
|
|
90
|
+
repeat: scalar,
|
|
91
|
+
influence: scalar,
|
|
92
|
+
animatedInfluence: z.boolean(),
|
|
93
|
+
/** The keyed influence's F-Curve, when `animatedInfluence`. */
|
|
94
|
+
influenceCurve: z.object({
|
|
95
|
+
extrapolation: z.string(),
|
|
96
|
+
interpolation: z.array(z.number()),
|
|
97
|
+
keysBase64: z.string(),
|
|
98
|
+
cycles: z.tuple([z.number(), z.number(), z.number(), z.number()]).optional(),
|
|
99
|
+
unsupported: z.array(z.string()).optional(),
|
|
100
|
+
}).strict().optional(),
|
|
101
|
+
animatedTime: z.boolean(),
|
|
102
|
+
blendIn: scalar,
|
|
103
|
+
blendOut: scalar,
|
|
104
|
+
blendType: z.string(),
|
|
105
|
+
extrapolation: z.string(),
|
|
106
|
+
reversed: z.boolean(),
|
|
107
|
+
mute: z.boolean(),
|
|
108
|
+
}).strict();
|
|
109
|
+
|
|
110
|
+
/** An armature's animation stack: NLA tracks bottom to top, then the active action over them. */
|
|
111
|
+
export const armatureAnimationSchema = z.object({
|
|
112
|
+
useNla: z.boolean(),
|
|
113
|
+
influence: scalar,
|
|
114
|
+
blendType: z.string(),
|
|
115
|
+
extrapolation: z.string(),
|
|
116
|
+
/** The NLA is in tweak mode (`use_tweak_mode`), which the presenter does not play. */
|
|
117
|
+
tweak: z.boolean().optional(),
|
|
118
|
+
tracks: z.array(z.object({ name: z.string(), mute: z.boolean(), solo: z.boolean(), strips: z.array(nlaStripSchema) }).strict()),
|
|
119
|
+
/** Bones whose channels a driver sets. */
|
|
120
|
+
drivers: z.array(z.string()).optional(),
|
|
121
|
+
}).strict();
|
|
122
|
+
|
|
55
123
|
export const armatureSchema = z
|
|
56
124
|
.object({
|
|
57
125
|
object: z.string(),
|
|
@@ -63,10 +131,14 @@ export const armatureSchema = z
|
|
|
63
131
|
bones: z.array(boneSchema),
|
|
64
132
|
/** `animation_data.action`, the action it plays (`session.py` `_armatures`). */
|
|
65
133
|
action: z.string().nullable().optional(),
|
|
134
|
+
animation: armatureAnimationSchema.optional(),
|
|
66
135
|
})
|
|
67
136
|
.strict();
|
|
68
137
|
|
|
69
138
|
export type BlenderArmature = z.infer<typeof armatureSchema>;
|
|
139
|
+
export type BlenderArmatureBone = z.infer<typeof boneSchema>;
|
|
140
|
+
export type BlenderArmatureAnimation = z.infer<typeof armatureAnimationSchema>;
|
|
141
|
+
export type BlenderNlaStrip = z.infer<typeof nlaStripSchema>;
|
|
70
142
|
type Bone = z.infer<typeof boneSchema>;
|
|
71
143
|
|
|
72
144
|
/**
|
|
@@ -193,6 +193,8 @@ export class RuntimeSkeletons {
|
|
|
193
193
|
return world.invert();
|
|
194
194
|
});
|
|
195
195
|
mesh.updateWorldMatrix(true, false);
|
|
196
|
+
// A re-export rebinds the same mesh: its old skeleton's bone texture goes with it.
|
|
197
|
+
(skinned.skeleton as THREE.Skeleton | undefined)?.dispose();
|
|
196
198
|
skinned.bind(new THREE.Skeleton(bones, inverses), mesh.matrixWorld.clone());
|
|
197
199
|
return skinned;
|
|
198
200
|
}
|
|
@@ -7,7 +7,7 @@ import * as THREE from 'three';
|
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { onPresenterChange, presenterChanged } from './blender-presenter-change';
|
|
9
9
|
import { bytesFromBase64 } from './blender-base64';
|
|
10
|
-
import { ArmatureOverlay, armatureSchema } from './blender-runtime-armature';
|
|
10
|
+
import { ArmatureOverlay, armatureSchema, type BlenderArmature } from './blender-runtime-armature';
|
|
11
11
|
import { type FrameSkin, RuntimeSkeletons, skinGeometry } from './blender-runtime-skeleton';
|
|
12
12
|
import { UNKNOWN_GEOMETRY, UNKNOWN_IMAGE } from './blender-runtime-frame';
|
|
13
13
|
import {
|
|
@@ -607,6 +607,8 @@ export const frameSchema = z
|
|
|
607
607
|
actions: z.record(z.string(), z.number().int().nonnegative()).default({}),
|
|
608
608
|
/** The scene's frame range and rate (`session.py`), beside `frame`. */
|
|
609
609
|
clock: z.object({ start: z.number().int(), end: z.number().int(), fps: z.number().finite().positive() }).strict().optional(),
|
|
610
|
+
/** Actions with no user and no fake user, which the next save drops. */
|
|
611
|
+
unkeptActions: z.array(z.string()).default([]),
|
|
610
612
|
/** The active object's active vertex group, per vertex
|
|
611
613
|
* (`blender-runtime-weights.ts`); null when nothing is painted. */
|
|
612
614
|
weights: weightsSchema.nullable().default(null),
|
|
@@ -1126,18 +1128,23 @@ export class BlenderRuntimeView {
|
|
|
1126
1128
|
animationFacts(): {
|
|
1127
1129
|
frame: number;
|
|
1128
1130
|
clock: { start: number; end: number; fps: number } | null;
|
|
1131
|
+
/** Actions the next save drops: no user and no fake user. */
|
|
1132
|
+
unkept: readonly string[];
|
|
1129
1133
|
active: string | null;
|
|
1130
1134
|
actions: Readonly<Record<string, number>>;
|
|
1131
|
-
|
|
1135
|
+
/** Each armature as the frame describes it: bones (rest, channels, constraints, Blender's own
|
|
1136
|
+
* pose), its action and its NLA stack. */
|
|
1137
|
+
armatures: Readonly<Record<string, BlenderArmature>>;
|
|
1132
1138
|
skinArmature(object: string): string | null;
|
|
1133
1139
|
} {
|
|
1134
1140
|
const frame = this.frame;
|
|
1135
1141
|
return {
|
|
1136
1142
|
frame: frame?.frame ?? 0,
|
|
1137
1143
|
clock: frame?.clock ?? null,
|
|
1144
|
+
unkept: frame?.unkeptActions ?? [],
|
|
1138
1145
|
active: frame?.active ?? null,
|
|
1139
1146
|
actions: frame?.actions ?? {},
|
|
1140
|
-
armatures:
|
|
1147
|
+
armatures: frame?.armatures ?? {},
|
|
1141
1148
|
skinArmature: (name) => {
|
|
1142
1149
|
const object = this.objectForBlenderName(name) as THREE.Mesh | null;
|
|
1143
1150
|
const skin = object?.geometry?.userData['blenderSkin'] as { armature: string } | undefined;
|
package/package.json
CHANGED
package/wasm/BUNDLE.json
CHANGED
|
@@ -45,6 +45,6 @@
|
|
|
45
45
|
"note": "Emscripten 6.0.5, Release -O3 objects and -O2 link, retained public build recipe and dependency licenses. Scoped render-evaluated export preserves render-only geometry and render-specific modifier results without replacing the viewport export state. Prior geometry identity and memory fixes remain. Full-scene Three versus Cycles rendering parity remains incomplete.",
|
|
46
46
|
"callTrampoline": "CPython 3.13.13 is compiled with PY_CALL_TRAMPOLINE for Blender CPython slot arity casts. This applies to CPython dispatch, not arbitrary indirect calls.",
|
|
47
47
|
"engineIds": "The session preserves the compiled native CYCLES engine. VOLTER_THREE selects the explicit raster presenter; BLENDER_EEVEE and BLENDER_WORKBENCH remain presentation engines.",
|
|
48
|
-
"exportDoor": "The source-owned bpy_web_export.cc exports Principled physical inputs, normal images, eight named UV layers and evaluated corner normals, and takes graph_materials/graph_images/graph_generated (a graph-drawn shader mix reduced to the one Principled BSDF it reaches, graph images at the door revisions with every UDIM tile, orco for deformed meshes whose graph reads Generated coordinates). A frame can defer its changed meshes and pictures (defer) for the caller to pull one at a time (export_mesh, export_image), each call freeing the previous one's columns and a picture it decoded; an undo step keeps the revisions when Blender kept the depsgraph (session_undo); Blender's CPU subdivision descriptor is released once a mesh is read; the memory door reports malloc in-use and free. Datablock revisions are keyed by session_uid. writefile.cc writes one copy of the document per save in the browser build. CPU evaluators adopt stencil tables without cloning; browser subdivision descriptors are released immediately after CPU tessellation/deformation. Native evaluated corner triangles accompany corner normals. Consumed local conversion stencils are released before later UV channels are constructed. Caller-thread file-read, evaluated-object and subdivision checkpoints provide backpressure for resumable startup. Shared geometry revisions advance once per frame. Existing synchronous export remains available. Optional instance_sources exports stable evaluated collection and particle source meshes, including excluded library meshes and converted curves. Frame-local instance_geometry tokens verify Python placement metadata against the exported geometry and are consumed before presentation. Geometry resource identity preserves implicit input storage only for unmodified geometry and separates evaluated modifier/deformation outputs. Object and mesh resource keys have independent namespaces. Scoped begin_render_export/end_render_export reads the render engine depsgraph and isolates viewport revision, undo, deferred resource and image watcher state. Geometry resource identity preserves implicit input storage only for unmodified geometry and separates evaluated modifier/deformation outputs. Object and mesh resource keys have independent namespaces. A frame ships each armature-deformed mesh's skin with its geometry (write_skin: four joints and weights per drawn vertex by the armature's deform bones, keyed by skin_digest so it is re-sent only when the binding changes) and each armature's assigned action
|
|
48
|
+
"exportDoor": "The source-owned bpy_web_export.cc exports Principled physical inputs, normal images, eight named UV layers and evaluated corner normals, and takes graph_materials/graph_images/graph_generated (a graph-drawn shader mix reduced to the one Principled BSDF it reaches, graph images at the door revisions with every UDIM tile, orco for deformed meshes whose graph reads Generated coordinates). A frame can defer its changed meshes and pictures (defer) for the caller to pull one at a time (export_mesh, export_image), each call freeing the previous one's columns and a picture it decoded; an undo step keeps the revisions when Blender kept the depsgraph (session_undo); Blender's CPU subdivision descriptor is released once a mesh is read; the memory door reports malloc in-use and free. Datablock revisions are keyed by session_uid. writefile.cc writes one copy of the document per save in the browser build. CPU evaluators adopt stencil tables without cloning; browser subdivision descriptors are released immediately after CPU tessellation/deformation. Native evaluated corner triangles accompany corner normals. Consumed local conversion stencils are released before later UV channels are constructed. Caller-thread file-read, evaluated-object and subdivision checkpoints provide backpressure for resumable startup. Shared geometry revisions advance once per frame. Existing synchronous export remains available. Optional instance_sources exports stable evaluated collection and particle source meshes, including excluded library meshes and converted curves. Frame-local instance_geometry tokens verify Python placement metadata against the exported geometry and are consumed before presentation. Geometry resource identity preserves implicit input storage only for unmodified geometry and separates evaluated modifier/deformation outputs. Object and mesh resource keys have independent namespaces. Scoped begin_render_export/end_render_export reads the render engine depsgraph and isolates viewport revision, undo, deferred resource and image watcher state. Geometry resource identity preserves implicit input storage only for unmodified geometry and separates evaluated modifier/deformation outputs. Object and mesh resource keys have independent namespaces. A frame ships each armature-deformed mesh's skin with its geometry (write_skin: four joints and weights per drawn vertex by the armature's deform bones, keyed by skin_digest so it is re-sent only when the binding changes), every action in the file with its revision (actions), and each armature's assigned action.",
|
|
49
49
|
"preloadedPayload": "releasePreloadedFileData releases the JS preload buffer after session ready. The measured working payload exclusions are retained."
|
|
50
50
|
}
|