Skip to content

UVRegion ​

An immutable texture region mapped onto a mesh. It holds one of three states:

StateFace geometryMoves as
"stacked"one rectangle shared by every facethe whole region
"unfolded"a net, one cell per active face or group of facesthe whole region
"free"a rectangle per active faceeach face on its own
ts
new UVRegion(data: UVRegionData)
UVRegion.from(value: UVRegion | UVRegionData): UVRegion
UVRegion.fromLayout(layout: UVLayoutData, identity: UVRegionIdentity): UVRegion

UVRegion.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 ​

ts
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 ​

PropertyTypeDescription
idstringStable region identifier.
namestring | undefinedOptional display label.
colorstringCSS color used by the UV overlay.
stateUVRegionStateCurrent geometry mode.
slotsreadonly UVSlot[]Every slot the region carries geometry for, active or not.
activeSlotsreadonly UVSlot[]Active slots in consumer-defined order.
movementScope"region" | "slot"Whether dragging moves the region or one slot.
stackedFaceUVSlot | nullSlot rect was taken from, when stacked.
boundsSelectionRectThe shared rectangle when stacked, otherwise the union of the active faces.

Methods ​

slotRect(slot?) ​

ts
slotRect(slot?: UVSlot | null): SelectionRect

Returns 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) ​

ts
slotGeometry(slot: UVSlot): UVGeometry

Returns 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() ​

ts
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() ​

ts
free(): UVRegion

Hands 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?) ​

ts
unfold(net?: UVNet): UVRegion

Lays 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?) ​

ts
stack(slot?: UVSlot | null): UVRegion

Uses 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?) ​

ts
rotated(direction: RotationDirection, slot?: UVSlot | null): UVRegion

Turns 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 slot turns in place. Returns this when slot is null or not active.

Four turns return the starting geometry. Nothing here knows about the canvas; UVMap.rotate() clamps the result.

resized(rect, slot?, options?) ​

ts
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: rect becomes the shared rectangle and every face is reset to it, so a later unfold() lays out faces of that size. A triangle or compound face keeps its corner or parts, scaled to rect. slot is ignored.
  • unfolded: only slot changes 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. With options.aligned, every face whose same edge lies on the same line and touches slot along 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 slot changes.

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) ​

ts
withGeometry(slot: UVSlot, geometry: UVGeometry): UVRegion

Replaces 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?) ​

ts
movedTo(position: Vec2, slot?: UVSlot | null): UVRegion

Moves 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) ​

ts
isTarget(slot: UVSlot | null): slot is UVSlot

Whether 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) ​

ts
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) ​

ts
translated(delta: Vec2): UVRegion

Moves the whole region by delta, in any state. Returns this for a zero delta.

renamed(name) ​

ts
renamed(name: string): UVRegion

Returns a copy carrying name, or this when the name is unchanged.

toLayout() ​

ts
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() ​

ts
toJSON(): UVRegionData

Returns an independent serializable copy. JSON.stringify() calls it automatically.

Rotation helpers ​

rotateGeometry(geometry, turns) ​

ts
rotateGeometry(geometry: UVGeometry, turns: number): UVGeometry

Turns 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) ​

ts
geometryRotation(geometry: UVGeometry): UVQuarterTurn

Returns the geometry's rotation, or 0.

withRotation(geometry, rotation) ​

ts
withRotation(geometry: UVGeometry, rotation: number): UVGeometry

Returns 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) ​

ts
boundingRect(geometry: UVGeometry): SelectionRect

Returns a copy of the geometry's bounding rect, without its shape or rotation.

triangleCorner(geometry) ​

ts
triangleCorner(geometry: UVGeometry): UVTriangleCorner | null

Returns the corner of a triangle geometry, before any rotation is applied, or null for a rect or compound.

rotateUv(u, v, turns) ​

ts
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) ​

ts
rotateCorner(corner: UVTriangleCorner, turns: number): UVTriangleCorner

Turns 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().