[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.

shapecraft / tests / style.test.ts
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})