Panel Core#

The resolved intermediate format: fully numeric, still named, and a real file format rather than a private data structure.

scenet.core#

.. py:module:: scenet.core

Panel Core: the resolved intermediate format.

Every position is absolute and numeric, but identifiers survive – which is what separates this from SVG. A Core document can be read, diffed, hand-adjusted and re-emitted.

Golden-file tests target this tier rather than the SVG, because it changes only when layout genuinely changes. Diffing SVG text is brittle: a reordered attribute or a different path-rounding convention produces a huge diff that means nothing.

.. py:class:: Blob

module:

scenet.core

Bases: :py:class:~scenet.core.CoreModel

A rounded mass – a head, a hand – drawn as a filled circle.

.. attribute:: centre

(x, y) of the centre.

.. attribute:: radius

Radius in panel units.

.. py:attribute:: Blob.centre

module:

scenet.core

type:

tuple[float, float]

.. py:attribute:: Blob.radius

module:

scenet.core

type:

float

.. py:class:: Box

module:

scenet.core

Bases: :py:class:~scenet.core.CoreModel

A rectangle, as stored in a Core document.

The serialisable twin of :class:BBox <scenet.geom.BBox>. The geometry code works in BBox; this is what gets written to JSON, with every value already rounded so the file is byte-identical across platforms.

.. attribute:: x

Left edge.

.. attribute:: y

Top edge.

.. attribute:: width

Extent rightward.

.. attribute:: height

Extent downward.

.. py:attribute:: Box.x

module:

scenet.core

type:

float

.. py:attribute:: Box.y

module:

scenet.core

type:

float

.. py:attribute:: Box.width

module:

scenet.core

type:

float

.. py:attribute:: Box.height

module:

scenet.core

type:

float

.. py:property:: Box.right

module:

scenet.core

type:

float

The right edge, x + width.

.. py:property:: Box.bottom

module:

scenet.core

type:

float

The bottom edge, y + height.

.. py:method:: Box.of(bbox)

module:

scenet.core

classmethod:
Build a `Box` from a geometry `BBox`, rounding for emission.

:type bbox: :sphinx_autodoc_typehints_type:`\:py\:class\:\`\~scenet.geom.BBox\``
:param bbox: The box to convert.

:rtype: :sphinx_autodoc_typehints_type:`\:py\:class\:\`\~typing.Self\``
:returns: The serialisable equivalent, with all four values rounded.

.. py:method:: Box.as_bbox() :module: scenet.core

Convert back to a geometry `BBox` for further computation.

:rtype: :sphinx_autodoc_typehints_type:`\:py\:class\:\`\~scenet.geom.BBox\``

.. py:class:: Capsule

module:

scenet.core

Bases: :py:class:~scenet.core.CoreModel

A limb segment, as a thick line with rounded ends.

.. py:attribute:: Capsule.start

module:

scenet.core

type:

tuple[float, float]

.. py:attribute:: Capsule.end

module:

scenet.core

type:

tuple[float, float]

.. py:attribute:: Capsule.width

module:

scenet.core

type:

float

.. py:class:: CoreActor

module:

scenet.core

Bases: :py:class:~scenet.core.CoreModel

One character, fully resolved: placed, posed, scaled and measured.

This is the geometric contract in its final form. Everything the emitter needs to draw the figure, and everything the balloon placer needed to avoid it, with no reference to artwork of any kind.

.. attribute:: id

The actor id from the panel source.

.. attribute:: reference

Which puppet was used. Two actors may share one.

.. attribute:: pose

Which named pose was applied.

.. attribute:: expression

Which named expression was applied.

.. attribute:: transform

Where the root joint landed, and the scale and mirroring applied.

.. attribute:: anchors

Named attachment points – mouth, eyes, and whatever else the puppet declared – already in panel coordinates.

.. attribute:: face_exclusion

The disc no balloon may overlap.

.. attribute:: gaze

Unit direction the character is facing, (dx, dy). Derived from the head’s rotation, and what balloon placement reads to keep out of a line of sight.

.. attribute:: gaze_aim

Unit direction from the eyes to whoever this character is looking_at, or None when they are looking at nobody. Separate from gaze because it is known only after every actor has been placed, and because balloon placement must go on reading the same vector it always has. This is what aims the pupils.

.. attribute:: face_marks

The drawn face – brows, eyes, pupils, nose, mouth – as resolved numeric primitives. Empty when the puppet declares no features, or when the figure is too small for features to read as anything but a smudge.

.. attribute:: marks

Which emanata were asked for – plewds, grawlixes, and so on.

.. attribute:: emanata

The marks drawn around the character, as the same strokes and discs a face is made of. Empty when there are no marks, or when the figure is too small to have a face. Ids are numbered per mark: plewd_0, plewd_1, …

.. attribute:: emanata_zones

One polygon per mark, enclosing what it draws. Balloons and captions pay to cover these and are never forbidden to. Kept apart from hull on purpose: the hull is what staging spaces characters by, and a character who starts sweating must not move.

.. attribute:: hull

Convex silhouette, used for the soft occlusion cost.

.. attribute:: capsules

Limb segments, as thick rounded lines.

.. attribute:: blobs

Rounded masses such as the head.

.. attribute:: depth

Painter’s order. Lower is drawn first, so higher sits in front.

.. py:attribute:: CoreActor.id

module:

scenet.core

type:

str

.. py:attribute:: CoreActor.reference

module:

scenet.core

type:

str

.. py:attribute:: CoreActor.pose

module:

scenet.core

type:

str

.. py:attribute:: CoreActor.transform

module:

scenet.core

type:

~scenet.core.Transform

.. py:attribute:: CoreActor.anchors

module:

scenet.core

type:

dict[str, tuple[float, float]]

.. py:attribute:: CoreActor.face_exclusion

module:

scenet.core

type:

~scenet.core.Disc

.. py:attribute:: CoreActor.gaze

module:

scenet.core

type:

tuple[float, float]

.. py:attribute:: CoreActor.hull

module:

scenet.core

type:

tuple[tuple[float, float], …]

.. py:attribute:: CoreActor.expression

module:

scenet.core

type:

str

.. py:attribute:: CoreActor.gaze_aim

module:

scenet.core

type:

tuple[float, float] | None

.. py:attribute:: CoreActor.face_marks

module:

scenet.core

type:

tuple[~scenet.core.FaceStroke | ~scenet.core.FaceDisc, …]

.. py:attribute:: CoreActor.marks

module:

scenet.core

type:

tuple[~scenet.ir.Mark, …]

.. py:attribute:: CoreActor.emanata

module:

scenet.core

type:

tuple[~scenet.core.FaceStroke | ~scenet.core.FaceDisc, …]

.. py:attribute:: CoreActor.emanata_zones

module:

scenet.core

type:

tuple[tuple[tuple[float, float], …], …]

.. py:attribute:: CoreActor.capsules

module:

scenet.core

type:

tuple[~scenet.core.Capsule, …]

.. py:attribute:: CoreActor.blobs

module:

scenet.core

type:

tuple[~scenet.core.Blob, …]

.. py:attribute:: CoreActor.depth

module:

scenet.core

type:

int

.. py:property:: CoreActor.bounds

module:

scenet.core

type:

~scenet.geom.BBox

Axis-aligned bounds of the silhouette.

.. py:class:: CoreAtmosphere

module:

scenet.core

Bases: :py:class:~scenet.core.CoreModel

What the air is doing, resolved.

.. attribute:: time

When the panel happens. Recorded rather than re-derived, for the same reason CoreCaption.italic is: the emitter must not be able to draw something the solver did not resolve.

.. attribute:: weather

What is falling, if anything.

.. attribute:: tone

The atmosphere’s own value at this hour.

.. attribute:: veil

The noise layer – fog, or cloud for rain and snow.

.. attribute:: streaks

Rain, every streak at the same angle.

.. attribute:: flecks

Snow.

.. attribute:: streak_width

Stroke width for a streak, in panel units.

.. attribute:: fall_tone

What rain and snow are drawn in – ink over a bright sky, paper over a dark one. Resolved rather than left to the emitter, because which one reads is a fact about this panel.

.. py:attribute:: CoreAtmosphere.time

module:

scenet.core

type:

~scenet.ir.TimeOfDay

.. py:attribute:: CoreAtmosphere.weather

module:

scenet.core

type:

~scenet.ir.Weather

.. py:attribute:: CoreAtmosphere.tone

module:

scenet.core

type:

str

.. py:attribute:: CoreAtmosphere.veil

module:

scenet.core

type:

~scenet.core.CoreVeil | None

.. py:attribute:: CoreAtmosphere.streaks

module:

scenet.core

type:

tuple[~scenet.core.CoreStreak, …]

.. py:attribute:: CoreAtmosphere.flecks

module:

scenet.core

type:

tuple[~scenet.core.Disc, …]

.. py:attribute:: CoreAtmosphere.streak_width

module:

scenet.core

type:

float

.. py:attribute:: CoreAtmosphere.fall_tone

module:

scenet.core

type:

str

.. py:class:: CoreBackdrop

module:

scenet.core

Bases: :py:class:~scenet.core.CoreModel

Where the panel is, resolved: masses, tones, and the air.

.. attribute:: horizon

Where the ground meets what is behind it, in panel units.

.. attribute:: seed

What every silhouette in here was generated from. Kept so that a Core document explains itself: two panels with the same masses and different skylines differ here, and here is where to look.

.. attribute:: masses

The tonal masses, back to front.

.. attribute:: atmosphere

The air, or None when the weather is clear.

Optional on :class:PanelCore <scenet.core.PanelCore>, so every document written before this existed is still a valid one – which is why adding it did not need a format_version bump, the same argument captions made.

.. py:attribute:: CoreBackdrop.horizon

module:

scenet.core

type:

float

.. py:attribute:: CoreBackdrop.seed

module:

scenet.core

type:

int

.. py:attribute:: CoreBackdrop.masses

module:

scenet.core

type:

tuple[~scenet.core.CoreMass, …]

.. py:attribute:: CoreBackdrop.atmosphere

module:

scenet.core

type:

~scenet.core.CoreAtmosphere | None

.. py:class:: CoreBalloon

module:

scenet.core

Bases: :py:class:~scenet.core.CoreModel

One balloon, placed and with its lettering already broken into lines.

.. attribute:: id

Stable identifier, b0, b1, … in script order.

.. attribute:: speaker

Actor id of whoever is talking.

.. attribute:: order

Position in reading order, counting from zero.

.. attribute:: kind

Which sort of balloon to draw.

.. attribute:: box

Where it sits.

.. attribute:: lines

The resolved line breaking, not the source string.

.. attribute:: font_size

Type size in panel units.

.. attribute:: line_height

Baseline-to-baseline distance in panel units.

.. attribute:: tail

The pointer to the speaker’s mouth.

Storing broken lines rather than the original string is deliberate. Wrapping is decided during compilation against real font metrics; if the emitter re-measured, it could disagree with the solver about how wide the balloon needed to be, and the text would overflow the shape drawn for it.

.. py:attribute:: CoreBalloon.id

module:

scenet.core

type:

str

.. py:attribute:: CoreBalloon.speaker

module:

scenet.core

type:

str

.. py:attribute:: CoreBalloon.order

module:

scenet.core

type:

int

.. py:attribute:: CoreBalloon.kind

module:

scenet.core

type:

~scenet.ir.BalloonKind

.. py:attribute:: CoreBalloon.box

module:

scenet.core

type:

~scenet.core.Box

.. py:attribute:: CoreBalloon.lines

module:

scenet.core

type:

tuple[str, …]

.. py:attribute:: CoreBalloon.font_size

module:

scenet.core

type:

float

.. py:attribute:: CoreBalloon.line_height

module:

scenet.core

type:

float

.. py:attribute:: CoreBalloon.tail

module:

scenet.core

type:

~scenet.core.Tail

.. py:class:: CoreCaption

module:

scenet.core

Bases: :py:class:~scenet.core.CoreModel

One caption box, placed and with its lettering already broken into lines.

.. attribute:: id

Stable identifier, c0, c1, … in the order the captions appear.

.. attribute:: order

Position in the panel’s reading order. Captions and balloons share one sequence, so a caption written between two lines of dialogue takes the number between theirs.

.. attribute:: kind

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

.. attribute:: box

Where it sits.

.. attribute:: lines

The resolved line breaking, quotation marks included. A spoken caption’s quotes are part of the text by the time it reaches here, because marks added after measurement would not fit the box drawn for them.

.. attribute:: font_size

Type size in panel units.

.. attribute:: line_height

Baseline-to-baseline distance in panel units.

.. attribute:: italic

Whether the lettering is set in the italic face. Recorded rather than re-derived from kind so the emitter cannot draw the box in a face the solver did not measure it in.

.. attribute:: fill

The value the box is filled with, resolved from the declared tone.

.. attribute:: ink

The value the lettering is drawn in. Chosen against fill by contrast, so a dark box is lettered in paper – reversed type. Resolved rather than left to the emitter for the same reason italic is, and the same reason

attr:

CoreAtmosphere.fall_tone <scenet.core.CoreAtmosphere> is: which mark reads is a fact about the panel.

.. attribute:: speaker

Who is talking, for a spoken caption. Not an actor id: the speaker is off panel, so this resolves to nobody in actors.

There is no tail. That is the difference that makes this its own type rather than a fifth :class:BalloonKind <scenet.ir.BalloonKind>.

fill and ink are defaulted to the white and the near-black every caption has had since captions shipped, so a Core document written before tones existed is still a valid one – which is why adding them needed no format_version bump.

.. py:attribute:: CoreCaption.id

module:

scenet.core

type:

str

.. py:attribute:: CoreCaption.order

module:

scenet.core

type:

int

.. py:attribute:: CoreCaption.kind

module:

scenet.core

type:

~scenet.ir.CaptionKind

.. py:attribute:: CoreCaption.box

module:

scenet.core

type:

~scenet.core.Box

.. py:attribute:: CoreCaption.lines

module:

scenet.core

type:

tuple[str, …]

.. py:attribute:: CoreCaption.font_size

module:

scenet.core

type:

float

.. py:attribute:: CoreCaption.line_height

module:

scenet.core

type:

float

.. py:attribute:: CoreCaption.italic

module:

scenet.core

type:

bool

.. py:attribute:: CoreCaption.fill

module:

scenet.core

type:

str

.. py:attribute:: CoreCaption.ink

module:

scenet.core

type:

str

.. py:attribute:: CoreCaption.speaker

module:

scenet.core

type:

str | None

.. py:class:: CoreMass

module:

scenet.core

Bases: :py:class:~scenet.core.CoreModel

One tonal mass of the backdrop, resolved to a numeric polygon.

The same discipline as capsules, blobs and face_marks: everything the emitter needs is a number by the time it gets here, so drawing a backdrop involves no layout decision at all.

.. attribute:: id

Stable identifier, m0, m1, … back to front.

.. attribute:: kind

What the mass is made of. Carried so a Core document stays readable – it is not consulted when drawing, since the tone is already resolved.

.. attribute:: plane

How far back it sits.

.. attribute:: depth

Painter’s order, shared with the actors. Backdrop planes are negative; a foreground mass sits above the frontmost actor.

.. attribute:: tone

The #rrggbb fill, already chosen from the value ladder.

.. attribute:: polygon

The silhouette, in panel coordinates.

.. py:attribute:: CoreMass.id

module:

scenet.core

type:

str

.. py:attribute:: CoreMass.kind

module:

scenet.core

type:

~scenet.ir.MassKind

.. py:attribute:: CoreMass.plane

module:

scenet.core

type:

~scenet.ir.Plane

.. py:attribute:: CoreMass.depth

module:

scenet.core

type:

int

.. py:attribute:: CoreMass.tone

module:

scenet.core

type:

str

.. py:attribute:: CoreMass.polygon

module:

scenet.core

type:

tuple[tuple[float, float], …]

.. py:class:: CoreStreak

module:

scenet.core

Bases: :py:class:~scenet.core.CoreModel

One streak of falling rain.

The width is on :class:CoreAtmosphere <scenet.core.CoreAtmosphere> rather than here: every streak in a panel shares it, and repeating it per streak would make the file longer without making it say anything more.

.. py:attribute:: CoreStreak.start

module:

scenet.core

type:

tuple[float, float]

.. py:attribute:: CoreStreak.end

module:

scenet.core

type:

tuple[float, float]

.. py:class:: CoreVeil

module:

scenet.core

Bases: :py:class:~scenet.core.CoreModel

The atmospheric noise layer, as parameters rather than as pixels.

SVG has Perlin noise built in through feTurbulence, and the specification includes reference code, so a fixed seed is reproducible by definition: the emitted text is byte-identical. Browsers agree only approximately on what to paint from it, which is fine and is exactly why the determinism contract is on the SVG text and has never been on pixels.

.. attribute:: tone

The #rrggbb the veil is tinted.

.. attribute:: opacity

How much of it lands, 0 .. 1.

.. attribute:: frequency

baseFrequency, per panel unit.

.. attribute:: octaves

numOctaves.

.. attribute:: seed

seed, derived from the declared content and the panel size.

.. py:attribute:: CoreVeil.tone

module:

scenet.core

type:

str

.. py:attribute:: CoreVeil.opacity

module:

scenet.core

type:

float

.. py:attribute:: CoreVeil.frequency

module:

scenet.core

type:

float

.. py:attribute:: CoreVeil.octaves

module:

scenet.core

type:

int

.. py:attribute:: CoreVeil.seed

module:

scenet.core

type:

int

.. py:class:: Disc

module:

scenet.core

Bases: :py:class:~scenet.core.CoreModel

A circle, as stored in a Core document.

Usually a face exclusion zone – the region no balloon may cover. Also a fleck of falling snow, which is the same shape and wants the same rounding.

.. attribute:: cx

Centre x.

.. attribute:: cy

Centre y.

.. attribute:: r

Radius.

.. py:attribute:: Disc.cx

module:

scenet.core

type:

float

.. py:attribute:: Disc.cy

module:

scenet.core

type:

float

.. py:attribute:: Disc.r

module:

scenet.core

type:

float

.. py:method:: Disc.of(circle)

module:

scenet.core

classmethod:
Build a `Disc` from a geometry `Circle`, rounding for emission.

:type circle: :sphinx_autodoc_typehints_type:`\:py\:class\:\`\~scenet.geom.Circle\``
:param circle: The circle to convert.

:rtype: :sphinx_autodoc_typehints_type:`\:py\:class\:\`\~typing.Self\``
:returns: The serialisable equivalent.

.. py:method:: Disc.as_circle() :module: scenet.core

Convert back to a geometry `Circle` for further computation.

:rtype: :sphinx_autodoc_typehints_type:`\:py\:class\:\`\~scenet.geom.Circle\``

.. py:class:: FaceDisc

module:

scenet.core

Bases: :py:class:~scenet.core.CoreModel

A round mark on a face – an eye, or the pupil inside it.

.. attribute:: mark

Always disc.

.. attribute:: id

Which feature this draws.

.. attribute:: centre

(x, y) of the centre.

.. attribute:: radius

Radius in panel units.

.. attribute:: filled

Filled marks are pupils; outlined ones are the eyes around them.

.. attribute:: width

Outline width in panel units, ignored when filled.

.. py:attribute:: FaceDisc.mark

module:

scenet.core

type:

~typing.Literal[‘disc’]

.. py:attribute:: FaceDisc.id

module:

scenet.core

type:

str

.. py:attribute:: FaceDisc.centre

module:

scenet.core

type:

tuple[float, float]

.. py:attribute:: FaceDisc.radius

module:

scenet.core

type:

float

.. py:attribute:: FaceDisc.filled

module:

scenet.core

type:

bool

.. py:attribute:: FaceDisc.width

module:

scenet.core

type:

float

.. py:data:: FaceMark

module:

scenet.core

value:

scenet.core.FaceStroke | scenet.core.FaceDisc

One mark on a drawn face, or of the emanata drawn around it – they are made of the same two primitives. Tagged by a defaulted literal rather than a pydantic discriminator, for the same reason ScriptEvent is: a discriminator would require the tag in every hand-written document.

.. py:class:: FaceStroke

module:

scenet.core

Bases: :py:class:~scenet.core.CoreModel

One line of a drawn face, already sampled into straight segments.

Curves are sampled during compilation rather than emitted as Bezier control points, so that everything an expression does is a number here. A face can then be read, diffed and hand-adjusted exactly like the rest of this tier, and the emitter has nothing left to decide.

.. attribute:: mark

Always stroke. What distinguishes this from a FaceDisc.

.. attribute:: id

Which feature this draws – brow_l, mouth, and so on.

.. attribute:: points

The polyline, in panel coordinates.

.. attribute:: width

Stroke width in panel units.

.. attribute:: closed

Whether the last point joins back to the first, which is what makes an open mouth a shape rather than a stray arc.

.. py:attribute:: FaceStroke.mark

module:

scenet.core

type:

~typing.Literal[‘stroke’]

.. py:attribute:: FaceStroke.id

module:

scenet.core

type:

str

.. py:attribute:: FaceStroke.points

module:

scenet.core

type:

tuple[tuple[float, float], …]

.. py:attribute:: FaceStroke.width

module:

scenet.core

type:

float

.. py:attribute:: FaceStroke.closed

module:

scenet.core

type:

bool

.. py:class:: PanelCore

module:

scenet.core

Bases: :py:class:~scenet.core.CoreModel

A fully resolved panel: numeric, named, and ready to emit.

The middle tier, and the architectural idea of the project. Every position here is absolute and final, but identifiers survive – which is exactly what separates this from SVG. You can read a Core document, see that alice sits at x=280 with her balloon top-left of her head, change one number, and emit it again.

The approach is borrowed from Vega-Lite, which compiles a high-level grammar into a lower-level one before emitting anything drawable.

.. attribute:: format_version

Bumped when the shape of this document changes incompatibly.

.. attribute:: width

Panel width in panel units.

.. attribute:: height

Panel height in panel units.

.. attribute:: actors

Resolved characters, in declaration order.

.. attribute:: balloons

Resolved balloons, in reading order.

.. attribute:: captions

Resolved caption boxes, in reading order. Balloons and captions share one order sequence, since the reader takes them in one sequence.

.. attribute:: backdrop

Where the panel is, or None when it says nothing about that – which is every panel written before the setting layer existed.

Golden-file tests target this tier rather than the SVG, because it changes only when the layout genuinely changes. Diffing SVG text is brittle – a reordered attribute or a different path-rounding convention produces an enormous diff that means nothing.

.. admonition:: Example

from scenet import compile_source core = compile_source(“{cast: {a: {reference: alice}}}”).core core.width, core.height (1000.0, 1000.0) core.actor(“a”).reference ‘alice’ core.to_json().splitlines()[0] ‘{’

.. py:attribute:: PanelCore.format_version

module:

scenet.core

type:

int

.. py:attribute:: PanelCore.width

module:

scenet.core

type:

float

.. py:attribute:: PanelCore.height

module:

scenet.core

type:

float

.. py:attribute:: PanelCore.actors

module:

scenet.core

type:

tuple[~scenet.core.CoreActor, …]

.. py:attribute:: PanelCore.balloons

module:

scenet.core

type:

tuple[~scenet.core.CoreBalloon, …]

.. py:attribute:: PanelCore.captions

module:

scenet.core

type:

tuple[~scenet.core.CoreCaption, …]

.. py:attribute:: PanelCore.backdrop

module:

scenet.core

type:

~scenet.core.CoreBackdrop | None

.. py:property:: PanelCore.bounds

module:

scenet.core

type:

~scenet.geom.BBox

The panel rectangle, origin at (0, 0).

.. py:method:: PanelCore.actor(actor_id)

module:

scenet.core

Look up one actor by id.

type actor_id:
sphinx_autodoc_typehints_type:

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

param actor_id:

The id used in the panel source.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.core.CoreActor``

returns:

That actor.

raises KeyError:

No actor in this panel has that id.

.. py:method:: PanelCore.to_json()

module:

scenet.core

Serialise deterministically.

Keys are sorted and floats already rounded at construction, so the same input yields byte-identical output on any platform. A trailing newline keeps the file well-formed for line-oriented tools like git diff.

rtype:
sphinx_autodoc_typehints_type:

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

.. py:method:: PanelCore.from_json(text)

module:

scenet.core

classmethod:
Read a Core document back in.

The inverse of :meth:`to_json <scenet.core.PanelCore.to_json>`, and the reason Panel
Core is a real format rather than a private data structure: a layout can be
exported, adjusted by hand or by another tool, and read back for emission.

:type text: :sphinx_autodoc_typehints_type:`\:py\:class\:\`str\``
:param text: A Core document.

:rtype: :sphinx_autodoc_typehints_type:`\:py\:class\:\`\~typing.Self\``
:returns: The parsed panel.

:raises pydantic.ValidationError: The document is not a valid Core panel.
:raises json.JSONDecodeError: The text is not JSON at all.

.. py:class:: Tail

module:

scenet.core

Bases: :py:class:~scenet.core.CoreModel

The pointer from a balloon to its speaker’s mouth.

control is present only when the straight route was obstructed and the tail had to bend, which keeps the common case honest about being a simple straight line.

.. py:attribute:: Tail.start

module:

scenet.core

type:

tuple[float, float]

.. py:attribute:: Tail.end

module:

scenet.core

type:

tuple[float, float]

.. py:attribute:: Tail.control

module:

scenet.core

type:

tuple[float, float] | None

.. py:attribute:: Tail.width

module:

scenet.core

type:

float

.. py:property:: Tail.is_curved

module:

scenet.core

type:

bool

Whether this tail had to bend around an obstacle.

.. py:class:: Transform

module:

scenet.core

Bases: :py:class:~scenet.core.CoreModel

Where a puppet’s root joint lands, and how it is scaled and mirrored.

.. py:attribute:: Transform.x

module:

scenet.core

type:

float

.. py:attribute:: Transform.y

module:

scenet.core

type:

float

.. py:attribute:: Transform.scale

module:

scenet.core

type:

float

.. py:attribute:: Transform.mirrored

module:

scenet.core

type:

bool

.. py:function:: point_pair(point)

module:

scenet.core

Convert a point to the rounded (x, y) pair a Core document stores.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\tuple`\ \[:py:class:`float`, :py:class:`float`]`

.. py:function:: round_pairs(points)

module:

scenet.core

Convert a sequence of points – a hull, typically – to rounded pairs.

rtype:
sphinx_autodoc_typehints_type:

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

.. py:function:: vector_pair(vector)

module:

scenet.core

Convert a vector to the rounded (dx, dy) pair a Core document stores.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\tuple`\ \[:py:class:`float`, :py:class:`float`]`