2026-04-27 16:10:41 +02:00
|
|
|
import { describe, expect, it } from "vitest";
|
2026-05-18 15:40:29 +02:00
|
|
|
import { Object3D } from "three";
|
2026-04-27 16:10:41 +02:00
|
|
|
|
2026-05-18 12:59:37 +02:00
|
|
|
import { createDefaultProjectTimeSettings } from "../../src/document/project-time-settings";
|
|
|
|
|
import { createDefaultWorldSettings } from "../../src/document/world-settings";
|
2026-04-27 16:10:41 +02:00
|
|
|
import {
|
2026-05-18 15:40:29 +02:00
|
|
|
collectTerrainBrushRaycastObjectsForTerrain,
|
2026-05-18 12:59:37 +02:00
|
|
|
createViewportSimulationMembershipSignatures,
|
|
|
|
|
resolveViewportWorldState
|
2026-04-27 16:10:41 +02:00
|
|
|
} from "../../src/viewport-three/viewport-host";
|
|
|
|
|
import type { RuntimeSceneDefinition } from "../../src/runtime-three/runtime-scene-build";
|
|
|
|
|
|
|
|
|
|
describe("createViewportSimulationMembershipSignatures", () => {
|
|
|
|
|
it("tracks structural simulation membership without including per-frame values", () => {
|
|
|
|
|
const runtimeScene = {
|
|
|
|
|
localLights: {
|
|
|
|
|
pointLights: [
|
|
|
|
|
{
|
|
|
|
|
entityId: "point-a",
|
|
|
|
|
intensity: 1
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
spotLights: [
|
|
|
|
|
{
|
|
|
|
|
entityId: "spot-a",
|
|
|
|
|
intensity: 2
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
volumes: {
|
|
|
|
|
light: [
|
|
|
|
|
{
|
|
|
|
|
brushId: "volume-a",
|
|
|
|
|
lights: [{}, {}]
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
modelInstances: [
|
|
|
|
|
{
|
|
|
|
|
instanceId: "model-a",
|
|
|
|
|
visible: true
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
entities: {
|
|
|
|
|
interactables: [
|
|
|
|
|
{
|
|
|
|
|
entityId: "interactable-a",
|
|
|
|
|
interactionEnabled: true
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
} as RuntimeSceneDefinition;
|
|
|
|
|
|
|
|
|
|
const changedValuesScene = {
|
|
|
|
|
...runtimeScene,
|
|
|
|
|
localLights: {
|
|
|
|
|
pointLights: [
|
|
|
|
|
{
|
|
|
|
|
entityId: "point-a",
|
|
|
|
|
intensity: 5
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
spotLights: [
|
|
|
|
|
{
|
|
|
|
|
entityId: "spot-a",
|
|
|
|
|
intensity: 8
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
modelInstances: [
|
|
|
|
|
{
|
|
|
|
|
instanceId: "model-a",
|
|
|
|
|
visible: false
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
entities: {
|
|
|
|
|
interactables: [
|
|
|
|
|
{
|
|
|
|
|
entityId: "interactable-a",
|
|
|
|
|
interactionEnabled: false
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
} as RuntimeSceneDefinition;
|
|
|
|
|
|
|
|
|
|
const changedStructureScene = {
|
|
|
|
|
...runtimeScene,
|
|
|
|
|
volumes: {
|
|
|
|
|
light: [
|
|
|
|
|
{
|
|
|
|
|
brushId: "volume-a",
|
|
|
|
|
lights: [{}]
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
} as RuntimeSceneDefinition;
|
|
|
|
|
|
|
|
|
|
expect(createViewportSimulationMembershipSignatures(changedValuesScene)).toEqual(
|
|
|
|
|
createViewportSimulationMembershipSignatures(runtimeScene)
|
|
|
|
|
);
|
|
|
|
|
expect(
|
|
|
|
|
createViewportSimulationMembershipSignatures(changedStructureScene)
|
|
|
|
|
).not.toEqual(createViewportSimulationMembershipSignatures(runtimeScene));
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-05-18 12:59:37 +02:00
|
|
|
|
2026-05-18 15:40:29 +02:00
|
|
|
describe("collectTerrainBrushRaycastObjectsForTerrain", () => {
|
|
|
|
|
it("limits terrain brush raycasts to the active terrain pick meshes", () => {
|
|
|
|
|
const targetPickMesh = new Object3D();
|
|
|
|
|
const targetChunkPickMesh = new Object3D();
|
|
|
|
|
const otherTerrainPickMesh = new Object3D();
|
|
|
|
|
|
|
|
|
|
const raycastObjects = collectTerrainBrushRaycastObjectsForTerrain(
|
|
|
|
|
new Map([
|
|
|
|
|
[
|
|
|
|
|
"terrain-target",
|
|
|
|
|
{
|
|
|
|
|
pickMeshes: [targetPickMesh, targetChunkPickMesh]
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
"terrain-other",
|
|
|
|
|
{
|
|
|
|
|
pickMeshes: [otherTerrainPickMesh]
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
]),
|
|
|
|
|
"terrain-target"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(raycastObjects).toEqual([targetPickMesh, targetChunkPickMesh]);
|
|
|
|
|
expect(raycastObjects).not.toContain(otherTerrainPickMesh);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns no terrain brush raycast objects when the terrain has no render mesh", () => {
|
|
|
|
|
expect(
|
|
|
|
|
collectTerrainBrushRaycastObjectsForTerrain(new Map(), "terrain-missing")
|
|
|
|
|
).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-18 12:59:37 +02:00
|
|
|
describe("resolveViewportWorldState", () => {
|
|
|
|
|
it("applies document project time when the runtime scene is unavailable", () => {
|
|
|
|
|
const world = createDefaultWorldSettings();
|
|
|
|
|
const time = createDefaultProjectTimeSettings();
|
|
|
|
|
time.startTimeOfDayHours = 0;
|
|
|
|
|
world.projectTimeLightingEnabled = true;
|
|
|
|
|
world.ambientLight = {
|
|
|
|
|
colorHex: "#ffffff",
|
|
|
|
|
intensity: 1
|
|
|
|
|
};
|
|
|
|
|
world.timeOfDay.night.ambientColorHex = "#101820";
|
|
|
|
|
world.timeOfDay.night.ambientIntensityFactor = 0.25;
|
|
|
|
|
|
|
|
|
|
const state = resolveViewportWorldState({
|
|
|
|
|
currentWorld: world,
|
|
|
|
|
currentDocument: { time },
|
|
|
|
|
currentSimulationScene: null,
|
|
|
|
|
currentSimulationClock: {
|
|
|
|
|
timeOfDayHours: 0,
|
|
|
|
|
dayCount: 0,
|
|
|
|
|
dayLengthMinutes: time.dayLengthMinutes
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(state.resolvedTime?.dayPhase).toBe("night");
|
|
|
|
|
expect(state.resolvedWorld?.ambientLight).toEqual({
|
|
|
|
|
colorHex: "#101820",
|
|
|
|
|
intensity: 0.25
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("prefers runtime scene time settings when a runtime scene is available", () => {
|
|
|
|
|
const documentWorld = createDefaultWorldSettings();
|
|
|
|
|
const runtimeWorld = createDefaultWorldSettings();
|
|
|
|
|
const documentTime = createDefaultProjectTimeSettings();
|
|
|
|
|
const runtimeTime = createDefaultProjectTimeSettings();
|
|
|
|
|
documentTime.sunriseTimeOfDayHours = 6;
|
|
|
|
|
documentTime.sunsetTimeOfDayHours = 18;
|
|
|
|
|
runtimeTime.sunriseTimeOfDayHours = 10;
|
|
|
|
|
runtimeTime.sunsetTimeOfDayHours = 20;
|
|
|
|
|
|
|
|
|
|
const state = resolveViewportWorldState({
|
|
|
|
|
currentWorld: documentWorld,
|
|
|
|
|
currentDocument: { time: documentTime },
|
|
|
|
|
currentSimulationScene: {
|
|
|
|
|
time: runtimeTime,
|
|
|
|
|
world: runtimeWorld
|
|
|
|
|
},
|
|
|
|
|
currentSimulationClock: {
|
|
|
|
|
timeOfDayHours: 9,
|
|
|
|
|
dayCount: 0,
|
|
|
|
|
dayLengthMinutes: runtimeTime.dayLengthMinutes
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(state.world).toBe(runtimeWorld);
|
|
|
|
|
expect(state.timeSettings).toBe(runtimeTime);
|
|
|
|
|
expect(state.resolvedTime?.dayPhase).toBe("night");
|
|
|
|
|
});
|
|
|
|
|
});
|