Skip to content

API Reference

GlyphStudio’s backend exposes Tauri commands organized by domain. Commands marked with [live] are fully implemented; others are planned stubs.

Canvas commands [live]

CommandDescription
init_canvasInitialize pixel buffer with width/height, creates default layer, returns composited frame
get_canvas_stateReturn full composited RGBA frame + layer metadata + undo/redo state
write_pixelWrite a single pixel to a layer (legacy, outside stroke transactions)
read_pixelRead pixel from composite or specific layer (for color picker)

Stroke commands [live]

CommandDescription
begin_strokeOpen a stroke transaction with tool name and RGBA color; validates layer is editable
stroke_pointsAppend pixel coordinates to active stroke; records before/after patches per pixel
end_strokeCommit stroke to undo stack, clear redo stack, return composited frame

Undo/Redo commands [live]

CommandDescription
undoRevert the last committed stroke (applies before patches), return composited frame
redoRe-apply an undone stroke (applies after patches), return composited frame

Layer commands [live]

CommandDescription
create_layerAdd a new transparent layer, auto-name, set as active
delete_layerRemove a layer (cannot delete the last one)
rename_layerSet layer name
select_layerSet active layer for editing
set_layer_visibilityToggle layer visibility (hidden layers excluded from composite)
set_layer_lockToggle layer lock (locked layers reject stroke writes)
set_layer_opacitySet layer opacity (0.0–1.0, affects compositing)
reorder_layerMove layer to a new position in the stack

Project commands [live]

CommandDescription
new_projectCreate a new blank project with name, canvas size, color mode; initializes canvas state
open_projectLoad project from .pxs file, rehydrate canvas state
save_projectSerialize and persist project to .pxs file
get_project_infoGet current project metadata (id, name, path, dirty state) with frame
mark_dirtyMark the project as dirty after mutations
list_recent_projectsGet recent project list from local storage
export_pngExport composited frame as PNG file
export_frame_sequenceExport all frames as numbered PNG files (name_0001.png, …)
export_sprite_stripExport all frames as a single horizontal or vertical PNG strip

Recovery commands [live]

CommandDescription
autosave_recoveryWrite a recovery snapshot to the recovery directory
check_recoveryDetect recoverable projects from a previous unclean shutdown
restore_recoveryRestore a project from a recovery file, rehydrate canvas state
discard_recoveryDelete a recovery file without restoring

Selection commands [live]

CommandDescription
set_selection_rectSet rectangular selection bounds (x, y, width, height)
clear_selectionClear the current selection
get_selectionGet current selection bounds (or null)
copy_selectionCopy selected pixels from active layer to clipboard
cut_selectionCopy selected pixels then clear to transparent, return frame
paste_selectionPaste clipboard at selection origin (or top-left), return frame
delete_selectionClear selected pixels to transparent, return frame

Transform commands [live]

CommandDescription
begin_selection_transformExtract selected pixels into floating payload, clear source region
move_selection_previewMove payload to absolute offset from source origin
nudge_selectionNudge payload by relative delta (dx, dy)
commit_selection_transformStamp payload at final position, end session, return frame
cancel_selection_transformRestore original pixels, end session, return frame
flip_selection_horizontalFlip the floating payload horizontally
flip_selection_verticalFlip the floating payload vertically
rotate_selection_90_cwRotate the floating payload 90° clockwise
rotate_selection_90_ccwRotate the floating payload 90° counter-clockwise

Transform commands (except commit/cancel) return a TransformPreview:

interface TransformPreview {
sourceX: number;
sourceY: number;
payloadWidth: number;
payloadHeight: number;
offsetX: number;
offsetY: number;
payloadData: number[]; // RGBA flat array
frame: CanvasFrame; // Current canvas state (source cleared)
}

Timeline commands [live]

CommandDescription
get_timelineGet frame list, active frame, and canvas state
create_frameCreate a new blank frame with one layer, switch to it
duplicate_frameDeep copy current frame (all layers), switch to copy
delete_frameDelete frame by id (cannot delete last frame)
select_frameSwitch to frame by id, stash/restore layer data
rename_frameRename a frame by id
reorder_frameMove frame to a new position in the timeline
insert_frame_atInsert a blank frame at a specific position
duplicate_frame_atDeep copy current frame to a specific position
set_frame_durationSet or clear per-frame duration override (ms)
get_onion_skin_framesGet composited previous/next frame data for onion skin overlay

Timeline commands return a TimelineState:

interface TimelineState {
frames: FrameInfo[];
activeFrameIndex: number;
activeFrameId: string;
frame: CanvasFrame;
}
interface FrameInfo {
id: string;
name: string;
index: number;
durationMs: number | null; // per-frame timing override
}

get_onion_skin_frames returns an OnionSkinData:

interface OnionSkinData {
width: number;
height: number;
prevData: number[] | null; // composited RGBA of previous frame
nextData: number[] | null; // composited RGBA of next frame
}

Palette commands (planned)

CommandDescription
get_palette_catalogList available palettes and contracts
apply_palette_operationUpdate slots, create ramps, set contract, remap, quantize
preview_palette_remapNon-destructive remap preview with pixel counts

AI orchestration commands (planned)

CommandDescription
queue_ai_jobQueue a generation/analysis job with prompt, palette mode, candidate count
cancel_ai_jobCancel a running job
get_ai_jobGet job state and candidate references
accept_ai_candidateAccept a candidate as new layer, draft layer, or draft track
discard_ai_candidateMark candidate for cleanup

AI job types

region-draft · variant-proposal · cleanup · requantize · silhouette-repair · inbetween · locomotion-draft · workflow-run

Motion assistance commands [live]

CommandDescription
begin_motion_sessionStart a motion session, capture source pixels from selection/anchor/frame
generate_motion_proposalsGenerate deterministic motion proposals for the active session
get_motion_sessionGet current motion session state (or null)
accept_motion_proposalSelect a proposal for later commit
reject_motion_proposalDeselect the current proposal
cancel_motion_sessionCancel the session entirely, project unchanged
commit_motion_proposalCommit selected proposal as real timeline frames (insert after active)
undo_motion_commitUndo the last motion commit (remove inserted frames)
redo_motion_commitRedo an undone motion commit (re-insert stashed frames)
list_motion_templatesList available motion templates with anchor requirements
apply_motion_templateStart a motion session using a template (auto-selects best anchor)

Motion session commands return a MotionSessionInfo:

interface MotionSessionInfo {
sessionId: string;
intent: string; // idle_bob | walk_cycle_stub | run_cycle_stub | hop
direction: string | null; // left | right | up | down
targetMode: string; // active_selection | anchor_binding | whole_frame
outputFrameCount: number; // 2 or 4
sourceFrameId: string;
anchorKind: string | null; // head | torso | arm_left | ... (when anchor-targeted)
proposals: MotionProposalInfo[];
selectedProposalId: string | null;
status: string; // configuring | generating | reviewing | committing | error
}
interface MotionProposalInfo {
id: string;
label: string;
description: string;
previewFrames: number[][]; // RGBA flat arrays, one per generated frame
previewWidth: number;
previewHeight: number;
}

commit_motion_proposal, undo_motion_commit, and redo_motion_commit return a MotionCommitResult:

interface MotionCommitResult {
insertedFrameIds: string[]; // IDs of frames added to timeline
activeFrameId: string; // current active frame after operation
activeFrameIndex: number;
}

Anchor commands [live]

CommandDescription
create_anchorCreate an anchor on the active frame (kind, position, optional name)
update_anchorUpdate anchor position, name, or kind
delete_anchorRemove an anchor from the active frame
list_anchorsList all anchors on the active frame
bind_anchor_to_selectionBind the current selection rectangle as an anchor’s target region
clear_anchor_bindingClear the bound region from an anchor
move_anchorMove anchor to new position (for drag)
resize_anchor_boundsResize an anchor’s bound region
validate_anchorsCheck for duplicate names, out-of-canvas positions, empty bounds
copy_anchors_to_frameCopy anchors to a specific target frame (by name matching)
copy_anchors_to_all_framesCopy anchors to all other frames
propagate_anchor_updatesPush a single anchor’s changes to matching anchors on all frames
set_anchor_parentSet parent anchor by name (validates cycles, self-parenting, missing parent)
clear_anchor_parentClear parent, making anchor a root
set_anchor_falloffSet falloff weight (clamped 0.1–3.0) for hierarchy-scaled motion

Anchor commands return an AnchorInfo:

interface AnchorInfo {
id: string;
name: string;
kind: string; // head | torso | arm_left | arm_right | leg_left | leg_right | custom
x: number;
y: number;
bounds: { x: number; y: number; width: number; height: number } | null;
parentName: string | null;
falloffWeight: number; // 0.1–3.0, default 1.0
}

Hierarchy behavior:

  • delete_anchor clears parentName on any children referencing the deleted anchor
  • update_anchor with name change updates children’s parentName to the new name
  • propagate_anchor_updates includes parentName and falloffWeight in propagation
  • Secondary-motion amplitude scales by (1 + depth) * falloffWeight — deeper anchors move more

Sandbox commands [live]

CommandDescription
begin_sandbox_sessionOpen sandbox from a frame span — composites each frame, stores isolated previews. Never mutates project state.
get_sandbox_sessionReturn current sandbox session info (or null if none active)
close_sandbox_sessionClose sandbox session, free preview data
analyze_sandbox_motionDeterministic motion analysis — loop closure, drift, timing, issues. Requires active sandbox session.
get_sandbox_anchor_pathsExtract anchor paths across the sandbox frame span with per-frame coordinates, contact heuristics. Matches by name.
apply_sandbox_timingApply uniform duration to the sandbox span frames. Validates span still exists (stale-session check). Stays in sandbox after apply.
duplicate_sandbox_spanDeep-copy the sandbox span (layers, anchors, duration) and insert after the original. New IDs throughout. Jumps timeline to first new frame.

Sandbox sessions return a SandboxSessionInfo:

interface SandboxSessionInfo {
sessionId: string;
source: 'timeline_span' | 'motion_proposal';
startFrameIndex: number;
endFrameIndex: number;
frameCount: number;
previewFrames: number[][]; // composited RGBA per frame
previewWidth: number;
previewHeight: number;
}

Analysis returns a SandboxMetricsSummary:

interface SandboxMetricsSummary {
sessionId: string;
frameCount: number;
previewWidth: number;
previewHeight: number;
bboxes: (BBoxInfo | null)[]; // per-frame bounding box
adjacentDeltas: number[]; // normalized frame-to-frame deltas
loopDiagnostics: LoopDiagnostics; // first/last frame similarity
driftDiagnostics: DriftDiagnostics; // center-of-mass translation
timingDiagnostics: TimingDiagnostics; // cadence and abruptness
issues: DiagnosticIssue[]; // max 5, ordered by severity
}

Anchor paths return a SandboxAnchorPathsResult:

interface AnchorPathInfo {
anchorName: string;
anchorKind: string;
samples: AnchorPointSample[]; // per-frame {frameIndex, x, y, present}
contactHints: ContactHint[]; // {frameIndex, label, confidence}
totalDistance: number;
maxDisplacement: number;
}

Apply timing returns a SandboxTimingApplyResult:

interface SandboxTimingApplyResult {
sessionId: string;
framesAffected: number;
durationMs: number | null;
}

Duplicate span returns a SandboxDuplicateSpanResult:

interface SandboxDuplicateSpanResult {
sessionId: string;
newFrameIds: string[];
insertPosition: number;
firstNewFrameId: string;
}

Secondary motion commands [live]

CommandDescription
list_secondary_motion_templatesList all environmental/secondary motion templates with hints and hierarchy metadata.
apply_secondary_motion_templateBegin a motion session using a secondary template with direction, strength, frame count, and phase offset. Amplitude scales by anchor hierarchy depth and falloff weight.
check_secondary_readinessCheck template readiness against current frame anchors. Returns tier (ready/limited/blocked), anchor summary, hierarchy status, and fix hints.

Available templates: wind_soft, wind_medium, wind_gust, idle_sway, hanging_swing, foliage_rustle.

Parameters: direction (optional), strength (0.1–2.0), frameCount (2/4/6), phaseOffset (0–TAU).

Readiness returns a SecondaryReadinessInfo:

interface SecondaryReadinessInfo {
templateId: string;
templateName: string;
tier: 'ready' | 'limited' | 'blocked';
totalAnchors: number;
rootAnchors: string[];
childAnchors: string[];
hierarchyPresent: boolean;
hierarchyBeneficial: boolean;
notes: string[];
fixHints: string[];
}

Preset commands

CommandDescription
save_motion_presetSave a new preset with name, kind (locomotion/secondary_motion), anchors, and motion settings.
list_motion_presetsList all saved presets (summary only — ID, name, kind, anchor count, hierarchy flag).
get_motion_presetGet full preset document by ID (anchors, motion settings, timestamps).
delete_motion_presetDelete a preset by ID.
rename_motion_presetRename a preset. Returns updated summary.
apply_motion_presetApply a preset to the current frame — creates missing anchors, updates existing by name, skips at 8-anchor limit. Accepts optional overrides (strength, direction, phaseOffset). Returns PresetApplyResult.
apply_motion_preset_to_spanBatch-apply a preset to a range of frames (inclusive, 0-based). Max 64 frames. Accepts optional overrides. Returns BatchApplyResult.
apply_motion_preset_to_all_framesBatch-apply a preset to every frame. Accepts optional overrides. Returns BatchApplyResult.
check_motion_preset_compatibilityCheck how well a preset matches the current frame. Returns tier (compatible/partial/incompatible), matching/missing/extra anchors, and notes.
preview_motion_preset_applyNon-mutating preview — shows per-anchor diffs (create/update/skip), effective settings after overrides, and warnings. Accepts scope (“current”/“span”/“all”).

Apply result:

interface PresetApplyResult {
createdAnchors: string[];
updatedAnchors: string[];
skipped: string[];
warnings: string[];
appliedSettings?: PresetMotionSettings;
}

Batch apply result:

interface BatchApplyResult {
totalFrames: number;
appliedFrames: number;
skippedFrames: number;
perFrame: BatchFrameResult[];
summary: string[];
appliedSettings?: PresetMotionSettings;
}

Overrides (optional, does not modify saved preset):

interface PresetApplyOverrides {
strength?: number; // 0.1–2.0
direction?: string; // left/right/up/down
phaseOffset?: number; // 0–TAU
}

Preview result:

interface PresetPreviewResult {
presetName: string;
presetKind: MotionPresetKind;
anchorDiffs: PresetAnchorDiff[];
effectiveSettings: PresetMotionSettings;
warnings: string[];
scopeFrames: number;
}

Compatibility result:

interface PresetCompatibility {
tier: 'compatible' | 'partial' | 'incompatible';
matchingAnchors: string[];
missingAnchors: string[];
extraAnchors: string[];
wouldExceedLimit: boolean;
notes: string[];
}

Presets are persisted at %LOCALAPPDATA%/GlyphStudio/presets/{id}.preset.json (user-level, not project-embedded). Overrides are transient — they only affect the current apply and never mutate saved preset defaults.

Locomotion analysis commands (planned)

CommandDescription
analyze_locomotionAnalyze weight class, cadence, stride, contact timing, CoM path
plan_locomotionPropose motion plan with movement type and target feel
generate_locomotion_draft_trackGenerate constrained draft frames from a plan

Validation commands (planned)

CommandDescription
run_validationRun scoped or full validation across categories
preview_validation_repairNon-destructive repair preview
apply_validation_repairApply a suggested repair

Response format

All canvas/layer/stroke commands return a CanvasFrame:

interface CanvasFrame {
width: number;
height: number;
data: number[]; // RGBA flat array (width × height × 4)
layers: LayerInfo[]; // Layer metadata for the panel
activeLayerId: string | null;
canUndo: boolean;
canRedo: boolean;
}

Clip commands [live]

CommandArgsReturnsDescription
create_clipname: string, startFrame: number, endFrame: numberClipInfoCreate a named clip spanning the given frame range
list_clipsClipInfo[]List all clips in the project with validation warnings
update_clipclipId: string, name?, startFrame?, endFrame?, loopClip?, fpsOverride?, tags?ClipInfoUpdate any clip properties; broken ranges warn instead of silently clamping
delete_clipclipId: stringvoidRemove a clip definition
validate_clipsClipValidationResultValidate all clips against current frame topology without modifying anything
set_clip_pivotclipId: string, mode: PivotMode, customX?: number, customY?: numberClipInfoSet or update a clip’s pivot/origin point
clear_clip_pivotclipId: stringClipInfoRemove a clip’s pivot (revert to no pivot)
set_clip_tagsclipId: string, tags: string[]ClipInfoReplace all tags (normalized, deduped, max 16)
add_clip_tagclipId: string, tag: stringClipInfoAdd a single tag (normalized, deduped, rejects empty/over-limit)
remove_clip_tagclipId: string, tag: stringClipInfoRemove a tag by value

ClipInfo

type ClipValidity = 'valid' | 'warning' | 'invalid';
type PivotMode = 'center' | 'bottom_center' | 'custom';
interface PivotPoint { x: number; y: number; }
interface ClipPivot {
mode: PivotMode;
customPoint?: PivotPoint | null; // pixel coords for custom mode
}
interface ClipInfo {
id: string;
name: string;
startFrame: number; // 0-based inclusive
endFrame: number; // 0-based inclusive
frameCount: number;
loopClip: boolean;
fpsOverride: number | null;
tags: string[];
pivot: ClipPivot | null; // clip-level pivot/origin
warnings: string[]; // non-empty when range is questionable
validity: ClipValidity; // valid / warning / invalid
}
interface ClipValidationResult {
totalClips: number;
validCount: number;
warningCount: number;
invalidCount: number;
clips: ClipInfo[];
}

Export commands [live]

CommandArgsReturnsDescription
preview_sprite_sheet_layoutscope: ExportScope, layout: ExportLayoutExportPreviewResultNon-mutating layout preview — returns placement rects, dimensions, clip grouping, warnings
export_clip_sequenceclipId: string, dirPath: stringExportResultExport one clip as numbered PNG sequence; collision-safe naming
export_clip_sheetclipId: string, filePath: string, layout: ExportLayout, emitManifest?: boolean, manifestFormat?: ManifestFormatExportResultExport one clip as sprite sheet (strip or grid), optional manifest in chosen format; collision-safe naming
export_all_clips_sheetfilePath: string, layout: ExportLayout, emitManifest?: boolean, manifestFormat?: ManifestFormatExportResultExport all valid clips into one combined sheet; invalid clips skipped with warning; collision-safe naming
export_clip_sequence_with_manifestclipId: string, dirPath: string, manifestFormat?: ManifestFormatExportResultExport one clip as numbered PNGs + JSON manifest in chosen format; collision-safe naming

ExportScope

type ExportScope =
| { type: 'current_frame' }
| { type: 'selected_span'; start: number; end: number }
| { type: 'current_clip'; clipId: string }
| { type: 'all_clips' };

ExportLayout

type ExportLayout =
| { type: 'horizontal_strip' }
| { type: 'vertical_strip' }
| { type: 'grid'; columns?: number | null };

ManifestFormat

type ManifestFormat = 'glyphstudio_native' | 'generic_runtime';
  • glyphstudio_native (default): Rich manifest with export type, sheet dimensions, grid layout, generated timestamp, per-clip placements and files.
  • generic_runtime: Lean runtime manifest with just frame dimensions, per-clip start index, count, loop flag, fps, tags, pivot, and placement/file data. No timestamps or sheet metadata — designed for game engines.

ExportPreviewResult

interface ExportPreviewResult {
outputWidth: number;
outputHeight: number;
frameWidth: number;
frameHeight: number;
frameCount: number;
columns: number;
rows: number;
placements: ExportPreviewFramePlacement[];
clipGroups: ExportPreviewClipGroup[];
warnings: string[];
}

ExportResult

interface ExportResult {
files: ExportedFileInfo[];
manifest: ExportedFileInfo | null;
frameCount: number;
clipCount: number;
skippedClips: number; // invalid clips skipped (all-clips export)
wasSuffixed: boolean; // true if any filename was suffixed to avoid overwrite
warnings: string[];
}
interface ExportedFileInfo {
path: string;
width: number;
height: number;
}

Export settings persistence

Export settings are persisted in localStorage (user-local, not in the project file):

  • Scope, layout, selected clip, span range, manifest toggle + format, last output directory/file
  • Restored on panel mount with graceful fallback: missing clips fall back to first available, out-of-bounds spans are clamped
  • Export settings are never part of undo history or project data

Export Again

The Export Again button re-runs the most recent export to the same output path without showing a save dialog.

  • Only enabled when preview is fresh (not stale) and a previous export succeeded with a valid path
  • If the previously exported clip no longer exists, blocks with a clear error
  • Collision safety still applies — existing files are suffixed, never silently overwritten
  • When preview is stale, the panel shows a compact last-export summary with a “Preview again to re-export” hint

Asset catalog commands [live]

CommandArgsReturnsDescription
list_assetsAssetSummary[]List all catalog entries with file existence check
get_asset_catalog_entryassetId: stringAssetSummaryGet a single catalog entry by ID
upsert_asset_catalog_entryid?: string, name: string, filePath: string, kind: AssetKind, tags?: string[], canvasWidth?: number, canvasHeight?: number, frameCount?: number, clipCount?: number, thumbnailPath?: stringAssetSummaryInsert or update a catalog entry; creates new ID if none provided
remove_asset_catalog_entryassetId: stringbooleanRemove from catalog (does NOT delete the project file)
refresh_asset_catalogAssetSummary[]Re-check file existence for all entries
generate_asset_thumbnail— (uses current canvas)stringGenerate a 64×64 PNG thumbnail from the first frame of the open project; returns the thumbnail file path

AssetKind

type AssetKind = 'character' | 'prop' | 'environment' | 'effect' | 'ui' | 'custom';

AssetStatus

type AssetStatus = 'ok' | 'missing';

AssetSummary

interface AssetSummary {
id: string;
name: string;
filePath: string;
kind: AssetKind;
tags: string[];
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
canvasWidth: number;
canvasHeight: number;
frameCount: number;
clipCount: number;
thumbnailPath: string | null;
status: AssetStatus; // 'ok' or 'missing' based on file check
}

The catalog is stored at {data_local_dir}/GlyphStudio/asset-catalog.json, separate from project files. It is an index layer — removing a catalog entry never deletes the backing .pxs file.

Lifecycle sync

The catalog is automatically updated during project lifecycle operations:

  • save_project — upserts the current project’s catalog entry with fresh metadata (name, canvas size, frame/clip counts, timestamps, thumbnail). User-managed fields (kind, tags) are preserved from existing entries.
  • open_project — ensures the opened project exists in the catalog with current metadata and a fresh thumbnail.
  • New projects are cataloged on their first save (no file path = no catalog entry yet).
  • Save As (save with a new file path) creates or updates the entry for the new path. The old path entry remains independently.
  • Catalog sync is best-effort — failures never block the actual save/open operation.

Thumbnails

Thumbnails are 64×64 PNG images generated by nearest-neighbor downscale from the first composited frame. This preserves the crisp pixel-art look at small sizes. Thumbnails are stored at {data_local_dir}/GlyphStudio/thumbnails/{frame-id}.png with deterministic paths based on the first frame’s ID.

Thumbnails are generated automatically during lifecycle sync (save/open) and can be requested explicitly via generate_asset_thumbnail. If generation fails, the existing thumbnail is preserved; if no thumbnail exists, the asset browser shows a kind-badge placeholder instead.

Asset browser

The asset browser panel (mounted in the RightDock “Assets” tab) provides:

  • Thumbnail rendering — each row shows the asset’s thumbnail via convertFileSrc (Tauri local file → webview URL), with image-rendering: pixelated for crisp display. Falls back to a kind-badge placeholder (CHR, PRP, ENV, etc.) when no thumbnail is available or the image fails to load.
  • Search + filter + sort — search by name/tag/kind, filter by kind or status, sort by recent/alpha/kind.
  • Selection + quick preview — clicking a row selects it and opens a preview pane with larger thumbnail, full metadata (kind, canvas size, frames, clips, status, updated date, tags), file path, and an Open button. Clicking the same row toggles the preview closed. Selection persists across catalog refreshes (cleared only if the asset is removed).
  • Current-project highlight — the open project is marked with an accent border and “Open” badge. Path comparison is slash-normalized for cross-platform correctness.
  • Auto-refresh — the list refreshes after save/open lifecycle events.

Bundle Packaging

Commands

CommandArgsReturnsDescription
preview_asset_bundlebundleName: string, exportAction: 'sequence' | 'sheet' | 'all_clips_sheet', clipId?: string, layout: ExportLayout, manifestFormat?: ManifestFormat, contents?: ExportBundleContentsBundlePreviewResultPreview bundle file list before export (authoritative)
export_asset_bundleoutputPath: string, bundleName: string, format: 'folder' | 'zip', exportAction, clipId?, layout, manifestFormat?, contents?ExportBundleResultExport a portable asset bundle as folder or zip
preview_catalog_bundleassetIds: string[], includeManifest?: bool, includePreview?: boolCatalogBundlePreviewResultPreview a multi-asset catalog bundle (per-asset status + file counts)
export_catalog_bundleassetIds: string[], outputPath: string, bundleName: string, format: 'folder' | 'zip', includeManifest?, includePreview?, layout: ExportLayout, manifestFormat?CatalogBundleExportResultExport a multi-asset catalog bundle with per-asset subfolders

ExportBundleContents

interface ExportBundleContents {
images: boolean; // sprite sheet or sequence
manifest: boolean; // manifest JSON
preview: boolean; // 128×128 thumbnail
}

BundlePreviewResult

interface BundlePreviewResult {
files: BundlePreviewFile[];
estimatedBytes: number;
warnings: string[];
}

ExportBundleResult

interface ExportBundleResult {
outputPath: string;
format: 'folder' | 'zip';
files: string[];
totalBytes: number;
wasSuffixed: boolean;
warnings: string[];
}

Bundle structure

Bundles use a deterministic folder layout:

{bundle_name}/
images/ — sprite sheets or frame sequences
manifests/ — manifest JSON files
preview/ — optional 128×128 thumbnail (thumbnail.png)

For zip bundles, the folder is compressed and the intermediate folder removed. Collision-safe naming applies to both folder and zip outputs.

Bundle export is outside undo history — it’s a one-way output operation. The preview command is authoritative: what it lists is exactly what export will write.

Catalog bundle structure

Multi-asset catalog bundles use per-asset subfolders:

{bundle_name}/
assets/
{asset_name}/
images/ — sprite sheets or frame sequences
manifests/ — manifest JSON files
preview/ — optional thumbnail
{asset_name_2}/
...

Each asset’s .pxs file is loaded independently (does not affect the currently open project). Missing assets block export in the first pass. Selection is tracked by asset ID with stale-ID pruning on refresh.

CatalogBundlePreviewResult

interface CatalogBundlePreviewResult {
assets: CatalogBundleAssetEntry[]; // per-asset status
totalFiles: number;
warnings: string[];
}
interface CatalogBundleAssetEntry {
assetId: string;
assetName: string;
status: 'ok' | 'missing' | 'error';
fileCount: number;
warnings: string[];
}

CatalogBundleExportResult

interface CatalogBundleExportResult {
outputPath: string;
format: 'folder' | 'zip';
assetCount: number;
skippedCount: number;
files: string[];
totalBytes: number;
wasSuffixed: boolean;
warnings: string[];
}

Multi-select mode

The asset browser supports an explicit multi-select toggle for catalog packaging. When active:

  • Clicking an asset toggles its checkbox (no preview pane)
  • Actions bar shows selected count, “All” (select all visible), “Clear”
  • Hidden-by-filter count shown when filters hide selected assets (“3 selected (1 hidden)”)
  • Missing assets in selection show a warning with names and block export
  • “Clear missing” button removes missing assets from selection without clearing valid ones
  • Selection persists across catalog refresh (stale IDs pruned)
  • Exiting multi-select clears all selections

Package Again

Both single-asset and catalog packaging support a “Package Again” button:

  • Enabled only when preview is fresh (not stale) and a previous export succeeded
  • Re-exports to the same output directory without showing a file dialog
  • Blocked when settings change — shows “preview again to package” hint
  • Never bypasses validity checks or silently reuses outdated selection

Persisted packaging settings

Packaging settings are persisted locally via localStorage (not in project files):

  • Bundle format (folder/zip) — single-asset and catalog independently
  • Include manifest / include preview toggles
  • Last output directory
  • Last packaging mode (single/catalog)

Asset multi-selection is not persisted across app restarts. Format and toggle settings restore on panel mount.

Package Metadata

Commands

CommandArgsReturnsDescription
get_asset_package_metadataPackageMetadataGet the current project’s package metadata
set_asset_package_metadatapackageName?: string, version?: string, author?: string, description?: string, tags?: string[]PackageMetadataUpdate package metadata (partial update, marks project dirty)

PackageMetadata

interface PackageMetadata {
packageName: string; // defaults empty, UI defaults from project name
version: string; // defaults "0.1.0"
author: string; // optional
description: string; // optional, max 500 chars
tags: string[]; // optional, max 20
}

Persistence

Package metadata is stored in the project file (.pxs) alongside clip definitions and canvas data. Old projects without metadata open safely with default values via serde(default). The skip_serializing_if guard keeps old-format files clean when metadata is at defaults.

Manifest integration

When package metadata is set (non-default), a package block is included in all manifest outputs:

  • GlyphStudio Native manifests — full package object with packageName, version, author, description
  • Generic Runtime manifests — same package object (lean format still includes identity)
  • Bundle manifests — same package object

If all metadata fields are at defaults (empty name, version 0.1.0, no author/description), the package field is omitted entirely for clean output.

Scene Composition

Commands

CommandArgsReturnsDescription
new_scenename: string, width: number, height: numberSceneInfoCreate a new empty scene
open_scenefilePath: stringSceneInfoOpen an existing .pscn scene file
save_sceneSceneInfoSave scene to its known file path
save_scene_asfilePath: stringSceneInfoSave scene to a new file path
get_scene_infoSceneInfoGet info about the currently open scene
get_scene_instancesSceneAssetInstance[]Get all instances in the current scene
add_scene_instancesourcePath: string, assetId?: string, name?: string, x?: number, y?: number, clipId?: stringSceneAssetInstanceAdd an asset instance to the scene (rejects missing sources)
remove_scene_instanceinstanceId: stringbooleanRemove an instance from the scene
move_scene_instanceinstanceId: string, x: number, y: numberSceneAssetInstanceMove an instance to integer coordinates
set_scene_instance_layerinstanceId: string, zOrder: numberSceneAssetInstanceChange instance z-order
set_scene_instance_visibilityinstanceId: string, visible: booleanSceneAssetInstanceToggle instance visibility
set_scene_instance_opacityinstanceId: string, opacity: numberSceneAssetInstanceSet instance opacity (clamped 0.0–1.0)
set_scene_instance_clipinstanceId: string, clipId?: stringSceneAssetInstanceAssign a clip to an instance (null to clear)
set_scene_playback_fpsfps: numberSceneInfoSet global scene FPS (clamped 1–60)
set_scene_looplooping: booleanSceneInfoSet scene looping flag
get_scene_playback_stateScenePlaybackStateGet full playback state with resolved clip info per instance
list_source_clipssourcePath: stringSourceClipInfo[]List clips available in a source .pxs project
get_source_asset_framessourcePath: string, clipId?: stringSourceAssetFramesLoad composited frame images for a clip (base64 PNGs)
export_scene_framefilePath: string, tick: numberSceneExportResultExport camera-aware composited scene frame as PNG
set_scene_instance_parallaxinstanceId: string, parallax: numberSceneAssetInstanceSet per-instance parallax factor (clamped 0.1–3.0)
get_scene_cameraSceneCameraGet current scene camera state
set_scene_camera_positionx: number, y: numberSceneCameraSet camera center position
set_scene_camera_zoomzoom: numberSceneCameraSet camera zoom (clamped 0.1–10.0)
reset_scene_cameraSceneCameraReset camera to default (origin, zoom 1.0)
get_scene_camera_at_ticktick: numberSceneCameraGet resolved camera at a tick (evaluates keyframe interpolation)
list_scene_camera_keyframesSceneCameraKeyframe[]List all camera keyframes sorted by tick
add_scene_camera_keyframetick, x, y, zoom, interpolation?, name?SceneCameraKeyframe[]Add/replace keyframe at tick, returns all keyframes
update_scene_camera_keyframetick, x?, y?, zoom?, interpolation?, name?SceneCameraKeyframe[]Patch keyframe fields at tick
delete_scene_camera_keyframetick: numberSceneCameraKeyframe[]Delete keyframe, returns remaining keyframes
get_scene_timeline_summarySceneTimelineSummaryGet scene timeline span and timing info
seek_scene_ticktick: numberSceneTimelineSummaryValidate seek target against timeline
unlink_scene_instance_from_sourceinstanceId: stringSceneAssetInstanceSever source relationship — sets characterLinkMode to 'unlinked'. Rejects non-character or already-unlinked instances
relink_scene_instance_to_sourceinstanceId: stringSceneAssetInstanceRestore source relationship — clears characterLinkMode. Rejects non-character or not-currently-unlinked instances
restore_scene_instancesinstances: SceneAssetInstance[]SceneAssetInstance[]Replace all scene instances atomically (used by undo/redo backend sync). Sets scene dirty flag.
get_scene_provenanceSceneProvenancePayloadGet persisted provenance entries and drilldown map from current scene
sync_scene_provenanceprovenance: PersistedSceneProvenanceEntry[], provenance_drilldown: HashMap<String, ...>()Write frontend provenance state to in-memory SceneDocument before save

SceneAssetInstance

interface SceneAssetInstance {
instanceId: string;
sourcePath: string; // path to .pxs source
assetId?: string; // optional catalog ID
name: string; // display name
clipId?: string; // which clip to play
x: number; // scene position
y: number;
zOrder: number; // higher = in front
visible: boolean;
opacity: number; // 0.0–1.0
parallax: number; // 1.0 = normal, <1.0 = bg, >1.0 = fg
}

SceneCamera

interface SceneCamera {
x: number; // camera center X
y: number; // camera center Y
zoom: number; // 1.0 = 100%
name?: string; // optional label
}

SceneCameraKeyframe

interface SceneCameraKeyframe {
tick: number; // tick at which this key takes effect
x: number; // camera X position
y: number; // camera Y position
zoom: number; // zoom factor
interpolation: 'hold' | 'linear';
name?: string; // optional shot/key label
}

SceneCameraShot (frontend-derived)

interface SceneCameraShot {
name: string; // from keyframe name, or "Shot N"
startTick: number; // inclusive
endTick: number; // exclusive (next shot start or scene end)
durationTicks: number;
interpolation: 'hold' | 'linear';
keyframeIndex: number; // index into sorted keyframes array
}

Derived via deriveShotsFromCameraKeyframes(keyframes, totalTicks) — each keyframe defines a shot segment that runs until the next keyframe.

CameraTimelineMarker (frontend-derived)

interface CameraTimelineMarker {
tick: number;
x: number;
y: number;
zoom: number;
interpolation: 'hold' | 'linear';
name: string | undefined;
index: number; // sorted position
}

Derived via deriveCameraTimelineMarkers(keyframes) — sorted marker positions for the camera timeline lane.

Character → scene bridge helpers

Pure functions exported from @glyphstudio/state for the character scene bridge.

HelperSignaturePurpose
placeCharacterBuild(build, options?) → SceneAssetInstanceCreate a character scene instance from a build (snapshot-first)
reapplyCharacterBuild(instance, build) → SceneAssetInstance | nullRefresh character snapshot while preserving scene-local state
checkPlaceability(build, issues) → PlaceabilityResultCheck if a build can be placed (errors block, warnings allowed)
isCharacterInstance(instance) → booleanCheck if a scene instance is character-derived
isSourceBuildAvailable(instance, buildIds) → booleanCheck if source build exists in library
deriveSourceStatus(instance, buildIds) → CharacterSourceStatusClassify as 'linked', 'missing-source', 'unlinked', or 'not-character'
sourceStatusLabel(status) → stringHuman-readable status label
instanceBuildName(instance) → stringBuild name with “Unknown build” fallback
snapshotSummary(instance) → stringSnapshot text (e.g. “4/12 equipped”)
isSnapshotPossiblyStale(instance, sourceBuild?) → booleanLightweight staleness check (count + name heuristic); returns false for unlinked
createSlotSnapshot(build) → CharacterSlotSnapshotCreate a frozen slot snapshot from a build
canReapplyFromSource(instance, buildIds) → booleanTrue when linked + source exists (reapply is lawful)
canRelinkToSource(instance, buildIds) → booleanTrue when unlinked + source exists (relink is lawful)
unlinkFromSource(instance) → SceneAssetInstanceSever source relationship; preserves snapshot and overrides
relinkToSource(instance) → SceneAssetInstanceRestore source relationship; does not mutate snapshot
effectiveCompositionAsBuild(instance) → CharacterBuild | nullSynthetic build from effective composition for compatibility checks

Character instance override helpers

HelperSignaturePurpose
applyOverridesToSnapshot(snapshot?, overrides?) → EffectiveSlotCompositionApply local overrides to a snapshot
deriveEffectiveSlots(instance) → EffectiveSlotCompositionEffective slot composition (snapshot + overrides)
deriveEffectiveCharacterSlotStates(instance) → EffectiveCharacterSlotState[]Per-slot UI-ready state for all 12 canonical slots
setSlotOverride(instance, override) → SceneAssetInstanceSet a local override (immutable)
clearSlotOverride(instance, slotId) → SceneAssetInstanceClear a single override (immutable)
clearAllOverrides(instance) → SceneAssetInstanceClear all overrides (immutable)
hasOverrides(instance) → booleanCheck if any overrides exist
getOverrideCount(instance) → numberCount of local overrides
overrideSummary(instance) → stringCompact summary (e.g. “2 local overrides”)
effectiveSlotSummary(instance) → stringEffective slot count (e.g. “4/12 effective”)

Camera timeline lane helpers

HelperSignaturePurpose
deriveCameraTimelineMarkers(keyframes) → CameraTimelineMarker[]Sorted markers for lane rendering
deriveShotsFromCameraKeyframes(keyframes, totalTicks) → SceneCameraShot[]Shot segments between keyframes
findCurrentCameraShotAtTick(shots, tick) → SceneCameraShot | nullWhich shot contains the given tick
findCameraKeyframeAtTick(keyframes, tick) → { keyframe, index } | nullExact keyframe at a tick

All helpers are pure functions exported from @glyphstudio/state. The camera timeline lane uses these to project cameraKeyframes[] into visual elements without maintaining a separate data model.

Scene history helpers

Pure functions and types exported from @glyphstudio/state for the scene undo/redo system.

Contract layer (sceneHistory)

ExportTypePurpose
SceneHistoryOperationKindtypeUnion of 20 operation kind strings
SceneHistorySnapshottype{ instances: SceneAssetInstance[], camera?: SceneCamera }
SceneHistoryEntrytypeBefore/after snapshots + kind + metadata + timestamp
SceneHistoryOperationMetadatatypeOptional instanceId, camera, override metadata
ALL_SCENE_HISTORY_OPERATION_KINDSconstArray of all 20 operation kind strings
describeSceneHistoryOperationfnHuman-readable label for an operation kind
isSceneHistoryChangefnDetect no-op (identical before/after instances)
createSceneHistoryEntryfnBuild a history entry from before/after + kind + metadata
captureSceneSnapshotfnCreate a snapshot from an instance array

Engine layer (sceneHistoryEngine)

ExportTypePurpose
SceneHistoryStatetypePast/future stacks + maxEntries + isApplyingHistory
createEmptySceneHistoryStatefnFresh state with empty stacks
canUndoScenefnWhether undo is available
canRedoScenefnWhether redo is available
recordSceneHistoryEntryfnPush entry onto past, clear future
undoSceneHistoryfnPop past → return snapshot + push to future
redoSceneHistoryfnPop future → return snapshot + push to past
finishApplyingHistoryfnClear isApplyingHistory flag
applySceneEditWithHistoryfnDetect no-op, record entry, return new state

Provenance layer (sceneProvenance)

ExportTypePurpose
SceneProvenanceEntrytypeAppend-only entry: sequence, kind, label, timestamp, metadata
createSceneProvenanceEntryfnBuild a provenance entry from kind + metadata (auto-sequences)
describeSceneProvenanceEntryfnLabel enrichment with instanceId, slotId, changedFields
resetProvenanceSequencefnReset sequence counter (called on scene change)
setProvenanceSequencefnSet sequence counter to a specific value (used during hydration to continue from persisted max)
peekProvenanceSequencefnCurrent next sequence value (testing only)

Drilldown layer (sceneProvenanceDrilldown)

ExportTypePurpose
SceneProvenanceDifftypeDiscriminated union — 20 diff variants keyed by type
SceneProvenanceDrilldowntypeDiff + entry metadata (label, timestamp, sequence)
SceneProvenanceDrilldownSourcetypeCaptured before/after instance slices + kind + metadata + optional beforeCamera/afterCamera + optional beforeKeyframe/afterKeyframe
captureProvenanceDrilldownSourcefnExtract focused before/after slices at edit seam (by metadata instanceId)
deriveProvenanceDifffnDerive typed diff from captured source slices
deriveProvenanceDrilldownfnWrap diff with provenance entry metadata
describeProvenanceDifffnHuman-readable one-line description of a diff

Store layer (sceneEditorStore)

ExportTypePurpose
useSceneEditorStoreZustand storeCentralized scene instances + history + provenance
SceneEditorStatetypeStore shape (instances, history, provenance, actions)
SceneUndoRedoResulttype{ instances: SceneAssetInstance[], rollback: () => void, camera?: SceneCamera }

Store state:

FieldTypeDescription
instancesSceneAssetInstance[]Current authoritative frontend scene state
historySceneHistoryStateUndo/redo stacks
provenanceSceneProvenanceEntry[]Persisted append-only activity log (restored on scene load)
drilldownBySequenceRecord<number, SceneProvenanceDrilldownSource>Captured before/after slices keyed by provenance sequence
canUndobooleanWhether undo is available
canRedobooleanWhether redo is available
cameraSceneCamera | undefinedLast committed camera state (for history snapshots)
keyframesSceneCameraKeyframe[]Authored camera keyframes (for history snapshots)

Store actions:

ActionSignatureDescription
loadInstances(instances) → voidLoad from backend without history or provenance (refresh, initial load)
loadCamera(camera) → voidLoad camera state without history (initial load, backend sync)
loadKeyframes(keyframes) → voidLoad keyframes without history (initial load, backend sync)
applyEdit(kind, nextInstances, metadata?, nextCamera?, nextKeyframes?) → voidRecord edit with history, provenance, and drilldown capture. Camera edits pass nextCamera; keyframe edits pass nextKeyframes.
undo() → SceneUndoRedoResult | undefinedUndo with rollback closure for backend sync failure
redo() → SceneUndoRedoResult | undefinedRedo with rollback closure for backend sync failure
loadPersistedProvenance(provenance, drilldownBySequence) → voidHydrate persisted provenance and drilldown into the store; sets sequence counter to max(persisted) + 1
resetHistory() → voidClear history stacks, provenance log, drilldown captures, and sequence counter (scene change / new scene)

Authored operation parity coverage

All 20 scene operation kinds are fully covered across history, provenance, drilldown, UI rendering, and persistence. No operation falls through to generic labeling.

Operation kinds by family:

FamilyOperation kindMetadataDrilldown renderer
Instanceadd-instanceinstanceIdInstanceDiffView
Instanceremove-instanceinstanceIdInstanceDiffView
Instancemove-instanceinstanceIdInstanceDiffView
Instanceset-instance-visibilityinstanceIdInstanceDiffView
Instanceset-instance-opacityinstanceIdInstanceDiffView
Instanceset-instance-layerinstanceIdInstanceDiffView
Instanceset-instance-clipinstanceIdInstanceDiffView
Instanceset-instance-parallaxinstanceIdInstanceDiffView
Character sourcereapply-character-sourceinstanceIdInstanceDiffView
Character sourceunlink-character-sourceinstanceIdInstanceDiffView
Character sourcerelink-character-sourceinstanceIdInstanceDiffView
Character overrideset-character-overrideinstanceId, slotIdCharacterOverrideDiffView
Character overrideremove-character-overrideinstanceId, slotIdCharacterOverrideDiffView
Character overrideclear-all-character-overridesinstanceIdCharacterOverrideDiffView
Cameraset-scene-cameracamera fieldsCameraDiffView
Playback configset-scene-playbackbeforePlayback, afterPlaybackPlaybackDiffView
Keyframeadd-camera-keyframetickKeyframeDiffView
Keyframeremove-camera-keyframetickKeyframeDiffView
Keyframemove-camera-keyframetick, previousTickKeyframeDiffView
Keyframeedit-camera-keyframetick, changedFieldsKeyframeDiffView

Persistence shapes:

Drilldown fieldUsed byContents
beforeInstance / afterInstanceInstance, character, override opsFull SceneAssetInstance snapshot
beforeCamera / afterCameraCamera opsSceneCamera (x, y, zoom)
beforeKeyframe / afterKeyframeKeyframe opsSceneCameraKeyframe (tick, x, y, zoom, interpolation, name?)
beforePlayback / afterPlaybackPlayback opsScenePlaybackConfig (fps, looping)

Structured value summary helpers (structuredValueSummary)

ExportTypePurpose
FieldConfigtypeField key + label + optional formatter
FieldChangetypeExtracted changed field with formatted before/after
StructuredValueSummarytypeChanges array + changedFieldKeys + isNoOp + description
SummaryFamilytype'scalar' | 'position' | 'multi-field' | 'state-transition' | 'fallback'
extractChangedFieldsfnCompare before/after objects, return only changed fields in config order
summarizeMultiFieldChangefnFull multi-field summary with compact description
summarizeScalarChangefnSingle-field before/after summary
fallbackSummaryfnHonest fallback when structure is unknown
classifySummaryFamilyfnClassify field configs into a summary family
fmtNumberfnFormat number with up to 1 decimal
fmtPercentfnFormat 0–1 value as percentage
fmtBoolfnFormat boolean as Yes/No
CAMERA_FIELD_CONFIGSconstCamera fields: Pan X, Pan Y, Zoom
KEYFRAME_FIELD_CONFIGSconstKeyframe fields: X, Y, Zoom, Interpolation, Name
POSITION_FIELD_CONFIGSconstPosition fields: X, Y
PLAYBACK_FIELD_CONFIGSconstPlayback fields: FPS, Looping

Scene comparison helpers (sceneComparison)

ExportTypePurpose
SceneComparisonModetype'current-vs-entry' | 'entry-vs-entry'
SceneComparisonSnapshottypeScene state at one point: instances, camera?, keyframes?, playbackConfig?
SceneComparisonAnchortypeTagged union: { type: 'current' } or { type: 'entry' } with snapshot
SceneComparisonRequesttypeResolved pair of anchors (left = older, right = newer)
SceneComparisonResulttypeFull comparison across instances, camera, keyframes, playback
InstanceComparisonSectiontypeInstance domain: added/removed/changed/unchanged counts + entries
InstanceComparisonEntrytypeSingle instance with status and field-level diffs
InstanceFieldDifftypeSingle field diff: field, label, before, after
CameraComparisonSectiontypeCamera domain: status, before/after, changedFields
KeyframeComparisonSectiontypeKeyframe domain: status, entries by tick
KeyframeComparisonEntrytypeSingle keyframe with status and changedFields
PlaybackComparisonSectiontypePlayback domain: status, before/after, changedFields
RestorePreviewResulttypeRestore impact: comparison + noImpact flag + label
ComparisonUnavailableReasontypeWhy a section is unavailable
createCurrentAnchorfnBuild anchor from current live scene state
createEntryAnchorfnBuild anchor from provenance entry + drilldown source
createComparisonRequestfnCreate request from two anchors, infers mode
validateComparisonRequestfnValidate request well-formedness
describeComparisonfnHuman-readable label (e.g., “#3 vs Current”)
resolveComparisonScopesfnDetermine which domains are comparable
deriveSceneComparisonfnPure derivation engine: anchors → structured result
deriveRestorePreviewfnRestore impact: entry anchor + current snapshot → RestorePreviewResult

SceneTimelineSummary

interface SceneTimelineSummary {
fps: number;
looping: boolean;
totalTicks: number; // longest clip span, minimum 1
totalDurationMs: number;
contributingInstances: number;
longestClipFrames: number;
}

SceneInfo

interface SceneInfo {
sceneId: string;
name: string;
canvasWidth: number;
canvasHeight: number;
instanceCount: number;
fps: number;
looping: boolean;
filePath: string | null;
dirty: boolean;
}

ScenePlaybackState

interface ScenePlaybackState {
fps: number;
looping: boolean;
instances: InstanceClipState[];
}
interface InstanceClipState {
instanceId: string;
clipId: string | null;
clipName: string | null;
frameCount: number;
clipFps: number | null; // clip FPS override, null = use scene FPS
clipLoop: boolean;
status: 'resolved' | 'no_clip' | 'missing_source' | 'missing_clip' | 'no_clips_in_source';
}

SourceClipInfo / SourceAssetFrames / SceneExportResult

interface SourceClipInfo {
id: string; name: string; startFrame: number; endFrame: number;
frameCount: number; loopClip: boolean; fpsOverride: number | null;
}
interface SourceAssetFrames {
width: number; height: number;
frames: string[]; // base64-encoded PNGs
clipId: string | null; frameCount: number;
}
interface SceneExportResult {
outputPath: string; width: number; height: number;
warnings: string[]; // per-instance issues
}

Clip resolution policy

Instance stateRendering behaviorPanel display
resolvedAnimated frames from assigned clipClip name (green)
no_clipStatic first frame”none” (italic)
missing_sourceWarning placeholder with dashed border”(source missing)” (orange)
missing_clipFirst-frame fallback”(missing)” (orange)
no_clips_in_sourceStatic first frame”(no clips)” (orange)

Persistence

Scene files use the .pscn extension and are stored separately from .pxs sprite project files. Scenes reference assets by file path and optional catalog ID — they do not embed source asset content. Missing source files degrade gracefully (placeholder state, no crash).

Design decisions

  • Scenes are a separate artifact type from sprite projects
  • Global scene clock first; per-instance offsets deferred
  • Scene transforms in 10A: move, visibility, opacity, z-order only
  • Scene export starts with current composited frame (PNG)
  • Scene undo/redo uses full-snapshot history in TypeScript state, separate from canvas stroke undo in Rust
  • Scene operations do not corrupt sprite project undo history
  • Missing source assets render as placeholder boxes (no crash)
  • Adding missing/non-loadable assets is rejected at command level
  • Instance positions are integer-only
  • New instances default to center of scene, topmost z-order
  • Scene workspace has its own canvas, separate from sprite editor

Scene workspace

The Scene tab in the top bar activates a dedicated workspace:

  • Scene canvas — dark stage with grid overlay, scene bounds visible
  • Instance rendering — animated frames composited by z-order, blob-URL cached per clip
  • Click to select — shows selection outline
  • Drag to move — integer coordinates, responsive local update with backend commit on mouse-up
  • Add Asset — dropdown populated from asset catalog (missing assets filtered out)
  • Instances panel — right dock shows all instances sorted by z-order with visibility toggle, bring forward/send backward, remove, opacity slider, clip picker, parallax depth control with BG/MG/FG presets
  • Camera controls — pan (middle-click drag), zoom (scroll wheel or +/−/reset buttons), camera state persists in scene file
  • Undo/redo — toolbar buttons and keyboard shortcuts (Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y); full-snapshot scene history with backend sync via restore_scene_instances; rollback on sync failure
  • Activity panel — read-only scene provenance timeline in the Activity tab; shows successful forward edits with labels, timestamps, and metadata; newest-first ordering; persisted with scene document (restored and new entries share one unified timeline); click any entry to open drilldown pane showing the captured change with operation-aware before/after rendering
  • Parallax depth — per-instance parallax factor (0.1–3.0); camera movement reveals depth separation between layers
  • Playback controls — stop/step-back/play-pause/step-forward/loop, FPS input, scrubber, tick/time readout
  • Scene scrubber — draggable timeline scrubber with jump-to-start/end; scrubbing pauses playback, play resumes from scrubbed position
  • Missing-source survivability — missing sources render as warning placeholder with dashed border; missing clips show orange warning border with fallback frame
  • Scene export — camera-aware composition at current tick; export reflects camera pan, zoom, parallax, and current animation frame
  • Camera timeline lane — dedicated lane in the scene timeline showing keyframe markers and shot span bars:
    • Keyframe markers rendered at exact tick positions (diamond for linear, square for hold interpolation)
    • Shot bars span from keyframe to next keyframe (last shot extends to End)
    • Click marker or shot bar to select source keyframe and seek playhead
    • Lane header shows current shot name at playhead
    • Lane toolbar: add key at playhead, delete selected, previous/next key, jump to selected
    • Empty state shows placeholder message when no camera keyframes exist
    • All lane visuals derive from deriveCameraTimelineMarkers(), deriveShotsFromCameraKeyframes(), and findCurrentCameraShotAtTick() — no separate lane data model
    • Selection syncs bidirectionally with the Camera Keyframe Panel via shared selectedKeyframeTick state

Defaults

  • packageName defaults empty; the UI pre-fills from the project name
  • version defaults to 0.1.0
  • author and description are optional; empty values are never serialized
  • Missing optional fields never block export or packaging

Sprite editor (frontend-only)

The sprite editor operates entirely within the React/TypeScript frontend — no Tauri commands are involved. All state lives in useSpriteEditorStore (Zustand).

Domain types (@glyphstudio/domain)

interface SpriteLayer {
id: string;
name: string;
visible: boolean;
index: number;
}
interface SpriteFrame {
id: string;
name: string;
index: number;
durationMs: number;
layers: SpriteLayer[];
}
interface SpriteDocument {
id: string;
name: string;
width: number;
height: number;
frames: SpriteFrame[];
palette: SpritePalette;
createdAt: string;
updatedAt: string;
}
interface SpritePixelBuffer {
width: number;
height: number;
data: Uint8ClampedArray; // RGBA row-major
}

Store state

FieldTypeDescription
documentSpriteDocument | nullActive sprite document
pixelBuffersRecord<string, SpritePixelBuffer>Pixel data keyed by layerId
activeFrameIndexnumberCurrently selected frame
activeLayerIdstring | nullLayer receiving paint operations
activeToolSpriteToolIdCurrent tool (pencil/eraser/fill/eyedropper/select)
toolConfigSpriteToolConfigBrush size, shape, colors
selectionSpriteSelectionRect | nullActive selection bounds
clipboardSpritePixelBuffer | nullCopied pixel data
undoStack / redoStackSpriteEditorState[]Undo/redo snapshots
onionSkinSpriteOnionSkinOnion skin settings (enabled, prev/next opacity)
isPlayingbooleanAnimation playback state
fpsnumberPlayback frames per second
loopbooleanLoop playback
isDirtybooleanUnsaved changes flag

Store actions

Document lifecycle

ActionDescription
newDocument(name, w, h)Create document with one frame, one layer, blank buffer
closeDocument()Clear document and all state
undo() / redo()Navigate undo/redo stacks

Frame management

ActionDescription
addFrame()Append frame with one layer and blank buffer
duplicateFrame()Deep copy active frame (all layers and buffers)
removeFrame(frameId)Delete frame and all its layer buffers
setActiveFrame(index)Switch frame, update activeLayerId
reorderFrame(from, to)Move frame in timeline

Layer management

ActionDescription
addLayer()Add layer to active frame, create blank buffer, set as active
removeLayer(layerId)Remove layer and buffer; update activeLayerId if needed
setActiveLayer(layerId)Set which layer receives paint operations
toggleLayerVisibility(layerId)Toggle layer visible/hidden
renameLayer(layerId, name)Update layer display name
moveLayer(fromIndex, toIndex)Reorder layer within frame stack

Pixel operations

ActionDescription
commitPixels(buffer)Write pixel data to activeLayerId buffer
cutSelection()Copy selection pixels, clear to transparent
copySelection()Copy selection pixels to clipboard
pasteSelection()Paste clipboard at selection origin
deleteSelection()Clear selection pixels to transparent

Import/Export

ActionDescription
importSpriteSheet(data, cols, rows, w, h)Slice image into frames with layer-keyed buffers
exportSpriteSheet()Flatten visible layers per frame, return composite strip
exportCurrentFrame()Flatten visible layers of active frame

Compositing helper

function flattenLayers(
layers: SpriteLayer[],
pixelBuffers: Record<string, SpritePixelBuffer>,
width: number,
height: number
): SpritePixelBuffer

Composites visible layers bottom-to-top using source-over alpha blending. Hidden layers are skipped. Missing buffers are treated as transparent. Returns a new SpritePixelBuffer — never mutates inputs.

Keyboard shortcuts (sprite editor)

ShortcutAction
Ctrl+ZUndo
Ctrl+Y / Ctrl+Shift+ZRedo
Ctrl+CCopy selection
Ctrl+XCut selection
Ctrl+VPaste
DeleteDelete selection pixels
EscapeClear selection
SpacePlay/pause animation
XSwap foreground/background color
Left/Right arrowPrevious/next frame
Scroll wheelZoom in/out

Events (planned)

EventPayload
job:queuedjobId, type
job:progressjobId, progress, stage
job:succeededjobId, candidateIds
job:failedjobId, error
project:autosave_updatedprojectId, savedAt
project:recovery_availableprojectId, recoveryBranchId