feat(mesh): add tile mesh generation package
Add @tts/mesh, a package for generating extruded tile meshes from 2D shapes. It provides shape generators (rect, hex, circle, rounded rect, frame), earcut-based tessellation of top/bottom faces with UVs, and outward-normal wall generation, combined into raw BufferGeometry-ready typed arrays.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
circleShape,
|
||||
frameShape,
|
||||
hexShape,
|
||||
polygonShape,
|
||||
rectShape,
|
||||
roundedRectShape,
|
||||
shapeFromThree,
|
||||
signedArea,
|
||||
} from './shapes.js';
|
||||
|
||||
describe('shape generators', () => {
|
||||
it('rectShape is a CCW unit-ish rectangle', () => {
|
||||
const s = rectShape(2, 4);
|
||||
expect(s.outline).toEqual([
|
||||
[-1, -2],
|
||||
[1, -2],
|
||||
[1, 2],
|
||||
[-1, 2],
|
||||
]);
|
||||
expect(signedArea(s.outline)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('polygonShape produces a regular polygon with the right vertex count', () => {
|
||||
const s = polygonShape(6, 2);
|
||||
expect(s.outline).toHaveLength(6);
|
||||
// All vertices on the circle of radius 2.
|
||||
for (const [x, y] of s.outline) {
|
||||
expect(Math.hypot(x!, y!)).toBeCloseTo(2, 5);
|
||||
}
|
||||
expect(signedArea(s.outline)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('hexShape is a pointy-top hexagon', () => {
|
||||
const s = hexShape(1);
|
||||
expect(s.outline).toHaveLength(6);
|
||||
// Pointy-top: a vertex sits at the top (angle 90deg, max y).
|
||||
const top = s.outline.reduce((a, b) => (b[1]! > a[1]! ? b : a));
|
||||
expect(top[0]).toBeCloseTo(0, 5);
|
||||
expect(top[1]).toBeCloseTo(1, 5);
|
||||
});
|
||||
|
||||
it('circleShape approximates a circle', () => {
|
||||
const s = circleShape(3, 64);
|
||||
expect(s.outline).toHaveLength(64);
|
||||
for (const [x, y] of s.outline) {
|
||||
expect(Math.hypot(x!, y!)).toBeCloseTo(3, 5);
|
||||
}
|
||||
});
|
||||
|
||||
it('roundedRectShape clamps the corner radius', () => {
|
||||
const s = roundedRectShape(2, 2, 5);
|
||||
// Radius clamped to half the smaller dimension (1).
|
||||
const minX = Math.min(...s.outline.map((p) => p[0]!));
|
||||
const maxX = Math.max(...s.outline.map((p) => p[0]!));
|
||||
expect(minX).toBeCloseTo(-1, 5);
|
||||
expect(maxX).toBeCloseTo(1, 5);
|
||||
expect(s.outline.length).toBe(4 * 4); // 4 corners * 4 segments
|
||||
});
|
||||
|
||||
it('frameShape has a hole wound CW', () => {
|
||||
const s = frameShape(4, 4, 2);
|
||||
expect(s.outline).toHaveLength(4);
|
||||
expect(s.holes).toHaveLength(1);
|
||||
expect(signedArea(s.outline)).toBeGreaterThan(0);
|
||||
expect(signedArea(s.holes![0]!)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('shapeFromThree converts three.js vectors', () => {
|
||||
const fake = {
|
||||
getPoints: () => [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1, y: 0 },
|
||||
{ x: 1, y: 1 },
|
||||
],
|
||||
getPointsHoles: () => [],
|
||||
};
|
||||
const s = shapeFromThree(fake);
|
||||
expect(s.outline).toEqual([
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
[1, 1],
|
||||
]);
|
||||
expect(s.holes).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user