Skip to content

Z-order

Every shape has an index, a fractional index string such as "a0", "a1" or "a0V". Shapes draw in ascending index order, so the highest index is the top shape. A fractional index always has room between two neighbours, so moving a shape rewrites that shape’s index and no other.

The fractional-indexing package generates the strings. Only the package’s z-index module imports it.

Parsing an index

parseZIndex(input) turns a string from a file, a paste or a network message into a ZIndex. It returns a Result. A malformed string gives { kind: "invalid-index", input }, and nothing throws.

A string is valid when the library can generate a key above it and a key below it. That rules out an empty string, a string with a trailing zero such as "a00", and a string with an unknown head such as "!!".

Every other function in the package takes a ZIndex, so it needs no further checks.

Sorting

compareShapes(a, b) is a comparator for Array.prototype.sort. It orders by index, then by id, so two shapes with the same index still have a fixed order. Indices compare by UTF-16 code unit, which is the order the library defines. "a0V" sorts before "a1", and "Zz" sorts before "a0".

Equal indices can come from two clients that add a shape at the same time. They do not break sorting. They only matter when a reorder needs room between the tied shapes, and that case returns a ZOrderError.

A new shape’s index

indexAbove(shapes) returns an index above every index in shapes, so a new shape lands on top. It returns "a0" for an empty list. Shapes with tied top indices are fine.

indicesAbove(shapes, count) returns count ascending indices above every index in shapes. A paste gives them to the copies in their z-order, so the copies land on top and keep their order among themselves.

Reordering

bringToFront, sendToBack, bringForward and sendBackward take all the shapes and a ReadonlySet<ShapeId> of the selected ids. They do not change anything. They return a ReorderResult, which holds the shapes whose index must change as IndexPatch values, { id, index }. A shape that is not in a patch keeps its index, so the caller writes only what moved.

  • Patch order. Patches follow the new z-order, lowest first.
  • Relative order. The selected shapes keep their order among themselves. A selection moves as one group.
  • Front and back. bringToFront puts the selection above every shape. sendToBack puts it below every shape.
  • One step. bringForward finds the topmost selected shape, then the first unselected shape above it whose page bounds touch the selection’s bounds, and puts the selection directly above that shape. Shapes in between that do not overlap keep their place. sendBackward does the same downwards from the lowest selected shape. A step always passes a shape the user can see, so one action visibly does something.
  • Nothing to do. A selection that already holds the front or the back, an empty selection, ids that match no shape, and a step with no overlapping shape all return an empty patch list. That is a success, not an error.
  • Overlap. Overlap compares the frameBounds of each shape with the bounds of the selection, so rotation counts. Boxes that only touch count as overlapping.
docs/examples/bring-forward.ts
import { DEFAULT_STYLE, bringForward, indexAbove, radians, shapeId } from "@draw/shapes";
import type { IndexPatch, Shape } from "@draw/shapes";
function push(shapes: readonly Shape[], id: string, x: number): readonly Shape[] {
const shape: Shape = {
id: shapeId(id),
frame: { x, y: 0, width: 40, height: 40, rotation: radians(0) },
drawable: { kind: "rectangle", cornerRadius: 0 },
index: indexAbove(shapes),
style: DEFAULT_STYLE,
groupIds: [],
};
return [...shapes, shape];
}
const shapes = push(push(push([], "back", 0), "far", 200), "front", 20);
const result = bringForward(shapes, new Set([shapeId("back")]));
export const patches: readonly IndexPatch[] = result.ok ? result.value : [];

The three shapes sit at indices a0 (back), a1 (far) and a2 (front). back and front overlap, and far is 200 units to the right. bringForward skips far and puts back above front, so the only patch is back at a3.

Errors

Error Meaning
ZIndexError invalid-index parseZIndex was given a string that is not a fractional index.
ZOrderError tied-indices Two neighbouring shapes share index, so there is no room between them for another key.

A reorder needs room between its two neighbours. When those neighbours share an index there is none, so it returns this error and patches nothing. Tied indices elsewhere in the stack do no harm.