From cafc90e5db5f47fc3d90b756b102d69194fc3c08 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Wed, 13 May 2026 13:55:33 +0200 Subject: [PATCH] auto-git: [change] tests/domain/terrains.test.ts --- tests/domain/terrains.test.ts | 316 +++++++++++++++++++++++++++------- 1 file changed, 254 insertions(+), 62 deletions(-) diff --git a/tests/domain/terrains.test.ts b/tests/domain/terrains.test.ts index e5a3f17e..7989c371 100644 --- a/tests/domain/terrains.test.ts +++ b/tests/domain/terrains.test.ts @@ -7,9 +7,13 @@ import { getTerrainFoliageBlockerMaskValueAtSample, getTerrainFoliageMask, getTerrainFoliageMaskValueAtSample, + getTerrainFootprintDepth, + getTerrainFootprintWidth, + getTerrainHeightAtSample, getTerrainPaintWeightSampleOffset, createTerrainWithAddedLayer, createTerrainWithRemovedLayer, + sampleTerrainHeightAtLocalPosition, sampleTerrainFoliageBlockerMaskAtWorldPosition, sampleTerrainFoliageMaskAtWorldPosition, updateTerrainBoundsCacheAfterHeightPatch, @@ -17,80 +21,268 @@ import { createTerrain } from "../../src/document/terrains"; +function createIndexedHeights( + sampleCountX: number, + sampleCountZ: number +): number[] { + return Array.from({ length: sampleCountX * sampleCountZ }, (_, index) => { + const sampleX = index % sampleCountX; + const sampleZ = Math.floor(index / sampleCountX); + + return sampleX + sampleZ * 10; + }); +} + +function createIndexedPaintWeights( + sampleCountX: number, + sampleCountZ: number +): number[] { + const paintWeights: number[] = []; + + for (let sampleZ = 0; sampleZ < sampleCountZ; sampleZ += 1) { + for (let sampleX = 0; sampleX < sampleCountX; sampleX += 1) { + paintWeights.push( + sampleX * 0.01 + sampleZ * 0.02, + sampleX * 0.03, + sampleZ * 0.04 + ); + } + } + + return paintWeights; +} + +function createIndexedMaskValues( + sampleCountX: number, + sampleCountZ: number +): number[] { + const sampleCount = sampleCountX * sampleCountZ; + + return Array.from({ length: sampleCount }, (_, index) => index / sampleCount); +} + describe("terrain grid resizing", () => { - it("keeps terrain centered while bilinearly resampling heights and paint weights", () => { - const foliageLayerId = "foliage-layer-resample"; + it("changes Cell Size from 1m to 0.5m by increasing resolution and preserving footprint", () => { const terrain = createTerrain({ - id: "terrain-resample-main", - position: { - x: 0, - y: 1, - z: 0 - }, - sampleCountX: 2, - sampleCountZ: 2, + id: "terrain-cell-size-resolution", + sampleCountX: 9, + sampleCountZ: 9, cellSize: 1, - heights: [0, 2, 4, 6], - paintWeights: [ - 0, - 0, - 0, - 0.6, - 0, - 0, - 0, - 0.5, - 0, - 0, - 0, - 0.25 - ], - foliageMasks: { - [foliageLayerId]: createTerrainFoliageMask({ - layerId: foliageLayerId, - resolutionX: 2, - resolutionZ: 2, - values: [0, 1, 0, 0.5] - }) - }, - foliageBlockerMask: createTerrainFoliageBlockerMask({ - resolutionX: 2, - resolutionZ: 2, - values: [0, 0.25, 0.5, 1] - }) + heights: createIndexedHeights(9, 9) + }); + + const resizedTerrain = resizeTerrainGrid(terrain, { + sampleCountX: terrain.sampleCountX, + sampleCountZ: terrain.sampleCountZ, + cellSize: 0.5 + }); + + expect(resizedTerrain.sampleCountX).toBe(17); + expect(resizedTerrain.sampleCountZ).toBe(17); + expect(resizedTerrain.cellSize).toBe(0.5); + expect(getTerrainFootprintWidth(resizedTerrain)).toBe(8); + expect(getTerrainFootprintDepth(resizedTerrain)).toBe(8); + expect(resizedTerrain.position).toEqual(terrain.position); + }); + + it("preserves terrain heights at representative local positions when Cell Size changes", () => { + const terrain = createTerrain({ + id: "terrain-cell-size-height-preserve", + sampleCountX: 9, + sampleCountZ: 9, + cellSize: 1, + heights: createIndexedHeights(9, 9) + }); + + const resizedTerrain = resizeTerrainGrid(terrain, { + sampleCountX: terrain.sampleCountX, + sampleCountZ: terrain.sampleCountZ, + cellSize: 0.5 + }); + + expect(sampleTerrainHeightAtLocalPosition(resizedTerrain, 2.5, 3.5)).toBeCloseTo( + sampleTerrainHeightAtLocalPosition(terrain, 2.5, 3.5) ?? 0 + ); + expect(sampleTerrainHeightAtLocalPosition(resizedTerrain, 6, 1)).toBeCloseTo( + sampleTerrainHeightAtLocalPosition(terrain, 6, 1) ?? 0 + ); + }); + + it("preserves paint weights at representative local positions when Cell Size changes", () => { + const terrain = createTerrain({ + id: "terrain-cell-size-paint-preserve", + sampleCountX: 9, + sampleCountZ: 9, + cellSize: 1, + paintWeights: createIndexedPaintWeights(9, 9) + }); + + const resizedTerrain = resizeTerrainGrid(terrain, { + sampleCountX: terrain.sampleCountX, + sampleCountZ: terrain.sampleCountZ, + cellSize: 0.5 + }); + const originalOffset = getTerrainPaintWeightSampleOffset(terrain, 4, 2); + const resizedOffset = getTerrainPaintWeightSampleOffset(resizedTerrain, 8, 4); + + expect(resizedTerrain.paintWeights[resizedOffset]).toBeCloseTo( + terrain.paintWeights[originalOffset] ?? 0 + ); + expect(resizedTerrain.paintWeights[resizedOffset + 1]).toBeCloseTo( + terrain.paintWeights[originalOffset + 1] ?? 0 + ); + expect(resizedTerrain.paintWeights[resizedOffset + 2]).toBeCloseTo( + terrain.paintWeights[originalOffset + 2] ?? 0 + ); + }); + + it("increases Samples X/Z by extending terrain instead of stretching existing data", () => { + const terrain = createTerrain({ + id: "terrain-samples-extend", + sampleCountX: 3, + sampleCountZ: 3, + cellSize: 1, + heights: createIndexedHeights(3, 3) + }); + + const resizedTerrain = resizeTerrainGrid(terrain, { + sampleCountX: 5, + sampleCountZ: 5, + cellSize: terrain.cellSize + }); + + expect(resizedTerrain.position).toEqual(terrain.position); + expect(resizedTerrain.cellSize).toBe(1); + expect(getTerrainFootprintWidth(resizedTerrain)).toBe(4); + expect(getTerrainFootprintDepth(resizedTerrain)).toBe(4); + expect(getTerrainHeightAtSample(resizedTerrain, 0, 0)).toBe(0); + expect(getTerrainHeightAtSample(resizedTerrain, 1, 1)).toBe(11); + expect(getTerrainHeightAtSample(resizedTerrain, 2, 2)).toBe(22); + }); + + it("decreases Samples X/Z by cropping terrain instead of squashing existing data", () => { + const terrain = createTerrain({ + id: "terrain-samples-crop", + sampleCountX: 5, + sampleCountZ: 5, + cellSize: 1, + heights: createIndexedHeights(5, 5) }); const resizedTerrain = resizeTerrainGrid(terrain, { sampleCountX: 3, sampleCountZ: 3, - cellSize: 1 + cellSize: terrain.cellSize }); - const centerOffset = getTerrainPaintWeightSampleOffset(resizedTerrain, 1, 1); - const resizedMask = getTerrainFoliageMask(resizedTerrain, foliageLayerId); - expect(resizedTerrain.position).toEqual({ - x: -0.5, - y: 1, - z: -0.5 + expect(resizedTerrain.position).toEqual(terrain.position); + expect(resizedTerrain.cellSize).toBe(1); + expect(getTerrainFootprintWidth(resizedTerrain)).toBe(2); + expect(getTerrainFootprintDepth(resizedTerrain)).toBe(2); + expect(getTerrainHeightAtSample(resizedTerrain, 0, 0)).toBe(0); + expect(getTerrainHeightAtSample(resizedTerrain, 1, 1)).toBe(11); + expect(getTerrainHeightAtSample(resizedTerrain, 2, 2)).toBe(22); + }); + + it("fills newly extended height samples from the nearest existing edge height", () => { + const terrain = createTerrain({ + id: "terrain-samples-edge-fill", + sampleCountX: 3, + sampleCountZ: 3, + cellSize: 1, + heights: createIndexedHeights(3, 3) }); - expect(resizedTerrain.sampleCountX).toBe(3); - expect(resizedTerrain.sampleCountZ).toBe(3); - expect(resizedTerrain.heights[4]).toBeCloseTo(3); - expect(resizedTerrain.paintWeights[centerOffset]).toBeCloseTo(0.15); - expect(resizedTerrain.paintWeights[centerOffset + 1]).toBeCloseTo(0.125); - expect(resizedTerrain.paintWeights[centerOffset + 2]).toBeCloseTo(0.0625); - expect(resizedMask?.resolutionX).toBe(3); - expect(resizedMask?.resolutionZ).toBe(3); - expect(resizedMask?.values[4]).toBeCloseTo(0.375); - expect(resizedTerrain.foliageBlockerMask.resolutionX).toBe(3); - expect(resizedTerrain.foliageBlockerMask.resolutionZ).toBe(3); - expect(resizedTerrain.foliageBlockerMask.values[4]).toBeCloseTo(0.4375); - expect(terrain.position).toEqual({ - x: 0, - y: 1, - z: 0 + + const resizedTerrain = resizeTerrainGrid(terrain, { + sampleCountX: 5, + sampleCountZ: 5, + cellSize: terrain.cellSize }); - expect(terrain.heights).toEqual([0, 2, 4, 6]); + + expect(getTerrainHeightAtSample(resizedTerrain, 4, 1)).toBe(12); + expect(getTerrainHeightAtSample(resizedTerrain, 1, 4)).toBe(21); + expect(getTerrainHeightAtSample(resizedTerrain, 4, 4)).toBe(22); + }); + + it("keeps foliage masks and blocker masks valid after cell-size and sample-count edits", () => { + const foliageLayerId = "foliage-layer-resize-valid"; + const terrain = createTerrain({ + id: "terrain-masks-resize-valid", + sampleCountX: 3, + sampleCountZ: 3, + cellSize: 1, + foliageMasks: { + [foliageLayerId]: createTerrainFoliageMask({ + layerId: foliageLayerId, + resolutionX: 3, + resolutionZ: 3, + values: createIndexedMaskValues(3, 3) + }) + }, + foliageBlockerMask: createTerrainFoliageBlockerMask({ + resolutionX: 3, + resolutionZ: 3, + values: createIndexedMaskValues(3, 3) + }) + }); + + const resolutionChangedTerrain = resizeTerrainGrid(terrain, { + sampleCountX: terrain.sampleCountX, + sampleCountZ: terrain.sampleCountZ, + cellSize: 0.5 + }); + const extendedTerrain = resizeTerrainGrid(resolutionChangedTerrain, { + sampleCountX: 6, + sampleCountZ: 4, + cellSize: resolutionChangedTerrain.cellSize + }); + const resizedMask = getTerrainFoliageMask(extendedTerrain, foliageLayerId); + + expect(resizedMask?.resolutionX).toBe(extendedTerrain.sampleCountX); + expect(resizedMask?.resolutionZ).toBe(extendedTerrain.sampleCountZ); + expect(resizedMask?.values).toHaveLength( + extendedTerrain.sampleCountX * extendedTerrain.sampleCountZ + ); + expect(extendedTerrain.foliageBlockerMask.resolutionX).toBe( + extendedTerrain.sampleCountX + ); + expect(extendedTerrain.foliageBlockerMask.resolutionZ).toBe( + extendedTerrain.sampleCountZ + ); + expect(extendedTerrain.foliageBlockerMask.values).toHaveLength( + extendedTerrain.sampleCountX * extendedTerrain.sampleCountZ + ); + expect(resizedMask?.values.every((value) => Number.isFinite(value))).toBe( + true + ); + expect( + extendedTerrain.foliageBlockerMask.values.every((value) => + Number.isFinite(value) + ) + ).toBe(true); + }); + + it("chooses deterministic sample counts for non-divisible footprint and cell-size edits", () => { + const terrain = createTerrain({ + id: "terrain-cell-size-non-divisible", + sampleCountX: 9, + sampleCountZ: 9, + cellSize: 1, + heights: createIndexedHeights(9, 9) + }); + + const resizedTerrain = resizeTerrainGrid(terrain, { + sampleCountX: terrain.sampleCountX, + sampleCountZ: terrain.sampleCountZ, + cellSize: 3 + }); + + expect(resizedTerrain.cellSize).toBe(3); + expect(resizedTerrain.sampleCountX).toBe(4); + expect(resizedTerrain.sampleCountZ).toBe(4); + expect(getTerrainFootprintWidth(resizedTerrain)).toBe(9); + expect(getTerrainFootprintDepth(resizedTerrain)).toBe(9); + expect(resizedTerrain.position).toEqual(terrain.position); }); });