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:
objectAn 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, heightrather 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 thany, 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
otherfalls 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.0when 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
marginon 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 * marginwider 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:
objectA 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:
objectA 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:
objectA direction and magnitude, with no position.
Distinct from
Pointon 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
Noneat 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
dxnegated.
.. 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 – so0.125becomes0.12and0.135becomes0.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.0afterwards normalises-0.0to0.0. Without it a coordinate that rounds to negative zero emits as-0.0and 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
PRECISIONdecimal 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
starttoendpasses 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.