an.expression.binding
How the axes reach a character: the binding and the mouth-set resolver (an#98).
Renderer-free, like an.characters.play — an validate,
an character validate and the cutout face solver all call the same
functions here, so the three cannot disagree about whether an expression can
resolve on a character.
A channel binding maps a numeric axis onto
(slot, property, gain): the solver emitsrest + Σ axis·gainon that slot’s node. The brow angle’s per-side sign lives in the gain — the two sides rotate in opposite screen directions for one axis sign.A set binding maps a lid axis onto a slot’s swap set (
eyelid); the solver reads a key off the ladder inan.expression.axes.resolve_mouth_setis the ONE chain for “which mouth set does this line use”:viseme@<form>if declared and it covers the keys the line uses, elsevisemewith a warning naming the missing keys, else anExpressionResolutionError(a speaking overlay face with no neutral mouth set).
>>> from an.characters.schema import CharacterDescriptor
>>> desc = CharacterDescriptor(name="m")
>>> sorted({b.axis for b in default_binding(desc)})
['brow_angle_l', 'brow_angle_r', 'brow_height_l', 'brow_height_r', 'lid_open_l', 'lid_open_r']
>>> resolve_mouth_set(desc, None, keys_used=["A", "X"])
'viseme'
>>> import warnings
>>> with warnings.catch_warnings(record=True) as w:
... warnings.simplefilter("always")
... resolve_mouth_set(desc, "happy", keys_used=["A", "X"])
'viseme'
>>> "viseme@happy" in str(w[0].message)
True
- an.expression.binding.BROW_ANGLE_TRAVEL: float = 0.35
Brow rotation per unit of brow_angle_*, radians. Art direction.
- an.expression.binding.BROW_HEIGHT_TRAVEL: float = 10.0
Brow travel per unit of brow_height_*, in the rig’s view-box units (scaled to scene pixels by the entity’s rig factor). Art direction; about the synthesized eye’s half-height.
- class an.expression.binding.ChannelBinding(axis: str, slot: str, property: str, gain: float, rig_scaled: bool = False)[source]
A numeric axis driving one transform property of one slot’s node.
- rig_scaled: bool = False
Whether the gain is a view-box length (scaled by the rig factor).
- exception an.expression.binding.ExpressionResolutionError(who: str, problems: Iterable[str])[source]
An expression that cannot resolve on a character;
problemssays why.
- an.expression.binding.GAZE_TRAVEL: float = 6.0
Pupil travel per unit of gaze_*, in view-box units — the default when a descriptor declares no travel of its own (add_gaze writes gaze_travel).
- an.expression.binding.LID_SQUASH_GAIN: float = 0.5
On a rig whose eye squashes instead of swapping art, a lid offset scales the eye by this much per unit.
- class an.expression.binding.SetBinding(axis: str, slot: str, set_family: str = 'eyelid')[source]
A lid axis driving one slot’s swap set through the ladder.
- an.expression.binding.binding_for(desc: CharacterDescriptor) list[ChannelBinding | SetBinding][source]
The descriptor’s declared
expression_binding(additive field), else the default.A declared binding is a list of dicts in the two dataclasses’ shapes (
{"axis", "slot", "property", "gain"[, "rig_scaled"]}or{"axis", "slot", "set_family"}). An unknown axis in it is an error.
- an.expression.binding.declared_mouth_variants(desc: CharacterDescriptor) dict[str, str][source]
{form: set name}for everyviseme@<form>set the descriptor declares.>>> declared_mouth_variants(CharacterDescriptor(name="m")) {}
- an.expression.binding.default_binding(desc: CharacterDescriptor) list[ChannelBinding | SetBinding][source]
The binding the default rig implies, from the slots it actually has.
The brow angle’s screen sign: PixiJS rotation is clockwise-positive with y down, so on the LEFT brow (screen-left) a clockwise turn drops the inner end while on the RIGHT brow it lifts it — the axis says “+ = inner end up”, hence
-travelon the left and+travelon the right.
- an.expression.binding.expression_problems(desc: CharacterDescriptor | None, *, preset: str | None, axes: Collection[str] = (), who: str) list[str][source]
Every reason an expression cannot resolve on
desc— empty means it can.Shared by
an validate(each becomes an error Finding) and the compiler (which raisesExpressionResolutionErrorwith the same list).>>> expression_problems(CharacterDescriptor(name="m"), preset="joyful", who="m") ["unknown expression preset 'joyful' (known: neutral, happy, sad, angry, surprised, afraid, disgusted, thinking, skeptical, amused)"] >>> expression_problems(CharacterDescriptor(name="m", face_overlay=False), preset="happy", who="m")[0].startswith("'m' has its face baked") True
- an.expression.binding.preset_axes(preset: str | None, *, axes: Mapping[str, float] | None = None, intensity: float = 1.0) dict[str, float][source]
The numeric axis offsets an expression asks for: the preset’s, with
axeslayered over them, scaled byintensityand clamped. Only non-zero offsets are returned, so a neutral expression is{}.An unknown preset or axis is a
ValueError— validate reports it as an error, the compiler refuses it.
- an.expression.binding.resolve_mouth_set(desc: CharacterDescriptor, preset: str | None, *, keys_used: Collection[str], who: str | None = None) str[source]
Which mouth set a line under
presetuses — the one chain, shared.viseme@<form>if the preset prefers a form the descriptor declares and that set coverskeys_used; elsevisemewith a warning naming what was missing; elseExpressionResolutionError. A descriptor with novisemeset and no covering variant cannot speak at all — that is the error, not a fallback.