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