feat(mesh): support uvBounds and planar back-face UVs

Add an optional uvBounds to extrudeShape/extrudeShapeParts so UVs can be derived from a framing rectangle instead of the shape's bounding box, letting a traced silhouette align with its full image. The bottom face now uses the same planar xy UVs as the top face (no horizontal mirror), so the texture is consistent across front, back, and walls.
This commit is contained in:
2026-08-08 15:12:07 +08:00
parent dc8127dba1
commit 21fc1b29c1
6 changed files with 146 additions and 43 deletions
+22 -4
View File
@@ -72,18 +72,18 @@ describe('capFaces', () => {
expect(uvs[5]).toBeCloseTo(1, 5);
});
it('bottom face UVs are a left/right flip of the top face', () => {
it('bottom face UVs match the top face (planar xy, no mirror)', () => {
const face = capFaces(rectShape(2, 2), 1);
const outlineCount = 4;
// Top face: outline point i maps to ((x-minX)/spanX, (y-minY)/spanY).
// Bottom face mirrors U (1 - u) but keeps V, so the texture reads
// correctly from underneath.
// Bottom face uses the same planar xy mapping (no horizontal mirror), so
// the texture is consistent across front, back, and walls.
for (let i = 0; i < outlineCount; i++) {
const topU = face.uvs[i * 2]!;
const topV = face.uvs[i * 2 + 1]!;
const bottomU = face.uvs[(outlineCount + i) * 2]!;
const bottomV = face.uvs[(outlineCount + i) * 2 + 1]!;
expect(bottomU).toBeCloseTo(1 - topU, 5);
expect(bottomU).toBeCloseTo(topU, 5);
expect(bottomV).toBeCloseTo(topV, 5);
}
});
@@ -93,6 +93,24 @@ describe('capFaces', () => {
const topIndices = face.indices.slice(0, 6);
expect(fanArea(face.positions, topIndices)).toBeGreaterThan(0);
});
it('derives UVs from uvBounds instead of the shape bbox', () => {
// A 1x1 shape centered at origin inside a 4x4 framing (0..4): UVs should
// span -0.125..0.125, not 0..1.
const face = capFaces(rectShape(1, 1), 1, 1, {
minX: 0,
minY: 0,
maxX: 4,
maxY: 4,
});
const outlineCount = 4;
// Bottom-left vertex at (-0.5,-0.5) -> u=(0.5-0)/4=-0.125, v=-0.125.
expect(face.uvs[0]).toBeCloseTo(-0.125, 5);
expect(face.uvs[1]).toBeCloseTo(-0.125, 5);
// Top-left vertex at (-0.5,0.5) -> u=-0.125, v=0.125.
expect(face.uvs[(outlineCount - 1) * 2]).toBeCloseTo(-0.125, 5);
expect(face.uvs[(outlineCount - 1) * 2 + 1]).toBeCloseTo(0.125, 5);
});
});
function hexOutline(): number[][] {