The language#

The intermediate representation: a validated semantic scene graph, and the real definition of the language. Nothing here carries a coordinate.

scenet.ir#

.. py:module:: scenet.ir

The intermediate representation: a validated semantic scene graph.

This is the language’s real definition. The YAML surface syntax is one way to produce it; a comic-script frontend will be another. Nothing here carries a coordinate – computing those is the solver’s job.

Validation is strict on purpose. Panel sources are untrusted input, and a typo in a predicate or an actor id should be a clear error at parse time rather than a silently wrong picture.

.. py:class:: AnchorX

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

Where along the panel width an actor would like to stand.

Horizontal only. Actors stand on a ground line, so their vertical position is derived from the camera rather than requested – which is why this has no vertical counterpart and :class:PlacementZone <scenet.ir.PlacementZone>, used for balloons, does.

These are weak preferences. Non-overlap and declared left-to-right ordering are required constraints and will override an anchor without complaint; two actors both asking for center will simply be pushed apart around it.

.. py:attribute:: AnchorX.LEFT_EDGE

module:

scenet.ir

value:

‘left_edge’

.. py:attribute:: AnchorX.LEFT_THIRD

module:

scenet.ir

value:

‘left_third’

.. py:attribute:: AnchorX.CENTRE

module:

scenet.ir

value:

‘center’

.. py:attribute:: AnchorX.RIGHT_THIRD

module:

scenet.ir

value:

‘right_third’

.. py:attribute:: AnchorX.RIGHT_EDGE

module:

scenet.ir

value:

‘right_edge’

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

module:

scenet.ir

.. py:class:: BalloonKind

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

What kind of balloon carries a line, which is how it gets drawn.

The kind changes the outline and the tail, never the placement: a whisper is subject to exactly the same face-avoidance and reading-order rules as a shout.

Kind

Outline

Tail

speech

plain ellipse

tapered pointer

thought

scalloped cloud

trail of bubbles

whisper

dashed ellipse

tapered pointer

shout

jagged burst

tapered pointer

.. py:attribute:: BalloonKind.SPEECH

module:

scenet.ir

value:

‘speech’

.. py:attribute:: BalloonKind.THOUGHT

module:

scenet.ir

value:

‘thought’

.. py:attribute:: BalloonKind.WHISPER

module:

scenet.ir

value:

‘whisper’

.. py:attribute:: BalloonKind.SHOUT

module:

scenet.ir

value:

‘shout’

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

module:

scenet.ir

.. py:class:: CameraAngle

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

The camera’s height relative to the subject.

Affects headroom rather than perspective: this is a flat, orthographic compiler, so a tilted camera does not foreshorten anything. What it changes is how much air sits above the head – which is the compositional cue readers actually take from an angle, and one that survives being drawn flat.

A low camera looks up and the subject looms, so the head rides high in the frame with little space above it. A high camera looks down, so the head sits lower and more space opens up above. See

func:

headroom_for <scenet.solve.camera.headroom_for> for the exact factors.

.. py:attribute:: CameraAngle.LOW

module:

scenet.ir

value:

‘low’

.. py:attribute:: CameraAngle.EYE_LEVEL

module:

scenet.ir

value:

‘eye_level’

.. py:attribute:: CameraAngle.HIGH

module:

scenet.ir

value:

‘high’

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

module:

scenet.ir

.. py:class:: CameraSpec

module:

scenet.ir

Bases: :py:class:~scenet.ir.Strict

How the panel is framed.

.. attribute:: shot

Requested framing; an upper bound on tightness, see

class:

ShotType <scenet.ir.ShotType>.

.. attribute:: angle

Camera height, see :class:CameraAngle <scenet.ir.CameraAngle>.

There is exactly one camera per panel, and every actor is drawn at the scale it implies. Scaling each actor to its own crop landmark instead would make everybody the same apparent height and erase the body differences a comic uses to tell characters apart.

.. py:attribute:: CameraSpec.shot

module:

scenet.ir

type:

~scenet.ir.ShotType

.. py:attribute:: CameraSpec.angle

module:

scenet.ir

type:

~scenet.ir.CameraAngle

.. py:class:: CaptionEvent

module:

scenet.ir

Bases: :py:class:~scenet.ir.Strict

One caption box: the panel speaking in its own voice.

A caption is what lets a panel say where and when it happens without a character having to explain it out loud. MIDNIGHT. THE DOCKS. in the corner does the work of an establishing shot with no artwork at all, which is how comics established place long before they had reliable backgrounds.

.. attribute:: verb

Always caption, written as - caption: {...}.

.. attribute:: text

What the box says. As with dialogue, line breaking is computed.

.. attribute:: kind

What the box is doing, which decides how it is set.

.. attribute:: tone

What the box is filled with. Defaults to paper, the white every caption has been since captions shipped, so no existing panel moves.

.. attribute:: prefer

Where it would like to sit. Defaults to top_left, which is where a locale caption conventionally goes.

.. attribute:: by

Who is speaking, for a spoken caption only.

A caption is not a fifth balloon kind. It has no speaker to point at and no tail, and :attr:CoreBalloon.tail <scenet.core.CoreBalloon.tail> is required – a fifth kind would mean inventing a speaker and leaving a field dead.

by is the one place where the rule that every actor id resolves does not hold, and deliberately: an off-panel speaker is not in the panel, so requiring them to be in the cast would defeat the point of saying they are off panel.

.. admonition:: Example

from scenet.ir import CaptionEvent CaptionEvent(text=”Midnight. The docks.”).kind.value ‘locale’

.. py:attribute:: CaptionEvent.verb

module:

scenet.ir

type:

~typing.Literal[‘caption’]

.. py:attribute:: CaptionEvent.text

module:

scenet.ir

type:

str

.. py:attribute:: CaptionEvent.kind

module:

scenet.ir

type:

~scenet.ir.CaptionKind

.. py:attribute:: CaptionEvent.tone

module:

scenet.ir

type:

~scenet.ir.CaptionTone

.. py:attribute:: CaptionEvent.prefer

module:

scenet.ir

type:

~scenet.ir.PlacementZone

.. py:attribute:: CaptionEvent.by

module:

scenet.ir

type:

str | None

.. py:method:: CaptionEvent.check_speaker_is_meaningful()

module:

scenet.ir

Only an off-panel line has a speaker to name.

rtype:

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

returns:

The validated event.

raises ValueError:

by was given for a kind that has no speaker. A locale box states a place; nobody says it, so naming who did is a mistake worth reporting rather than a field to ignore.

.. py:class:: CaptionKind

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

What a caption box is doing, which is how it gets set.

These four are the letterers’ own vocabulary, taken from Blambot’s Comic Book Grammar & Tradition rather than invented – for the same reason the predicates were taken from Visual Genome. Note that “narration”, the obvious guess, is not among them.

Kind

What it is

How it is set

locale

Location and time – “Midnight. The docks.”

Italic

monologue

A character’s inner voice

Italic

spoken

Off-panel dialogue

Roman, in quotation marks

editorial

The voice of the writer or editor

Italic

monologue has largely replaced the thought balloon in modern comics, so a panel has two ways to render an inner voice: this and

attr:

BalloonKind.THOUGHT <scenet.ir.BalloonKind>. Both are correct. They are different eras of the same convention, not a duplication.

.. py:attribute:: CaptionKind.LOCALE

module:

scenet.ir

value:

‘locale’

.. py:attribute:: CaptionKind.MONOLOGUE

module:

scenet.ir

value:

‘monologue’

.. py:attribute:: CaptionKind.SPOKEN

module:

scenet.ir

value:

‘spoken’

.. py:attribute:: CaptionKind.EDITORIAL

module:

scenet.ir

value:

‘editorial’

.. py:property:: CaptionKind.is_italic

module:

scenet.ir

type:

bool

Whether this kind is set in italic. Everything except spoken.

.. py:property:: CaptionKind.is_quoted

module:

scenet.ir

type:

bool

Whether this kind takes quotation marks.

spoken only, because it is the one kind where somebody is talking.

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

module:

scenet.ir

.. py:class:: CaptionTone

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

The value a caption box is filled with.

A caption box is opaque, so its lettering is never at risk: the text sits on the fill whatever is behind it. What a tone changes is whether the box reads. On a clear day the atmosphere is #eeeeee and a white box on it is 1.16:1 – legible, and invisible. The failing case is the pale end, not the dark one.

Tone

Fill

Where it comes from

Lettered in

paper

#ffffff

the paper the panel is printed on

ink

pale

#adadad

the day row of the value ladder, far plane

ink

ink

#090909

the day row of the value ladder, foreground

paper

Drawn from the ladder in solve/backdrop.py, not from a second palette. Two of the three are rungs of it, taken by index rather than restated, which is what keeps lettering and backdrop from drifting apart as either is tuned. The day row is the ladder at its widest, so tones taken from it span the most ground.

A tone is fixed, not a function of the panel’s hour. A caption’s value is a property of the caption; letting it drift with time would make the contrast table a function of the panel and the legibility floor unenforceable.

ink produces what letterers call reversed type: the lettering inverts to paper, because black type on a near-black box is not lettering, it is a filled rectangle. Which mark reads is resolved by the solver – see

attr:

CoreCaption.ink <scenet.core.CoreCaption> – exactly as falling rain’s is.

There is no free-form colour here, and no yellow. A fill: taking any string would be the language’s one open vocabulary and would let an author produce an unreadable box; the classic yellow locale caption would be the first non-neutral value in the codebase, and the language has no colour policy yet to put it under.

.. py:attribute:: CaptionTone.PAPER

module:

scenet.ir

value:

‘paper’

.. py:attribute:: CaptionTone.PALE

module:

scenet.ir

value:

‘pale’

.. py:attribute:: CaptionTone.INK

module:

scenet.ir

value:

‘ink’

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

module:

scenet.ir

.. py:class:: CastMember

module:

scenet.ir

Bases: :py:class:~scenet.ir.Strict

One character present in the panel.

.. attribute:: reference

Name of a puppet in the library. This is what gets drawn; the key this member is filed under in cast is the actor id used everywhere else.

.. attribute:: pose

Named pose from that puppet’s declared set.

.. attribute:: expression

Named expression from that puppet’s declared set. Selected by name exactly as a pose is, because a face is the same kind of thing as a body: a small closed set of arrangements the character can be in.

.. attribute:: marks

Emanata drawn around the character – sweat, dizziness, swearing, a hasty exit. A list, because they compose with each other and with the expression. Kept sorted, so the order they were written in never changes the output.

.. attribute:: at

Preferred horizontal anchor.

.. attribute:: facing

Which way the figure is turned.

The split between actor id and reference is what lets one puppet appear twice in a panel as two different people:

cast:
  guard_left:  {reference: bob, pose: arms_crossed}
  guard_right: {reference: bob, pose: standing_neutral, facing: left}

.. py:attribute:: CastMember.reference

module:

scenet.ir

type:

str

.. py:attribute:: CastMember.pose

module:

scenet.ir

type:

str

.. py:attribute:: CastMember.expression

module:

scenet.ir

type:

str

.. py:attribute:: CastMember.marks

module:

scenet.ir

type:

tuple[~scenet.ir.Mark, …]

.. py:attribute:: CastMember.at

module:

scenet.ir

type:

~scenet.ir.AnchorX

.. py:attribute:: CastMember.facing

module:

scenet.ir

type:

~scenet.ir.Facing

.. py:method:: CastMember.check_marks(marks)

module:

scenet.ir

classmethod:

Reject a mark listed twice, and put the rest in a fixed order.

rtype:
sphinx_autodoc_typehints_type:

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

returns:

The marks, sorted.

raises ValueError:

A mark appears more than once. Harmless to draw, but almost certainly a slip, and this language reports slips.

.. py:class:: Facing

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

Which way an actor is turned.

Mirroring the whole puppet, gaze vector included. Defaults to right, so a cast written left to right ends up looking into the panel rather than out of it.

.. py:attribute:: Facing.LEFT

module:

scenet.ir

value:

‘left’

.. py:attribute:: Facing.RIGHT

module:

scenet.ir

value:

‘right’

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

module:

scenet.ir

.. py:class:: Horizon

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

Where the ground meets whatever is behind it.

One line for the whole panel, which every mass is composed against: masses of the ground sort start at it and run down, masses that stand in the world rise from it. Named rather than given as a number for the same reason at: is – the author is saying how the panel is composed, not typing a coordinate.

.. py:attribute:: Horizon.HIGH

module:

scenet.ir

value:

‘high’

.. py:attribute:: Horizon.MID

module:

scenet.ir

value:

‘mid’

.. py:attribute:: Horizon.LOW

module:

scenet.ir

value:

‘low’

.. py:property:: Horizon.fraction

module:

scenet.ir

type:

float

Where the line sits, as a fraction of panel height.

A high horizon sits nearer the top of the frame, so more ground is in view and the camera reads as looking down over it.

.. admonition:: Example

from scenet.ir import Horizon Horizon.HIGH.fraction < Horizon.LOW.fraction True

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

module:

scenet.ir

.. py:class:: Mark

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

Something drawn around a character, rather than on them, to say how they are.

The vocabulary is Mort Walker’s, from The Lexicon of Comicana (1980), which grew out of his 1964 National Cartoonists Society piece “Let’s Get Down to Grawlixes”. The book is tongue-in-cheek, but the terms entered real use, and they are comics’ own names for comics’ own conventions. So the set is closed and citable, and nothing in it is invented here.

Mark

What it is

What it says

plewds

Droplets flying off the head

sweating: effort, heat, nerves

squeans

Little starbursts and circles over the head

dizzy, drunk, or sick

grawlixes

Symbols over the head standing in for words

swearing

briffits

A dust cloud left at the feet

gone, fast

“Emanata” is Walker’s general term for these, which is why it names the module that draws them and the Core field that holds them.

A mark is not an expression. A character can be angry and sweating, so marks are a list that composes with expression: rather than a second one. Grawlixes are drawn as symbols – a jarn (spiral), a nittle (bursting star), a bolt and a hash – rather than typed: an oath written as @#$%! already works, as dialogue.

All four are drawn outside the head circle, in the space balloons are placed in. They never move a character. A balloon prefers not to cover them, and covers them anyway rather than fail when a panel is too crowded to oblige.

.. py:attribute:: Mark.PLEWDS

module:

scenet.ir

value:

‘plewds’

.. py:attribute:: Mark.SQUEANS

module:

scenet.ir

value:

‘squeans’

.. py:attribute:: Mark.GRAWLIXES

module:

scenet.ir

value:

‘grawlixes’

.. py:attribute:: Mark.BRIFFITS

module:

scenet.ir

value:

‘briffits’

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

module:

scenet.ir

.. py:class:: Mass

module:

scenet.ir

Bases: :py:class:~scenet.ir.Strict

One tonal mass in the backdrop: what it is, how far back, how wide.

.. attribute:: kind

What the mass is made of, which decides its silhouette.

.. attribute:: plane

How far back it sits, which decides its value and its draw order.

.. attribute:: spans

How much of the panel’s width it covers.

Backdrops are never author-drawn, and there are two reasons. The structural one: crisp architecture needs a vanishing point, and this is deliberately a flat, orthographic compiler, so drawn buildings would fight the compiler’s own model. Soft tonal masses have no perspective to get wrong.

The second is that this is how comics actually establish place. Notan – the Japanese light/dark mass principle, which reached Western art teaching through Arthur Wesley Dow’s Composition (1899) – says place is read from the arrangement of masses rather than from rendered detail.

.. admonition:: Example

from scenet.ir import Mass, MassKind Mass(kind=MassKind.SKY).plane.value ‘mid’

.. py:attribute:: Mass.kind

module:

scenet.ir

type:

~scenet.ir.MassKind

.. py:attribute:: Mass.plane

module:

scenet.ir

type:

~scenet.ir.Plane

.. py:attribute:: Mass.spans

module:

scenet.ir

type:

~scenet.ir.Spans

.. py:class:: MassKind

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

What a tonal mass in the backdrop is made of.

Subsetted from the supercategories of COCO-Stuff, the canonical taxonomy of stuff – “amorphous background regions” as opposed to things with a well-defined shape. Its own argument is that stuff classes explain scene type and the geometric properties of a scene, which is exactly the job here. Taken from an existing vocabulary for the same reason the predicates were taken from Visual Genome.

Deliberately not the leaf names. COCO-Stuff’s actual classes are building-other, sky-other, wall-brick, water-other and so on, where the -other suffix marks the catch-all inside a supercategory. building-other is not a word anyone should have to type.

Seven are outdoor – building, ground, plant, sky, solid, structural, water – and five indoor: ceiling, floor, furniture, wall, window. COCO-Stuff’s own indoor/outdoor split is where that distinction comes from, so it did not have to be invented either. Its textile, food and rawmaterial supercategories are left out: drapery and objects, not scene-defining masses.

A kind decides the shape a mass takes, never its value. Value comes from the plane, which is what keeps the notan reading honest – see

class:

Plane <scenet.ir.Plane>.

.. py:attribute:: MassKind.BUILDING

module:

scenet.ir

value:

‘building’

.. py:attribute:: MassKind.CEILING

module:

scenet.ir

value:

‘ceiling’

.. py:attribute:: MassKind.FLOOR

module:

scenet.ir

value:

‘floor’

.. py:attribute:: MassKind.FURNITURE

module:

scenet.ir

value:

‘furniture’

.. py:attribute:: MassKind.GROUND

module:

scenet.ir

value:

‘ground’

.. py:attribute:: MassKind.PLANT

module:

scenet.ir

value:

‘plant’

.. py:attribute:: MassKind.SKY

module:

scenet.ir

value:

‘sky’

.. py:attribute:: MassKind.SOLID

module:

scenet.ir

value:

‘solid’

.. py:attribute:: MassKind.STRUCTURAL

module:

scenet.ir

value:

‘structural’

.. py:attribute:: MassKind.WALL

module:

scenet.ir

value:

‘wall’

.. py:attribute:: MassKind.WATER

module:

scenet.ir

value:

‘water’

.. py:attribute:: MassKind.WINDOW

module:

scenet.ir

value:

‘window’

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

module:

scenet.ir

.. py:class:: PanelIR

module:

scenet.ir

Bases: :py:class:~scenet.ir.Strict

A complete, validated panel: the language’s real definition.

Every frontend produces one of these and nothing else, which is what lets the YAML syntax and the comic-script syntax coexist without the solver knowing either exists. Nothing here carries a coordinate – computing those is the solver’s job, and keeping them out is what makes a panel reusable at any size.

.. attribute:: panel

Dimensions and margin.

.. attribute:: camera

Framing and angle.

.. attribute:: cast

Actor id to character. Declaration order is not significant; staging decides left-to-right order.

.. attribute:: staging

Spatial and attentional relations between actors.

.. attribute:: script

Dialogue and captions, in reading order.

Validation is strict and total: unknown keys are rejected, every actor id mentioned in staging or script must exist in cast, and the ordering relations must not contain a cycle. A misspelled key that was silently ignored would produce a panel that is subtly wrong with no indication of why, which for a language meant to be precise is the worst possible failure.

.. admonition:: Example

from scenet import parse_panel panel = parse_panel(“panel: {size: [800.0, 600.0]}”) panel.panel.width, panel.camera.shot.value (800.0, ‘medium_shot’)

.. seealso:: :func:compile_ir <scenet.pipeline.compile_ir>, to turn one of these into geometry.

.. py:attribute:: PanelIR.panel

module:

scenet.ir

type:

~scenet.ir.PanelSpec

.. py:attribute:: PanelIR.camera

module:

scenet.ir

type:

~scenet.ir.CameraSpec

.. py:attribute:: PanelIR.setting

module:

scenet.ir

type:

~scenet.ir.SettingSpec

.. py:attribute:: PanelIR.cast

module:

scenet.ir

type:

dict[str, ~scenet.ir.CastMember]

.. py:attribute:: PanelIR.staging

module:

scenet.ir

type:

tuple[~scenet.ir.Relation, …]

.. py:attribute:: PanelIR.script

module:

scenet.ir

type:

tuple[~scenet.ir.SayEvent | ~scenet.ir.CaptionEvent, …]

.. py:method:: PanelIR.check_references_resolve()

module:

scenet.ir

Every actor id mentioned anywhere must exist in the cast.

Caught here rather than in the solver so the error names the offending identifier while the source is still in view.

rtype:
sphinx_autodoc_typehints_type:

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

.. py:method:: PanelIR.check_ordering_is_consistent()

module:

scenet.ir

Horizontal ordering must not contain a cycle.

The layout engine is a linear constraint solver, so ordering has to be decided before it runs – see docs/reference/language.md. A cycle such as ‘a left_of b, b left_of a’ has no solution, and detecting it here produces a comprehensible message instead of an opaque solver failure.

rtype:
sphinx_autodoc_typehints_type:

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

.. py:method:: PanelIR.ordering_constraints()

module:

scenet.ir

Normalised (left, right) pairs from both left_of and right_of relations.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\tuple`\ \[:py:class:`tuple`\ \[:py:class:`str`, :py:class:`str`], :py:data:`…<Ellipsis>`]`

.. py:method:: PanelIR.gaze_targets()

module:

scenet.ir

Who is looking at whom.

rtype:
sphinx_autodoc_typehints_type:

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

returns:

A mapping from each looking actor to the actor they are looking at. An actor may look at only one target, so a later looking_at relation for the same subject replaces an earlier one.

.. admonition:: Example

from scenet import parse_panel panel = parse_panel( … “{cast: {a: {reference: alice}, b: {reference: bob}},” … “ staging: [a looking_at b]}” … ) panel.gaze_targets() {‘a’: ‘b’}

.. py:method:: PanelIR.ground_groups()

module:

scenet.ir

Actors joined by ground_shared_with, as connected components.

Union-find rather than pairwise handling, so that ‘a with b’ plus ‘b with c’ puts all three on one ground line without the author saying ‘a with c’.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\tuple`\ \[:py:class:`frozenset`\ \[:py:class:`str`], :py:data:`…<Ellipsis>`]`

.. py:class:: PanelSpec

module:

scenet.ir

Bases: :py:class:~scenet.ir.Strict

The panel’s own dimensions.

.. attribute:: size

(width, height) in panel units. Everything else in the language is expressed relative to these, so they set what a unit means.

.. attribute:: margin

Inset on all four sides. Balloons are kept inside it; actors may bleed past it, which is ordinary comics practice.

.. admonition:: Example

from scenet import PanelSpec PanelSpec(size=(1200.0, 600.0)).width 1200.0

.. py:attribute:: PanelSpec.size

module:

scenet.ir

type:

tuple[float, float]

.. py:attribute:: PanelSpec.margin

module:

scenet.ir

type:

float

.. py:property:: PanelSpec.width

module:

scenet.ir

type:

float

Panel width in panel units.

.. py:property:: PanelSpec.height

module:

scenet.ir

type:

float

Panel height in panel units.

.. py:method:: PanelSpec.check_positive()

module:

scenet.ir

Reject a panel with no usable area.

rtype:

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

returns:

The validated spec.

raises ValueError:

A dimension is zero or negative, or the margins meet in the middle leaving nothing to compose in.

.. py:class:: PlacementZone

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

Where in the panel a balloon would prefer to sit.

Two-dimensional, unlike AnchorX: an actor is placed along the ground line and so only needs a horizontal anchor, whereas a balloon floats and needs both axes. These are hints of the weakest priority – occlusion and reading order override them freely.

.. py:attribute:: PlacementZone.TOP_LEFT

module:

scenet.ir

value:

‘top_left’

.. py:attribute:: PlacementZone.TOP_CENTRE

module:

scenet.ir

value:

‘top_center’

.. py:attribute:: PlacementZone.TOP_RIGHT

module:

scenet.ir

value:

‘top_right’

.. py:attribute:: PlacementZone.MIDDLE_LEFT

module:

scenet.ir

value:

‘middle_left’

.. py:attribute:: PlacementZone.MIDDLE_CENTRE

module:

scenet.ir

value:

‘middle_center’

.. py:attribute:: PlacementZone.MIDDLE_RIGHT

module:

scenet.ir

value:

‘middle_right’

.. py:attribute:: PlacementZone.BOTTOM_LEFT

module:

scenet.ir

value:

‘bottom_left’

.. py:attribute:: PlacementZone.BOTTOM_CENTRE

module:

scenet.ir

value:

‘bottom_center’

.. py:attribute:: PlacementZone.BOTTOM_RIGHT

module:

scenet.ir

value:

‘bottom_right’

.. py:property:: PlacementZone.fractions

module:

scenet.ir

type:

tuple[float, float]

The zone’s centre as a fraction of panel width and height.

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

module:

scenet.ir

.. py:class:: Plane

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

How far back a mass sits, which decides both its draw order and its value.

Four planes, ordered from the back of the panel forward. They map onto the existing integer :attr:CoreActor.depth <scenet.core.CoreActor.depth> painter’s order rather than introducing a second ordering mechanism: the three backdrop planes take negative depths, and foreground takes one above the frontmost actor, so a foreground mass draws over the cast the way a silhouetted doorway does.

Value follows from the plane and from nothing else, which is what makes the aerial perspective rule parametric: with distance, contrast drops toward the atmosphere. Reading front to back, a mass never gets darker. See

func:

tone_for <scenet.solve.backdrop.tone_for>.

.. py:attribute:: Plane.FOREGROUND

module:

scenet.ir

value:

‘foreground’

.. py:attribute:: Plane.NEAR

module:

scenet.ir

value:

‘near’

.. py:attribute:: Plane.MID

module:

scenet.ir

value:

‘mid’

.. py:attribute:: Plane.FAR

module:

scenet.ir

value:

‘far’

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

module:

scenet.ir

.. py:class:: Predicate

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

How one actor stands in relation to another.

Drawn from the spatial subset of the Visual Genome vocabulary rather than invented, so a scene stays convertible to and from the scene-graph representations used elsewhere in computer vision.

left_of and right_of are the load-bearing ones: they are resolved at parse time into a linear ordering, because Cassowary cannot express the disjunction “A left of B or B left of A”.

.. py:attribute:: Predicate.LEFT_OF

module:

scenet.ir

value:

‘left_of’

.. py:attribute:: Predicate.RIGHT_OF

module:

scenet.ir

value:

‘right_of’

.. py:attribute:: Predicate.IN_FRONT_OF

module:

scenet.ir

value:

‘in_front_of’

.. py:attribute:: Predicate.BEHIND

module:

scenet.ir

value:

‘behind’

.. py:attribute:: Predicate.LOOKING_AT

module:

scenet.ir

value:

‘looking_at’

.. py:attribute:: Predicate.GROUND_SHARED_WITH

module:

scenet.ir

value:

‘ground_shared_with’

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

module:

scenet.ir

.. py:class:: Relation

module:

scenet.ir

Bases: :py:class:~scenet.ir.Strict

One staging fact, written as a sentence.

.. attribute:: subject

Actor id the sentence is about.

.. attribute:: predicate

What relation holds.

.. attribute:: object

The other actor id.

Authored as alice left_of bob rather than a three-key mapping because staging is read far more often than it is written, and a sentence is legible at a glance.

raises pydantic.ValidationError:

The subject and object are the same actor. No predicate here is meaningful reflexively.

.. py:attribute:: Relation.subject

module:

scenet.ir

type:

str

.. py:attribute:: Relation.predicate

module:

scenet.ir

type:

~scenet.ir.Predicate

.. py:attribute:: Relation.object

module:

scenet.ir

type:

str

.. py:method:: Relation.check_not_reflexive()

module:

scenet.ir

Reject a relation between an actor and itself.

rtype:

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

returns:

The validated relation.

raises ValueError:

Subject and object are the same actor id. No predicate in the language means anything reflexively, so this is always a typo.

.. py:class:: SayEvent

module:

scenet.ir

Bases: :py:class:~scenet.ir.Strict

One line of dialogue.

.. attribute:: verb

Always say. The tag the surface syntax writes as - say: {...}, carried into the model so that a script entry knows which sort of event it is without the frontend having to remember.

.. attribute:: by

Actor id of the speaker; must be in the cast.

.. attribute:: text

What is said. Line breaking is the compiler’s job, so write it as one string and do not insert newlines yourself.

.. attribute:: prefer

Optional hint about where the balloon should sit. The weakest of all the placement terms – face avoidance and reading order override it.

.. attribute:: kind

Which sort of balloon carries it.

Script order is reading order, and reading order is a hard constraint. Reorder these and you reorder the panel.

.. py:attribute:: SayEvent.verb

module:

scenet.ir

type:

~typing.Literal[‘say’]

.. py:attribute:: SayEvent.by

module:

scenet.ir

type:

str

.. py:attribute:: SayEvent.text

module:

scenet.ir

type:

str

.. py:attribute:: SayEvent.prefer

module:

scenet.ir

type:

~scenet.ir.PlacementZone | None

.. py:attribute:: SayEvent.kind

module:

scenet.ir

type:

~scenet.ir.BalloonKind

.. py:class:: SettingSpec

module:

scenet.ir

Bases: :py:class:~scenet.ir.Strict

Where and when the panel happens, as tonal masses rather than drawn geometry.

.. attribute:: horizon

Where the ground meets what is behind it.

.. attribute:: masses

The backdrop, back to front. Written directly, or produced by naming a place in the surface syntax.

.. attribute:: time

When it happens, which shifts the value ladder.

.. attribute:: weather

What the air is doing.

There is no place field here, deliberately. place: docks is surface syntax that the frontend expands into exactly the mass list an author could have written themselves – the same treatment alice left_of bob gets, which reaches the IR as a

class:

Relation <scenet.ir.Relation> and never as text. That is what keeps a preset a library for convenience rather than a second, opaque format: by the time anything downstream sees a backdrop, there is one representation of it.

A panel with no masses and clear weather has no backdrop at all, which is what every panel written before this block existed still gets.

.. admonition:: Example

from scenet import parse_panel panel = parse_panel(“setting: {place: docks, time: night}”) panel.setting.time.value, len(panel.setting.masses) > 0 (‘night’, True)

.. py:attribute:: SettingSpec.horizon

module:

scenet.ir

type:

~scenet.ir.Horizon

.. py:attribute:: SettingSpec.masses

module:

scenet.ir

type:

tuple[~scenet.ir.Mass, …]

.. py:attribute:: SettingSpec.time

module:

scenet.ir

type:

~scenet.ir.TimeOfDay

.. py:attribute:: SettingSpec.weather

module:

scenet.ir

type:

~scenet.ir.Weather

.. py:property:: SettingSpec.is_bare

module:

scenet.ir

type:

bool

no masses, and nothing in the air.

type:

Whether there is nothing to draw

.. py:class:: ShotType

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

How tightly the camera frames the cast.

Ordered from widest to tightest, and the order is enforced by a test: reading down the ladder, the figure never gets smaller.

A shot type is defined by two things in two different units. The crop landmark is anatomical – the waist, the chest, the shoulders – which is what stops a shot type baking in one body and one pose; naming a fraction of panel height instead would do exactly that. The headroom is a plain fraction of panel height, because it is about composition within the frame rather than anatomy. docs/reference/shot_types.md is normative.

The requested shot is an upper bound on tightness, not a promise. If the cast cannot fit across the panel at that framing the camera retreats, and says so in

attr:

CompileResult.notes <scenet.pipeline.CompileResult.notes>.

.. admonition:: Example

from scenet import ShotType ShotType(“close_up”) <ShotType.CLOSE_UP: ‘close_up’>

.. py:attribute:: ShotType.LONG_SHOT

module:

scenet.ir

value:

‘long_shot’

.. py:attribute:: ShotType.WIDE

module:

scenet.ir

value:

‘wide’

.. py:attribute:: ShotType.FULL_SHOT

module:

scenet.ir

value:

‘full_shot’

.. py:attribute:: ShotType.MEDIUM_FULL

module:

scenet.ir

value:

‘medium_full’

.. py:attribute:: ShotType.COWBOY

module:

scenet.ir

value:

‘cowboy’

.. py:attribute:: ShotType.MEDIUM_SHOT

module:

scenet.ir

value:

‘medium_shot’

.. py:attribute:: ShotType.MEDIUM_CLOSE_UP

module:

scenet.ir

value:

‘medium_close_up’

.. py:attribute:: ShotType.CLOSE_UP

module:

scenet.ir

value:

‘close_up’

.. py:attribute:: ShotType.BIG_CLOSE_UP

module:

scenet.ir

value:

‘big_close_up’

.. py:attribute:: ShotType.EXTREME_CLOSE_UP

module:

scenet.ir

value:

‘extreme_close_up’

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

module:

scenet.ir

.. py:class:: Spans

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

How much of the panel’s width a mass covers.

Resolved to an extent in the frontend, and that is not cosmetic. CLAUDE.md requires any construct that would reintroduce a left/right disjunction to resolve it before the solver, because Cassowary cannot express “A left of B or B left of A”. A span is an absolute extent rather than a relation, which is what stops masses becoming an unordered beside.

.. py:attribute:: Spans.FULL

module:

scenet.ir

value:

‘full’

.. py:attribute:: Spans.LEFT

module:

scenet.ir

value:

‘left’

.. py:attribute:: Spans.CENTRE

module:

scenet.ir

value:

‘center’

.. py:attribute:: Spans.RIGHT

module:

scenet.ir

value:

‘right’

.. py:property:: Spans.fractions

module:

scenet.ir

type:

tuple[float, float]

The extent as (start, end) fractions of panel width.

left and right overlap slightly in the middle. Butting them exactly would leave a seam down the centre of the panel wherever both are used at the same plane, which reads as a mistake rather than as two masses.

.. admonition:: Example

from scenet.ir import Spans Spans.FULL.fractions (0.0, 1.0)

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

module:

scenet.ir

.. py:class:: Strict

module:

scenet.ir

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

Reject unknown keys everywhere.

A misspelled key that is silently ignored produces a panel that is subtly wrong with no indication of why, which is the worst possible failure for a language meant to be precise.

.. py:class:: TimeOfDay

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

When the panel happens, which shifts the whole value ladder.

Each time supplies two numbers – the value of the foreground and the value of the atmosphere – and the planes are spaced evenly between them. So night is not a blue filter over a daytime panel: it is a darker, more compressed ladder, which is what night actually does to a drawn scene. The ladder stays monotonic in depth at every time of day, by construction rather than by tuning.

.. py:attribute:: TimeOfDay.DAWN

module:

scenet.ir

value:

‘dawn’

.. py:attribute:: TimeOfDay.DAY

module:

scenet.ir

value:

‘day’

.. py:attribute:: TimeOfDay.DUSK

module:

scenet.ir

value:

‘dusk’

.. py:attribute:: TimeOfDay.NIGHT

module:

scenet.ir

value:

‘night’

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

module:

scenet.ir

.. py:class:: Weather

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

What the air is doing between the reader and the panel.

clouds and fog are first-class stuff in COCO-Stuff, so this vocabulary did not have to be invented either. fog renders as a turbulence veil over the backdrop; rain and snow add that veil as cloud and put falling marks over everything, because weather is between the reader and the figures rather than behind them.

.. py:attribute:: Weather.CLEAR

module:

scenet.ir

value:

‘clear’

.. py:attribute:: Weather.RAIN

module:

scenet.ir

value:

‘rain’

.. py:attribute:: Weather.FOG

module:

scenet.ir

value:

‘fog’

.. py:attribute:: Weather.SNOW

module:

scenet.ir

value:

‘snow’

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

module:

scenet.ir

scenet.places#

The named-place library. A preset expands into a mass list an author could have written themselves, which is what keeps it a library rather than a second, opaque format – and the expansion happens in the frontend, so nothing downstream ever sees a place.

.. py:module:: scenet.places

The place library: named settings, and what each expands into.

The thing an author wants to write is where the scene is, not a list of shapes. So the headline surface is a named place:

   setting:
     place: docks

The rule that keeps that honest: a preset expands into a mass list the author could have written themselves, and is never a second opaque format. A library for convenience, not a parallel language. The expansion happens in the frontend, so by the time anything downstream sees a backdrop there is exactly one representation of it – the same treatment alice left_of bob gets on its way to a

class:

Relation <scenet.ir.Relation>.

Free prose is deliberately not offered. setting: "a rainy street corner at midnight" needs language understanding, and frontends/script_front.py already refuses to interpret prose on the grounds that guessing produces panels that are confidently wrong. A named place is the honest middle: it reads like a description and resolves deterministically.

Within a place, masses are listed back to front. Draw order comes from the plane, so the listing order only decides ties within one plane – but reading the list in the order it will be painted is what makes a preset reviewable.

.. py:data:: PLACES

module:

scenet.places

type:

dict[~scenet.places.Place, tuple[~scenet.ir.Mass, …]]

value:

{Place.ALLEY: (Mass(kind=<MassKind.SKY: ‘sky’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.BUILDING: ‘building’>, plane=<Plane.NEAR: ‘near’>, spans=<Spans.LEFT: ‘left’>), Mass(kind=<MassKind.BUILDING: ‘building’>, plane=<Plane.NEAR: ‘near’>, spans=<Spans.RIGHT: ‘right’>), Mass(kind=<MassKind.GROUND: ‘ground’>, plane=<Plane.NEAR: ‘near’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.BUILDING: ‘building’>, plane=<Plane.FOREGROUND: ‘foreground’>, spans=<Spans.LEFT: ‘left’>)), Place.DESERT: (Mass(kind=<MassKind.SKY: ‘sky’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.SOLID: ‘solid’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.RIGHT: ‘right’>), Mass(kind=<MassKind.GROUND: ‘ground’>, plane=<Plane.MID: ‘mid’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.GROUND: ‘ground’>, plane=<Plane.NEAR: ‘near’>, spans=<Spans.FULL: ‘full’>)), Place.DOCKS: (Mass(kind=<MassKind.SKY: ‘sky’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.BUILDING: ‘building’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.LEFT: ‘left’>), Mass(kind=<MassKind.WATER: ‘water’>, plane=<Plane.MID: ‘mid’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.STRUCTURAL: ‘structural’>, plane=<Plane.MID: ‘mid’>, spans=<Spans.RIGHT: ‘right’>), Mass(kind=<MassKind.GROUND: ‘ground’>, plane=<Plane.NEAR: ‘near’>, spans=<Spans.FULL: ‘full’>)), Place.FIELD: (Mass(kind=<MassKind.SKY: ‘sky’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.PLANT: ‘plant’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.GROUND: ‘ground’>, plane=<Plane.MID: ‘mid’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.GROUND: ‘ground’>, plane=<Plane.NEAR: ‘near’>, spans=<Spans.FULL: ‘full’>)), Place.FOREST: (Mass(kind=<MassKind.SKY: ‘sky’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.PLANT: ‘plant’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.PLANT: ‘plant’>, plane=<Plane.NEAR: ‘near’>, spans=<Spans.LEFT: ‘left’>), Mass(kind=<MassKind.PLANT: ‘plant’>, plane=<Plane.NEAR: ‘near’>, spans=<Spans.RIGHT: ‘right’>), Mass(kind=<MassKind.GROUND: ‘ground’>, plane=<Plane.NEAR: ‘near’>, spans=<Spans.FULL: ‘full’>)), Place.MOUNTAIN: (Mass(kind=<MassKind.SKY: ‘sky’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.SOLID: ‘solid’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.SOLID: ‘solid’>, plane=<Plane.MID: ‘mid’>, spans=<Spans.LEFT: ‘left’>), Mass(kind=<MassKind.PLANT: ‘plant’>, plane=<Plane.MID: ‘mid’>, spans=<Spans.RIGHT: ‘right’>), Mass(kind=<MassKind.GROUND: ‘ground’>, plane=<Plane.NEAR: ‘near’>, spans=<Spans.FULL: ‘full’>)), Place.OFFICE: (Mass(kind=<MassKind.WALL: ‘wall’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.WINDOW: ‘window’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.CENTRE: ‘center’>), Mass(kind=<MassKind.CEILING: ‘ceiling’>, plane=<Plane.MID: ‘mid’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.FLOOR: ‘floor’>, plane=<Plane.NEAR: ‘near’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.FURNITURE: ‘furniture’>, plane=<Plane.NEAR: ‘near’>, spans=<Spans.FULL: ‘full’>)), Place.ROOM: (Mass(kind=<MassKind.WALL: ‘wall’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.WINDOW: ‘window’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.RIGHT: ‘right’>), Mass(kind=<MassKind.CEILING: ‘ceiling’>, plane=<Plane.MID: ‘mid’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.FLOOR: ‘floor’>, plane=<Plane.NEAR: ‘near’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.FURNITURE: ‘furniture’>, plane=<Plane.NEAR: ‘near’>, spans=<Spans.LEFT: ‘left’>)), Place.SHORE: (Mass(kind=<MassKind.SKY: ‘sky’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.WATER: ‘water’>, plane=<Plane.MID: ‘mid’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.GROUND: ‘ground’>, plane=<Plane.NEAR: ‘near’>, spans=<Spans.FULL: ‘full’>)), Place.STREET: (Mass(kind=<MassKind.SKY: ‘sky’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.BUILDING: ‘building’>, plane=<Plane.FAR: ‘far’>, spans=<Spans.FULL: ‘full’>), Mass(kind=<MassKind.BUILDING: ‘building’>, plane=<Plane.MID: ‘mid’>, spans=<Spans.LEFT: ‘left’>), Mass(kind=<MassKind.BUILDING: ‘building’>, plane=<Plane.MID: ‘mid’>, spans=<Spans.RIGHT: ‘right’>), Mass(kind=<MassKind.GROUND: ‘ground’>, plane=<Plane.NEAR: ‘near’>, spans=<Spans.FULL: ‘full’>))}

What each place is made of. Every entry here is a plain mass list, and a test proves it: whatever a preset produces, an author could have typed.

.. py:class:: Place

module:

scenet.places

Bases: :py:class:~enum.StrEnum

A named setting, expanded into masses by the frontend.

Ten to start with, chosen to span the distinctions that change how a backdrop is built rather than to be a catalogue: exterior and interior, built and natural, open and enclosed. alley is the one with a foreground mass, which is what makes it read as a place you are standing in rather than looking at.

.. admonition:: Example

from scenet.places import PLACES, Place [mass.kind.value for mass in PLACES[Place.SHORE]] [‘sky’, ‘water’, ‘ground’]

.. py:attribute:: Place.ALLEY

module:

scenet.places

value:

‘alley’

.. py:attribute:: Place.DESERT

module:

scenet.places

value:

‘desert’

.. py:attribute:: Place.DOCKS

module:

scenet.places

value:

‘docks’

.. py:attribute:: Place.FIELD

module:

scenet.places

value:

‘field’

.. py:attribute:: Place.FOREST

module:

scenet.places

value:

‘forest’

.. py:attribute:: Place.MOUNTAIN

module:

scenet.places

value:

‘mountain’

.. py:attribute:: Place.OFFICE

module:

scenet.places

value:

‘office’

.. py:attribute:: Place.ROOM

module:

scenet.places

value:

‘room’

.. py:attribute:: Place.SHORE

module:

scenet.places

value:

‘shore’

.. py:attribute:: Place.STREET

module:

scenet.places

value:

‘street’

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

module:

scenet.places