Something went wrong. Try again.
A three.js based web UI to create and export vases using code.
Something went wrong. Try again.
5.3 kB · 142 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143import { describe, expect, it } from 'vitest'
import { runShape } from './runner.ts'import { seedShapes } from '../shapes/seed.ts'import type { Shape } from '../geometry/types.ts'
function snapPositions(shape: Shape): number[][] { const out: number[][] = [] for (let i = 0; i < shape.positions.length; i += 3) { out.push([ Math.round(shape.positions[i] * 1e4) / 1e4, Math.round(shape.positions[i + 1] * 1e4) / 1e4, Math.round(shape.positions[i + 2] * 1e4) / 1e4, ]) } return out}
/** Geometric watertightness: every undirected edge (by position, not index) is used by 2 triangles. */function expectWatertight(shape: Shape): void { const positions = snapPositions(shape) const counts = new Map<string, number>() for (let i = 0; i < shape.indices.length; i += 3) { const a = shape.indices[i] const b = shape.indices[i + 1] const c = shape.indices[i + 2] for (const [u, v] of [ [a, b], [b, c], [c, a], ]) { const pu = positions[u].join(',') const pv = positions[v].join(',') const key = pu < pv ? `${pu}|${pv}` : `${pv}|${pu}` counts.set(key, (counts.get(key) ?? 0) + 1) } } const bad = [...counts.values()].filter((c) => c !== 2) expect(bad.length, `non-watertight edges: ${bad.slice(0, 10)}`).toBe(0)}
function shapeSize(shape: Shape): { width: number; depth: number; height: number } { const pos = shape.positions let minX = Infinity let minY = Infinity let minZ = Infinity let maxX = -Infinity let maxY = -Infinity let maxZ = -Infinity for (let i = 0; i < pos.length; i += 3) { if (pos[i] < minX) minX = pos[i] if (pos[i + 1] < minY) minY = pos[i + 1] if (pos[i + 2] < minZ) minZ = pos[i + 2] if (pos[i] > maxX) maxX = pos[i] if (pos[i + 1] > maxY) maxY = pos[i + 1] if (pos[i + 2] > maxZ) maxZ = pos[i + 2] } return { width: maxX - minX, depth: maxY - minY, height: maxZ - minZ }}
const spiral = seedShapes.find((s) => s.name === 'spiral_lamp_shade')!const wavy = seedShapes.find((s) => s.name === 'wavy_lamp_shade')!const vase = seedShapes.find((s) => s.name === 'fluted_vase')!
describe('runner', () => { it('runs the spiral shape and produces a watertight solid', () => { const result = runShape(spiral.source) expect(result.error).toBeNull() expect(result.shape).not.toBeNull() const shape = result.shape! expect(shape.indices.length / 3).toBeGreaterThan(10_000) expectWatertight(shape) })
it('declares parameters in order', () => { const result = runShape(spiral.source) expect(result.declared[0]).toBe('total_sections') expect(result.declared).toContain('star_points') expect(result.params.total_height).toBe(240) })
it('applies overrides before execution', () => { const result = runShape(wavy.source, { overrides: { fin_count: 12 } }) expect(result.params.fin_count).toBe(12) })
it('keeps params ordered by declaration even when a middle param is overridden', () => { const src = 'setParams({ a: 1, b: 2, c: 3 })' const result = runShape(src, { overrides: { b: 99 } }) expect(Object.keys(result.params)).toEqual(['a', 'b', 'c']) expect(result.params.b).toBe(99) expect(result.params.a).toBe(1) expect(result.params.c).toBe(3) })
it('runs the wavy shape in fast mode (merged, non-watertight is fine)', () => { const result = runShape(wavy.source) expect(result.error).toBeNull() expect(result.shape).not.toBeNull() expect(result.shape!.indices.length / 3).toBeGreaterThan(10_000) })
it('runs exact CSG on a reduced wavy shape', () => { const result = runShape(wavy.source, { overrides: { fin_count: 8 }, mode: 'exact' }) expect(result.error).toBeNull() expect(result.shape).not.toBeNull() const shape = result.shape! expect(shape.kind).toBe('solid') expect(shape.indices.length / 3).toBeGreaterThan(1000) // The union is watertight; the socket cut on the wavy body may leave minor // open edges, so we only assert solidity here. })
it('runs the fluted vase (hollowed by a boolean cut) and stays watertight', () => { const result = runShape(vase.source, { overrides: { flutes: 16, total_height: 160 } }) expect(result.error).toBeNull() expect(result.shape).not.toBeNull() const shape = result.shape! expect(shape.kind).toBe('solid') expect(shape.indices.length / 3).toBeGreaterThan(5000) expectWatertight(shape) })
it('reports syntax errors', () => { const result = runShape('setParams({ total_height: 200, }) const = 5') expect(result.error).not.toBeNull() expect(result.shape).toBeNull() })
it('seed shapes fit a default 22 × 22 × 25 cm print bed', () => { // Printer bed limits (mm) matching the app's Print check defaults. const bed = { width: 220, depth: 220, height: 250 } for (const seed of seedShapes) { const result = runShape(seed.source) expect(result.error).toBeNull() const size = shapeSize(result.shape!) expect(size.width, `${seed.name} width`).toBeLessThanOrEqual(bed.width) expect(size.depth, `${seed.name} depth`).toBeLessThanOrEqual(bed.depth) expect(size.height, `${seed.name} height`).toBeLessThanOrEqual(bed.height) } })})