# Ludic Assembly · schema 0.1

A private TypeScript/ESM library for deterministic low-poly asset construction. Version 0.1 is the initial versioned contract. Qualification records the selected model corpus, bounded repair policy, independent installation and browser checks; visual acceptance remains a separate claim. The current version is recorded in `package.json`.

Live documentation: https://libraries.ludic-assembly.victorqueiroz.dev/

## Install

Use the private npm registry at GitHub Packages. Add the registry scope to a project `.npmrc`:

```ini
@victorqueiroz:registry=https://npm.pkg.github.com
```

Authenticate locally using a GitHub Packages credential with read access, stored outside the repository. In GitHub Actions, grant the consuming repository access to the package, use `packages: read`, and provide its `GITHUB_TOKEN` to `actions/setup-node` through `NODE_AUTH_TOKEN`. The release workflow publishes with `packages: write`. Never commit a token or a resolved credential file. The package is private even though this documentation is public.

```sh
npm install @victorqueiroz/ludic-assembly@0.1.0
```

## Core

```ts
import { compileAsset, validateAsset, type AssetSpec } from "@victorqueiroz/ludic-assembly";

const diagnostics = validateAsset(spec); // [] when schema AND geometry are valid
const compiled = compileAsset(spec); // throws AssetCompileError otherwise
```

`compileAsset(unknown, options?)` returns material mesh layers, resolved part instances and world translations, per-instance bounds and anchors, overall bounds, the declared units and ground pivot, triangle count and a SHA-256 source hash. `validateAsset` runs the same semantic pipeline and returns structured diagnostics. Inputs are not mutated.

`options.maxInstances` defaults to 1024; `options.maxTriangles` defaults to 200000. Structural limits allow at most 256 part definitions, groups, materials and parameters each. Dimensions must be positive and at most 1000000 declared units; bevels and edge insets may be zero. Values must be finite. Primitive dimensions remain in declared units: the compiler does not silently convert tiles to meters.

The structural JSON schema is exported at `@victorqueiroz/ludic-assembly/schema`. It checks spelling, types and required fields. Reference resolution, cycles, finite runtime values, geometric feasibility and budgets require `validateAsset` or `compileAsset`.

### AssetSpec

- `schema`: exactly `asset-assembly/0.1`.
- `units`: `meters` or `tiles`.
- `parameters`: optional named finite scalar values.
- `materials`: named `{ color: "#rrggbb", tintable?: boolean }` entries.
- `parts`: named shape/material/placement entries, with optional `parent` group and custom `anchors`.
- `groups`: optional named `{ parent?: string, position: {x,y,z} }` translation frames.
- `groundPivot`: optional published pivot point; defaults to `[0,0,0]`. It does not move the geometry or change the part origins.

IDs start with an ASCII letter and contain letters, digits, underscores or hyphens, up to 80 characters. A numeric field is a finite literal or `{ parameter: "name" }`. Callbacks, arithmetic strings, free text and unknown fields fail explicitly. Declarative primitive attributes are rounded to 12 decimal places to bound cross-runtime floating-point variation. The raw compatibility builder retains historical floats. Canonical hashing sorts object keys; array order remains meaningful. A source hash identifies a specification, not every possible mesh-equivalent description.

### Shapes

| Shape      | Fields                                         | Local geometry                                    |
| ---------- | ---------------------------------------------- | ------------------------------------------------- |
| `box`      | `size: {width,height,depth}`, optional `bevel` | X/Z centered; Y from zero to height               |
| `cylinder` | `radius`, `height`                             | 16 sides, centered on Y                           |
| `funnel`   | `bottomRadius`, `topRadius`, `height`          | Capped 16-sided frustum                           |
| `beam`     | `from`, `to`, `width`                          | Beveled square section between explicit endpoints |
| `tube`     | `from`, `to`, `radius`                         | Round shaft between explicit endpoints            |
| `coil`     | `radius`, `thickness`                          | Closed ring in the YZ plane                       |

`tube` is a solid shaft; the historical name does not imply a hollow bore. Beam/tube endpoints must be distinct. Normal box bevel defaults to 0.018 units and is capped by the dimensions. Zero bevel produces six closed flat faces. The style permits fractional dimensions and rounded primitives; this is not an integer voxel volume.

### Frames, anchors and placement

The coordinate frame is right handed: +X right, +Y up, −Z front. Shapes own local origins. Built-in anchors refer to their geometric bounds before material tinting or camera fitting:

`center`, `top-center`, `bottom-center`, `left-center`, `right-center`, `front-center`, `back-center`.

Custom anchors are explicit local points and cannot override built-in names.

```ts
placement: {
  type: "absolute",
  selfAnchor: "bottom-center",
  position: { x: 0, y: 0.72, z: 0 }
}
```

Absolute placement puts `selfAnchor` at the requested point in the parent group frame. Groups recursively add translations. Attachments use the resolved target's world position; the offset is target-local. In version 0.1 all frames are translation-only, so their axes are parallel. A part's parent does not add a second offset to an already resolved attachment.

```ts
placement: {
  type: "attach",
  selfAnchor: "bottom-center",
  target: { part: "base", anchor: "top-center" },
  offset: { x: 0, y: 0, z: 0 }
}
```

Declarations resolve by stable ID, independent of object insertion order. Missing references and dependency cycles fail. Repeated parts must be addressed by explicit instance IDs when used as attachment targets.

```ts
placement: {
  type: "repeat", pattern: "corners",
  selfAnchor: "top-center",
  target: { part: "top", face: "bottom" },
  edgeInset: { x: 0.04, z: 0.04 }
}
```

Corner repetition creates four instances against the bounds of the top or bottom face. Insets measure target edge to instance outer bound. X/Z placement uses each instance's bounds; `selfAnchor` sets its Y alignment. Targets must contain four distinct, non-overlapping support bounds. For width W, support width w and inset i, the centers are ±(W/2 − i − w/2).

Stable instance IDs are `part/0` through `part/3`, ordered X-negative/Z-negative, X-negative/Z-positive, X-positive/Z-negative, X-positive/Z-positive. A repeated parent target such as `legs` is ambiguous; use `legs/0`. Custom anchors propagate with each instance.

Rotation, arbitrary expressions, animated joints, mirror/line repetition, Boolean operations and soft constraints are outside schema 0.1. The lamp fixture is a static assembly with editable proportions, not an animation API. Unsupported fields fail rather than suggesting those operations succeeded.

## Mesh and material ownership

`meshForAsset(compiled, tint?)` combines layers into a new mesh. Meshes contain numeric `positions`, `normals`, linear-space RGB `colors` and triangle `indices`. Compiler layers are named by material ID. Fixed materials retain their albedo; tintable materials multiply their albedo by the optional sRGB `#rrggbb` tint converted to linear space. GPU adapters own their own uploaded resources; disposing one must not mutate the compiled source.

Prefer precompiled meshes and bakes for fixed production assets. Runtime compilation is an intentional option for variation, editing and development. Compilation does not consume randomness, read application state, modify saves or use network services.

## CPU bake

```ts
import { bakeAsset } from "@victorqueiroz/ludic-assembly/bake";
const frame = bakeAsset(compiled, { ppu: 128, samples: 2, tint: "#409c94" });
```

Returns RGBA `color` and encoded RGBA `depth` bytes, dimensions, projected bounds/depth range, source hash, bake version and units. The caller owns the returned arrays. Image encoding and file I/O belong to the caller. Optional `basis: {right,up,back}` supplies the orthographic camera. `bakeMesh(mesh, ppu?, samples?, basis?)` supports compatibility consumers.

The default basis preserves Foundry's historical projection. Inputs must be finite and nonempty. Sampling is an integer from one to four. The raster allocation is limited to 16777216 supersampled pixels. A deterministic CPU bake does not establish equivalence to every GPU renderer's lighting, shadowing or coverage.

## Browser preview

```ts
import { createPreview } from "@victorqueiroz/ludic-assembly/browser";
const preview = createPreview(canvas, { size: 360, onPick: console.log });
preview.setAsset(compiled, "#409c94");
preview.setCamera({ yaw: Math.PI / 4, pitch: 0.7 });
preview.dispose();
```

The preview uses an event-driven Canvas 2D CPU rasterizer. Its separate `maxTriangles` preview budget defaults to 20000; raising it is an explicit caller choice. The core compilation budget is independent. It redraws on edits and pointer movement rather than continuously scheduling frames. `setMesh` previews a precompiled mesh. `pick(x,y)` uses canvas backing-store coordinates and returns a stable instance ID when an AssetSpec compilation is loaded. Pointer capture preserves dragging across element boundaries. Call `dispose()` when removing the preview: it removes listeners and releases mesh references. Calls that mutate a disposed preview throw. The module has no import-time DOM side effects.

## Compatibility builder

`MeshBuilder` from `/compat` preserves imperative `box`, `cylinder`, `beam`, `tube`, `funnel` and `coil` calls. Colors are numeric sRGB values. It accepts optional joint pivot heights and per-vertex joint IDs for adapters that already use them; defaults specific to Foundry remain in Foundry. The compiler applies its own strict validation and degenerate-triangle cleanup; raw compatibility output is retained byte-for-byte.

## Diagnostics and reproducibility

`repairJSONDelimiters(text)` from `/text` offers an explicit syntax repair for model-produced JSON. It returns a proposed string, diagnostic code and exact text edits, or `null`. It only appends up to three missing final delimiters or removes up to three trailing delimiters after one complete valid JSON value. It never changes fields, numbers or relationships, and rejects multiple documents, broken strings and trailing non-delimiter data. The caller chooses whether to apply the edit, then validates the result normally. The playground exposes this as an explicit button; the compiler never applies it silently.

Diagnostics include `code`, JSON Pointer `path`, `message` and optional `part`, `targetPart`, `received`, `expected`. Codes distinguish structural errors, unknown fields/shapes/placements/parameters/materials/anchors, missing or ambiguous references, dependency cycles, impossible bounds, non-finite values and geometry budgets. Compilation never silently drops an unresolved part.

`examples/` contains complete assets. `tests/foundry-baseline.json` records pre-extraction mesh/bake hashes. `docs/research/extraction-provenance.json` identifies the source commit. `benchmarks/protocol.json` declares the LLM comparison before trials, and `qualification/` records results and limitations. Mathematical success, human visual acceptance and sustained device performance are separate claims.
