diff --git a/src/runtime-three/runtime-scene-build.ts b/src/runtime-three/runtime-scene-build.ts index 068183b9..2d7744d6 100644 --- a/src/runtime-three/runtime-scene-build.ts +++ b/src/runtime-three/runtime-scene-build.ts @@ -891,12 +891,12 @@ function buildRuntimeTerrain( sampleCountX: terrain.sampleCountX, sampleCountZ: terrain.sampleCountZ, cellSize: terrain.cellSize, - heights: [...terrain.heights], + heights: terrain.heights, layers: terrain.layers.map((layer) => ({ materialId: layer.materialId, material: resolveRuntimeMaterial(document, layer.materialId) })), - paintWeights: [...terrain.paintWeights] + paintWeights: terrain.paintWeights }; } diff --git a/src/viewport-three/viewport-host.ts b/src/viewport-three/viewport-host.ts index c72c8258..c9e652e6 100644 --- a/src/viewport-three/viewport-host.ts +++ b/src/viewport-three/viewport-host.ts @@ -9701,7 +9701,7 @@ export class ViewportHost { const committed = this.terrainBrushCommitHandler?.({ - terrain: cloneTerrain(finalPreviewTerrain), + terrain: finalPreviewTerrain, commandLabel: getTerrainBrushCommandLabel(toolState.tool), tool: toolState.tool }) === true; diff --git a/tests/geometry/terrain-brush.test.ts b/tests/geometry/terrain-brush.test.ts index 08833f48..def4823c 100644 --- a/tests/geometry/terrain-brush.test.ts +++ b/tests/geometry/terrain-brush.test.ts @@ -6,6 +6,7 @@ import { getTerrainSampleLayerWeights } from "../../src/document/terrains"; import { + applyTerrainBrushStampInPlace, applyTerrainBrushStamp, getTerrainBrushWeight, sampleTerrainHeightAtWorldPosition @@ -112,6 +113,41 @@ describe("terrain brush geometry", () => { expect(getTerrainBrushWeight(2, 1, 0.5)).toBe(0); }); + it("can stamp terrain in place and report dirty sample bounds", () => { + const terrain = createTerrain({ + id: "terrain-stamp-in-place", + position: { x: 0, y: 0, z: 0 }, + sampleCountX: 5, + sampleCountZ: 5, + cellSize: 1, + heights: new Array(25).fill(0) + }); + const originalHeights = terrain.heights; + const originalPaintWeights = terrain.paintWeights; + + const result = applyTerrainBrushStampInPlace({ + terrain, + center: { x: 2, z: 2 }, + settings: { + radius: 0.6, + strength: 0.5, + falloff: 0 + }, + tool: "raise" + }); + + expect(result.changed).toBe(true); + expect(result.dirtyBounds).toEqual({ + minSampleX: 2, + maxSampleX: 2, + minSampleZ: 2, + maxSampleZ: 2 + }); + expect(terrain.heights).toBe(originalHeights); + expect(terrain.paintWeights).toBe(originalPaintWeights); + expect(terrain.heights[2 + 2 * 5]).toBeCloseTo(0.5); + }); + it("paints terrain layer weights toward the active layer while preserving a normalized blend", () => { const terrain = createTerrain({ id: "terrain-paint", diff --git a/tests/geometry/terrain-mesh.test.ts b/tests/geometry/terrain-mesh.test.ts index 12d86d85..d623e1fb 100644 --- a/tests/geometry/terrain-mesh.test.ts +++ b/tests/geometry/terrain-mesh.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; import { createTerrain } from "../../src/document/terrains"; import { buildTerrainDerivedMeshData, + buildTerrainLodChunkMeshData, buildTerrainLodMeshData, resolveTerrainLodLevelIndex, resolveTerrainLodLevelIndexWithHysteresis @@ -131,6 +132,25 @@ describe("terrain mesh generation", () => { }); }); + it("can rebuild one terrain LoD chunk from its starting sample", () => { + const terrain = createTerrain({ + sampleCountX: 130, + sampleCountZ: 70 + }); + + const chunk = buildTerrainLodChunkMeshData(terrain, 64, 0); + + expect(chunk).toMatchObject({ + chunkX: 1, + chunkZ: 0, + startSampleX: 64, + startSampleZ: 0, + endSampleX: 128, + endSampleZ: 64 + }); + expect(chunk?.levels.length).toBeGreaterThan(1); + }); + it("generates smaller terrain LoD levels as stride increases", () => { const terrain = createTerrain({ sampleCountX: 65, diff --git a/tests/serialization/local-draft-storage.test.ts b/tests/serialization/local-draft-storage.test.ts index 4a490278..2f6345fc 100644 --- a/tests/serialization/local-draft-storage.test.ts +++ b/tests/serialization/local-draft-storage.test.ts @@ -10,6 +10,7 @@ import { createEmptySceneDocument, createProjectDocumentFromSceneDocument } from "../../src/document/scene-document"; +import { createTerrain } from "../../src/document/terrains"; import { serializeSceneDocument } from "../../src/serialization/scene-document-json"; import { DEFAULT_SCENE_DRAFT_STORAGE_KEY, @@ -246,6 +247,38 @@ describe("local draft storage", () => { }); }); + it("skips oversized terrain-heavy autosaves and clears stale drafts", () => { + const storage = new MemoryStorage(); + storage.setItem(DEFAULT_SCENE_DRAFT_STORAGE_KEY, "stale draft"); + const terrain = createTerrain({ + id: "terrain-large-draft", + sampleCountX: 17, + sampleCountZ: 17 + }); + const document = { + ...createEmptyProjectDocument(), + scenes: { + "scene-main": { + ...createEmptyProjectScene({ + id: "scene-main", + name: "Terrain Draft" + }), + terrains: { + [terrain.id]: terrain + } + } + } + }; + + const result = saveSceneDocumentDraft(storage, document, null, undefined, { + maxSerializedBytes: 512 + }); + + expect(result.status).toBe("skipped"); + expect(result.message).toContain("Autosave skipped"); + expect(storage.getItem(DEFAULT_SCENE_DRAFT_STORAGE_KEY)).toBeNull(); + }); + it("loads older raw scene-document drafts without requiring viewport layout state", () => { const storage = new MemoryStorage(); storage.setItem(