@ludic-labs/assembly 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/LICENSE +2 -0
  3. package/README.md +50 -2
  4. package/dist/bake.d.ts +80 -0
  5. package/dist/bake.js +214 -0
  6. package/dist/browser.d.ts +89 -0
  7. package/dist/browser.js +534 -0
  8. package/dist/compat.d.ts +17 -0
  9. package/dist/compat.js +153 -0
  10. package/dist/compiler.d.ts +18 -0
  11. package/dist/compiler.js +1264 -0
  12. package/dist/diagnostics.d.ts +5 -0
  13. package/dist/diagnostics.js +8 -0
  14. package/dist/export.d.ts +54 -0
  15. package/dist/export.js +272 -0
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.js +3 -0
  18. package/dist/legacy.d.ts +36 -0
  19. package/dist/legacy.js +180 -0
  20. package/dist/math.d.ts +16 -0
  21. package/dist/math.js +62 -0
  22. package/dist/mesh.d.ts +11 -0
  23. package/dist/mesh.js +153 -0
  24. package/dist/motion.d.ts +63 -0
  25. package/dist/motion.js +1476 -0
  26. package/dist/pixel.d.ts +268 -0
  27. package/dist/pixel.js +942 -0
  28. package/dist/required.d.ts +2 -0
  29. package/dist/required.js +6 -0
  30. package/dist/shapes.d.ts +71 -0
  31. package/dist/shapes.js +396 -0
  32. package/dist/sprite-brief.d.ts +5 -0
  33. package/dist/sprite-brief.js +47 -0
  34. package/dist/sprite-compiler.d.ts +62 -0
  35. package/dist/sprite-compiler.js +1745 -0
  36. package/dist/sprite-decompile.d.ts +68 -0
  37. package/dist/sprite-decompile.js +786 -0
  38. package/dist/sprite-format.d.ts +4 -0
  39. package/dist/sprite-format.js +155 -0
  40. package/dist/sprite-guide.d.ts +40 -0
  41. package/dist/sprite-guide.js +330 -0
  42. package/dist/sprite-scene.d.ts +277 -0
  43. package/dist/sprite-scene.js +664 -0
  44. package/dist/sprite-types.d.ts +110 -0
  45. package/dist/sprite-types.js +1 -0
  46. package/dist/sprite.d.ts +8 -0
  47. package/dist/sprite.js +8 -0
  48. package/dist/svg/schema.d.ts +71 -0
  49. package/dist/svg/schema.js +217 -0
  50. package/dist/svg/types.d.ts +148 -0
  51. package/dist/svg/types.js +1 -0
  52. package/dist/svg.d.ts +26 -0
  53. package/dist/svg.js +539 -0
  54. package/dist/text.d.ts +12 -0
  55. package/dist/text.js +75 -0
  56. package/dist/types.d.ts +478 -0
  57. package/dist/types.js +1 -0
  58. package/docs/svg/README.md +150 -0
  59. package/docs/svg/specification.md +108 -0
  60. package/examples/svg/arcane-shield.json +261 -0
  61. package/examples/svg/aurora-ribbon.json +158 -0
  62. package/examples/svg/bouncing-ember.json +76 -0
  63. package/examples/svg/ember-fountain.json +224 -0
  64. package/examples/svg/lantern-keeper.json +1159 -0
  65. package/examples/svg/night-workshop.json +1120 -0
  66. package/examples/svg/waygate-platform.json +64 -0
  67. package/examples/svg/waygate-spark.json +92 -0
  68. package/examples/svg/waygate-wisp.json +105 -0
  69. package/examples/svg/waygate.json +485 -0
  70. package/package.json +132 -4
  71. package/schemas/asset-assembly-0.1.schema.json +354 -0
  72. package/schemas/asset-assembly-0.2.schema.json +429 -0
  73. package/schemas/asset-assembly-0.3.schema.json +572 -0
  74. package/schemas/motion-set-0.1.schema.json +178 -0
  75. package/schemas/pixel-sprite-0.1.schema.json +892 -0
  76. package/schemas/pixel-sprite-scene-0.1.schema.json +149 -0
  77. package/schemas/svg-asset-0.1.schema.json +1157 -0
  78. package/tools/svg-asset.mjs +106 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,78 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@ludic-labs/assembly` (previously `@victorqueiroz/ludic-assembly`). Qualification evidence for each release is in `qualification/`; benchmark outcomes and visual acceptance are recorded separately.
4
+
5
+ ## Unreleased
6
+
7
+ - Publish the package publicly under `@ludic-labs/assembly`; installation no longer requires GitHub Packages credentials. The license is unchanged.
8
+ - Automatically publish tested `main` revisions through npm trusted publishing. New revisions advance the stable patch unless the source declares a higher version; rerunning an already published revision is a no-op.
9
+ - Add three editable SVG effects: aurora ribbon, ember fountain and arcane shield. The homepage and Waygate page offer still previews, opt-in motion, source editing and animated SVG downloads.
10
+ - Remove the Pixel sprites promotion and navigation link from the homepage; its documentation remains available at `/sprites/`.
11
+
12
+ ## 0.4.0 — 2026-11-05
13
+
14
+ ### Added
15
+
16
+ - `@victorqueiroz/ludic-assembly/sprite` entry point and the `pixel-sprite/0.1` notation, a JSON form of pixel art that models write, read and edit: palettes of named ramps, cels built from ordered layers (literal `pixels` rows, shapes, stamps, components, `silhouette`, `shade`, `outline`, `bevel`, `speck`, `mirror`, `recolor`, `repaint`, `move`, recipes), animations and a part for every pixel (`pickSprite`). Views project a 3D asset at one density into a layout guide (part boxes, masks, depth, light tones; cutaways with `hide`, door states with `pose`) that the compiler checks drawings against. `spriteBrief` writes the guide as a Markdown brief for a model; `decompileSprite` turns existing pixel art into an equivalent structured document. Host-independent, deterministic, typed, with structured `SPRITE_*` diagnostics and budgets. Schemas: `/schema/pixel-sprite-0.1` and `/schema/pixel-sprite-scene-0.1`. Model-facing guide: [`docs/sprite.md`](docs/sprite.md) (served as `/sprite.md`).
17
+ - Sprite scenes (`pixel-sprite-scene/0.1`): placements of gallery or scene-local assets drawn by sprite documents, one shared palette derived from the placed materials, per-placement views from the scene camera and yaw, cutaway views, door states, composition by per-pixel 3D depth (`composeSpriteScene`), picking by placement and part (`pickScene`) and a footprint report of alignment and draw order (`spriteSceneFootprint`).
18
+ - `/pixel`: images report per-pixel depth (`image.depth`), used to composite 2D layers over a render.
19
+ - Documentation: a pixel sprites page with a live corner kitchen (17 placements, two views, opening doors, picking; drawn by Codex, by Claude, by a guided image model brought into the notation, and by a machine baseline) and the measured comparison of notation authors and cheap image models. The kitchen's scene-local assets are public examples in `examples/scenes/kitchen/`.
20
+ - Experimental `@victorqueiroz/ludic-assembly/pixel` entry point (`pixel-profile/0.1`). `renderPixelArt` draws a compiled asset, or a scene from `composePixelScene`, straight to a logical pixel grid at a fixed density. It uses derived hue-shifted ramps, banded camera-relative light, coloured or dark outlines, inner lines decided per pair of parts, feature accents, sub-pixel face suppression and a palette capped across all views. Also: 2:1 dimetric and top-down camera presets, `scene`/`fantasy`/`arcade` presets from an itch.io style survey, UV-free surface patterns (`stripes`, `checker`, `grid`, `bricks`) evaluated in each asset's own frame, `pickPixel` and structured `PIXEL_*` diagnostics and budgets. No owner visual acceptance; no live or animated renderer.
21
+ - Documentation: the pixel art section renders every gallery asset with the pixel renderer instead of downscaling its thumbnail, and adds a village scene composed only of gallery assets (`examples/scenes/village-corner.json`), shown from five cameras and in three profiles. `npm run test:browser` checks it in Chromium, Firefox and WebKit.
22
+ - Initial `svg-asset/0.1` notation and isolated `/svg` entry point: parameterized SVG paths and primitives, scoped symbols, repeats, paint definitions, deterministic clips/particles, static and sampled animated SVG export. Includes a versioned schema, CLI frame export, original character/interior/effect examples, a local authoring preview and browser pixel qualification. This is a new 2D contract, separate from the existing assembly schema versions; visual acceptance and publication remain separate.
23
+ - Add Ember’s Waygate to the documentation: a complete platform game using the SVG notation examples, a freestanding C++23/CMake WebAssembly runtime and a WebGL 1.0 atlas renderer. Includes an asset atelier, keyboard and pointer controls, pause/restart, context recovery, source provenance and native/browser qualification.
24
+ - Experimental `@victorqueiroz/ludic-assembly/export` entry point: `exportGLB` writes a deterministic static binary glTF 2.0 of a compiled or posed asset or a small scene (stable instance IDs as node names, per-material base colors, untinted vertex colors, units recorded and never converted, a root turn to glTF's +Z front), and `bakeScene` renders a transparent square image with the preview rasterizer. Rigs and clips are not exported.
25
+ - Browser previews: `snapshot()` returns the shown scene, camera, framing and clip frame.
26
+ - Browser previews: a `camera` getter; vertical drags tilt the view; arrow keys orbit a focused preview (Shift for finer steps) and Home restores the initial view. Only the first pointer and the primary mouse button orbit.
27
+ - Documentation: Markdown routes for LLMs and coding agents. `/llms.txt` indexes the site, `/llms-full.txt` combines the quick start, guide, API reference, gallery catalog and city notes, and every page has a Markdown twin (`/index.md`, `/guide.md`, `/city.md`, `/gallery.md`, `/gallery/<id>.md` with the asset's full definition), linked from its HTML head. `npm run test:llms` checks structure, links and parity with the HTML.
28
+ - Documentation: the home page starts newcomers in one step (install command, a browser module that renders a gallery asset, a prompt for coding agents, or direct export), with a live preview running the same calls and the full gallery below. The walkthrough moved to `/guide/`; old home page fragments redirect there.
29
+ - Documentation: every live example exports its source JSON (where it has one), a GLB of the shown mesh and a 1024 × 1024 PNG of the current view. GLB and PNG are disabled while an edited description is invalid.
30
+
31
+ ### Changed
32
+
33
+ - Browser previews: dragging grabs the model, so it turns in the direction of the drag; it previously turned the opposite way. The playable city's orbit follows the same convention.
34
+ - Gallery: the crab's claws are rebuilt as curved, toothed fixed and movable fingers held across its face, hinged so "Snap the claws" visibly opens and closes each pincer.
35
+
36
+ ### Fixed
37
+
38
+ - Documentation previews keep the reader's dragged view when a clip changes, and "Rotate 45°" turns from the current view instead of a stale copy.
39
+
40
+ ## 0.3.0 — 2026-11-04
41
+
42
+ ### Added
43
+
44
+ - **Schema `asset-assembly/0.3`**, a superset of 0.2:
45
+ - part `rotation` in degrees (X, then Y, then Z about fixed axes); bounds and anchors come from the rotated geometry; quarter turns are exact;
46
+ - part `array`: `line` (any step vector), `grid` (counts per axis) and `radial` (axis, start, sweep, optional orientation), with stable `part/0…n−1` instance IDs;
47
+ - primitives `sphere`, `ellipsoid`, `cone`, `wedge`, `lathe` (profiles that may rise, fall or close into rings) and `extrude` (concave outlines), plus `sides` (3–32) on `cylinder`, `funnel` and `tube`;
48
+ - continuous DOFs that loop after whole turns, for wheels, rotors and clock hands;
49
+ - joints copied per array instance (`joint/0…n−1`), with axes that turn with radially oriented copies;
50
+ - diagnostics `INVALID_ROTATION`, `SELF_INTERSECTING_OUTLINE`, `SELF_INTERSECTING_PROFILE` and `JOINT_ARRAY_MISMATCH`, and `UNSUPPORTED_COMBINATION` for arrays with corner repetition or mirrored sides.
51
+ - Structural schema `schemas/asset-assembly-0.3.schema.json`, exported as `@victorqueiroz/ludic-assembly/schema/asset-assembly-0.3`.
52
+ - `posedMesh(asset, pose, tint?)`: the mesh of `meshForAsset(poseAsset(…))` without building posed instances.
53
+ - Preview playback: a bounded cache of rendered looping frames (`frameCacheBytes`) and automatic suspension off-screen or in hidden documents (`pauseWhenHidden`, `suspended`).
54
+ - Documentation gallery: 100 original assets across ten categories, each with a live page (orbit, accent color, parameters, motion clips and editable JSON with diagnostics), deterministic thumbnails and category, feature and text filters. `tools/check-gallery.mjs` validates gallery entries; `tools/render-asset.mjs` renders review sheets.
55
+ - Preregistered schema 0.3 LLM qualification (`benchmarks/protocol-0.3.json`): relational 0.3 authoring 72.1% correct on the first try against 60.8% for an expanded form of the same schema (+11.3 points, paired 95% interval 3.8 to 19.2), 83.8% after one bounded repair, with 0.74× the tokens. The advantage comes from two of three model families.
56
+
57
+ ### Changed
58
+
59
+ - The CPU rasterizer is about 3.5–4× faster with byte-identical output.
60
+ - Motion joint references and the motion-set 0.1 structural schema accept array copy indices (`roller/3`); every previously valid motion set is unchanged.
61
+ - The schema generator rewrites a schema file only when its content changes.
62
+ - Validators no longer coerce enumerated fields. A non-string `units`, shape or placement `type`, radial `axis`, slot option, clip `interpolation`, `symmetry` or `solve`, or grounding `type` is now a diagnostic. Previously `["meters"]` was accepted, and a value such as `{"toString":null}` threw instead of producing a diagnostic.
63
+
64
+ ### Fixed
65
+
66
+ - Compiled coils thinner than about 2% of their radius were partly wound inward; they now face outward. Every other coil is byte-identical, and the raw `MeshBuilder.coil` from `/compat` keeps its historical output.
67
+
68
+ ### Compatibility
69
+
70
+ Schema 0.1 and 0.2 descriptions compile to the same meshes, bakes and hashes as in 0.2.0, except the thin coils above, which were partly inside out; 0.3 fields fail as unknown in older schemas.
71
+
72
+ ## 0.2.0 — 2026-11-03
73
+
74
+ Characters: schema `asset-assembly/0.2` (joints, mirrored sides, slots, identities, states and limbs), `motion-set/0.1` (poses, expressions, clips, contacts, grounding, travel and the planted-foot solve), `bakeClip`, preview playback and `fromLegacyRig`. Passed the preregistered character qualification with narrow margins.
75
+
76
+ ## 0.1.0 — 2026-11-02
77
+
78
+ Schema `asset-assembly/0.1`: declarative static assemblies with named parts, anchors, attachments and corner repetition; deterministic compilation, structured diagnostics, CPU bakes, browser previews and the compatibility builder. Passed the preregistered static-asset qualification.
package/LICENSE ADDED
@@ -0,0 +1,2 @@
1
+ Copyright (c) 2026 Victor Queiroz. All rights reserved.
2
+ Private software. No license to redistribute is granted.
package/README.md CHANGED
@@ -1,3 +1,51 @@
1
- # Temporary Holding Version
1
+ # Ludic Assembly
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Deterministic declarative low-poly assets: TypeScript/JSON specifications, named parts and anchors, material layers, CPU bakes and live browser previews.
4
+
5
+ **Continuing the research in a new session? Start with [the research handoff](docs/research/README.md).** It records the preferred C source, achievements, open gates, review packet and bounded next-session deliverables. The stable library and the private experiments have separate status.
6
+
7
+ - Private repository: https://github.com/VictorQueiroz/ludic-assembly
8
+ - Live documentation: https://libraries.ludic-assembly.victorqueiroz.dev/ (one-step start and gallery), with [the guide](https://libraries.ludic-assembly.victorqueiroz.dev/guide/) and, for LLMs and coding agents, [llms.txt](https://libraries.ludic-assembly.victorqueiroz.dev/llms.txt) and [llms-full.txt](https://libraries.ludic-assembly.victorqueiroz.dev/llms-full.txt)
9
+ - Package: `@ludic-labs/assembly`, published publicly on [npm](https://www.npmjs.com/package/@ludic-labs/assembly).
10
+ - [API and installation](docs/api.md)
11
+ - [SVG Asset Notation 0.1](docs/svg/README.md): a separate research-informed 2D construction language, with characters, interiors, effects, animation, schema, exports and a local authoring gallery. Initial version; owner visual acceptance is separate.
12
+ - [Research library](docs/research/papers.md), [proposed 3D and SVG editing direction](docs/research/editable-assets-proposal.md), and [visual references for owner rating](docs/research/visual-review.md).
13
+ - [Camera-independent fox animation and derived SVG views](docs/research/camera-independent-fox-proposal.md), with a private compiler-backed turntable and idle experiment, capability audit and remaining gait plan.
14
+ - [Organic fox surface experiment](experiments/fox-organic/README.md), testing richer anatomy and layered fur after the primitive fox fell short of the visual target.
15
+ - [Drawing construction, proportions and gaze](docs/research/construction-and-gaze-proposal.md), with primary literature, an executed facial landmark audit and a general-purpose construction proposal.
16
+ - [Head calibration pilot](experiments/fox-head-calibration/README.md), implementing private construction graphs, bounded eye geometry, gaze/proportion boards and non-fox fixtures for visual review.
17
+ - [Guided surface-covering pilot](experiments/surface-covering-study/README.md), comparing beard treatments with reusable surface guides, a foliage fixture, SVG measurements and unreviewed full-fox motion previews.
18
+ - [C vector/motion outcomes](experiments/fox-vector-motion/README.md) and [bounded batch / Vast.ai execution plan](docs/research/batch-rendering-and-vast-proposal.md), documenting visibility fixes, export tradeoffs, resource measurements and planned remote execution.
19
+ - [Qualified repair protocol](benchmarks/protocol-repair.json) and [initial unassisted protocol](benchmarks/protocol.json); [character protocol](benchmarks/protocol-character.json); [schema 0.3 protocol](benchmarks/protocol-0.3.json)
20
+ - [Gallery authoring guide](examples/gallery/AUTHORING.md): the contract, conventions and quality bar for the documentation assets.
21
+ - [Schema roadmap from gallery authoring](docs/roadmap.md): limitations reported by the gallery authors, what 0.3 changed and candidate work.
22
+
23
+ Schema 0.1 is the static assembly contract; schema 0.2 and motion-set 0.1 add characters in version 0.2.0; schema 0.3 adds rotations, line/grid/radial arrays, spheres, cones, wedges, lathes, extrusions, continuous and per-instance joints in version 0.3.0; version 0.4.0 adds the `pixel-sprite/0.1` notation (`/sprite`), the initial `svg-asset/0.1` notation (`/svg`) and the experimental `/pixel` and `/export` entry points ([changelog](CHANGELOG.md)). [Qualification](qualification/report.json) records implementation checks, model-comparison results and earlier failed attempts. Results cover the published corpus and model aliases; visual acceptance is recorded separately.
24
+
25
+ ```sh
26
+ npm ci
27
+ npm run lint
28
+ npm run typecheck
29
+ npm test
30
+ npm run build:docs
31
+ npm run test:browser
32
+ node tools/check-gallery.mjs # gallery contract checks
33
+ ```
34
+
35
+ The documentation uses React 19, TypeScript 7 and Vite. After the first documentation build, `npm run dev`
36
+ serves the same pre-rendered routes with Vite hot updates. Every page has its own canonical metadata and
37
+ social image; gallery thumbnails come from a deterministic antialiased render of that asset. Every page
38
+ also has a Markdown twin for LLMs and agents, indexed by `/llms.txt`. See
39
+ [the documentation architecture and development checks](docs/documentation.md).
40
+
41
+ Schema 0.3 passed its preregistered LLM qualification: relational authoring is 72.1% correct on the first try against 60.8% for an expanded form of the same schema with absolute coordinates (+11.3 points, paired 95% interval 3.8 to 19.2), 83.8% after one bounded repair, with 0.74× the tokens. The advantage comes from two of three model families; the third was perfect with the expanded form, and its relational failures were all one stray closing delimiter. See [the report](qualification/benchmarks/schema-v3-qualification-v1.json).
42
+
43
+ Characters (`asset-assembly/0.2`, `motion-set/0.1`) add joints, mirrored sides, identities with swappable face parts, face states and expressions, verified motion clips and a planted-foot solve, with a six-step live guide in the documentation. They passed the preregistered character qualification with narrow margins; owner visual acceptance is pending. See the [API section](docs/api.md#characters-identities-and-motion-schema-02) and the [design record](docs/research/character-proposal.md).
44
+
45
+ Core has no runtime dependencies on host applications, React, Node or the DOM. Optional `/browser`, `/bake` and `/compat` entry points isolate integration concerns; the experimental `/export` entry point writes deterministic static GLB files and transparent scene images. The experimental `/pixel` entry point renders the same compiled assets, or scenes composed from them, as fixed-density pixel art with derived palettes, outlines and surface patterns that need no texture coordinates; see the [API section](docs/api.md#pixel-art-experimental). The `/sprite` entry point is a notation for pixel art that language models write and edit (`pixel-sprite/0.1`): exact pixels with named parts, drawn per view on a layout guide projected from a 3D asset, and composed into scenes with one grid, one palette and picking; see the [notation guide](docs/sprite.md) and the [API section](docs/api.md#pixel-sprites-sprite). Fixed production assets may be compiled offline; interactive examples compile in the browser without any model or network request.
46
+
47
+ The documentation gallery contains 108 original assets written in the asset language (`examples/gallery/`), each with its own live page, and six owner-authorized exported production meshes, each with an individual live preview. Every live example exports its source JSON (where it has one), a GLB of the shown mesh and a PNG of the current view. Source provenance is retained in the private research archive. Consumers build from this repository alone.
48
+
49
+ Copyright © 2026 Victor Queiroz. All rights reserved.
50
+
51
+ The [playable city](https://libraries.ludic-assembly.victorqueiroz.dev/city/) combines 46 gallery asset types in an experimental WebGL 1.0 / C++23 WebAssembly game. See [build requirements, controls and measurement limits](demos/city/README.md). Documentation builds require CMake, Ninja, Clang with WebAssembly support and wasm-ld.
package/dist/bake.d.ts ADDED
@@ -0,0 +1,80 @@
1
+ import type { CompiledAsset, CompiledClip, MeshData, Vec3 } from "./types.js";
2
+ export declare const BAKE_VERSION = 1;
3
+ export declare const BAKE_PPU = 128;
4
+ export interface CameraBasis {
5
+ back: Vec3;
6
+ right: Vec3;
7
+ up: Vec3;
8
+ }
9
+ export declare const fixedBasis: CameraBasis;
10
+ export interface BakedFrame {
11
+ width: number;
12
+ height: number;
13
+ left: number;
14
+ top: number;
15
+ depthMin: number;
16
+ depthMax: number;
17
+ color: Uint8Array;
18
+ depth: Uint8Array;
19
+ }
20
+ /** Integer projected-pixel bounds: +X is right and +Y is up, before canvas translation. */
21
+ export interface BakeViewport {
22
+ left: number;
23
+ top: number;
24
+ width: number;
25
+ height: number;
26
+ }
27
+ /** Deterministic orthographic triangle rasterizer shared by bakes and browser previews. */
28
+ export declare function bakeMesh(mesh: MeshData, ppu?: number, samples?: number, basis?: CameraBasis, viewport?: BakeViewport | null): BakedFrame;
29
+ export declare function sampleDepth(frame: BakedFrame, x: number, y: number): number;
30
+ export declare function bakeAsset(asset: CompiledAsset, camera?: {
31
+ basis?: CameraBasis;
32
+ ppu?: number;
33
+ samples?: number;
34
+ tint?: string;
35
+ }): {
36
+ width: number;
37
+ height: number;
38
+ left: number;
39
+ top: number;
40
+ depthMin: number;
41
+ depthMax: number;
42
+ color: Uint8Array;
43
+ depth: Uint8Array;
44
+ sourceHash: string;
45
+ bakeVersion: number;
46
+ units: "meters" | "tiles";
47
+ };
48
+ /**
49
+ * Bake every frame of a compiled clip at an integer frame rate. Looping clips yield
50
+ * round(duration × fps) frames (the closing frame repeats the first and is omitted); others add the end
51
+ * frame. Each frame keeps its projected `left`/`top`, so frames align when composited into a strip.
52
+ * Root travel is reported per frame and never baked into the image.
53
+ */
54
+ export declare function bakeClip(asset: CompiledAsset, clip: CompiledClip, options?: {
55
+ fps?: number;
56
+ basis?: CameraBasis;
57
+ ppu?: number;
58
+ samples?: number;
59
+ tint?: string;
60
+ face?: CompiledClip;
61
+ }): {
62
+ clip: string;
63
+ fps: number;
64
+ frames: {
65
+ width: number;
66
+ height: number;
67
+ left: number;
68
+ top: number;
69
+ depthMin: number;
70
+ depthMax: number;
71
+ color: Uint8Array;
72
+ depth: Uint8Array;
73
+ frame: number;
74
+ phase: number;
75
+ travel: Vec3;
76
+ }[];
77
+ sourceHash: string;
78
+ bakeVersion: number;
79
+ units: "meters" | "tiles";
80
+ };
package/dist/bake.js ADDED
@@ -0,0 +1,214 @@
1
+ import { requireValue } from "./required.js";
2
+ import { dot, normalize, cross } from "./math.js";
3
+ import { meshForAsset } from "./mesh.js";
4
+ import { posedMesh, samplePose } from "./motion.js";
5
+ export const BAKE_VERSION = 1;
6
+ export const BAKE_PPU = 128;
7
+ export const fixedBasis = (() => {
8
+ // Preserve the original camera arithmetic for byte-identical legacy bakes.
9
+ const yaw = Math.atan2(24, -26), pitch = Math.atan2(28, Math.hypot(24, 26)), radius = Math.hypot(24, 28, 26);
10
+ const back = normalize([
11
+ 14 + Math.sin(yaw) * Math.cos(pitch) * radius - 14,
12
+ Math.sin(pitch) * radius,
13
+ 10 + Math.cos(yaw) * Math.cos(pitch) * radius - 10,
14
+ ]);
15
+ const right = normalize(cross([0, 1, 0], back));
16
+ return { back, right, up: cross(back, right) };
17
+ })();
18
+ /** Matches the native device's unshadowed material lighting and colour transform. */
19
+ const sunDirection = normalize([-35, 60, -35]);
20
+ const ambientLow = [0.36, 0.43, 0.39];
21
+ const ambientHigh = [0.94, 0.96, 0.86];
22
+ const sunColor = [1.35, 1.22, 1.04];
23
+ function shade(mesh, at, output) {
24
+ const normal = mesh.normals, sun = Math.max(0, requireValue(normal[at]) * sunDirection[0] +
25
+ requireValue(normal[at + 1]) * sunDirection[1] +
26
+ requireValue(normal[at + 2]) * sunDirection[2]);
27
+ for (let c = 0; c < 3; c++) {
28
+ const t = requireValue(normal[at + 1]) * 0.5 + 0.5;
29
+ const ambient = requireValue(ambientLow[c]) * (1 - t) + requireValue(ambientHigh[c]) * t;
30
+ const lit = requireValue(mesh.colors[at + c]) * (ambient + sun * requireValue(sunColor[c]));
31
+ output[at + c] = Math.round(255 *
32
+ Math.max(0, Math.min(1, (lit * (2.51 * lit + 0.03)) / (lit * (2.43 * lit + 0.59) + 0.14))) **
33
+ (1 / 2.2));
34
+ }
35
+ }
36
+ /** Deterministic orthographic triangle rasterizer shared by bakes and browser previews. */
37
+ export function bakeMesh(mesh, ppu = BAKE_PPU, samples = 2, basis = fixedBasis, viewport = null) {
38
+ if (!mesh.positions.length || mesh.positions.some((v) => !Number.isFinite(v)))
39
+ throw new RangeError("Bake requires finite nonempty geometry");
40
+ if (!Number.isFinite(ppu) || ppu <= 0 || !Number.isInteger(samples) || samples < 1 || samples > 4)
41
+ throw new RangeError("Invalid bake resolution");
42
+ if (viewport &&
43
+ (viewport.width < 1 ||
44
+ viewport.height < 1 ||
45
+ ![
46
+ viewport.left,
47
+ viewport.top,
48
+ viewport.width,
49
+ viewport.height,
50
+ viewport.left + viewport.width,
51
+ viewport.top - viewport.height,
52
+ ].every(Number.isSafeInteger)))
53
+ throw new RangeError("Bake viewport requires safe integer bounds and positive dimensions");
54
+ const axes = [basis.right, basis.up, basis.back];
55
+ if (axes.some((v) => v.length !== 3 || v.some((n) => !Number.isFinite(n)) || Math.abs(Math.hypot(...v) - 1) > 1e-6) ||
56
+ Math.abs(dot(basis.right, basis.up)) > 1e-6 ||
57
+ Math.abs(dot(basis.right, basis.back)) > 1e-6 ||
58
+ Math.abs(dot(basis.up, basis.back)) > 1e-6)
59
+ throw new RangeError("Bake camera basis must be finite and orthonormal");
60
+ if (mesh.positions.length % 3 ||
61
+ mesh.normals.length !== mesh.positions.length ||
62
+ mesh.colors.length !== mesh.positions.length ||
63
+ mesh.indices.length % 3 ||
64
+ mesh.indices.some((i) => !Number.isInteger(i) || i < 0 || i >= mesh.positions.length / 3))
65
+ throw new RangeError("Invalid bake mesh attributes or indices");
66
+ // Keep projections and screen coordinates in doubles. Float32 changes coverage/depth bytes.
67
+ const vertices = new Float64Array(mesh.positions.length);
68
+ const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
69
+ for (let n = 0; n < mesh.positions.length; n += 3) {
70
+ const x = requireValue(mesh.positions[n]), y = requireValue(mesh.positions[n + 1]), z = requireValue(mesh.positions[n + 2]);
71
+ vertices[n] = x * basis.right[0] + y * basis.right[1] + z * basis.right[2];
72
+ vertices[n + 1] = x * basis.up[0] + y * basis.up[1] + z * basis.up[2];
73
+ vertices[n + 2] = x * basis.back[0] + y * basis.back[1] + z * basis.back[2];
74
+ for (let axis = 0; axis < 3; axis++) {
75
+ min[axis] = Math.min(requireValue(min[axis]), requireValue(vertices[n + axis]));
76
+ max[axis] = Math.max(requireValue(max[axis]), requireValue(vertices[n + axis]));
77
+ }
78
+ }
79
+ const left = Math.floor(min[0] * ppu - 3) / ppu;
80
+ const right = Math.ceil(max[0] * ppu + 3) / ppu;
81
+ const bottom = Math.floor(min[1] * ppu - 3) / ppu;
82
+ const top = Math.ceil(max[1] * ppu + 3) / ppu;
83
+ const fullWidth = Math.round((right - left) * ppu), fullHeight = Math.round((top - bottom) * ppu), pixelLeft = Math.round(left * ppu), pixelTop = Math.round(top * ppu), startX = viewport ? Math.max(0, viewport.left - pixelLeft) : 0, startY = viewport ? Math.max(0, pixelTop - viewport.top) : 0, endX = viewport ? Math.min(fullWidth, viewport.left + viewport.width - pixelLeft) : fullWidth, endY = viewport ? Math.min(fullHeight, pixelTop - viewport.top + viewport.height) : fullHeight, width = Math.max(0, endX - startX), height = Math.max(0, endY - startY), outputLeft = viewport ? left + startX / ppu : left, outputTop = viewport ? top - startY / ppu : top;
84
+ const depthMin = min[2] - 0.001, depthMax = max[2] + 0.001;
85
+ if (!width || !height)
86
+ return {
87
+ width: 0,
88
+ height: 0,
89
+ left: outputLeft,
90
+ top: outputTop,
91
+ depthMin,
92
+ depthMax,
93
+ color: new Uint8Array(0),
94
+ depth: new Uint8Array(0),
95
+ };
96
+ if (width * height * samples * samples > 16_777_216)
97
+ throw new RangeError("Bake pixel budget exceeded");
98
+ const sw = width * samples, sh = height * samples, sampleX = startX * samples, sampleY = startY * samples, zbuf = new Float64Array(sw * sh).fill(-Infinity), rgb = new Uint8Array(sw * sh * 3);
99
+ for (let n = 0; n < vertices.length; n += 3) {
100
+ vertices[n] = (requireValue(vertices[n]) - left) * ppu * samples;
101
+ vertices[n + 1] = (top - requireValue(vertices[n + 1])) * ppu * samples;
102
+ }
103
+ const shaded = new Uint8Array(mesh.positions.length), hasShade = new Uint8Array(mesh.positions.length / 3);
104
+ for (let n = 0; n < mesh.indices.length; n += 3) {
105
+ const id = requireValue(mesh.indices[n]), ai = id * 3, normal = mesh.normals;
106
+ if (requireValue(normal[ai]) * basis.back[0] +
107
+ requireValue(normal[ai + 1]) * basis.back[1] +
108
+ requireValue(normal[ai + 2]) * basis.back[2] <=
109
+ 0)
110
+ continue;
111
+ const bi = requireValue(mesh.indices[n + 1]) * 3, ci = requireValue(mesh.indices[n + 2]) * 3, ax = requireValue(vertices[ai]), ay = requireValue(vertices[ai + 1]), az = requireValue(vertices[ai + 2]), bx = requireValue(vertices[bi]), by = requireValue(vertices[bi + 1]), bz = requireValue(vertices[bi + 2]), cx = requireValue(vertices[ci]), cy = requireValue(vertices[ci + 1]), cz = requireValue(vertices[ci + 2]);
112
+ const area = (cx - ax) * (by - ay) - (cy - ay) * (bx - ax);
113
+ if (Math.abs(area) < 1e-10)
114
+ continue;
115
+ if (!hasShade[id]) {
116
+ shade(mesh, ai, shaded);
117
+ hasShade[id] = 1;
118
+ }
119
+ const red = requireValue(shaded[ai]), green = requireValue(shaded[ai + 1]), blue = requireValue(shaded[ai + 2]), firstY = Math.max(sampleY, Math.floor(Math.min(ay, by, cy))), lastY = Math.min(sampleY + sh, Math.ceil(Math.max(ay, by, cy))), firstX = Math.max(sampleX, Math.floor(Math.min(ax, bx, cx))), lastX = Math.min(sampleX + sw, Math.ceil(Math.max(ax, bx, cx))), cbY = cy - by, cbX = cx - bx, acY = ay - cy, acX = ax - cx;
120
+ for (let y = firstY; y < lastY; y++) {
121
+ const sampleCenterY = y + 0.5, uY = (sampleCenterY - by) * cbX, vY = (sampleCenterY - cy) * acX, row = (y - sampleY) * sw;
122
+ for (let x = firstX; x < lastX; x++) {
123
+ // Do not increment edges or multiply by reciprocal area: rounding is observable.
124
+ const u = ((x + 0.5 - bx) * cbY - uY) / area, v = ((x + 0.5 - cx) * acY - vY) / area, w = 1 - u - v;
125
+ if (u < -1e-9 || v < -1e-9 || w < -1e-9)
126
+ continue;
127
+ const d = u * az + v * bz + w * cz, at = x - sampleX + row;
128
+ if (d > requireValue(zbuf[at])) {
129
+ zbuf[at] = d;
130
+ rgb[at * 3] = red;
131
+ rgb[at * 3 + 1] = green;
132
+ rgb[at * 3 + 2] = blue;
133
+ }
134
+ }
135
+ }
136
+ }
137
+ const color = new Uint8Array(width * height * 4), depth = new Uint8Array(color.length);
138
+ for (let y = 0; y < height; y++)
139
+ for (let x = 0; x < width; x++) {
140
+ let coverage = 0, d = -Infinity;
141
+ let red = 0, green = 0, blue = 0;
142
+ for (let sy = 0; sy < samples; sy++)
143
+ for (let sx = 0; sx < samples; sx++) {
144
+ const at = x * samples + sx + (y * samples + sy) * sw;
145
+ if (!Number.isFinite(zbuf[at]))
146
+ continue;
147
+ coverage++;
148
+ d = Math.max(d, requireValue(zbuf[at]));
149
+ red += requireValue(rgb[at * 3]);
150
+ green += requireValue(rgb[at * 3 + 1]);
151
+ blue += requireValue(rgb[at * 3 + 2]);
152
+ }
153
+ if (!coverage)
154
+ continue;
155
+ const o = (x + y * width) * 4, q = Math.round(((d - depthMin) / (depthMax - depthMin)) * 65535);
156
+ color[o] = Math.round(red / coverage);
157
+ color[o + 1] = Math.round(green / coverage);
158
+ color[o + 2] = Math.round(blue / coverage);
159
+ color[o + 3] = Math.round((255 * coverage) / (samples * samples));
160
+ depth[o] = q >> 8;
161
+ depth[o + 1] = q & 255;
162
+ depth[o + 2] = 0;
163
+ depth[o + 3] = 255;
164
+ }
165
+ return { width, height, left: outputLeft, top: outputTop, depthMin, depthMax, color, depth };
166
+ }
167
+ export function sampleDepth(frame, x, y) {
168
+ const o = (x + y * frame.width) * 4;
169
+ return (frame.depthMin +
170
+ ((requireValue(frame.depth[o]) * 256 + requireValue(frame.depth[o + 1])) / 65535) *
171
+ (frame.depthMax - frame.depthMin));
172
+ }
173
+ export function bakeAsset(asset, camera = {}) {
174
+ return {
175
+ ...bakeMesh(meshForAsset(asset, camera.tint), camera.ppu ?? BAKE_PPU, camera.samples ?? 2, camera.basis ?? fixedBasis),
176
+ sourceHash: asset.sourceHash,
177
+ bakeVersion: BAKE_VERSION,
178
+ units: asset.units,
179
+ };
180
+ }
181
+ /**
182
+ * Bake every frame of a compiled clip at an integer frame rate. Looping clips yield
183
+ * round(duration × fps) frames (the closing frame repeats the first and is omitted); others add the end
184
+ * frame. Each frame keeps its projected `left`/`top`, so frames align when composited into a strip.
185
+ * Root travel is reported per frame and never baked into the image.
186
+ */
187
+ export function bakeClip(asset, clip, options = {}) {
188
+ const fps = options.fps ?? 12;
189
+ if (!Number.isInteger(fps) || fps < 1 || fps > 120)
190
+ throw new RangeError("Bake fps must be an integer 1–120");
191
+ const span = Math.max(1, Math.round(clip.duration * fps)), count = clip.loop ? span : span + 1;
192
+ if (count > 600)
193
+ throw new RangeError("Clip bake exceeds 600 frames");
194
+ const frames = [];
195
+ for (let frame = 0; frame < count; frame++) {
196
+ const pose = samplePose(asset, clip, clip.loop ? { frame, fps } : { phase: frame / span }, {
197
+ face: options.face,
198
+ });
199
+ frames.push({
200
+ ...bakeMesh(posedMesh(asset, pose, options.tint), options.ppu ?? BAKE_PPU, options.samples ?? 2, options.basis ?? fixedBasis),
201
+ frame,
202
+ phase: pose.phase,
203
+ travel: pose.travel,
204
+ });
205
+ }
206
+ return {
207
+ clip: clip.name,
208
+ fps,
209
+ frames,
210
+ sourceHash: asset.sourceHash,
211
+ bakeVersion: BAKE_VERSION,
212
+ units: asset.units,
213
+ };
214
+ }
@@ -0,0 +1,89 @@
1
+ import type { Bounds, CompiledAsset, CompiledClip, MeshData, SampledPose, Vec3 } from "./types.js";
2
+ import { meshForAsset } from "./mesh.js";
3
+ export declare const assetMesh: typeof meshForAsset;
4
+ export interface PreviewOptions {
5
+ size?: number;
6
+ yaw?: number;
7
+ pitch?: number;
8
+ maxTriangles?: number;
9
+ /** Rendered looping frames retained per preview, in bytes. Zero disables caching. */
10
+ frameCacheBytes?: number;
11
+ /** Suspend automatic playback outside the viewport or in a hidden document (default true). */
12
+ pauseWhenHidden?: boolean;
13
+ onPick?: (instanceId: string | null) => void;
14
+ /** Playback: called after each rendered clip frame. */
15
+ onFrame?: (frame: number, pose: SampledPose) => void;
16
+ }
17
+ /** Static assets drawn with a playing clip, e.g. the furniture a character sits on. */
18
+ export interface PreviewProp {
19
+ asset: CompiledAsset;
20
+ tint?: string;
21
+ offset?: Vec3;
22
+ }
23
+ export interface PlayOptions {
24
+ tint?: string;
25
+ /** Integer sampling rate; frames are sampled at frame / fps seconds. */
26
+ fps?: number;
27
+ /** Translation applied to the posed asset, e.g. a seat anchor from seatOn. */
28
+ offset?: Vec3;
29
+ props?: PreviewProp[];
30
+ /** Start paused on the first frame when false. Respect prefers-reduced-motion in the host page. */
31
+ autoplay?: boolean;
32
+ /** A face-only clip or expression overlaid on the body clip (see samplePose). */
33
+ face?: CompiledClip;
34
+ /** World bounds to frame instead of the clip's sampled bounds, e.g. a close-up of the head. */
35
+ fit?: Bounds;
36
+ }
37
+ /** What a preview draws: the shown asset (posed during playback), then its props. */
38
+ export interface PreviewSnapshot {
39
+ /** Empty when nothing is loaded or a raw mesh was set with setMesh. */
40
+ scene: PreviewProp[];
41
+ camera: {
42
+ yaw: number;
43
+ pitch: number;
44
+ };
45
+ /** Bounds the playing clip is framed to: its explicit `fit`, or the union of its sampled bounds. */
46
+ fit?: Bounds;
47
+ /** Playing or paused clip and its integer frame. */
48
+ clip?: string;
49
+ frame?: number;
50
+ }
51
+ /**
52
+ * A CPU preview with optional scheduled clip playback. Call dispose when removed.
53
+ *
54
+ * Dragging grabs the model: it turns with the pointer horizontally and tilts vertically. When the
55
+ * canvas has focus, arrow keys orbit the same way (Shift for finer steps) and Home restores the
56
+ * initial view.
57
+ */
58
+ export declare function createPreview(canvas: HTMLCanvasElement, options?: PreviewOptions): {
59
+ setAsset(value: CompiledAsset, shownTint?: string): void;
60
+ setMesh(value: MeshData): void;
61
+ /** Recolor the current asset without restarting its pose, framing or playback clock. */
62
+ setTint(value?: string | null): void;
63
+ setCamera(value: {
64
+ yaw: number;
65
+ pitch?: number;
66
+ }): void;
67
+ /** The current camera, including any orbit from dragging or the keyboard. */
68
+ readonly camera: {
69
+ yaw: number;
70
+ pitch: number;
71
+ };
72
+ pick: (x: number, y: number) => string | null;
73
+ /** The current frame's scene and camera, e.g. for `exportGLB` or `bakeScene` from `/export`. */
74
+ snapshot(): PreviewSnapshot;
75
+ /**
76
+ * Play a compiled clip of a rigged asset. Framing is fixed to the union of sampled
77
+ * bounds, travel is not applied (the clip plays in place), and frames are scheduled only while playing.
78
+ */
79
+ play(source: CompiledAsset, clip: CompiledClip, play?: PlayOptions): void;
80
+ pause(): void;
81
+ resume(): void;
82
+ /** Show one integer frame of the playing clip; playback continues from it if it was running. */
83
+ seek(n: number): void;
84
+ readonly playing: boolean;
85
+ readonly suspended: boolean;
86
+ readonly frame: number | undefined;
87
+ dispose(): void;
88
+ readonly disposed: boolean;
89
+ };