Characters#

What a puppet must declare, and how a declared puppet becomes a posed figure in panel coordinates. The solver sees only this contract, never artwork.

scenet.assets.contract#

.. py:module:: scenet.assets.contract

What a character puppet must declare.

The solver never sees artwork – only this contract. That is what keeps rendering swappable: the same panel lays out identically whether it is drawn as wireframe boxes, as vector puppets, or eventually as real artwork.

A character is a skeleton plus parametric limbs rather than a picture, so a pose is a set of joint angles and not a drawing. That avoids the combinatorial explosion of one image per pose per expression per facing direction.

.. py:class:: AnchorSpec

module:

scenet.assets.contract

Bases: :py:class:~scenet.assets.contract.Strict

A named point that rides along with a joint.

Anchors are how the solver addresses anatomy without knowing anatomy: the balloon tail terminates at mouth, and it neither knows nor cares how the head is drawn.

.. py:attribute:: AnchorSpec.joint

module:

scenet.assets.contract

type:

str

.. py:attribute:: AnchorSpec.offset

module:

scenet.assets.contract

type:

tuple[float, float]

.. py:class:: BlobPart

module:

scenet.assets.contract

Bases: :py:class:~scenet.assets.contract.Strict

A rounded mass – the head, or a torso – centred on a joint.

.. py:attribute:: BlobPart.at

module:

scenet.assets.contract

type:

str

.. py:attribute:: BlobPart.radius

module:

scenet.assets.contract

type:

float

.. py:attribute:: BlobPart.offset

module:

scenet.assets.contract

type:

tuple[float, float]

.. py:class:: BonePart

module:

scenet.assets.contract

Bases: :py:class:~scenet.assets.contract.Strict

A limb segment drawn as a capsule between two joints.

.. py:attribute:: BonePart.from_joint

module:

scenet.assets.contract

type:

str

.. py:attribute:: BonePart.to_joint

module:

scenet.assets.contract

type:

str

.. py:attribute:: BonePart.width

module:

scenet.assets.contract

type:

float

.. py:class:: BrowState

module:

scenet.assets.contract

Bases: :py:class:~enum.StrEnum

What the eyebrows are doing.

angled_in puts the inner ends down, which is the anger brow; angled_out puts them up, which is the sad or frightened one. Inner and outer are resolved against the face centre rather than the screen, so both survive mirroring.

.. py:attribute:: BrowState.NEUTRAL

module:

scenet.assets.contract

value:

‘neutral’

.. py:attribute:: BrowState.RAISED

module:

scenet.assets.contract

value:

‘raised’

.. py:attribute:: BrowState.LOWERED

module:

scenet.assets.contract

value:

‘lowered’

.. py:attribute:: BrowState.ANGLED_IN

module:

scenet.assets.contract

value:

‘angled_in’

.. py:attribute:: BrowState.ANGLED_OUT

module:

scenet.assets.contract

value:

‘angled_out’

.. py:method:: BrowState.new(value)

module:

scenet.assets.contract

.. py:class:: ExpressionSpec

module:

scenet.assets.contract

Bases: :py:class:~scenet.assets.contract.Strict

One named expression, as a state per feature.

An expression is to features what a pose is to joints: a record the panel selects by name. A face deforms rather than rotating about bones, which is why this is a set of states and not a set of angles – a nose joint would swing a nose.

Every field defaults, so a neutral expression is {} and a happy one is {mouth: smile}. Unknown keys are rejected, so mouth: raised – a real state, on the wrong feature – is an error rather than a silently ignored line.

.. py:attribute:: ExpressionSpec.brow

module:

scenet.assets.contract

type:

~scenet.assets.contract.BrowState

.. py:attribute:: ExpressionSpec.eyes

module:

scenet.assets.contract

type:

~scenet.assets.contract.EyeState

.. py:attribute:: ExpressionSpec.mouth

module:

scenet.assets.contract

type:

~scenet.assets.contract.MouthState

.. py:class:: EyeState

module:

scenet.assets.contract

Bases: :py:class:~enum.StrEnum

How open the eyes are.

half is the heavy-lidded eye of boredom, distinct from narrowed, which is the squint of anger or suspicion: one is drooping, the other is tightened.

.. py:attribute:: EyeState.OPEN

module:

scenet.assets.contract

value:

‘open’

.. py:attribute:: EyeState.WIDE

module:

scenet.assets.contract

value:

‘wide’

.. py:attribute:: EyeState.NARROWED

module:

scenet.assets.contract

value:

‘narrowed’

.. py:attribute:: EyeState.HALF

module:

scenet.assets.contract

value:

‘half’

.. py:attribute:: EyeState.CLOSED

module:

scenet.assets.contract

value:

‘closed’

.. py:method:: EyeState.new(value)

module:

scenet.assets.contract

.. py:class:: FaceSpec

module:

scenet.assets.contract

Bases: :py:class:~scenet.assets.contract.Strict

The region a balloon may never cover, and what is drawn inside it.

A circle rather than a polygon: faces are roughly round, the test is cheap, and the cost of being slightly generous here is a balloon placed a little further away, which is never wrong.

features is optional. A puppet that declares none is drawn exactly as it was before faces existed – a head is a filled circle – and a puppet that declares some has those drawn and no others. There is no requirement to declare all of them, because a stylised character genuinely may have no eyebrows.

.. py:attribute:: FaceSpec.joint

module:

scenet.assets.contract

type:

str

.. py:attribute:: FaceSpec.radius

module:

scenet.assets.contract

type:

float

.. py:attribute:: FaceSpec.offset

module:

scenet.assets.contract

type:

tuple[float, float]

.. py:attribute:: FaceSpec.features

module:

scenet.assets.contract

type:

dict[~scenet.assets.contract.Feature, ~scenet.assets.contract.FeatureSpec]

.. py:class:: Feature

module:

scenet.assets.contract

Bases: :py:class:~enum.StrEnum

The points on a face that an expression can move.

Named after the groups of the MPEG-4 FBA facial definition parameters – brow, eye, nose, mouth – rather than its full point set. The standard defines 66 displacements over dozens of points, which is a measurement rather than a notation; the grouping is the part worth reusing, and it lands at about the right size for a drawn face. docs/reference/asset_contract.md records the mapping to MediaPipe landmark indices, which is what buys convertibility later without importing 478 points into the language now.

Two omissions are deliberate. Pupils are derived, not declared – they are offset inside the eye by where the character is looking, so authoring them would be authoring something the compiler already knows. And there is no jaw: the head is a circle that does not deform, so a jaw group would have no geometry to move.

.. py:attribute:: Feature.BROW_L

module:

scenet.assets.contract

value:

‘brow_l’

.. py:attribute:: Feature.BROW_R

module:

scenet.assets.contract

value:

‘brow_r’

.. py:attribute:: Feature.EYE_L

module:

scenet.assets.contract

value:

‘eye_l’

.. py:attribute:: Feature.EYE_R

module:

scenet.assets.contract

value:

‘eye_r’

.. py:attribute:: Feature.NOSE

module:

scenet.assets.contract

value:

‘nose’

.. py:attribute:: Feature.MOUTH

module:

scenet.assets.contract

value:

‘mouth’

.. py:property:: Feature.is_paired

module:

scenet.assets.contract

type:

bool

Whether this feature is one of a left/right pair.

.. py:property:: Feature.twin

module:

scenet.assets.contract

type:

~scenet.assets.contract.Feature | None

The other half of a left/right pair, or None for a single feature.

.. py:method:: Feature.new(value)

module:

scenet.assets.contract

.. py:class:: FeatureSpec

module:

scenet.assets.contract

Bases: :py:class:~scenet.assets.contract.Strict

Where one facial feature sits, and how big it is.

Structurally an :class:AnchorSpec <scenet.assets.contract.AnchorSpec> with a size, and resolved through exactly the same forward kinematics – but declared under face rather than in anchors, deliberately. Anchors are how the solver addresses anatomy: the balloon tail terminates at mouth and the solver never learns how a head is drawn. Features are artwork. Mixing them would put drawing landmarks into the one namespace that is supposed to be free of them.

.. attribute:: joint

Which joint this feature rides. Practically always head.

.. attribute:: offset

Rest-pose displacement from that joint, in native units.

.. attribute:: size

What the number means depends on the feature – an eye’s radius, a brow’s half-width, a mouth’s half-width, a nose’s length. Zero means the feature has no extent and is drawn as a bare point, which is rarely what anybody wants.

.. py:attribute:: FeatureSpec.joint

module:

scenet.assets.contract

type:

str

.. py:attribute:: FeatureSpec.offset

module:

scenet.assets.contract

type:

tuple[float, float]

.. py:attribute:: FeatureSpec.size

module:

scenet.assets.contract

type:

float

.. py:class:: GazeSpec

module:

scenet.assets.contract

Bases: :py:class:~scenet.assets.contract.Strict

Where a character’s line of sight starts.

.. attribute:: origin

Name of a declared anchor, conventionally eyes. Validated to exist.

The direction is not stored: it is derived at solve time from whom the character is looking at, so a looking_at relation is enough and nobody has to compute an angle by hand.

.. py:attribute:: GazeSpec.origin

module:

scenet.assets.contract

type:

str

.. py:class:: JointSpec

module:

scenet.assets.contract

Bases: :py:class:~scenet.assets.contract.Strict

One joint in the skeleton.

offset is the rest-pose displacement from the parent joint, in native units. A joint’s pose angle rotates the bone arriving at it and everything below it, which is the formulation that makes posing read naturally: bending elbow_l swings the upper arm and takes the forearm and hand with it.

.. py:attribute:: JointSpec.parent

module:

scenet.assets.contract

type:

str | None

.. py:attribute:: JointSpec.offset

module:

scenet.assets.contract

type:

tuple[float, float]

.. py:class:: Landmark

module:

scenet.assets.contract

Bases: :py:class:~enum.StrEnum

Vertical body landmarks, measured downward from the top of the head.

These are the crop lines a shot type names – see docs/reference/shot_types.md.

.. py:attribute:: Landmark.HEAD_TOP

module:

scenet.assets.contract

value:

‘head_top’

.. py:attribute:: Landmark.EYES

module:

scenet.assets.contract

value:

‘eyes’

.. py:attribute:: Landmark.CHIN

module:

scenet.assets.contract

value:

‘chin’

.. py:attribute:: Landmark.SHOULDERS

module:

scenet.assets.contract

value:

‘shoulders’

.. py:attribute:: Landmark.CHEST

module:

scenet.assets.contract

value:

‘chest’

.. py:attribute:: Landmark.WAIST

module:

scenet.assets.contract

value:

‘waist’

.. py:attribute:: Landmark.MID_THIGH

module:

scenet.assets.contract

value:

‘mid_thigh’

.. py:attribute:: Landmark.KNEES

module:

scenet.assets.contract

value:

‘knees’

.. py:attribute:: Landmark.FEET

module:

scenet.assets.contract

value:

‘feet’

.. py:method:: Landmark.new(value)

module:

scenet.assets.contract

.. py:class:: MouthState

module:

scenet.assets.contract

Bases: :py:class:~enum.StrEnum

What the mouth is doing.

grin is an open smile, smile a closed one, and small the reticent little mouth that does most of the work in a coy face.

.. py:attribute:: MouthState.NEUTRAL

module:

scenet.assets.contract

value:

‘neutral’

.. py:attribute:: MouthState.FLAT

module:

scenet.assets.contract

value:

‘flat’

.. py:attribute:: MouthState.SMILE

module:

scenet.assets.contract

value:

‘smile’

.. py:attribute:: MouthState.GRIN

module:

scenet.assets.contract

value:

‘grin’

.. py:attribute:: MouthState.FROWN

module:

scenet.assets.contract

value:

‘frown’

.. py:attribute:: MouthState.OPEN

module:

scenet.assets.contract

value:

‘open’

.. py:attribute:: MouthState.SMALL

module:

scenet.assets.contract

value:

‘small’

.. py:method:: MouthState.new(value)

module:

scenet.assets.contract

.. py:class:: PuppetLibrary

module:

scenet.assets.contract

Bases: :py:class:object

Puppets loaded from a directory of *.puppet.yaml files.

.. py:method:: PuppetLibrary.init(puppets)

module:

scenet.assets.contract

Wrap an already-loaded mapping of puppets.

type puppets:
sphinx_autodoc_typehints_type:

\:py\:class\:\dict`\ \[:py:class:`str`, :py:class:`~scenet.assets.contract.PuppetSpec`]`

param puppets:

Puppet name to specification. Usually built by

meth:

from_directory <scenet.assets.contract.PuppetLibrary.from_directory> rather than passed in directly – but constructing one by hand is how you supply your own characters without touching the filesystem.

.. py:method:: PuppetLibrary.from_directory(directory)

module:

scenet.assets.contract

classmethod:
Load every `*.puppet.yaml` in a directory.

:type directory: :sphinx_autodoc_typehints_type:`\:py\:class\:\`\~pathlib.Path\``
:param directory: Directory to scan. Not searched recursively.

:rtype: :sphinx_autodoc_typehints_type:`\:py\:class\:\`\~typing.Self\``
:returns: A library containing every puppet found.

:raises ValueError: Two files declare the same puppet name.
:raises AssetError: A file is not a YAML mapping.

Files are visited in sorted order so that a duplicate-name collision reports the
same offender on every platform, whatever order the filesystem hands them back.

.. py:method:: PuppetLibrary.get(name) :module: scenet.assets.contract

Look up one puppet by name.

:type name: :sphinx_autodoc_typehints_type:`\:py\:class\:\`str\``
:param name: The name a cast member's `reference` field points at.

:rtype: :sphinx_autodoc_typehints_type:`\:py\:class\:\`\~scenet.assets.contract.PuppetSpec\``
:returns: That puppet's specification.

:raises UnknownPuppetError: No puppet by that name. The message lists what is

available, because the usual cause is a typo.

.. py:method:: PuppetLibrary.names() :module: scenet.assets.contract

Every puppet name in this library, sorted.

.. admonition:: Example

   >>> from scenet import default_library
   >>> default_library().names()
   ('alice', 'bob')

:rtype: :sphinx_autodoc_typehints_type:`\:py\:class\:\`tuple\`\\ \\\[\:py\:class\:\`str\`\, \:py\:data\:\`...\<Ellipsis\>\`\]`

.. py:class:: PuppetSpec

module:

scenet.assets.contract

Bases: :py:class:~scenet.assets.contract.Strict

The complete geometric contract for one character.

.. py:attribute:: PuppetSpec.name

module:

scenet.assets.contract

type:

str

.. py:attribute:: PuppetSpec.units_per_head

module:

scenet.assets.contract

type:

float

.. py:attribute:: PuppetSpec.landmarks

module:

scenet.assets.contract

type:

dict[~scenet.assets.contract.Landmark, float]

.. py:attribute:: PuppetSpec.joints

module:

scenet.assets.contract

type:

dict[str, ~scenet.assets.contract.JointSpec]

.. py:attribute:: PuppetSpec.root

module:

scenet.assets.contract

type:

str

.. py:attribute:: PuppetSpec.root_landmark

module:

scenet.assets.contract

type:

~scenet.assets.contract.Landmark

.. py:attribute:: PuppetSpec.parts

module:

scenet.assets.contract

type:

tuple[~scenet.assets.contract.BonePart | ~scenet.assets.contract.BlobPart, …]

.. py:attribute:: PuppetSpec.anchors

module:

scenet.assets.contract

type:

dict[str, ~scenet.assets.contract.AnchorSpec]

.. py:attribute:: PuppetSpec.face

module:

scenet.assets.contract

type:

~scenet.assets.contract.FaceSpec

.. py:attribute:: PuppetSpec.gaze

module:

scenet.assets.contract

type:

~scenet.assets.contract.GazeSpec

.. py:attribute:: PuppetSpec.poses

module:

scenet.assets.contract

type:

dict[str, dict[str, float]]

.. py:attribute:: PuppetSpec.expressions

module:

scenet.assets.contract

type:

dict[str, ~scenet.assets.contract.ExpressionSpec]

.. py:method:: PuppetSpec.check_landmarks_complete_and_ordered()

module:

scenet.assets.contract

Require every landmark, in head-to-foot order.

rtype:

:sphinx_autodoc_typehints_type:\:py\:class\:\~typing.Self``

returns:

The validated puppet.

raises ValueError:

A landmark is missing, head_top is not zero, or the values do not increase downward.

All nine landmarks are required rather than optional-with-defaults because any shot type may crop at any of them, so a puppet missing one is a puppet that cannot be framed at some perfectly ordinary shot.

.. py:method:: PuppetSpec.check_skeleton_is_a_tree()

module:

scenet.assets.contract

Require the skeleton to be a tree rooted at root.

rtype:

:sphinx_autodoc_typehints_type:\:py\:class\:\~typing.Self``

returns:

The validated puppet.

raises ValueError:

The root is undefined or has a parent, a joint names a parent that does not exist, or a joint sits in a cycle.

Forward kinematics accumulates each joint’s transform from its parent’s. A cycle would make that non-terminating and an orphan would leave a limb with no defined position, so both are rejected here rather than discovered at pose time.

.. py:method:: PuppetSpec.check_joint_references()

module:

scenet.assets.contract

Require every joint name mentioned anywhere to exist.

Covers parts, anchors, the face, the gaze origin, and every angle in every declared pose.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~typing.Self``

returns:

The validated puppet.

raises ValueError:

Something references a joint or anchor that is not declared.

.. py:method:: PuppetSpec.check_face_is_drawable()

module:

scenet.assets.contract

Require paired features to come in pairs, and expressions to be usable.

rtype:

:sphinx_autodoc_typehints_type:\:py\:class\:\~typing.Self``

returns:

The validated puppet.

raises ValueError:

One half of a left/right pair is declared without the other, a puppet declares expressions but no features for them to move, or it declares expressions without a neutral one.

The neutral check is not pedantry. CastMember.expression defaults to neutral, so a puppet that declares expressions without one fails on every panel that does not name an expression explicitly – which is most of them.

.. py:property:: PuppetSpec.total_height

module:

scenet.assets.contract

type:

float

Head top to feet, in the puppet’s own native units.

.. py:property:: PuppetSpec.heads_tall

module:

scenet.assets.contract

type:

float

Height in head-heights – the classic figure-drawing proportion.

The unit the camera works in. Two puppets of different heads_tall framed at the same shot produce figures of visibly different build, which is the whole reason the shipped library has a 7.5-head character and a taller one.

.. py:method:: PuppetSpec.pose_angles(pose)

module:

scenet.assets.contract

Look up the joint angles for a named pose.

type pose:

:sphinx_autodoc_typehints_type:\:py\:class\:\str``

param pose:

Name of a pose this puppet declares.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\dict`\ \[:py:class:`str`, :py:class:`float`]`

returns:

Joint name to angle in degrees. Joints absent from the mapping keep their rest angle.

raises UnknownPoseError:

This puppet has no pose by that name. The message lists the ones it does have. Also a KeyError, so except KeyError keeps working.

.. py:method:: PuppetSpec.expression_states(expression)

module:

scenet.assets.contract

Look up the feature states for a named expression.

The counterpart of :meth:pose_angles <scenet.assets.contract.PuppetSpec.pose_angles>, and deliberately identical in shape down to the error: an expression is selected by name exactly as a pose is.

type expression:

:sphinx_autodoc_typehints_type:\:py\:class\:\str``

param expression:

Name of an expression this puppet declares.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.assets.contract.ExpressionSpec``

returns:

The states its features take.

raises UnknownExpressionError:

This puppet has no expression by that name. The message lists the ones it does have. Also a KeyError, so except KeyError keeps working.

.. admonition:: Example

from scenet import default_library alice = default_library().get(“alice”) alice.expression_states(“angry”).mouth.value ‘frown’

.. py:class:: Strict

module:

scenet.assets.contract

Bases: :py:class:~pydantic.main.BaseModel

Base for every puppet model: frozen, and rejecting unknown keys.

A misspelled key in a puppet file that was silently ignored would produce a character that is subtly wrong – an arm the wrong length, an anchor in the wrong place – with nothing to point at.

.. py:function:: default_library()

module:

scenet.assets.contract

Load the puppets shipped with Scenet.

Two characters of deliberately different build, so that a bug in camera scaling cannot hide behind two figures that happen to be the same height.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.assets.contract.PuppetLibrary``

returns:

A library containing alice and bob.

.. admonition:: Example

from scenet import default_library library = default_library() round(library.get(“alice”).heads_tall, 1) 7.5

.. py:function:: load_puppet(path)

module:

scenet.assets.contract

Read one *.puppet.yaml file into a validated specification.

type path:
sphinx_autodoc_typehints_type:

\:py\:class\:\~pathlib.Path``

param path:

The puppet file to read.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.assets.contract.PuppetSpec``

returns:

The validated puppet, ready to be posed.

raises AssetError:

The file is not a YAML mapping.

raises pydantic.ValidationError:

The mapping is not a well-formed puppet – an out-of-order landmark, a skeleton that is not a tree, a joint referring to a parent that does not exist.

.. admonition:: Example

from scenet import default_library, load_puppet from scenet.assets.contract import DEFAULT_LIBRARY_PATH alice = load_puppet(DEFAULT_LIBRARY_PATH / “alice.puppet.yaml”) alice.name ‘alice’ round(alice.heads_tall, 1) 7.5

.. seealso::

meth:

PuppetLibrary.from_directory <scenet.assets.contract.PuppetLibrary.from_directory>, to read a whole directory at once.

scenet.assets.kinematics#

.. py:module:: scenet.assets.kinematics

Forward kinematics: skeleton plus pose, resolved into concrete geometry.

Everything downstream – camera scaling, actor placement, balloon avoidance, tail routing, rendering – consumes the output of this module and nothing else from the asset layer. That boundary is the whole point: swap the puppet for hand-drawn artwork exposing the same anchors and hulls, and layout is unchanged.

.. py:class:: ResolvedCapsule

module:

scenet.assets.kinematics

Bases: :py:class:object

A limb segment: a thick line with rounded ends.

.. py:attribute:: ResolvedCapsule.start

module:

scenet.assets.kinematics

type:

~scenet.geom.Point

.. py:attribute:: ResolvedCapsule.end

module:

scenet.assets.kinematics

type:

~scenet.geom.Point

.. py:attribute:: ResolvedCapsule.width

module:

scenet.assets.kinematics

type:

float

.. py:method:: ResolvedCapsule.init(start, end, width)

module:

scenet.assets.kinematics

.. py:class:: ResolvedBlob

module:

scenet.assets.kinematics

Bases: :py:class:object

A rounded mass – head, hand, foot – after posing.

.. attribute:: centre

Where it sits, in panel coordinates.

.. attribute:: radius

Radius in panel units, already scaled by the camera.

.. py:attribute:: ResolvedBlob.centre

module:

scenet.assets.kinematics

type:

~scenet.geom.Point

.. py:attribute:: ResolvedBlob.radius

module:

scenet.assets.kinematics

type:

float

.. py:method:: ResolvedBlob.init(centre, radius)

module:

scenet.assets.kinematics

.. py:class:: ResolvedFeature

module:

scenet.assets.kinematics

Bases: :py:class:object

One facial feature point after posing.

.. attribute:: centre

Where it sits, in panel coordinates.

.. attribute:: size

Its extent in panel units, already scaled. What the number measures depends on the feature – see

class:

FeatureSpec <scenet.assets.contract.FeatureSpec>.

.. py:attribute:: ResolvedFeature.centre

module:

scenet.assets.kinematics

type:

~scenet.geom.Point

.. py:attribute:: ResolvedFeature.size

module:

scenet.assets.kinematics

type:

float

.. py:method:: ResolvedFeature.init(centre, size)

module:

scenet.assets.kinematics

.. py:class:: ResolvedPuppet

module:

scenet.assets.kinematics

Bases: :py:class:object

A posed figure in panel coordinates.

Produced once per actor per compile, then treated as read-only by every consumer.

.. py:attribute:: ResolvedPuppet.name

module:

scenet.assets.kinematics

type:

str

.. py:attribute:: ResolvedPuppet.pose

module:

scenet.assets.kinematics

type:

str

.. py:attribute:: ResolvedPuppet.facing_right

module:

scenet.assets.kinematics

type:

bool

.. py:attribute:: ResolvedPuppet.scale

module:

scenet.assets.kinematics

type:

float

.. py:attribute:: ResolvedPuppet.joints

module:

scenet.assets.kinematics

type:

dict[str, ~scenet.geom.Point]

.. py:attribute:: ResolvedPuppet.anchors

module:

scenet.assets.kinematics

type:

dict[str, ~scenet.geom.Point]

.. py:attribute:: ResolvedPuppet.landmarks

module:

scenet.assets.kinematics

type:

dict[~scenet.assets.contract.Landmark, float]

.. py:attribute:: ResolvedPuppet.capsules

module:

scenet.assets.kinematics

type:

tuple[~scenet.assets.kinematics.ResolvedCapsule, …]

.. py:attribute:: ResolvedPuppet.blobs

module:

scenet.assets.kinematics

type:

tuple[~scenet.assets.kinematics.ResolvedBlob, …]

.. py:attribute:: ResolvedPuppet.face

module:

scenet.assets.kinematics

type:

~scenet.geom.Circle

.. py:attribute:: ResolvedPuppet.gaze

module:

scenet.assets.kinematics

type:

~scenet.geom.Vector

.. py:attribute:: ResolvedPuppet.hull

module:

scenet.assets.kinematics

type:

tuple[~scenet.geom.Point, …]

.. py:attribute:: ResolvedPuppet.expression

module:

scenet.assets.kinematics

type:

str

.. py:attribute:: ResolvedPuppet.features

module:

scenet.assets.kinematics

type:

dict[~scenet.assets.contract.Feature, ~scenet.assets.kinematics.ResolvedFeature]

.. py:property:: ResolvedPuppet.bounds

module:

scenet.assets.kinematics

type:

~scenet.geom.BBox

Axis-aligned bounds of the posed silhouette.

.. py:method:: ResolvedPuppet.anchor(name)

module:

scenet.assets.kinematics

Look up one named attachment point in panel coordinates.

type name:
sphinx_autodoc_typehints_type:

\:py\:class\:\str``

param name:

An anchor the puppet declared – mouth and eyes are the ones the compiler itself relies on.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.geom.Point``

returns:

Where that anchor ended up after posing, scaling and mirroring.

raises KeyError:

This puppet declares no anchor by that name.

.. py:method:: ResolvedPuppet.init(name, pose, facing_right, scale, joints, anchors, landmarks, capsules, blobs, face, gaze, hull, expression=’neutral’, features=)

module:

scenet.assets.kinematics

.. py:function:: solve_pose(spec, pose)

module:

scenet.assets.kinematics

Resolve joint positions in the puppet’s own units, root at the origin.

Returns the joint positions and the accumulated world angle at each joint, the latter being what anchors and gaze need in order to ride along with rotation.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\tuple`\ \[:py:class:`dict`\ \[:py:class:`str`, :py:class:`~scenet.geom.Point`], :py:class:`dict`\ \[:py:class:`str`, :py:class:`float`]]`

.. py:function:: resolve(spec, *, pose, facing_right, scale, origin, expression=’neutral’)

module:

scenet.assets.kinematics

Pose, mirror, scale and place a puppet.

origin is where the puppet’s root joint lands in panel coordinates. Mirroring happens in the puppet’s own frame before scaling, so a mirrored figure is the exact reflection of the original rather than being offset by rounding.

expression selects which face is drawn. It is validated and carried here, but the states it names are not applied until the marks are built – feature points are where the anatomy is, and are the same whatever the face is doing.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.assets.kinematics.ResolvedPuppet``

.. py:function:: convex_hull(points)

module:

scenet.assets.kinematics

Andrew’s monotone chain, returning hull vertices counter-clockwise.

Implemented here rather than delegated to shapely because it runs on a handful of points per actor and the result must be bit-for-bit reproducible; shapely is reserved for the genuinely hard polygon work in balloon placement.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\tuple`\ \[:py:class:`~scenet.geom.Point`, :py:data:`…<Ellipsis>`]`

scenet.assets.emanata#

.. py:module:: scenet.assets.emanata

Drawing emanata: the marks around a character that say what state they are in.

Plewds, squeans, grawlixes and briffits – Mort Walker’s names, from The Lexicon of Comicana. See :class:Mark <scenet.ir.Mark> for what each one says.

Like a face, this is artwork, and lives in the asset layer for the same reason. Unlike a face, it is drawn outside the head circle, in the space balloons are placed in, so it has to say something to the solver. What it says is a zone per mark: a convex polygon around everything that mark draws. The solver reads the zones as a soft cost and never sees a plewd. Replace these drawings with hand-drawn ones that occupy the same zones and no layout changes.

Emanata never enter the hull. The hull is what staging spaces characters by, so a sweating character would otherwise stand further from everyone else, and the camera might retreat to fit them – a mark that moved the people in a panel would be saying something the author never wrote.

Everything is placed from the face circle, the facing direction and the feet landmark, which every puppet has. So marks need nothing from a puppet beyond the contract it already meets, and are not declared per puppet the way expressions are.

Level of detail follows the face’s rule. A plewd at long_shot is a dot: below MIN_EMANATA_DETAIL, each symbol collapses to a filled dot where it would have been drawn. Below the face’s own MIN_FEATURE_RADIUS, nothing is drawn – a character too small to have a face is too small to be sweating.

.. py:data:: MIN_EMANATA_DETAIL

module:

scenet.assets.emanata

value:

60.0

Face radius in panel units below which emanata are drawn as dots rather than as shapes. Calibrated by eye against scripts/contact_sheet.py --marks: in a 1000-unit panel, full_shot (a face radius of about 75) still draws a plewd you can tell is a drop, and long_shot (about 51) does not.

.. py:data:: STROKE_FRACTION

module:

scenet.assets.emanata

value:

0.035

Stroke width, as a fraction of the face radius. Lighter than a face’s own lines: these are small symbols, and drawn at face weight they clot into blots.

.. py:data:: ZONE_PADDING

module:

scenet.assets.emanata

value:

0.05

How far a zone reaches past what it encloses, as a fraction of the face radius, on top of the stroke width. Enough that a balloon cannot sit flush against a mark.

.. py:data:: ZONE_SAMPLES

module:

scenet.assets.emanata

value:

8

Points sampled around each disc and each padded point when a zone is built. Eight is plenty: the zone feeds a soft cost, not a hard edge.

.. py:data:: DROP_SAMPLES

module:

scenet.assets.emanata

value:

11

points along its rounded end. Odd, so the samples are symmetric about the drop’s own axis and a mirrored figure’s sweat is the exact reflection of the original.

type:

How a drop is sampled

.. py:data:: SINGULAR

module:

scenet.assets.emanata

type:

dict[~scenet.ir.Mark, str]

value:

{Mark.BRIFFITS: ‘briffit’, Mark.GRAWLIXES: ‘grawlix’, Mark.PLEWDS: ‘plewd’, Mark.SQUEANS: ‘squean’}

The id each mark’s primitives are numbered under, plewd_0, plewd_1, …

.. py:class:: ResolvedEmanata

module:

scenet.assets.emanata

Bases: :py:class:object

Every mark drawn around one character, and the space they take up.

.. attribute:: marks

The drawing, as the same strokes and discs a face is made of, in a fixed order – by mark, then by position – so the same marks always serialise to the same bytes.

.. attribute:: zones

One convex polygon per mark, enclosing everything it draws. This is the whole of what the solver sees.

.. py:attribute:: ResolvedEmanata.marks

module:

scenet.assets.emanata

type:

tuple[~scenet.assets.face.ResolvedStroke | ~scenet.assets.face.ResolvedDisc, …]

.. py:attribute:: ResolvedEmanata.zones

module:

scenet.assets.emanata

type:

tuple[tuple[~scenet.geom.Point, …], …]

.. py:method:: ResolvedEmanata.init(marks=(), zones=())

module:

scenet.assets.emanata

.. py:function:: build_emanata(puppet, marks)

module:

scenet.assets.emanata

Draw the marks around one character.

type puppet:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.assets.kinematics.ResolvedPuppet``

param puppet:

The posed figure. Its face circle, facing and feet are all that is read.

type marks:
sphinx_autodoc_typehints_type:

\:py\:class\:\tuple`\ \[:py:class:`~scenet.ir.Mark`, :py:data:`…<Ellipsis>`]`

param marks:

Which marks to draw. Order does not matter.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.assets.emanata.ResolvedEmanata``

returns:

The drawing and its zones, or nothing at all when there are no marks or the figure is too small for them to read.

.. admonition:: Example

from scenet import compile_source core = compile_source( … “{camera: {shot: medium_shot}, cast: {a: {reference: alice, marks: [plewds]}}}” … ).core core.actor(“a”).emanata[0].id ‘plewd_0’