an.stage.props#
Props: a rig whose art is not a person.
A lamp, a sword, a sign. Structurally a prop is what a character already is — bones, slots, skins, attachments, swap sets — and the cutout compiler builds both with the same rig builder, which is why an#108 made the art store an argument rather than writing a second one.
What differs is the descriptor, and the difference is entirely in the defaults:
|
|
|---|---|
seven-bone humanoid |
one |
face, eyes, brows, mouth |
one |
|
no animations |
viseme + eyelid |
none |
|
no face at all |
**Why not CharacterDescriptor with kind: "prop".** Three measured reasons,
each of which turns a one-field change into a silent wrong render:
CharacterDescriptor.model_post_initre-seeds the humanoid skeleton, the face slots, the default skin andidle_breath/blinkfrom an empty list — soCharacterDescriptor(name="sword")is a seven-bone person with a blinking face, not an empty rig.The compiler’s placeholder fallback draws a person where a lamp should be (the an#33 failure mode), so a prop whose art fails to resolve renders as a humanoid rather than as nothing.
an character validatescores a correct prop at 22 blocking findings —REQUIRED_PARTS(12) plusMOUTH_SHAPES(9), none of which a lamp has, plus “has no character.json”, which is the tool saying out loud that it was handed the wrong kind of document. (Measured ontests/fixtures/props/lamp/; an earlier draft of this docstring said 21, having added the two lists without running the tool.)
**Why not a new minimal document with states.** That is asset_sets
renamed: it would need a rename table at the compiler boundary and would cap a
prop at one moving piece. A prop reuses the swap-channel machinery instead, so
a two-state lamp is asset_sets={"lamp": {"off": ..., "on": ...}} and
set lamp on in the scene — the same words a character’s viseme swap uses.
Module Attributes
the bones' rest pose ( |
|
Its own versioned document, registered from the module that owns the schema — the same rule |
Functions
|
One bone, at the CENTRE of the view_box (an#408). |
One slot on that bone. |
Classes
|
The on-disk prop schema. |
- an.stage.props.PROP_DOCUMENT_KIND: DocumentKind = DocumentKind(name='PropDescriptor', version_field='schema_version', current_version='0.2.0')#
Its own versioned document, registered from the module that owns the schema — the same rule
CharacterDescriptorfollows, and the reason the migration registry is keyed per KIND: two documents at0.1.0that migrate differently is exactly the collision an#77 fixed.
- an.stage.props.PROP_SCHEMA_VERSION = '0.2.0'#
the bones’ rest pose (
rotation_deg,scale_*) poses the built parts; the migration protects a 0.1.0 rig that carried one.- Type:
0.2.0 (an#339)
- class an.stage.props.PropDescriptor(**data)[source]#
Bases:
RigDocumentThe on-disk prop schema. Saved as
prop.json.>>> p = PropDescriptor(name="lamp") >>> p.kind 'PropDescriptor' >>> [b.name for b in p.bones], [s.name for s in p.slots] (['root'], ['body'])
A prop is not seeded with a face, a skeleton or an idle animation — the three things
CharacterDescriptorfills in from an empty list:>>> p.animations, p.asset_sets, p.skins ({}, {}, {})
Two states are the swap-channel machinery a character’s viseme already uses, not a second vocabulary:
>>> lamp = PropDescriptor( ... name="lamp", ... skins={"default": Skin(slots={"body": { ... "off": Attachment(path="parts/off.svg"), ... "on": Attachment(path="parts/on.svg"), ... }})}, ... asset_sets={"lamp": {"off": "off", "on": "on"}}, ... ) >>> sorted(lamp.asset_sets["lamp"]) ['off', 'on'] >>> back = PropDescriptor.model_validate_json(lamp.model_dump_json()) >>> back.skins["default"].slots["body"]["on"].path 'parts/on.svg'
A prop stands where it is put by its declared
origin(an#338), in view_box units; unset, by the centre of its bones’ extent, and the stored document does not mention it:>>> PropDescriptor(name="tripod", origin=(512, 1010)).origin (512.0, 1010.0) >>> "origin" in PropDescriptor(name="lamp").model_dump() False
- animations: dict[str, Any]#
Present so
playand the rig builder read the same attribute on either descriptor. Empty by default — a prop has noidle_breathand noblink, and seeding one would animate a lamp.
- asset_sets: dict[str, dict[str, str]]#
{channel: {key: attachment_name}}— the same indirection a character uses for visemes. Empty by default: a prop with no moving parts declares none, and declaring a channel a rig cannot serve is what makes a swap silently keep the previous texture.
- face_overlay: Literal[True]#
Always true, and not a knob.
face_overlay=Falsemeans “the face is baked into the head art”, which makes the builder suppress every slot nested under the head bone’s primary slot. A prop has no head bone, so the flag can only do harm; it exists because the shared builder reads it.
- model_config: ClassVar[ConfigDict] = {'extra': 'allow', 'populate_by_name': True}#
Configuration for the model, should be a dictionary conforming to [
ConfigDict][pydantic.config.ConfigDict].
- model_post_init(_PropDescriptor__context)[source]#
Override this method to perform additional initialization after
__init__andmodel_construct. This is useful if you want to do some validation that requires the entire model to be initialized.- Return type:
- source: AssetSource | None#
Where this art came from and what its licence obliges.
Nonemeans “we made this” — not “unknown”. Same field asCharacterDescriptor, becausean creditsshould not need to know which store it came from.
- an.stage.props.default_prop_bones(view_box=(0, 0, 1024, 1024))[source]#
One bone, at the CENTRE of the view_box (an#408).
A part is centred on its bone (anchor 0.5), so with the bone at the centre a full-canvas part’s art coordinates ARE the rig’s: an
originread off the drawing (a foot at(512, 1000)in a 1024 box) lands where it says. Before an#408 the bone sat at(0, 0)and an origin had to be measured from the art’s centre. Nothing moves for a prop without anorigin: it is placed by its bones’ extent centre, which is this bone wherever it is.>>> [(b.name, b.x, b.y) for b in default_prop_bones()] [('root', 512.0, 512.0)]