Add tests for playAnimation interaction link validation and runtime dispatch
This commit is contained in:
@@ -3,8 +3,14 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { createBoxBrush } from "../../src/document/brushes";
|
import { createBoxBrush } from "../../src/document/brushes";
|
||||||
import { createEmptySceneDocument } from "../../src/document/scene-document";
|
import { createEmptySceneDocument } from "../../src/document/scene-document";
|
||||||
import { validateSceneDocument } from "../../src/document/scene-document-validation";
|
import { validateSceneDocument } from "../../src/document/scene-document-validation";
|
||||||
|
import { createModelInstance } from "../../src/assets/model-instances";
|
||||||
|
import { createProjectAssetStorageKey, type ModelAssetRecord } from "../../src/assets/project-assets";
|
||||||
import { createInteractableEntity, createPlayerStartEntity, createTeleportTargetEntity, createTriggerVolumeEntity } from "../../src/entities/entity-instances";
|
import { createInteractableEntity, createPlayerStartEntity, createTeleportTargetEntity, createTriggerVolumeEntity } from "../../src/entities/entity-instances";
|
||||||
import { createTeleportPlayerInteractionLink, createToggleVisibilityInteractionLink } from "../../src/interactions/interaction-links";
|
import {
|
||||||
|
createPlayAnimationInteractionLink,
|
||||||
|
createTeleportPlayerInteractionLink,
|
||||||
|
createToggleVisibilityInteractionLink
|
||||||
|
} from "../../src/interactions/interaction-links";
|
||||||
|
|
||||||
describe("interaction link validation", () => {
|
describe("interaction link validation", () => {
|
||||||
it("accepts valid Trigger Volume and Interactable links", () => {
|
it("accepts valid Trigger Volume and Interactable links", () => {
|
||||||
@@ -125,4 +131,67 @@ describe("interaction link validation", () => {
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("detects playAnimation links that reference a missing clip on the target model asset", () => {
|
||||||
|
const modelAsset = {
|
||||||
|
id: "asset-model-animated",
|
||||||
|
kind: "model",
|
||||||
|
sourceName: "animated.glb",
|
||||||
|
mimeType: "model/gltf-binary",
|
||||||
|
storageKey: createProjectAssetStorageKey("asset-model-animated"),
|
||||||
|
byteLength: 1024,
|
||||||
|
metadata: {
|
||||||
|
kind: "model" as const,
|
||||||
|
format: "glb" as const,
|
||||||
|
sceneName: null,
|
||||||
|
nodeCount: 1,
|
||||||
|
meshCount: 1,
|
||||||
|
materialNames: [],
|
||||||
|
textureNames: [],
|
||||||
|
animationNames: ["Idle", "Run"],
|
||||||
|
boundingBox: null,
|
||||||
|
warnings: []
|
||||||
|
}
|
||||||
|
} satisfies ModelAssetRecord;
|
||||||
|
const modelInstance = createModelInstance({
|
||||||
|
id: "model-instance-animated",
|
||||||
|
assetId: modelAsset.id
|
||||||
|
});
|
||||||
|
const triggerVolume = createTriggerVolumeEntity({
|
||||||
|
id: "entity-trigger-main"
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = {
|
||||||
|
...createEmptySceneDocument(),
|
||||||
|
assets: {
|
||||||
|
[modelAsset.id]: modelAsset
|
||||||
|
},
|
||||||
|
modelInstances: {
|
||||||
|
[modelInstance.id]: modelInstance
|
||||||
|
},
|
||||||
|
entities: {
|
||||||
|
[triggerVolume.id]: triggerVolume
|
||||||
|
},
|
||||||
|
interactionLinks: {
|
||||||
|
"link-play-missing-clip": createPlayAnimationInteractionLink({
|
||||||
|
id: "link-play-missing-clip",
|
||||||
|
sourceEntityId: triggerVolume.id,
|
||||||
|
trigger: "enter",
|
||||||
|
targetModelInstanceId: modelInstance.id,
|
||||||
|
clipName: "Walk"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validation = validateSceneDocument(document);
|
||||||
|
|
||||||
|
expect(validation.errors).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
code: "missing-play-animation-clip",
|
||||||
|
path: "interactionLinks.link-play-missing-clip.action.clipName"
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { createTeleportPlayerInteractionLink, createToggleVisibilityInteractionLink } from "../../src/interactions/interaction-links";
|
import {
|
||||||
|
createPlayAnimationInteractionLink,
|
||||||
|
createTeleportPlayerInteractionLink,
|
||||||
|
createToggleVisibilityInteractionLink,
|
||||||
|
createStopAnimationInteractionLink
|
||||||
|
} from "../../src/interactions/interaction-links";
|
||||||
import { RuntimeInteractionSystem } from "../../src/runtime-three/runtime-interaction-system";
|
import { RuntimeInteractionSystem } from "../../src/runtime-three/runtime-interaction-system";
|
||||||
import type { RuntimeSceneDefinition } from "../../src/runtime-three/runtime-scene-build";
|
import type { RuntimeSceneDefinition } from "../../src/runtime-three/runtime-scene-build";
|
||||||
|
|
||||||
@@ -28,6 +33,10 @@ function createRuntimeSceneFixture(): RuntimeSceneDefinition {
|
|||||||
brushes: [],
|
brushes: [],
|
||||||
colliders: [],
|
colliders: [],
|
||||||
sceneBounds: null,
|
sceneBounds: null,
|
||||||
|
localLights: {
|
||||||
|
pointLights: [],
|
||||||
|
spotLights: []
|
||||||
|
},
|
||||||
modelInstances: [],
|
modelInstances: [],
|
||||||
entities: {
|
entities: {
|
||||||
playerStarts: [],
|
playerStarts: [],
|
||||||
@@ -155,6 +164,49 @@ describe("RuntimeInteractionSystem", () => {
|
|||||||
expect(dispatches).toEqual(["link-teleport:entity-teleport-main:8"]);
|
expect(dispatches).toEqual(["link-teleport:entity-teleport-main:8"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("dispatches animation actions with the authored target model instance and clip", () => {
|
||||||
|
const runtimeScene = createRuntimeSceneFixture();
|
||||||
|
runtimeScene.interactionLinks = [
|
||||||
|
createPlayAnimationInteractionLink({
|
||||||
|
id: "link-play-animation",
|
||||||
|
sourceEntityId: "entity-interactable-console",
|
||||||
|
trigger: "click",
|
||||||
|
targetModelInstanceId: "model-instance-animated",
|
||||||
|
clipName: "Walk",
|
||||||
|
loop: false
|
||||||
|
}),
|
||||||
|
createStopAnimationInteractionLink({
|
||||||
|
id: "link-stop-animation",
|
||||||
|
sourceEntityId: "entity-interactable-console",
|
||||||
|
trigger: "click",
|
||||||
|
targetModelInstanceId: "model-instance-animated"
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const interactionSystem = new RuntimeInteractionSystem();
|
||||||
|
const dispatches: string[] = [];
|
||||||
|
|
||||||
|
interactionSystem.dispatchClickInteraction("entity-interactable-console", runtimeScene, {
|
||||||
|
teleportPlayer: () => {
|
||||||
|
throw new Error("Teleport should not dispatch in this fixture.");
|
||||||
|
},
|
||||||
|
toggleBrushVisibility: () => {
|
||||||
|
throw new Error("Visibility should not dispatch in this fixture.");
|
||||||
|
},
|
||||||
|
playAnimation: (instanceId, clipName, loop, link) => {
|
||||||
|
dispatches.push(`${link.id}:${instanceId}:${clipName}:${loop === false ? "once" : "loop"}`);
|
||||||
|
},
|
||||||
|
stopAnimation: (instanceId, link) => {
|
||||||
|
dispatches.push(`${link.id}:${instanceId}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(dispatches).toEqual([
|
||||||
|
"link-play-animation:model-instance-animated:Walk:once",
|
||||||
|
"link-stop-animation:model-instance-animated"
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("dispatches visibility actions only when exiting an occupied Trigger Volume", () => {
|
it("dispatches visibility actions only when exiting an occupied Trigger Volume", () => {
|
||||||
const runtimeScene = createRuntimeSceneFixture();
|
const runtimeScene = createRuntimeSceneFixture();
|
||||||
runtimeScene.interactionLinks = [
|
runtimeScene.interactionLinks = [
|
||||||
|
|||||||
@@ -180,7 +180,8 @@ describe("scene document JSON", () => {
|
|||||||
};
|
};
|
||||||
document.world.background = {
|
document.world.background = {
|
||||||
mode: "image",
|
mode: "image",
|
||||||
assetId: imageAsset.id
|
assetId: imageAsset.id,
|
||||||
|
environmentIntensity: 0.75
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
|
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
|
||||||
@@ -630,7 +631,8 @@ describe("scene document JSON", () => {
|
|||||||
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
||||||
expect(migratedDocument.world.background).toEqual({
|
expect(migratedDocument.world.background).toEqual({
|
||||||
mode: "image",
|
mode: "image",
|
||||||
assetId: imageAsset.id
|
assetId: imageAsset.id,
|
||||||
|
environmentIntensity: 0.5
|
||||||
});
|
});
|
||||||
expect(migratedDocument.entities[pointLight.id]).toEqual(pointLight);
|
expect(migratedDocument.entities[pointLight.id]).toEqual(pointLight);
|
||||||
expect(migratedDocument.entities[spotLight.id]).toEqual(spotLight);
|
expect(migratedDocument.entities[spotLight.id]).toEqual(spotLight);
|
||||||
@@ -792,7 +794,7 @@ describe("scene document JSON", () => {
|
|||||||
boundingBox: null,
|
boundingBox: null,
|
||||||
warnings: []
|
warnings: []
|
||||||
}
|
}
|
||||||
};
|
} satisfies ModelAssetRecord;
|
||||||
|
|
||||||
const migratedDocument = migrateSceneDocument({
|
const migratedDocument = migrateSceneDocument({
|
||||||
version: 11,
|
version: 11,
|
||||||
|
|||||||
Reference in New Issue
Block a user