[READ-ONLY] Mirror of https://github.com/flo-bit/shapecraft. flo-bit.dev/shapecraft/
0

Configure Feed

Select the types of activity you want to include in your feed.

cleanup 2

+10 -920
+5 -4
demo/editor/editor.ts
··· 31 31 for (const [key, def] of Object.entries(schema)) { 32 32 const d = def.default 33 33 // For range/integer with [min,max] default, show midpoint in editor 34 - if (Array.isArray(d) && d.length === 2 && typeof d[0] === 'number' && (def.type === 'range' || def.type === 'integer')) { 34 + if (Array.isArray(d) && d.length === 2 && typeof d[0] === 'number' && typeof d[1] === 'number' && (def.type === 'range' || def.type === 'integer')) { 35 35 values[key] = (d[0] + d[1]) / 2 36 36 } else { 37 37 values[key] = structuredClone(d) ··· 78 78 // Reset to schema defaults first (resolve [min,max] to midpoint for editor) 79 79 for (const [key, def] of Object.entries(schema)) { 80 80 const d = def.default 81 - if (Array.isArray(d) && d.length === 2 && typeof d[0] === 'number' && (def.type === 'range' || def.type === 'integer')) { 81 + if (Array.isArray(d) && d.length === 2 && typeof d[0] === 'number' && typeof d[1] === 'number' && (def.type === 'range' || def.type === 'integer')) { 82 82 values[key] = def.type === 'integer' ? Math.round((d[0] + d[1]) / 2) : (d[0] + d[1]) / 2 83 83 } else { 84 84 values[key] = structuredClone(d) ··· 216 216 setter = (v) => { input.value = v } 217 217 row.appendChild(input) 218 218 } else if (def.type === 'color-array') { 219 + const caDef = def // narrowed to ColorArrayOption; preserved into the closures below 219 220 const wrap = document.createElement('div') 220 221 wrap.style.cssText = 'display: flex; flex-wrap: wrap; gap: 4px;' 221 222 ··· 235 236 }) 236 237 swatch.addEventListener('contextmenu', (e) => { 237 238 e.preventDefault() 238 - if (arr.length > (def.min ?? 1)) { 239 + if (arr.length > (caDef.min ?? 1)) { 239 240 arr.splice(idx, 1) 240 241 rebuild() 241 242 onChange() ··· 244 245 wrap.appendChild(swatch) 245 246 } 246 247 247 - if (!def.max || arr.length < def.max) { 248 + if (!caDef.max || arr.length < caDef.max) { 248 249 const add = document.createElement('button') 249 250 add.textContent = '+' 250 251 add.style.cssText = 'width: 24px; height: 24px; border: 1px dashed #555; background: none; color: #888; cursor: pointer; font-size: 14px;'
+1 -1
demo/editor/types.ts
··· 1 - export type { OptionDef, OptionSchema, OptionValues, RangeOption, IntegerOption, ColorOption, ColorArrayOption, BooleanOption, SelectOption } from '../../src/schema' 1 + export type { OptionDef, OptionSchema, OptionValues, RangeOption, IntegerOption, ColorOption, ColorArrayOption, BooleanOption, SelectOption } from '../../src/core/schema'
-15
demo/forest.html
··· 1 - <!DOCTYPE html> 2 - <html lang="en"> 3 - <head> 4 - <meta charset="UTF-8"> 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 - <title>Shapecraft — Forest</title> 7 - <style> 8 - body { margin: 0; overflow: hidden; background: #1a1a2e; } 9 - canvas { display: block; } 10 - </style> 11 - </head> 12 - <body> 13 - <script type="module" src="./forest.ts"></script> 14 - </body> 15 - </html>
-151
demo/forest.ts
··· 1 - import * as THREE from 'three' 2 - import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 3 - import { toThreeMesh } from '../src/three' 4 - import { plane, createRng } from '../src' 5 - import { fbm } from '../src/noise' 6 - import { heightGradient } from '../src/color' 7 - import { tree, pine, palm } from '../src/generators' 8 - 9 - // --- Renderer --- 10 - const renderer = new THREE.WebGLRenderer({ antialias: true }) 11 - renderer.setSize(window.innerWidth, window.innerHeight) 12 - renderer.setPixelRatio(window.devicePixelRatio) 13 - renderer.shadowMap.enabled = true 14 - renderer.shadowMap.type = THREE.PCFSoftShadowMap 15 - renderer.toneMapping = THREE.ACESFilmicToneMapping 16 - renderer.toneMappingExposure = 1.1 17 - document.body.appendChild(renderer.domElement) 18 - 19 - // --- Scene --- 20 - const scene = new THREE.Scene() 21 - 22 - // --- Sky --- 23 - const skyGeo = new THREE.SphereGeometry(80, 16, 12) 24 - const skyColors = new Float32Array(skyGeo.getAttribute('position').count * 3) 25 - const skyPos = skyGeo.getAttribute('position') 26 - for (let i = 0; i < skyPos.count; i++) { 27 - const t = Math.max(0, skyPos.getY(i) / 80) 28 - skyColors[i * 3] = 0.55 + (0.2 - 0.55) * t 29 - skyColors[i * 3 + 1] = 0.7 + (0.35 - 0.7) * t 30 - skyColors[i * 3 + 2] = 0.85 + (0.65 - 0.85) * t 31 - } 32 - skyGeo.setAttribute('color', new THREE.BufferAttribute(skyColors, 3)) 33 - scene.add(new THREE.Mesh(skyGeo, new THREE.MeshBasicMaterial({ vertexColors: true, side: THREE.BackSide }))) 34 - 35 - // --- Fog --- 36 - scene.fog = new THREE.FogExp2(0x8eb3d9, 0.012) 37 - 38 - // --- Camera --- 39 - const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 200) 40 - camera.position.set(12, 8, 20) 41 - 42 - const controls = new OrbitControls(camera, renderer.domElement) 43 - controls.target.set(0, 1, 0) 44 - controls.enableDamping = true 45 - controls.dampingFactor = 0.08 46 - controls.maxPolarAngle = Math.PI / 2 - 0.05 47 - controls.update() 48 - 49 - // --- Lighting --- 50 - scene.add(new THREE.HemisphereLight(0x87ceeb, 0x3a5a2a, 0.6)) 51 - 52 - const sun = new THREE.DirectionalLight(0xfff4e0, 1.4) 53 - sun.position.set(10, 15, 8) 54 - sun.castShadow = true 55 - sun.shadow.mapSize.set(4096, 4096) 56 - sun.shadow.camera.left = -25 57 - sun.shadow.camera.right = 25 58 - sun.shadow.camera.top = 25 59 - sun.shadow.camera.bottom = -25 60 - sun.shadow.camera.near = 0.01 61 - sun.shadow.camera.far = 60 62 - sun.shadow.bias = -0.001 63 - scene.add(sun) 64 - 65 - scene.add(new THREE.DirectionalLight(0xb0c4de, 0.3).translateX(-5).translateY(4).translateZ(-3)) 66 - 67 - // --- Terrain --- 68 - const terrainNoise = fbm({ seed: 7, octaves: 4, scale: 0.08, min: 0, max: 0.8 }) 69 - const terrainMesh = plane({ size: 50, segments: 80 }) 70 - .displace((pos) => terrainNoise.get(pos[0], pos[2])) 71 - .vertexColor(heightGradient([ 72 - [-0.05, [0.22, 0.35, 0.12]], 73 - [0.15, [0.28, 0.42, 0.15]], 74 - [0.4, [0.32, 0.40, 0.18]], 75 - [0.7, [0.38, 0.36, 0.2]], 76 - ])) 77 - const terrainObj = toThreeMesh(terrainMesh) 78 - terrainObj.receiveShadow = true 79 - scene.add(terrainObj) 80 - 81 - // --- Scatter trees --- 82 - const rand = createRng(123) 83 - 84 - function addTree(mesh: ReturnType<typeof tree>, x: number, z: number) { 85 - const obj = toThreeMesh(mesh) 86 - const groundY = terrainNoise.get(x, z) 87 - obj.position.set(x, groundY, z) 88 - obj.castShadow = true 89 - obj.receiveShadow = true 90 - scene.add(obj) 91 - } 92 - 93 - // Common trees — center area 94 - for (let i = 0; i < 12; i++) { 95 - const x = (rand() - 0.5) * 20 96 - const z = (rand() - 0.5) * 20 - 5 97 - if (Math.abs(x) < 2 && Math.abs(z + 5) < 2) continue 98 - addTree( 99 - tree({ 100 - seed: i + 1, 101 - height: [2, 3.5] as any, 102 - canopyRadius: [0.6, 1.1] as any, 103 - }), 104 - x, z, 105 - ) 106 - } 107 - 108 - // Pine trees — back/left cluster 109 - for (let i = 0; i < 15; i++) { 110 - const x = -8 + (rand() - 0.5) * 18 111 - const z = -12 + (rand() - 0.5) * 14 112 - addTree( 113 - pine({ 114 - seed: i + 50, 115 - height: [2.5, 5] as any, 116 - coneTilt: [0.05, 0.3] as any, 117 - swayAmount: [0.1, 0.4] as any, 118 - }), 119 - x, z, 120 - ) 121 - } 122 - 123 - // Palm trees — right/front area 124 - for (let i = 0; i < 8; i++) { 125 - const x = 6 + (rand() - 0.5) * 16 126 - const z = 4 + (rand() - 0.5) * 14 127 - addTree( 128 - palm({ 129 - seed: i + 100, 130 - height: [2.5, 5] as any, 131 - trunkCurve: [0.2, 0.8] as any, 132 - frondDroop: [0.8, 1.3] as any, 133 - }), 134 - x, z, 135 - ) 136 - } 137 - 138 - // --- Resize --- 139 - window.addEventListener('resize', () => { 140 - camera.aspect = window.innerWidth / window.innerHeight 141 - camera.updateProjectionMatrix() 142 - renderer.setSize(window.innerWidth, window.innerHeight) 143 - }) 144 - 145 - // --- Animate --- 146 - function animate() { 147 - requestAnimationFrame(animate) 148 - controls.update() 149 - renderer.render(scene, camera) 150 - } 151 - animate()
+1 -2
demo/main.ts
··· 139 139 140 140 // --- Editor with generator switcher --- 141 141 let currentEditorEl: HTMLElement | null = null 142 - let lastOpts: Record<string, any> = {} 143 142 144 143 function mountEditor(type: 'common' | 'pine' | 'palm' | 'bush') { 145 144 activeGenerator = type ··· 149 148 const presets = type === 'pine' ? pinePresets : type === 'palm' ? palmPresets : type === 'bush' ? bushPresets : treePresets 150 149 151 150 currentEditorEl = createEditor(schema, { 152 - onChange: (opts) => { lastOpts = opts; rebuildTrees(opts) }, 151 + onChange: (opts) => { rebuildTrees(opts) }, 153 152 }, { presets }) 154 153 155 154 // Add generator switcher at the top
-701
plan-v0.md
··· 1 - # Shapecraft — Implementation Plan 2 - 3 - ## Overview 4 - 5 - 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. 6 - 7 - **Key design principles:** 8 - - Generators are just functions that return meshes. No class hierarchies, no registration. 9 - - Composition = calling functions inside functions. 10 - - Immutable by default — every transform/modifier returns a new Mesh. 11 - - Three.js is the internal engine but the API should feel library-agnostic where possible. 12 - - The package name is "shapecraft" but avoid hardcoding it deeply — keep it easy to rename. 13 - 14 - --- 15 - 16 - ## Tech Stack 17 - 18 - - **Language:** TypeScript (strict) 19 - - **3D Engine:** Three.js (peer dependency — user provides it) 20 - - **Noise:** Existing UberNoise library (bundled, see `uber-noise.ts` and its deps `simplex-noise/` and `alea/`) 21 - - **Build:** Vite (library mode for the package, dev server for demos) 22 - - **Test:** Vitest 23 - - **Package:** Single flat package, subpath exports for tree-shaking (`shapecraft`, `shapecraft/noise`, `shapecraft/three`, etc.) 24 - 25 - --- 26 - 27 - ## Project Structure 28 - 29 - ``` 30 - shapecraft/ 31 - ├── package.json 32 - ├── tsconfig.json 33 - ├── vite.config.ts 34 - ├── vitest.config.ts 35 - ├── index.ts # Main barrel export 36 - ├── src/ 37 - │ ├── mesh.ts # Core Mesh class 38 - │ ├── types.ts # Shared types (Vec3, Color, etc.) 39 - │ ├── math.ts # Vec3/Mat4 helpers (thin wrappers over THREE) 40 - │ ├── primitives/ 41 - │ │ ├── index.ts 42 - │ │ ├── box.ts 43 - │ │ ├── sphere.ts 44 - │ │ ├── cylinder.ts 45 - │ │ ├── plane.ts 46 - │ │ ├── torus.ts 47 - │ │ └── cone.ts 48 - │ ├── ops/ 49 - │ │ ├── index.ts 50 - │ │ ├── merge.ts # Combine multiple meshes into one 51 - │ │ ├── clone.ts # Deep clone a mesh 52 - │ │ └── center.ts # Recenter mesh to origin 53 - │ ├── modifiers/ 54 - │ │ ├── index.ts 55 - │ │ ├── twist.ts 56 - │ │ ├── bend.ts 57 - │ │ ├── taper.ts 58 - │ │ ├── lattice.ts # Simple lattice deform 59 - │ │ └── smooth.ts # Laplacian smooth 60 - │ ├── noise/ 61 - │ │ ├── index.ts # Re-exports UberNoise + helpers 62 - │ │ ├── uber-noise.ts # Existing UberNoise (verbatim, with imports fixed) 63 - │ │ ├── simplex-noise/ # Existing simplex-noise dependency 64 - │ │ │ └── simplex-noise.ts 65 - │ │ ├── alea/ # Existing alea dependency 66 - │ │ │ └── alea.ts 67 - │ │ └── helpers.ts # Convenience: fbm(), ridged(), etc. that return UberNoise instances 68 - │ ├── color/ 69 - │ │ ├── index.ts 70 - │ │ ├── vertex-color.ts # Apply vertex colors (flat or procedural fn) 71 - │ │ ├── face-color.ts # Apply per-face colors 72 - │ │ ├── gradient.ts # Height/angle gradient helpers 73 - │ │ └── utils.ts # Color parsing, lerp, hex<->rgb 74 - │ ├── uv/ 75 - │ │ ├── index.ts 76 - │ │ ├── projections.ts # Box, planar, cylindrical, spherical UV projection 77 - │ │ └── textures.ts # Procedural texture generators (checkerboard, wood, etc.) — STUB for v0 78 - │ ├── three/ 79 - │ │ ├── index.ts 80 - │ │ ├── to-three.ts # Mesh → THREE.Mesh / THREE.BufferGeometry 81 - │ │ └── from-three.ts # THREE.BufferGeometry → Mesh 82 - │ └── worker/ 83 - │ ├── index.ts 84 - │ └── pool.ts # STUB for v0 — types + placeholder 85 - ├── demo/ 86 - │ ├── index.html 87 - │ ├── main.ts # Vite dev entry — renders demo scene 88 - │ └── generators/ 89 - │ ├── chair.ts # Example generator: a simple chair 90 - │ ├── table.ts # Example generator: table with chairs 91 - │ └── terrain.ts # Example generator: noise terrain 92 - └── tests/ 93 - ├── mesh.test.ts 94 - ├── primitives.test.ts 95 - ├── ops.test.ts 96 - ├── modifiers.test.ts 97 - └── noise.test.ts 98 - ``` 99 - 100 - --- 101 - 102 - ## Phase 1: Foundation 103 - 104 - ### 1.1 — Project setup 105 - 106 - - Init `package.json` with: 107 - - `name: "shapecraft"` 108 - - `type: "module"` 109 - - `peerDependencies: { "three": ">=0.150.0" }` 110 - - `devDependencies: { "three": "^0.170.0", "@types/three": "...", "typescript": "...", "vite": "...", "vitest": "..." }` 111 - - `exports` field with subpath exports: 112 - ```json 113 - { 114 - ".": "./src/index.ts", 115 - "./noise": "./src/noise/index.ts", 116 - "./three": "./src/three/index.ts", 117 - "./modifiers": "./src/modifiers/index.ts", 118 - "./ops": "./src/ops/index.ts", 119 - "./color": "./src/color/index.ts", 120 - "./uv": "./src/uv/index.ts" 121 - } 122 - ``` 123 - - Scripts: `"dev": "vite demo"`, `"build": "vite build"`, `"test": "vitest"` 124 - - `tsconfig.json`: strict, ESNext, paths alias `@/*` → `./src/*` 125 - - `vite.config.ts`: library mode config (entry `src/index.ts`, external `three`) 126 - - `vitest.config.ts`: basic setup 127 - 128 - ### 1.2 — Types (`src/types.ts`) 129 - 130 - ```ts 131 - export type Vec2 = [number, number] 132 - export type Vec3 = [number, number, number] 133 - export type Vec4 = [number, number, number, number] 134 - export type ColorInput = string | Vec3 | Vec4 | number // hex string, rgb tuple, rgba tuple, or 0xRRGGBB 135 - export type ColorFn = (position: Vec3, normal: Vec3, index: number) => ColorInput 136 - export type DisplaceFn = (position: Vec3, normal: Vec3, uv: Vec2 | null, index: number) => number 137 - export type WarpFn = (position: Vec3, index: number) => Vec3 138 - export type NoiseLike = { get(x: number, y?: number, z?: number): number } 139 - ``` 140 - 141 - ### 1.3 — Core Mesh class (`src/mesh.ts`) 142 - 143 - This is the most important file. The Mesh wraps a `THREE.BufferGeometry` internally. 144 - 145 - ```ts 146 - import * as THREE from 'three' 147 - 148 - class Mesh { 149 - /** Internal geometry — users CAN access this but the API doesn't require it */ 150 - readonly geometry: THREE.BufferGeometry 151 - 152 - constructor(geometry: THREE.BufferGeometry) 153 - 154 - // --- Accessors (read from geometry attributes) --- 155 - get positions(): Float32Array 156 - get indices(): Uint32Array | Uint16Array | null 157 - get normals(): Float32Array | null 158 - get uvs(): Float32Array | null 159 - get colors(): Float32Array | null 160 - get vertexCount(): number 161 - get faceCount(): number 162 - get boundingBox(): THREE.Box3 163 - 164 - // --- Transforms (all return new Mesh, geometry is cloned) --- 165 - translate(x: number, y: number, z: number): Mesh 166 - rotate(axis: Vec3 | 'x' | 'y' | 'z', angle: number): Mesh 167 - rotateX(angle: number): Mesh 168 - rotateY(angle: number): Mesh 169 - rotateZ(angle: number): Mesh 170 - scale(x: number, y?: number, z?: number): Mesh 171 - transform(matrix: THREE.Matrix4): Mesh 172 - 173 - // --- Modifiers (return new Mesh) --- 174 - displace(fn: DisplaceFn): Mesh 175 - displaceNoise(noise: NoiseLike, amplitude?: number): Mesh 176 - warp(fn: WarpFn): Mesh 177 - subdivide(iterations?: number): Mesh // uses THREE's subdivision if available, else loop subdivision 178 - computeNormals(): Mesh 179 - 180 - // --- Coloring (return new Mesh) --- 181 - vertexColor(color: ColorInput | ColorFn): Mesh 182 - faceColor(fn: (centroid: Vec3, normal: Vec3, faceIndex: number) => ColorInput): Mesh 183 - 184 - // --- UV (return new Mesh) --- 185 - computeUVs(projection?: 'box' | 'planar' | 'cylindrical' | 'spherical'): Mesh 186 - 187 - // --- Utility --- 188 - center(): Mesh 189 - clone(): Mesh 190 - 191 - // --- Serialization (for future worker support) --- 192 - serialize(): ArrayBuffer 193 - static deserialize(buffer: ArrayBuffer): Mesh 194 - } 195 - ``` 196 - 197 - **Implementation notes:** 198 - - Every method that returns a new Mesh should: clone the geometry, apply the operation to the clone, return `new Mesh(clone)`. 199 - - Use a private helper `cloneGeometry()` that does `geometry.clone()` and ensures all attributes are properly copied. 200 - - `displace(fn)`: iterate vertices, call fn with position + normal + uv, move vertex along its normal by the returned amount. Recompute normals after. 201 - - `warp(fn)`: iterate vertices, replace position with fn result. Recompute normals after. 202 - - `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). 203 - - `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. 204 - - `serialize()`/`deserialize()`: pack all typed arrays into a single ArrayBuffer with a simple header (attribute count, sizes). This is for future worker support. 205 - 206 - ### 1.4 — Math utilities (`src/math.ts`) 207 - 208 - Thin wrappers — don't reimplement, use THREE internally: 209 - 210 - ```ts 211 - import * as THREE from 'three' 212 - 213 - export function vec3(x: number, y: number, z: number): THREE.Vector3 214 - export function mat4(): THREE.Matrix4 215 - export function makeTranslation(x: number, y: number, z: number): THREE.Matrix4 216 - export function makeRotation(axis: Vec3 | 'x' | 'y' | 'z', angle: number): THREE.Matrix4 217 - export function makeScale(x: number, y: number, z: number): THREE.Matrix4 218 - export function parseColor(input: ColorInput): THREE.Color 219 - ``` 220 - 221 - --- 222 - 223 - ## Phase 2: Primitives 224 - 225 - All primitives are **functions** that return a `Mesh`. They use Three.js geometry constructors internally. 226 - 227 - ### `src/primitives/box.ts` 228 - ```ts 229 - export interface BoxOptions { 230 - width?: number // default 1 231 - height?: number // default 1 232 - depth?: number // default 1 233 - // OR shorthand: 234 - size?: Vec3 | number // overrides width/height/depth 235 - widthSegments?: number 236 - heightSegments?: number 237 - depthSegments?: number 238 - } 239 - export function box(options?: BoxOptions): Mesh 240 - ``` 241 - Internally: `new THREE.BoxGeometry(...)` → wrap in Mesh. 242 - 243 - ### `src/primitives/sphere.ts` 244 - ```ts 245 - export interface SphereOptions { 246 - radius?: number // default 0.5 247 - widthSegments?: number // default 16 248 - heightSegments?: number // default 12 249 - } 250 - export function sphere(options?: SphereOptions): Mesh 251 - ``` 252 - 253 - ### `src/primitives/cylinder.ts` 254 - ```ts 255 - export interface CylinderOptions { 256 - radius?: number // default 0.5 (sets both top and bottom) 257 - radiusTop?: number // overrides radius for top 258 - radiusBottom?: number // overrides radius for bottom 259 - height?: number // default 1 260 - segments?: number // default 16 261 - } 262 - export function cylinder(options?: CylinderOptions): Mesh 263 - ``` 264 - 265 - ### `src/primitives/plane.ts` 266 - ```ts 267 - export interface PlaneOptions { 268 - width?: number // default 1 269 - height?: number // default 1 270 - // OR shorthand: 271 - size?: number | Vec2 // overrides width/height 272 - widthSegments?: number // default 1 273 - heightSegments?: number // default 1 274 - // OR shorthand: 275 - segments?: number | Vec2 // overrides both segment counts 276 - } 277 - export function plane(options?: PlaneOptions): Mesh 278 - ``` 279 - **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. 280 - 281 - ### `src/primitives/cone.ts` 282 - ```ts 283 - export interface ConeOptions { 284 - radius?: number 285 - height?: number 286 - segments?: number 287 - } 288 - export function cone(options?: ConeOptions): Mesh 289 - ``` 290 - 291 - ### `src/primitives/torus.ts` 292 - ```ts 293 - export interface TorusOptions { 294 - radius?: number 295 - tube?: number 296 - radialSegments?: number 297 - tubularSegments?: number 298 - } 299 - export function torus(options?: TorusOptions): Mesh 300 - ``` 301 - 302 - ### `src/primitives/index.ts` 303 - Re-export all primitives. 304 - 305 - --- 306 - 307 - ## Phase 3: Operations 308 - 309 - ### `src/ops/merge.ts` 310 - ```ts 311 - export function merge(...meshes: Mesh[]): Mesh 312 - ``` 313 - 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. 314 - 315 - **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. 316 - 317 - ### `src/ops/center.ts` 318 - ```ts 319 - export function center(mesh: Mesh): Mesh 320 - ``` 321 - Compute bounding box center, translate by negative center. 322 - 323 - ### `src/ops/clone.ts` 324 - ```ts 325 - export function clone(mesh: Mesh): Mesh // just calls mesh.clone() 326 - ``` 327 - 328 - --- 329 - 330 - ## Phase 4: Modifiers 331 - 332 - Modifiers are standalone functions that return `WarpFn` or can be applied directly. Two patterns: 333 - 334 - **Pattern A — warp functions** (for use with `mesh.warp(fn)`): 335 - ```ts 336 - // Returns a WarpFn: (position: Vec3) => Vec3 337 - export function twist(options: { axis?: 'x' | 'y' | 'z', amount: number }): WarpFn 338 - export function bend(options: { axis?: 'x' | 'y' | 'z', amount: number }): WarpFn 339 - export function taper(options: { axis?: 'x' | 'y' | 'z', curve?: (t: number) => number }): WarpFn 340 - ``` 341 - 342 - **Pattern B — mesh-in mesh-out** (for complex operations): 343 - ```ts 344 - export function smooth(mesh: Mesh, iterations?: number): Mesh 345 - export function subdivide(mesh: Mesh, iterations?: number): Mesh 346 - ``` 347 - 348 - ### `src/modifiers/twist.ts` 349 - Rotate vertices around an axis proportional to their position along that axis. 350 - ``` 351 - angle = position[axis] * amount 352 - rotate position around axis by angle 353 - ``` 354 - 355 - ### `src/modifiers/bend.ts` 356 - Curve vertices along an axis. Map position along axis to an arc. 357 - 358 - ### `src/modifiers/taper.ts` 359 - Scale vertices perpendicular to an axis based on a curve function of their position along the axis. 360 - ``` 361 - t = normalize position along axis to [0, 1] 362 - scale = curve(t) 363 - position[perpendicular axes] *= scale 364 - ``` 365 - 366 - ### `src/modifiers/smooth.ts` 367 - Laplacian smoothing: for each vertex, move it toward the average of its neighbors. Requires building an adjacency map from the index buffer. 368 - 369 - ### `src/modifiers/lattice.ts` 370 - STUB for v0. Just export the type and a placeholder that returns the input unchanged. 371 - 372 - --- 373 - 374 - ## Phase 5: Noise Integration 375 - 376 - ### Copy existing code 377 - Copy the user's existing noise implementation into `src/noise/`: 378 - - `src/noise/uber-noise.ts` — the UberNoise class (adjust imports to use relative paths within the package) 379 - - `src/noise/simplex-noise/simplex-noise.ts` — existing simplex noise 380 - - `src/noise/alea/alea.ts` — existing alea PRNG 381 - 382 - **Important:** Remove the `globalThis` assignments at the bottom of `uber-noise.ts`. We don't want to pollute globals — everything is imported. 383 - 384 - ### `src/noise/helpers.ts` 385 - Convenience factory functions that create pre-configured UberNoise instances: 386 - 387 - ```ts 388 - import { UberNoise, type NoiseOptions } from './uber-noise' 389 - 390 - /** Basic simplex noise */ 391 - export function simplex(options?: Partial<NoiseOptions>): UberNoise 392 - 393 - /** FBM (fractional Brownian motion) with sensible defaults */ 394 - export function fbm(options?: Partial<NoiseOptions> & { octaves?: number }): UberNoise 395 - // Default: octaves 4, lacunarity 2, gain 0.5 396 - 397 - /** Ridged noise */ 398 - export function ridged(options?: Partial<NoiseOptions>): UberNoise 399 - // Sets sharpness: -1 400 - 401 - /** Billowed noise */ 402 - export function billowed(options?: Partial<NoiseOptions>): UberNoise 403 - // Sets sharpness: 1 404 - 405 - /** Stepped/terraced noise */ 406 - export function stepped(steps: number, options?: Partial<NoiseOptions>): UberNoise 407 - // Sets steps 408 - 409 - /** Warped noise */ 410 - export function warped(amount: number, options?: Partial<NoiseOptions>): UberNoise 411 - // Sets warp amount 412 - ``` 413 - 414 - ### `src/noise/index.ts` 415 - ```ts 416 - export { UberNoise, type NoiseOptions } from './uber-noise' 417 - export { simplex, fbm, ridged, billowed, stepped, warped } from './helpers' 418 - ``` 419 - 420 - ### Integration with Mesh 421 - 422 - `mesh.displaceNoise(noise, amplitude)` should accept an `UberNoise` instance (or anything with a `.get(x, y, z)` method): 423 - 424 - ```ts 425 - displaceNoise(noise: NoiseLike, amplitude: number = 1): Mesh { 426 - return this.displace((pos, normal) => { 427 - return noise.get(pos[0], pos[1], pos[2]) * amplitude 428 - }) 429 - } 430 - ``` 431 - 432 - This keeps the coupling loose — any object with `get(x, y?, z?)` works. 433 - 434 - --- 435 - 436 - ## Phase 6: Color & UV 437 - 438 - ### `src/color/utils.ts` 439 - ```ts 440 - export function parseColor(input: ColorInput): [number, number, number] // rgb 0-1 441 - export function lerpColor(a: ColorInput, b: ColorInput, t: number): [number, number, number] 442 - export function hexToRgb(hex: string): [number, number, number] 443 - export function rgbToHex(r: number, g: number, b: number): string 444 - ``` 445 - Use `THREE.Color` internally for parsing. 446 - 447 - ### `src/color/gradient.ts` 448 - ```ts 449 - export type GradientStop = [number, ColorInput] // [threshold, color] 450 - 451 - /** Create a function that maps a value to a color based on gradient stops */ 452 - export function gradient(stops: GradientStop[]): (value: number) => [number, number, number] 453 - 454 - /** Shorthand for height-based gradient (maps y position to color) */ 455 - export function heightGradient(stops: GradientStop[]): ColorFn 456 - ``` 457 - 458 - ### `src/color/vertex-color.ts` 459 - Implementation of `Mesh.vertexColor()`: 460 - - If given a flat color, set all vertex colors to that color. 461 - - If given a function, iterate all vertices, call the function with (position, normal, vertexIndex), set the color attribute. 462 - 463 - ### `src/color/face-color.ts` 464 - Implementation of `Mesh.faceColor()`: 465 - - Call `geometry.toNonIndexed()` first (un-share vertices). 466 - - Iterate face by face (every 3 vertices), compute centroid and normal, call fn, set all 3 vertex colors. 467 - 468 - ### `src/uv/projections.ts` 469 - ```ts 470 - export function projectUVs(geometry: THREE.BufferGeometry, mode: 'box' | 'planar' | 'cylindrical' | 'spherical'): void 471 - ``` 472 - Modifies geometry in place (called on a clone inside `Mesh.computeUVs()`). 473 - 474 - - **planar**: project from Y axis down onto XZ plane. `u = x`, `v = z` (normalized to bounding box). 475 - - **box**: tri-planar — pick projection axis per face based on face normal, project from that axis. 476 - - **cylindrical**: `u = atan2(z, x) / (2π)`, `v = y` (normalized). 477 - - **spherical**: `u = atan2(z, x) / (2π)`, `v = acos(y/r) / π`. 478 - 479 - ### `src/uv/textures.ts` 480 - STUB for v0. Export types and a couple basic functions: 481 - ```ts 482 - export function checkerboard(size?: number): (u: number, v: number) => [number, number, number] 483 - ``` 484 - Full procedural texture system is post-v0. 485 - 486 - --- 487 - 488 - ## Phase 7: Three.js Bridge 489 - 490 - ### `src/three/to-three.ts` 491 - ```ts 492 - import * as THREE from 'three' 493 - import type { Mesh } from '../mesh' 494 - 495 - /** Convert a shapecraft Mesh to a THREE.Mesh ready to add to a scene */ 496 - export function toThreeMesh(mesh: Mesh, options?: { 497 - material?: THREE.Material 498 - flatShading?: boolean // default true for low-poly look 499 - wireframe?: boolean 500 - }): THREE.Mesh 501 - 502 - /** Get just the geometry (if user wants to provide their own material) */ 503 - export function toThreeGeometry(mesh: Mesh): THREE.BufferGeometry 504 - ``` 505 - 506 - `toThreeMesh` implementation: 507 - - If mesh has vertex colors, use `THREE.MeshStandardMaterial({ vertexColors: true, flatShading })`. 508 - - If no vertex colors, use `THREE.MeshStandardMaterial({ color: 0xcccccc, flatShading })`. 509 - - If wireframe requested, set `material.wireframe = true`. 510 - - The geometry is already a `THREE.BufferGeometry` internally, so just reference it (or clone if immutability is desired). 511 - 512 - ### `src/three/from-three.ts` 513 - ```ts 514 - /** Import an existing Three.js geometry into shapecraft */ 515 - export function fromThreeGeometry(geometry: THREE.BufferGeometry): Mesh 516 - ``` 517 - 518 - --- 519 - 520 - ## Phase 8: Worker Support (STUB) 521 - 522 - ### `src/worker/pool.ts` 523 - For v0, just define the interface and a simple synchronous fallback: 524 - 525 - ```ts 526 - export interface WorkerPoolOptions { 527 - maxWorkers?: number 528 - } 529 - 530 - export class WorkerPool { 531 - constructor(options?: WorkerPoolOptions) 532 - 533 - /** Run a generator function in a worker. For v0, runs synchronously. */ 534 - async run<T>(fn: (...args: any[]) => Mesh, ...args: any[]): Promise<Mesh> 535 - 536 - /** Batch run multiple generators. For v0, runs sequentially. */ 537 - async batch(tasks: Array<[Function, ...any[]]>): Promise<Mesh[]> 538 - 539 - dispose(): void 540 - } 541 - ``` 542 - 543 - The v0 implementation just calls the functions directly. Real worker support comes later and will use `Mesh.serialize()`/`deserialize()` with `Transferable`. 544 - 545 - --- 546 - 547 - ## Phase 9: Demo 548 - 549 - ### `demo/index.html` 550 - Basic HTML shell that loads `demo/main.ts` via Vite. 551 - 552 - ### `demo/main.ts` 553 - Sets up a Three.js scene with: 554 - - Renderer, camera, controls (OrbitControls) 555 - - Ambient light + directional light 556 - - Calls all three demo generators 557 - - Adds them to the scene 558 - - Animation loop 559 - 560 - ### `demo/generators/chair.ts` 561 - ```ts 562 - export function chair(options?: { seatHeight?: number, legRadius?: number }): Mesh 563 - ``` 564 - A simple 4-legged chair: 565 - - Box for seat 566 - - 4 cylinders for legs 567 - - Box for back 568 - - Vertex colored brown tones 569 - - Returns merged mesh 570 - 571 - ### `demo/generators/table.ts` 572 - ```ts 573 - export function diningSet(options?: { chairs?: number, tableRadius?: number }): Mesh 574 - ``` 575 - - Cylinder for table top 576 - - 4 cylinder legs 577 - - N chairs arranged in a circle using the `chair()` generator 578 - - Demonstrates composition 579 - 580 - ### `demo/generators/terrain.ts` 581 - ```ts 582 - export function terrain(options?: { size?: number, segments?: number, seed?: number }): Mesh 583 - ``` 584 - - Large subdivided plane 585 - - Displaced with UberNoise (fbm, maybe ridged mix) 586 - - Height-based vertex coloring (green → brown → gray → white) 587 - - Demonstrates noise integration 588 - 589 - --- 590 - 591 - ## Phase 10: Tests 592 - 593 - ### `tests/mesh.test.ts` 594 - - Construction from geometry 595 - - Transforms: translate, rotate, scale produce correct vertex positions 596 - - Immutability: original mesh unchanged after transform 597 - - `vertexCount` and `faceCount` correct 598 - - `clone()` produces independent copy 599 - 600 - ### `tests/primitives.test.ts` 601 - - Each primitive returns a Mesh 602 - - Vertex counts are correct for given parameters 603 - - Default options produce reasonable geometry 604 - - `size` shorthand works for box and plane 605 - 606 - ### `tests/ops.test.ts` 607 - - `merge()` combines vertex counts correctly 608 - - `merge()` handles mixed color/no-color meshes (fills missing with white) 609 - - `center()` puts bounding box center at origin 610 - 611 - ### `tests/modifiers.test.ts` 612 - - `twist()` modifies positions (not identity) 613 - - `taper()` scales vertices correctly at extremes 614 - - `displace()` moves vertices along normals 615 - - `displaceNoise()` produces non-zero displacement 616 - 617 - ### `tests/noise.test.ts` 618 - - UberNoise produces values in expected range 619 - - `fbm()` helper returns configured UberNoise 620 - - `ridged()` produces values with expected characteristics 621 - - Seeded noise is deterministic 622 - 623 - --- 624 - 625 - ## Implementation Order for Claude Code 626 - 627 - Follow this order. Each step should be completable and testable before moving to the next. 628 - 629 - 1. **Project scaffolding** — `package.json`, `tsconfig.json`, `vite.config.ts`, `vitest.config.ts`, directory structure, install deps. 630 - 631 - 2. **Types + Math** — `src/types.ts`, `src/math.ts`. 632 - 633 - 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. 634 - 635 - 4. **Primitives** — All 6 primitives. Write `tests/primitives.test.ts`. At this point you can already do `box().translate(1,0,0)`. 636 - 637 - 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))`. 638 - 639 - 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`. 640 - 641 - 7. **Displace + warp on Mesh** — Implement `mesh.displace()`, `mesh.displaceNoise()`, `mesh.warp()`. These depend on noise being available for testing. 642 - 643 - 8. **Modifiers** — `twist`, `bend`, `taper`, `smooth` (lattice as stub). Write `tests/modifiers.test.ts`. 644 - 645 - 9. **Color system** — `src/color/*`. Implement `mesh.vertexColor()` and `mesh.faceColor()`, plus gradient helpers. 646 - 647 - 10. **UV projections** — `src/uv/projections.ts`, implement `mesh.computeUVs()`. Texture stubs. 648 - 649 - 11. **Three.js bridge** — `src/three/to-three.ts`, `src/three/from-three.ts`. 650 - 651 - 12. **Worker stubs** — `src/worker/pool.ts`. 652 - 653 - 13. **Main barrel exports** — `src/index.ts` re-exporting everything. 654 - 655 - 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. 656 - 657 - 15. **Final pass** — Run all tests, fix any issues, make sure `npm run dev` launches the demo and it looks good. 658 - 659 - --- 660 - 661 - ## API Surface Summary (what gets exported) 662 - 663 - ```ts 664 - // shapecraft (main) 665 - export { Mesh } from './mesh' 666 - export { box, sphere, cylinder, plane, cone, torus } from './primitives' 667 - export { merge, center, clone } from './ops' 668 - export { twist, bend, taper, smooth, subdivide } from './modifiers' 669 - export { vertexColor, faceColor, gradient, heightGradient, lerpColor } from './color' 670 - export { projectUVs } from './uv' 671 - 672 - // shapecraft/noise 673 - export { UberNoise, simplex, fbm, ridged, billowed, stepped, warped } from './noise' 674 - 675 - // shapecraft/three 676 - export { toThreeMesh, toThreeGeometry, fromThreeGeometry } from './three' 677 - ``` 678 - 679 - --- 680 - 681 - ## Notes & Gotchas for the Implementer 682 - 683 - 1. **Always clone geometry before modifying.** Every Mesh method that modifies geometry must clone first. Never mutate the internal geometry of an existing Mesh. 684 - 685 - 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). 686 - 687 - 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. 688 - 689 - 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. 690 - 691 - 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. 692 - 693 - 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. 694 - 695 - 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. 696 - 697 - 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. 698 - 699 - 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. 700 - 701 - 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.
+1 -1
src/color/palette.ts
··· 1 - import { interpolate, formatRgb, parse, converter } from 'culori' 1 + import { interpolate, parse, converter } from 'culori' 2 2 import type { ColorInput, ColorFn, Vec3 } from '../core/types' 3 3 import type { NoiseLike } from '../core/types' 4 4
-4
src/core/mesh.ts
··· 6 6 return geometry.clone() 7 7 } 8 8 9 - function fract(x: number): number { 10 - return x - Math.floor(x) 11 - } 12 - 13 9 export class Mesh { 14 10 readonly geometry: THREE.BufferGeometry 15 11
-1
src/generators/vegetation/trees/pine-tree.ts
··· 90 90 // Canopy — stacked pyramid cones 91 91 const canopyParts: Mesh[] = [] 92 92 const canopyStart = trunkHeight * 0.6 93 - const canopyHeight = o.height - canopyStart 94 93 const layerCount = o.layers 95 94 96 95 // Per-layer jitter seeds from the canopy stream
-1
src/modifiers/index.ts
··· 2 2 export { bend } from './bend' 3 3 export { taper } from './taper' 4 4 export { smooth } from './smooth' 5 - export { lattice } from './lattice'
-10
src/modifiers/lattice.ts
··· 1 - import { Mesh } from '../core/mesh' 2 - 3 - // STUB for v0 — lattice deformation placeholder 4 - export interface LatticeOptions { 5 - divisions?: [number, number, number] 6 - } 7 - 8 - export function lattice(_mesh: Mesh, _options?: LatticeOptions): Mesh { 9 - return _mesh.clone() 10 - }
-1
src/uv/index.ts
··· 1 1 export { projectUVs } from './projections' 2 - export { checkerboard } from './textures'
-8
src/uv/textures.ts
··· 1 - // STUB for v0 — procedural textures 2 - export function checkerboard(size: number = 8): (u: number, v: number) => [number, number, number] { 3 - return function checker(u: number, v: number): [number, number, number] { 4 - const x = Math.floor(u * size) 5 - const y = Math.floor(v * size) 6 - return (x + y) % 2 === 0 ? [1, 1, 1] : [0, 0, 0] 7 - } 8 - }
-1
src/worker/index.ts
··· 1 - export { WorkerPool, type WorkerPoolOptions } from './pool'
-19
src/worker/pool.ts
··· 1 - import { Mesh } from '../core/mesh' 2 - 3 - export interface WorkerPoolOptions { 4 - maxWorkers?: number 5 - } 6 - 7 - export class WorkerPool { 8 - constructor(_options?: WorkerPoolOptions) {} 9 - 10 - async run<T>(fn: (...args: any[]) => Mesh, ...args: any[]): Promise<Mesh> { 11 - return fn(...args) 12 - } 13 - 14 - async batch(tasks: Array<[(...args: any[]) => Mesh, ...any[]]>): Promise<Mesh[]> { 15 - return tasks.map(([fn, ...args]) => fn(...args)) 16 - } 17 - 18 - dispose(): void {} 19 - }
+2
tsconfig.json
··· 4 4 "module": "ESNext", 5 5 "moduleResolution": "bundler", 6 6 "strict": true, 7 + "noUnusedLocals": true, 8 + "noUnusedParameters": true, 7 9 "esModuleInterop": true, 8 10 "skipLibCheck": true, 9 11 "forceConsistentCasingInFileNames": true,