auto-git:

[change] src/viewport-three/viewport-host.ts
 [change] tests/domain/build-runtime-scene.test.ts
 [change] tests/domain/custom-material.command.test.ts
 [change] tests/domain/delete-project-asset.test.ts
 [change] tests/domain/scene-document-validation.test.ts
 [change] tests/serialization/project-document-json.test.ts
 [change] tests/serialization/project-package.test.ts
 [change] tests/serialization/scene-document-json.test.ts
 [change] tests/unit/whitebox-material-inspector.integration.test.tsx
This commit is contained in:
2026-05-18 16:21:06 +02:00
parent 1fb96650c5
commit 2df1eb7eed
9 changed files with 226 additions and 166 deletions

View File

@@ -971,7 +971,9 @@ export function resolveViewportWorldState(options: {
}): ViewportResolvedWorldState { }): ViewportResolvedWorldState {
const world = options.currentSimulationScene?.world ?? options.currentWorld; const world = options.currentSimulationScene?.world ?? options.currentWorld;
const timeSettings = const timeSettings =
options.currentSimulationScene?.time ?? options.currentDocument?.time ?? null; options.currentSimulationScene?.time ??
options.currentDocument?.time ??
null;
if (world === null) { if (world === null) {
return { return {
@@ -1528,10 +1530,7 @@ export class ViewportHost {
this.terrainGridResizeOverlayGroup.add(visual.group); this.terrainGridResizeOverlayGroup.add(visual.group);
} }
applyRendererRenderCategory( applyRendererRenderCategory(this.terrainGridResizeOverlayGroup, "overlay");
this.terrainGridResizeOverlayGroup,
"overlay"
);
this.scene.add(this.terrainGridResizeOverlayGroup); this.scene.add(this.terrainGridResizeOverlayGroup);
this.scene.add(this.pathGroup); this.scene.add(this.pathGroup);
this.scene.add(this.entityGroup); this.scene.add(this.entityGroup);
@@ -2790,17 +2789,13 @@ export class ViewportHost {
} }
private applyWorld() { private applyWorld() {
const { const { world, timeSettings, resolvedTime, resolvedWorld } =
world, resolveViewportWorldState({
timeSettings, currentWorld: this.currentWorld,
resolvedTime, currentDocument: this.currentDocument,
resolvedWorld currentSimulationScene: this.currentSimulationScene,
} = resolveViewportWorldState({ currentSimulationClock: this.currentSimulationClock
currentWorld: this.currentWorld, });
currentDocument: this.currentDocument,
currentSimulationScene: this.currentSimulationScene,
currentSimulationClock: this.currentSimulationClock
});
if (world === null) { if (world === null) {
return; return;
@@ -7705,7 +7700,10 @@ export class ViewportHost {
color: color:
materialDef === null materialDef === null
? fallbackColor ? fallbackColor
: Number.parseInt(materialDef.swatchColorHex.replace("#", ""), 16), : Number.parseInt(
materialDef.swatchColorHex.replace("#", ""),
16
),
wireframe: true, wireframe: true,
side: DoubleSide side: DoubleSide
}) })
@@ -7773,7 +7771,9 @@ export class ViewportHost {
); );
} }
private createRoadJunctionMaterial(junction: SplineCorridorJunction): Material { private createRoadJunctionMaterial(
junction: SplineCorridorJunction
): Material {
return this.createRoadGeneratedMaterial( return this.createRoadGeneratedMaterial(
junction.materialId, junction.materialId,
PATH_JUNCTION_PREVIEW_COLOR PATH_JUNCTION_PREVIEW_COLOR
@@ -7802,7 +7802,10 @@ export class ViewportHost {
const meshes: Array<Mesh<BufferGeometry, Material>> = []; const meshes: Array<Mesh<BufferGeometry, Material>> = [];
if (surfaceGeometry !== null) { if (surfaceGeometry !== null) {
const mesh = new Mesh(surfaceGeometry, this.createRoadSurfaceMaterial(path)); const mesh = new Mesh(
surfaceGeometry,
this.createRoadSurfaceMaterial(path)
);
mesh.castShadow = false; mesh.castShadow = false;
mesh.receiveShadow = true; mesh.receiveShadow = true;
mesh.userData.pathId = path.id; mesh.userData.pathId = path.id;
@@ -7825,7 +7828,10 @@ export class ViewportHost {
} }
const edge = path.road.edges[side]; const edge = path.road.edges[side];
const edgeMesh = new Mesh(edgeGeometry, this.createRoadEdgeMaterial(edge)); const edgeMesh = new Mesh(
edgeGeometry,
this.createRoadEdgeMaterial(edge)
);
edgeMesh.castShadow = false; edgeMesh.castShadow = false;
edgeMesh.receiveShadow = true; edgeMesh.receiveShadow = true;
edgeMesh.userData.pathId = path.id; edgeMesh.userData.pathId = path.id;
@@ -7851,7 +7857,10 @@ export class ViewportHost {
const meshes: Array<Mesh<BufferGeometry, Material>> = []; const meshes: Array<Mesh<BufferGeometry, Material>> = [];
if (geometry !== null) { if (geometry !== null) {
const mesh = new Mesh(geometry, this.createRoadJunctionMaterial(junction)); const mesh = new Mesh(
geometry,
this.createRoadJunctionMaterial(junction)
);
mesh.castShadow = false; mesh.castShadow = false;
mesh.receiveShadow = true; mesh.receiveShadow = true;
mesh.userData.splineCorridorJunctionId = junction.id; mesh.userData.splineCorridorJunctionId = junction.id;
@@ -7872,7 +7881,10 @@ export class ViewportHost {
}); });
if (edgeGeometry !== null) { if (edgeGeometry !== null) {
const edgeMesh = new Mesh(edgeGeometry, this.createRoadEdgeMaterial(edge)); const edgeMesh = new Mesh(
edgeGeometry,
this.createRoadEdgeMaterial(edge)
);
edgeMesh.castShadow = false; edgeMesh.castShadow = false;
edgeMesh.receiveShadow = true; edgeMesh.receiveShadow = true;
edgeMesh.userData.splineCorridorJunctionId = junction.id; edgeMesh.userData.splineCorridorJunctionId = junction.id;
@@ -7937,7 +7949,10 @@ export class ViewportHost {
for (const mesh of renderObjects.meshes) { for (const mesh of renderObjects.meshes) {
this.roadSurfaceGroup.add(mesh); this.roadSurfaceGroup.add(mesh);
} }
this.roadSurfaceRenderObjects.set(`junction:${junction.id}`, renderObjects); this.roadSurfaceRenderObjects.set(
`junction:${junction.id}`,
renderObjects
);
} }
this.applyShadowState(); this.applyShadowState();
@@ -8199,7 +8214,8 @@ export class ViewportHost {
resolvedPath.segments[0]!.start.z resolvedPath.segments[0]!.start.z
), ),
...resolvedPath.segments.map( ...resolvedPath.segments.map(
(segment) => new Vector3(segment.end.x, segment.end.y, segment.end.z) (segment) =>
new Vector3(segment.end.x, segment.end.y, segment.end.z)
) )
]; ];
@@ -8234,7 +8250,9 @@ export class ViewportHost {
return mesh; return mesh;
} }
private createPathSegmentMeshes(path: ScenePath): PathRenderObjects["segments"] { private createPathSegmentMeshes(
path: ScenePath
): PathRenderObjects["segments"] {
const resolvedPath = resolveScenePath(path, { const resolvedPath = resolveScenePath(path, {
terrains: this.getPathTerrainGlueTerrains() terrains: this.getPathTerrainGlueTerrains()
}); });
@@ -8373,7 +8391,11 @@ export class ViewportHost {
segment.start.y, segment.start.y,
segment.start.z segment.start.z
); );
const endCenter = new Vector3(segment.end.x, segment.end.y, segment.end.z); const endCenter = new Vector3(
segment.end.x,
segment.end.y,
segment.end.z
);
const leftStart = this.resolvePathRoadPreviewPoint( const leftStart = this.resolvePathRoadPreviewPoint(
path, path,
startCenter.clone().add(perpendicular), startCenter.clone().add(perpendicular),
@@ -8550,7 +8572,11 @@ export class ViewportHost {
}) })
); );
marker.position.set(options.center.x, options.center.y + 0.08, options.center.z); marker.position.set(
options.center.x,
options.center.y + 0.08,
options.center.z
);
marker.rotation.x = Math.PI * 0.5; marker.rotation.x = Math.PI * 0.5;
marker.renderOrder = PATH_RENDER_ORDER + 4; marker.renderOrder = PATH_RENDER_ORDER + 4;
marker.userData.nonPickable = true; marker.userData.nonPickable = true;
@@ -8689,7 +8715,8 @@ export class ViewportHost {
const resolvedPointPositions = const resolvedPointPositions =
this.createResolvedPathPointPositionMap(path); this.createResolvedPathPointPositionMap(path);
const pointMeshes = path.points.map((point) => { const pointMeshes = path.points.map((point) => {
const pointPosition = resolvedPointPositions.get(point.id) ?? point.position; const pointPosition =
resolvedPointPositions.get(point.id) ?? point.position;
const outlineMesh = new Mesh( const outlineMesh = new Mesh(
new SphereGeometry(PATH_POINT_OUTLINE_RADIUS, 16, 12), new SphereGeometry(PATH_POINT_OUTLINE_RADIUS, 16, 12),
new MeshBasicMaterial({ new MeshBasicMaterial({
@@ -8824,7 +8851,8 @@ export class ViewportHost {
continue; continue;
} }
const pointPosition = resolvedPointPositions.get(point.id) ?? point.position; const pointPosition =
resolvedPointPositions.get(point.id) ?? point.position;
pointMesh.outlineMesh.position.set( pointMesh.outlineMesh.position.set(
pointPosition.x, pointPosition.x,
@@ -11546,7 +11574,10 @@ export class ViewportHost {
return null; return null;
} }
const hits = this.raycaster.intersectObjects(renderObjects.pickMeshes, true); const hits = this.raycaster.intersectObjects(
renderObjects.pickMeshes,
true
);
const width = getTerrainFootprintWidth(terrain); const width = getTerrainFootprintWidth(terrain);
const depth = getTerrainFootprintDepth(terrain); const depth = getTerrainFootprintDepth(terrain);
@@ -11660,8 +11691,12 @@ export class ViewportHost {
? terrain.position.z ? terrain.position.z
: terrain.position.z + depth * alpha; : terrain.position.z + depth * alpha;
const height = const height =
sampleDocumentTerrainHeightAtWorldPosition(terrain, worldX, worldZ, true) ?? sampleDocumentTerrainHeightAtWorldPosition(
0; terrain,
worldX,
worldZ,
true
) ?? 0;
return new Vector3( return new Vector3(
worldX, worldX,
@@ -11781,7 +11816,10 @@ export class ViewportHost {
const arrowEnd = midpoint.clone().addScaledVector(direction, arrowLength); const arrowEnd = midpoint.clone().addScaledVector(direction, arrowLength);
const shaftEnd = arrowEnd const shaftEnd = arrowEnd
.clone() .clone()
.addScaledVector(direction, -TERRAIN_GRID_RESIZE_ARROW_HEAD_LENGTH * 0.72); .addScaledVector(
direction,
-TERRAIN_GRID_RESIZE_ARROW_HEAD_LENGTH * 0.72
);
const shaftLength = Math.max(0.1, arrowStart.distanceTo(shaftEnd)); const shaftLength = Math.max(0.1, arrowStart.distanceTo(shaftEnd));
const shaftCenter = arrowStart.clone().add(shaftEnd).multiplyScalar(0.5); const shaftCenter = arrowStart.clone().add(shaftEnd).multiplyScalar(0.5);
@@ -11796,8 +11834,10 @@ export class ViewportHost {
visual.pickShaft.scale.set(1, shaftLength, 1); visual.pickShaft.scale.set(1, shaftLength, 1);
const previousArrowGeometry = visual.line.geometry; const previousArrowGeometry = visual.line.geometry;
visual.line.geometry = visual.line.geometry = new BufferGeometry().setFromPoints([
new BufferGeometry().setFromPoints([arrowStart, arrowEnd]); arrowStart,
arrowEnd
]);
previousArrowGeometry.dispose(); previousArrowGeometry.dispose();
visual.headOutline.position visual.headOutline.position
.copy(arrowEnd) .copy(arrowEnd)
@@ -11984,7 +12024,7 @@ export class ViewportHost {
this.hasTerrainGridResizeChanged( this.hasTerrainGridResizeChanged(
activeDrag.baseTerrain, activeDrag.baseTerrain,
finalPreviewTerrain finalPreviewTerrain
); );
this.activeTerrainGridResizeDrag = null; this.activeTerrainGridResizeDrag = null;
this.terrainGridResizeHover = null; this.terrainGridResizeHover = null;
@@ -12041,8 +12081,7 @@ export class ViewportHost {
} }
const targetTerrainId = this.currentTerrainBrushState.terrainId; const targetTerrainId = this.currentTerrainBrushState.terrainId;
const raycastObjects = const raycastObjects = this.getTerrainBrushRaycastObjects(targetTerrainId);
this.getTerrainBrushRaycastObjects(targetTerrainId);
if (raycastObjects.length === 0) { if (raycastObjects.length === 0) {
return null; return null;

View File

@@ -280,7 +280,9 @@ describe("buildRuntimeSceneFromDocument", () => {
}); });
const runtimeScene = buildRuntimeSceneFromDocument({ const runtimeScene = buildRuntimeSceneFromDocument({
...createEmptySceneDocument({ name: "Mapped Rail Camera Rig Runtime Scene" }), ...createEmptySceneDocument({
name: "Mapped Rail Camera Rig Runtime Scene"
}),
paths: { paths: {
[path.id]: path [path.id]: path
}, },
@@ -683,7 +685,9 @@ describe("buildRuntimeSceneFromDocument", () => {
expect(runtimeScene.time).not.toBe(document.time); expect(runtimeScene.time).not.toBe(document.time);
expect(runtimeScene.world).toEqual(document.world); expect(runtimeScene.world).toEqual(document.world);
expect(runtimeScene.world).not.toBe(document.world); expect(runtimeScene.world).not.toBe(document.world);
expect(runtimeScene.world.sunLight.direction).not.toBe(document.world.sunLight.direction); expect(runtimeScene.world.sunLight.direction).not.toBe(
document.world.sunLight.direction
);
expect(runtimeScene.brushes).toHaveLength(1); expect(runtimeScene.brushes).toHaveLength(1);
expect(runtimeScene.modelInstances).toEqual([ expect(runtimeScene.modelInstances).toEqual([
{ {
@@ -832,7 +836,9 @@ describe("buildRuntimeSceneFromDocument", () => {
y: 0, y: 0,
z: 0 z: 0
}); });
expect(runtimeScene.brushes[0].faces.posY.material?.id).toBe("starter-concrete-checker"); expect(runtimeScene.brushes[0].faces.posY.material?.id).toBe(
"starter-concrete-checker"
);
expect(runtimeScene.colliders).toHaveLength(2); expect(runtimeScene.colliders).toHaveLength(2);
expect(runtimeScene.colliders[0]).toMatchObject({ expect(runtimeScene.colliders[0]).toMatchObject({
kind: "trimesh", kind: "trimesh",
@@ -863,7 +869,9 @@ describe("buildRuntimeSceneFromDocument", () => {
}); });
const brushCollider = runtimeScene.colliders[0]; const brushCollider = runtimeScene.colliders[0];
if (brushCollider.kind !== "trimesh") { if (brushCollider.kind !== "trimesh") {
throw new Error(`Expected a trimesh brush collider, received ${brushCollider.kind}.`); throw new Error(
`Expected a trimesh brush collider, received ${brushCollider.kind}.`
);
} }
expect(Array.from(brushCollider.vertices)).toHaveLength(24); expect(Array.from(brushCollider.vertices)).toHaveLength(24);
expect(Array.from(brushCollider.indices)).toHaveLength(36); expect(Array.from(brushCollider.indices)).toHaveLength(36);
@@ -905,8 +913,7 @@ describe("buildRuntimeSceneFromDocument", () => {
allowLookInputTargetSwitch: playerStart.allowLookInputTargetSwitch, allowLookInputTargetSwitch: playerStart.allowLookInputTargetSwitch,
targetButtonCyclesActiveTarget: targetButtonCyclesActiveTarget:
playerStart.targetButtonCyclesActiveTarget, playerStart.targetButtonCyclesActiveTarget,
invertMouseCameraHorizontal: invertMouseCameraHorizontal: playerStart.invertMouseCameraHorizontal,
playerStart.invertMouseCameraHorizontal,
movement: { movement: {
templateKind: "default", templateKind: "default",
moveSpeed: DEFAULT_PLAYER_START_MOVE_SPEED, moveSpeed: DEFAULT_PLAYER_START_MOVE_SPEED,
@@ -1377,28 +1384,30 @@ describe("buildRuntimeSceneFromDocument", () => {
[overnightNpc.id]: overnightNpc [overnightNpc.id]: overnightNpc
} }
}; };
document.scheduler.routines["routine-daytime"] = createProjectScheduleRoutine({ document.scheduler.routines["routine-daytime"] =
id: "routine-daytime", createProjectScheduleRoutine({
title: "Day Shift", id: "routine-daytime",
target: createActorControlTargetRef(daytimeNpc.actorId), title: "Day Shift",
startHour: 9,
endHour: 17,
effect: createSetActorPresenceControlEffect({
target: createActorControlTargetRef(daytimeNpc.actorId), target: createActorControlTargetRef(daytimeNpc.actorId),
active: true startHour: 9,
}) endHour: 17,
}); effect: createSetActorPresenceControlEffect({
document.scheduler.routines["routine-overnight"] = createProjectScheduleRoutine({ target: createActorControlTargetRef(daytimeNpc.actorId),
id: "routine-overnight", active: true
title: "Night Shift", })
target: createActorControlTargetRef(overnightNpc.actorId), });
startHour: 22, document.scheduler.routines["routine-overnight"] =
endHour: 2, createProjectScheduleRoutine({
effect: createSetActorPresenceControlEffect({ id: "routine-overnight",
title: "Night Shift",
target: createActorControlTargetRef(overnightNpc.actorId), target: createActorControlTargetRef(overnightNpc.actorId),
active: true startHour: 22,
}) endHour: 2,
}); effect: createSetActorPresenceControlEffect({
target: createActorControlTargetRef(overnightNpc.actorId),
active: true
})
});
const daytimeRuntimeScene = buildRuntimeSceneFromDocument(document, { const daytimeRuntimeScene = buildRuntimeSceneFromDocument(document, {
runtimeClock: { runtimeClock: {
@@ -1625,9 +1634,12 @@ describe("buildRuntimeSceneFromDocument", () => {
it("blocks first-person runtime builds when PlayerStart is missing", () => { it("blocks first-person runtime builds when PlayerStart is missing", () => {
expect(() => expect(() =>
buildRuntimeSceneFromDocument(createEmptySceneDocument({ name: "Missing Player Start" }), { buildRuntimeSceneFromDocument(
navigationMode: "firstPerson" createEmptySceneDocument({ name: "Missing Player Start" }),
}) {
navigationMode: "firstPerson"
}
)
).toThrow("First-person run requires an authored Player Start"); ).toThrow("First-person run requires an authored Player Start");
}); });
@@ -1765,7 +1777,9 @@ describe("buildRuntimeSceneFromDocument", () => {
); );
expect(runtimeScene.playerStart?.entityId).toBe(enabledPlayerStart.id); expect(runtimeScene.playerStart?.entityId).toBe(enabledPlayerStart.id);
expect(runtimeScene.brushes.map((brush) => brush.id)).toEqual([hiddenBrush.id]); expect(runtimeScene.brushes.map((brush) => brush.id)).toEqual([
hiddenBrush.id
]);
expect(runtimeScene.brushes[0]?.visible).toBe(false); expect(runtimeScene.brushes[0]?.visible).toBe(false);
expect(runtimeScene.modelInstances).toEqual([ expect(runtimeScene.modelInstances).toEqual([
expect.objectContaining({ expect.objectContaining({
@@ -1856,7 +1870,10 @@ describe("buildRuntimeSceneFromDocument", () => {
z: 8 z: 8
} }
}); });
const { asset, loadedAsset } = createFixtureLoadedModelAssetFromGeometry("asset-runtime-collider", new BoxGeometry(1, 2, 1)); const { asset, loadedAsset } = createFixtureLoadedModelAssetFromGeometry(
"asset-runtime-collider",
new BoxGeometry(1, 2, 1)
);
const modelInstance = createModelInstance({ const modelInstance = createModelInstance({
id: "model-instance-runtime-collider", id: "model-instance-runtime-collider",
assetId: asset.id, assetId: asset.id,
@@ -1944,7 +1961,9 @@ describe("buildRuntimeSceneFromDocument", () => {
runtimeScene.colliders[0] === undefined || runtimeScene.colliders[0] === undefined ||
runtimeScene.colliders[0].source !== "terrain" runtimeScene.colliders[0].source !== "terrain"
) { ) {
throw new Error("Expected the runtime collider to be an authored terrain heightfield."); throw new Error(
"Expected the runtime collider to be an authored terrain heightfield."
);
} }
expect(Array.from(runtimeScene.colliders[0].heights)).toEqual( expect(Array.from(runtimeScene.colliders[0].heights)).toEqual(
@@ -2142,13 +2161,13 @@ describe("buildRuntimeSceneFromDocument", () => {
}) })
}) })
]); ]);
expect(runtimeScene.splineCorridorClipIntervalsByPath.get(pathA.id)).toEqual( expect(
[ runtimeScene.splineCorridorClipIntervalsByPath.get(pathA.id)
expect.objectContaining({ ).toEqual([
junctionId: junction.id expect.objectContaining({
}) junctionId: junction.id
] })
); ]);
}); });
it("adds spline corridor box colliders for collision-enabled edges and repeaters", () => { it("adds spline corridor box colliders for collision-enabled edges and repeaters", () => {
@@ -2249,7 +2268,10 @@ describe("buildRuntimeSceneFromDocument", () => {
it("adds static-simple imported-model colliders as compound box pieces", () => { it("adds static-simple imported-model colliders as compound box pieces", () => {
const wallGeometry = new PlaneGeometry(4, 4, 4, 4); const wallGeometry = new PlaneGeometry(4, 4, 4, 4);
wallGeometry.rotateY(Math.PI * 0.5); wallGeometry.rotateY(Math.PI * 0.5);
const { asset, loadedAsset } = createFixtureLoadedModelAssetFromGeometry("asset-runtime-static-simple", wallGeometry); const { asset, loadedAsset } = createFixtureLoadedModelAssetFromGeometry(
"asset-runtime-static-simple",
wallGeometry
);
const modelInstance = createModelInstance({ const modelInstance = createModelInstance({
id: "model-instance-runtime-static-simple", id: "model-instance-runtime-static-simple",
assetId: asset.id, assetId: asset.id,
@@ -2266,7 +2288,9 @@ describe("buildRuntimeSceneFromDocument", () => {
const runtimeScene = buildRuntimeSceneFromDocument( const runtimeScene = buildRuntimeSceneFromDocument(
{ {
...createEmptySceneDocument({ name: "Imported Static Simple Collider Scene" }), ...createEmptySceneDocument({
name: "Imported Static Simple Collider Scene"
}),
assets: { assets: {
[asset.id]: asset [asset.id]: asset
}, },
@@ -2292,11 +2316,18 @@ describe("buildRuntimeSceneFromDocument", () => {
decomposition: "surface-voxel-boxes" decomposition: "surface-voxel-boxes"
}); });
if (runtimeScene.colliders[0].source !== "modelInstance" || runtimeScene.colliders[0].kind !== "compound") { if (
throw new Error("Expected the runtime collider to be a generated compound model collider."); runtimeScene.colliders[0].source !== "modelInstance" ||
runtimeScene.colliders[0].kind !== "compound"
) {
throw new Error(
"Expected the runtime collider to be a generated compound model collider."
);
} }
expect(runtimeScene.colliders[0].pieces.every((piece) => piece.kind === "box")).toBe(true); expect(
runtimeScene.colliders[0].pieces.every((piece) => piece.kind === "box")
).toBe(true);
}); });
it("preserves rotated whitebox box transforms for runner rendering and collision bounds", () => { it("preserves rotated whitebox box transforms for runner rendering and collision bounds", () => {
@@ -2537,31 +2568,32 @@ describe("buildRuntimeSceneFromDocument", () => {
document.assets[asset.id] = asset; document.assets[asset.id] = asset;
document.entities[npc.id] = npc; document.entities[npc.id] = npc;
document.paths[path.id] = path; document.paths[path.id] = path;
document.scheduler.routines["routine-patrol"] = createProjectScheduleRoutine({ document.scheduler.routines["routine-patrol"] =
id: "routine-patrol", createProjectScheduleRoutine({
title: "Patrolling", id: "routine-patrol",
target: actorTarget, title: "Patrolling",
startHour: 9, target: actorTarget,
endHour: 13, startHour: 9,
effects: [ endHour: 13,
createSetActorPresenceControlEffect({ effects: [
target: actorTarget, createSetActorPresenceControlEffect({
active: true target: actorTarget,
}), active: true
createPlayActorAnimationControlEffect({ }),
target: actorTarget, createPlayActorAnimationControlEffect({
clipName: "Walk", target: actorTarget,
loop: true clipName: "Walk",
}), loop: true
createFollowActorPathControlEffect({ }),
target: actorTarget, createFollowActorPathControlEffect({
pathId: path.id, target: actorTarget,
speed: 2, pathId: path.id,
loop: false, speed: 2,
progressMode: "deriveFromTime" loop: false,
}) progressMode: "deriveFromTime"
] })
}); ]
});
const runtimeScene = buildRuntimeSceneFromDocument(document, { const runtimeScene = buildRuntimeSceneFromDocument(document, {
runtimeClock: { runtimeClock: {

View File

@@ -9,10 +9,7 @@ import {
createProjectAssetStorageKey, createProjectAssetStorageKey,
type ImageAssetRecord type ImageAssetRecord
} from "../../src/assets/project-assets"; } from "../../src/assets/project-assets";
import { import { BOX_FACE_IDS, type WhiteboxFaceId } from "../../src/document/brushes";
BOX_FACE_IDS,
type WhiteboxFaceId
} from "../../src/document/brushes";
import { createCustomMaterialDef } from "../../src/materials/starter-material-library"; import { createCustomMaterialDef } from "../../src/materials/starter-material-library";
function createImageAsset(id: string): ImageAssetRecord { function createImageAsset(id: string): ImageAssetRecord {

View File

@@ -318,7 +318,9 @@ describe("deleteProjectAssetFromProjectDocument", () => {
); );
const nextScene = nextProjectDocument.scenes[fixture.sceneId]; const nextScene = nextProjectDocument.scenes[fixture.sceneId];
expect(nextProjectDocument.foliagePrototypes[foliagePrototype.id]).toBeUndefined(); expect(
nextProjectDocument.foliagePrototypes[foliagePrototype.id]
).toBeUndefined();
expect(nextScene.foliageLayers[foliageLayer.id]?.prototypeIds).toEqual([ expect(nextScene.foliageLayers[foliageLayer.id]?.prototypeIds).toEqual([
bundledPrototype.id bundledPrototype.id
]); ]);

View File

@@ -140,9 +140,7 @@ describe("validateSceneDocument", () => {
id: "asset-invalid-material-map-hdr", id: "asset-invalid-material-map-hdr",
sourceName: "studio.hdr", sourceName: "studio.hdr",
mimeType: "image/vnd.radiance", mimeType: "image/vnd.radiance",
storageKey: createProjectAssetStorageKey( storageKey: createProjectAssetStorageKey("asset-invalid-material-map-hdr")
"asset-invalid-material-map-hdr"
)
}; };
const audioAsset: AudioAssetRecord = { const audioAsset: AudioAssetRecord = {
id: "asset-invalid-material-map-audio", id: "asset-invalid-material-map-audio",

View File

@@ -218,9 +218,9 @@ describe("project document JSON", () => {
document.scenes[document.activeSceneId]!.foliageLayers[layer.id] = layer; document.scenes[document.activeSceneId]!.foliageLayers[layer.id] = layer;
expect(parseProjectDocumentJson(serializeProjectDocument(document))).toEqual( expect(
document parseProjectDocumentJson(serializeProjectDocument(document))
); ).toEqual(document);
}); });
it("round-trips terrain foliage masks in project scenes", () => { it("round-trips terrain foliage masks in project scenes", () => {
@@ -256,9 +256,9 @@ describe("project document JSON", () => {
scene.foliageLayers[layer.id] = layer; scene.foliageLayers[layer.id] = layer;
scene.terrains[terrain.id] = terrain; scene.terrains[terrain.id] = terrain;
expect(parseProjectDocumentJson(serializeProjectDocument(document))).toEqual( expect(
document parseProjectDocumentJson(serializeProjectDocument(document))
); ).toEqual(document);
}); });
it("round-trips NPC dialogue references in project scenes", () => { it("round-trips NPC dialogue references in project scenes", () => {

View File

@@ -316,9 +316,7 @@ describe("project package serialization", () => {
kind: "image", kind: "image",
sourceName: "custom-material-map.png", sourceName: "custom-material-map.png",
mimeType: "image/png", mimeType: "image/png",
storageKey: createProjectAssetStorageKey( storageKey: createProjectAssetStorageKey("asset-image-material-package"),
"asset-image-material-package"
),
byteLength: 4, byteLength: 4,
metadata: { metadata: {
kind: "image", kind: "image",
@@ -373,7 +371,9 @@ describe("project package serialization", () => {
"assets/asset-image-material-package/custom-material-map.png" "assets/asset-image-material-package/custom-material-map.png"
]) ])
); );
await expect(restoredStorage.getAsset(imageAsset.storageKey)).resolves.toEqual({ await expect(
restoredStorage.getAsset(imageAsset.storageKey)
).resolves.toEqual({
files: { files: {
[imageAsset.sourceName]: { [imageAsset.sourceName]: {
bytes: expect.any(ArrayBuffer), bytes: expect.any(ArrayBuffer),

View File

@@ -186,22 +186,7 @@ describe("scene document JSON", () => {
} }
], ],
paintWeights: [ paintWeights: [
0.2, 0.2, 0.3, 0.1, 0, 0.4, 0.1, 0, 0, 0, 0.5, 0, 0, 0.25, 0.25, 0.25, 0
0.3,
0.1,
0,
0.4,
0.1,
0,
0,
0,
0.5,
0,
0,
0.25,
0.25,
0.25,
0
] ]
}); });
const document = createEmptySceneDocument({ const document = createEmptySceneDocument({
@@ -1073,9 +1058,9 @@ describe("scene document JSON", () => {
expect( expect(
migratedDocument.paths[path.id]?.road.edges.right.collisionEnabled migratedDocument.paths[path.id]?.road.edges.right.collisionEnabled
).toBe(false); ).toBe(false);
expect(migratedDocument.paths[path.id]?.repeaters[0]?.collisionEnabled).toBe( expect(
false migratedDocument.paths[path.id]?.repeaters[0]?.collisionEnabled
); ).toBe(false);
expect(SPLINE_CORRIDOR_COLLISION_SCENE_DOCUMENT_VERSION).toBe(106); expect(SPLINE_CORRIDOR_COLLISION_SCENE_DOCUMENT_VERSION).toBe(106);
}); });
@@ -1175,7 +1160,8 @@ describe("scene document JSON", () => {
splineCorridorJunctions: Record<string, { shapeMode?: unknown }>; splineCorridorJunctions: Record<string, { shapeMode?: unknown }>;
}; };
legacyDocument.version = SPLINE_CORRIDOR_JUNCTION_EDGE_SCENE_DOCUMENT_VERSION; legacyDocument.version =
SPLINE_CORRIDOR_JUNCTION_EDGE_SCENE_DOCUMENT_VERSION;
delete legacyDocument.splineCorridorJunctions[junction.id]?.shapeMode; delete legacyDocument.splineCorridorJunctions[junction.id]?.shapeMode;
const migratedDocument = migrateSceneDocument(legacyDocument); const migratedDocument = migrateSceneDocument(legacyDocument);
@@ -3276,12 +3262,13 @@ describe("scene document JSON", () => {
document.brushes[targetBrush.id] = targetBrush; document.brushes[targetBrush.id] = targetBrush;
document.entities[linkedNpc.id] = linkedNpc; document.entities[linkedNpc.id] = linkedNpc;
document.entities[silentNpc.id] = silentNpc; document.entities[silentNpc.id] = silentNpc;
document.interactionLinks["link-npc-toggle"] = createToggleVisibilityInteractionLink({ document.interactionLinks["link-npc-toggle"] =
id: "link-npc-toggle", createToggleVisibilityInteractionLink({
sourceEntityId: linkedNpc.id, id: "link-npc-toggle",
trigger: "click", sourceEntityId: linkedNpc.id,
targetBrushId: targetBrush.id trigger: "click",
}); targetBrushId: targetBrush.id
});
const legacyDocument = JSON.parse( const legacyDocument = JSON.parse(
serializeSceneDocument(document) serializeSceneDocument(document)

View File

@@ -143,18 +143,19 @@ describe("whitebox material inspector", () => {
.mockImplementation(() => undefined); .mockImplementation(() => undefined);
const { container, store, brush } = await renderMaterialInspectorFixture(); const { container, store, brush } = await renderMaterialInspectorFixture();
expect( expect(container.querySelector(".material-browser button")).toHaveAttribute(
container.querySelector(".material-browser button") "data-testid",
).toHaveAttribute("data-testid", "material-button-default-whitebox"); "material-button-default-whitebox"
expect(screen.getByTestId("material-button-default-whitebox")).toHaveTextContent(
"Default Whitebox"
); );
expect(
screen.getByTestId("material-button-default-whitebox")
).toHaveTextContent("Default Whitebox");
fireEvent.click(screen.getByTestId("create-custom-material")); fireEvent.click(screen.getByTestId("create-custom-material"));
const customMaterial = Object.values(store.getState().document.materials).find( const customMaterial = Object.values(
(material) => material.kind === "custom" store.getState().document.materials
); ).find((material) => material.kind === "custom");
expect(customMaterial).toBeDefined(); expect(customMaterial).toBeDefined();
expect( expect(
@@ -186,19 +187,23 @@ describe("whitebox material inspector", () => {
throw new Error("Expected a custom material to be created."); throw new Error("Expected a custom material to be created.");
} }
expect(store.getState().document.materials[editedMaterialId]).toMatchObject({ expect(store.getState().document.materials[editedMaterialId]).toMatchObject(
kind: "custom", {
albedoColorHex: "#224466", kind: "custom",
roughness: 0.37, albedoColorHex: "#224466",
metallic: 0.58 roughness: 0.37,
}); metallic: 0.58
}
);
const exportedProject = store.exportDocumentJson(); const exportedProject = store.exportDocumentJson();
const reloadedStore = createEditorStore(); const reloadedStore = createEditorStore();
reloadedStore.importDocumentJson(exportedProject); reloadedStore.importDocumentJson(exportedProject);
expect(reloadedStore.getState().document.materials[editedMaterialId]).toMatchObject({ expect(
reloadedStore.getState().document.materials[editedMaterialId]
).toMatchObject({
kind: "custom", kind: "custom",
albedoColorHex: "#224466", albedoColorHex: "#224466",
roughness: 0.37, roughness: 0.37,