[READ-ONLY] Mirror of https://github.com/flo-bit/shapecraft.
flo-bit.dev/shapecraft/
4.0 kB
103 lines
1import { describe, it, expect } from 'vitest'
2import { tree, pine, palm, bush, grass, fern, flower, deadTree, leafyTree, rock, sharpRock, blockRock, mushroom } from '../src/generators'
3import type { Mesh } from '../src/core/mesh'
4import type { Asset } from '../src/core/asset'
5
6const generators = [
7 { name: 'tree', gen: tree },
8 { name: 'pine', gen: pine },
9 { name: 'palm', gen: palm },
10 { name: 'bush', gen: bush },
11 { name: 'grass', gen: grass },
12 { name: 'fern', gen: fern },
13 { name: 'flower', gen: flower },
14 { name: 'dead', gen: deadTree },
15 { name: 'leafy', gen: leafyTree },
16 { name: 'rock', gen: rock },
17 { name: 'sharp', gen: sharpRock },
18 { name: 'block', gen: blockRock },
19 { name: 'mushroom', gen: mushroom },
20] as const
21
22// Generators that support snow (trees, shrubs, rocks). Grass/ferns don't take snow options.
23const snowGenerators = generators.filter((g) => ['tree', 'pine', 'palm', 'bush', 'leafy', 'rock', 'sharp', 'block'].includes(g.name))
24
25// Golden snapshots captured after the named-stream RNG migration. A change here means
26// generator output shifted — intentional changes should update these numbers deliberately.
27const golden: Record<string, { verts: number }> = {
28 tree: { verts: 1830 },
29 pine: { verts: 840 },
30 palm: { verts: 3630 },
31 bush: { verts: 3600 },
32 grass: { verts: 1632 },
33 fern: { verts: 13932 },
34 flower: { verts: 1050 },
35 dead: { verts: 1400 },
36 leafy: { verts: 7020 },
37 rock: { verts: 1440 },
38 sharp: { verts: 696 },
39 block: { verts: 432 },
40 mushroom: { verts: 1584 },
41}
42
43function allFinite(m: Mesh | Asset): boolean {
44 const p = m.positions
45 for (let i = 0; i < p.length; i++) if (!Number.isFinite(p[i])) return false
46 return true
47}
48
49describe.each(generators)('$name generator', ({ name, gen }) => {
50 it('produces a valid, colored, finite mesh', () => {
51 const m = gen({ seed: 1 })
52 expect(m.vertexCount).toBeGreaterThan(0)
53 expect(m.colors).not.toBeNull()
54 expect(allFinite(m)).toBe(true)
55 })
56
57 it('matches the golden vertex count for seed 1', () => {
58 expect(gen({ seed: 1 }).vertexCount).toBe(golden[name].verts)
59 })
60
61 it('is deterministic: same seed → identical positions', () => {
62 const a = gen({ seed: 7 }).positions
63 const b = gen({ seed: 7 }).positions
64 expect(Array.from(a)).toEqual(Array.from(b))
65 })
66
67 it('different seeds produce different geometry', () => {
68 const a = gen({ seed: 1 }).positions
69 const b = gen({ seed: 2 }).positions
70 expect(Array.from(a)).not.toEqual(Array.from(b))
71 })
72})
73
74describe('stream independence at the model level', () => {
75 // The headline benefit of named streams: a feature that only affects color (snow)
76 // must not perturb geometry, because positions come from independent streams.
77 it.each(snowGenerators)('$name: painted snow (depth 0) leaves geometry byte-identical', ({ gen }) => {
78 const bare = gen({ seed: 3, snowColors: [] })
79 const snowy = gen({ seed: 3, snowColors: ['#ffffff', '#eeeeee'] })
80
81 // Same geometry...
82 expect(Array.from(snowy.positions)).toEqual(Array.from(bare.positions))
83 // ...but different coloring.
84 expect(Array.from(snowy.colors!)).not.toEqual(Array.from(bare.colors!))
85 })
86})
87
88describe('geometric snow (snowDepth > 0)', () => {
89 it.each(snowGenerators)('$name: adds a snow layer of real geometry', ({ gen }) => {
90 const bare = gen({ seed: 4, snowColors: [] })
91 const snowy = gen({ seed: 4, snowColors: ['#eef0f5'], snowDepth: 0.08 })
92 // The snow shell adds vertices on top of the model.
93 expect(snowy.vertexCount).toBeGreaterThan(bare.vertexCount)
94 // And reaches higher than the bare model (snow sits on top).
95 expect(snowy.boundingBox.max.y).toBeGreaterThan(bare.boundingBox.max.y)
96 })
97
98 it.each(snowGenerators)('$name: geometric snow is deterministic', ({ gen }) => {
99 const a = gen({ seed: 4, snowColors: ['#eef0f5'], snowDepth: 0.08 }).positions
100 const b = gen({ seed: 4, snowColors: ['#eef0f5'], snowDepth: 0.08 }).positions
101 expect(Array.from(a)).toEqual(Array.from(b))
102 })
103})