auto-git:

[change] src/app/app.css
 [change] src/commands/set-scene-loading-screen-command.ts
 [change] src/document/migrate-scene-document.ts
 [change] src/document/scene-document-validation.ts
 [change] src/document/scene-document.ts
 [change] src/runner-web/RunnerCanvas.tsx
 [change] src/runtime-three/first-person-navigation-controller.ts
 [change] src/runtime-three/navigation-controller.ts
 [change] src/runtime-three/orbit-visitor-navigation-controller.ts
 [change] src/runtime-three/runtime-host.ts
 [change] tests/domain/editor-store.test.ts
 [change] tests/serialization/local-draft-storage.test.ts
 [change] tests/serialization/project-document-json.test.ts
 [change] tests/serialization/project-package.test.ts
 [change] tests/unit/runner-canvas.test.tsx
 [change] tests/unit/runtime-host.test.ts
This commit is contained in:
2026-04-11 04:19:50 +02:00
parent 75986da19d
commit e205cea50c
16 changed files with 2232 additions and 563 deletions

View File

@@ -5,11 +5,21 @@ import { strToU8, unzipSync, Zip, ZipDeflate } from "fflate";
import { afterEach, describe, expect, it, vi } from "vitest";
import { loadAudioAssetFromStorage } from "../../src/assets/audio-assets";
import { loadModelAssetFromStorage, importModelAssetFromFiles } from "../../src/assets/gltf-model-import";
import {
loadModelAssetFromStorage,
importModelAssetFromFiles
} from "../../src/assets/gltf-model-import";
import { loadImageAssetFromStorage } from "../../src/assets/image-assets";
import { createModelInstance } from "../../src/assets/model-instances";
import { createInMemoryProjectAssetStorage, type ProjectAssetStorage } from "../../src/assets/project-asset-storage";
import { createProjectAssetStorageKey, type AudioAssetRecord, type ImageAssetRecord } from "../../src/assets/project-assets";
import {
createInMemoryProjectAssetStorage,
type ProjectAssetStorage
} from "../../src/assets/project-asset-storage";
import {
createProjectAssetStorageKey,
type AudioAssetRecord,
type ImageAssetRecord
} from "../../src/assets/project-assets";
import {
createEmptyProjectScene,
createEmptySceneDocument,
@@ -22,11 +32,24 @@ import {
} from "../../src/serialization/project-package";
import { serializeProjectDocument } from "../../src/serialization/scene-document-json";
const tinyGlbFixturePath = path.resolve(process.cwd(), "fixtures/assets/tiny-triangle.glb");
const externalTriangleGltfPath = path.resolve(process.cwd(), "fixtures/assets/external-triangle/scene.gltf");
const externalTriangleBinPath = path.resolve(process.cwd(), "fixtures/assets/external-triangle/triangle.bin");
const tinyGlbFixturePath = path.resolve(
process.cwd(),
"fixtures/assets/tiny-triangle.glb"
);
const externalTriangleGltfPath = path.resolve(
process.cwd(),
"fixtures/assets/external-triangle/scene.gltf"
);
const externalTriangleBinPath = path.resolve(
process.cwd(),
"fixtures/assets/external-triangle/triangle.bin"
);
function createTestFile(bytes: Uint8Array | Buffer, name: string, type: string): File {
function createTestFile(
bytes: Uint8Array | Buffer,
name: string,
type: string
): File {
const arrayBuffer = new ArrayBuffer(bytes.byteLength);
new Uint8Array(arrayBuffer).set(bytes);
@@ -72,7 +95,9 @@ function buildZipArchive(entries: Record<string, Uint8Array>): Uint8Array {
zip.end();
const archiveBytes = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0));
const archiveBytes = new Uint8Array(
chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0)
);
let offset = 0;
for (const chunk of chunks) {
@@ -177,7 +202,11 @@ describe("project package serialization", () => {
await storage.putAsset(imageAsset.storageKey, {
files: {
[imageAsset.sourceName]: {
bytes: cloneArrayBuffer(strToU8("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1024\" height=\"512\"></svg>")),
bytes: cloneArrayBuffer(
strToU8(
'<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="512"></svg>'
)
),
mimeType: imageAsset.mimeType
}
}
@@ -191,7 +220,10 @@ describe("project package serialization", () => {
}
});
const imageLoadListeners = new WeakMap<object, { load?: () => void; error?: () => void }>();
const imageLoadListeners = new WeakMap<
object,
{ load?: () => void; error?: () => void }
>();
const mockImageWidth = 1024;
const mockImageHeight = 512;
@@ -247,18 +279,32 @@ describe("project package serialization", () => {
const packageBytes = await saveProjectPackage(document, storage);
const restoredStorage = createInMemoryProjectAssetStorage();
const restoredDocument = await loadProjectPackage(packageBytes, restoredStorage);
const restoredDocument = await loadProjectPackage(
packageBytes,
restoredStorage
);
expect(restoredDocument).toEqual(document);
const restoredModel = await loadModelAssetFromStorage(restoredStorage, importedModel.asset);
const restoredImage = await loadImageAssetFromStorage(restoredStorage, imageAsset);
const restoredAudio = await loadAudioAssetFromStorage(restoredStorage, audioAsset);
const restoredModel = await loadModelAssetFromStorage(
restoredStorage,
importedModel.asset
);
const restoredImage = await loadImageAssetFromStorage(
restoredStorage,
imageAsset
);
const restoredAudio = await loadAudioAssetFromStorage(
restoredStorage,
audioAsset
);
expect(restoredModel.metadata.format).toBe("glb");
expect(restoredModel.template.children.length).toBeGreaterThan(0);
expect(restoredImage.metadata.width).toBe(imageAsset.metadata.width);
expect(restoredAudio.metadata.durationSeconds).toBe(audioAsset.metadata.durationSeconds);
expect(restoredAudio.metadata.durationSeconds).toBe(
audioAsset.metadata.durationSeconds
);
});
it("preserves multi-file gltf asset bundles inside the packaged assets directory", async () => {
@@ -290,7 +336,10 @@ describe("project package serialization", () => {
await loadProjectPackage(packageBytes, restoredStorage);
const restoredModel = await loadModelAssetFromStorage(restoredStorage, importedModel.asset);
const restoredModel = await loadModelAssetFromStorage(
restoredStorage,
importedModel.asset
);
expect(restoredModel.metadata.format).toBe("gltf");
expect(restoredModel.template.children.length).toBeGreaterThan(0);
@@ -320,7 +369,9 @@ describe("project package serialization", () => {
}
});
await expect(saveProjectPackage(document, storage)).rejects.toThrow("Missing stored binary data for image asset missing.png.");
await expect(saveProjectPackage(document, storage)).rejects.toThrow(
"Missing stored binary data for image asset missing.png."
);
});
it("fails project load when scene.json is missing", async () => {
@@ -328,7 +379,9 @@ describe("project package serialization", () => {
"assets/readme.txt": strToU8("not a project")
});
await expect(loadProjectPackage(packageBytes, null)).rejects.toThrow("project package is missing scene.json");
await expect(loadProjectPackage(packageBytes, null)).rejects.toThrow(
"project package is missing scene.json"
);
});
it("fails project load when a declared asset has no packaged files", async () => {
@@ -357,7 +410,9 @@ describe("project package serialization", () => {
[PROJECT_PACKAGE_SCENE_PATH]: strToU8(serializeProjectDocument(document))
});
await expect(loadProjectPackage(packageBytes, createInMemoryProjectAssetStorage())).rejects.toThrow(
await expect(
loadProjectPackage(packageBytes, createInMemoryProjectAssetStorage())
).rejects.toThrow(
"project package is missing bundled files for image asset missing.svg"
);
});
@@ -366,8 +421,13 @@ describe("project package serialization", () => {
const document = createProjectDocument(
createEmptySceneDocument({ name: "Portable Scene Without Storage" })
);
const packageBytes = await saveProjectPackage(document, createInMemoryProjectAssetStorage());
const packageBytes = await saveProjectPackage(
document,
createInMemoryProjectAssetStorage()
);
await expect(loadProjectPackage(packageBytes, null as ProjectAssetStorage | null)).resolves.toEqual(document);
await expect(
loadProjectPackage(packageBytes, null as ProjectAssetStorage | null)
).resolves.toEqual(document);
});
});