Geometry#

Points, vectors, boxes and circles – the entire vocabulary the solver has for describing a character.

scenet.geom#

.. py:module:: scenet.geom

Geometry primitives: points, vectors, boxes and circles.

Everything downstream of the frontend speaks in these four types. They are the whole vocabulary the solver has for describing a character – a silhouette is a list of Point, a face is a Circle, a balloon is a BBox, a gaze is a Vector – which is what keeps the layout engine from ever needing to know what anything looks like.

Coordinate system. SVG’s: x runs rightward, y runs downward, and the origin is the panel’s top-left corner. Angles are in degrees, measured clockwise, which is what “clockwise” means once y points down. All lengths are in panel units; a panel declares its own size, so a unit is whatever fraction of the panel you decide it is.

Strictness. Every containment and intersection test here is strict: a point exactly on a circle’s edge is not inside it, and two shapes that merely touch do not intersect. Applied consistently, a shared boundary belongs to neither shape – so two balloons resting edge to edge are legal, and a tail grazing a face outline is not diverted. Mixing strict and non-strict tests would make those cases depend on which function you happened to ask. The one deliberate exception is BBox.contains, documented there.

Immutability. Every type here is frozen and slotted. The balloon search creates these in tight loops, so the memory layout matters; and a resolved layout that cannot be mutated is a layout the emitter cannot quietly change on its way out.

A note on annotations: self- and forward-references are quoted (-> "Point"). Before Python 3.14 and PEP 649 a bare -> Point inside class Point is evaluated at definition time and raises NameError, and BBox and Circle refer to each other mutually so no ordering fixes it. Self is deliberately not used: these methods construct their own class by name, so Self would promise subclass-preserving behaviour the bodies do not deliver.

.. py:class:: BBox

module:

scenet.geom

Bases: :py:class:object

An axis-aligned rectangle, given as a corner plus a size.

Balloons are boxes, panel frames are boxes, and every silhouette has one as its bounds. Stored as x, y, width, height rather than as two corners because that is what SVG wants and what the constraint solver’s variables map onto directly.

.. attribute:: x

Left edge.

.. attribute:: y

Top edge.

.. attribute:: width

Extent rightward.

.. attribute:: height

Extent downward.

.. admonition:: Example

from scenet.geom import BBox balloon = BBox(10.0, 20.0, 100.0, 50.0) balloon.right, balloon.bottom (110.0, 70.0) balloon.centre Point(x=60.0, y=45.0)

.. py:attribute:: BBox.x

module:

scenet.geom

type:

float

.. py:attribute:: BBox.y

module:

scenet.geom

type:

float

.. py:attribute:: BBox.width

module:

scenet.geom

type:

float

.. py:attribute:: BBox.height

module:

scenet.geom

type:

float

.. py:property:: BBox.right

module:

scenet.geom

type:

float

The right edge, x + width.

.. py:property:: BBox.bottom

module:

scenet.geom

type:

float

The bottom edge, y + height. Larger than y, because y runs downward.

.. py:property:: BBox.centre

module:

scenet.geom

type:

~scenet.geom.Point

The midpoint of the box.

.. py:property:: BBox.area

module:

scenet.geom

type:

float

Width times height.

.. py:method:: BBox.contains(other)

module:

scenet.geom

Whether another box lies entirely within this one.

Edges may coincide: a box exactly filling this one is contained. This is the module’s one deliberate departure from strictness, because a balloon sitting flush against the panel margin is inside the panel.

type other:
sphinx_autodoc_typehints_type:

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

param other:

The box to test.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\bool``

returns:

True if no part of other falls outside this box.

.. py:method:: BBox.overlap_area(other)

module:

scenet.geom

Return the area shared with another box.

type other:
sphinx_autodoc_typehints_type:

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

param other:

The box to intersect with.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\float``

returns:

The overlapping area, or 0.0 when the boxes are disjoint or merely touch.

.. admonition:: Example

from scenet.geom import BBox BBox(0.0, 0.0, 10.0, 10.0).overlap_area(BBox(5.0, 5.0, 10.0, 10.0)) 25.0 BBox(0.0, 0.0, 10.0, 10.0).overlap_area(BBox(10.0, 0.0, 10.0, 10.0)) 0.0

.. py:method:: BBox.intersects_circle(circle)

module:

scenet.geom

Whether a circle overlaps this box.

Uses the closest-point test: clamp the circle’s centre to the box, then compare that distance against the radius. Strict, so tangency is not an intersection.

This is the test that keeps a balloon off a face.

type circle:
sphinx_autodoc_typehints_type:

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

param circle:

The circle to test, typically an actor’s face exclusion zone.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\bool``

returns:

True if the two overlap by any positive amount.

.. py:method:: BBox.expanded(margin)

module:

scenet.geom

Return this box grown by margin on every side.

type margin:
sphinx_autodoc_typehints_type:

\:py\:class\:\float``

param margin:

Distance to add to each edge. A negative value shrinks the box, and a sufficiently negative one inverts it, which is not checked for.

rtype:
sphinx_autodoc_typehints_type:

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

returns:

A new box, 2 * margin wider and taller than this one.

.. py:method:: BBox.moved_to(x, y)

module:

scenet.geom

Return this box with the same size at a new top-left corner.

type x:
sphinx_autodoc_typehints_type:

\:py\:class\:\float``

param x:

New left edge.

type y:
sphinx_autodoc_typehints_type:

\:py\:class\:\float``

param y:

New top edge.

rtype:
sphinx_autodoc_typehints_type:

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

returns:

A new box.

.. py:method:: BBox.init(x, y, width, height)

module:

scenet.geom

.. py:class:: Circle

module:

scenet.geom

Bases: :py:class:object

A disc, used throughout as a face exclusion zone.

A head is not a circle, but for layout purposes it is close enough and vastly cheaper than the alternative: every “would this balloon cover someone’s face?” test reduces to one distance comparison.

.. attribute:: cx

Centre x.

.. attribute:: cy

Centre y.

.. attribute:: r

Radius.

.. admonition:: Example

from scenet.geom import Circle, Point head = Circle(100.0, 100.0, 30.0) head.contains_point(Point(110.0, 100.0)) True head.contains_point(Point(130.0, 100.0)) False

.. py:attribute:: Circle.cx

module:

scenet.geom

type:

float

.. py:attribute:: Circle.cy

module:

scenet.geom

type:

float

.. py:attribute:: Circle.r

module:

scenet.geom

type:

float

.. py:property:: Circle.centre

module:

scenet.geom

type:

~scenet.geom.Point

The centre, as a point.

.. py:method:: Circle.contains_point(point)

module:

scenet.geom

Whether a point lies strictly inside this circle.

type point:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.geom.Point``

param point:

The point to test.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\bool``

returns:

True if the point is inside; False if it is outside or exactly on the edge.

.. py:method:: Circle.as_bbox()

module:

scenet.geom

Return the smallest axis-aligned box containing this circle.

rtype:
sphinx_autodoc_typehints_type:

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

.. py:method:: Circle.init(cx, cy, r)

module:

scenet.geom

.. py:class:: Point

module:

scenet.geom

Bases: :py:class:object

A position in panel space.

The most common type in the codebase. Anchors are points, silhouette hulls are sequences of points, and both ends of a balloon tail are points.

.. attribute:: x

Distance rightward from the panel’s left edge.

.. attribute:: y

Distance downward from the panel’s top edge.

.. admonition:: Example

from scenet.geom import Point mouth = Point(120.0, 84.0) mouth.translated(0, 10) Point(x=120.0, y=94.0)

.. py:attribute:: Point.x

module:

scenet.geom

type:

float

.. py:attribute:: Point.y

module:

scenet.geom

type:

float

.. py:method:: Point.translated(dx, dy)

module:

scenet.geom

Return this point moved by an offset.

type dx:
sphinx_autodoc_typehints_type:

\:py\:class\:\float``

param dx:

Rightward offset.

type dy:
sphinx_autodoc_typehints_type:

\:py\:class\:\float``

param dy:

Downward offset.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.geom.Point``

returns:

A new point; this one is unchanged.

.. py:method:: Point.rotated_around(origin, degrees)

module:

scenet.geom

Return this point rotated about another.

This is the workhorse of forward kinematics: posing a limb is rotating its far end around its joint.

type origin:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.geom.Point``

param origin:

The centre of rotation.

type degrees:
sphinx_autodoc_typehints_type:

\:py\:class\:\float``

param degrees:

Rotation angle, clockwise on screen – which is the positive direction once y points downward.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.geom.Point``

returns:

A new point.

.. admonition:: Example

from scenet.geom import Point, rounded turned = Point(10.0, 0.0).rotated_around(Point(0.0, 0.0), 90) rounded(turned.x), rounded(turned.y) (0.0, 10.0)

.. py:method:: Point.distance_to(other)

module:

scenet.geom

Return the straight-line distance to another point.

type other:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.geom.Point``

param other:

The point to measure to.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\float``

returns:

Distance in panel units, never negative.

.. py:method:: Point.as_tuple()

module:

scenet.geom

Return (x, y) rounded for emission.

rtype:
sphinx_autodoc_typehints_type:

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

returns:

The pair a Panel Core document stores, already passed through

func:

rounded <scenet.geom.rounded>.

.. py:method:: Point.init(x, y)

module:

scenet.geom

.. py:class:: Vector

module:

scenet.geom

Bases: :py:class:object

A direction and magnitude, with no position.

Distinct from Point on purpose. A gaze direction and an eye position are both pairs of floats and mean entirely different things; giving them different types means the type checker catches you confusing them.

.. attribute:: dx

Rightward component.

.. attribute:: dy

Downward component.

.. admonition:: Example

from scenet.geom import Vector Vector(3.0, 4.0).length 5.0 Vector(3.0, 4.0).normalised() Vector(dx=0.6, dy=0.8)

.. py:attribute:: Vector.dx

module:

scenet.geom

type:

float

.. py:attribute:: Vector.dy

module:

scenet.geom

type:

float

.. py:property:: Vector.length

module:

scenet.geom

type:

float

The vector’s magnitude.

.. py:method:: Vector.normalised()

module:

scenet.geom

Return this vector scaled to unit length.

A zero vector is returned unchanged rather than raising. That lets callers treat “this character is not looking at anything” as a neutral term in the arithmetic instead of special-casing None at every use.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.geom.Vector``

returns:

A unit vector in the same direction, or the zero vector unchanged.

.. py:method:: Vector.mirrored_x()

module:

scenet.geom

Return this vector reflected left-to-right.

Used when an actor faces the other way: the whole puppet is mirrored, and its gaze has to come along.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.geom.Vector``

returns:

A new vector with dx negated.

.. py:method:: Vector.dot(other)

module:

scenet.geom

Return the dot product with another vector.

type other:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.geom.Vector``

param other:

The vector to project onto.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\float``

returns:

Positive when the two point broadly the same way, negative when opposed, zero when perpendicular.

.. py:method:: Vector.as_tuple()

module:

scenet.geom

Return (dx, dy) rounded for emission.

rtype:
sphinx_autodoc_typehints_type:

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

.. py:method:: Vector.init(dx, dy)

module:

scenet.geom

.. py:function:: rounded(value)

module:

scenet.geom

Round a coordinate for emission.

Uses Python’s built-in round, which is round-half-to-even – so 0.125 becomes 0.12 and 0.135 becomes 0.14. That asymmetry looks like a bug the first time you meet it in a golden file and is not one: half-to-even is deterministic and unbiased, which is exactly what byte-identical output needs.

Adding 0.0 afterwards normalises -0.0 to 0.0. Without it a coordinate that rounds to negative zero emits as -0.0 and produces a spurious diff against an otherwise identical file.

type value:
sphinx_autodoc_typehints_type:

\:py\:class\:\float``

param value:

A coordinate in panel units.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\float``

returns:

The value rounded to PRECISION decimal places, never negative zero.

.. admonition:: Example

from scenet.geom import rounded rounded(12.3456) 12.35 rounded(-0.001) 0.0

.. py:function:: segment_intersects_circle(start, end, circle)

module:

scenet.geom

Whether the line segment start to end passes through a circle.

This is the test that decides whether a straight balloon tail would cross somebody’s face, and so whether the tail has to bend instead. Implemented by projecting the circle’s centre onto the segment, clamped to its extent, and comparing that distance to the radius – exact, and cheaper than solving the quadratic.

type start:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.geom.Point``

param start:

One end of the segment.

type end:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.geom.Point``

param end:

The other end.

type circle:
sphinx_autodoc_typehints_type:

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

param circle:

The obstacle.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\bool``

returns:

True if any part of the segment lies strictly inside the circle. A segment that only grazes the edge does not count.

.. admonition:: Example

from scenet.geom import Circle, Point, segment_intersects_circle face = Circle(50.0, 50.0, 20.0) segment_intersects_circle(Point(0.0, 50.0), Point(100.0, 50.0), face) True segment_intersects_circle(Point(0.0, 0.0), Point(100.0, 0.0), face) False

.. seealso::

meth:

Circle.contains_point <scenet.geom.Circle.contains_point>, which this falls back to for a zero-length segment.