[READ-ONLY] Mirror of https://github.com/flo-bit/shapecraft.
flo-bit.dev/shapecraft/
4.7 kB
106 lines
1import { describe, it, expect } from 'vitest'
2import { lowpoly, ghibli, styles, resolveStyle, stylePalette, styleMaterial } from '../src/style'
3import { resolveOptions } from '../src/core/schema'
4import { tree, treeSchema } from '../src/generators'
5import type { Asset } from '../src/core/asset'
6
7describe('style profiles', () => {
8 it('resolveStyle defaults to lowpoly and accepts names or profiles', () => {
9 expect(resolveStyle()).toBe(lowpoly)
10 expect(resolveStyle('ghibli')).toBe(ghibli)
11 expect(resolveStyle(ghibli)).toBe(ghibli)
12 expect(() => resolveStyle('vaporwave')).toThrow(/Unknown style/)
13 })
14
15 it('every registered style covers the core semantic roles', () => {
16 for (const style of Object.values(styles)) {
17 for (const role of ['bark', 'leaf', 'snow', 'rock', 'wood']) {
18 expect(stylePalette(style, role).length, `${style.name}/${role}`).toBeGreaterThan(0)
19 }
20 }
21 })
22
23 it('styleMaterial follows the shading model', () => {
24 expect(styleMaterial(lowpoly)).toEqual({ vertexColors: true, flatShading: true })
25 expect(styleMaterial(ghibli)).toEqual({ vertexColors: true, flatShading: false })
26 })
27})
28
29describe('style resolution through the schema', () => {
30 it('role-tagged color options take the style palette as default', () => {
31 const o = resolveOptions(treeSchema, {}, undefined, () => 0.5, ghibli)
32 expect(o.trunkColors).toEqual(ghibli.palettes.bark)
33 expect(o.canopyColors).toEqual(ghibli.palettes.leaf)
34 })
35
36 it('without a style, schema defaults stand', () => {
37 const o = resolveOptions(treeSchema, {}, undefined, () => 0.5)
38 expect(o.canopyColors).toEqual(treeSchema.canopyColors.default)
39 })
40
41 it('presets and explicit overrides beat the style palette', () => {
42 const presets = { autumn: { canopyColors: ['#c44422'] } }
43 const fromPreset = resolveOptions(treeSchema, { preset: 'autumn' }, presets, () => 0.5, ghibli)
44 expect(fromPreset.canopyColors).toEqual(['#c44422'])
45 const fromOverride = resolveOptions(treeSchema, { canopyColors: ['#123456'] }, presets, () => 0.5, ghibli)
46 expect(fromOverride.canopyColors).toEqual(['#123456'])
47 })
48
49 it('style palettes are copied, not shared', () => {
50 const o = resolveOptions(treeSchema, {}, undefined, () => 0.5, ghibli)
51 ;(o.canopyColors as string[]).push('#000000')
52 expect(ghibli.palettes.leaf).toHaveLength(4)
53 })
54})
55
56describe('styled tree generator', () => {
57 function partsOf(asset: Asset): Record<string, Asset> {
58 const out: Record<string, Asset> = {}
59 for (const c of asset.children) out[c.name] = c
60 return out
61 }
62
63 it('default style produces the unchanged lowpoly tree (golden parity)', () => {
64 const plain = tree({ seed: 5 })
65 const styled = tree({ seed: 5, style: 'lowpoly' })
66 const a = partsOf(plain).canopy.geometry!.geometry.getAttribute('position')
67 const b = partsOf(styled).canopy.geometry!.geometry.getAttribute('position')
68 expect(b.array).toEqual(a.array)
69 expect(partsOf(plain).trunk.material?.flatShading).toBe(true)
70 })
71
72 it('ghibli tree differs in geometry, shading, and palette', () => {
73 const low = tree({ seed: 5 })
74 const ghib = tree({ seed: 5, style: 'ghibli' })
75 const lowCanopy = partsOf(low).canopy
76 const ghibCanopy = partsOf(ghib).canopy
77
78 expect(ghibCanopy.material?.flatShading).toBe(false)
79 // Finer detail multiplier → more triangles (ghibli is indexed, lowpoly is facet soup)
80 const ghibTris = ghibCanopy.geometry!.geometry.getIndex()!.count / 3
81 const lowTris = lowCanopy.geometry!.geometry.getAttribute('position').count / 3
82 expect(ghibTris).toBeGreaterThan(lowTris)
83 // Gradient path keeps geometry welded/indexed (faceted path un-indexes)
84 expect(ghibCanopy.geometry!.geometry.getIndex()).not.toBeNull()
85 expect(lowCanopy.geometry!.geometry.getIndex()).toBeNull()
86 })
87
88 it('ghibli is deterministic per seed', () => {
89 const a = tree({ seed: 9, style: 'ghibli' })
90 const b = tree({ seed: 9, style: 'ghibli' })
91 const pa = partsOf(a).canopy.geometry!.geometry.getAttribute('position')
92 const pb = partsOf(b).canopy.geometry!.geometry.getAttribute('position')
93 expect(pb.array).toEqual(pa.array)
94 })
95
96 it('user colors still win under a style', () => {
97 const t = tree({ seed: 3, style: 'ghibli', preset: 'autumn' })
98 // Autumn preset canopy must survive the ghibli palette (orange-ish, not green)
99 const colors = partsOf(t).canopy.geometry!.geometry.getAttribute('color')
100 let reds = 0
101 for (let i = 0; i < colors.count; i++) {
102 if (colors.getX(i) > colors.getY(i)) reds++
103 }
104 expect(reds / colors.count).toBeGreaterThan(0.5)
105 })
106})