Files
webeditor3d/tests/domain/foliage.test.ts

205 lines
6.1 KiB
TypeScript
Raw Normal View History

import { describe, expect, it } from "vitest";
import {
createProjectAssetStorageKey,
type ModelAssetRecord
} from "../../src/assets/project-assets";
import {
createEmptySceneDocument,
createSceneDocumentFromProject,
createEmptyProjectDocument
} from "../../src/document/scene-document";
import { createTerrain, createTerrainFoliageMask } from "../../src/document/terrains";
import { validateSceneDocument } from "../../src/document/scene-document-validation";
import { BUNDLED_FOLIAGE_PROTOTYPES } from "../../src/foliage/bundled-foliage-manifest";
import {
FOLIAGE_PROTOTYPE_LOD_LEVELS,
createFoliageLayer,
createFoliagePrototype,
type FoliagePrototypeLod
} from "../../src/foliage/foliage";
function createModelAsset(id: string): ModelAssetRecord {
return {
id,
kind: "model",
sourceName: `${id}.glb`,
mimeType: "model/gltf-binary",
storageKey: createProjectAssetStorageKey(id),
byteLength: 1024,
metadata: {
kind: "model",
format: "glb",
sceneName: null,
nodeCount: 1,
meshCount: 1,
materialNames: [],
textureNames: [],
animationNames: [],
boundingBox: null,
warnings: []
}
};
}
function createProjectAssetLods(modelAssetId: string): FoliagePrototypeLod[] {
return FOLIAGE_PROTOTYPE_LOD_LEVELS.map((level) => ({
level,
source: "projectAsset",
modelAssetId,
maxDistance: 20 + level * 20,
castShadow: level < 2
}));
}
describe("foliage document foundations", () => {
it("accepts scene foliage layers that reference bundled prototype ids", () => {
const bundledPrototype = BUNDLED_FOLIAGE_PROTOTYPES[0];
const document = createEmptySceneDocument();
const layer = createFoliageLayer({
id: "foliage-layer-meadow",
name: "Meadow",
prototypeIds: [bundledPrototype.id]
});
document.foliageLayers[layer.id] = layer;
expect(validateSceneDocument(document).errors).toEqual([]);
});
it("accepts custom project-asset sourced foliage prototypes", () => {
const modelAsset = createModelAsset("asset-custom-foliage");
const document = createEmptySceneDocument();
const prototype = createFoliagePrototype({
id: "foliage-custom-clover",
label: "Custom Clover",
category: "grass",
lods: createProjectAssetLods(modelAsset.id)
});
const layer = createFoliageLayer({
id: "foliage-layer-custom",
name: "Custom Foliage",
prototypeIds: [prototype.id]
});
document.assets[modelAsset.id] = modelAsset;
document.foliagePrototypes[prototype.id] = prototype;
document.foliageLayers[layer.id] = layer;
expect(validateSceneDocument(document).errors).toEqual([]);
});
it("validates layer ranges and prototype references", () => {
const document = createEmptySceneDocument();
const layer = createFoliageLayer({
id: "foliage-layer-invalid",
name: "Invalid",
prototypeIds: ["missing-foliage-prototype"]
});
document.foliageLayers[layer.id] = {
...layer,
minScale: 2,
maxScale: 1,
maxSlopeDegrees: 120,
noiseStrength: 1.5
};
expect(validateSceneDocument(document).errors).toEqual(
expect.arrayContaining([
expect.objectContaining({ code: "invalid-foliage-layer-scale-range" }),
expect.objectContaining({ code: "invalid-foliage-layer-max-slope" }),
expect.objectContaining({
code: "invalid-foliage-layer-noise-strength"
}),
expect.objectContaining({ code: "missing-foliage-layer-prototype" })
])
);
});
it("validates terrain foliage mask layer references and ranges", () => {
const document = createEmptySceneDocument();
const terrain = createTerrain({
id: "terrain-invalid-foliage-mask",
sampleCountX: 2,
sampleCountZ: 2,
foliageMasks: {
"missing-foliage-layer": createTerrainFoliageMask({
layerId: "missing-foliage-layer",
resolutionX: 2,
resolutionZ: 2,
values: [0, 0.25, 0.5, 1]
})
}
});
document.terrains[terrain.id] = {
...terrain,
foliageMasks: {
"missing-foliage-layer": {
...terrain.foliageMasks["missing-foliage-layer"]!,
values: [0, 2, 0.5, 1]
}
}
};
expect(validateSceneDocument(document).errors).toEqual(
expect.arrayContaining([
expect.objectContaining({ code: "missing-terrain-foliage-mask-layer" }),
expect.objectContaining({ code: "invalid-terrain-foliage-mask-value" })
])
);
});
it("validates custom project-asset LOD references", () => {
const document = createEmptySceneDocument();
const prototype = createFoliagePrototype({
id: "foliage-missing-model",
label: "Missing Model",
category: "other",
lods: createProjectAssetLods("asset-missing")
});
document.foliagePrototypes[prototype.id] = prototype;
expect(validateSceneDocument(document).errors).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "missing-foliage-prototype-model-asset"
})
])
);
});
it("keeps project custom prototypes global and layers scene-local", () => {
const modelAsset = createModelAsset("asset-project-foliage");
const project = createEmptyProjectDocument({
assets: {
[modelAsset.id]: modelAsset
}
});
const prototype = createFoliagePrototype({
id: "foliage-project-reed",
label: "Project Reed",
category: "grass",
lods: createProjectAssetLods(modelAsset.id)
});
const layer = createFoliageLayer({
id: "foliage-layer-project-reed",
name: "Project Reed Layer",
prototypeIds: [prototype.id]
});
project.foliagePrototypes[prototype.id] = prototype;
project.scenes[project.activeSceneId]!.foliageLayers[layer.id] = layer;
const sceneDocument = createSceneDocumentFromProject(project);
expect(sceneDocument.foliagePrototypes).toEqual(project.foliagePrototypes);
expect(sceneDocument.foliageLayers).toEqual(
project.scenes[project.activeSceneId]!.foliageLayers
);
expect(validateSceneDocument(sceneDocument).errors).toEqual([]);
});
});