UVRegion
An immutable texture region mapped onto a mesh. It holds one of three states:
| State | Face geometry | Moves as |
|---|---|---|
"stacked" | one rectangle shared by every face | the whole region |
"unfolded" | a net, one cell per active face or group of faces | the whole region |
"free" | a rectangle per active face | each face on its own |
new UVRegion(data: UVRegionData)
UVRegion.from(value: UVRegion | UVRegionData): UVRegion
UVRegion.fromLayout(layout: UVLayoutData, identity: UVRegionIdentity): UVRegionUVRegion.from() returns an existing instance unchanged or builds one from serialized data. UVRegion.fromLayout() builds a region from a layout and the identity its owner assigns. Geometry returned by the region is copied, so callers cannot mutate the stored state.
Types
type UVSlot = string;
// The six default names of a box; a shape may name further slots.
const DEFAULT_UV_SLOTS: readonly UVSlot[];
// Clockwise quarter turns in texture space, where y points down.
type UVQuarterTurn = 0 | 1 | 2 | 3;
type UVRect = SelectionRect & {
rotation?: UVQuarterTurn;
};
type UVTriangle = {
shape: "triangle";
corner: "top-left" | "top-right" | "bottom-left" | "bottom-right";
rect: SelectionRect;
rotation?: UVQuarterTurn;
};
type UVNormalizedRect = {
x: number;
y: number;
width: number;
height: number;
};
type UVCompoundPart =
| UVNormalizedRect
| {
shape: "triangle";
corner: UVTriangle["corner"];
rect: UVNormalizedRect;
};
type UVCompound = {
shape: "compound";
rect: SelectionRect;
parts: UVCompoundPart[];
rotation?: UVQuarterTurn;
};
type UVGeometry = UVRect | UVTriangle | UVCompound;
type UVRegionState = "stacked" | "unfolded" | "free";
type UVRegionData =
| {
id: string;
name?: string;
color: string;
state: "stacked";
rect: UVRect;
faces?: Record<UVSlot, UVGeometry>;
activeFaces?: UVSlot[];
stackedFace?: UVSlot;
}
| {
id: string;
name?: string;
color: string;
state: "unfolded" | "free";
faces: Record<UVSlot, UVGeometry>;
activeFaces?: UVSlot[];
};state is required, and payloads written before the three-state rename do not load. Stacked regions use optional faces and activeFaces to retain custom topology for a later free() or unfold(), and stackedFace records which face rect was taken from. A payload may still place those faces away from rect, in which case free() restores them around it; stack() itself writes them onto rect.
activeFaces defaults to the slots faces carries, keeps the order it was given, and drops any slot the region has no geometry for. The constructor throws a RangeError when no active slot is left. A triangle occupies the half of rect containing the named right-angle corner. A compound covers the union of its parts, each positioned in the 0 to 1 space of rect so parts scale with it. Normalized parts must remain inside that space and have positive dimensions. The area no part covers, such as the notch of an L, is outside the region.
A geometry is stored as it looks after rotation: a rotated rect has its width and height swapped, a triangle names the corner it now occupies, and compound parts sit where they turned to. rotation only tells a mesh consumer how to orient its UVs inside that rect, and is left out when it is 0. A stacked region keeps its rotation on rect, shared by every slot. rotateUv() and rotateCorner() turn mesh UVs to match.
Slot names are labels. The consumer decides how "front", "top" and any further slot map onto mesh geometry; @jolly-pixel/voxel.renderer derives them from a shape and may emit names like "top.1".
Properties
| Property | Type | Description |
|---|---|---|
id | string | Stable region identifier. |
name | string | undefined | Optional display label. |
color | string | CSS color used by the UV overlay. |
state | UVRegionState | Current geometry mode. |
slots | readonly UVSlot[] | Every slot the region carries geometry for, active or not. |
activeSlots | readonly UVSlot[] | Active slots in consumer-defined order. |
movementScope | "region" | "slot" | Whether dragging moves the region or one slot. |
stackedFace | UVSlot | null | Slot rect was taken from, when stacked. |
bounds | SelectionRect | The shared rectangle when stacked, otherwise the union of the active faces. |
Methods
slotRect(slot?)
slotRect(slot?: UVSlot | null): SelectionRectReturns a copy of the rectangle sampled by slot. Only a free region answers per slot; a stacked or unfolded region returns bounds whatever slot is asked for, because it moves as one. null, the default, returns bounds.
slotGeometry(slot)
slotGeometry(slot: UVSlot): UVGeometryReturns a copy of the slot's geometry. A stacked region always returns its shared rectangle. An unfolded region returns the cell the net gave that slot, which is what slotRect() will not tell you.
listSlots()
listSlots(): { slot: UVSlot | null; geometry: UVGeometry }[]Returns copied geometry in the region's own slot order. A stacked region returns one entry with slot: null. Unfolded and free regions both return one entry per active slot, so an overlay draws and hit-tests every cell of a net individually even though dragging any of them moves the whole region.
free()
free(): UVRegionHands every face its own position. From "unfolded" this changes nothing but the state: the cells stay where the net put them and become individually draggable. From "stacked" it restores retained faces and shapes onto the shared rectangle, each keeping the size and shape its slot carries. A stacked face holds no offset of its own, so a region that moved while stacked frees at its current rectangle. Returns this when already free.
unfold(net?)
unfold(net?: UVNet): UVRegionLays the active faces out as a net anchored on the current bounds top-left. Each face keeps its own size and shape; only its position changes. net decides the arrangement and defaults to UVNet.packed; a grid net gives each face a fixed cell.
The packing net places the tallest face first, then fills the lowest free spot left of the strip, so a short cell slides in beside a tall neighbour instead of starting a new row. It repeats that over every strip width a shelf packer could need and keeps the net whose bounding box has the smallest perimeter, then the smallest area. Six equal faces give a 2x3 net; a pole's two horizontal side strips end up stacked on top of each other rather than side by side, and a stair's half-height back and top slots pair into full rows.
A grid net can put several slots in one cell. They share the rectangle of the first one listed, so their faces show the same pixels.
Unfolding always lays the net out again, whatever state it starts from, so a hand-arranged free layout is discarded. An unfolded region is laid out again too, which is how a different net applies to it. That transition undoes with the whole previous region, so undo brings the arrangement back. It returns this when the net would put every face where it already is.
Nothing here knows about the canvas. A net larger than the texture keeps going past the edge; UVMap.applyState() is what pulls it back inside.
stack(slot?)
stack(slot?: UVSlot | null): UVRegionUses the largest active slot's rectangle as the shared rectangle, so a partial slot such as a stair's tread never becomes the region's footprint. slot only picks between equally large ones, and a rectangle wins over a triangle or a compound at the same size. Slot topology is retained, but per-slot positions are not: every slot is stacked on the shared rectangle.
Stacking a net therefore lands the region on whichever cell won, which for equal-sized faces is the first one in activeFaces order. That is the cell the net started from, so unfold() followed by stack() returns a box region to the rectangle it began with.
When the active faces carry different rotations, the stacked region takes the most common one; a tie goes to the rotation of the slot being stacked on. Faces with another rotation are turned in place to match.
rotated(direction, slot?)
rotated(direction: RotationDirection, slot?: UVSlot | null): UVRegionTurns the region 90 degrees clockwise ("cw") or counter-clockwise ("ccw"), keeping each rect's top-left corner:
- stacked: the shared rect and every slot turn together.
- unfolded: the whole net turns as one piece around its
bounds, whose top-left corner stays put. - free: only
slotturns in place. Returnsthiswhenslotisnullor not active.
Four turns return the starting geometry. Nothing here knows about the canvas; UVMap.rotate() clamps the result.
resized(rect, slot?, options?)
resized(rect: SelectionRect, slot?: UVSlot | null, options?: UVResizeOptions): UVRegion
interface UVResizeOptions {
aligned?: boolean;
}Gives the region or one of its faces a new rectangle, independent of the mesh size:
- stacked:
rectbecomes the shared rectangle and every face is reset to it, so a laterunfold()lays out faces of that size. A triangle or compound face keeps its corner or parts, scaled torect.slotis ignored. - unfolded: only
slotchanges size, and the faces of the net behave like solid boxes. A growing edge pushes the faces it would overlap, and they push the faces they reach in turn. A shrinking edge pulls back the faces that touched it, and the faces touching those, each stopping where it would hit a face that stays. Faces out of contact never move, faces never overlap, and face order never changes. Faces that already overlapped, as in a net saved by an older version, are pushed apart right or down, whichever is shorter. Faces with the exact same rectangle are one cell, as a grid net with shared cells lays them out: they resize and push together, and they stay on one rectangle. Withoptions.aligned, every face whose same edge lies on the same line and touchesslotalong it, directly or through another such face, moves that edge by the same amount, so a row keeps its bottom aligned and a column its right side. - free: only
slotchanges.
Rotation is kept, and so is the shape of a triangle or compound face: its rectangle changes and its parts scale with it. Returns this when slot is missing or inactive outside the stacked state, or nothing changes. Throws a RangeError when rect is smaller than 1px on either axis. Nothing here knows about the canvas.
withGeometry(slot, geometry)
withGeometry(slot: UVSlot, geometry: UVGeometry): UVRegionReplaces one slot's geometry of a free region, as a network peer does when it receives a slot rotation. Returns this for any other state or a slot that is not active.
movedTo(position, slot?)
movedTo(position: Vec2, slot?: UVSlot | null): UVRegionMoves the region so its rectangle starts at position, keeping every size and rotation. A stacked or unfolded region moves whole and ignores slot. A free region moves only slot, and returns this when slot is null or not active. Returns this when nothing moves.
isTarget(slot)
isTarget(slot: UVSlot | null): slot is UVSlotWhether slot is one of the active slots. Every per-slot operation (movedTo, rotated, resized, withGeometry) and UVMap selection act only on active slots.
resizeTargets(selectedSlot)
resizeTargets(selectedSlot: UVSlot | null): UVResizeTarget[]
interface UVResizeTarget {
id: string;
slot: UVSlot | null;
rect: SelectionRect;
handles: readonly UVResizeHandle[];
}The rectangles a resize drag can grab, with the edges and corners each offers: the shared rectangle with every handle when stacked, every net cell with only its east and south edges when unfolded, faces sharing a rectangle counting once under the first active one, and selectedSlot with every handle when free. A triangle or compound face offers the handles of its bounding rectangle.
translated(delta)
translated(delta: Vec2): UVRegionMoves the whole region by delta, in any state. Returns this for a zero delta.
renamed(name)
renamed(name: string): UVRegionReturns a copy carrying name, or this when the name is unchanged.
toLayout()
toLayout(): UVLayoutData
interface UVRegionIdentity {
id: string;
name?: string;
color: string;
}
type UVLayoutData = Omit<UVRegionData, keyof UVRegionIdentity>;Returns the region's geometry without its identity. A document that owns an external region, such as a voxel model block, stores this layout and gives the region its identity when it shows it.
toJSON()
toJSON(): UVRegionDataReturns an independent serializable copy. JSON.stringify() calls it automatically.
Rotation helpers
rotateGeometry(geometry, turns)
rotateGeometry(geometry: UVGeometry, turns: number): UVGeometryTurns a geometry clockwise by turns quarter turns (negative values turn counter-clockwise), keeping the top-left corner of its rect, and adds turns to its rotation.
geometryRotation(geometry)
geometryRotation(geometry: UVGeometry): UVQuarterTurnReturns the geometry's rotation, or 0.
withRotation(geometry, rotation)
withRotation(geometry: UVGeometry, rotation: number): UVGeometryReturns a shallow copy of geometry with rotation set to that many quarter turns, without moving or reshaping it. The key is left out when the turns reduce to 0. A nested rect or parts is shared with the input.
boundingRect(geometry)
boundingRect(geometry: UVGeometry): SelectionRectReturns a copy of the geometry's bounding rect, without its shape or rotation.
triangleCorner(geometry)
triangleCorner(geometry: UVGeometry): UVTriangleCorner | nullReturns the corner of a triangle geometry, before any rotation is applied, or null for a rect or compound.
rotateUv(u, v, turns)
rotateUv(u: number, v: number, turns: number): [number, number]Turns a mesh UV inside the unit square, with v pointing up, so that it follows a clockwise texture-space rotation. After one turn the face's top edge samples the rect's right edge, which is where the overlay draws the orientation marker.
rotateCorner(corner, turns)
rotateCorner(corner: UVTriangleCorner, turns: number): UVTriangleCornerTurns a triangle corner clockwise. A mesh consumer recovers the corner a triangle had before rotating with rotateCorner(corner, -geometryRotation(geometry)), applies that orientation first, then rotateUv().