···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.