diff --git a/src/document/migrate-scene-document.ts b/src/document/migrate-scene-document.ts index 5d3a826b..59b8a340 100644 --- a/src/document/migrate-scene-document.ts +++ b/src/document/migrate-scene-document.ts @@ -1489,7 +1489,7 @@ export function migrateSceneDocument(source: unknown): SceneDocument { const assets = readAssets(source.assets); return { - version: ENTITY_NAMES_SCENE_DOCUMENT_VERSION, + version: SCENE_DOCUMENT_VERSION, name: expectString(source.name, "name"), world: readWorldSettings(source.world), materials, diff --git a/src/geometry/model-instance-collider-generation.ts b/src/geometry/model-instance-collider-generation.ts index f6d64c71..e29ebd86 100644 --- a/src/geometry/model-instance-collider-generation.ts +++ b/src/geometry/model-instance-collider-generation.ts @@ -6,7 +6,6 @@ import { Mesh, Quaternion, Vector3, - type BufferAttribute, type BufferGeometry } from "three"; @@ -206,7 +205,13 @@ function computeWorldBoundsFromLocalBox(localBounds: GeneratedColliderBounds, mo return computeBoundsFromPoints(corners.map((corner) => corner.applyMatrix4(modelMatrix))); } -function readIndexedVertex(position: BufferAttribute, index: number, matrix: Matrix4): Vector3 { +interface PositionLikeAttribute { + getX(index: number): number; + getY(index: number): number; + getZ(index: number): number; +} + +function readIndexedVertex(position: PositionLikeAttribute, index: number, matrix: Matrix4): Vector3 { return new Vector3(position.getX(index), position.getY(index), position.getZ(index)).applyMatrix4(matrix); } diff --git a/src/runtime-three/rapier-collision-world.ts b/src/runtime-three/rapier-collision-world.ts index a4e4ef74..db68e6d0 100644 --- a/src/runtime-three/rapier-collision-world.ts +++ b/src/runtime-three/rapier-collision-world.ts @@ -1,5 +1,5 @@ import RAPIER from "@dimforge/rapier3d-compat"; -import { Euler, MathUtils, Quaternion, Vector3 } from "three"; +import { Euler, MathUtils, Quaternion } from "three"; import type { Vec3 } from "../core/vector"; import type { @@ -18,14 +18,6 @@ const COLLISION_EPSILON = 1e-5; let rapierInitPromise: Promise | null = null; -function cloneVec3(vector: Vec3): Vec3 { - return { - x: vector.x, - y: vector.y, - z: vector.z - }; -} - function componentScale(vector: Vec3, scale: Vec3): Vec3 { return { x: vector.x * scale.x, diff --git a/src/runtime-three/runtime-host.ts b/src/runtime-three/runtime-host.ts index edeef583..7ac06bd4 100644 --- a/src/runtime-three/runtime-host.ts +++ b/src/runtime-three/runtime-host.ts @@ -510,7 +510,11 @@ export class RuntimeHost { name: modelInstance.name, position: modelInstance.position, rotationDegrees: modelInstance.rotationDegrees, - scale: modelInstance.scale + scale: modelInstance.scale, + collision: { + mode: "none", + visible: false + } }, asset, loadedAsset, diff --git a/tests/domain/build-runtime-scene.test.ts b/tests/domain/build-runtime-scene.test.ts index c0ff5c1e..49cc2fa9 100644 --- a/tests/domain/build-runtime-scene.test.ts +++ b/tests/domain/build-runtime-scene.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { BoxGeometry } from "three"; import { createBoxBrush } from "../../src/document/brushes"; import { createEmptySceneDocument } from "../../src/document/scene-document"; @@ -15,6 +16,7 @@ import { createTeleportPlayerInteractionLink, createToggleVisibilityInteractionL import { createModelInstance } from "../../src/assets/model-instances"; import { createProjectAssetStorageKey, type AudioAssetRecord } from "../../src/assets/project-assets"; import { buildRuntimeSceneFromDocument } from "../../src/runtime-three/runtime-scene-build"; +import { createFixtureLoadedModelAssetFromGeometry } from "../helpers/model-collider-fixtures"; describe("buildRuntimeSceneFromDocument", () => { it("builds runtime brush data, colliders, and an authored player spawn from the document", () => { @@ -286,6 +288,7 @@ describe("buildRuntimeSceneFromDocument", () => { expect(runtimeScene.colliders).toEqual([ { kind: "box", + source: "brush", brushId: "brush-room-floor", min: { x: -4, @@ -526,4 +529,65 @@ describe("buildRuntimeSceneFromDocument", () => { }) ).toThrow("First-person run requires an authored Player Start"); }); + + it("adds generated imported-model colliders to the runtime scene build", () => { + const floorBrush = createBoxBrush({ + id: "brush-runtime-floor", + center: { + x: 0, + y: -0.5, + z: 0 + }, + size: { + x: 8, + y: 1, + z: 8 + } + }); + const { asset, loadedAsset } = createFixtureLoadedModelAssetFromGeometry("asset-runtime-collider", new BoxGeometry(1, 2, 1)); + const modelInstance = createModelInstance({ + id: "model-instance-runtime-collider", + assetId: asset.id, + position: { + x: 2, + y: 1, + z: 0 + }, + collision: { + mode: "static", + visible: true + } + }); + + const runtimeScene = buildRuntimeSceneFromDocument( + { + ...createEmptySceneDocument({ name: "Imported Collider Scene" }), + assets: { + [asset.id]: asset + }, + brushes: { + [floorBrush.id]: floorBrush + }, + modelInstances: { + [modelInstance.id]: modelInstance + } + }, + { + loadedModelAssets: { + [asset.id]: loadedAsset + } + } + ); + + expect(runtimeScene.colliders).toHaveLength(2); + expect(runtimeScene.colliders[1]).toMatchObject({ + source: "modelInstance", + instanceId: modelInstance.id, + assetId: asset.id, + kind: "trimesh", + mode: "static", + visible: true + }); + expect(runtimeScene.sceneBounds?.max.y).toBeGreaterThanOrEqual(2); + }); }); diff --git a/tests/serialization/scene-document-json.test.ts b/tests/serialization/scene-document-json.test.ts index bf9dae9f..659002db 100644 --- a/tests/serialization/scene-document-json.test.ts +++ b/tests/serialization/scene-document-json.test.ts @@ -585,6 +585,64 @@ describe("scene document JSON", () => { expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document); }); + it("round-trips authored model-instance collision settings", () => { + const asset = { + id: "asset-model-collider", + kind: "model", + sourceName: "collision-test.glb", + mimeType: "model/gltf-binary", + storageKey: createProjectAssetStorageKey("asset-model-collider"), + byteLength: 64, + metadata: { + kind: "model", + format: "glb", + sceneName: "Collision Test Scene", + nodeCount: 1, + meshCount: 1, + materialNames: [], + textureNames: [], + animationNames: [], + boundingBox: { + min: { + x: -1, + y: 0, + z: -1 + }, + max: { + x: 1, + y: 2, + z: 1 + }, + size: { + x: 2, + y: 2, + z: 2 + } + }, + warnings: [] + } + } satisfies ModelAssetRecord; + const modelInstance = createModelInstance({ + id: "model-instance-collider", + assetId: asset.id, + collision: { + mode: "dynamic", + visible: true + } + }); + const document = { + ...createEmptySceneDocument({ name: "Model Collision Scene" }), + assets: { + [asset.id]: asset + }, + modelInstances: { + [modelInstance.id]: modelInstance + } + }; + + expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document); + }); + it("round-trips canonical interaction links", () => { const triggerVolume = createTriggerVolumeEntity({ id: "entity-trigger-main"