Skip to content

Geometry

The geometry kernel measures every length in base units of 0.4 mm and compares only integers.

units

Exact integer units for LEGO® System geometry.

bricky measures every length in base units of 0.4 mm, the largest length that divides every System dimension exactly. Positions are therefore integers, and every check compares integers.

Agents work in studs (x and y) and plates (z), never in base units. The helpers below convert both ways and refuse values that land off the grid.

MM_PER_UNIT module-attribute

MM_PER_UNIT: Final = 0.4

Millimeters per base unit.

STUD module-attribute

STUD: Final = 20

Stud pitch: 8.0 mm.

PLATE module-attribute

PLATE: Final = 8

Plate height: 3.2 mm.

BRICK module-attribute

BRICK: Final = 3 * PLATE

Brick height: 9.6 mm, three plates.

STUD_DIAMETER module-attribute

STUD_DIAMETER: Final = 12

Stud diameter: 4.8 mm.

STUD_HEIGHT module-attribute

STUD_HEIGHT: Final = 4

Stud height: 1.7 mm in reality, rounded down to 1.6 mm to stay on the grid.

studs

studs(value: float) -> int

Convert a horizontal length in studs to base units.

Raises:

Type Description
ValueError

If the length is not a whole number of base units.

plates

plates(value: float) -> int

Convert a height in plates to base units.

Raises:

Type Description
ValueError

If the height is not a whole number of base units.

to_studs

to_studs(units: int) -> float

Convert a horizontal length in base units to studs.

to_plates

to_plates(units: int) -> float

Convert a height in base units to plates.

geometry

Integer geometry: vectors, the 24 grid rotations, poses, boxes and convex solids.

Every element takes one of the 24 rotations that map grid axes onto grid axes. Each rotation is a signed permutation, so all arithmetic stays exact. Boxes stay axis-aligned under every rotation, which reduces most collision tests to a handful of integer comparisons. The body of a slope, a round part or any other part that boxes cannot describe is a union of convex solids with integer corners. Every face of such a solid lies in a plane with an integer normal, and separating axes decide exactly whether two solids overlap.

ALIASES module-attribute

ALIASES: Final[dict[str, str]] = {
    "R0": "+x+y",
    "R90": "+y-x",
    "R180": "-x-y",
    "R270": "-y+x",
}

Short names for the four rotations about the vertical axis.

The rotations run counter-clockwise, seen from above.

Vector dataclass

Vector(x: int, y: int, z: int)

An integer vector, in base units unless stated otherwise.

dot

dot(other: Vector) -> int

Return the dot product.

cross

cross(other: Vector) -> Vector

Return the cross product.

as_tuple

as_tuple() -> tuple[int, int, int]

Return the components as a tuple.

Rotation dataclass

Rotation(x_axis: Vector, y_axis: Vector)

A proper rotation mapping grid axes onto grid axes: one of 24.

A rotation's name gives the images of the local x and y axes: "+x+y" is the identity, and "+y-x" rotates a part 90° counter-clockwise seen from above. :data:ALIASES gives short names to the four rotations about the vertical axis.

x_axis instance-attribute

x_axis: Vector

Where the rotation takes the x axis.

y_axis instance-attribute

y_axis: Vector

Where the rotation takes the y axis.

z_axis class-attribute instance-attribute

z_axis: Vector = field(
    init=False, repr=False, compare=False
)

Where the rotation takes the z axis: the cross product of the other two.

name property

name: str

The canonical name: the images of local x and y, such as "+y-x".

parse classmethod

parse(text: str) -> Rotation

Parse a rotation name such as "+y-x" or an alias such as "R90".

Raises:

Type Description
ValueError

If the text names no rotation.

apply

apply(vector: Vector) -> Vector

Rotate a vector.

inverse

inverse() -> Rotation

Return the rotation that undoes this one.

Pose dataclass

Pose(
    rotation: Rotation = IDENTITY, position: Vector = ORIGIN
)

A rigid placement: rotate about the local origin, then translate.

apply

apply(point: Vector) -> Vector

Map a point from the local frame to the parent frame.

apply_direction

apply_direction(direction: Vector) -> Vector

Map a direction from the local frame to the parent frame.

Box dataclass

Box(min: Vector, max: Vector)

A half-open, axis-aligned box [min, max) with integer corners.

min instance-attribute

min: Vector

The minimum corner: the smallest coordinate on every axis.

max instance-attribute

max: Vector

The maximum corner: the largest coordinate on every axis.

size property

size: Vector

The extent along each axis.

bounds property

bounds: Box

The box itself, so a box and a :class:Convex both answer bounds.

vertices property

vertices: tuple[Vector, ...]

The eight corners.

Bit 1 of a corner's index picks high x, bit 2 high y and bit 4 high z.

faces property

faces: tuple[tuple[int, ...], ...]

Each face's indices into :attr:vertices, counter-clockwise from outside.

planes property

planes: tuple[Plane, ...]

The plane of each face, in the order of :attr:faces.

directions property

directions: tuple[Vector, ...]

The distinct directions of the edges: the three grid axes.

volume property

volume: int

The volume in cubic base units.

spanning classmethod

spanning(a: Vector, b: Vector) -> Box

Return the box with opposite corners a and b, in any order.

bounding classmethod

bounding(boxes: Iterable[Box]) -> Box

Return the smallest box that contains all the given boxes.

Raises:

Type Description
ValueError

If no boxes are given.

extent

extent(axis: Vector) -> tuple[int, int]

Return the minimum and maximum of axis.dot(vertex) over the vertices.

contains

contains(point: Vector) -> bool

Return whether a point lies in the box.

Low faces count as inside, and high faces as outside.

overlaps

overlaps(other: Box) -> bool

Return whether the boxes share positive volume.

Faces that only touch do not count.

intersection

intersection(other: Box) -> Box | None

Return the box both boxes share, or None if they share no volume.

transformed

transformed(pose: Pose) -> Box

Return the box placed by a pose; grid rotations keep boxes axis-aligned.

Convex dataclass

Convex(
    vertices: tuple[Vector, ...],
    faces: tuple[tuple[int, ...], ...],
)

A convex solid with integer corners, such as the body of a slope.

Each face lists corner indices counter-clockwise seen from outside. The solid keeps every face's plane as an integer outward normal and offset, so the tests for a point inside and for an overlap never round.

Raises:

Type Description
ValueError

If a face has fewer than three corners, is not flat or faces inward, or the solid is concave.

directions class-attribute instance-attribute

directions: tuple[Vector, ...] = field(
    init=False, repr=False, compare=False
)

The distinct directions of the edges, each as a primitive vector.

prism classmethod

prism(
    polygon: Sequence[tuple[int, int]],
    axis: Literal["x", "y", "z"],
    start: int,
    end: int,
) -> Convex

Extrude a convex polygon along an axis, from start to end.

The polygon runs counter-clockwise in its plane, in coordinates (a, b) chosen so that a, b and the axis are right-handed: (x, y) along z, (y, z) along x, and (z, x) along y.

Raises:

Type Description
ValueError

If the polygon is not convex and counter-clockwise, or ``start

= end``.

transformed

transformed(pose: Pose) -> Convex

Return the solid placed by a pose.

Proper rotations keep the faces wound outward.

contains

contains(point: Vector) -> bool

Return whether a point lies in the solid.

A point on a face counts as inside when the face looks toward lower coordinates (its normal's first nonzero component is negative) and outside otherwise, as with :meth:Box.contains, so solids that share a face never both contain a point on it.

extent

extent(axis: Vector) -> tuple[int, int]

Return the minimum and maximum of axis.dot(vertex) over the vertices.

overlaps

overlaps(a: Solid, b: Solid) -> bool

Return whether two solids share positive volume.

Touching faces or edges do not count. The test uses separating axes. Two convex solids keep their insides apart exactly when some axis, among their face normals and the cross products of their edge directions, puts them on opposite sides, touching at most.