cutan.expression.binding¶
How the axes reach a character: the binding and the mouth-set resolver (an#98).
Renderer-free, like cutan.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 incutan.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 neutralmouth set).
>>> from cutan.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
Module Attributes
Brow travel per unit of |
|
Brow rotation per unit of |
|
Pupil travel per unit of |
|
On a rig whose eye squashes instead of swapping art, a lid offset scales the eye by this much per unit. |
Functions
|
The descriptor's declared |
|
|
|
The binding the default rig implies, from the slots it actually has. |
|
Every reason an expression cannot resolve on |
|
The numeric axis offsets an expression asks for: the preset's, with |
|
Why |
|
What an expression's lids ask for that |
|
Which mouth set a line under |
|
Whether any of |
|
The swap-set name for a mouth form ( |
Classes
|
A numeric axis driving one transform property of one slot's node. |
|
A lid axis driving one slot's swap set through the ladder. |
Exceptions
|
An expression that cannot resolve on a character; |
- cutan.expression.binding.BROW_ANGLE_TRAVEL: float = 0.35¶
Brow rotation per unit of
brow_angle_*, radians. Art direction.
- cutan.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 cutan.expression.binding.ChannelBinding(axis, slot, property, gain, rig_scaled=False)[source]¶
Bases:
objectA numeric axis driving one transform property of one slot’s node.
- exception cutan.expression.binding.ExpressionResolutionError(who, problems)[source]¶
Bases:
ValueErrorAn expression that cannot resolve on a character;
problemssays why.
- cutan.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_gazewritesgaze_travel).
- cutan.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 cutan.expression.binding.SetBinding(axis, slot, set_family='eyelid')[source]¶
Bases:
objectA lid axis driving one slot’s swap set through the ladder.
- cutan.expression.binding.binding_for(desc)[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.- Return type:
- cutan.expression.binding.declared_mouth_variants(desc)[source]¶
{form: set name}for everyviseme@<form>set the descriptor declares.>>> declared_mouth_variants(CharacterDescriptor(name="m")) {}
- cutan.expression.binding.default_binding(desc)[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.- Return type:
- cutan.expression.binding.expression_problems(desc, *, preset, axes=(), who)[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
- cutan.expression.binding.lid_rung_problems(desc, preset, *, axes=None, intensity=1.0, who=None)[source]¶
What an expression’s lids ask for that
desc’s eyelid art cannot show (an#272): a rig whoseeyelidset drawsOPENandCLOSEDpicks a DRAWING per lid state (the ladder: wide, open, half, closed), so a partial lid on a rig with noHALFdrawing showsOPEN, the same picture as no expression at all. One sentence per lid axis that falls short; empty for a preset with no lid, a rig whose lids squash (no closed art: continuous), or a baked face.>>> d = CharacterDescriptor(name="bob", asset_sets={"eyelid": {"OPEN": "o", "CLOSED": "c"}}) >>> lid_rung_problems(d, None, axes={"lid_open_l": -0.45}) ["bob's eyelids have no 'HALF' drawing, so lid_open_l -0.45 shows 'OPEN' (its eyelid set: CLOSED, OPEN): add a HALF eyelid drawing to the set (`an character add-half-lid bob` makes one from the rig's own lids), or use -0.85 or lower to close the lid"] >>> lid_rung_problems(d, None, axes={"lid_open_l": -0.9}) []
- cutan.expression.binding.missing_mouth_form(desc, preset, *, who=None)[source]¶
Why
preset’s mouth form will not show ondesc— the descriptor declares noviseme@<form>set, so the mouth stays on the neutral chart, silent or speaking (an#253) — with the command that adds it;Nonewhen the preset has no form, the set exists, or there is no overlay mouth to change (a baked face, a rig with novisemeset).One sentence for
an validate(anexpressionand a dialogue[emotion]) and the compiler’s silent-mouth hold.>>> d = CharacterDescriptor(name="ned", asset_sets={"viseme": {"X": "mouth_x"}}) >>> missing_mouth_form(d, "angry") "ned declares no 'viseme@angry' set, so under 'angry' its mouth keeps the neutral chart, silent or speaking: add it with `an character mouths ned --variants angry`" >>> missing_mouth_form(d, "thinking") is None True
- cutan.expression.binding.preset_axes(preset, *, axes=None, intensity=1.0)[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.
- cutan.expression.binding.resolve_mouth_set(desc, preset, *, keys_used, who=None)[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.- Return type: