auto-git:
[add] src/rendering/terrain-layer-material.ts [add] tests/domain/terrains.test.ts [change] src/app/App.tsx [change] src/core/terrain-brush.ts [change] src/document/migrate-scene-document.ts [change] src/document/scene-document-validation.ts [change] src/document/scene-document.ts [change] src/document/terrains.ts [change] src/geometry/terrain-brush.ts [change] src/geometry/terrain-mesh.ts [change] src/runtime-three/rapier-collision-world.ts [change] src/runtime-three/runtime-host.ts [change] src/runtime-three/runtime-scene-build.ts [change] src/viewport-three/ViewportCanvas.tsx [change] src/viewport-three/ViewportPanel.tsx [change] src/viewport-three/viewport-host.ts [change] tests/domain/build-runtime-scene.test.ts [change] tests/domain/rapier-collision-world.test.ts [change] tests/domain/terrain.command.test.ts [change] tests/domain/water-material.test.ts [change] tests/geometry/terrain-brush.test.ts [change] tests/geometry/terrain-mesh.test.ts [change] tests/serialization/scene-document-json.test.ts [change] tests/unit/terrain-foundation.integration.test.tsx [change] tests/unit/viewport-canvas.test.tsx
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
import { createBoxBrush } from "../../src/document/brushes";
|
||||
import { createScenePath } from "../../src/document/paths";
|
||||
import { createDefaultProjectTimeSettings } from "../../src/document/project-time-settings";
|
||||
import { createTerrain } from "../../src/document/terrains";
|
||||
import { createEmptySceneDocument } from "../../src/document/scene-document";
|
||||
import {
|
||||
DEFAULT_PLAYER_START_MOVE_SPEED,
|
||||
@@ -1351,6 +1352,56 @@ describe("buildRuntimeSceneFromDocument", () => {
|
||||
expect(runtimeScene.sceneBounds?.max.y).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("adds authored terrain heightfield colliders to the runtime scene build", () => {
|
||||
const terrain = createTerrain({
|
||||
id: "terrain-runtime-heightfield",
|
||||
position: {
|
||||
x: -4,
|
||||
y: 0,
|
||||
z: -4
|
||||
},
|
||||
sampleCountX: 3,
|
||||
sampleCountZ: 3,
|
||||
cellSize: 4,
|
||||
heights: [0, 1, 0, 0, 2, 0, 0, 1, 0]
|
||||
});
|
||||
|
||||
const runtimeScene = buildRuntimeSceneFromDocument({
|
||||
...createEmptySceneDocument({ name: "Authored Terrain Collider Scene" }),
|
||||
terrains: {
|
||||
[terrain.id]: terrain
|
||||
}
|
||||
});
|
||||
|
||||
expect(runtimeScene.terrains[0]).toMatchObject({
|
||||
id: terrain.id,
|
||||
collisionEnabled: true
|
||||
});
|
||||
expect(runtimeScene.colliders[0]).toMatchObject({
|
||||
source: "terrain",
|
||||
terrainId: terrain.id,
|
||||
kind: "heightfield",
|
||||
rows: 3,
|
||||
cols: 3,
|
||||
minX: 0,
|
||||
maxX: 8,
|
||||
minZ: 0,
|
||||
maxZ: 8
|
||||
});
|
||||
|
||||
if (
|
||||
runtimeScene.colliders[0] === undefined ||
|
||||
runtimeScene.colliders[0].source !== "terrain"
|
||||
) {
|
||||
throw new Error("Expected the runtime collider to be an authored terrain heightfield.");
|
||||
}
|
||||
|
||||
expect(Array.from(runtimeScene.colliders[0].heights)).toEqual(
|
||||
terrain.heights
|
||||
);
|
||||
expect(runtimeScene.sceneBounds?.max.y).toBe(2);
|
||||
});
|
||||
|
||||
it("adds static-simple imported-model colliders as compound box pieces", () => {
|
||||
const wallGeometry = new PlaneGeometry(4, 4, 4, 4);
|
||||
wallGeometry.rotateY(Math.PI * 0.5);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { BoxGeometry, PlaneGeometry } from "three";
|
||||
import { createModelInstance } from "../../src/assets/model-instances";
|
||||
import { createBoxBrush } from "../../src/document/brushes";
|
||||
import { createEmptySceneDocument } from "../../src/document/scene-document";
|
||||
import { createTerrain } from "../../src/document/terrains";
|
||||
import {
|
||||
createNpcEntity,
|
||||
createPlayerStartEntity
|
||||
@@ -954,4 +955,93 @@ describe("RapierCollisionWorld", () => {
|
||||
collisionWorld.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("initializes and resolves first-person motion against authored terrain colliders", async () => {
|
||||
const terrain = createTerrain({
|
||||
id: "terrain-authored-collision",
|
||||
position: {
|
||||
x: -4,
|
||||
y: 0,
|
||||
z: -4
|
||||
},
|
||||
sampleCountX: 5,
|
||||
sampleCountZ: 5,
|
||||
cellSize: 2,
|
||||
heights: [
|
||||
0,
|
||||
0.25,
|
||||
0.5,
|
||||
0.75,
|
||||
1,
|
||||
0.75,
|
||||
1,
|
||||
1.25,
|
||||
1.5,
|
||||
1.75,
|
||||
1.5,
|
||||
1.75,
|
||||
2,
|
||||
2.25,
|
||||
2.5,
|
||||
2.25,
|
||||
2.5,
|
||||
2.75,
|
||||
3,
|
||||
3.25,
|
||||
3,
|
||||
3.25,
|
||||
3.5,
|
||||
3.75,
|
||||
4
|
||||
]
|
||||
});
|
||||
const runtimeScene = buildRuntimeSceneFromDocument({
|
||||
...createEmptySceneDocument({ name: "Authored Terrain Collision Scene" }),
|
||||
terrains: {
|
||||
[terrain.id]: terrain
|
||||
}
|
||||
});
|
||||
const collisionWorld = await RapierCollisionWorld.create(
|
||||
runtimeScene.colliders,
|
||||
runtimeScene.playerCollider
|
||||
);
|
||||
|
||||
try {
|
||||
const highLanding = collisionWorld.resolveFirstPersonMotion(
|
||||
{
|
||||
x: 4,
|
||||
y: 8,
|
||||
z: 4
|
||||
},
|
||||
{
|
||||
x: 0,
|
||||
y: -10,
|
||||
z: 0
|
||||
},
|
||||
runtimeScene.playerCollider
|
||||
);
|
||||
const lowLanding = collisionWorld.resolveFirstPersonMotion(
|
||||
{
|
||||
x: -2,
|
||||
y: 8,
|
||||
z: -2
|
||||
},
|
||||
{
|
||||
x: 0,
|
||||
y: -10,
|
||||
z: 0
|
||||
},
|
||||
runtimeScene.playerCollider
|
||||
);
|
||||
|
||||
expect(highLanding.grounded).toBe(true);
|
||||
expect(highLanding.feetPosition.y).toBeGreaterThan(3.9);
|
||||
expect(highLanding.feetPosition.y).toBeLessThan(4.1);
|
||||
expect(lowLanding.grounded).toBe(true);
|
||||
expect(lowLanding.feetPosition.y).toBeGreaterThan(1.0);
|
||||
expect(lowLanding.feetPosition.y).toBeLessThan(1.2);
|
||||
} finally {
|
||||
collisionWorld.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,7 +62,36 @@ describe("terrain commands", () => {
|
||||
sampleCountX: 3,
|
||||
sampleCountZ: 3,
|
||||
cellSize: 1.5,
|
||||
heights: [0, 1, 0, 1, 3, 1, 0, 1, 0]
|
||||
heights: [0, 1, 0, 1, 3, 1, 0, 1, 0],
|
||||
paintWeights: [
|
||||
0.2,
|
||||
0,
|
||||
0,
|
||||
0.1,
|
||||
0.3,
|
||||
0,
|
||||
0,
|
||||
0.15,
|
||||
0,
|
||||
0.25,
|
||||
0.25,
|
||||
0,
|
||||
0.05,
|
||||
0.1,
|
||||
0.1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
0.1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
});
|
||||
|
||||
store.executeCommand(
|
||||
|
||||
63
tests/domain/terrains.test.ts
Normal file
63
tests/domain/terrains.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
getTerrainPaintWeightSampleOffset,
|
||||
resizeTerrainGrid,
|
||||
createTerrain
|
||||
} from "../../src/document/terrains";
|
||||
|
||||
describe("terrain grid resizing", () => {
|
||||
it("keeps terrain centered while bilinearly resampling heights and paint weights", () => {
|
||||
const terrain = createTerrain({
|
||||
id: "terrain-resample-main",
|
||||
position: {
|
||||
x: 0,
|
||||
y: 1,
|
||||
z: 0
|
||||
},
|
||||
sampleCountX: 2,
|
||||
sampleCountZ: 2,
|
||||
cellSize: 1,
|
||||
heights: [0, 2, 4, 6],
|
||||
paintWeights: [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0.6,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0.5,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0.25
|
||||
]
|
||||
});
|
||||
|
||||
const resizedTerrain = resizeTerrainGrid(terrain, {
|
||||
sampleCountX: 3,
|
||||
sampleCountZ: 3,
|
||||
cellSize: 1
|
||||
});
|
||||
const centerOffset = getTerrainPaintWeightSampleOffset(resizedTerrain, 1, 1);
|
||||
|
||||
expect(resizedTerrain.position).toEqual({
|
||||
x: -0.5,
|
||||
y: 1,
|
||||
z: -0.5
|
||||
});
|
||||
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(terrain.position).toEqual({
|
||||
x: 0,
|
||||
y: 1,
|
||||
z: 0
|
||||
});
|
||||
expect(terrain.heights).toEqual([0, 2, 4, 6]);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,9 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { MAX_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT } from "../../src/document/brushes";
|
||||
import { createBoxBrush } from "../../src/document/brushes";
|
||||
import { createTerrain } from "../../src/document/terrains";
|
||||
import { buildBoxBrushDerivedMeshData } from "../../src/geometry/box-brush-mesh";
|
||||
import { buildTerrainDerivedMeshData } from "../../src/geometry/terrain-mesh";
|
||||
import { collectWaterContactPatches, createWaterMaterial } from "../../src/rendering/water-material";
|
||||
|
||||
describe("water material helpers", () => {
|
||||
@@ -497,6 +499,92 @@ describe("water material helpers", () => {
|
||||
expect(patches[0]?.halfDepth ?? 0).toBeGreaterThan(0.05);
|
||||
});
|
||||
|
||||
it("builds shoreline contact patches from authored terrain meshes", () => {
|
||||
const terrain = createTerrain({
|
||||
id: "terrain-waterline-main",
|
||||
position: {
|
||||
x: -4,
|
||||
y: 0,
|
||||
z: -4
|
||||
},
|
||||
sampleCountX: 5,
|
||||
sampleCountZ: 5,
|
||||
cellSize: 2,
|
||||
heights: [
|
||||
-1.2,
|
||||
-0.8,
|
||||
-0.4,
|
||||
0,
|
||||
0.4,
|
||||
-1,
|
||||
-0.6,
|
||||
-0.2,
|
||||
0.2,
|
||||
0.6,
|
||||
-0.8,
|
||||
-0.4,
|
||||
0,
|
||||
0.4,
|
||||
0.8,
|
||||
-0.6,
|
||||
-0.2,
|
||||
0.2,
|
||||
0.6,
|
||||
1,
|
||||
-0.4,
|
||||
0,
|
||||
0.4,
|
||||
0.8,
|
||||
1.2
|
||||
]
|
||||
});
|
||||
const derivedMesh = buildTerrainDerivedMeshData(terrain);
|
||||
const patches = collectWaterContactPatches(
|
||||
{
|
||||
center: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
},
|
||||
rotationDegrees: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
},
|
||||
size: {
|
||||
x: 10,
|
||||
y: 2,
|
||||
z: 10
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
kind: "triangleMesh",
|
||||
vertices: derivedMesh.positions,
|
||||
indices: derivedMesh.indices,
|
||||
mergeProfile: "aggressive",
|
||||
transform: {
|
||||
position: terrain.position,
|
||||
rotationDegrees: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
},
|
||||
scale: {
|
||||
x: 1,
|
||||
y: 1,
|
||||
z: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
expect(patches.length).toBeGreaterThan(0);
|
||||
expect(patches[0]?.shape).toBe("segment");
|
||||
expect(patches[0]?.halfWidth ?? 0).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it("keeps foam patches for both long and short edges of a box intersecting the water surface", () => {
|
||||
const intersectingBox = createBoxBrush({
|
||||
center: {
|
||||
@@ -780,4 +868,4 @@ describe("water material helpers", () => {
|
||||
expect(material.uniforms["reflectionEnabled"]?.value).toBe(0.36);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createDefaultTerrainBrushSettings } from "../../src/core/terrain-brush";
|
||||
import { createTerrain } from "../../src/document/terrains";
|
||||
import {
|
||||
createTerrain,
|
||||
getTerrainSampleLayerWeights
|
||||
} from "../../src/document/terrains";
|
||||
import {
|
||||
applyTerrainBrushStamp,
|
||||
getTerrainBrushWeight,
|
||||
@@ -108,4 +111,52 @@ describe("terrain brush geometry", () => {
|
||||
it("returns zero influence outside the brush radius", () => {
|
||||
expect(getTerrainBrushWeight(2, 1, 0.5)).toBe(0);
|
||||
});
|
||||
|
||||
it("paints terrain layer weights toward the active layer while preserving a normalized blend", () => {
|
||||
const terrain = createTerrain({
|
||||
id: "terrain-paint",
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
sampleCountX: 3,
|
||||
sampleCountZ: 3,
|
||||
cellSize: 1
|
||||
});
|
||||
|
||||
const paintedTerrain = applyTerrainBrushStamp({
|
||||
terrain,
|
||||
center: { x: 1, z: 1 },
|
||||
settings: {
|
||||
radius: 1.2,
|
||||
strength: 0.5,
|
||||
falloff: 0
|
||||
},
|
||||
tool: "paint",
|
||||
layerIndex: 2
|
||||
});
|
||||
|
||||
const paintedWeights = getTerrainSampleLayerWeights(paintedTerrain, 1, 1);
|
||||
|
||||
expect(paintedWeights[2]).toBeCloseTo(0.5);
|
||||
expect(
|
||||
paintedWeights[0] +
|
||||
paintedWeights[1] +
|
||||
paintedWeights[2] +
|
||||
paintedWeights[3]
|
||||
).toBeCloseTo(1);
|
||||
|
||||
const repaintedBaseTerrain = applyTerrainBrushStamp({
|
||||
terrain: paintedTerrain,
|
||||
center: { x: 1, z: 1 },
|
||||
settings: {
|
||||
radius: 1.2,
|
||||
strength: 0.5,
|
||||
falloff: 0
|
||||
},
|
||||
tool: "paint",
|
||||
layerIndex: 0
|
||||
});
|
||||
|
||||
const baseWeights = getTerrainSampleLayerWeights(repaintedBaseTerrain, 1, 1);
|
||||
expect(baseWeights[0]).toBeGreaterThan(paintedWeights[0]);
|
||||
expect(baseWeights[2]).toBeLessThan(paintedWeights[2]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,4 +56,46 @@ describe("terrain mesh generation", () => {
|
||||
|
||||
expect(Array.from(derivedMesh.uvs)).toEqual([10, -6, 12, -6, 10, -4, 12, -4]);
|
||||
});
|
||||
|
||||
it("derives full per-vertex layer weights from the compact terrain paint data", () => {
|
||||
const terrain = createTerrain({
|
||||
sampleCountX: 2,
|
||||
sampleCountZ: 2,
|
||||
paintWeights: [
|
||||
0.2,
|
||||
0.3,
|
||||
0.1,
|
||||
0,
|
||||
0.5,
|
||||
0,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.25,
|
||||
0.25,
|
||||
0.25
|
||||
]
|
||||
});
|
||||
|
||||
const derivedMesh = buildTerrainDerivedMeshData(terrain);
|
||||
|
||||
expect(Array.from(derivedMesh.layerWeights)).toEqual([
|
||||
expect.closeTo(0.4, 5),
|
||||
expect.closeTo(0.2, 5),
|
||||
expect.closeTo(0.3, 5),
|
||||
expect.closeTo(0.1, 5),
|
||||
expect.closeTo(0.5, 5),
|
||||
expect.closeTo(0, 5),
|
||||
expect.closeTo(0.5, 5),
|
||||
expect.closeTo(0, 5),
|
||||
expect.closeTo(0.7, 5),
|
||||
expect.closeTo(0.1, 5),
|
||||
expect.closeTo(0.1, 5),
|
||||
expect.closeTo(0.1, 5),
|
||||
expect.closeTo(0.25, 5),
|
||||
expect.closeTo(0.25, 5),
|
||||
expect.closeTo(0.25, 5),
|
||||
expect.closeTo(0.25, 5)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,8 @@ import { createScenePath } from "../../src/document/paths";
|
||||
import { createDefaultProjectTimeSettings } from "../../src/document/project-time-settings";
|
||||
import { createTerrain } from "../../src/document/terrains";
|
||||
import {
|
||||
AUTHORED_TERRAIN_PAINT_SCENE_DOCUMENT_VERSION,
|
||||
AUTHORED_TERRAIN_FOUNDATION_SCENE_DOCUMENT_VERSION,
|
||||
FOLLOW_ACTOR_PATH_SMOOTH_SCENE_DOCUMENT_VERSION,
|
||||
ANIMATION_PLAYBACK_SCENE_DOCUMENT_VERSION,
|
||||
ENTITY_NAMES_SCENE_DOCUMENT_VERSION,
|
||||
@@ -80,6 +82,7 @@ describe("scene document JSON", () => {
|
||||
const terrain = createTerrain({
|
||||
id: "terrain-roundtrip-main",
|
||||
name: "Plateau",
|
||||
collisionEnabled: false,
|
||||
position: {
|
||||
x: -6,
|
||||
y: 1,
|
||||
@@ -98,6 +101,79 @@ describe("scene document JSON", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("migrates pre-paint terrain documents by defaulting terrain layer data", () => {
|
||||
const legacyTerrainDocument = {
|
||||
...createEmptySceneDocument({
|
||||
name: "Legacy Terrain Paint Scene"
|
||||
}),
|
||||
version: AUTHORED_TERRAIN_FOUNDATION_SCENE_DOCUMENT_VERSION
|
||||
};
|
||||
const terrain = createTerrain({
|
||||
id: "terrain-legacy-paint",
|
||||
sampleCountX: 3,
|
||||
sampleCountZ: 3
|
||||
});
|
||||
legacyTerrainDocument.terrains[terrain.id] = {
|
||||
id: terrain.id,
|
||||
kind: terrain.kind,
|
||||
name: terrain.name,
|
||||
visible: terrain.visible,
|
||||
enabled: terrain.enabled,
|
||||
position: terrain.position,
|
||||
sampleCountX: terrain.sampleCountX,
|
||||
sampleCountZ: terrain.sampleCountZ,
|
||||
cellSize: terrain.cellSize,
|
||||
heights: terrain.heights
|
||||
} as typeof terrain;
|
||||
|
||||
const migratedDocument = parseSceneDocumentJson(
|
||||
JSON.stringify(legacyTerrainDocument)
|
||||
);
|
||||
const migratedTerrain = migratedDocument.terrains[terrain.id];
|
||||
|
||||
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
||||
expect(migratedTerrain.layers).toHaveLength(4);
|
||||
expect(migratedTerrain.paintWeights).toHaveLength(27);
|
||||
expect(migratedTerrain.paintWeights.every((weight) => weight === 0)).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it("migrates pre-collision terrain documents by defaulting terrain collision to enabled", () => {
|
||||
const legacyTerrainDocument = {
|
||||
...createEmptySceneDocument({
|
||||
name: "Legacy Terrain Collision Scene"
|
||||
}),
|
||||
version: AUTHORED_TERRAIN_PAINT_SCENE_DOCUMENT_VERSION
|
||||
};
|
||||
const terrain = createTerrain({
|
||||
id: "terrain-legacy-collision",
|
||||
sampleCountX: 3,
|
||||
sampleCountZ: 3
|
||||
});
|
||||
legacyTerrainDocument.terrains[terrain.id] = {
|
||||
id: terrain.id,
|
||||
kind: terrain.kind,
|
||||
name: terrain.name,
|
||||
visible: terrain.visible,
|
||||
enabled: terrain.enabled,
|
||||
position: terrain.position,
|
||||
sampleCountX: terrain.sampleCountX,
|
||||
sampleCountZ: terrain.sampleCountZ,
|
||||
cellSize: terrain.cellSize,
|
||||
heights: terrain.heights,
|
||||
layers: terrain.layers,
|
||||
paintWeights: terrain.paintWeights
|
||||
} as typeof terrain;
|
||||
|
||||
const migratedDocument = parseSceneDocumentJson(
|
||||
JSON.stringify(legacyTerrainDocument)
|
||||
);
|
||||
|
||||
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
||||
expect(migratedDocument.terrains[terrain.id]?.collisionEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it("migrates pre-terrain scene documents by defaulting the terrain registry to empty", () => {
|
||||
const legacyDocument = {
|
||||
...createEmptySceneDocument({ name: "Legacy Terrainless Scene" }),
|
||||
|
||||
@@ -132,4 +132,54 @@ describe("Terrain foundation", () => {
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/9 x 9 samples/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("updates selected terrain grid settings and collision from the inspector", async () => {
|
||||
const store = createEditorStore();
|
||||
|
||||
render(<App store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(viewportHostInstances.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add" }));
|
||||
fireEvent.click(await screen.findByTestId("add-menu-terrain"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(Object.keys(store.getState().document.terrains)).toHaveLength(1);
|
||||
});
|
||||
|
||||
const createdTerrain = Object.values(store.getState().document.terrains)[0];
|
||||
|
||||
if (createdTerrain === undefined) {
|
||||
throw new Error("Expected the created terrain to exist.");
|
||||
}
|
||||
|
||||
fireEvent.change(screen.getByTestId("terrain-grid-sample-count-x"), {
|
||||
target: { value: "5" }
|
||||
});
|
||||
fireEvent.change(screen.getByTestId("terrain-grid-sample-count-z"), {
|
||||
target: { value: "7" }
|
||||
});
|
||||
fireEvent.change(screen.getByTestId("terrain-grid-cell-size"), {
|
||||
target: { value: "2" }
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("terrain-grid-apply"));
|
||||
|
||||
await waitFor(() => {
|
||||
const updatedTerrain = store.getState().document.terrains[createdTerrain.id];
|
||||
|
||||
expect(updatedTerrain?.sampleCountX).toBe(5);
|
||||
expect(updatedTerrain?.sampleCountZ).toBe(7);
|
||||
expect(updatedTerrain?.cellSize).toBe(2);
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId("terrain-collision-enabled"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
store.getState().document.terrains[createdTerrain.id]?.collisionEnabled
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -428,4 +428,58 @@ describe("ViewportCanvas", () => {
|
||||
screen.getByTestId("viewport-terrain-brush-preview-topLeft")
|
||||
).toHaveTextContent("terrain · smooth");
|
||||
});
|
||||
|
||||
it("shows the active terrain paint layer in the viewport overlay", () => {
|
||||
const sceneDocument = createEmptySceneDocument();
|
||||
const terrainBrushState: ArmedTerrainBrushState = {
|
||||
terrainId: "terrain-selected",
|
||||
tool: "paint",
|
||||
layerIndex: 2,
|
||||
radius: 2.5,
|
||||
strength: 0.4,
|
||||
falloff: 0.7
|
||||
};
|
||||
|
||||
render(
|
||||
<ViewportCanvas
|
||||
panelId="topLeft"
|
||||
world={sceneDocument.world}
|
||||
sceneDocument={sceneDocument}
|
||||
editorSimulationScene={null}
|
||||
editorSimulationClock={null}
|
||||
projectAssets={sceneDocument.assets}
|
||||
loadedModelAssets={{}}
|
||||
loadedImageAssets={{}}
|
||||
whiteboxSelectionMode="object"
|
||||
whiteboxSnapEnabled
|
||||
whiteboxSnapStep={1}
|
||||
viewportGridVisible={true}
|
||||
selection={{ kind: "terrains", ids: [terrainBrushState.terrainId] }}
|
||||
activeSelectionId={terrainBrushState.terrainId}
|
||||
terrainBrushState={terrainBrushState}
|
||||
toolMode="select"
|
||||
toolPreview={{ kind: "none" }}
|
||||
transformSession={createInactiveTransformSession()}
|
||||
cameraState={createDefaultViewportPanelCameraState()}
|
||||
viewMode="perspective"
|
||||
displayMode="normal"
|
||||
layoutMode="single"
|
||||
isActivePanel
|
||||
focusRequestId={0}
|
||||
focusSelection={{ kind: "none" }}
|
||||
onSelectionChange={vi.fn()}
|
||||
onTerrainBrushCommit={vi.fn(() => true)}
|
||||
onCommitCreation={vi.fn(() => true)}
|
||||
onCameraStateChange={vi.fn()}
|
||||
onToolPreviewChange={vi.fn()}
|
||||
onTransformSessionChange={vi.fn()}
|
||||
onTransformCommit={vi.fn()}
|
||||
onTransformCancel={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByTestId("viewport-terrain-brush-preview-topLeft")
|
||||
).toHaveTextContent("terrain · paint · layer 3");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user