demo
src
tests
webapp
src
lib
components
vitest-examples
routes
···
17
17
rock, rockSchema, rockPresets,
18
18
sharpRock, sharpRockSchema, sharpRockPresets,
19
19
blockRock, blockRockSchema, blockRockPresets,
20
20
+
mushroom, mushroomSchema, mushroomPresets,
20
21
} from '../src/generators'
21
22
import { createEditor } from './editor/editor'
22
23
import type { OptionSchema } from '../src/core/schema'
···
44
45
rock: { label: 'Rock', gen: rock, schema: rockSchema, presets: rockPresets, sizeKey: 'size', defaultSize: 0.6 },
45
46
sharp: { label: 'Sharp', gen: sharpRock, schema: sharpRockSchema, presets: sharpRockPresets, sizeKey: 'size', defaultSize: 1.0 },
46
47
block: { label: 'Blocks', gen: blockRock, schema: blockRockSchema, presets: blockRockPresets, sizeKey: 'size', defaultSize: 0.7 },
48
48
+
mushroom: { label: 'Mushroom', gen: mushroom, schema: mushroomSchema, presets: mushroomPresets, sizeKey: 'height', defaultSize: 0.35 },
47
49
}
48
50
49
51
// --- Renderer ---
···
1
1
+
export { mushroom, mushroomSchema, mushroomPresets, type MushroomOptions } from './mushroom'
···
1
1
+
import { sphere } from '../../../primitives'
2
2
+
import { merge } from '../../../ops'
3
3
+
import { setup, trunk, foliageBlob, facetShade, scatterOnSurface } from '../../../build'
4
4
+
import { pickRandom } from '../../../color'
5
5
+
import { UberNoise } from '../../../noise'
6
6
+
import type { Mesh } from '../../../core/mesh'
7
7
+
import type { OptionSchema, OptionInput } from '../../../core/schema'
8
8
+
9
9
+
export const mushroomSchema = {
10
10
+
seed: { type: 'integer', default: 1, min: 1, max: 100, label: 'Seed' },
11
11
+
height: { type: 'range', default: 0.35, min: 0.1, max: 1.2, step: 0.05, label: 'Stem Height' },
12
12
+
stemRadius: { type: 'range', default: 0.045, min: 0.015, max: 0.15, step: 0.005, label: 'Stem Radius' },
13
13
+
lean: { type: 'range', default: 0.1, min: 0, max: 0.4, step: 0.02, label: 'Lean' },
14
14
+
capRadius: { type: 'range', default: 0.16, min: 0.05, max: 0.5, step: 0.01, label: 'Cap Radius' },
15
15
+
capSquash: { type: 'range', default: 0.6, min: 0.25, max: 1.2, step: 0.05, label: 'Cap Squash' },
16
16
+
capCurl: { type: 'range', default: 0.35, min: 0, max: 0.8, step: 0.05, label: 'Cap Curl' },
17
17
+
capNoise: { type: 'range', default: 0.12, min: 0, max: 0.5, step: 0.02, label: 'Cap Lumpiness' },
18
18
+
count: { type: 'integer', default: 1, min: 1, max: 9, label: 'Cluster' },
19
19
+
spread: { type: 'range', default: 1.0, min: 0.4, max: 2, step: 0.05, label: 'Spread' },
20
20
+
spots: { type: 'integer', default: 0, min: 0, max: 20, label: 'Spots' },
21
21
+
spotColor: { type: 'color', default: '#f2efe6', label: 'Spot Color' },
22
22
+
capColors: { type: 'color-array', default: ['#9c6b3f', '#a87844', '#8a5a32'], min: 1, max: 6, label: 'Cap Colors' },
23
23
+
gillColor: { type: 'color', default: '#e8dcc2', label: 'Gill Color' },
24
24
+
stemColors: { type: 'color-array', default: ['#ddd2bc', '#cfc4ae'], min: 1, max: 4, label: 'Stem Colors' },
25
25
+
} satisfies OptionSchema
26
26
+
27
27
+
export type MushroomOptions = Partial<OptionInput<typeof mushroomSchema>> & { preset?: string }
28
28
+
29
29
+
export const mushroomPresets: Record<string, Partial<MushroomOptions>> = {
30
30
+
default: {},
31
31
+
toadstool: {
32
32
+
capColors: ['#c43a2e', '#b33327', '#cf4334'],
33
33
+
spots: 9,
34
34
+
capSquash: 0.7,
35
35
+
stemColors: ['#efe9da', '#e2dccb'],
36
36
+
gillColor: '#efe7d2',
37
37
+
},
38
38
+
chanterelle: {
39
39
+
capColors: ['#e0a83c', '#d49a32', '#e8b54a'],
40
40
+
capSquash: 0.3,
41
41
+
capCurl: 0.1,
42
42
+
gillColor: '#dba939',
43
43
+
stemColors: ['#e3b34e', '#d6a843'],
44
44
+
},
45
45
+
cluster: {
46
46
+
count: 6,
47
47
+
height: 0.28,
48
48
+
capRadius: 0.1,
49
49
+
capSquash: 0.75,
50
50
+
capColors: ['#b08d57', '#a3814e', '#bd9a62'],
51
51
+
},
52
52
+
inkcap: {
53
53
+
height: 0.55,
54
54
+
stemRadius: 0.03,
55
55
+
capRadius: 0.11,
56
56
+
capSquash: 1.1,
57
57
+
capCurl: 0.65,
58
58
+
capColors: ['#8d8678', '#999285', '#7e776b'],
59
59
+
gillColor: '#55504a',
60
60
+
stemColors: ['#e6e2d8', '#d8d4ca'],
61
61
+
},
62
62
+
}
63
63
+
64
64
+
export function mushroom(options: MushroomOptions = {}): Mesh {
65
65
+
const { o, rng } = setup(mushroomSchema, options, mushroomPresets)
66
66
+
const shapeRng = rng.stream('shape')
67
67
+
const colorRng = rng.stream('color')
68
68
+
const spotRng = rng.stream('spot')
69
69
+
70
70
+
const colorNoise = new UberNoise({ seed: colorRng.seed(), scale: 4 })
71
71
+
const gill = pickRandom(o.gillColor, colorRng)
72
72
+
73
73
+
const parts: Mesh[] = []
74
74
+
let anchorR = 0
75
75
+
for (let i = 0; i < o.count; i++) {
76
76
+
// Tallest mushroom at the center, smaller ones huddled around it.
77
77
+
const rank = i / Math.max(1, o.count - 1)
78
78
+
const scale = (1 - 0.4 * rank) * shapeRng.float(0.85, 1.15)
79
79
+
const height = o.height * scale
80
80
+
const capR = o.capRadius * scale * shapeRng.float(0.9, 1.1)
81
81
+
const stemR = o.stemRadius * scale
82
82
+
83
83
+
if (i === 0) anchorR = capR
84
84
+
const placeAngle = ((i - 1) / Math.max(1, o.count - 1)) * Math.PI * 2 + shapeRng.float(-0.5, 0.5)
85
85
+
const dist = i === 0 ? 0 : (anchorR + capR) * o.spread * shapeRng.float(0.9, 1.1)
86
86
+
const px = Math.cos(placeAngle) * dist
87
87
+
const pz = Math.sin(placeAngle) * dist
88
88
+
89
89
+
const leanAngle = shapeRng.float(0, Math.PI * 2)
90
90
+
const leanX = Math.cos(leanAngle) * o.lean * height
91
91
+
const leanZ = Math.sin(leanAngle) * o.lean * height
92
92
+
93
93
+
parts.push(trunk({
94
94
+
height,
95
95
+
baseRadius: stemR * 1.25,
96
96
+
topRadius: stemR,
97
97
+
taper: 2,
98
98
+
lean: [leanX, leanZ],
99
99
+
noiseSeed: shapeRng.seed(),
100
100
+
noiseScale: 5,
101
101
+
noiseAmount: 0.12,
102
102
+
segments: 6,
103
103
+
heightSegments: 3,
104
104
+
colors: o.stemColors,
105
105
+
}).translate(px, 0, pz))
106
106
+
107
107
+
// Cap: a squashed blob with everything below the curl plane clamped flat —
108
108
+
// the dome reads as the cap, the flat clamp as the gilled underside.
109
109
+
const sy = capR * o.capSquash
110
110
+
const cutY = -sy * o.capCurl
111
111
+
const capBase = pickRandom(o.capColors, colorRng)
112
112
+
const capShade = facetShade({ base: capBase, noise: colorNoise, ambient: 0.55, range: 0.45, noiseAmount: 0.1 })
113
113
+
const gillShade = facetShade({ base: gill, ambient: 0.7, range: 0.3 })
114
114
+
115
115
+
const cap = foliageBlob({
116
116
+
radius: capR,
117
117
+
detail: capR * 0.45,
118
118
+
noiseSeed: shapeRng.seed(),
119
119
+
noiseScale: 1.2,
120
120
+
noiseOctaves: 2,
121
121
+
noiseAmount: o.capNoise,
122
122
+
jitter: capR * 0.02,
123
123
+
jitterSeed: shapeRng.seed(),
124
124
+
})
125
125
+
.scale(1, o.capSquash, 1)
126
126
+
.warp((p) => (p[1] < cutY ? [p[0], cutY, p[2]] : p))
127
127
+
.translate(px + leanX, height - cutY - capR * 0.06, pz + leanZ)
128
128
+
.faceColor((c, n) => (n[1] < -0.35 ? gillShade(c, n) : capShade(c, n)))
129
129
+
parts.push(cap)
130
130
+
131
131
+
// Optional fly-agaric spots: small flattened flecks sitting on the cap dome.
132
132
+
if (o.spots > 0) {
133
133
+
const points = scatterOnSurface(cap, o.spots, { rng: spotRng, minNormalY: 0.35 })
134
134
+
for (const { position, normal } of points) {
135
135
+
const s = capR * 0.14 * (0.7 + spotRng() * 0.6)
136
136
+
let spot = sphere({ radius: s, widthSegments: 5, heightSegments: 3 }).scale(1, 0.45, 1)
137
137
+
// Tilt the flattened fleck so it lies flush with the cap surface.
138
138
+
const tilt = Math.acos(Math.max(-1, Math.min(1, normal[1])))
139
139
+
if (tilt > 1e-3) spot = spot.rotate([normal[2], 0, -normal[0]], tilt)
140
140
+
parts.push(
141
141
+
spot
142
142
+
.translate(
143
143
+
position[0] + normal[0] * s * 0.15,
144
144
+
position[1] + normal[1] * s * 0.15,
145
145
+
position[2] + normal[2] * s * 0.15,
146
146
+
)
147
147
+
.vertexColor(o.spotColor),
148
148
+
)
149
149
+
}
150
150
+
}
151
151
+
}
152
152
+
153
153
+
return merge(...parts)
154
154
+
}
···
1
1
export * from './trees'
2
2
export * from './shrubs'
3
3
export * from './plants'
4
4
+
export * from './fungi'
···
1
1
-
import { merge, snow as applySnow } from '../../../ops'
1
1
+
import { merge, snow as applySnow, decimate } from '../../../ops'
2
2
import { setup, branches, foliageBlob, facetShade, heightShade, metaballs, type MetaBall } from '../../../build'
3
3
import { pickRandom } from '../../../color'
4
4
import { UberNoise } from '../../../noise'
···
17
17
taper: { type: 'range', default: 0.5, min: 0.3, max: 1.2, step: 0.05, label: 'Taper' },
18
18
leafSize: { type: 'range', default: 0.55, min: 0.2, max: 1.2, step: 0.05, label: 'Leaf Cluster Size' },
19
19
blobCanopy: { type: 'boolean', default: false, label: 'Merge Canopy' },
20
20
+
blobDetail: { type: 'range', default: 0.4, min: 0.1, max: 1, step: 0.05, label: 'Canopy Detail' },
20
21
canopyNoise: { type: 'range', default: 0.5, min: 0, max: 1.2, step: 0.05, label: 'Canopy Noise' },
21
22
jitter: { type: 'range', default: 0.05, min: 0, max: 0.15, step: 0.005, label: 'Jitter' },
22
23
snowColors: { type: 'color-array', default: [], min: 0, max: 6, label: 'Snow Colors' },
···
91
92
center: tip.position,
92
93
radius: o.leafSize * (0.7 + leafRng() * 0.6),
93
94
}))
95
95
+
let canopy = metaballs(balls, { resolution: 28, isolevel: 0.5, support: 2.0 })
96
96
+
if (o.blobDetail < 1) canopy = decimate(canopy, { ratio: o.blobDetail })
94
97
parts.push(
95
95
-
metaballs(balls, { resolution: 26, isolevel: 0.5, support: 2.0 })
98
98
+
canopy
96
99
.jitter(o.leafSize * o.jitter, { seed: leafRng.seed() })
97
100
.faceColor(shadeFor(pickRandom(o.leafColors, colorRng))),
98
101
)
···
6
6
export { box, sphere, cylinder, plane, cone, torus, icosphere } from './primitives'
7
7
8
8
// Operations
9
9
-
export { merge, center, clone, loft, tube, thicken, snow } from './ops'
10
10
-
export type { SnowOptions } from './ops'
9
9
+
export { merge, center, clone, loft, tube, thicken, snow, decimate } from './ops'
10
10
+
export type { SnowOptions, DecimateOptions } from './ops'
11
11
12
12
// Modifiers
13
13
export { twist, bend, taper } from './modifiers'
···
46
46
export { rock, rockSchema, rockPresets } from './generators'
47
47
export { sharpRock, sharpRockSchema, sharpRockPresets } from './generators'
48
48
export { blockRock, blockRockSchema, blockRockPresets } from './generators'
49
49
+
export { mushroom, mushroomSchema, mushroomPresets } from './generators'
···
2
2
type NoiseDerivFunction2D,
3
3
type NoiseDerivFunction3D,
4
4
type NoiseDerivFunction4D,
5
5
-
NoiseFunction2D,
6
6
-
NoiseFunction3D,
7
7
-
NoiseFunction4D,
5
5
+
type NoiseFunction2D,
6
6
+
type NoiseFunction3D,
7
7
+
type NoiseFunction4D,
8
8
createNoise2DWithDerivatives,
9
9
createNoise3DWithDerivatives,
10
10
createNoise4DWithDerivatives,
···
1
1
+
import * as THREE from 'three'
2
2
+
import { SimplifyModifier } from 'three/examples/jsm/modifiers/SimplifyModifier.js'
3
3
+
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
4
4
+
import { Mesh } from '../core/mesh'
5
5
+
6
6
+
export interface DecimateOptions {
7
7
+
/** Fraction of vertices to KEEP (0–1). Default 0.5. Ignored if `count` is given. */
8
8
+
ratio?: number
9
9
+
/** Explicit target vertex count. */
10
10
+
count?: number
11
11
+
}
12
12
+
13
13
+
/**
14
14
+
* Reduce a mesh's triangle count via quadric-error edge collapse (Three's
15
15
+
* SimplifyModifier), preserving the silhouette. Returns position+normal geometry only —
16
16
+
* baked vertex colors/UVs are dropped, so re-apply procedural coloring afterwards. Ideal
17
17
+
* for thinning dense isosurfaces (metaballs/surface nets) and for building LODs.
18
18
+
*/
19
19
+
export function decimate(mesh: Mesh, options: DecimateOptions = {}): Mesh {
20
20
+
const src = mesh.geometry
21
21
+
const srcPos = src.getAttribute('position')
22
22
+
if (!srcPos) return mesh.clone()
23
23
+
24
24
+
// Weld by position only — flat (per-face) normals would otherwise block welding and
25
25
+
// leave the simplifier with no shared edges to collapse.
26
26
+
const posGeo = new THREE.BufferGeometry()
27
27
+
posGeo.setAttribute('position', new THREE.BufferAttribute((srcPos.array as Float32Array).slice(), 3))
28
28
+
const index = src.getIndex()
29
29
+
if (index) posGeo.setIndex(new THREE.BufferAttribute((index.array as Uint32Array | Uint16Array).slice(), 1))
30
30
+
const welded = mergeVertices(posGeo)
31
31
+
32
32
+
const vcount = welded.getAttribute('position').count
33
33
+
const target = Math.max(4, Math.min(vcount, options.count ?? Math.floor(vcount * (options.ratio ?? 0.5))))
34
34
+
const toRemove = vcount - target
35
35
+
36
36
+
const out = toRemove > 0 ? new SimplifyModifier().modify(welded, toRemove) : welded
37
37
+
out.computeVertexNormals()
38
38
+
return new Mesh(out)
39
39
+
}
···
4
4
export { loft, tube } from './loft'
5
5
export { thicken } from './thicken'
6
6
export { snow, type SnowOptions } from './snow'
7
7
+
export { decimate, type DecimateOptions } from './decimate'
···
1
1
import { describe, it, expect } from 'vitest'
2
2
-
import { tree, pine, palm, bush, grass, fern, flower, deadTree, leafyTree, rock, sharpRock, blockRock } from '../src/generators'
2
2
+
import { tree, pine, palm, bush, grass, fern, flower, deadTree, leafyTree, rock, sharpRock, blockRock, mushroom } from '../src/generators'
3
3
import type { Mesh } from '../src/core/mesh'
4
4
5
5
const generators = [
···
15
15
{ name: 'rock', gen: rock },
16
16
{ name: 'sharp', gen: sharpRock },
17
17
{ name: 'block', gen: blockRock },
18
18
+
{ name: 'mushroom', gen: mushroom },
18
19
] as const
19
20
20
21
// Generators that support snow (trees, shrubs, rocks). Grass/ferns don't take snow options.
···
35
36
rock: { verts: 1440 },
36
37
sharp: { verts: 696 },
37
38
block: { verts: 432 },
39
39
+
mushroom: { verts: 1584 },
38
40
}
39
41
40
42
function allFinite(m: Mesh): boolean {
···
1
1
import { describe, it, expect } from 'vitest'
2
2
import * as THREE from 'three'
3
3
import { box, sphere } from '../src/primitives'
4
4
-
import { merge, center, clone } from '../src/ops'
4
4
+
import { merge, center, clone, decimate } from '../src/ops'
5
5
6
6
describe('ops', () => {
7
7
it('merge combines vertex counts', () => {
···
35
35
const c = clone(b)
36
36
expect(c.vertexCount).toBe(b.vertexCount)
37
37
expect(c.geometry).not.toBe(b.geometry)
38
38
+
})
39
39
+
40
40
+
it('decimate reduces face count while preserving the silhouette', () => {
41
41
+
const s = sphere({ radius: 1, widthSegments: 32, heightSegments: 24 })
42
42
+
const before = s.faceCount
43
43
+
const d = decimate(s, { ratio: 0.3 })
44
44
+
expect(d.faceCount).toBeLessThan(before)
45
45
+
expect(d.vertexCount).toBeGreaterThan(0)
46
46
+
// Bounding box (silhouette) stays close to the original sphere.
47
47
+
const bb = d.boundingBox
48
48
+
expect(bb.max.x).toBeGreaterThan(0.85)
49
49
+
expect(bb.max.x).toBeLessThan(1.15)
50
50
+
})
51
51
+
52
52
+
it('decimate hits an explicit target count', () => {
53
53
+
const s = sphere({ radius: 1, widthSegments: 24, heightSegments: 16 })
54
54
+
const d = decimate(s, { count: 80 })
55
55
+
expect(d.vertexCount).toBeLessThanOrEqual(120)
38
56
})
39
57
})
···
18
18
},
19
19
"devDependencies": {
20
20
"@eslint/js": "^10.0.1",
21
21
+
"@lucide/svelte": "^1.17.0",
21
22
"@playwright/test": "^1.60.0",
22
23
"@sveltejs/adapter-auto": "^7.0.1",
23
24
"@sveltejs/kit": "^2.63.0",
···
25
26
"@tailwindcss/forms": "^0.5.11",
26
27
"@tailwindcss/typography": "^0.5.19",
27
28
"@tailwindcss/vite": "^4.3.0",
29
29
+
"@threlte/core": "^8.5.16",
30
30
+
"@threlte/extras": "^9.21.0",
28
31
"@types/node": "^22",
32
32
+
"@types/three": "0.170.0",
29
33
"@vitest/browser-playwright": "^4.1.8",
30
34
"eslint": "^10.4.1",
31
35
"eslint-config-prettier": "^10.1.8",
···
43
47
"vite": "^8.0.16",
44
48
"vitest": "^4.1.8",
45
49
"vitest-browser-svelte": "^2.1.1"
50
50
+
},
51
51
+
"dependencies": {
52
52
+
"culori": "^4.0.2",
53
53
+
"three": "0.170.0"
46
54
}
47
55
}
···
7
7
importers:
8
8
9
9
.:
10
10
+
dependencies:
11
11
+
culori:
12
12
+
specifier: ^4.0.2
13
13
+
version: 4.0.2
14
14
+
three:
15
15
+
specifier: 0.170.0
16
16
+
version: 0.170.0
10
17
devDependencies:
11
18
'@eslint/js':
12
19
specifier: ^10.0.1
13
20
version: 10.0.1(eslint@10.4.1(jiti@2.7.0))
21
21
+
'@lucide/svelte':
22
22
+
specifier: ^1.17.0
23
23
+
version: 1.17.0(svelte@5.56.3(@typescript-eslint/types@8.61.0))
14
24
'@playwright/test':
15
25
specifier: ^1.60.0
16
26
version: 1.60.0
···
32
42
'@tailwindcss/vite':
33
43
specifier: ^4.3.0
34
44
version: 4.3.0(vite@8.0.16(@types/node@22.19.20)(jiti@2.7.0))
45
45
+
'@threlte/core':
46
46
+
specifier: ^8.5.16
47
47
+
version: 8.5.16(svelte@5.56.3(@typescript-eslint/types@8.61.0))(three@0.170.0)
48
48
+
'@threlte/extras':
49
49
+
specifier: ^9.21.0
50
50
+
version: 9.21.0(@types/three@0.170.0)(svelte@5.56.3(@typescript-eslint/types@8.61.0))(three@0.170.0)
35
51
'@types/node':
36
52
specifier: ^22
37
53
version: 22.19.20
54
54
+
'@types/three':
55
55
+
specifier: 0.170.0
56
56
+
version: 0.170.0
38
57
'@vitest/browser-playwright':
39
58
specifier: ^4.1.8
40
59
version: 4.1.8(playwright@1.60.0)(vite@8.0.16(@types/node@22.19.20)(jiti@2.7.0))(vitest@4.1.8)
···
175
194
176
195
'@jridgewell/trace-mapping@0.3.31':
177
196
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
197
197
+
198
198
+
'@lucide/svelte@1.17.0':
199
199
+
resolution: {integrity: sha512-q06YCFBN5CO8cd1ADmLCxWRVMVb7xxvHzqC0lvNoxGa+FLW6Cd1Y1AOxgbQk4Iwe68vkAMCRveNHint4WoaVKg==}
200
200
+
peerDependencies:
201
201
+
svelte: ^5
178
202
179
203
'@napi-rs/wasm-runtime@1.1.4':
180
204
resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
···
431
455
peerDependencies:
432
456
svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0
433
457
458
458
+
'@threejs-kit/instanced-sprite-mesh@2.5.1':
459
459
+
resolution: {integrity: sha512-pmt1ALRhbHhCJQTj2FuthH6PeLIeaM4hOuS2JO3kWSwlnvx/9xuUkjFR3JOi/myMqsH7pSsLIROSaBxDfttjeA==}
460
460
+
peerDependencies:
461
461
+
three: '>=0.170.0'
462
462
+
463
463
+
'@threlte/core@8.5.16':
464
464
+
resolution: {integrity: sha512-+aa9Zgz0/Qat82H0hTm1pTEtS2BIP1Nfu7pKaei+3+TvDqER0YnZ4hdgI9fYU9Z5r1lrzY3JxMyrdISp4B0kfw==}
465
465
+
peerDependencies:
466
466
+
svelte: '>=5'
467
467
+
three: '>=0.160'
468
468
+
469
469
+
'@threlte/extras@9.21.0':
470
470
+
resolution: {integrity: sha512-ZkBx4IH+7vcxVM09Rex9XJNMKKbQt4JZ5J/3TnBd/Fge8IzSgl0ydgn70riDf/8tDLMDID6VdZPPrlr5vCHHJA==}
471
471
+
peerDependencies:
472
472
+
svelte: '>=5'
473
473
+
three: '>=0.160'
474
474
+
475
475
+
'@tweenjs/tween.js@23.1.3':
476
476
+
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
477
477
+
434
478
'@tybys/wasm-util@0.10.2':
435
479
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
436
480
···
455
499
'@types/node@22.19.20':
456
500
resolution: {integrity: sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==}
457
501
502
502
+
'@types/stats.js@0.17.4':
503
503
+
resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==}
504
504
+
505
505
+
'@types/three@0.170.0':
506
506
+
resolution: {integrity: sha512-CUm2uckq+zkCY7ZbFpviRttY+6f9fvwm6YqSqPfA5K22s9w7R4VnA3rzJse8kHVvuzLcTx+CjNCs2NYe0QFAyg==}
507
507
+
458
508
'@types/trusted-types@2.0.7':
459
509
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
510
510
+
511
511
+
'@types/webxr@0.5.24':
512
512
+
resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==}
460
513
461
514
'@typescript-eslint/eslint-plugin@8.61.0':
462
515
resolution: {integrity: sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==}
···
557
610
'@vitest/utils@4.1.8':
558
611
resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==}
559
612
613
613
+
'@webgpu/types@0.1.70':
614
614
+
resolution: {integrity: sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA==}
615
615
+
560
616
acorn-jsx@5.3.2:
561
617
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
562
618
peerDependencies:
···
586
642
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
587
643
engines: {node: 18 || 20 || >=22}
588
644
645
645
+
bidi-js@1.0.3:
646
646
+
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
647
647
+
589
648
brace-expansion@5.0.6:
590
649
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
591
650
engines: {node: 18 || 20 || >=22}
592
651
652
652
+
camera-controls@3.1.2:
653
653
+
resolution: {integrity: sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==}
654
654
+
engines: {node: '>=22.0.0', npm: '>=10.5.1'}
655
655
+
peerDependencies:
656
656
+
three: '>=0.126.1'
657
657
+
593
658
chai@6.2.2:
594
659
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
595
660
engines: {node: '>=18'}
···
617
682
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
618
683
engines: {node: '>=4'}
619
684
hasBin: true
685
685
+
686
686
+
culori@4.0.2:
687
687
+
resolution: {integrity: sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==}
688
688
+
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
620
689
621
690
debug@4.4.3:
622
691
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
···
640
709
641
710
devalue@5.8.1:
642
711
resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==}
712
712
+
713
713
+
diet-sprite@0.0.1:
714
714
+
resolution: {integrity: sha512-zSHI2WDAn1wJqJYxcmjWfJv3Iw8oL9reQIbEyx2x2/EZ4/qmUTIo8/5qOCurnAcq61EwtJJaZ0XTy2NRYqpB5A==}
715
715
+
716
716
+
earcut@2.2.4:
717
717
+
resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==}
643
718
644
719
enhanced-resolve@5.23.0:
645
720
resolution: {integrity: sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==}
···
757
832
peerDependenciesMeta:
758
833
picomatch:
759
834
optional: true
835
835
+
836
836
+
fflate@0.8.3:
837
837
+
resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==}
760
838
761
839
file-entry-cache@8.0.0:
762
840
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
···
932
1010
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
933
1011
engines: {node: '>=10'}
934
1012
1013
1013
+
maath@0.10.8:
1014
1014
+
resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==}
1015
1015
+
peerDependencies:
1016
1016
+
'@types/three': '>=0.134.0'
1017
1017
+
three: '>=0.134.0'
1018
1018
+
935
1019
magic-string@0.30.21:
936
1020
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
1021
1021
+
1022
1022
+
meshoptimizer@0.18.1:
1023
1023
+
resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==}
937
1024
938
1025
mini-svg-data-uri@1.4.4:
939
1026
resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
···
1125
1212
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
1126
1213
engines: {node: '>= 14.18.0'}
1127
1214
1215
1215
+
require-from-string@2.0.2:
1216
1216
+
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
1217
1217
+
engines: {node: '>=0.10.0'}
1218
1218
+
1128
1219
rolldown@1.0.3:
1129
1220
resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==}
1130
1221
engines: {node: ^20.19.0 || >=22.12.0}
···
1195
1286
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
1196
1287
engines: {node: '>=6'}
1197
1288
1289
1289
+
three-instanced-uniforms-mesh@0.52.4:
1290
1290
+
resolution: {integrity: sha512-YwDBy05hfKZQtU+Rp0KyDf9yH4GxfhxMbVt9OYruxdgLfPwmDG5oAbGoW0DrKtKZSM3BfFcCiejiOHCjFBTeng==}
1291
1291
+
peerDependencies:
1292
1292
+
three: '>=0.125.0'
1293
1293
+
1294
1294
+
three-mesh-bvh@0.9.10:
1295
1295
+
resolution: {integrity: sha512-UOlTgPIeqUURcwaG8knxvBaruwZlC4X3/WSHEFO7rYvMVv/YNUrkfFEszvfj36pXV88dCHoHNnIp0PifkirnTQ==}
1296
1296
+
peerDependencies:
1297
1297
+
three: '>= 0.159.0'
1298
1298
+
1299
1299
+
three-perf@1.0.11:
1300
1300
+
resolution: {integrity: sha512-OgBpZjwL+csQKGKZjpkH/QHdbGFMxqngMbSEJeSnVNfXDYd6On7WHNv/GhUZH4YxIpNMwMahBWrNnsJvnbSJHQ==}
1301
1301
+
peerDependencies:
1302
1302
+
three: '>=0.170'
1303
1303
+
1304
1304
+
three-viewport-gizmo@2.2.0:
1305
1305
+
resolution: {integrity: sha512-Jo9Liur1rUmdKk75FZumLU/+hbF+RtJHi1qsKZpntjKlCYScK6tjbYoqvJ9M+IJphrlQJF5oReFW7Sambh0N4Q==}
1306
1306
+
peerDependencies:
1307
1307
+
three: '>=0.162.0 <1.0.0'
1308
1308
+
1309
1309
+
three@0.170.0:
1310
1310
+
resolution: {integrity: sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==}
1311
1311
+
1198
1312
tinybench@2.9.0:
1199
1313
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
1200
1314
···
1214
1328
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
1215
1329
engines: {node: '>=6'}
1216
1330
1331
1331
+
troika-three-text@0.52.4:
1332
1332
+
resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==}
1333
1333
+
peerDependencies:
1334
1334
+
three: '>=0.125.0'
1335
1335
+
1336
1336
+
troika-three-utils@0.52.4:
1337
1337
+
resolution: {integrity: sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==}
1338
1338
+
peerDependencies:
1339
1339
+
three: '>=0.125.0'
1340
1340
+
1341
1341
+
troika-worker-utils@0.52.0:
1342
1342
+
resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==}
1343
1343
+
1217
1344
ts-api-utils@2.5.0:
1218
1345
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
1219
1346
engines: {node: '>=18.12'}
···
1222
1349
1223
1350
tslib@2.8.1:
1224
1351
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
1352
1352
+
1353
1353
+
tweakpane@3.1.10:
1354
1354
+
resolution: {integrity: sha512-rqwnl/pUa7+inhI2E9ayGTqqP0EPOOn/wVvSWjZsRbZUItzNShny7pzwL3hVlaN4m9t/aZhsP0aFQ9U5VVR2VQ==}
1225
1355
1226
1356
type-check@0.4.0:
1227
1357
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
···
1346
1476
jsdom:
1347
1477
optional: true
1348
1478
1479
1479
+
webgl-sdf-generator@1.1.1:
1480
1480
+
resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==}
1481
1481
+
1349
1482
which@2.0.2:
1350
1483
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
1351
1484
engines: {node: '>= 8'}
···
1471
1604
dependencies:
1472
1605
'@jridgewell/resolve-uri': 3.1.2
1473
1606
'@jridgewell/sourcemap-codec': 1.5.5
1607
1607
+
1608
1608
+
'@lucide/svelte@1.17.0(svelte@5.56.3(@typescript-eslint/types@8.61.0))':
1609
1609
+
dependencies:
1610
1610
+
svelte: 5.56.3(@typescript-eslint/types@8.61.0)
1474
1611
1475
1612
'@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
1476
1613
dependencies:
···
1661
1798
dependencies:
1662
1799
svelte: 5.56.3(@typescript-eslint/types@8.61.0)
1663
1800
1801
1801
+
'@threejs-kit/instanced-sprite-mesh@2.5.1(@types/three@0.170.0)(three@0.170.0)':
1802
1802
+
dependencies:
1803
1803
+
diet-sprite: 0.0.1
1804
1804
+
earcut: 2.2.4
1805
1805
+
maath: 0.10.8(@types/three@0.170.0)(three@0.170.0)
1806
1806
+
three: 0.170.0
1807
1807
+
three-instanced-uniforms-mesh: 0.52.4(three@0.170.0)
1808
1808
+
troika-three-utils: 0.52.4(three@0.170.0)
1809
1809
+
transitivePeerDependencies:
1810
1810
+
- '@types/three'
1811
1811
+
1812
1812
+
'@threlte/core@8.5.16(svelte@5.56.3(@typescript-eslint/types@8.61.0))(three@0.170.0)':
1813
1813
+
dependencies:
1814
1814
+
svelte: 5.56.3(@typescript-eslint/types@8.61.0)
1815
1815
+
three: 0.170.0
1816
1816
+
1817
1817
+
'@threlte/extras@9.21.0(@types/three@0.170.0)(svelte@5.56.3(@typescript-eslint/types@8.61.0))(three@0.170.0)':
1818
1818
+
dependencies:
1819
1819
+
'@threejs-kit/instanced-sprite-mesh': 2.5.1(@types/three@0.170.0)(three@0.170.0)
1820
1820
+
camera-controls: 3.1.2(three@0.170.0)
1821
1821
+
svelte: 5.56.3(@typescript-eslint/types@8.61.0)
1822
1822
+
three: 0.170.0
1823
1823
+
three-mesh-bvh: 0.9.10(three@0.170.0)
1824
1824
+
three-perf: 1.0.11(three@0.170.0)
1825
1825
+
three-viewport-gizmo: 2.2.0(three@0.170.0)
1826
1826
+
troika-three-text: 0.52.4(three@0.170.0)
1827
1827
+
transitivePeerDependencies:
1828
1828
+
- '@types/three'
1829
1829
+
1830
1830
+
'@tweenjs/tween.js@23.1.3': {}
1831
1831
+
1664
1832
'@tybys/wasm-util@0.10.2':
1665
1833
dependencies:
1666
1834
tslib: 2.8.1
···
1685
1853
dependencies:
1686
1854
undici-types: 6.21.0
1687
1855
1856
1856
+
'@types/stats.js@0.17.4': {}
1857
1857
+
1858
1858
+
'@types/three@0.170.0':
1859
1859
+
dependencies:
1860
1860
+
'@tweenjs/tween.js': 23.1.3
1861
1861
+
'@types/stats.js': 0.17.4
1862
1862
+
'@types/webxr': 0.5.24
1863
1863
+
'@webgpu/types': 0.1.70
1864
1864
+
fflate: 0.8.3
1865
1865
+
meshoptimizer: 0.18.1
1866
1866
+
1688
1867
'@types/trusted-types@2.0.7': {}
1868
1868
+
1869
1869
+
'@types/webxr@0.5.24': {}
1689
1870
1690
1871
'@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)':
1691
1872
dependencies:
···
1849
2030
convert-source-map: 2.0.0
1850
2031
tinyrainbow: 3.1.0
1851
2032
2033
2033
+
'@webgpu/types@0.1.70': {}
2034
2034
+
1852
2035
acorn-jsx@5.3.2(acorn@8.16.0):
1853
2036
dependencies:
1854
2037
acorn: 8.16.0
···
1870
2053
1871
2054
balanced-match@4.0.4: {}
1872
2055
2056
2056
+
bidi-js@1.0.3:
2057
2057
+
dependencies:
2058
2058
+
require-from-string: 2.0.2
2059
2059
+
1873
2060
brace-expansion@5.0.6:
1874
2061
dependencies:
1875
2062
balanced-match: 4.0.4
1876
2063
2064
2064
+
camera-controls@3.1.2(three@0.170.0):
2065
2065
+
dependencies:
2066
2066
+
three: 0.170.0
2067
2067
+
1877
2068
chai@6.2.2: {}
1878
2069
1879
2070
chokidar@4.0.3:
···
1894
2085
1895
2086
cssesc@3.0.0: {}
1896
2087
2088
2088
+
culori@4.0.2: {}
2089
2089
+
1897
2090
debug@4.4.3:
1898
2091
dependencies:
1899
2092
ms: 2.1.3
···
1906
2099
1907
2100
devalue@5.8.1: {}
1908
2101
2102
2102
+
diet-sprite@0.0.1: {}
2103
2103
+
2104
2104
+
earcut@2.2.4: {}
2105
2105
+
1909
2106
enhanced-resolve@5.23.0:
1910
2107
dependencies:
1911
2108
graceful-fs: 4.2.11
···
2040
2237
optionalDependencies:
2041
2238
picomatch: 4.0.4
2042
2239
2240
2240
+
fflate@0.8.3: {}
2241
2241
+
2043
2242
file-entry-cache@8.0.0:
2044
2243
dependencies:
2045
2244
flat-cache: 4.0.1
···
2168
2367
dependencies:
2169
2368
p-locate: 5.0.0
2170
2369
2370
2370
+
maath@0.10.8(@types/three@0.170.0)(three@0.170.0):
2371
2371
+
dependencies:
2372
2372
+
'@types/three': 0.170.0
2373
2373
+
three: 0.170.0
2374
2374
+
2171
2375
magic-string@0.30.21:
2172
2376
dependencies:
2173
2377
'@jridgewell/sourcemap-codec': 1.5.5
2378
2378
+
2379
2379
+
meshoptimizer@0.18.1: {}
2174
2380
2175
2381
mini-svg-data-uri@1.4.4: {}
2176
2382
···
2277
2483
2278
2484
readdirp@4.1.2: {}
2279
2485
2486
2486
+
require-from-string@2.0.2: {}
2487
2487
+
2280
2488
rolldown@1.0.3:
2281
2489
dependencies:
2282
2490
'@oxc-project/types': 0.133.0
···
2376
2584
2377
2585
tapable@2.3.3: {}
2378
2586
2587
2587
+
three-instanced-uniforms-mesh@0.52.4(three@0.170.0):
2588
2588
+
dependencies:
2589
2589
+
three: 0.170.0
2590
2590
+
troika-three-utils: 0.52.4(three@0.170.0)
2591
2591
+
2592
2592
+
three-mesh-bvh@0.9.10(three@0.170.0):
2593
2593
+
dependencies:
2594
2594
+
three: 0.170.0
2595
2595
+
2596
2596
+
three-perf@1.0.11(three@0.170.0):
2597
2597
+
dependencies:
2598
2598
+
three: 0.170.0
2599
2599
+
troika-three-text: 0.52.4(three@0.170.0)
2600
2600
+
tweakpane: 3.1.10
2601
2601
+
2602
2602
+
three-viewport-gizmo@2.2.0(three@0.170.0):
2603
2603
+
dependencies:
2604
2604
+
three: 0.170.0
2605
2605
+
2606
2606
+
three@0.170.0: {}
2607
2607
+
2379
2608
tinybench@2.9.0: {}
2380
2609
2381
2610
tinyexec@1.2.4: {}
···
2389
2618
2390
2619
totalist@3.0.1: {}
2391
2620
2621
2621
+
troika-three-text@0.52.4(three@0.170.0):
2622
2622
+
dependencies:
2623
2623
+
bidi-js: 1.0.3
2624
2624
+
three: 0.170.0
2625
2625
+
troika-three-utils: 0.52.4(three@0.170.0)
2626
2626
+
troika-worker-utils: 0.52.0
2627
2627
+
webgl-sdf-generator: 1.1.1
2628
2628
+
2629
2629
+
troika-three-utils@0.52.4(three@0.170.0):
2630
2630
+
dependencies:
2631
2631
+
three: 0.170.0
2632
2632
+
2633
2633
+
troika-worker-utils@0.52.0: {}
2634
2634
+
2392
2635
ts-api-utils@2.5.0(typescript@6.0.3):
2393
2636
dependencies:
2394
2637
typescript: 6.0.3
2395
2638
2396
2639
tslib@2.8.1:
2397
2640
optional: true
2641
2641
+
2642
2642
+
tweakpane@3.1.10: {}
2398
2643
2399
2644
type-check@0.4.0:
2400
2645
dependencies:
···
2470
2715
'@vitest/browser-playwright': 4.1.8(playwright@1.60.0)(vite@8.0.16(@types/node@22.19.20)(jiti@2.7.0))(vitest@4.1.8)
2471
2716
transitivePeerDependencies:
2472
2717
- msw
2718
2718
+
2719
2719
+
webgl-sdf-generator@1.1.1: {}
2473
2720
2474
2721
which@2.0.2:
2475
2722
dependencies:
···
4
4
<meta charset="utf-8" />
5
5
<meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
<meta name="text-scale" content="scale" />
7
7
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
8
8
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
9
+
<link
10
10
+
href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap"
11
11
+
rel="stylesheet"
12
12
+
/>
7
13
%sveltekit.head%
8
14
</head>
9
9
-
<body data-sveltekit-preload-data="hover">
15
15
+
<body data-sveltekit-preload-data="hover" style="background: #0d0e11">
10
16
<div style="display: contents">%sveltekit.body%</div>
11
17
</body>
12
18
</html>
···
1
1
+
<script lang="ts">
2
2
+
import { Download, RotateCcw } from '@lucide/svelte';
3
3
+
import type { GeneratorEntry } from '$lib/registry';
4
4
+
import { displayValue, getPreset, resetParams, setParam, setPreset } from '$lib/editor.svelte';
5
5
+
import SliderControl from './controls/SliderControl.svelte';
6
6
+
import ToggleControl from './controls/ToggleControl.svelte';
7
7
+
import SelectControl from './controls/SelectControl.svelte';
8
8
+
import ColorControl from './controls/ColorControl.svelte';
9
9
+
import ColorArrayControl from './controls/ColorArrayControl.svelte';
10
10
+
11
11
+
let { entry, onexport }: { entry: GeneratorEntry; onexport: () => void } = $props();
12
12
+
13
13
+
const presetNames = $derived(Object.keys(entry.presets));
14
14
+
</script>
15
15
+
16
16
+
<aside class="flex w-84 shrink-0 flex-col border-l border-line bg-panel">
17
17
+
<div class="flex items-center justify-between px-4 pt-[15px] pb-[11px]">
18
18
+
<h3 class="text-[15px] font-bold tracking-tight">Parameters</h3>
19
19
+
<button
20
20
+
type="button"
21
21
+
class="grid size-7.5 place-items-center rounded-[9px] border border-line2 bg-panel2 text-muted hover:text-ink"
22
22
+
title="Reset to defaults"
23
23
+
onclick={() => resetParams(entry.id)}
24
24
+
>
25
25
+
<RotateCcw size={15} />
26
26
+
</button>
27
27
+
</div>
28
28
+
{#if presetNames.length > 1}
29
29
+
<div class="mx-3.5 mb-3.5">
30
30
+
<select
31
31
+
class="h-9.5 w-full rounded-[9px] border-line2 bg-panel2 text-[13px] font-semibold text-ink focus:border-accent focus:ring-0"
32
32
+
value={getPreset(entry.id)}
33
33
+
onchange={(e) => setPreset(entry.id, e.currentTarget.value)}
34
34
+
>
35
35
+
{#each presetNames as p (p)}
36
36
+
<option value={p}>{p}</option>
37
37
+
{/each}
38
38
+
</select>
39
39
+
</div>
40
40
+
{/if}
41
41
+
<div class="flex min-h-0 flex-1 flex-col gap-3.5 overflow-y-auto px-4 pb-4">
42
42
+
{#each Object.entries(entry.schema) as [key, def] (entry.id + ':' + key)}
43
43
+
{#if def.type === 'range'}
44
44
+
<SliderControl
45
45
+
label={def.label ?? key}
46
46
+
value={displayValue(entry, key) as number | [number, number]}
47
47
+
min={def.min}
48
48
+
max={def.max}
49
49
+
step={def.step ?? 0.01}
50
50
+
onchange={(v) => setParam(entry.id, key, v)}
51
51
+
/>
52
52
+
{:else if def.type === 'integer'}
53
53
+
<SliderControl
54
54
+
label={def.label ?? key}
55
55
+
value={displayValue(entry, key) as number | [number, number]}
56
56
+
min={def.min}
57
57
+
max={def.max}
58
58
+
step={1}
59
59
+
onchange={(v) => setParam(entry.id, key, v)}
60
60
+
/>
61
61
+
{:else if def.type === 'boolean'}
62
62
+
<ToggleControl
63
63
+
label={def.label ?? key}
64
64
+
value={displayValue(entry, key) as boolean}
65
65
+
onchange={(v) => setParam(entry.id, key, v)}
66
66
+
/>
67
67
+
{:else if def.type === 'select'}
68
68
+
<SelectControl
69
69
+
label={def.label ?? key}
70
70
+
value={displayValue(entry, key) as string}
71
71
+
options={def.options}
72
72
+
onchange={(v) => setParam(entry.id, key, v)}
73
73
+
/>
74
74
+
{:else if def.type === 'color'}
75
75
+
<ColorControl
76
76
+
label={def.label ?? key}
77
77
+
value={displayValue(entry, key) as string}
78
78
+
onchange={(v) => setParam(entry.id, key, v)}
79
79
+
/>
80
80
+
{:else if def.type === 'color-array'}
81
81
+
<ColorArrayControl
82
82
+
label={def.label ?? key}
83
83
+
colors={displayValue(entry, key) as string[]}
84
84
+
min={def.min}
85
85
+
max={def.max}
86
86
+
onchange={(v) => setParam(entry.id, key, v)}
87
87
+
/>
88
88
+
{/if}
89
89
+
{/each}
90
90
+
</div>
91
91
+
<div class="flex flex-col gap-[7px] border-t border-line px-3.5 py-3">
92
92
+
<button
93
93
+
type="button"
94
94
+
class="flex h-10 w-full items-center justify-center gap-1.5 rounded-[9px] bg-accent text-[13px] font-bold text-accent-ink hover:bg-[#b6f37e]"
95
95
+
onclick={onexport}
96
96
+
>
97
97
+
<Download size={17} />Export GLB
98
98
+
</button>
99
99
+
<div class="mono text-center text-[11px] text-faint">binary glTF · vertex colors</div>
100
100
+
</div>
101
101
+
</aside>
···
1
1
+
<script lang="ts">
2
2
+
import { goto } from '$app/navigation';
3
3
+
import { resolve } from '$app/paths';
4
4
+
import { TreeDeciduous, Sprout, Mountain } from '@lucide/svelte';
5
5
+
import { CATEGORIES, generatorsInCategory, type CategoryId } from '$lib/registry';
6
6
+
7
7
+
let { active }: { active: CategoryId } = $props();
8
8
+
9
9
+
const icons = { trees: TreeDeciduous, plants: Sprout, rocks: Mountain };
10
10
+
</script>
11
11
+
12
12
+
<nav class="flex w-15 shrink-0 flex-col items-center gap-1.5 border-r border-line bg-panel py-3">
13
13
+
{#each CATEGORIES as c (c.id)}
14
14
+
{@const CategoryIcon = icons[c.id]}
15
15
+
<button
16
16
+
type="button"
17
17
+
class="relative grid size-10 place-items-center rounded-[11px] {active === c.id
18
18
+
? 'bg-accent2 text-accent'
19
19
+
: 'text-faint hover:bg-panel2 hover:text-ink'}"
20
20
+
title={c.label}
21
21
+
onclick={() => goto(resolve('/edit/[id]', { id: generatorsInCategory(c.id)[0].id }))}
22
22
+
>
23
23
+
{#if active === c.id}
24
24
+
<span class="absolute top-2 bottom-2 -left-2.5 w-[3px] rounded-sm bg-accent"></span>
25
25
+
{/if}
26
26
+
<CategoryIcon size={20} />
27
27
+
</button>
28
28
+
{/each}
29
29
+
</nav>
···
1
1
+
<script lang="ts">
2
2
+
import * as THREE from 'three';
3
3
+
import { T, useThrelte } from '@threlte/core';
4
4
+
import { OrbitControls } from '@threlte/extras';
5
5
+
import { plane } from 'shapecraft';
6
6
+
import { fbm } from 'shapecraft/noise';
7
7
+
import { heightGradient } from 'shapecraft/color';
8
8
+
import { toThreeMesh } from 'shapecraft/three';
9
9
+
10
10
+
let {
11
11
+
model,
12
12
+
viewDistance,
13
13
+
showGrid,
14
14
+
wireframe
15
15
+
}: { model: THREE.Mesh | null; viewDistance: number; showGrid: boolean; wireframe: boolean } =
16
16
+
$props();
17
17
+
18
18
+
const { scene, invalidate } = useThrelte();
19
19
+
20
20
+
// Material mutation happens outside Threlte's prop system, so re-render manually.
21
21
+
$effect(() => {
22
22
+
if (model) {
23
23
+
(model.material as THREE.MeshStandardMaterial).wireframe = wireframe;
24
24
+
invalidate();
25
25
+
}
26
26
+
});
27
27
+
scene.background = new THREE.Color(0x8eb3d9);
28
28
+
scene.fog = new THREE.FogExp2(0x8eb3d9, 0.022);
29
29
+
30
30
+
// Sky dome with a simple vertical gradient
31
31
+
const skyGeo = new THREE.SphereGeometry(50, 16, 12);
32
32
+
const skyPos = skyGeo.getAttribute('position');
33
33
+
const skyColors = new Float32Array(skyPos.count * 3);
34
34
+
for (let i = 0; i < skyPos.count; i++) {
35
35
+
const t = Math.max(0, skyPos.getY(i) / 50);
36
36
+
skyColors[i * 3] = 0.55 + (0.2 - 0.55) * t;
37
37
+
skyColors[i * 3 + 1] = 0.7 + (0.35 - 0.7) * t;
38
38
+
skyColors[i * 3 + 2] = 0.85 + (0.65 - 0.85) * t;
39
39
+
}
40
40
+
skyGeo.setAttribute('color', new THREE.BufferAttribute(skyColors, 3));
41
41
+
const sky = new THREE.Mesh(
42
42
+
skyGeo,
43
43
+
new THREE.MeshBasicMaterial({ vertexColors: true, side: THREE.BackSide })
44
44
+
);
45
45
+
46
46
+
// Gently rolling ground, built with shapecraft itself
47
47
+
const groundNoise = fbm({ seed: 7, octaves: 3, scale: 0.15, min: 0, max: 0.3 });
48
48
+
const ground = toThreeMesh(
49
49
+
plane({ size: 30, segments: 60 })
50
50
+
.displace((pos: number[]) => groundNoise.get(pos[0], pos[2]))
51
51
+
.vertexColor(
52
52
+
heightGradient([
53
53
+
[-0.05, [0.25, 0.38, 0.12]],
54
54
+
[0.1, [0.3, 0.45, 0.15]],
55
55
+
[0.25, [0.35, 0.42, 0.18]]
56
56
+
])
57
57
+
)
58
58
+
);
59
59
+
ground.receiveShadow = true;
60
60
+
const modelY = groundNoise.get(0, 0);
61
61
+
62
62
+
const camPos = $derived([viewDistance * 0.6, viewDistance * 0.5, viewDistance] as [
63
63
+
number,
64
64
+
number,
65
65
+
number
66
66
+
]);
67
67
+
const target = $derived([0, viewDistance * 0.16, 0] as [number, number, number]);
68
68
+
</script>
69
69
+
70
70
+
<T.PerspectiveCamera makeDefault position={camPos} fov={50}>
71
71
+
<OrbitControls enableDamping dampingFactor={0.08} maxPolarAngle={Math.PI / 2 - 0.05} {target} />
72
72
+
</T.PerspectiveCamera>
73
73
+
74
74
+
<T.HemisphereLight args={[0x87ceeb, 0x3a5a2a, 0.6]} />
75
75
+
<T.DirectionalLight
76
76
+
position={[8, 12, 5]}
77
77
+
intensity={1.4}
78
78
+
color={0xfff4e0}
79
79
+
castShadow
80
80
+
oncreate={(sun) => {
81
81
+
sun.shadow.mapSize.set(2048, 2048);
82
82
+
sun.shadow.camera.left = -12;
83
83
+
sun.shadow.camera.right = 12;
84
84
+
sun.shadow.camera.top = 12;
85
85
+
sun.shadow.camera.bottom = -12;
86
86
+
sun.shadow.camera.near = 0.01;
87
87
+
sun.shadow.camera.far = 40;
88
88
+
sun.shadow.bias = -0.001;
89
89
+
}}
90
90
+
/>
91
91
+
<T.DirectionalLight position={[-5, 4, -3]} intensity={0.3} color={0xb0c4de} />
92
92
+
93
93
+
<T is={sky} />
94
94
+
<T is={ground} />
95
95
+
96
96
+
{#if showGrid}
97
97
+
<T.GridHelper args={[30, 30, 0xe8f0f8, 0xc4d2de]} position.y={0.32} />
98
98
+
{/if}
99
99
+
100
100
+
{#if model}
101
101
+
<T is={model} position.y={modelY} castShadow receiveShadow />
102
102
+
{/if}
···
1
1
+
<script lang="ts">
2
2
+
import { Sparkles, Dices, Download } from '@lucide/svelte';
3
3
+
import { CATEGORIES, type GeneratorEntry } from '$lib/registry';
4
4
+
5
5
+
let {
6
6
+
entry,
7
7
+
ondice,
8
8
+
onexport
9
9
+
}: { entry: GeneratorEntry; ondice: () => void; onexport: () => void } = $props();
10
10
+
11
11
+
const category = $derived(CATEGORIES.find((c) => c.id === entry.category)!);
12
12
+
</script>
13
13
+
14
14
+
<header class="flex h-13 shrink-0 items-center gap-3.5 border-b border-line bg-panel px-4">
15
15
+
<div class="flex items-center gap-2 font-bold tracking-tight">
16
16
+
<span
17
17
+
class="grid size-6 place-items-center rounded-[7px] bg-gradient-to-br from-accent to-[#6db83e] text-accent-ink"
18
18
+
>
19
19
+
<Sparkles size={15} strokeWidth={2} />
20
20
+
</span>
21
21
+
shapecraft
22
22
+
</div>
23
23
+
<div class="h-5.5 w-px bg-line2"></div>
24
24
+
<div class="flex items-center gap-2 text-[13px] text-muted">
25
25
+
<b class="font-semibold text-ink">{category.label}</b>
26
26
+
<span class="text-faint">/</span>
27
27
+
<b class="font-semibold text-accent">{entry.label}</b>
28
28
+
</div>
29
29
+
<div class="flex-1"></div>
30
30
+
<button
31
31
+
type="button"
32
32
+
class="grid size-8.5 place-items-center rounded-[9px] border border-line2 bg-panel2 text-muted hover:border-[#3c424d] hover:text-ink"
33
33
+
title="Randomize seed"
34
34
+
onclick={ondice}
35
35
+
>
36
36
+
<Dices size={18} />
37
37
+
</button>
38
38
+
<button
39
39
+
type="button"
40
40
+
class="flex h-8.5 items-center gap-1.5 rounded-[9px] bg-accent px-3.5 text-[13px] font-semibold text-accent-ink hover:bg-[#b6f37e]"
41
41
+
onclick={onexport}
42
42
+
>
43
43
+
<Download size={16} />Export GLB
44
44
+
</button>
45
45
+
</header>
···
1
1
+
<script lang="ts">
2
2
+
import { Search } from '@lucide/svelte';
3
3
+
import { resolve } from '$app/paths';
4
4
+
import { CATEGORIES, generatorsInCategory, type GeneratorEntry } from '$lib/registry';
5
5
+
6
6
+
let { entry }: { entry: GeneratorEntry } = $props();
7
7
+
8
8
+
let query = $state('');
9
9
+
10
10
+
const category = $derived(CATEGORIES.find((c) => c.id === entry.category)!);
11
11
+
const all = $derived(generatorsInCategory(entry.category));
12
12
+
const types = $derived(all.filter((g) => g.label.toLowerCase().includes(query.toLowerCase())));
13
13
+
14
14
+
const hues: Record<string, number> = { trees: 120, plants: 95, rocks: 30 };
15
15
+
</script>
16
16
+
17
17
+
<aside class="flex w-57 shrink-0 flex-col border-r border-line bg-panel">
18
18
+
<div class="flex items-center justify-between px-4 pt-4 pb-2.5">
19
19
+
<h3 class="text-[15px] font-bold tracking-tight">{category.label}</h3>
20
20
+
<span class="rounded-full border border-line2 bg-panel2 px-2 py-0.5 text-[11px] text-muted">
21
21
+
{all.length} types
22
22
+
</span>
23
23
+
</div>
24
24
+
<div
25
25
+
class="mx-3 mb-2.5 flex h-8.5 items-center gap-2 rounded-[9px] border border-line2 bg-bg px-2.5"
26
26
+
>
27
27
+
<Search size={15} class="shrink-0 text-faint" />
28
28
+
<input
29
29
+
type="text"
30
30
+
class="w-full border-none bg-transparent p-0 text-[13px] text-ink placeholder:text-faint focus:ring-0"
31
31
+
placeholder="Search types…"
32
32
+
bind:value={query}
33
33
+
/>
34
34
+
</div>
35
35
+
<div class="flex min-h-0 flex-col gap-[3px] overflow-y-auto px-2 py-1">
36
36
+
{#each types as g, i (g.id)}
37
37
+
<a
38
38
+
href={resolve('/edit/[id]', { id: g.id })}
39
39
+
class="flex items-center gap-2.5 rounded-[10px] px-2 py-[7px] {g.id === entry.id
40
40
+
? 'bg-panel2 shadow-[inset_0_0_0_1px_var(--color-line2)]'
41
41
+
: 'hover:bg-panel2'}"
42
42
+
>
43
43
+
<span
44
44
+
class="mono grid h-7.5 w-9.5 shrink-0 place-items-center rounded-[7px] text-[9px] font-semibold text-white/70 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)]"
45
45
+
style="background:linear-gradient(135deg, hsl({hues[g.category]} 28% {40 -
46
46
+
i * 2}%), hsl({hues[g.category]} 34% {24 - i}%))"
47
47
+
>
48
48
+
{g.label.slice(0, 3).toUpperCase()}
49
49
+
</span>
50
50
+
<span>
51
51
+
<span class="block text-[13.5px] font-semibold {g.id === entry.id ? 'text-accent' : ''}">
52
52
+
{g.label}
53
53
+
</span>
54
54
+
<span class="mono block text-[11px] text-faint">
55
55
+
{Object.keys(g.schema).length} params
56
56
+
</span>
57
57
+
</span>
58
58
+
</a>
59
59
+
{/each}
60
60
+
</div>
61
61
+
</aside>
···
1
1
+
<script lang="ts">
2
2
+
import type * as THREE from 'three';
3
3
+
import { Canvas } from '@threlte/core';
4
4
+
import { Box, Dices, Grid3x3, TreeDeciduous, Sprout, Mountain } from '@lucide/svelte';
5
5
+
import type { GeneratorEntry } from '$lib/registry';
6
6
+
import Scene from './Scene.svelte';
7
7
+
8
8
+
let {
9
9
+
model,
10
10
+
entry,
11
11
+
seed,
12
12
+
tris,
13
13
+
error,
14
14
+
ondice
15
15
+
}: {
16
16
+
model: THREE.Mesh | null;
17
17
+
entry: GeneratorEntry;
18
18
+
seed: number | undefined;
19
19
+
tris: number;
20
20
+
error: string | null;
21
21
+
ondice: () => void;
22
22
+
} = $props();
23
23
+
24
24
+
let showGrid = $state(false);
25
25
+
let wireframe = $state(false);
26
26
+
27
27
+
const icons = { trees: TreeDeciduous, plants: Sprout, rocks: Mountain };
28
28
+
const CategoryIcon = $derived(icons[entry.category]);
29
29
+
30
30
+
const float =
31
31
+
'absolute rounded-xl border border-white/10 bg-[rgba(16,18,22,0.66)] backdrop-blur-[10px]';
32
32
+
</script>
33
33
+
34
34
+
<div class="relative min-w-0 flex-1 overflow-hidden">
35
35
+
<Canvas>
36
36
+
<Scene {model} viewDistance={entry.viewDistance} {showGrid} {wireframe} />
37
37
+
</Canvas>
38
38
+
39
39
+
<!-- title card -->
40
40
+
<div class="{float} top-4 left-4 flex items-center gap-2.5 px-3.5 py-2.5">
41
41
+
<span class="grid size-7.5 place-items-center rounded-lg bg-accent2 text-accent">
42
42
+
<CategoryIcon size={17} />
43
43
+
</span>
44
44
+
<div>
45
45
+
<h4 class="text-sm font-bold text-white">{entry.label}</h4>
46
46
+
<div class="mono text-[11px] text-white/60">
47
47
+
seed {seed ?? '–'} · {tris.toLocaleString()} tris
48
48
+
</div>
49
49
+
</div>
50
50
+
</div>
51
51
+
52
52
+
<!-- view tools -->
53
53
+
<div class="{float} top-4 right-4 flex flex-col gap-[3px] p-[5px]">
54
54
+
<button
55
55
+
type="button"
56
56
+
class="grid size-8.5 place-items-center rounded-lg {showGrid
57
57
+
? 'bg-accent text-accent-ink'
58
58
+
: 'text-white/70 hover:bg-white/10 hover:text-white'}"
59
59
+
title="Toggle grid"
60
60
+
onclick={() => (showGrid = !showGrid)}
61
61
+
>
62
62
+
<Grid3x3 size={17} />
63
63
+
</button>
64
64
+
<button
65
65
+
type="button"
66
66
+
class="grid size-8.5 place-items-center rounded-lg {wireframe
67
67
+
? 'bg-accent text-accent-ink'
68
68
+
: 'text-white/70 hover:bg-white/10 hover:text-white'}"
69
69
+
title="Toggle wireframe"
70
70
+
onclick={() => (wireframe = !wireframe)}
71
71
+
>
72
72
+
<Box size={17} />
73
73
+
</button>
74
74
+
</div>
75
75
+
76
76
+
<!-- status -->
77
77
+
<div class="{float} bottom-4 left-4 flex items-center gap-2.5 px-3 py-[7px]">
78
78
+
{#if error}
79
79
+
<span class="size-1.5 rounded-full bg-red-400"></span>
80
80
+
<span class="mono text-[11px] text-red-300">{error}</span>
81
81
+
{:else}
82
82
+
<span class="size-1.5 rounded-full bg-accent"></span>
83
83
+
<span class="mono text-[11px] text-white/80">{tris.toLocaleString()} tris · GLB ready</span>
84
84
+
{/if}
85
85
+
</div>
86
86
+
87
87
+
<!-- randomize -->
88
88
+
<button
89
89
+
type="button"
90
90
+
class="absolute bottom-4 left-1/2 grid size-10.5 -translate-x-1/2 place-items-center rounded-[10px] bg-accent text-accent-ink shadow-lg hover:bg-[#b6f37e]"
91
91
+
title="Randomize seed"
92
92
+
onclick={ondice}
93
93
+
>
94
94
+
<Dices size={20} />
95
95
+
</button>
96
96
+
</div>
···
1
1
+
<script lang="ts">
2
2
+
import { Plus, X } from '@lucide/svelte';
3
3
+
4
4
+
let {
5
5
+
label,
6
6
+
colors,
7
7
+
min = 0,
8
8
+
max = 6,
9
9
+
onchange
10
10
+
}: {
11
11
+
label: string;
12
12
+
colors: string[];
13
13
+
min?: number;
14
14
+
max?: number;
15
15
+
onchange: (v: string[]) => void;
16
16
+
} = $props();
17
17
+
18
18
+
function setColor(i: number, c: string) {
19
19
+
onchange(colors.map((x, j) => (j === i ? c : x)));
20
20
+
}
21
21
+
function remove(i: number) {
22
22
+
onchange(colors.filter((_, j) => j !== i));
23
23
+
}
24
24
+
function add() {
25
25
+
onchange([...colors, colors[colors.length - 1] ?? '#8a9078']);
26
26
+
}
27
27
+
</script>
28
28
+
29
29
+
<div>
30
30
+
<div class="mb-2 flex items-baseline justify-between">
31
31
+
<span class="text-[12.5px] font-medium text-muted">{label}</span>
32
32
+
{#if colors.length === 0}
33
33
+
<span class="text-xs text-faint">off</span>
34
34
+
{/if}
35
35
+
</div>
36
36
+
<div class="flex flex-wrap items-center gap-1.5">
37
37
+
{#each colors as c, i (i)}
38
38
+
<span class="group relative">
39
39
+
<label
40
40
+
class="block size-6.5 cursor-pointer rounded-[7px] shadow-[inset_0_0_0_1px_rgba(255,255,255,0.14)]"
41
41
+
style="background:{c}"
42
42
+
>
43
43
+
<input
44
44
+
type="color"
45
45
+
class="absolute inset-0 size-full cursor-pointer border-none p-0 opacity-0"
46
46
+
value={c}
47
47
+
oninput={(e) => setColor(i, e.currentTarget.value)}
48
48
+
/>
49
49
+
</label>
50
50
+
{#if colors.length > min}
51
51
+
<button
52
52
+
type="button"
53
53
+
class="absolute -top-1.5 -right-1.5 hidden size-4 place-items-center rounded-full bg-line2 text-muted group-hover:grid hover:text-ink"
54
54
+
title="Remove color"
55
55
+
onclick={() => remove(i)}
56
56
+
>
57
57
+
<X size={10} />
58
58
+
</button>
59
59
+
{/if}
60
60
+
</span>
61
61
+
{/each}
62
62
+
{#if colors.length < max}
63
63
+
<button
64
64
+
type="button"
65
65
+
class="grid size-6.5 place-items-center rounded-[7px] border border-dashed border-line2 text-muted hover:text-ink"
66
66
+
title="Add color"
67
67
+
onclick={add}
68
68
+
>
69
69
+
<Plus size={14} />
70
70
+
</button>
71
71
+
{/if}
72
72
+
</div>
73
73
+
</div>
···
1
1
+
<script lang="ts">
2
2
+
let { label, value, onchange }: { label: string; value: string; onchange: (v: string) => void } =
3
3
+
$props();
4
4
+
</script>
5
5
+
6
6
+
<div class="flex items-center justify-between">
7
7
+
<span class="text-[12.5px] font-medium text-muted">{label}</span>
8
8
+
<div class="flex items-center gap-2">
9
9
+
<span class="mono text-xs text-faint">{value}</span>
10
10
+
<label
11
11
+
class="relative block size-6.5 cursor-pointer rounded-[7px] shadow-[inset_0_0_0_1px_rgba(255,255,255,0.14)]"
12
12
+
style="background:{value}"
13
13
+
>
14
14
+
<input
15
15
+
type="color"
16
16
+
class="absolute inset-0 size-full cursor-pointer border-none p-0 opacity-0"
17
17
+
{value}
18
18
+
oninput={(e) => onchange(e.currentTarget.value)}
19
19
+
/>
20
20
+
</label>
21
21
+
</div>
22
22
+
</div>
···
1
1
+
<script lang="ts">
2
2
+
let {
3
3
+
label,
4
4
+
value,
5
5
+
options,
6
6
+
onchange
7
7
+
}: { label: string; value: string; options: string[]; onchange: (v: string) => void } = $props();
8
8
+
</script>
9
9
+
10
10
+
<div class="flex items-center justify-between gap-3">
11
11
+
<span class="text-[12.5px] font-medium text-muted">{label}</span>
12
12
+
<select
13
13
+
class="h-8 min-w-28 rounded-lg border-line2 bg-panel2 py-0 pl-3 text-[13px] font-semibold text-ink focus:border-accent focus:ring-0"
14
14
+
{value}
15
15
+
onchange={(e) => onchange(e.currentTarget.value)}
16
16
+
>
17
17
+
{#each options as o (o)}
18
18
+
<option value={o}>{o}</option>
19
19
+
{/each}
20
20
+
</select>
21
21
+
</div>
···
1
1
+
<script lang="ts">
2
2
+
let {
3
3
+
label,
4
4
+
value,
5
5
+
min,
6
6
+
max,
7
7
+
step = 0.01,
8
8
+
onchange
9
9
+
}: {
10
10
+
label: string;
11
11
+
value: number | [number, number];
12
12
+
min: number;
13
13
+
max: number;
14
14
+
step?: number;
15
15
+
onchange: (v: number | [number, number]) => void;
16
16
+
} = $props();
17
17
+
18
18
+
const isRange = $derived(Array.isArray(value));
19
19
+
const lo = $derived(Array.isArray(value) ? value[0] : value);
20
20
+
const hi = $derived(Array.isArray(value) ? value[1] : value);
21
21
+
22
22
+
const pct = (v: number) => ((v - min) / (max - min)) * 100;
23
23
+
24
24
+
const decimals = $derived(step >= 1 ? 0 : step >= 0.1 ? 1 : step >= 0.01 ? 2 : 3);
25
25
+
const fmt = (v: number) => v.toFixed(decimals);
26
26
+
</script>
27
27
+
28
28
+
<div>
29
29
+
<div class="mb-2 flex items-baseline justify-between">
30
30
+
<span class="text-[12.5px] font-medium text-muted">{label}</span>
31
31
+
<span class="mono text-xs text-ink">{isRange ? `${fmt(lo)} – ${fmt(hi)}` : fmt(lo)}</span>
32
32
+
</div>
33
33
+
<div class="rng" class:single={!isRange}>
34
34
+
<div class="rng-track"></div>
35
35
+
<div class="rng-fill" style="left:{isRange ? pct(lo) : 0}%; right:{100 - pct(hi)}%"></div>
36
36
+
{#if isRange}
37
37
+
<input
38
38
+
type="range"
39
39
+
{min}
40
40
+
{max}
41
41
+
{step}
42
42
+
value={lo}
43
43
+
oninput={(e) => onchange([Math.min(+e.currentTarget.value, hi), hi])}
44
44
+
/>
45
45
+
<input
46
46
+
type="range"
47
47
+
{min}
48
48
+
{max}
49
49
+
{step}
50
50
+
value={hi}
51
51
+
oninput={(e) => onchange([lo, Math.max(+e.currentTarget.value, lo)])}
52
52
+
/>
53
53
+
{:else}
54
54
+
<input
55
55
+
type="range"
56
56
+
{min}
57
57
+
{max}
58
58
+
{step}
59
59
+
value={lo}
60
60
+
oninput={(e) => onchange(+e.currentTarget.value)}
61
61
+
/>
62
62
+
{/if}
63
63
+
</div>
64
64
+
</div>
65
65
+
66
66
+
<style>
67
67
+
.rng {
68
68
+
position: relative;
69
69
+
height: 22px;
70
70
+
}
71
71
+
.rng-track {
72
72
+
position: absolute;
73
73
+
left: 0;
74
74
+
right: 0;
75
75
+
top: 9px;
76
76
+
height: 4px;
77
77
+
border-radius: 3px;
78
78
+
background: var(--color-line2);
79
79
+
}
80
80
+
.rng-fill {
81
81
+
position: absolute;
82
82
+
top: 9px;
83
83
+
height: 4px;
84
84
+
border-radius: 3px;
85
85
+
background: var(--color-accent);
86
86
+
}
87
87
+
.rng input[type='range'] {
88
88
+
-webkit-appearance: none;
89
89
+
appearance: none;
90
90
+
position: absolute;
91
91
+
inset: 0;
92
92
+
width: 100%;
93
93
+
height: 22px;
94
94
+
margin: 0;
95
95
+
padding: 0;
96
96
+
background: transparent;
97
97
+
pointer-events: none;
98
98
+
}
99
99
+
.rng.single input[type='range'] {
100
100
+
pointer-events: auto;
101
101
+
}
102
102
+
.rng input[type='range']:focus {
103
103
+
outline: none;
104
104
+
box-shadow: none;
105
105
+
}
106
106
+
.rng input[type='range']::-webkit-slider-runnable-track {
107
107
+
background: transparent;
108
108
+
height: 22px;
109
109
+
}
110
110
+
.rng input[type='range']::-webkit-slider-thumb {
111
111
+
-webkit-appearance: none;
112
112
+
pointer-events: auto;
113
113
+
width: 14px;
114
114
+
height: 14px;
115
115
+
margin-top: 4px;
116
116
+
border-radius: 50%;
117
117
+
border: none;
118
118
+
background: #eaf6dc;
119
119
+
box-shadow:
120
120
+
0 0 0 3px rgb(166 232 106 / 0.25),
121
121
+
0 1px 3px rgb(0 0 0 / 0.45);
122
122
+
cursor: grab;
123
123
+
}
124
124
+
.rng input[type='range']:active::-webkit-slider-thumb {
125
125
+
cursor: grabbing;
126
126
+
}
127
127
+
.rng input[type='range']::-moz-range-thumb {
128
128
+
pointer-events: auto;
129
129
+
width: 14px;
130
130
+
height: 14px;
131
131
+
border-radius: 50%;
132
132
+
border: none;
133
133
+
background: #eaf6dc;
134
134
+
box-shadow: 0 0 0 3px rgb(166 232 106 / 0.25);
135
135
+
cursor: grab;
136
136
+
}
137
137
+
</style>
···
1
1
+
<script lang="ts">
2
2
+
import { Check } from '@lucide/svelte';
3
3
+
4
4
+
let {
5
5
+
label,
6
6
+
value,
7
7
+
onchange
8
8
+
}: { label: string; value: boolean; onchange: (v: boolean) => void } = $props();
9
9
+
</script>
10
10
+
11
11
+
<button
12
12
+
type="button"
13
13
+
role="switch"
14
14
+
aria-checked={value}
15
15
+
class="flex items-center gap-2.5 py-0.5"
16
16
+
onclick={() => onchange(!value)}
17
17
+
>
18
18
+
<span
19
19
+
class="grid size-4.5 place-items-center rounded-md border-[1.5px] {value
20
20
+
? 'border-accent bg-accent text-accent-ink'
21
21
+
: 'border-line2 text-transparent'}"
22
22
+
>
23
23
+
<Check size={13} strokeWidth={3} />
24
24
+
</span>
25
25
+
<span class="text-[13px] font-semibold">{label}</span>
26
26
+
</button>
···
1
1
+
import type { GeneratorEntry } from './registry';
2
2
+
3
3
+
/**
4
4
+
* Editor state, kept at module level keyed by generator id so parameter
5
5
+
* tweaks survive switching between generators within a session.
6
6
+
*/
7
7
+
const overridesById: Record<string, Record<string, unknown>> = $state({});
8
8
+
const presetById: Record<string, string> = $state({});
9
9
+
10
10
+
export function getOverrides(id: string): Record<string, unknown> {
11
11
+
if (!overridesById[id]) overridesById[id] = {};
12
12
+
return overridesById[id];
13
13
+
}
14
14
+
15
15
+
export function getPreset(id: string): string {
16
16
+
return presetById[id] ?? 'default';
17
17
+
}
18
18
+
19
19
+
export function setPreset(id: string, preset: string) {
20
20
+
presetById[id] = preset;
21
21
+
// A preset is a fresh baseline: drop manual overrides so its values show through.
22
22
+
overridesById[id] = {};
23
23
+
}
24
24
+
25
25
+
export function setParam(id: string, key: string, value: unknown) {
26
26
+
getOverrides(id);
27
27
+
overridesById[id][key] = value;
28
28
+
}
29
29
+
30
30
+
export function resetParams(id: string) {
31
31
+
overridesById[id] = {};
32
32
+
presetById[id] = 'default';
33
33
+
}
34
34
+
35
35
+
/** The value the UI should display for a param: override → preset → schema default. */
36
36
+
export function displayValue(entry: GeneratorEntry, key: string): unknown {
37
37
+
const override = overridesById[entry.id]?.[key];
38
38
+
if (override !== undefined) return override;
39
39
+
const preset = entry.presets[getPreset(entry.id)];
40
40
+
if (preset && preset[key] !== undefined) return preset[key];
41
41
+
return entry.schema[key].default;
42
42
+
}
43
43
+
44
44
+
/** Options object passed to the generator function. */
45
45
+
export function buildOptions(entry: GeneratorEntry): Record<string, unknown> {
46
46
+
return { ...overridesById[entry.id], preset: getPreset(entry.id) };
47
47
+
}
48
48
+
49
49
+
export function randomizeSeed(entry: GeneratorEntry) {
50
50
+
const def = entry.schema.seed;
51
51
+
if (!def || (def.type !== 'integer' && def.type !== 'range')) return;
52
52
+
const seed = Math.floor(def.min + Math.random() * (def.max - def.min + 1));
53
53
+
setParam(entry.id, 'seed', seed);
54
54
+
}
···
1
1
+
import type * as THREE from 'three';
2
2
+
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
3
3
+
4
4
+
export async function exportGLB(object: THREE.Object3D, filename: string) {
5
5
+
const exporter = new GLTFExporter();
6
6
+
const result = await exporter.parseAsync(object, { binary: true });
7
7
+
const blob = new Blob([result as ArrayBuffer], { type: 'model/gltf-binary' });
8
8
+
const url = URL.createObjectURL(blob);
9
9
+
const a = document.createElement('a');
10
10
+
a.href = url;
11
11
+
a.download = filename.endsWith('.glb') ? filename : `${filename}.glb`;
12
12
+
a.click();
13
13
+
URL.revokeObjectURL(url);
14
14
+
}
···
1
1
+
import type { OptionSchema } from 'shapecraft/core/schema';
2
2
+
import type { Mesh } from 'shapecraft/core/mesh';
3
3
+
import {
4
4
+
tree,
5
5
+
treeSchema,
6
6
+
treePresets,
7
7
+
pine,
8
8
+
pineSchema,
9
9
+
pinePresets,
10
10
+
palm,
11
11
+
palmSchema,
12
12
+
palmPresets,
13
13
+
bush,
14
14
+
bushSchema,
15
15
+
bushPresets,
16
16
+
grass,
17
17
+
grassSchema,
18
18
+
grassPresets,
19
19
+
fern,
20
20
+
fernSchema,
21
21
+
fernPresets,
22
22
+
flower,
23
23
+
flowerSchema,
24
24
+
flowerPresets,
25
25
+
deadTree,
26
26
+
deadTreeSchema,
27
27
+
deadTreePresets,
28
28
+
leafyTree,
29
29
+
leafyTreeSchema,
30
30
+
leafyTreePresets,
31
31
+
rock,
32
32
+
rockSchema,
33
33
+
rockPresets,
34
34
+
sharpRock,
35
35
+
sharpRockSchema,
36
36
+
sharpRockPresets,
37
37
+
blockRock,
38
38
+
blockRockSchema,
39
39
+
blockRockPresets
40
40
+
} from 'shapecraft/generators';
41
41
+
42
42
+
export type CategoryId = 'trees' | 'plants' | 'rocks';
43
43
+
44
44
+
export interface Category {
45
45
+
id: CategoryId;
46
46
+
label: string;
47
47
+
}
48
48
+
49
49
+
export const CATEGORIES: Category[] = [
50
50
+
{ id: 'trees', label: 'Trees' },
51
51
+
{ id: 'plants', label: 'Plants' },
52
52
+
{ id: 'rocks', label: 'Rocks' }
53
53
+
];
54
54
+
55
55
+
export interface GeneratorEntry {
56
56
+
id: string;
57
57
+
label: string;
58
58
+
category: CategoryId;
59
59
+
gen: (opts: Record<string, unknown>) => Mesh;
60
60
+
schema: OptionSchema;
61
61
+
presets: Record<string, Record<string, unknown>>;
62
62
+
/** Rough camera distance that frames the default model nicely */
63
63
+
viewDistance: number;
64
64
+
}
65
65
+
66
66
+
export const GENERATORS: GeneratorEntry[] = [
67
67
+
{
68
68
+
id: 'common',
69
69
+
label: 'Common Tree',
70
70
+
category: 'trees',
71
71
+
gen: tree,
72
72
+
schema: treeSchema,
73
73
+
presets: treePresets,
74
74
+
viewDistance: 7
75
75
+
},
76
76
+
{
77
77
+
id: 'leafy',
78
78
+
label: 'Leafy Tree',
79
79
+
category: 'trees',
80
80
+
gen: leafyTree,
81
81
+
schema: leafyTreeSchema,
82
82
+
presets: leafyTreePresets,
83
83
+
viewDistance: 7
84
84
+
},
85
85
+
{
86
86
+
id: 'pine',
87
87
+
label: 'Pine',
88
88
+
category: 'trees',
89
89
+
gen: pine,
90
90
+
schema: pineSchema,
91
91
+
presets: pinePresets,
92
92
+
viewDistance: 8
93
93
+
},
94
94
+
{
95
95
+
id: 'palm',
96
96
+
label: 'Palm',
97
97
+
category: 'trees',
98
98
+
gen: palm,
99
99
+
schema: palmSchema,
100
100
+
presets: palmPresets,
101
101
+
viewDistance: 9
102
102
+
},
103
103
+
{
104
104
+
id: 'dead',
105
105
+
label: 'Dead Tree',
106
106
+
category: 'trees',
107
107
+
gen: deadTree,
108
108
+
schema: deadTreeSchema,
109
109
+
presets: deadTreePresets,
110
110
+
viewDistance: 8
111
111
+
},
112
112
+
{
113
113
+
id: 'bush',
114
114
+
label: 'Bush',
115
115
+
category: 'plants',
116
116
+
gen: bush,
117
117
+
schema: bushSchema,
118
118
+
presets: bushPresets,
119
119
+
viewDistance: 3
120
120
+
},
121
121
+
{
122
122
+
id: 'grass',
123
123
+
label: 'Grass',
124
124
+
category: 'plants',
125
125
+
gen: grass,
126
126
+
schema: grassSchema,
127
127
+
presets: grassPresets,
128
128
+
viewDistance: 2.5
129
129
+
},
130
130
+
{
131
131
+
id: 'fern',
132
132
+
label: 'Fern',
133
133
+
category: 'plants',
134
134
+
gen: fern,
135
135
+
schema: fernSchema,
136
136
+
presets: fernPresets,
137
137
+
viewDistance: 3.5
138
138
+
},
139
139
+
{
140
140
+
id: 'flower',
141
141
+
label: 'Flower',
142
142
+
category: 'plants',
143
143
+
gen: flower,
144
144
+
schema: flowerSchema,
145
145
+
presets: flowerPresets,
146
146
+
viewDistance: 2.5
147
147
+
},
148
148
+
{
149
149
+
id: 'rock',
150
150
+
label: 'Rock',
151
151
+
category: 'rocks',
152
152
+
gen: rock,
153
153
+
schema: rockSchema,
154
154
+
presets: rockPresets,
155
155
+
viewDistance: 3
156
156
+
},
157
157
+
{
158
158
+
id: 'sharp',
159
159
+
label: 'Sharp Rock',
160
160
+
category: 'rocks',
161
161
+
gen: sharpRock,
162
162
+
schema: sharpRockSchema,
163
163
+
presets: sharpRockPresets,
164
164
+
viewDistance: 4
165
165
+
},
166
166
+
{
167
167
+
id: 'block',
168
168
+
label: 'Block Rock',
169
169
+
category: 'rocks',
170
170
+
gen: blockRock,
171
171
+
schema: blockRockSchema,
172
172
+
presets: blockRockPresets,
173
173
+
viewDistance: 3.5
174
174
+
}
175
175
+
];
176
176
+
177
177
+
export function getGenerator(id: string): GeneratorEntry | undefined {
178
178
+
return GENERATORS.find((g) => g.id === id);
179
179
+
}
180
180
+
181
181
+
export function generatorsInCategory(category: CategoryId): GeneratorEntry[] {
182
182
+
return GENERATORS.filter((g) => g.category === category);
183
183
+
}
···
1
1
-
<script>
2
2
-
import { greet } from './greet';
3
3
-
4
4
-
let { host = 'SvelteKit', guest = 'Vitest' } = $props();
5
5
-
</script>
6
6
-
7
7
-
<h1>{greet(host)}</h1>
8
8
-
<p>{greet(guest)}</p>
···
1
1
-
import { page } from 'vitest/browser';
2
2
-
import { describe, expect, it } from 'vitest';
3
3
-
import { render } from 'vitest-browser-svelte';
4
4
-
import Welcome from './Welcome.svelte';
5
5
-
6
6
-
describe('Welcome.svelte', () => {
7
7
-
it('renders greetings for host and guest', async () => {
8
8
-
render(Welcome, { host: 'SvelteKit', guest: 'Vitest' });
9
9
-
10
10
-
await expect
11
11
-
.element(page.getByRole('heading', { level: 1 }))
12
12
-
.toHaveTextContent('Hello, SvelteKit!');
13
13
-
await expect.element(page.getByText('Hello, Vitest!')).toBeInTheDocument();
14
14
-
});
15
15
-
});
···
1
1
-
import { describe, it, expect } from 'vitest';
2
2
-
import { greet } from './greet';
3
3
-
4
4
-
describe('greet', () => {
5
5
-
it('returns a greeting', () => {
6
6
-
expect(greet('Svelte')).toBe('Hello, Svelte!');
7
7
-
});
8
8
-
});
···
1
1
-
export function greet(name: string): string {
2
2
-
return 'Hello, ' + name + '!';
3
3
-
}
···
1
1
+
// Pure client-side tool: three.js / WebGL has no use for SSR here.
2
2
+
export const ssr = false;
···
1
1
-
<h1>Welcome to SvelteKit</h1>
2
2
-
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
1
1
+
<!-- Redirects to the editor in +page.ts -->
···
1
1
+
import { redirect } from '@sveltejs/kit';
2
2
+
3
3
+
export function load() {
4
4
+
redirect(307, '/edit/common');
5
5
+
}
···
1
1
-
<script lang="ts">
2
2
-
import { resolve } from '$app/paths';
3
3
-
</script>
4
4
-
5
5
-
<a href={resolve('/demo/playwright')}>playwright</a>
···
1
1
-
<h1>Playwright e2e test demo</h1>
···
1
1
-
import { expect, test } from '@playwright/test';
2
2
-
3
3
-
test('has expected h1', async ({ page }) => {
4
4
-
await page.goto('/demo/playwright');
5
5
-
await expect(page.locator('h1')).toBeVisible();
6
6
-
});
···
1
1
+
<script lang="ts">
2
2
+
import { page } from '$app/state';
3
3
+
import { toThreeMesh } from 'shapecraft/three';
4
4
+
import { getGenerator } from '$lib/registry';
5
5
+
import { buildOptions, displayValue, randomizeSeed } from '$lib/editor.svelte';
6
6
+
import { exportGLB } from '$lib/export-glb';
7
7
+
import TopBar from '$lib/components/TopBar.svelte';
8
8
+
import Rail from '$lib/components/Rail.svelte';
9
9
+
import TypeList from '$lib/components/TypeList.svelte';
10
10
+
import Viewport from '$lib/components/Viewport.svelte';
11
11
+
import ParamPanel from '$lib/components/ParamPanel.svelte';
12
12
+
13
13
+
const entry = $derived(getGenerator(page.params.id!)!);
14
14
+
15
15
+
const result = $derived.by(() => {
16
16
+
try {
17
17
+
return { model: toThreeMesh(entry.gen(buildOptions(entry))), error: null };
18
18
+
} catch (e) {
19
19
+
return { model: null, error: e instanceof Error ? e.message : String(e) };
20
20
+
}
21
21
+
});
22
22
+
const model = $derived(result.model);
23
23
+
const tris = $derived(
24
24
+
model ? (model.geometry.index?.count ?? model.geometry.getAttribute('position').count) / 3 : 0
25
25
+
);
26
26
+
const seed = $derived(displayValue(entry, 'seed') as number | undefined);
27
27
+
28
28
+
function dice() {
29
29
+
randomizeSeed(entry);
30
30
+
}
31
31
+
function doExport() {
32
32
+
if (model) exportGLB(model, `${entry.id}-seed-${seed ?? 0}`);
33
33
+
}
34
34
+
</script>
35
35
+
36
36
+
<svelte:head>
37
37
+
<title>{entry.label} · shapecraft</title>
38
38
+
</svelte:head>
39
39
+
40
40
+
<div class="flex h-dvh flex-col">
41
41
+
<TopBar {entry} ondice={dice} onexport={doExport} />
42
42
+
<div class="flex min-h-0 flex-1">
43
43
+
<Rail active={entry.category} />
44
44
+
<TypeList {entry} />
45
45
+
<Viewport {model} {entry} {seed} {tris} error={result.error} ondice={dice} />
46
46
+
<ParamPanel {entry} onexport={doExport} />
47
47
+
</div>
48
48
+
</div>
···
1
1
+
import { error } from '@sveltejs/kit';
2
2
+
import { getGenerator } from '$lib/registry';
3
3
+
import type { PageLoad } from './$types';
4
4
+
5
5
+
export const load: PageLoad = ({ params }) => {
6
6
+
if (!getGenerator(params.id)) error(404, `Unknown generator "${params.id}"`);
7
7
+
};
···
1
1
@import 'tailwindcss';
2
2
@plugin '@tailwindcss/forms';
3
3
@plugin '@tailwindcss/typography';
4
4
+
5
5
+
/* Atelier (dirA) design tokens */
6
6
+
@theme {
7
7
+
--color-bg: #0d0e11;
8
8
+
--color-panel: #14161b;
9
9
+
--color-panel2: #191c22;
10
10
+
--color-line: #242830;
11
11
+
--color-line2: #2f343d;
12
12
+
--color-ink: #e9eaed;
13
13
+
--color-muted: #969ba5;
14
14
+
--color-faint: #666c76;
15
15
+
--color-accent: #a6e86a;
16
16
+
--color-accent2: #3c5526;
17
17
+
--color-accent-ink: #16270a;
18
18
+
19
19
+
--font-sans: 'Hanken Grotesk', system-ui, sans-serif;
20
20
+
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
21
21
+
}
22
22
+
23
23
+
html,
24
24
+
body {
25
25
+
height: 100%;
26
26
+
}
27
27
+
28
28
+
body {
29
29
+
background: var(--color-bg);
30
30
+
color: var(--color-ink);
31
31
+
font-family: var(--font-sans);
32
32
+
font-size: 14px;
33
33
+
-webkit-font-smoothing: antialiased;
34
34
+
overflow: hidden;
35
35
+
}
36
36
+
37
37
+
.mono {
38
38
+
font-family: var(--font-mono);
39
39
+
font-feature-settings: 'tnum';
40
40
+
}
···
14
14
filename.split(/[/\\]/).includes('node_modules') ? undefined : true
15
15
},
16
16
17
17
+
// Consume the shapecraft library as TS source so generator edits hot-reload in the viewport.
18
18
+
alias: {
19
19
+
shapecraft: '../src',
20
20
+
'shapecraft/*': '../src/*'
21
21
+
},
22
22
+
17
23
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
18
24
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
19
25
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
20
26
adapter: adapter()
21
27
})
22
28
],
29
29
+
server: {
30
30
+
fs: {
31
31
+
// Allow serving shapecraft source from one level up
32
32
+
allow: ['..']
33
33
+
}
34
34
+
},
23
35
test: {
24
36
expect: { requireAssertions: true },
25
37
projects: [