@volter/blender-engine 0.1.133 → 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/protocol.ts +0 -9
- package/browser/rna.ts +29 -86
- package/browser/runtime.ts +0 -16
- package/browser/session.py +471 -190
- package/browser/three/blender-runtime-armature.ts +74 -0
- package/browser/three/blender-runtime-skeleton.ts +206 -0
- package/browser/three/blender-runtime-view.ts +132 -7
- package/browser/worker.ts +0 -7
- package/package.json +1 -1
- package/wasm/BUNDLE.json +11 -11
- package/wasm/blender_browser.js +1 -1
- package/wasm/blender_browser.wasm.br +0 -0
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
|
|
|
@@ -1372,6 +1478,13 @@ def _armatures(view_layer):
|
|
|
1372
1478
|
# `ARM_DRAW_MODE_*` switch, `:906-936`).
|
|
1373
1479
|
"mode": obj.mode,
|
|
1374
1480
|
"bones": _armature_bones(obj, locked),
|
|
1481
|
+
# THE ACTION IT PLAYS (`animation_data.action`): what the presenter's mixer plays on
|
|
1482
|
+
# this armature's skeleton, baked through the clip door at the revision the frame's
|
|
1483
|
+
# `actions` names for it.
|
|
1484
|
+
"action": (obj.animation_data.action.name
|
|
1485
|
+
if obj.animation_data is not None and obj.animation_data.action is not None
|
|
1486
|
+
else None),
|
|
1487
|
+
**_armature_animation(obj),
|
|
1375
1488
|
}
|
|
1376
1489
|
return armatures
|
|
1377
1490
|
|
|
@@ -1642,6 +1755,12 @@ class Session:
|
|
|
1642
1755
|
"scale_length": float(scene.unit_settings.scale_length),
|
|
1643
1756
|
}
|
|
1644
1757
|
frame["armatures"] = _armatures(view_layer)
|
|
1758
|
+
# THE SCENE'S CLOCK beside the frame number: what a Timeline draws and a mixer maps time by.
|
|
1759
|
+
frame["clock"] = {"start": int(scene.frame_start), "end": int(scene.frame_end),
|
|
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)
|
|
1645
1764
|
frame["weights"] = _weights(scene, view_layer, frame, self._known)
|
|
1646
1765
|
warnings = list(frame.get("warnings", ()))
|
|
1647
1766
|
warnings.extend(_WARNINGS)
|
|
@@ -3844,6 +3963,13 @@ def rna_context(object_name=None, collection_path=None):
|
|
|
3844
3963
|
# list longer than _OUTLINER_PAGE answers its first page and says how many more
|
|
3845
3964
|
# there are, so a scene of 20,000 objects cannot turn one read into a
|
|
3846
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.
|
|
3847
3973
|
|
|
3848
3974
|
_OUTLINER_PAGE = 64
|
|
3849
3975
|
|
|
@@ -4182,6 +4308,60 @@ def _outliner_object(obj, view_layer, seen, chosen, active):
|
|
|
4182
4308
|
return row
|
|
4183
4309
|
|
|
4184
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
|
+
|
|
4185
4365
|
# ---- the node editor's tree ------------------------------------------------
|
|
4186
4366
|
#
|
|
4187
4367
|
# THE ONE READ A NODE VIEW NEEDS. The generic `rna_view` door answers every one
|
|
@@ -4617,31 +4797,6 @@ def _matrix_rows(matrix):
|
|
|
4617
4797
|
return [[float(v) for v in row] for row in matrix]
|
|
4618
4798
|
|
|
4619
4799
|
|
|
4620
|
-
def rna_rig(object_name=None):
|
|
4621
|
-
"""EVERY SKIN BINDING THE SCENE NEEDS -- or one named mesh's.
|
|
4622
|
-
|
|
4623
|
-
A LIST, not a singleton, and that is what the PRESENTER's question is: a
|
|
4624
|
-
presented frame carries every object at once, and the presenter has to know
|
|
4625
|
-
which of its meshes are `THREE.SkinnedMesh`es before it builds them. Asking
|
|
4626
|
-
per object would be one round trip per mesh; asking by NAME stays available
|
|
4627
|
-
for a `cyclotron eval` that wants to read one.
|
|
4628
|
-
"""
|
|
4629
|
-
scene_frame = int(bpy.context.scene.frame_current)
|
|
4630
|
-
if object_name:
|
|
4631
|
-
obj = bpy.data.objects.get(object_name)
|
|
4632
|
-
if obj is None:
|
|
4633
|
-
raise ValueError("The engine holds no object named %r" % (object_name,))
|
|
4634
|
-
subjects = [obj]
|
|
4635
|
-
else:
|
|
4636
|
-
subjects = [obj for obj in bpy.context.view_layer.objects
|
|
4637
|
-
if obj.type == "MESH" and obj.data is not None
|
|
4638
|
-
and obj.find_armature() is not None]
|
|
4639
|
-
rigs = []
|
|
4640
|
-
for obj in subjects:
|
|
4641
|
-
_asked({"checkpoint": "rig-object"})
|
|
4642
|
-
rigs.append(_rig_of(obj))
|
|
4643
|
-
return {"frame": scene_frame, "named": object_name, "rigs": rigs}
|
|
4644
|
-
|
|
4645
4800
|
|
|
4646
4801
|
def _rig_of(obj):
|
|
4647
4802
|
"""ONE MESH'S SKIN BINDING: the armature's bones, and up to four weighted
|
|
@@ -4841,50 +4996,6 @@ def _summary_objects():
|
|
|
4841
4996
|
return out
|
|
4842
4997
|
|
|
4843
4998
|
|
|
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
4999
|
def rna_action_clip(object_name=None, bake=True, action_name=None):
|
|
4889
5000
|
"""ONE ACTION AS A THREE.JS CLIP: per bone, the LOCAL transform it has at
|
|
4890
5001
|
every integer frame of the action's own range.
|
|
@@ -4957,8 +5068,12 @@ def rna_action_clip(object_name=None, bake=True, action_name=None):
|
|
|
4957
5068
|
adt = arm_obj.animation_data if arm_obj is not None else None
|
|
4958
5069
|
assigned = adt.action if adt is not None else None
|
|
4959
5070
|
if arm_obj is None or (assigned is None and not action_name):
|
|
4960
|
-
|
|
4961
|
-
|
|
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.")
|
|
4962
5077
|
return header
|
|
4963
5078
|
action = assigned
|
|
4964
5079
|
slot = getattr(adt, "action_slot", None) if adt is not None else None
|
|
@@ -4997,16 +5112,28 @@ def rna_action_clip(object_name=None, bake=True, action_name=None):
|
|
|
4997
5112
|
if not bake:
|
|
4998
5113
|
return header
|
|
4999
5114
|
frames = sorted(columns)
|
|
5000
|
-
|
|
5001
|
-
|
|
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])))
|
|
5002
5121
|
if last <= first:
|
|
5003
5122
|
last = first + 1
|
|
5004
5123
|
header["clipStart"] = first
|
|
5005
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)
|
|
5006
5129
|
channels = {}
|
|
5130
|
+
others = set()
|
|
5007
5131
|
for fcurve in curves:
|
|
5008
5132
|
match = _BONE_PATH.match(fcurve.data_path)
|
|
5009
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)
|
|
5010
5137
|
continue
|
|
5011
5138
|
bone, prop = match.group(1), match.group(2)
|
|
5012
5139
|
channels.setdefault((bone, prop), {})[int(fcurve.array_index)] = fcurve
|
|
@@ -5015,61 +5142,19 @@ def rna_action_clip(object_name=None, bake=True, action_name=None):
|
|
|
5015
5142
|
"door does not play (object transform, shape keys, a material). The "
|
|
5016
5143
|
"Timeline still draws its keys." % (action.name,))
|
|
5017
5144
|
return header
|
|
5018
|
-
rest_relative = {}
|
|
5019
|
-
for pchan in _pose_bones_parents_first(arm_obj):
|
|
5020
|
-
local = pchan.bone.matrix_local
|
|
5021
|
-
parent = pchan.parent
|
|
5022
|
-
rest_relative[pchan.name] = (parent.bone.matrix_local.inverted_safe() @ local
|
|
5023
|
-
if parent is not None else local.copy())
|
|
5024
|
-
times = [(frame - first) / fps for frame in range(first, last + 1)]
|
|
5025
5145
|
tracks = []
|
|
5026
5146
|
unplayed = []
|
|
5027
|
-
for bone in sorted(
|
|
5028
|
-
|
|
5029
|
-
if pchan is None:
|
|
5147
|
+
for (bone, prop) in sorted(channels):
|
|
5148
|
+
if arm_obj.pose.bones.get(bone) is None:
|
|
5030
5149
|
unplayed.append(bone)
|
|
5031
5150
|
continue
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
for frame in range(first, last + 1):
|
|
5037
|
-
location = _sample(channels.get((bone, "location")), pchan.location, frame)
|
|
5038
|
-
scale = _sample(channels.get((bone, "scale")), pchan.scale, frame, default=1.0)
|
|
5039
|
-
if rotation_mode == "QUATERNION":
|
|
5040
|
-
quat = mathutils.Quaternion(
|
|
5041
|
-
_sample(channels.get((bone, "rotation_quaternion")),
|
|
5042
|
-
pchan.rotation_quaternion, frame, size=4, default=None,
|
|
5043
|
-
identity=(1.0, 0.0, 0.0, 0.0)))
|
|
5044
|
-
elif rotation_mode == "AXIS_ANGLE":
|
|
5045
|
-
raw = _sample(channels.get((bone, "rotation_axis_angle")),
|
|
5046
|
-
pchan.rotation_axis_angle, frame, size=4, default=None,
|
|
5047
|
-
identity=(0.0, 0.0, 1.0, 0.0))
|
|
5048
|
-
quat = mathutils.Quaternion(raw[1:4], raw[0])
|
|
5049
|
-
else:
|
|
5050
|
-
quat = mathutils.Euler(
|
|
5051
|
-
_sample(channels.get((bone, "rotation_euler")), pchan.rotation_euler, frame),
|
|
5052
|
-
rotation_mode).to_quaternion()
|
|
5053
|
-
basis = mathutils.Matrix.LocRotScale(mathutils.Vector(location), quat,
|
|
5054
|
-
mathutils.Vector(scale))
|
|
5055
|
-
position, rotation, scaling = (rest @ basis).decompose()
|
|
5056
|
-
# QUATERNION CONTINUITY, and it is not cosmetic: three's
|
|
5057
|
-
# `QuaternionLinearInterpolant` takes the shorter arc between
|
|
5058
|
-
# NEIGHBOURING samples, so one sign flip in the middle of a bake
|
|
5059
|
-
# spins the bone the long way round for a frame.
|
|
5060
|
-
if previous is not None and rotation.dot(previous) < 0.0:
|
|
5061
|
-
rotation.negate()
|
|
5062
|
-
previous = rotation.copy()
|
|
5063
|
-
positions.extend((position.x, position.y, position.z))
|
|
5064
|
-
quaternions.extend((rotation.x, rotation.y, rotation.z, rotation.w))
|
|
5065
|
-
scales.extend((scaling.x, scaling.y, scaling.z))
|
|
5066
|
-
tracks.extend(_track(bone, "position", times, positions, 3))
|
|
5067
|
-
tracks.extend(_track(bone, "quaternion", times, quaternions, 4))
|
|
5068
|
-
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))]
|
|
5069
5155
|
header.update({
|
|
5070
5156
|
"tracks": tracks,
|
|
5071
5157
|
"duration": (last - first) / fps,
|
|
5072
|
-
"sampled": len(times),
|
|
5073
5158
|
"unplayedBones": unplayed,
|
|
5074
5159
|
})
|
|
5075
5160
|
return header
|
|
@@ -5082,52 +5167,94 @@ _KEY_TYPE_RANK = {"JITTER": 1, "GENERATED": 2, "MOVING_HOLD": 3, "BREAKDOWN": 4,
|
|
|
5082
5167
|
"KEYFRAME": 5, "EXTREME": 6}
|
|
5083
5168
|
|
|
5084
5169
|
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
is one, and the pose channel's CURRENT value where there is not -- which is
|
|
5088
|
-
what Blender does too (an unkeyed component simply keeps its value)."""
|
|
5089
|
-
fallback = list(identity) if identity is not None else [
|
|
5090
|
-
(current[i] if current is not None and i < len(current) else default) for i in range(size)]
|
|
5091
|
-
if identity is not None and current is not None and group is not None:
|
|
5092
|
-
fallback = [float(v) for v in current]
|
|
5093
|
-
if group is None:
|
|
5094
|
-
return fallback
|
|
5095
|
-
out = list(fallback)
|
|
5096
|
-
for index, fcurve in group.items():
|
|
5097
|
-
if 0 <= index < len(out):
|
|
5098
|
-
out[index] = float(fcurve.evaluate(frame))
|
|
5099
|
-
return out
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
def _track(bone, prop, times, values, stride):
|
|
5103
|
-
"""One three.js keyframe track, or two keys when nothing moves.
|
|
5170
|
+
_INTERPOLATION = {"CONSTANT": 0, "LINEAR": 1, "BEZIER": 2}
|
|
5171
|
+
_CYCLE_MODE = {"NONE": 0, "REPEAT": 1, "REPEAT_OFFSET": 2, "MIRROR": 3}
|
|
5104
5172
|
|
|
5105
|
-
The arrays cross as base64 Float32 (`timeBase64`, `valueBase64`), the shape
|
|
5106
|
-
every large payload in this file takes."""
|
|
5107
|
-
count = len(times)
|
|
5108
|
-
constant = True
|
|
5109
|
-
for i in range(1, count):
|
|
5110
|
-
for c in range(stride):
|
|
5111
|
-
if abs(values[i * stride + c] - values[c]) > 1e-6:
|
|
5112
|
-
constant = False
|
|
5113
|
-
break
|
|
5114
|
-
if not constant:
|
|
5115
|
-
break
|
|
5116
|
-
if constant:
|
|
5117
|
-
if count == 0:
|
|
5118
|
-
return []
|
|
5119
|
-
times = [times[0], times[-1]]
|
|
5120
|
-
values = values[0:stride] * 2
|
|
5121
|
-
return [{
|
|
5122
|
-
"bone": bone,
|
|
5123
|
-
"property": prop,
|
|
5124
|
-
"stride": stride,
|
|
5125
|
-
"count": len(times),
|
|
5126
|
-
"constant": constant,
|
|
5127
|
-
"timeBase64": base64.b64encode(array.array("f", times).tobytes()).decode("ascii"),
|
|
5128
|
-
"valueBase64": base64.b64encode(array.array("f", values).tobytes()).decode("ascii"),
|
|
5129
|
-
}]
|
|
5130
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
|
|
5131
5258
|
|
|
5132
5259
|
def rna_outliner(selected=None):
|
|
5133
5260
|
"""BLENDER'S VIEW LAYER TREE for the session's scene.
|
|
@@ -5152,10 +5279,16 @@ def rna_outliner(selected=None):
|
|
|
5152
5279
|
engine_active = view_layer.objects.active
|
|
5153
5280
|
active = engine_active.name if engine_active is not None else None
|
|
5154
5281
|
seen = {}
|
|
5155
|
-
# Every object
|
|
5282
|
+
# Every object's PLACE, with the row it sits under and the COLLECTION row it
|
|
5156
5283
|
# belongs to -- what `ObjectsChildrenBuilder`'s map holds, and what the
|
|
5157
|
-
# 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.
|
|
5158
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 = []
|
|
5159
5292
|
|
|
5160
5293
|
def place(obj, row, collection_row):
|
|
5161
5294
|
placements.setdefault(obj.name, []).append([row, collection_row, collection_row])
|
|
@@ -5182,10 +5315,12 @@ def rna_outliner(selected=None):
|
|
|
5182
5315
|
# Fold parenting before paging: a parent may follow its children in
|
|
5183
5316
|
# collection order, and must not disappear behind the flat cap.
|
|
5184
5317
|
for obj in collection.objects:
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5318
|
+
spot = {"place": obj, "name": obj.name, "children": []}
|
|
5319
|
+
occurrences.append(spot)
|
|
5320
|
+
place(obj, spot, row)
|
|
5321
|
+
children.append(spot)
|
|
5188
5322
|
row["children"] = children
|
|
5323
|
+
collections.append(row)
|
|
5189
5324
|
return row
|
|
5190
5325
|
|
|
5191
5326
|
scene_collection = _outliner_row(
|
|
@@ -5198,10 +5333,12 @@ def rna_outliner(selected=None):
|
|
|
5198
5333
|
child.collection.name))
|
|
5199
5334
|
for child in root.children]
|
|
5200
5335
|
for obj in root.collection.objects:
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5336
|
+
spot = {"place": obj, "name": obj.name, "children": []}
|
|
5337
|
+
occurrences.append(spot)
|
|
5338
|
+
place(obj, spot, scene_collection)
|
|
5339
|
+
children.append(spot)
|
|
5204
5340
|
scene_collection["children"] = children
|
|
5341
|
+
collections.append(scene_collection)
|
|
5205
5342
|
|
|
5206
5343
|
# `ObjectsChildrenBuilder::make_object_parent_hierarchy_collections`, parents
|
|
5207
5344
|
# before children: a child object is MOVED out of its collection's row and
|
|
@@ -5219,8 +5356,15 @@ def rna_outliner(selected=None):
|
|
|
5219
5356
|
placed.add(obj.name)
|
|
5220
5357
|
ordered.append(obj)
|
|
5221
5358
|
|
|
5222
|
-
|
|
5223
|
-
|
|
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 = []
|
|
5224
5368
|
for obj in ordered:
|
|
5225
5369
|
if obj.parent is None:
|
|
5226
5370
|
continue
|
|
@@ -5229,37 +5373,178 @@ def rna_outliner(selected=None):
|
|
|
5229
5373
|
if not parents or not mine:
|
|
5230
5374
|
continue
|
|
5231
5375
|
for parent_row, _parent_owner, parent_collection in parents:
|
|
5232
|
-
|
|
5376
|
+
moved_here = False
|
|
5233
5377
|
for entry in mine:
|
|
5234
5378
|
row, owner, _collection = entry
|
|
5235
5379
|
if owner is not parent_collection:
|
|
5236
5380
|
continue
|
|
5237
|
-
|
|
5381
|
+
moved.add(id(row))
|
|
5238
5382
|
parent_row["children"].append(row)
|
|
5239
5383
|
entry[1] = parent_row
|
|
5240
|
-
|
|
5384
|
+
moved_here = True
|
|
5241
5385
|
break
|
|
5242
|
-
if
|
|
5386
|
+
if moved_here:
|
|
5243
5387
|
continue
|
|
5244
|
-
duplicate =
|
|
5245
|
-
|
|
5246
|
-
struct="Object", objectType=obj.type, object=obj.name,
|
|
5247
|
-
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)
|
|
5248
5390
|
parent_row["children"].append(duplicate)
|
|
5249
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]
|
|
5395
|
+
|
|
5396
|
+
# THE SELECTION KEEPS ITS ROWS PAST THE PAGE. A row is how the editor
|
|
5397
|
+
# addresses an object -- the viewport's outline and gizmo, the Properties
|
|
5398
|
+
# rail -- so a selected or active object, the caller's or the engine's, keeps
|
|
5399
|
+
# its row and every row above it however far down a long list it sits.
|
|
5400
|
+
# BOUNDED LIKE THE PAGE ITSELF: the active object always, then at most one
|
|
5401
|
+
# page of the selection, so selecting everything cannot turn one read back
|
|
5402
|
+
# into the whole tree the page exists to keep out (Canyon Comet, 1,493
|
|
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.
|
|
5406
|
+
wanted = [active] if active is not None else []
|
|
5407
|
+
wanted += sorted(chosen)
|
|
5408
|
+
wanted += [o.name for o in view_layer.objects if o.select_get(view_layer=view_layer)]
|
|
5409
|
+
kept = set()
|
|
5410
|
+
for name in wanted:
|
|
5411
|
+
if len(kept) >= _OUTLINER_PAGE:
|
|
5412
|
+
break
|
|
5413
|
+
kept.add(name)
|
|
5414
|
+
holding = set()
|
|
5415
|
+
|
|
5416
|
+
def hold(row):
|
|
5417
|
+
inside = "place" in row and row["name"] in kept
|
|
5418
|
+
for child in row.get("children", []):
|
|
5419
|
+
inside = hold(child) or inside
|
|
5420
|
+
if inside:
|
|
5421
|
+
holding.add(id(row))
|
|
5422
|
+
return inside
|
|
5423
|
+
|
|
5424
|
+
hold(scene_collection)
|
|
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
|
|
5250
5448
|
|
|
5251
5449
|
# Cap the hierarchy's visible child lists, not the collection's flat input.
|
|
5252
5450
|
# Every parent participates in folding even when its child list needs a page.
|
|
5253
5451
|
def page_tree(row):
|
|
5254
|
-
|
|
5452
|
+
members = row.get("children", [])
|
|
5453
|
+
children, more = _outliner_page(members)
|
|
5454
|
+
beyond = [child for child in members[len(children):] if id(child) in holding]
|
|
5455
|
+
children = [kept_row(child) for child in children + beyond]
|
|
5255
5456
|
row["children"] = children
|
|
5256
|
-
if more:
|
|
5257
|
-
row["more"] = more
|
|
5457
|
+
if more - len(beyond):
|
|
5458
|
+
row["more"] = more - len(beyond)
|
|
5258
5459
|
for child in children:
|
|
5259
5460
|
page_tree(child)
|
|
5260
5461
|
|
|
5261
5462
|
page_tree(scene_collection)
|
|
5262
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
|
+
|
|
5263
5548
|
return {
|
|
5264
5549
|
"scene": scene_path,
|
|
5265
5550
|
"viewLayer": view_layer_path,
|
|
@@ -5511,12 +5796,8 @@ def _dispatch(request):
|
|
|
5511
5796
|
# THE RIG AND CLIP DOORS (the Timeline): both READS, so neither presents
|
|
5512
5797
|
# and neither owes a derivation. There is deliberately no writer beside
|
|
5513
5798
|
# them -- keying, moving a key and setting a range are edits.
|
|
5514
|
-
if op == "rig":
|
|
5515
|
-
return rna_rig(request.get("object"))
|
|
5516
5799
|
if op == "action-clip":
|
|
5517
5800
|
return rna_action_clip(request.get("object"), request.get("bake", True), request.get("action"))
|
|
5518
|
-
if op == "armature-actions":
|
|
5519
|
-
return rna_armature_actions()
|
|
5520
5801
|
if op == "outliner":
|
|
5521
5802
|
return rna_outliner(request.get("selected"))
|
|
5522
5803
|
if op == "outliner-set":
|