diff --git a/packages/adapter-mesh/src/index.ts b/packages/adapter-mesh/src/index.ts index af0a482..3fb8edb 100644 --- a/packages/adapter-mesh/src/index.ts +++ b/packages/adapter-mesh/src/index.ts @@ -53,6 +53,20 @@ function parseObj(source: string): Triangle[] { return triangles; } +function computeMeshBounds(triangles: Triangle[]): { min: Point3; max: Point3 } { + const min: Point3 = [Infinity, Infinity, Infinity]; + const max: Point3 = [-Infinity, -Infinity, -Infinity]; + for (const triangle of triangles) { + for (const vertex of triangle) { + for (let axis = 0; axis < 3; axis += 1) { + min[axis] = Math.min(min[axis], vertex[axis]); + max[axis] = Math.max(max[axis], vertex[axis]); + } + } + } + return { min, max }; +} + function cross(a: Point3, b: Point3): Point3 { return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; } @@ -114,9 +128,7 @@ export function objToVoxelPlan(source: string, options: ObjToVoxelPlanOptions = if (mode !== "surface" && mode !== "solid") fail("voxelMode must be surface or solid"); const block = validateBlock(options.defaultBlock ?? "minecraft:stone"); const parsed = parseObj(source); - const rawVertices = parsed.flat(); - const min: Point3 = [0, 1, 2].map((axis) => Math.min(...rawVertices.map((v) => v[axis]))) as Point3; - const max: Point3 = [0, 1, 2].map((axis) => Math.max(...rawVertices.map((v) => v[axis]))) as Point3; + const { min, max } = computeMeshBounds(parsed); const extents = subtract(max, min); if (extents.some((extent) => extent <= EPSILON)) fail("mesh bounds must have non-zero width, height and depth"); const scale = targetHeight / extents[1]; diff --git a/packages/adapter-mesh/test/adapter-mesh.test.ts b/packages/adapter-mesh/test/adapter-mesh.test.ts index 5eb075f..971d289 100644 --- a/packages/adapter-mesh/test/adapter-mesh.test.ts +++ b/packages/adapter-mesh/test/adapter-mesh.test.ts @@ -54,4 +54,21 @@ describe("OBJ to VoxelPlan", () => { expect(solid.blocks.length).toBe(64); expect(() => objToVoxelPlan(source("gabled-cabin"), { targetHeight: 64, maxBlocks: 100 })).toThrow(/exceed maxBlocks/); }); + + it("computes bounds for large repeated face references without argument overflow", () => { + const repeatedFaceCount = 50_000; + const input = [ + "v 0 0 0", + "v 1 0 0", + "v 0 1 1", + ...Array.from({ length: repeatedFaceCount }, () => "f 1 2 3"), + ].join("\n"); + + const plan = objToVoxelPlan(input, { targetHeight: 1, maxBlocks: 1, voxelMode: "surface" }); + + expect(plan.size).toEqual([1, 1, 1]); + expect(plan.blocks).toHaveLength(1); + expect(plan.blocks[0].pos).toEqual([0, 0, 0]); + expect(plan.blocks.every(({ pos }) => pos.every((value, axis) => value >= 0 && value < plan.size[axis]))).toBe(true); + }); });