cutan.characters.play¶
Resolve a play against a character descriptor — the renderer-free half (an#7).
A PlayAction names a descriptor animation. Its tracks
speak the DESCRIPTOR’s vocabulary — bones, slots, attachment names, view-box
units, degrees — while the renderer’s channels speak the SCENE’s: node paths,
swap-set keys, scene pixels, radians. This module does everything on the
descriptor side of that line and knows no renderer, so that an validate
and the cutout compiler share ONE verdict on whether a play can resolve. The
compiler used to decide alone, and validate passed plays that compile then
refused — four measured cases: an unknown bone property, a bone with no slot
of its own, a frame naming art that is not on disk, and a slot suppressed by
face_overlay=false (an#7 review).
Every rule mirrors a rig-builder fact, and the builder imports the shared helpers rather than restating them, so the two cannot drift:
A bone track animates the node of the bone’s primary slot — the slot named like the bone (
primary_slot_per_bone());bone:root.*animates the entity container. A bone with no primary slot is a resolution error that says so: the old message (“no node of that name was built”) named the symptom and left the rule for the author to guess.A slot track resolves to exactly one swap set: the set whose keys name every frame’s attachment. Resolving frame-by-frame used to split a track across two channels; the runtime applies a pose’s properties in name order, so
blinknever closed once a second set that sorted beforeeyelidalso namedopen. Two candidates is an error naming both.Art is consulted when the caller can consult it (
art_exists): a frame whose attachment is declared but not on disk is reported as exactly that, not as “no set resolves it”.
>>> from cutan.characters.schema import CharacterDescriptor
>>> desc = CharacterDescriptor(name="maya")
>>> resolved = resolve_play(desc, "blink")
>>> [(t.slot, t.set_name) for t in resolved.tracks]
[('left_eye', 'eyelid'), ('right_eye', 'eyelid')]
Motion presets (an#166). A name the descriptor does not declare — or any
name on an entity with no descriptor (a procedural rig, a prop) — falls back
to cutan.motion.PRESETS. The DESCRIPTOR WINS a name both know: a rig that
ships its own hop means that one. play_source() makes that call and
play_problems() gives the whole verdict, for an validate and the
compiler alike. A preset resolves to tweens, not a clip:
expand_preset_play() builds them at the moved node’s pose, which the
caller reads off the built scene and the timeline before the play (an#212),
so an author never passes rest.
>>> play_problems(desc, "moonwalk")
["no animation 'moonwalk': the descriptor declares ['blink', 'idle_breath'] and no
motion preset has that name (presets: ['crawl', 'hop', 'nod', 'point', 'pop_in',
'shake', 'slide_in', 'slide_out', 'speech_pulse', 'squash_stretch', 'turn',
'waddle', 'walk'])"]
>>> play_source(desc, "hop"), play_source(None, "hop"), play_source(desc, "blink")
('preset', 'preset', 'descriptor')
Module Attributes
Descriptor bone-track properties → |
|
Bone-track properties whose values are view-box LENGTHS, so a renderer scales them by the rig's view-box → scene-pixel factor. |
|
a track on it animates the entity's container node rather than any slot. |
|
The bone whose primary slot's nested slots are the FACE — what |
|
the entity descriptor's own |
|
…or, for a name it does not declare, |
|
the target is the play's own, the rest pose is read off the built scene, and a figure's drawn |
|
What the compiler fills into a preset play's args beyond what the author wrote, as far as the play's LENGTH depends on it (cutan#12): a walk's resolved |
|
a turn opens from the side the character faces NOW, which only the timeline knows. |
Functions
|
The skin the rig draws: |
|
|
|
The |
|
A preset |
|
What |
|
Every reason |
|
Which library a |
|
The ONE node path a preset play moves — |
|
Every node path a preset play moves. |
|
|
|
Whether the motion preset |
|
How long a preset |
|
Why |
|
Why a |
|
|
|
Resolve |
|
Fill in each turn's |
|
|
|
Frame-rate sample times for a sine track, ALWAYS ending at |
|
The node path of a slot RELATIVE to its entity ( |
|
The slot |
|
Slots the rig builder never builds: with the face baked into the head art ( |
Classes
|
A resolved |
|
What an entity shows at one instant, read off a flat timeline. |
|
|
|
A resolved |
|
One |
|
|
Exceptions
|
A |
- cutan.characters.play.BONE_TRACK_PROPERTIES: dict[str, tuple[str, float]] = {'rotation_deg': ('rotation', 0.017453292519943295), 'scale_x': ('scale_x', 1.0), 'scale_y': ('scale_y', 1.0), 'x': ('x', 1.0), 'y': ('y', 1.0)}¶
Descriptor bone-track properties →
(runtime property, unit factor). The descriptor speaks degrees for rotation; the runtime is radians.
- class cutan.characters.play.BoneTrack(track, slot, property, unit, rig_scaled)[source]¶
Bases:
objectA resolved
bone:<name>.<prop>track.slotis the primary slot whose node carries the bone, orNonefor the entity container (bone:root).propertyis the RUNTIME name; values arerest + deviation * unit(times the rig’s pixel factor whenrig_scaled).
- cutan.characters.play.DESCRIPTOR_SOURCE = 'descriptor'¶
the entity descriptor’s own
animations…- Type:
Where a
playresolves
- class cutan.characters.play.Facing(view=None, direction=None)[source]¶
Bases:
objectWhat an entity shows at one instant, read off a flat timeline.
viewis the key of its view set last set on the ENTITY (None: not set in this shot, so the rig’s default —fronton a factory character);directionis"right"/"left"from the sign of the lastscale_xit was given (None: nothing set it, so its rest).
- cutan.characters.play.HEAD_BONE = 'head'¶
The bone whose primary slot’s nested slots are the FACE — what
face_overlay=falsesuppresses.
- cutan.characters.play.PRESET_SOURCE = 'preset'¶
…or, for a name it does not declare,
cutan.motion.PRESETS(an#166).
- exception cutan.characters.play.PlayResolutionError(animation, problems)[source]¶
Bases:
ValueErrorA
playthat cannot resolve;problemslists every reason found.
- cutan.characters.play.PresetContext¶
What the compiler fills into a preset play’s args beyond what the author wrote, as far as the play’s LENGTH depends on it (cutan#12): a walk’s resolved
gaitand the figure’s drawnscale(cutan.characters.methods.walk_preset_context()). Keys a preset does not take are ignored.
- cutan.characters.play.RESERVED_PRESET_ARGS: frozenset[str] = frozenset({'parts', 'rest', 'scale', 'target'})¶
the target is the play’s own, the rest pose is read off the built scene, and a figure’s drawn
scaleis its stage scale (a length in scene px is passed outright).- Type:
Preset parameters an author may NOT pass through
args
- cutan.characters.play.RIG_SCALED_PROPERTIES: frozenset[str] = frozenset({'x', 'y'})¶
Bone-track properties whose values are view-box LENGTHS, so a renderer scales them by the rig’s view-box → scene-pixel factor. Scales and angles are dimensionless.
- cutan.characters.play.ROOT_BONE = 'root'¶
a track on it animates the entity’s container node rather than any slot.
- Type:
The bone that stands for the whole rig
- class cutan.characters.play.SlotTrack(track, slot, set_name, frames)[source]¶
Bases:
objectA resolved
slot:<name>.attachmenttrack: one set, frames as KEYS.
- cutan.characters.play.TURN_PRESET = 'turn'¶
a turn opens from the side the character faces NOW, which only the timeline knows.
- Type:
The preset whose START depends on what came before it on the timeline
- class cutan.characters.play.TurnInference(index, start, entity, before, declared=None)[source]¶
Bases:
objectOne
playofTURN_PRESETand the state it starts from.indexis the play’s position in the flat list it was read from;declaredis thefrom_directionthe author passed (None: left to the timeline).
- class cutan.characters.play.TurnResolution(flats, turns, events)[source]¶
Bases:
objectresolve_turns()’ result:flatsis the input with each turn’s inferredfrom_directionfilled in;turnssays what each turn started from;eventsis the timeline with every preset play expanded, whichfacing_at()reads.
- cutan.characters.play.active_skin(desc)[source]¶
The skin the rig draws:
default, else the first declared, else empty.- Return type:
- cutan.characters.play.art_exists_for(characters_store, ref)[source]¶
rel_path -> is the art on disk, for a character in a filesystem store;Nonewhen the store has no root to look under (a dict, a fake) — a store that can answer nothing must assume presence, not absence, exactly as the rig builder’s part probe does.
- cutan.characters.play.drawn_attachment(desc, skin, slot)[source]¶
The
(name, attachment)a slot draws by default, orNone.- Return type:
tuple[str,Attachment] |None
- cutan.characters.play.expand_preset_play(action, *, start, rest_of, parts_of=None)[source]¶
A preset
playas the flat tweens and settlingset``s it stands for, at absolute times from ``start(an#166).rest_of(node_path)returns the pose to build that node’s move from (x,y,rotation,scale_x,scale_y,alpha— the compiler passes the pose the node HAS atstart, an#212), orNonewhen the built scene carries no such node — then this raises naming it, which is what the runtime would otherwise do mid-render.durationstretches the move to that length;speeddivides it. Assumespreset_problems()came back empty.A preset that moves several nodes of the entity (it takes
parts,PARTS_ARG—walk) getsparts_of(entity)’s paths with theirrest_ofposes, and every node its expansion moves is checked.>>> from cutan.characters.registration import PlayAction >>> flats = expand_preset_play( ... PlayAction(target="a", animation="hop", args={"height": 10}), ... start=1.0, rest_of=lambda p: {"y": 5.0}) >>> [(round(f.start, 3), type(f.action).__name__, f.action.property) for f in flats] [(1.0, 'TweenAction', 'y'), (1.25, 'TweenAction', 'y'), (1.5, 'SetAction', 'y')] >>> flats[0].action.to_value -5.0- Return type:
- cutan.characters.play.facing_at(events, entity, t, *, view_set='view')[source]¶
What
entityshows at timet: the latestview_setswap set on the entity and the sign of the latestscale_xit was given, at or beforet(a tween counts from its END, when its value has landed; at one instant the one LATER ineventswins — so pass them in authoring order, asresolve_turns()does).>>> from an.ir.compose import flatten, sequence >>> from cutan.motion import turn >>> flats = flatten(sequence(turn("ned", to="side", direction="left"))) >>> facing_at(flats, "ned", 0.0), facing_at(flats, "ned", 1.0) (Facing(view=None, direction=None), Facing(view='side', direction='left'))- Return type:
- cutan.characters.play.play_problems(desc, animation, *, art_exists=None, args=None, duration=None, speed=1.0, loop=None)[source]¶
Every reason
play(<entity>, animation, ...)cannot resolve — empty when it can. THE verdictan validatereports and the compiler raises on, for both sources (an#7, an#166).>>> play_problems(None, "hop", args={"heigth": 3}) ["motion preset 'hop' has no parameter 'heigth' (it takes: [...])"] >>> play_problems(None, "hop", loop=True) ["motion preset 'hop' is a one-shot: `loop: true` ...
- cutan.characters.play.play_source(desc, animation)[source]¶
Which library a
playofanimationresolves in —DESCRIPTOR_SOURCEwhendescdeclares it (the descriptor WINS a name a preset also has), elsePRESET_SOURCEwhen a motion preset has it.desc=Noneis an entity with no descriptor: presets only.Raises
PlayResolutionErrornaming BOTH vocabularies when neither has the name.- Return type:
- cutan.characters.play.preset_moved_node(action_target, animation, args=None)[source]¶
The ONE node path a preset play moves —
<target>/headfor anod, the target itself for the rest. Read off the expansion rather than restated per preset, so a preset added later needs no entry here.>>> preset_moved_node("charlie", "nod"), preset_moved_node("charlie", "hop") ('charlie/head', 'charlie')- Return type:
- cutan.characters.play.preset_moved_nodes(action_target, animation, args=None, *, parts=None)[source]¶
Every node path a preset play moves.
parts(the entity’s built part paths, relative to it) is what a multi-node preset chooses its limbs from;Nonelets it assume the rig contract’s names.>>> preset_moved_nodes("bob", "walk", {"distance": 80}, parts=["torso", "left_leg", "right_leg"]) ['bob', 'bob/left_leg', 'bob/right_leg'] >>> preset_moved_nodes("charlie", "nod") ['charlie/head']
- cutan.characters.play.preset_play_span(action, context=None)[source]¶
How long a preset
playruns, in seconds: itsdurationwhen set, else the preset’s natural length divided byspeed. What asequenceadvances by isplay_extent(), which is this for a preset source.contextis what the compiler adds to the args before expanding (PresetContext): a walk’s length depends on its resolved gait and the figure’s scale, so the extent must see them too (cutan#12).>>> from cutan.characters.registration import PlayAction >>> preset_play_span(PlayAction(target="a", animation="hop")) 0.5 >>> preset_play_span(PlayAction(target="a", animation="hop", speed=2.0)) 0.25 >>> walk = PlayAction(target="a", animation="walk", args={"distance": 160}) >>> round(preset_play_span(walk), 6), round(preset_play_span(walk, {"scale": 2.0}), 6) (0.8, 0.4)- Return type:
- cutan.characters.play.preset_problems(animation, *, args=None, duration=None, speed=1.0, loop=None)[source]¶
Why a
playof the motion presetanimationcannot expand.The parameters are checked by NAME against the preset’s signature, then by building it (at the identity pose), so a value the preset itself refuses —
cycles: 0, a string height — is reported in the preset’s own words.
- cutan.characters.play.preset_takes(animation, name)[source]¶
Whether the motion preset
animationhas the keywordname(Falsefor a name that is no preset: a descriptor animation takes nothing).>>> preset_takes("walk", "parts"), preset_takes("hop", "parts"), preset_takes("blink", "parts") (True, False, False)- Return type:
- cutan.characters.play.preset_target_problems(animation, target)[source]¶
Why
animationcannot be played ontarget: a preset that moves an entity’s PARTS (walk) is played on the entity, not on one of its parts — a torso asked to walk would glide away from its legs, and the gait resolution keys the whole character (cutan#22). Checked by the compiler and byan validatealike.>>> preset_target_problems("walk", "w/torso") ["motion preset 'walk' moves a whole character: play it on 'w', not on its part 'w/torso'"] >>> preset_target_problems("walk", "w"), preset_target_problems("point", "w/arm_r") ([], [])
- cutan.characters.play.primary_slot_per_bone(desc)[source]¶
{bone name: the slot that IS that bone}, when one exists.The anchor of node nesting (
slot_parent_chain()). In the defaultflatnesting the rigs are flat — arms are siblings of the torso, not children (CLAUDE.md pillar 4) — so bone parentage decides position only: a slot nests under the primary slot of its own bone when it is not that slot itself, which puts eyes and mouth underheadand leaves every limb a direct child of the entity.nesting: bones(an#340) follows the bone hierarchy to the nearest ancestor’s primary slot instead.>>> from types import SimpleNamespace as NS >>> primary_slot_per_bone(NS(slots=[NS(name="head", bone="head"), NS(name="mouth", bone="head")]))["head"] 'head'
- cutan.characters.play.resolve_part_arg(animation, args=None, *, parts=None)[source]¶
argswith a one-part preset’spartnamed by its SLOT resolved to its node path among the entity’s builtparts(an#340).A
nodmoves<target>/head; innesting: bonesthe head is built under its torso (torso/head). A part that is built as named, or whose name matches no single built path’s last segment, is left as given (and reported unbuilt as before).>>> resolve_part_arg("nod", {}, parts=["torso", "torso/head"])["part"] 'torso/head' >>> resolve_part_arg("nod", {}, parts=["head", "torso"]) # built as named: as given {}- Return type:
- cutan.characters.play.resolve_play(desc, animation, *, art_exists=None)[source]¶
Resolve
animationofdescinto renderer-ready tracks, or raisePlayResolutionErrorlisting every problem found.art_exists(rel_path)answers whether a skin attachment’s art is on disk; passNonewhen the caller cannot know, and every declared attachment is assumed present (the rig builder’s own rule for a store without a filesystem root).- Return type:
- cutan.characters.play.resolve_turns(flat_list, *, descriptor_of, rest_of)[source]¶
Fill in each turn’s
from_directionfrom the timeline before it (an#203): aplayofturnon an entity that does not pass one opens from the side the latest earlierscale_xleft the entity facing — soside(direction: left) thenbackis two plays, with nofrom_directionby hand. Turns are resolved in time order, each seeing the ones before it expanded. THE resolver the compiler expands with andan validatechecks with.An explicit
from_directionis kept —turnsrecords it with the inferred state soan validatecan say when the two disagree. A play that cannot resolve is left forplay_problems()to report.>>> from an.ir.compose import flatten, sequence >>> from cutan.characters.registration import PlayAction >>> flats = flatten(sequence( ... PlayAction(target="ned", animation="turn", args={"to": "side", "direction": "left"}), ... PlayAction(target="ned", animation="turn", args={"to": "back"}))) >>> res = resolve_turns(flats, descriptor_of=lambda e: None, ... rest_of=lambda p: {"scale_x": 1.0}) >>> res.flats[1].action.args["from_direction"], res.turns[1].before ('left', Facing(view='side', direction='left'))- Return type:
- cutan.characters.play.sampled_deviations(track, duration, fps)[source]¶
(time, deviation)pairs for a sine bone track at the frame rate —cutan.characters.idle.evaluate_track()’s formula, sampled, so the descriptor’s own evaluator stays the one definition of a sine track.
- cutan.characters.play.sine_sample_times(duration, fps)[source]¶
Frame-rate sample times for a sine track, ALWAYS ending at
duration.ceilrather thanround: withround, a 0.18 s track at 24 fps got samples up to 0.1667 s and then held that value to the clip end, so the cycle-closing sample (equal to the first) was never emitted and the clip wrapped with a jump (an#7 review).>>> sine_sample_times(0.19, 24)[-2:] [0.16666666666666666, 0.19] >>> len(sine_sample_times(6.0, 24)) 145
- cutan.characters.play.slot_node_path(desc, slot_name)[source]¶
The node path of a slot RELATIVE to its entity (
head/left_eye,torso) — the rig builder’s nesting rule, stated once.>>> slot_node_path(CharacterDescriptor(name="m"), "left_eye") 'head/left_eye' >>> slot_node_path(CharacterDescriptor(name="m"), "torso") 'torso'- Return type:
- cutan.characters.play.slot_parent(desc, slot)[source]¶
The slot
slotnests under, orNonewhen it is a direct child: the stage’s one rule,an.stage.rig.slot_parent_chain(nestingflatorbones, an#340).
- cutan.characters.play.suppressed_slots(desc)[source]¶
Slots the rig builder never builds: with the face baked into the head art (
face_overlay=false), every slot on the HEAD BONE but its primary slot (the head itself) — keyed on the bone, not on a slot named “head”. The compile pass hands them toan.stage.rig.build_rig_subtreeasskip_slots.>>> sorted(suppressed_slots(CharacterDescriptor(name="m", face_overlay=False))) ['left_brow', 'left_eye', 'mouth', 'right_brow', 'right_eye'] >>> suppressed_slots(CharacterDescriptor(name="m")) frozenset()