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:: FaceSpec

module:

scenet.assets.contract

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

The region a balloon may never cover.

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.

.. 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: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:: 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: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: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 KeyError:

This puppet has no pose by that name. The message lists the ones it does have.

.. 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:: 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: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)

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)

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.

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>`]`