Commands
Every change to a VoxelDocument is a VoxelCommand. The document emits each one on its "command" event and replays one with applyCommand().
import {
VoxelDocument,
isVoxelLayerCommand,
type VoxelCommandListener
} from "@jolly-pixel/voxel.renderer";
const onCommand: VoxelCommandListener = (command, { origin }) => {
if (isVoxelLayerCommand(command) && command.action === "voxel-set") {
console.log(origin, command.metadata.position);
}
};
const document = new VoxelDocument({ onCommand });Context
type VoxelCommandListener = (
command: VoxelCommand,
context: VoxelCommandContext
) => void;
interface VoxelCommandContext {
origin: "local" | "remote" | "replay";
clientId?: string | null;
redefinition?: BlockRedefinition;
}
type BlockRedefinition =
| "added"
| "metadata"
| "tiles"
| "mesh"
| "occlusion";origin is "local" for a change made on this document, "remote" for a peer's command applied with document.applyCommand(command, { origin: "remote" }), and "replay" for this client's pending command applied or rolled back around a peer's. A network adapter applies peer commands as "remote" and sends only "local" ones.
clientId is the peer applyCommand() was given.
redefinition is set on "block-defined" commands only. It compares the new definition with the one it replaced:
| Value | Change |
|---|---|
"added" | No block had this id. |
"metadata" | Only name or properties changed, or nothing did. |
"tiles" | Tile col, row or size moved; name and properties may have changed too. |
"mesh" | Another field that shapes the block's faces changed, such as a tile's blockset or rotation. |
"occlusion" | shapeId, alphaMode, face culling or blendGroup changed, so neighbouring faces may change too. |
Categories
type VoxelCommand =
| VoxelLayerCommand
| VoxelTemplateCommand
| VoxelBlockCommand
| VoxelBlocksetCommand
| VoxelMaterialGroupCommand
| VoxelBlendGroupCommand;| Category | Guard | Actions list |
|---|---|---|
| Layer | isVoxelLayerCommand() | VOXEL_LAYER_COMMAND_ACTIONS |
| Template | isVoxelTemplateCommand() | VOXEL_TEMPLATE_COMMAND_ACTIONS |
| Block | isVoxelBlockCommand() | VOXEL_BLOCK_COMMAND_ACTIONS |
| Blockset | isVoxelBlocksetCommand() | VOXEL_BLOCKSET_COMMAND_ACTIONS |
| Material group | isVoxelMaterialGroupCommand() | VOXEL_MATERIAL_GROUP_COMMAND_ACTIONS |
| Blend group | isVoxelBlendGroupCommand() | VOXEL_BLEND_GROUP_COMMAND_ACTIONS |
| All | VOXEL_COMMAND_ACTIONS |
Within layer commands, isVoxelEditCommand() matches voxel edits and isVoxelObjectLayerCommand() matches object layer commands. isVoxelLayerGeometryCommand() matches voxel edits plus "position-updated" and "position-rebased", the commands that change a layer's bounds. Each guard accepts any { action: string }. VoxelCommandAction and the per-category *CommandAction types are the matching unions.
World and blockset document commands
The same commands also split by owner. A world saves and shares its layers, templates and blockset links; blocks and groups belong to a BlocksetDocument.
type VoxelWorldContentCommand = VoxelLayerCommand | VoxelTemplateCommand;
type VoxelWorldCommand = VoxelWorldContentCommand | VoxelBlocksetCommand;
type BlocksetDocumentCommand =
| VoxelBlockCommand
| VoxelMaterialGroupCommand
| VoxelBlendGroupCommand
| { action: "tile-size-updated"; tileSize: number; }
| { action: "material-group-renamed"; groupId: string; to: string; };isVoxelWorldCommand() and isBlocksetDocumentCommand() narrow to either side; VOXEL_WORLD_COMMAND_ACTIONS and BLOCKSET_DOCUMENT_COMMAND_ACTIONS list them. A sync adapter sends the world side of a document's stream to the world's room.
Applying commands
function applyVoxelCommand(
target: VoxelCommandTarget,
command: VoxelCommand,
logger?: VoxelLogger
): VoxelCommand | null;
function applyVoxelWorldCommand(
target: VoxelWorldCommandTarget,
command: VoxelWorldCommand,
logger?: VoxelLogger
): VoxelWorldCommand | null;
interface VoxelWorldCommandTarget {
readonly world: VoxelWorld;
readonly blocksets: BlocksetList;
}
interface VoxelCommandTarget extends VoxelWorldCommandTarget {
readonly blocks: BlockRegistry;
readonly materialGroups: MaterialGroupList;
readonly blendGroups: BlendGroupList;
}Apply a command to plain state, such as a server's, and return it as applied, or null when nothing changed. Neither emits. applyVoxelWorldCommand() needs no block registry or groups. document.applyCommand() uses applyVoxelCommand() and then emits the result.
Layer commands
Layer commands carry layerId (voxel layers) or layerName (object layers), and a metadata object. VoxelWorld emits them; the document forwards them as local commands.
action | metadata | Notes |
|---|---|---|
"added" | { name, rank, options } | Refused when layerId exists. name comes back unique. |
"removed" | {} | |
"updated" | { options: VoxelLayerUpdate } | options.name renames the layer and comes back unique. |
"cloned" | { cloneId, rank, options } | layerId is the source; options.name is the resolved name. |
"merged" | { targetLayerId } | layerId is the source, which the merge removes. |
"position-updated" | { position } or { delta } | |
"position-rebased" | { position } | |
"layer-moved" | { rank } | The layer's new rank. |
"voxel-set" | { position, blockId, rotation, flipX, flipZ, flipY, merge? } | merge only when set, see merged cells. |
"voxel-removed" | { position } | |
"voxels-set" | { entries: VoxelSetOptions[] } | |
"voxels-removed" | { entries: VoxelRemoveOptions[] } | |
"voxels-patched" | VoxelPatch ({ cells, partners? }) | From transaction(), patchVoxels(), undo/redo and template placement. Five numbers per cell: x, y, z, blockId, transform; block 0 removes. partners gives the second shape of merged cells, three numbers each: cell, blockId, transform, where cell is the index of a non-air cell of cells; left out when there are none. |
"layer-transformed" | { rotation, flipX, flipZ, flipY } | From transformLayer(). |
"object-layer-added" | {} | |
"object-layer-removed" | {} | |
"object-layer-updated" | { patch: { visible? } } | |
"object-added" | { object: VoxelObjectJSON } | |
"object-removed" | { objectId } | |
"object-moved" | { objectId, fromLayerName, toLayerName } | |
"object-updated" | { objectId, patch } |
voxelPatchCells(cells) iterates a "voxels-patched" payload as { x, y, z, blockId, transform } objects. VoxelPatchBuilder builds a patch one cell at a time, pickVoxelPatch(patch, cellIndices) keeps some cells with their partners renumbered, and assertVoxelPatch(patch) throws a RangeError for a malformed payload:
const patch = new VoxelPatchBuilder()
.push({ x: 0, y: 0, z: 0 }, packVoxel(1, 0))
.push({ x: 1, y: 0, z: 0 }, packVoxel(1, 0), packVoxel(2, 16))
.toPatch();
// { cells: [0, 0, 0, 1, 0, 1, 0, 0, 1, 0], partners: [1, 2, 16] }voxelPatchWrites(patch) iterates the same payload as { position, packed, partner }, with VOXEL_ABSENT for an air cell and for a cell without a partner.
voxelPatchesRestoring(changes, side) turns cell changes a recorder received into one patch per layer id. "before" restores what the changes replaced, the earliest change of a cell winning; "after" replays them, the latest winning.
Template commands
type VoxelTemplateCommand =
| { action: "template-defined"; template: VoxelTemplateJSON; }
| { action: "template-updated"; templateId: string; patch: VoxelTemplatePatch; }
| { action: "template-removed"; templateId: string; };"template-defined"carries the whole serialized template, voxels included, and replaces a template with the same id. Transforming a stored template emits it too."template-updated"changesname,pivotorproperties, never voxels."template-removed"is emitted only for an id that existed.
Block commands
type VoxelBlockCommand =
| { action: "block-defined"; block: ResolvedBlockDefinition; }
| { action: "block-removed"; blockId: number; }
| { action: "block-moved"; blockId: number; toIndex: number; };"block-defined"carries the resolved definition, default blockset filled in."block-removed"is emitted only for a registered ID."block-moved"carries the clamped index the block landed on.
Direct document.blocks calls emit nothing.
Blockset commands
type VoxelBlocksetCommand =
| { action: "blockset-added"; blockset: BlocksetDefinition; }
| { action: "blockset-removed"; blocksetId: string; };"blockset-added" carries the slot the blockset received. view.load(), view.loadBlockset() and direct document.blocksets mutations emit nothing.
Material group commands
type VoxelMaterialGroupCommand =
| { action: "material-group-defined"; group: MaterialGroupJSON; }
| { action: "material-group-removed"; groupId: string; };The emitted definition has every finish field filled in. See MaterialGroup.
Blend group commands
type VoxelBlendGroupCommand =
| { action: "blend-group-defined"; group: BlendGroupJSON; }
| { action: "blend-group-removed"; groupId: string; };The emitted definition has every setting filled in. See BlendGroup.