···11+# Shapecraft — Implementation Plan
22+33+## Overview
44+55+A procedural 3D model generation library for the browser. Functional-first API, Three.js under the hood, composable generators and modifiers, first-class noise support via an existing UberNoise library.
66+77+**Key design principles:**
88+- Generators are just functions that return meshes. No class hierarchies, no registration.
99+- Composition = calling functions inside functions.
1010+- Immutable by default — every transform/modifier returns a new Mesh.
1111+- Three.js is the internal engine but the API should feel library-agnostic where possible.
1212+- The package name is "shapecraft" but avoid hardcoding it deeply — keep it easy to rename.
1313+1414+---
1515+1616+## Tech Stack
1717+1818+- **Language:** TypeScript (strict)
1919+- **3D Engine:** Three.js (peer dependency — user provides it)
2020+- **Noise:** Existing UberNoise library (bundled, see `uber-noise.ts` and its deps `simplex-noise/` and `alea/`)
2121+- **Build:** Vite (library mode for the package, dev server for demos)
2222+- **Test:** Vitest
2323+- **Package:** Single flat package, subpath exports for tree-shaking (`shapecraft`, `shapecraft/noise`, `shapecraft/three`, etc.)
2424+2525+---
2626+2727+## Project Structure
2828+2929+```
3030+shapecraft/
3131+├── package.json
3232+├── tsconfig.json
3333+├── vite.config.ts
3434+├── vitest.config.ts
3535+├── index.ts # Main barrel export
3636+├── src/
3737+│ ├── mesh.ts # Core Mesh class
3838+│ ├── types.ts # Shared types (Vec3, Color, etc.)
3939+│ ├── math.ts # Vec3/Mat4 helpers (thin wrappers over THREE)
4040+│ ├── primitives/
4141+│ │ ├── index.ts
4242+│ │ ├── box.ts
4343+│ │ ├── sphere.ts
4444+│ │ ├── cylinder.ts
4545+│ │ ├── plane.ts
4646+│ │ ├── torus.ts
4747+│ │ └── cone.ts
4848+│ ├── ops/
4949+│ │ ├── index.ts
5050+│ │ ├── merge.ts # Combine multiple meshes into one
5151+│ │ ├── clone.ts # Deep clone a mesh
5252+│ │ └── center.ts # Recenter mesh to origin
5353+│ ├── modifiers/
5454+│ │ ├── index.ts
5555+│ │ ├── twist.ts
5656+│ │ ├── bend.ts
5757+│ │ ├── taper.ts
5858+│ │ ├── lattice.ts # Simple lattice deform
5959+│ │ └── smooth.ts # Laplacian smooth
6060+│ ├── noise/
6161+│ │ ├── index.ts # Re-exports UberNoise + helpers
6262+│ │ ├── uber-noise.ts # Existing UberNoise (verbatim, with imports fixed)
6363+│ │ ├── simplex-noise/ # Existing simplex-noise dependency
6464+│ │ │ └── simplex-noise.ts
6565+│ │ ├── alea/ # Existing alea dependency
6666+│ │ │ └── alea.ts
6767+│ │ └── helpers.ts # Convenience: fbm(), ridged(), etc. that return UberNoise instances
6868+│ ├── color/
6969+│ │ ├── index.ts
7070+│ │ ├── vertex-color.ts # Apply vertex colors (flat or procedural fn)
7171+│ │ ├── face-color.ts # Apply per-face colors
7272+│ │ ├── gradient.ts # Height/angle gradient helpers
7373+│ │ └── utils.ts # Color parsing, lerp, hex<->rgb
7474+│ ├── uv/
7575+│ │ ├── index.ts
7676+│ │ ├── projections.ts # Box, planar, cylindrical, spherical UV projection
7777+│ │ └── textures.ts # Procedural texture generators (checkerboard, wood, etc.) — STUB for v0
7878+│ ├── three/
7979+│ │ ├── index.ts
8080+│ │ ├── to-three.ts # Mesh → THREE.Mesh / THREE.BufferGeometry
8181+│ │ └── from-three.ts # THREE.BufferGeometry → Mesh
8282+│ └── worker/
8383+│ ├── index.ts
8484+│ └── pool.ts # STUB for v0 — types + placeholder
8585+├── demo/
8686+│ ├── index.html
8787+│ ├── main.ts # Vite dev entry — renders demo scene
8888+│ └── generators/
8989+│ ├── chair.ts # Example generator: a simple chair
9090+│ ├── table.ts # Example generator: table with chairs
9191+│ └── terrain.ts # Example generator: noise terrain
9292+└── tests/
9393+ ├── mesh.test.ts
9494+ ├── primitives.test.ts
9595+ ├── ops.test.ts
9696+ ├── modifiers.test.ts
9797+ └── noise.test.ts
9898+```
9999+100100+---
101101+102102+## Phase 1: Foundation
103103+104104+### 1.1 — Project setup
105105+106106+- Init `package.json` with:
107107+ - `name: "shapecraft"`
108108+ - `type: "module"`
109109+ - `peerDependencies: { "three": ">=0.150.0" }`
110110+ - `devDependencies: { "three": "^0.170.0", "@types/three": "...", "typescript": "...", "vite": "...", "vitest": "..." }`
111111+ - `exports` field with subpath exports:
112112+ ```json
113113+ {
114114+ ".": "./src/index.ts",
115115+ "./noise": "./src/noise/index.ts",
116116+ "./three": "./src/three/index.ts",
117117+ "./modifiers": "./src/modifiers/index.ts",
118118+ "./ops": "./src/ops/index.ts",
119119+ "./color": "./src/color/index.ts",
120120+ "./uv": "./src/uv/index.ts"
121121+ }
122122+ ```
123123+ - Scripts: `"dev": "vite demo"`, `"build": "vite build"`, `"test": "vitest"`
124124+- `tsconfig.json`: strict, ESNext, paths alias `@/*` → `./src/*`
125125+- `vite.config.ts`: library mode config (entry `src/index.ts`, external `three`)
126126+- `vitest.config.ts`: basic setup
127127+128128+### 1.2 — Types (`src/types.ts`)
129129+130130+```ts
131131+export type Vec2 = [number, number]
132132+export type Vec3 = [number, number, number]
133133+export type Vec4 = [number, number, number, number]
134134+export type ColorInput = string | Vec3 | Vec4 | number // hex string, rgb tuple, rgba tuple, or 0xRRGGBB
135135+export type ColorFn = (position: Vec3, normal: Vec3, index: number) => ColorInput
136136+export type DisplaceFn = (position: Vec3, normal: Vec3, uv: Vec2 | null, index: number) => number
137137+export type WarpFn = (position: Vec3, index: number) => Vec3
138138+export type NoiseLike = { get(x: number, y?: number, z?: number): number }
139139+```
140140+141141+### 1.3 — Core Mesh class (`src/mesh.ts`)
142142+143143+This is the most important file. The Mesh wraps a `THREE.BufferGeometry` internally.
144144+145145+```ts
146146+import * as THREE from 'three'
147147+148148+class Mesh {
149149+ /** Internal geometry — users CAN access this but the API doesn't require it */
150150+ readonly geometry: THREE.BufferGeometry
151151+152152+ constructor(geometry: THREE.BufferGeometry)
153153+154154+ // --- Accessors (read from geometry attributes) ---
155155+ get positions(): Float32Array
156156+ get indices(): Uint32Array | Uint16Array | null
157157+ get normals(): Float32Array | null
158158+ get uvs(): Float32Array | null
159159+ get colors(): Float32Array | null
160160+ get vertexCount(): number
161161+ get faceCount(): number
162162+ get boundingBox(): THREE.Box3
163163+164164+ // --- Transforms (all return new Mesh, geometry is cloned) ---
165165+ translate(x: number, y: number, z: number): Mesh
166166+ rotate(axis: Vec3 | 'x' | 'y' | 'z', angle: number): Mesh
167167+ rotateX(angle: number): Mesh
168168+ rotateY(angle: number): Mesh
169169+ rotateZ(angle: number): Mesh
170170+ scale(x: number, y?: number, z?: number): Mesh
171171+ transform(matrix: THREE.Matrix4): Mesh
172172+173173+ // --- Modifiers (return new Mesh) ---
174174+ displace(fn: DisplaceFn): Mesh
175175+ displaceNoise(noise: NoiseLike, amplitude?: number): Mesh
176176+ warp(fn: WarpFn): Mesh
177177+ subdivide(iterations?: number): Mesh // uses THREE's subdivision if available, else loop subdivision
178178+ computeNormals(): Mesh
179179+180180+ // --- Coloring (return new Mesh) ---
181181+ vertexColor(color: ColorInput | ColorFn): Mesh
182182+ faceColor(fn: (centroid: Vec3, normal: Vec3, faceIndex: number) => ColorInput): Mesh
183183+184184+ // --- UV (return new Mesh) ---
185185+ computeUVs(projection?: 'box' | 'planar' | 'cylindrical' | 'spherical'): Mesh
186186+187187+ // --- Utility ---
188188+ center(): Mesh
189189+ clone(): Mesh
190190+191191+ // --- Serialization (for future worker support) ---
192192+ serialize(): ArrayBuffer
193193+ static deserialize(buffer: ArrayBuffer): Mesh
194194+}
195195+```
196196+197197+**Implementation notes:**
198198+- Every method that returns a new Mesh should: clone the geometry, apply the operation to the clone, return `new Mesh(clone)`.
199199+- Use a private helper `cloneGeometry()` that does `geometry.clone()` and ensures all attributes are properly copied.
200200+- `displace(fn)`: iterate vertices, call fn with position + normal + uv, move vertex along its normal by the returned amount. Recompute normals after.
201201+- `warp(fn)`: iterate vertices, replace position with fn result. Recompute normals after.
202202+- `vertexColor(color | fn)`: if color, set all vertex colors to that color. If fn, iterate vertices and call fn. Creates/updates a `color` attribute (Float32Array, itemSize 3).
203203+- `faceColor(fn)`: iterate faces (3 indices per face), compute centroid and face normal, call fn, set all 3 vertices of that face to the returned color. **Important:** this requires un-indexing the geometry first (so vertices aren't shared between faces). Use `geometry.toNonIndexed()` before applying.
204204+- `serialize()`/`deserialize()`: pack all typed arrays into a single ArrayBuffer with a simple header (attribute count, sizes). This is for future worker support.
205205+206206+### 1.4 — Math utilities (`src/math.ts`)
207207+208208+Thin wrappers — don't reimplement, use THREE internally:
209209+210210+```ts
211211+import * as THREE from 'three'
212212+213213+export function vec3(x: number, y: number, z: number): THREE.Vector3
214214+export function mat4(): THREE.Matrix4
215215+export function makeTranslation(x: number, y: number, z: number): THREE.Matrix4
216216+export function makeRotation(axis: Vec3 | 'x' | 'y' | 'z', angle: number): THREE.Matrix4
217217+export function makeScale(x: number, y: number, z: number): THREE.Matrix4
218218+export function parseColor(input: ColorInput): THREE.Color
219219+```
220220+221221+---
222222+223223+## Phase 2: Primitives
224224+225225+All primitives are **functions** that return a `Mesh`. They use Three.js geometry constructors internally.
226226+227227+### `src/primitives/box.ts`
228228+```ts
229229+export interface BoxOptions {
230230+ width?: number // default 1
231231+ height?: number // default 1
232232+ depth?: number // default 1
233233+ // OR shorthand:
234234+ size?: Vec3 | number // overrides width/height/depth
235235+ widthSegments?: number
236236+ heightSegments?: number
237237+ depthSegments?: number
238238+}
239239+export function box(options?: BoxOptions): Mesh
240240+```
241241+Internally: `new THREE.BoxGeometry(...)` → wrap in Mesh.
242242+243243+### `src/primitives/sphere.ts`
244244+```ts
245245+export interface SphereOptions {
246246+ radius?: number // default 0.5
247247+ widthSegments?: number // default 16
248248+ heightSegments?: number // default 12
249249+}
250250+export function sphere(options?: SphereOptions): Mesh
251251+```
252252+253253+### `src/primitives/cylinder.ts`
254254+```ts
255255+export interface CylinderOptions {
256256+ radius?: number // default 0.5 (sets both top and bottom)
257257+ radiusTop?: number // overrides radius for top
258258+ radiusBottom?: number // overrides radius for bottom
259259+ height?: number // default 1
260260+ segments?: number // default 16
261261+}
262262+export function cylinder(options?: CylinderOptions): Mesh
263263+```
264264+265265+### `src/primitives/plane.ts`
266266+```ts
267267+export interface PlaneOptions {
268268+ width?: number // default 1
269269+ height?: number // default 1
270270+ // OR shorthand:
271271+ size?: number | Vec2 // overrides width/height
272272+ widthSegments?: number // default 1
273273+ heightSegments?: number // default 1
274274+ // OR shorthand:
275275+ segments?: number | Vec2 // overrides both segment counts
276276+}
277277+export function plane(options?: PlaneOptions): Mesh
278278+```
279279+**Note:** Three.js PlaneGeometry creates a vertical plane (XY). We should rotate it to be horizontal (XZ) by default since that's more natural for terrain/floors. Document this clearly.
280280+281281+### `src/primitives/cone.ts`
282282+```ts
283283+export interface ConeOptions {
284284+ radius?: number
285285+ height?: number
286286+ segments?: number
287287+}
288288+export function cone(options?: ConeOptions): Mesh
289289+```
290290+291291+### `src/primitives/torus.ts`
292292+```ts
293293+export interface TorusOptions {
294294+ radius?: number
295295+ tube?: number
296296+ radialSegments?: number
297297+ tubularSegments?: number
298298+}
299299+export function torus(options?: TorusOptions): Mesh
300300+```
301301+302302+### `src/primitives/index.ts`
303303+Re-export all primitives.
304304+305305+---
306306+307307+## Phase 3: Operations
308308+309309+### `src/ops/merge.ts`
310310+```ts
311311+export function merge(...meshes: Mesh[]): Mesh
312312+```
313313+Implementation: use `THREE.BufferGeometryUtils.mergeGeometries()` (import from `three/addons/utils/BufferGeometryUtils.js`). Handle the case where some meshes have vertex colors and some don't — fill missing colors with white. Same for UVs — fill missing with zeros.
314314+315315+**Important:** `mergeGeometries` requires all geometries to have the same set of attributes. Before merging, normalize all geometries to have the same attribute set. If any mesh in the set has colors, ensure all do. If any has UVs, ensure all do.
316316+317317+### `src/ops/center.ts`
318318+```ts
319319+export function center(mesh: Mesh): Mesh
320320+```
321321+Compute bounding box center, translate by negative center.
322322+323323+### `src/ops/clone.ts`
324324+```ts
325325+export function clone(mesh: Mesh): Mesh // just calls mesh.clone()
326326+```
327327+328328+---
329329+330330+## Phase 4: Modifiers
331331+332332+Modifiers are standalone functions that return `WarpFn` or can be applied directly. Two patterns:
333333+334334+**Pattern A — warp functions** (for use with `mesh.warp(fn)`):
335335+```ts
336336+// Returns a WarpFn: (position: Vec3) => Vec3
337337+export function twist(options: { axis?: 'x' | 'y' | 'z', amount: number }): WarpFn
338338+export function bend(options: { axis?: 'x' | 'y' | 'z', amount: number }): WarpFn
339339+export function taper(options: { axis?: 'x' | 'y' | 'z', curve?: (t: number) => number }): WarpFn
340340+```
341341+342342+**Pattern B — mesh-in mesh-out** (for complex operations):
343343+```ts
344344+export function smooth(mesh: Mesh, iterations?: number): Mesh
345345+export function subdivide(mesh: Mesh, iterations?: number): Mesh
346346+```
347347+348348+### `src/modifiers/twist.ts`
349349+Rotate vertices around an axis proportional to their position along that axis.
350350+```
351351+angle = position[axis] * amount
352352+rotate position around axis by angle
353353+```
354354+355355+### `src/modifiers/bend.ts`
356356+Curve vertices along an axis. Map position along axis to an arc.
357357+358358+### `src/modifiers/taper.ts`
359359+Scale vertices perpendicular to an axis based on a curve function of their position along the axis.
360360+```
361361+t = normalize position along axis to [0, 1]
362362+scale = curve(t)
363363+position[perpendicular axes] *= scale
364364+```
365365+366366+### `src/modifiers/smooth.ts`
367367+Laplacian smoothing: for each vertex, move it toward the average of its neighbors. Requires building an adjacency map from the index buffer.
368368+369369+### `src/modifiers/lattice.ts`
370370+STUB for v0. Just export the type and a placeholder that returns the input unchanged.
371371+372372+---
373373+374374+## Phase 5: Noise Integration
375375+376376+### Copy existing code
377377+Copy the user's existing noise implementation into `src/noise/`:
378378+- `src/noise/uber-noise.ts` — the UberNoise class (adjust imports to use relative paths within the package)
379379+- `src/noise/simplex-noise/simplex-noise.ts` — existing simplex noise
380380+- `src/noise/alea/alea.ts` — existing alea PRNG
381381+382382+**Important:** Remove the `globalThis` assignments at the bottom of `uber-noise.ts`. We don't want to pollute globals — everything is imported.
383383+384384+### `src/noise/helpers.ts`
385385+Convenience factory functions that create pre-configured UberNoise instances:
386386+387387+```ts
388388+import { UberNoise, type NoiseOptions } from './uber-noise'
389389+390390+/** Basic simplex noise */
391391+export function simplex(options?: Partial<NoiseOptions>): UberNoise
392392+393393+/** FBM (fractional Brownian motion) with sensible defaults */
394394+export function fbm(options?: Partial<NoiseOptions> & { octaves?: number }): UberNoise
395395+// Default: octaves 4, lacunarity 2, gain 0.5
396396+397397+/** Ridged noise */
398398+export function ridged(options?: Partial<NoiseOptions>): UberNoise
399399+// Sets sharpness: -1
400400+401401+/** Billowed noise */
402402+export function billowed(options?: Partial<NoiseOptions>): UberNoise
403403+// Sets sharpness: 1
404404+405405+/** Stepped/terraced noise */
406406+export function stepped(steps: number, options?: Partial<NoiseOptions>): UberNoise
407407+// Sets steps
408408+409409+/** Warped noise */
410410+export function warped(amount: number, options?: Partial<NoiseOptions>): UberNoise
411411+// Sets warp amount
412412+```
413413+414414+### `src/noise/index.ts`
415415+```ts
416416+export { UberNoise, type NoiseOptions } from './uber-noise'
417417+export { simplex, fbm, ridged, billowed, stepped, warped } from './helpers'
418418+```
419419+420420+### Integration with Mesh
421421+422422+`mesh.displaceNoise(noise, amplitude)` should accept an `UberNoise` instance (or anything with a `.get(x, y, z)` method):
423423+424424+```ts
425425+displaceNoise(noise: NoiseLike, amplitude: number = 1): Mesh {
426426+ return this.displace((pos, normal) => {
427427+ return noise.get(pos[0], pos[1], pos[2]) * amplitude
428428+ })
429429+}
430430+```
431431+432432+This keeps the coupling loose — any object with `get(x, y?, z?)` works.
433433+434434+---
435435+436436+## Phase 6: Color & UV
437437+438438+### `src/color/utils.ts`
439439+```ts
440440+export function parseColor(input: ColorInput): [number, number, number] // rgb 0-1
441441+export function lerpColor(a: ColorInput, b: ColorInput, t: number): [number, number, number]
442442+export function hexToRgb(hex: string): [number, number, number]
443443+export function rgbToHex(r: number, g: number, b: number): string
444444+```
445445+Use `THREE.Color` internally for parsing.
446446+447447+### `src/color/gradient.ts`
448448+```ts
449449+export type GradientStop = [number, ColorInput] // [threshold, color]
450450+451451+/** Create a function that maps a value to a color based on gradient stops */
452452+export function gradient(stops: GradientStop[]): (value: number) => [number, number, number]
453453+454454+/** Shorthand for height-based gradient (maps y position to color) */
455455+export function heightGradient(stops: GradientStop[]): ColorFn
456456+```
457457+458458+### `src/color/vertex-color.ts`
459459+Implementation of `Mesh.vertexColor()`:
460460+- If given a flat color, set all vertex colors to that color.
461461+- If given a function, iterate all vertices, call the function with (position, normal, vertexIndex), set the color attribute.
462462+463463+### `src/color/face-color.ts`
464464+Implementation of `Mesh.faceColor()`:
465465+- Call `geometry.toNonIndexed()` first (un-share vertices).
466466+- Iterate face by face (every 3 vertices), compute centroid and normal, call fn, set all 3 vertex colors.
467467+468468+### `src/uv/projections.ts`
469469+```ts
470470+export function projectUVs(geometry: THREE.BufferGeometry, mode: 'box' | 'planar' | 'cylindrical' | 'spherical'): void
471471+```
472472+Modifies geometry in place (called on a clone inside `Mesh.computeUVs()`).
473473+474474+- **planar**: project from Y axis down onto XZ plane. `u = x`, `v = z` (normalized to bounding box).
475475+- **box**: tri-planar — pick projection axis per face based on face normal, project from that axis.
476476+- **cylindrical**: `u = atan2(z, x) / (2π)`, `v = y` (normalized).
477477+- **spherical**: `u = atan2(z, x) / (2π)`, `v = acos(y/r) / π`.
478478+479479+### `src/uv/textures.ts`
480480+STUB for v0. Export types and a couple basic functions:
481481+```ts
482482+export function checkerboard(size?: number): (u: number, v: number) => [number, number, number]
483483+```
484484+Full procedural texture system is post-v0.
485485+486486+---
487487+488488+## Phase 7: Three.js Bridge
489489+490490+### `src/three/to-three.ts`
491491+```ts
492492+import * as THREE from 'three'
493493+import type { Mesh } from '../mesh'
494494+495495+/** Convert a shapecraft Mesh to a THREE.Mesh ready to add to a scene */
496496+export function toThreeMesh(mesh: Mesh, options?: {
497497+ material?: THREE.Material
498498+ flatShading?: boolean // default true for low-poly look
499499+ wireframe?: boolean
500500+}): THREE.Mesh
501501+502502+/** Get just the geometry (if user wants to provide their own material) */
503503+export function toThreeGeometry(mesh: Mesh): THREE.BufferGeometry
504504+```
505505+506506+`toThreeMesh` implementation:
507507+- If mesh has vertex colors, use `THREE.MeshStandardMaterial({ vertexColors: true, flatShading })`.
508508+- If no vertex colors, use `THREE.MeshStandardMaterial({ color: 0xcccccc, flatShading })`.
509509+- If wireframe requested, set `material.wireframe = true`.
510510+- The geometry is already a `THREE.BufferGeometry` internally, so just reference it (or clone if immutability is desired).
511511+512512+### `src/three/from-three.ts`
513513+```ts
514514+/** Import an existing Three.js geometry into shapecraft */
515515+export function fromThreeGeometry(geometry: THREE.BufferGeometry): Mesh
516516+```
517517+518518+---
519519+520520+## Phase 8: Worker Support (STUB)
521521+522522+### `src/worker/pool.ts`
523523+For v0, just define the interface and a simple synchronous fallback:
524524+525525+```ts
526526+export interface WorkerPoolOptions {
527527+ maxWorkers?: number
528528+}
529529+530530+export class WorkerPool {
531531+ constructor(options?: WorkerPoolOptions)
532532+533533+ /** Run a generator function in a worker. For v0, runs synchronously. */
534534+ async run<T>(fn: (...args: any[]) => Mesh, ...args: any[]): Promise<Mesh>
535535+536536+ /** Batch run multiple generators. For v0, runs sequentially. */
537537+ async batch(tasks: Array<[Function, ...any[]]>): Promise<Mesh[]>
538538+539539+ dispose(): void
540540+}
541541+```
542542+543543+The v0 implementation just calls the functions directly. Real worker support comes later and will use `Mesh.serialize()`/`deserialize()` with `Transferable`.
544544+545545+---
546546+547547+## Phase 9: Demo
548548+549549+### `demo/index.html`
550550+Basic HTML shell that loads `demo/main.ts` via Vite.
551551+552552+### `demo/main.ts`
553553+Sets up a Three.js scene with:
554554+- Renderer, camera, controls (OrbitControls)
555555+- Ambient light + directional light
556556+- Calls all three demo generators
557557+- Adds them to the scene
558558+- Animation loop
559559+560560+### `demo/generators/chair.ts`
561561+```ts
562562+export function chair(options?: { seatHeight?: number, legRadius?: number }): Mesh
563563+```
564564+A simple 4-legged chair:
565565+- Box for seat
566566+- 4 cylinders for legs
567567+- Box for back
568568+- Vertex colored brown tones
569569+- Returns merged mesh
570570+571571+### `demo/generators/table.ts`
572572+```ts
573573+export function diningSet(options?: { chairs?: number, tableRadius?: number }): Mesh
574574+```
575575+- Cylinder for table top
576576+- 4 cylinder legs
577577+- N chairs arranged in a circle using the `chair()` generator
578578+- Demonstrates composition
579579+580580+### `demo/generators/terrain.ts`
581581+```ts
582582+export function terrain(options?: { size?: number, segments?: number, seed?: number }): Mesh
583583+```
584584+- Large subdivided plane
585585+- Displaced with UberNoise (fbm, maybe ridged mix)
586586+- Height-based vertex coloring (green → brown → gray → white)
587587+- Demonstrates noise integration
588588+589589+---
590590+591591+## Phase 10: Tests
592592+593593+### `tests/mesh.test.ts`
594594+- Construction from geometry
595595+- Transforms: translate, rotate, scale produce correct vertex positions
596596+- Immutability: original mesh unchanged after transform
597597+- `vertexCount` and `faceCount` correct
598598+- `clone()` produces independent copy
599599+600600+### `tests/primitives.test.ts`
601601+- Each primitive returns a Mesh
602602+- Vertex counts are correct for given parameters
603603+- Default options produce reasonable geometry
604604+- `size` shorthand works for box and plane
605605+606606+### `tests/ops.test.ts`
607607+- `merge()` combines vertex counts correctly
608608+- `merge()` handles mixed color/no-color meshes (fills missing with white)
609609+- `center()` puts bounding box center at origin
610610+611611+### `tests/modifiers.test.ts`
612612+- `twist()` modifies positions (not identity)
613613+- `taper()` scales vertices correctly at extremes
614614+- `displace()` moves vertices along normals
615615+- `displaceNoise()` produces non-zero displacement
616616+617617+### `tests/noise.test.ts`
618618+- UberNoise produces values in expected range
619619+- `fbm()` helper returns configured UberNoise
620620+- `ridged()` produces values with expected characteristics
621621+- Seeded noise is deterministic
622622+623623+---
624624+625625+## Implementation Order for Claude Code
626626+627627+Follow this order. Each step should be completable and testable before moving to the next.
628628+629629+1. **Project scaffolding** — `package.json`, `tsconfig.json`, `vite.config.ts`, `vitest.config.ts`, directory structure, install deps.
630630+631631+2. **Types + Math** — `src/types.ts`, `src/math.ts`.
632632+633633+3. **Core Mesh class** — `src/mesh.ts`. Start with constructor, accessors, transforms (`translate`, `rotate`, `scale`, `transform`), `clone()`, `center()`, `computeNormals()`. Write `tests/mesh.test.ts` alongside.
634634+635635+4. **Primitives** — All 6 primitives. Write `tests/primitives.test.ts`. At this point you can already do `box().translate(1,0,0)`.
636636+637637+5. **Merge operation** — `src/ops/merge.ts` + `center.ts` + `clone.ts`. Write `tests/ops.test.ts`. Now you can compose: `merge(box().translate(-1,0,0), sphere().translate(1,0,0))`.
638638+639639+6. **Noise integration** — Copy UberNoise + deps into `src/noise/`, fix imports, strip globals, write `src/noise/helpers.ts`, write `src/noise/index.ts`. Write `tests/noise.test.ts`.
640640+641641+7. **Displace + warp on Mesh** — Implement `mesh.displace()`, `mesh.displaceNoise()`, `mesh.warp()`. These depend on noise being available for testing.
642642+643643+8. **Modifiers** — `twist`, `bend`, `taper`, `smooth` (lattice as stub). Write `tests/modifiers.test.ts`.
644644+645645+9. **Color system** — `src/color/*`. Implement `mesh.vertexColor()` and `mesh.faceColor()`, plus gradient helpers.
646646+647647+10. **UV projections** — `src/uv/projections.ts`, implement `mesh.computeUVs()`. Texture stubs.
648648+649649+11. **Three.js bridge** — `src/three/to-three.ts`, `src/three/from-three.ts`.
650650+651651+12. **Worker stubs** — `src/worker/pool.ts`.
652652+653653+13. **Main barrel exports** — `src/index.ts` re-exporting everything.
654654+655655+14. **Demo** — `demo/index.html`, `demo/main.ts`, all three generators (`chair.ts`, `table.ts`, `terrain.ts`). This is the integration test that proves everything works together.
656656+657657+15. **Final pass** — Run all tests, fix any issues, make sure `npm run dev` launches the demo and it looks good.
658658+659659+---
660660+661661+## API Surface Summary (what gets exported)
662662+663663+```ts
664664+// shapecraft (main)
665665+export { Mesh } from './mesh'
666666+export { box, sphere, cylinder, plane, cone, torus } from './primitives'
667667+export { merge, center, clone } from './ops'
668668+export { twist, bend, taper, smooth, subdivide } from './modifiers'
669669+export { vertexColor, faceColor, gradient, heightGradient, lerpColor } from './color'
670670+export { projectUVs } from './uv'
671671+672672+// shapecraft/noise
673673+export { UberNoise, simplex, fbm, ridged, billowed, stepped, warped } from './noise'
674674+675675+// shapecraft/three
676676+export { toThreeMesh, toThreeGeometry, fromThreeGeometry } from './three'
677677+```
678678+679679+---
680680+681681+## Notes & Gotchas for the Implementer
682682+683683+1. **Always clone geometry before modifying.** Every Mesh method that modifies geometry must clone first. Never mutate the internal geometry of an existing Mesh.
684684+685685+2. **mergeGeometries attribute alignment.** Before calling `THREE.BufferGeometryUtils.mergeGeometries()`, ensure all geometries have the same attribute set. If any geometry has `color` attribute, add a default white color attribute to those that don't. Same pattern for UVs (fill with zeros).
686686+687687+3. **Plane orientation.** `THREE.PlaneGeometry` is XY-aligned. Rotate it -π/2 around X to make it XZ (horizontal) before wrapping in Mesh. This is more intuitive for terrain/floors.
688688+689689+4. **faceColor requires toNonIndexed().** Three.js indexed geometry shares vertices between faces, so you can't set per-face colors without first un-indexing. Call `geometry.toNonIndexed()` before applying face colors.
690690+691691+5. **UberNoise integration.** The existing UberNoise `.get(x, y, z, w)` signature matches what we need. The `NoiseLike` interface should be `{ get(x: number, y?: number, z?: number, w?: number): number }` so UberNoise satisfies it directly.
692692+693693+6. **Color attribute format.** Three.js expects vertex colors as a `Float32Array` with itemSize 3 (RGB, values 0-1). Use `THREE.Color` for parsing hex strings etc.
694694+695695+7. **subdivide()** — For v0, use a simple midpoint subdivision (split each triangle into 4). If `three/examples/jsm` has a SubdivisionModifier or similar, prefer that. Otherwise implement a basic loop subdivision.
696696+697697+8. **Performance note.** Cloning geometry on every operation is fine for the typical use case (building a mesh from a few dozen operations at setup time). It would be a problem if someone tried to modify meshes every frame — but that's not the intended use pattern. Document this.
698698+699699+9. **Import BufferGeometryUtils correctly.** In modern Three.js: `import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js'` or `import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'` depending on the version. Check which import works with the installed Three.js version and use that.
700700+701701+10. **Demo scene lighting.** Use a combination of `THREE.AmbientLight(0x404040)` and `THREE.DirectionalLight(0xffffff, 1)` positioned at `(5, 10, 7)` for good default look with flat shading.
···11-# Shapecraft — Implementation Plan
22-33-## Overview
44-55-A procedural 3D model generation library for the browser. Functional-first API, Three.js under the hood, composable generators and modifiers, first-class noise support via an existing UberNoise library.
66-77-**Key design principles:**
88-- Generators are just functions that return meshes. No class hierarchies, no registration.
99-- Composition = calling functions inside functions.
1010-- Immutable by default — every transform/modifier returns a new Mesh.
1111-- Three.js is the internal engine but the API should feel library-agnostic where possible.
1212-- The package name is "shapecraft" but avoid hardcoding it deeply — keep it easy to rename.
1313-1414----
1515-1616-## Tech Stack
1717-1818-- **Language:** TypeScript (strict)
1919-- **3D Engine:** Three.js (peer dependency — user provides it)
2020-- **Noise:** Existing UberNoise library (bundled, see `uber-noise.ts` and its deps `simplex-noise/` and `alea/`)
2121-- **Build:** Vite (library mode for the package, dev server for demos)
2222-- **Test:** Vitest
2323-- **Package:** Single flat package, subpath exports for tree-shaking (`shapecraft`, `shapecraft/noise`, `shapecraft/three`, etc.)
2424-2525----
2626-2727-## Project Structure
2828-2929-```
3030-shapecraft/
3131-├── package.json
3232-├── tsconfig.json
3333-├── vite.config.ts
3434-├── vitest.config.ts
3535-├── index.ts # Main barrel export
3636-├── src/
3737-│ ├── mesh.ts # Core Mesh class
3838-│ ├── types.ts # Shared types (Vec3, Color, etc.)
3939-│ ├── math.ts # Vec3/Mat4 helpers (thin wrappers over THREE)
4040-│ ├── primitives/
4141-│ │ ├── index.ts
4242-│ │ ├── box.ts
4343-│ │ ├── sphere.ts
4444-│ │ ├── cylinder.ts
4545-│ │ ├── plane.ts
4646-│ │ ├── torus.ts
4747-│ │ └── cone.ts
4848-│ ├── ops/
4949-│ │ ├── index.ts
5050-│ │ ├── merge.ts # Combine multiple meshes into one
5151-│ │ ├── clone.ts # Deep clone a mesh
5252-│ │ └── center.ts # Recenter mesh to origin
5353-│ ├── modifiers/
5454-│ │ ├── index.ts
5555-│ │ ├── twist.ts
5656-│ │ ├── bend.ts
5757-│ │ ├── taper.ts
5858-│ │ ├── lattice.ts # Simple lattice deform
5959-│ │ └── smooth.ts # Laplacian smooth
6060-│ ├── noise/
6161-│ │ ├── index.ts # Re-exports UberNoise + helpers
6262-│ │ ├── uber-noise.ts # Existing UberNoise (verbatim, with imports fixed)
6363-│ │ ├── simplex-noise/ # Existing simplex-noise dependency
6464-│ │ │ └── simplex-noise.ts
6565-│ │ ├── alea/ # Existing alea dependency
6666-│ │ │ └── alea.ts
6767-│ │ └── helpers.ts # Convenience: fbm(), ridged(), etc. that return UberNoise instances
6868-│ ├── color/
6969-│ │ ├── index.ts
7070-│ │ ├── vertex-color.ts # Apply vertex colors (flat or procedural fn)
7171-│ │ ├── face-color.ts # Apply per-face colors
7272-│ │ ├── gradient.ts # Height/angle gradient helpers
7373-│ │ └── utils.ts # Color parsing, lerp, hex<->rgb
7474-│ ├── uv/
7575-│ │ ├── index.ts
7676-│ │ ├── projections.ts # Box, planar, cylindrical, spherical UV projection
7777-│ │ └── textures.ts # Procedural texture generators (checkerboard, wood, etc.) — STUB for v0
7878-│ ├── three/
7979-│ │ ├── index.ts
8080-│ │ ├── to-three.ts # Mesh → THREE.Mesh / THREE.BufferGeometry
8181-│ │ └── from-three.ts # THREE.BufferGeometry → Mesh
8282-│ └── worker/
8383-│ ├── index.ts
8484-│ └── pool.ts # STUB for v0 — types + placeholder
8585-├── demo/
8686-│ ├── index.html
8787-│ ├── main.ts # Vite dev entry — renders demo scene
8888-│ └── generators/
8989-│ ├── chair.ts # Example generator: a simple chair
9090-│ ├── table.ts # Example generator: table with chairs
9191-│ └── terrain.ts # Example generator: noise terrain
9292-└── tests/
9393- ├── mesh.test.ts
9494- ├── primitives.test.ts
9595- ├── ops.test.ts
9696- ├── modifiers.test.ts
9797- └── noise.test.ts
9898-```
9999-100100----
101101-102102-## Phase 1: Foundation
103103-104104-### 1.1 — Project setup
105105-106106-- Init `package.json` with:
107107- - `name: "shapecraft"`
108108- - `type: "module"`
109109- - `peerDependencies: { "three": ">=0.150.0" }`
110110- - `devDependencies: { "three": "^0.170.0", "@types/three": "...", "typescript": "...", "vite": "...", "vitest": "..." }`
111111- - `exports` field with subpath exports:
112112- ```json
113113- {
114114- ".": "./src/index.ts",
115115- "./noise": "./src/noise/index.ts",
116116- "./three": "./src/three/index.ts",
117117- "./modifiers": "./src/modifiers/index.ts",
118118- "./ops": "./src/ops/index.ts",
119119- "./color": "./src/color/index.ts",
120120- "./uv": "./src/uv/index.ts"
121121- }
122122- ```
123123- - Scripts: `"dev": "vite demo"`, `"build": "vite build"`, `"test": "vitest"`
124124-- `tsconfig.json`: strict, ESNext, paths alias `@/*` → `./src/*`
125125-- `vite.config.ts`: library mode config (entry `src/index.ts`, external `three`)
126126-- `vitest.config.ts`: basic setup
127127-128128-### 1.2 — Types (`src/types.ts`)
129129-130130-```ts
131131-export type Vec2 = [number, number]
132132-export type Vec3 = [number, number, number]
133133-export type Vec4 = [number, number, number, number]
134134-export type ColorInput = string | Vec3 | Vec4 | number // hex string, rgb tuple, rgba tuple, or 0xRRGGBB
135135-export type ColorFn = (position: Vec3, normal: Vec3, index: number) => ColorInput
136136-export type DisplaceFn = (position: Vec3, normal: Vec3, uv: Vec2 | null, index: number) => number
137137-export type WarpFn = (position: Vec3, index: number) => Vec3
138138-export type NoiseLike = { get(x: number, y?: number, z?: number): number }
139139-```
140140-141141-### 1.3 — Core Mesh class (`src/mesh.ts`)
142142-143143-This is the most important file. The Mesh wraps a `THREE.BufferGeometry` internally.
144144-145145-```ts
146146-import * as THREE from 'three'
147147-148148-class Mesh {
149149- /** Internal geometry — users CAN access this but the API doesn't require it */
150150- readonly geometry: THREE.BufferGeometry
151151-152152- constructor(geometry: THREE.BufferGeometry)
153153-154154- // --- Accessors (read from geometry attributes) ---
155155- get positions(): Float32Array
156156- get indices(): Uint32Array | Uint16Array | null
157157- get normals(): Float32Array | null
158158- get uvs(): Float32Array | null
159159- get colors(): Float32Array | null
160160- get vertexCount(): number
161161- get faceCount(): number
162162- get boundingBox(): THREE.Box3
163163-164164- // --- Transforms (all return new Mesh, geometry is cloned) ---
165165- translate(x: number, y: number, z: number): Mesh
166166- rotate(axis: Vec3 | 'x' | 'y' | 'z', angle: number): Mesh
167167- rotateX(angle: number): Mesh
168168- rotateY(angle: number): Mesh
169169- rotateZ(angle: number): Mesh
170170- scale(x: number, y?: number, z?: number): Mesh
171171- transform(matrix: THREE.Matrix4): Mesh
172172-173173- // --- Modifiers (return new Mesh) ---
174174- displace(fn: DisplaceFn): Mesh
175175- displaceNoise(noise: NoiseLike, amplitude?: number): Mesh
176176- warp(fn: WarpFn): Mesh
177177- subdivide(iterations?: number): Mesh // uses THREE's subdivision if available, else loop subdivision
178178- computeNormals(): Mesh
179179-180180- // --- Coloring (return new Mesh) ---
181181- vertexColor(color: ColorInput | ColorFn): Mesh
182182- faceColor(fn: (centroid: Vec3, normal: Vec3, faceIndex: number) => ColorInput): Mesh
183183-184184- // --- UV (return new Mesh) ---
185185- computeUVs(projection?: 'box' | 'planar' | 'cylindrical' | 'spherical'): Mesh
186186-187187- // --- Utility ---
188188- center(): Mesh
189189- clone(): Mesh
190190-191191- // --- Serialization (for future worker support) ---
192192- serialize(): ArrayBuffer
193193- static deserialize(buffer: ArrayBuffer): Mesh
194194-}
195195-```
196196-197197-**Implementation notes:**
198198-- Every method that returns a new Mesh should: clone the geometry, apply the operation to the clone, return `new Mesh(clone)`.
199199-- Use a private helper `cloneGeometry()` that does `geometry.clone()` and ensures all attributes are properly copied.
200200-- `displace(fn)`: iterate vertices, call fn with position + normal + uv, move vertex along its normal by the returned amount. Recompute normals after.
201201-- `warp(fn)`: iterate vertices, replace position with fn result. Recompute normals after.
202202-- `vertexColor(color | fn)`: if color, set all vertex colors to that color. If fn, iterate vertices and call fn. Creates/updates a `color` attribute (Float32Array, itemSize 3).
203203-- `faceColor(fn)`: iterate faces (3 indices per face), compute centroid and face normal, call fn, set all 3 vertices of that face to the returned color. **Important:** this requires un-indexing the geometry first (so vertices aren't shared between faces). Use `geometry.toNonIndexed()` before applying.
204204-- `serialize()`/`deserialize()`: pack all typed arrays into a single ArrayBuffer with a simple header (attribute count, sizes). This is for future worker support.
205205-206206-### 1.4 — Math utilities (`src/math.ts`)
207207-208208-Thin wrappers — don't reimplement, use THREE internally:
209209-210210-```ts
211211-import * as THREE from 'three'
212212-213213-export function vec3(x: number, y: number, z: number): THREE.Vector3
214214-export function mat4(): THREE.Matrix4
215215-export function makeTranslation(x: number, y: number, z: number): THREE.Matrix4
216216-export function makeRotation(axis: Vec3 | 'x' | 'y' | 'z', angle: number): THREE.Matrix4
217217-export function makeScale(x: number, y: number, z: number): THREE.Matrix4
218218-export function parseColor(input: ColorInput): THREE.Color
219219-```
220220-221221----
222222-223223-## Phase 2: Primitives
224224-225225-All primitives are **functions** that return a `Mesh`. They use Three.js geometry constructors internally.
226226-227227-### `src/primitives/box.ts`
228228-```ts
229229-export interface BoxOptions {
230230- width?: number // default 1
231231- height?: number // default 1
232232- depth?: number // default 1
233233- // OR shorthand:
234234- size?: Vec3 | number // overrides width/height/depth
235235- widthSegments?: number
236236- heightSegments?: number
237237- depthSegments?: number
238238-}
239239-export function box(options?: BoxOptions): Mesh
240240-```
241241-Internally: `new THREE.BoxGeometry(...)` → wrap in Mesh.
242242-243243-### `src/primitives/sphere.ts`
244244-```ts
245245-export interface SphereOptions {
246246- radius?: number // default 0.5
247247- widthSegments?: number // default 16
248248- heightSegments?: number // default 12
249249-}
250250-export function sphere(options?: SphereOptions): Mesh
251251-```
252252-253253-### `src/primitives/cylinder.ts`
254254-```ts
255255-export interface CylinderOptions {
256256- radius?: number // default 0.5 (sets both top and bottom)
257257- radiusTop?: number // overrides radius for top
258258- radiusBottom?: number // overrides radius for bottom
259259- height?: number // default 1
260260- segments?: number // default 16
261261-}
262262-export function cylinder(options?: CylinderOptions): Mesh
263263-```
264264-265265-### `src/primitives/plane.ts`
266266-```ts
267267-export interface PlaneOptions {
268268- width?: number // default 1
269269- height?: number // default 1
270270- // OR shorthand:
271271- size?: number | Vec2 // overrides width/height
272272- widthSegments?: number // default 1
273273- heightSegments?: number // default 1
274274- // OR shorthand:
275275- segments?: number | Vec2 // overrides both segment counts
276276-}
277277-export function plane(options?: PlaneOptions): Mesh
278278-```
279279-**Note:** Three.js PlaneGeometry creates a vertical plane (XY). We should rotate it to be horizontal (XZ) by default since that's more natural for terrain/floors. Document this clearly.
280280-281281-### `src/primitives/cone.ts`
282282-```ts
283283-export interface ConeOptions {
284284- radius?: number
285285- height?: number
286286- segments?: number
287287-}
288288-export function cone(options?: ConeOptions): Mesh
289289-```
290290-291291-### `src/primitives/torus.ts`
292292-```ts
293293-export interface TorusOptions {
294294- radius?: number
295295- tube?: number
296296- radialSegments?: number
297297- tubularSegments?: number
298298-}
299299-export function torus(options?: TorusOptions): Mesh
300300-```
301301-302302-### `src/primitives/index.ts`
303303-Re-export all primitives.
304304-305305----
306306-307307-## Phase 3: Operations
308308-309309-### `src/ops/merge.ts`
310310-```ts
311311-export function merge(...meshes: Mesh[]): Mesh
312312-```
313313-Implementation: use `THREE.BufferGeometryUtils.mergeGeometries()` (import from `three/addons/utils/BufferGeometryUtils.js`). Handle the case where some meshes have vertex colors and some don't — fill missing colors with white. Same for UVs — fill missing with zeros.
314314-315315-**Important:** `mergeGeometries` requires all geometries to have the same set of attributes. Before merging, normalize all geometries to have the same attribute set. If any mesh in the set has colors, ensure all do. If any has UVs, ensure all do.
316316-317317-### `src/ops/center.ts`
318318-```ts
319319-export function center(mesh: Mesh): Mesh
320320-```
321321-Compute bounding box center, translate by negative center.
322322-323323-### `src/ops/clone.ts`
324324-```ts
325325-export function clone(mesh: Mesh): Mesh // just calls mesh.clone()
326326-```
327327-328328----
329329-330330-## Phase 4: Modifiers
331331-332332-Modifiers are standalone functions that return `WarpFn` or can be applied directly. Two patterns:
333333-334334-**Pattern A — warp functions** (for use with `mesh.warp(fn)`):
335335-```ts
336336-// Returns a WarpFn: (position: Vec3) => Vec3
337337-export function twist(options: { axis?: 'x' | 'y' | 'z', amount: number }): WarpFn
338338-export function bend(options: { axis?: 'x' | 'y' | 'z', amount: number }): WarpFn
339339-export function taper(options: { axis?: 'x' | 'y' | 'z', curve?: (t: number) => number }): WarpFn
340340-```
341341-342342-**Pattern B — mesh-in mesh-out** (for complex operations):
343343-```ts
344344-export function smooth(mesh: Mesh, iterations?: number): Mesh
345345-export function subdivide(mesh: Mesh, iterations?: number): Mesh
346346-```
347347-348348-### `src/modifiers/twist.ts`
349349-Rotate vertices around an axis proportional to their position along that axis.
350350-```
351351-angle = position[axis] * amount
352352-rotate position around axis by angle
353353-```
354354-355355-### `src/modifiers/bend.ts`
356356-Curve vertices along an axis. Map position along axis to an arc.
357357-358358-### `src/modifiers/taper.ts`
359359-Scale vertices perpendicular to an axis based on a curve function of their position along the axis.
360360-```
361361-t = normalize position along axis to [0, 1]
362362-scale = curve(t)
363363-position[perpendicular axes] *= scale
364364-```
365365-366366-### `src/modifiers/smooth.ts`
367367-Laplacian smoothing: for each vertex, move it toward the average of its neighbors. Requires building an adjacency map from the index buffer.
368368-369369-### `src/modifiers/lattice.ts`
370370-STUB for v0. Just export the type and a placeholder that returns the input unchanged.
371371-372372----
373373-374374-## Phase 5: Noise Integration
375375-376376-### Copy existing code
377377-Copy the user's existing noise implementation into `src/noise/`:
378378-- `src/noise/uber-noise.ts` — the UberNoise class (adjust imports to use relative paths within the package)
379379-- `src/noise/simplex-noise/simplex-noise.ts` — existing simplex noise
380380-- `src/noise/alea/alea.ts` — existing alea PRNG
381381-382382-**Important:** Remove the `globalThis` assignments at the bottom of `uber-noise.ts`. We don't want to pollute globals — everything is imported.
383383-384384-### `src/noise/helpers.ts`
385385-Convenience factory functions that create pre-configured UberNoise instances:
386386-387387-```ts
388388-import { UberNoise, type NoiseOptions } from './uber-noise'
389389-390390-/** Basic simplex noise */
391391-export function simplex(options?: Partial<NoiseOptions>): UberNoise
392392-393393-/** FBM (fractional Brownian motion) with sensible defaults */
394394-export function fbm(options?: Partial<NoiseOptions> & { octaves?: number }): UberNoise
395395-// Default: octaves 4, lacunarity 2, gain 0.5
396396-397397-/** Ridged noise */
398398-export function ridged(options?: Partial<NoiseOptions>): UberNoise
399399-// Sets sharpness: -1
400400-401401-/** Billowed noise */
402402-export function billowed(options?: Partial<NoiseOptions>): UberNoise
403403-// Sets sharpness: 1
404404-405405-/** Stepped/terraced noise */
406406-export function stepped(steps: number, options?: Partial<NoiseOptions>): UberNoise
407407-// Sets steps
408408-409409-/** Warped noise */
410410-export function warped(amount: number, options?: Partial<NoiseOptions>): UberNoise
411411-// Sets warp amount
412412-```
413413-414414-### `src/noise/index.ts`
415415-```ts
416416-export { UberNoise, type NoiseOptions } from './uber-noise'
417417-export { simplex, fbm, ridged, billowed, stepped, warped } from './helpers'
418418-```
419419-420420-### Integration with Mesh
421421-422422-`mesh.displaceNoise(noise, amplitude)` should accept an `UberNoise` instance (or anything with a `.get(x, y, z)` method):
423423-424424-```ts
425425-displaceNoise(noise: NoiseLike, amplitude: number = 1): Mesh {
426426- return this.displace((pos, normal) => {
427427- return noise.get(pos[0], pos[1], pos[2]) * amplitude
428428- })
429429-}
430430-```
431431-432432-This keeps the coupling loose — any object with `get(x, y?, z?)` works.
433433-434434----
435435-436436-## Phase 6: Color & UV
437437-438438-### `src/color/utils.ts`
439439-```ts
440440-export function parseColor(input: ColorInput): [number, number, number] // rgb 0-1
441441-export function lerpColor(a: ColorInput, b: ColorInput, t: number): [number, number, number]
442442-export function hexToRgb(hex: string): [number, number, number]
443443-export function rgbToHex(r: number, g: number, b: number): string
444444-```
445445-Use `THREE.Color` internally for parsing.
446446-447447-### `src/color/gradient.ts`
448448-```ts
449449-export type GradientStop = [number, ColorInput] // [threshold, color]
450450-451451-/** Create a function that maps a value to a color based on gradient stops */
452452-export function gradient(stops: GradientStop[]): (value: number) => [number, number, number]
453453-454454-/** Shorthand for height-based gradient (maps y position to color) */
455455-export function heightGradient(stops: GradientStop[]): ColorFn
456456-```
457457-458458-### `src/color/vertex-color.ts`
459459-Implementation of `Mesh.vertexColor()`:
460460-- If given a flat color, set all vertex colors to that color.
461461-- If given a function, iterate all vertices, call the function with (position, normal, vertexIndex), set the color attribute.
462462-463463-### `src/color/face-color.ts`
464464-Implementation of `Mesh.faceColor()`:
465465-- Call `geometry.toNonIndexed()` first (un-share vertices).
466466-- Iterate face by face (every 3 vertices), compute centroid and normal, call fn, set all 3 vertex colors.
467467-468468-### `src/uv/projections.ts`
469469-```ts
470470-export function projectUVs(geometry: THREE.BufferGeometry, mode: 'box' | 'planar' | 'cylindrical' | 'spherical'): void
471471-```
472472-Modifies geometry in place (called on a clone inside `Mesh.computeUVs()`).
11+# Shapecraft — 6-Month Roadmap (v1)
4732474474-- **planar**: project from Y axis down onto XZ plane. `u = x`, `v = z` (normalized to bounding box).
475475-- **box**: tri-planar — pick projection axis per face based on face normal, project from that axis.
476476-- **cylindrical**: `u = atan2(z, x) / (2π)`, `v = y` (normalized).
477477-- **spherical**: `u = atan2(z, x) / (2π)`, `v = acos(y/r) / π`.
33+> The original v0 build plan lives in [`plan-v0.md`](./plan-v0.md). This document is the
44+> forward-looking strategy: improving the models we have, making many more, and growing the
55+> library to support them.
4786479479-### `src/uv/textures.ts`
480480-STUB for v0. Export types and a couple basic functions:
481481-```ts
482482-export function checkerboard(size?: number): (u: number, v: number) => [number, number, number]
483483-```
484484-Full procedural texture system is post-v0.
77+**Team & horizon:** 4 people, full-time, 6 months (~24 person-months).
48584869---
48710488488-## Phase 7: Three.js Bridge
489489-490490-### `src/three/to-three.ts`
491491-```ts
492492-import * as THREE from 'three'
493493-import type { Mesh } from '../mesh'
494494-495495-/** Convert a shapecraft Mesh to a THREE.Mesh ready to add to a scene */
496496-export function toThreeMesh(mesh: Mesh, options?: {
497497- material?: THREE.Material
498498- flatShading?: boolean // default true for low-poly look
499499- wireframe?: boolean
500500-}): THREE.Mesh
1111+## Where we are
50112502502-/** Get just the geometry (if user wants to provide their own material) */
503503-export function toThreeGeometry(mesh: Mesh): THREE.BufferGeometry
504504-```
1313+Shapecraft is a procedural 3D model generation library for the browser: an immutable,
1414+functional `Mesh` API over Three.js, with a stylized low-poly toolbox (noise, palettes,
1515+face/vertex color, loft/tube/thicken, adaptive subdivision, jitter) and a **schema-driven
1616+editor** — each generator declares its options and gets a live UI, presets, and
1717+range-randomization for free.
50518506506-`toThreeMesh` implementation:
507507-- If mesh has vertex colors, use `THREE.MeshStandardMaterial({ vertexColors: true, flatShading })`.
508508-- If no vertex colors, use `THREE.MeshStandardMaterial({ color: 0xcccccc, flatShading })`.
509509-- If wireframe requested, set `material.wireframe = true`.
510510-- The geometry is already a `THREE.BufferGeometry` internally, so just reference it (or clone if immutability is desired).
1919+We ship **three models** today: common tree, pine, palm. All vegetation, all flat-shaded
2020+vertex-colored.
51121512512-### `src/three/from-three.ts`
513513-```ts
514514-/** Import an existing Three.js geometry into shapecraft */
515515-export function fromThreeGeometry(geometry: THREE.BufferGeometry): Mesh
516516-```
2222+**The core tension:** every model is ~200 lines of bespoke, copy-pasted code. Trunk warp,
2323+snow logic, face-shading, and the fragile "call `rand()` to keep the sequence stable" pattern
2424+are duplicated across all three. This does not scale to 20+ models. The ambition goes into the
2525+framework that makes each new model cheap.
5172651827---
51928520520-## Phase 8: Worker Support (STUB)
521521-522522-### `src/worker/pool.ts`
523523-For v0, just define the interface and a simple synchronous fallback:
524524-525525-```ts
526526-export interface WorkerPoolOptions {
527527- maxWorkers?: number
528528-}
2929+## North star
52930530530-export class WorkerPool {
531531- constructor(options?: WorkerPoolOptions)
3131+**All four scopes at once** — best-in-class vegetation, a full stylized environment kit, an
3232+asset-pipeline product, and a great general-purpose library. **All four consumers** — web/
3333+Three.js scenes, game engines, no-code designers, and developers using the API.
53234533533- /** Run a generator function in a worker. For v0, runs synchronously. */
534534- async run<T>(fn: (...args: any[]) => Mesh, ...args: any[]): Promise<Mesh>
3535+### The "all four" insight
53536536536- /** Batch run multiple generators. For v0, runs sequentially. */
537537- async batch(tasks: Array<[Function, ...any[]]>): Promise<Mesh[]>
3737+These don't pull in four directions; they share one spine. The non-negotiable shared platform
3838+is: generator framework + CSG/bevel + curves + weld/LOD + AO/wind + instancing + glTF export +
3939+workers + playground + npm publish. Build that once and all four audiences are served.
53840539539- dispose(): void
540540-}
541541-```
4141+So "all four" resolves to a strict order: **engine-first, then catalog, then platform polish —
4242+with the quality bar (LOD, AO, wind, export) baked into the shared pipeline from the start so
4343+it is free for every model.**
54244543543-The v0 implementation just calls the functions directly. Real worker support comes later and will use `Mesh.serialize()`/`deserialize()` with `Transferable`.
4545+The one risk is shipping four half-products. The mitigation is the sequencing below: the
4646+foundation serves all four scopes simultaneously, and we don't branch into audience-specific
4747+polish until the substrate is real.
5444854549---
54650547547-## Phase 9: Demo
548548-549549-### `demo/index.html`
550550-Basic HTML shell that loads `demo/main.ts` via Vite.
551551-552552-### `demo/main.ts`
553553-Sets up a Three.js scene with:
554554-- Renderer, camera, controls (OrbitControls)
555555-- Ambient light + directional light
556556-- Calls all three demo generators
557557-- Adds them to the scene
558558-- Animation loop
559559-560560-### `demo/generators/chair.ts`
561561-```ts
562562-export function chair(options?: { seatHeight?: number, legRadius?: number }): Mesh
563563-```
564564-A simple 4-legged chair:
565565-- Box for seat
566566-- 4 cylinders for legs
567567-- Box for back
568568-- Vertex colored brown tones
569569-- Returns merged mesh
570570-571571-### `demo/generators/table.ts`
572572-```ts
573573-export function diningSet(options?: { chairs?: number, tableRadius?: number }): Mesh
574574-```
575575-- Cylinder for table top
576576-- 4 cylinder legs
577577-- N chairs arranged in a circle using the `chair()` generator
578578-- Demonstrates composition
5151+## Thesis: build the multiplier before the models
57952580580-### `demo/generators/terrain.ts`
581581-```ts
582582-export function terrain(options?: { size?: number, segments?: number, seed?: number }): Mesh
583583-```
584584-- Large subdivided plane
585585-- Displaced with UberNoise (fbm, maybe ridged mix)
586586-- Height-based vertex coloring (green → brown → gray → white)
587587-- Demonstrates noise integration
5353+The highest-leverage work is not more models — it's the substrate that makes every subsequent
5454+model cheap. Spend the first third of the project there, then flood the catalog.
5885558956---
59057591591-## Phase 10: Tests
592592-593593-### `tests/mesh.test.ts`
594594-- Construction from geometry
595595-- Transforms: translate, rotate, scale produce correct vertex positions
596596-- Immutability: original mesh unchanged after transform
597597-- `vertexCount` and `faceCount` correct
598598-- `clone()` produces independent copy
599599-600600-### `tests/primitives.test.ts`
601601-- Each primitive returns a Mesh
602602-- Vertex counts are correct for given parameters
603603-- Default options produce reasonable geometry
604604-- `size` shorthand works for box and plane
605605-606606-### `tests/ops.test.ts`
607607-- `merge()` combines vertex counts correctly
608608-- `merge()` handles mixed color/no-color meshes (fills missing with white)
609609-- `center()` puts bounding box center at origin
610610-611611-### `tests/modifiers.test.ts`
612612-- `twist()` modifies positions (not identity)
613613-- `taper()` scales vertices correctly at extremes
614614-- `displace()` moves vertices along normals
615615-- `displaceNoise()` produces non-zero displacement
5858+## Two things to fix in week one (regardless of everything else)
61659617617-### `tests/noise.test.ts`
618618-- UberNoise produces values in expected range
619619-- `fbm()` helper returns configured UberNoise
620620-- `ridged()` produces values with expected characteristics
621621-- Seeded noise is deterministic
6060+1. **Determinism model.** Replace the single RNG with **named independent streams**
6161+ (`rng.stream('canopy')`, `rng.stream('snow')`). The current "consume a `rand()` to keep the
6262+ sequence stable" hack is a latent bug farm — change one branch and every downstream model
6363+ shifts. Fix it before it spreads into 20 more files.
6464+2. **Packaging gap.** Today `package.json` `main` points at raw `.ts` — shapecraft is not
6565+ actually consumable as a package. Decide now that it ships as a real built npm package with
6666+ **stable seeds as a compatibility guarantee**. This changes how we test (golden snapshots)
6767+ and how we version from day one.
6226862369---
62470625625-## Implementation Order for Claude Code
626626-627627-Follow this order. Each step should be completable and testable before moving to the next.
628628-629629-1. **Project scaffolding** — `package.json`, `tsconfig.json`, `vite.config.ts`, `vitest.config.ts`, directory structure, install deps.
630630-631631-2. **Types + Math** — `src/types.ts`, `src/math.ts`.
632632-633633-3. **Core Mesh class** — `src/mesh.ts`. Start with constructor, accessors, transforms (`translate`, `rotate`, `scale`, `transform`), `clone()`, `center()`, `computeNormals()`. Write `tests/mesh.test.ts` alongside.
634634-635635-4. **Primitives** — All 6 primitives. Write `tests/primitives.test.ts`. At this point you can already do `box().translate(1,0,0)`.
636636-637637-5. **Merge operation** — `src/ops/merge.ts` + `center.ts` + `clone.ts`. Write `tests/ops.test.ts`. Now you can compose: `merge(box().translate(-1,0,0), sphere().translate(1,0,0))`.
638638-639639-6. **Noise integration** — Copy UberNoise + deps into `src/noise/`, fix imports, strip globals, write `src/noise/helpers.ts`, write `src/noise/index.ts`. Write `tests/noise.test.ts`.
640640-641641-7. **Displace + warp on Mesh** — Implement `mesh.displace()`, `mesh.displaceNoise()`, `mesh.warp()`. These depend on noise being available for testing.
642642-643643-8. **Modifiers** — `twist`, `bend`, `taper`, `smooth` (lattice as stub). Write `tests/modifiers.test.ts`.
644644-645645-9. **Color system** — `src/color/*`. Implement `mesh.vertexColor()` and `mesh.faceColor()`, plus gradient helpers.
646646-647647-10. **UV projections** — `src/uv/projections.ts`, implement `mesh.computeUVs()`. Texture stubs.
648648-649649-11. **Three.js bridge** — `src/three/to-three.ts`, `src/three/from-three.ts`.
7171+## Workstreams
65072651651-12. **Worker stubs** — `src/worker/pool.ts`.
7373+### A. Generator framework (the model multiplier)
7474+- Extract repeated patterns into reusable building blocks: a `trunk()` / tapered-limb builder,
7575+ a `canopyShade()` color helper, a `snow()` modifier.
7676+- **Anchor / socket points** on meshes (canopy top, ground contact, attachment rings) for
7777+ composition.
7878+- **Skeleton / L-system branching engine**: recursive tapered branches with leaf/attachment
7979+ slots. One engine yields oak, birch, willow, dead tree, bush, fern, coral, and vines from
8080+ parameters instead of bespoke files.
8181+- RNG named streams + **golden-snapshot test harness** (vertex counts, bounds, hashes) so
8282+ "seed 5 looks like X" is locked across versions.
65283653653-13. **Main barrel exports** — `src/index.ts` re-exporting everything.
8484+### B. Library capability gaps (unlock new model classes)
8585+- **CSG booleans** (union / subtract / intersect) — windows in walls, holes, carved props,
8686+ hard-surface.
8787+- **Bevel / inset / extrude-faces** — architecture, crates, furniture, crisp edges.
8888+- First-class **Curve / Path type with parallel-transport frames** — fixes twist artifacts in
8989+ `tube`/`loft` and cleans up every stalk/branch/vine.
9090+- **Vertex weld + decimation + automatic LOD generation** — today `faceColor` un-indexes
9191+ everything (×3 vertices) with no way to weld back or produce LODs. Required for any real use
9292+ at scale and for game-engine export.
65493655655-14. **Demo** — `demo/index.html`, `demo/main.ts`, all three generators (`chair.ts`, `table.ts`, `terrain.ts`). This is the integration test that proves everything works together.
9494+### C. Quality lift across all models (cheap, high-impact, on-brand)
9595+- **Baked vertex ambient occlusion / cavity shading** — instantly makes everything read as 3D
9696+ instead of flat.
9797+- **Per-vertex wind weights** — trees sway in a shader, models ship game-ready.
9898+- **Real branches** on the trees (currently trunk-plus-blobs).
9999+- Poly-budget / triangle-count targets per model.
656100657657-15. **Final pass** — Run all tests, fix any issues, make sure `npm run dev` launches the demo and it looks good.
101101+### D. Product surface (make output usable)
102102+- **Instancing**: `scatter → THREE.InstancedMesh` (the forest demo builds 15 unique meshes
103103+ today — critical perf win).
104104+- **glTF / GLB export** (plus OBJ) — the asset pipeline for game engines and designers.
105105+- **Real Web Worker pool** — the pool is a synchronous stub; `serialize()`/Transferable are
106106+ already in place to make it real.
107107+- **Biome / scatter scene generator** — Poisson-disk, noise density maps, slope/altitude rules
108108+ (the forest demo is the seed).
109109+- **Hosted playground + gallery** — the schema editor is the unfair advantage; one-click glTF
110110+ export for the no-code crowd.
111111+- **npm publish pipeline** — real build, `.d.ts`, dual ESM. Docs, perf benchmarks, visual
112112+ regression (the `screenshot.cjs` scripts are the seed).
658113659114---
660115661661-## API Surface Summary (what gets exported)
116116+## Timeline
662117663663-```ts
664664-// shapecraft (main)
665665-export { Mesh } from './mesh'
666666-export { box, sphere, cylinder, plane, cone, torus } from './primitives'
667667-export { merge, center, clone } from './ops'
668668-export { twist, bend, taper, smooth, subdivide } from './modifiers'
669669-export { vertexColor, faceColor, gradient, heightGradient, lerpColor } from './color'
670670-export { projectUVs } from './uv'
118118+### Months 1–2 — Foundation (all 4 people; the whole ballgame, nothing skippable)
119119+- RNG named streams + golden-snapshot test harness.
120120+- Shared model primitives + skeleton/L-system branch engine.
121121+- CSG booleans + bevel/inset/extrude.
122122+- First-class Curve/Path with parallel-transport frames.
123123+- Weld + decimate + automatic LOD generation.
671124672672-// shapecraft/noise
673673-export { UberNoise, simplex, fbm, ridged, billowed, stepped, warped } from './noise'
125125+### Months 2–4 — Catalog explosion + quality baked in (split: 2 on models, 2 on platform)
126126+- **Models team:** ride the framework through vegetation (bush, grass, fern, dead tree, cactus,
127127+ birch, willow, mushroom, bamboo, flower) + rocks/cliffs + the prop/architecture set CSG now
128128+ enables (crates, fences, walls, simple buildings, furniture — chair/table already in the demo).
129129+- **Platform team:** bake AO/cavity shading + per-vertex wind weights into the shared pipeline
130130+ (every model above ships better-looking and game-ready automatically); add instancing and the
131131+ glTF/GLB exporter.
674132675675-// shapecraft/three
676676-export { toThreeMesh, toThreeGeometry, fromThreeGeometry } from './three'
677677-```
133133+### Months 4–6 — Platform & product (split: scene/biome + playground)
134134+- Real Web Worker pool.
135135+- Biome/scatter scene generator.
136136+- Hosted playground + gallery with one-click glTF export.
137137+- npm publish pipeline (build, `.d.ts`, dual ESM).
138138+- Docs, perf benchmarks, visual regression harness.
678139679140---
680141681681-## Notes & Gotchas for the Implementer
682682-683683-1. **Always clone geometry before modifying.** Every Mesh method that modifies geometry must clone first. Never mutate the internal geometry of an existing Mesh.
684684-685685-2. **mergeGeometries attribute alignment.** Before calling `THREE.BufferGeometryUtils.mergeGeometries()`, ensure all geometries have the same attribute set. If any geometry has `color` attribute, add a default white color attribute to those that don't. Same pattern for UVs (fill with zeros).
686686-687687-3. **Plane orientation.** `THREE.PlaneGeometry` is XY-aligned. Rotate it -π/2 around X to make it XZ (horizontal) before wrapping in Mesh. This is more intuitive for terrain/floors.
688688-689689-4. **faceColor requires toNonIndexed().** Three.js indexed geometry shares vertices between faces, so you can't set per-face colors without first un-indexing. Call `geometry.toNonIndexed()` before applying face colors.
690690-691691-5. **UberNoise integration.** The existing UberNoise `.get(x, y, z, w)` signature matches what we need. The `NoiseLike` interface should be `{ get(x: number, y?: number, z?: number, w?: number): number }` so UberNoise satisfies it directly.
692692-693693-6. **Color attribute format.** Three.js expects vertex colors as a `Float32Array` with itemSize 3 (RGB, values 0-1). Use `THREE.Color` for parsing hex strings etc.
694694-695695-7. **subdivide()** — For v0, use a simple midpoint subdivision (split each triangle into 4). If `three/examples/jsm` has a SubdivisionModifier or similar, prefer that. Otherwise implement a basic loop subdivision.
696696-697697-8. **Performance note.** Cloning geometry on every operation is fine for the typical use case (building a mesh from a few dozen operations at setup time). It would be a problem if someone tried to modify meshes every frame — but that's not the intended use pattern. Document this.
698698-699699-9. **Import BufferGeometryUtils correctly.** In modern Three.js: `import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js'` or `import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'` depending on the version. Check which import works with the installed Three.js version and use that.
142142+## Definition of success
700143701701-10. **Demo scene lighting.** Use a combination of `THREE.AmbientLight(0x404040)` and `THREE.DirectionalLight(0xffffff, 1)` positioned at `(5, 10, 7)` for good default look with flat shading.144144+- 20+ models spanning vegetation, rocks, props, and basic architecture — each with presets,
145145+ LODs, baked AO, and wind data.
146146+- Shapecraft installable from npm with stable, versioned seeds.
147147+- One-click glTF/GLB export from a hosted playground.
148148+- Forests/biomes rendered via instancing + workers without blocking the main thread.
149149+- A general-purpose, documented, benchmarked procedural-mesh library others can build on.
···11import { aleaFactory } from '../noise/alea/alea'
2233/**
44- * Create a seeded random number generator.
55- * Returns a function that produces values in [0, 1) on each call.
44+ * A seeded random number generator.
55+ *
66+ * It is callable — `rng()` returns the next float in [0, 1) — so it is a drop-in
77+ * replacement anywhere a `() => number` is expected (scatter, schema, palettes).
88+ *
99+ * The important addition is {@link Rng.stream}: a named, independent sub-generator.
1010+ * Pulling values from one stream never advances another, so a model can give each
1111+ * concern (`'trunk'`, `'canopy'`, `'snow'`, …) its own stream and toggling one feature
1212+ * can't shift the randomness of an unrelated one. This replaces the fragile
1313+ * "call `rand()` to keep the sequence stable" pattern.
1414+ */
1515+export interface Rng {
1616+ /** Next float in [0, 1). Drop-in compatible with `() => number`. */
1717+ (): number
1818+ /** Next float in [min, max) (defaults to [0, 1)). */
1919+ float(min?: number, max?: number): number
2020+ /** Integer in [min, max] inclusive. */
2121+ int(min: number, max: number): number
2222+ /** True with probability `p` (default 0.5). */
2323+ bool(p?: number): boolean
2424+ /** Randomly -1 or 1. */
2525+ sign(): number
2626+ /** A random element of `arr`. */
2727+ pick<T>(arr: readonly T[]): T
2828+ /** Resolve a fixed value or a `[min, max]` range. Rounds when `integer` is true. */
2929+ range(value: number | [number, number], integer?: boolean): number
3030+ /** Derive a fresh integer seed (e.g. for noise/jitter). Advances this stream. */
3131+ seed(): number
3232+ /** An independent, deterministic sub-stream identified by `name`. */
3333+ stream(name: string): Rng
3434+ /** An independent anonymous sub-stream (auto-numbered per parent). */
3535+ fork(): Rng
3636+}
3737+3838+/**
3939+ * Create a seeded RNG from a number or string seed.
640 */
77-export function createRng(seed: number | string): () => number {
88- return aleaFactory(seed).random
4141+export function createRng(seed: number | string): Rng {
4242+ return makeRng(String(seed))
4343+}
4444+4545+function makeRng(base: string): Rng {
4646+ const next = aleaFactory(base).random
4747+ let forkCounter = 0
4848+4949+ const rng = function (): number {
5050+ return next()
5151+ } as Rng
5252+5353+ rng.float = (min = 0, max = 1) => min + next() * (max - min)
5454+ rng.int = (min, max) => Math.floor(min + next() * (max - min + 1))
5555+ rng.bool = (p = 0.5) => next() < p
5656+ rng.sign = () => (next() < 0.5 ? -1 : 1)
5757+ rng.pick = <T>(arr: readonly T[]): T => arr[Math.floor(next() * arr.length)]
5858+ rng.range = (value, integer = false) => {
5959+ if (Array.isArray(value)) {
6060+ const r = value[0] + next() * (value[1] - value[0])
6161+ return integer ? Math.round(r) : r
6262+ }
6363+ return value
6464+ }
6565+ rng.seed = () => Math.floor(next() * 2147483647)
6666+ rng.stream = (name: string) => makeRng(`${base}/${name}`)
6767+ rng.fork = () => makeRng(`${base}#${forkCounter++}`)
6868+6969+ return rng
970}
···6060export type Randomizable<T> = T | [T, T]
61616262/**
6363+ * The *input* shape for a schema: like {@link OptionValues}, but numeric options also
6464+ * accept a `[min, max]` tuple (resolved to a number at generation time). Use this for a
6565+ * generator's public options type; use {@link OptionValues} for the resolved result.
6666+ */
6767+export type OptionInput<S extends OptionSchema> = {
6868+ [K in keyof S]: S[K] extends RangeOption ? Randomizable<number>
6969+ : S[K] extends IntegerOption ? Randomizable<number>
7070+ : S[K] extends ColorOption ? string
7171+ : S[K] extends ColorArrayOption ? string[]
7272+ : S[K] extends BooleanOption ? boolean
7373+ : S[K] extends SelectOption ? string
7474+ : never
7575+}
7676+7777+/**
6378 * Resolve options for a generator: apply preset, then overrides, then fill schema defaults.
6479 * Numeric values can be [min, max] tuples — resolved using rand() if provided.
6580 */