2026-05-02 03:48:47 +02:00
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
createProjectAssetStorageKey,
|
|
|
|
|
type ModelAssetRecord
|
|
|
|
|
} from "../../src/assets/project-assets";
|
|
|
|
|
import {
|
|
|
|
|
createEmptySceneDocument,
|
|
|
|
|
createSceneDocumentFromProject,
|
|
|
|
|
createEmptyProjectDocument
|
|
|
|
|
} from "../../src/document/scene-document";
|
2026-05-02 04:22:16 +02:00
|
|
|
import { createTerrain, createTerrainFoliageMask } from "../../src/document/terrains";
|
2026-05-02 03:48:47 +02:00
|
|
|
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" })
|
|
|
|
|
])
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-02 04:22:16 +02:00
|
|
|
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" })
|
|
|
|
|
])
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-02 03:48:47 +02:00
|
|
|
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([]);
|
|
|
|
|
});
|
|
|
|
|
});
|