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.StrEnumWhere 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
centerwill 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.StrEnumWhat 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
speechplain ellipse
tapered pointer
thoughtscalloped cloud
trail of bubbles
whisperdashed ellipse
tapered pointer
shoutjagged 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.StrEnumThe 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.StrictHow 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.StrictOne 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 alocalecaption conventionally goes... attribute:: by
Who is speaking, for a
spokencaption 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.byis 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:
bywas 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.StrEnumWhat 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
localeLocation and time – “Midnight. The docks.”
Italic
monologueA character’s inner voice
Italic
spokenOff-panel dialogue
Roman, in quotation marks
editorialThe voice of the writer or editor
Italic
monologuehas 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.
spokenonly, 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.StrEnumThe 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
#eeeeeeand 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#ffffffthe paper the panel is printed on
ink
pale#adadadthe
dayrow of the value ladder, far planeink
ink#090909the
dayrow of the value ladder, foregroundpaper
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. Thedayrow 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
timewould make the contrast table a function of the panel and the legibility floor unenforceable.inkproduces 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 yellowlocalecaption 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.StrictOne 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
castis 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
referenceis 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.StrEnumWhich 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.StrEnumWhere 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.StrEnumSomething 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
plewdsDroplets flying off the head
sweating: effort, heat, nerves
squeansLittle starbursts and circles over the head
dizzy, drunk, or sick
grawlixesSymbols over the head standing in for words
swearing
briffitsA 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.StrictOne 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.StrEnumWhat 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-otherand so on, where the-othersuffix marks the catch-all inside a supercategory.building-otheris 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. Itstextile,foodandrawmaterialsupercategories 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.StrictA 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;
stagingdecides 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
stagingorscriptmust exist incast, 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_atrelation 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.StrictThe 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.StrEnumWhere 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.StrEnumHow 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, andforegroundtakes 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.StrEnumHow 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_ofandright_ofare 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.StrictOne 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 bobrather 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.StrictOne 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.StrictWhere 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
placefield here, deliberately.place: docksis surface syntax that the frontend expands into exactly the mass list an author could have written themselves – the same treatmentalice left_of bobgets, 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.StrEnumHow 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.mdis 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.StrEnumHow much of the panel’s width a mass covers.
Resolved to an extent in the frontend, and that is not cosmetic.
CLAUDE.mdrequires 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 unorderedbeside... 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.leftandrightoverlap 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.BaseModelReject 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.StrEnumWhen 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
nightis 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.StrEnumWhat the air is doing between the reader and the panel.
cloudsandfogare first-class stuff in COCO-Stuff, so this vocabulary did not have to be invented either.fogrenders as a turbulence veil over the backdrop;rainandsnowadd 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, andfrontends/script_front.pyalready 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.StrEnumA 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.
alleyis 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