Toolbox
Open in GitHub

Xyz

State ownership primitives, a declarative Tweakpane debug layer, and Three.js scene warmup to kill first-frame hitches.

npm gzip

JOYCO's 3D toolkit. Keyed state ownership, typed definitions, CDN asset routing, ordered teardown, a declarative debug-pane layer over Tweakpane, and Three.js scene warmup that forces every shader compile and texture upload to happen before the player ever sees a hitch.

Mental model

Xyz has three tree-shakeable entrypoints:

  • Core (@joycostudio/xyz) — Derived and edge for state that multiple owners write to and one policy resolves; defineAssetSource for Chopper-aware CDN paths; Disposer for teardown that unwinds in reverse; Module and ModuleHost for ownership trees, selected-state sync, typed events, ticking, warmup, and inspection snapshots.
  • Debug (@joycostudio/xyz/debug) — DebugManager owns URL/keyboard activation and lazy pane creation; a declarative schema (slider, toggles, select, color, section, button, …) binds to a live Tweakpane folder without hand-wired glue.
  • Three (@joycostudio/xyz/three) — Warmup discovers a scene's full renderable graph, exposes it long enough to push every texture/shader through the driver once, then restores every mutation exactly as it found it.

Features

FeatureDescription
Keyed ownership (Derived)Independent writers claim a slot by key; value resolves all current intents through one explicit policy.
Transition sync (edge)Turns a selector into a callback that only fires when the selected value actually changes.
Asset routing (defineAssetSource)Keeps source names at call sites while selecting one output from a Chopper-compatible optimization plan.
Ordered teardown (Disposer)Registrations unwind in reverse order; disposal is idempotent and safe to register into after teardown.
Declarative debug schemaslider, sliders, bool, toggles, select, color, colorString, section, button build a DebugSchema with no Tweakpane import required.
URL + keyboard activationDebugManager lazy-mounts the pane on ?debug or Option+D, and stays in sync with popstate.
Survives enable/disableRegistrations remount from fresh definitions every cycle — no stale bindings after a toggle.
Scene warmupWarmup inventories geometries, materials, and textures (including TSL node graphs), pauses the driver, and forces one representative render before restoring visibility, culling, and draw ranges.
Late-resource auditingaudit() warns once per resource introduced after warmup — a throttled dev-only diagnostic, never a runtime upload.
No R3F dependencyThe Three.js utilities are raw Three.js — bring your own renderer/canvas ownership split.

Install

pnpm add @joycostudio/xyz

three and @tweakpane/core are optional peer dependencies — install them only if you use the /three or /debug subpaths.

defineAssetSource accepts Chopper's resolve function structurally, so Xyz does not require Chopper as a peer. Install @joycostudio/chopper in the consuming app when its config supplies the optimization plan.

Quick start

import { Derived, edge, maxOf } from '@joycostudio/xyz'
 
const blur = new Derived(0, maxOf)
blur.set('loader', 0.8)
blur.set('inspect', 0.35)
blur.value // 0.8
 
const syncPhase = edge(
  (state: { phase: string }) => state.phase,
  (next, previous) => console.log(previous, '→', next)
)

Asset routing

import chopper from './chopper.config'
import { defineAssetSource } from '@joycostudio/xyz'
 
export const asset = defineAssetSource({
  url: new URL('https://r2.joyco.studio/jam/'),
  planner: chopper.resolve,
  optimize: () =>
    typeof window === 'undefined' || new URLSearchParams(window.location.search).get('compressed') !== 'false',
  segments: {
    common: 'common',
    escapeFromHell: 'escape-from-hell',
  },
})
 
asset.common('textures/noise.png')
// https://r2.joyco.studio/jam/common/textures/compressed/noise.webp

The planner sees the stable source key without its query or hash. Exactly one planned output is selected; unmatched, failed, or multi-output plans fall back to the source path.

Quick start (modules)

import { Module, ModuleHost } from '@joycostudio/xyz'
 
class Counter extends Module<{ step: number }> {
  private count = 0
 
  advance() {
    this.emit('step', ++this.count)
  }
}
 
class Application extends Module {
  readonly counter = this.attach(new Counter())
 
  mount() {
    this.listen(this.counter, 'step', (count) => console.log(count))
  }
}
 
const host = new ModuleHost({
  ticker: { add: () => undefined }, // Supply your frame scheduler when modules tick.
  defaultTickPriority: 0,
})
const app = host.mount(new Application())
app.counter.advance()
host.dispose() // Stops listeners and disposes children before their owner.

Declare store selections with this.select(store, selector); the host shares one subscription per store and calls sync() when selected values change. Stores only need getState() and subscribe(); Zustand works directly without becoming an XYZ runtime dependency. Tickers supply milliseconds, and module tick(dt, time) receives seconds. Root modules choose priority; their children tick in ownership order.

host.inspect() returns detached ownership snapshots. host.subscribeActivity() captures bounded event/state traces on demand outside production. publishModuleTree, also exported from core, provides a same-origin development iframe bridge for a custom inspector. See the module reference and inspection reference.

Use named ESM imports so unused exports can be removed. Core utilities do not pull in the module runtime, inspector bridge, React, Tweakpane, or Three.js when bundled on their own. ModuleHost includes its snapshot and activity methods; activity recording starts only with a subscriber and is disabled in production. The iframe bridge is removable independently of the runtime.

Quick start (debug)

import { DebugManager, property, slider, bool } from '@joycostudio/xyz/debug'
import { Pane } from 'tweakpane'
 
const state = { amount: 0.5, enabled: true }
 
const debug = new DebugManager({
  createPane: () => new Pane({ title: 'Scene' }),
})
 
debug.bind({
  title: 'scene',
  schema: () => ({
    amount: slider(property(state, 'amount'), { min: 0, max: 1 }),
    enabled: bool(property(state, 'enabled')),
  }),
})

Open with ?debug=true in the URL, or press Option+D.

Quick start (three)

import { Warmup } from '@joycostudio/xyz/three'
 
const warmup = new Warmup({
  pause: () => (clock.running = false),
  resume: () => (clock.running = true),
  initTexture: (texture) => renderer.initTexture(texture),
  compile: (scene, camera) => renderer.compileAsync(scene, camera),
  render: () => renderer.render(scene, camera),
  nextFrame: () => new Promise((resolve) => requestAnimationFrame(() => resolve())),
})
 
await warmup.start(scene, camera) // one representative frame, then the scene is restored
warmup.audit(scene) // dev-only: warns about resources introduced after warmup

Architecture

LayerResponsibility
Core state (Derived, edge)State resolution primitives with no DOM or rendering dependency.
Module ownership (Module, ModuleHost)Renderer-independent lifecycle, shared store subscriptions, typed events, and one ticker callback per root.
Definitions and assetsLiteral-preserving configuration and pure source-to-CDN URL planning.
DisposerAn owner-local teardown stack shared by the debug and three layers (and your own code).
Debug schema (schema.ts)Pure DebugField/DebugDefinition builders — no Tweakpane import, fully testable in isolation.
Debug binding (bind.ts)Binds a DebugDefinition into a live FolderApi: tracks live-refresh fields, nested sections, and computed sources for copy.
DebugManagerOwns URL/keyboard activation and lazy pane creation; registrations survive disable/enable and remount fresh.
Warmup orchestrationWarmup pauses the driver, runs registered Warmable.warmup() hooks, uploads textures, compiles, renders once, then restores everything in reverse — even on failure.

Core API

State

ExportDescription
Derived<In, Out>.set(key, value) / .clear(key) claim or release a slot; .value resolves through combine.
maxOfResolve to the highest active value, never lower than the fallback.
latestOfResolve to the most recently claimed slot.
edge(select, onChange, equals?)Returns a sync function: runs select every call, onChange only when the result changes.
Disposer.add(teardown), .listen(target, type, fn, options?), .dispose() — reverse-order, idempotent.
defineAssetSource(definition)Create a callable CDN resolver with typed builders for every declared segment.

Debug

ExportDescription
DebugManager.register(source) / .bind(definition) to mount; .enable() / .disable() / .toggle() / .sync() control activation.
bindDebugSchema(host, definition, opts?)Binds a DebugDefinition into an existing Tweakpane folder; returns apply() / refresh() / values().
ref(read, write) / property(obj, key, changed?)Adapt a getter/setter pair, or an object property, into a ValueRef.
slider / slidersOne bounded numeric control, or several declared from an object's numeric keys at once.
bool / togglesOne boolean control, or several declared from an object's boolean keys at once.
select / enumFieldA string/number option control (enumField is an alias).
color / colorString / hexColorBind an RGB-compatible object, or a CSS color string (hexColor is an alias).
folder / sectionNest a DebugDefinition as a field; section adds a collapsed, copyable group.
buttonDeclare a sync or async debug action, with optional feedback text.
schemaValues(schema)Read every control's current value (recursing into folders, skipping buttons).

Three

ExportDescription
Warmup.add(entity) registers a Warmable; .start(scene, camera) runs once; .audit(scene); .dispose().
WarmableInterface for entities whose render state isn't already in the scene — implement a synchronous or asynchronous warmup() hook.
WarmupDriverRenderer-owned hooks the caller supplies: pause, resume, initTexture, compile, render, nextFrame.

Package exports

Import pathContents
@joycostudio/xyzDisposer, Derived, maxOf, latestOf, edge, defineAssetSource, and their types.
@joycostudio/xyzModule, ModuleHost, store/ticker contracts, ownership snapshots, and activity types.
@joycostudio/xyzpublishModuleTree, inspector protocol constants, ModuleTree, and TreeMessage.
@joycostudio/xyz/debugDebugManager, bindDebugSchema, ref, property, slider, sliders, bool, toggles, select, color, colorString, hexColor, enumField, folder, section, button, schemaValues, and their types.
@joycostudio/xyz/threeWarmup, discoverScene, and their types.

Docs

The Fumadocs Core-backed site in apps/docs keeps its authored pages in apps/docs/content/docs as Markdown/MDX. The site shell uses the JOYCO UI kit, while the content embeds React examples from apps/docs/demos, including raw Three.js examples with no React Three Fiber dependency.

pnpm docs

Open the printed local URL and choose a concept from the catalog. Each page starts with a live demo and its actual source files in tabs directly underneath, followed by the API reference. Source is read from the same files that run the demo, including supporting files for multi-file examples. Register examples in apps/docs/lib/demos.ts and embed them with <DemoPreview name="…" />. The debug runtime page demonstrates DebugManager, URL semantics, and Option+D handling, while the declarative schemas page collects the live Tweakpane helpers. Use pnpm docs:build to verify the production integration.

Authored flow and seq code fences render as server-rendered SVG diagrams using Trazo. Add title="Description" after the fence language for an accessible title. Trazo validates syntax during the docs build; Markdown endpoints retain the original fences. Readers can expand and copy the source, and wide graphs scroll without shrinking their labels. Diagram colors follow the site theme through CSS, without a client-side diagram renderer.


Development

pnpm install
pnpm test          # Vitest suite (run); pnpm test:watch for watch mode
pnpm typecheck      # tsc --noEmit, plus the docs app
pnpm build          # tsup bundles for core, modules, debug, and three
pnpm lint           # eslint --fix

Releases go through Changesets:

pnpm changeset          # describe the change and bump type in a feature PR

After the feature PR merges, the Release workflow creates or updates Version Packages; merging that generated PR triggers publication. Beta releases use the same flow with Changesets prerelease mode. Follow the release guide; versioning and publishing commands are reserved for GitHub Actions.

Related Toolboxs