From 9ceff5999f6f5bea941e23cedad9435aa680b2b7 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 31 Mar 2026 18:45:19 +0200 Subject: [PATCH] Add external triangle scene GLTF and update file import handling --- fixtures/assets/external-triangle/scene.gltf | 65 +++++++++++++++++++ src/app/App.tsx | 10 ++- .../project-asset-storage.test.ts | 20 ++++-- 3 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 fixtures/assets/external-triangle/scene.gltf diff --git a/fixtures/assets/external-triangle/scene.gltf b/fixtures/assets/external-triangle/scene.gltf new file mode 100644 index 00000000..5f6b1776 --- /dev/null +++ b/fixtures/assets/external-triangle/scene.gltf @@ -0,0 +1,65 @@ +{ + "asset": { + "version": "2.0", + "generator": "webeditor3d fixture" + }, + "scene": 0, + "scenes": [ + { + "name": "External Triangle Scene", + "nodes": [0] + } + ], + "nodes": [ + { + "mesh": 0, + "name": "External Triangle Node" + } + ], + "meshes": [ + { + "name": "External Triangle Mesh", + "primitives": [ + { + "attributes": { + "POSITION": 0 + }, + "material": 0 + } + ] + } + ], + "materials": [ + { + "name": "External Material", + "pbrMetallicRoughness": { + "baseColorFactor": [0.2, 0.6, 0.85, 1], + "metallicFactor": 0, + "roughnessFactor": 1 + } + } + ], + "buffers": [ + { + "byteLength": 36, + "uri": "triangle.bin" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 36 + } + ], + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 3, + "type": "VEC3", + "min": [0, 0, 0], + "max": [1, 1, 0] + } + ] +} diff --git a/src/app/App.tsx b/src/app/App.tsx index ebb4aa4a..c6a8e31f 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -45,6 +45,7 @@ import { } from "../assets/model-instance-labels"; import { importModelAssetFromFile, + importModelAssetFromFiles, loadModelAssetFromStorage, disposeModelTemplate, type ImportedModelAssetResult, @@ -1938,9 +1939,9 @@ export function App({ store, initialStatusMessage }: AppProps) { const handleImportModelChange = async (event: ChangeEvent) => { const input = event.currentTarget; - const file = input.files?.[0]; + const files = Array.from(input.files ?? []); - if (file === undefined) { + if (files.length === 0) { return; } @@ -1953,7 +1954,9 @@ export function App({ store, initialStatusMessage }: AppProps) { let importedModelForCleanup: ImportedModelAssetResult | null = null; try { - const importedModel = await importModelAssetFromFile(file, projectAssetStorage); + const importedModel = files.length === 1 + ? await importModelAssetFromFile(files[0], projectAssetStorage) + : await importModelAssetFromFiles(files, projectAssetStorage); importedModelForCleanup = importedModel; store.executeCommand( @@ -4014,6 +4017,7 @@ export function App({ store, initialStatusMessage }: AppProps) { ref={importModelInputRef} className="visually-hidden" type="file" + multiple accept=".glb,.gltf,model/gltf-binary,model/gltf+json,application/octet-stream" onChange={handleImportModelChange} /> diff --git a/tests/serialization/project-asset-storage.test.ts b/tests/serialization/project-asset-storage.test.ts index 9ae31def..90e3fb8d 100644 --- a/tests/serialization/project-asset-storage.test.ts +++ b/tests/serialization/project-asset-storage.test.ts @@ -4,21 +4,31 @@ import { createProjectAssetStorageKey } from "../../src/assets/project-assets"; import { createInMemoryProjectAssetStorage } from "../../src/assets/project-asset-storage"; describe("project asset storage", () => { - it("stores, clones, and deletes binary asset payloads", async () => { + it("stores, clones, and deletes binary asset file packages", async () => { const storage = createInMemoryProjectAssetStorage(); const storageKey = createProjectAssetStorageKey("asset-model-triangle"); const bytes = new Uint8Array([0, 1, 2, 3, 4]).buffer; + const sidecarBytes = new Uint8Array([9, 8, 7]).buffer; await storage.putAsset(storageKey, { - bytes, - mimeType: "model/gltf+json" + files: { + "tiny-triangle.gltf": { + bytes, + mimeType: "model/gltf+json" + }, + "triangle.bin": { + bytes: sidecarBytes, + mimeType: "application/octet-stream" + } + } }); const loadedAsset = await storage.getAsset(storageKey); expect(loadedAsset).not.toBeNull(); - expect(loadedAsset?.mimeType).toBe("model/gltf+json"); - expect(Array.from(new Uint8Array(loadedAsset?.bytes ?? new ArrayBuffer(0)))).toEqual([0, 1, 2, 3, 4]); + expect(Object.keys(loadedAsset?.files ?? {})).toEqual(["tiny-triangle.gltf", "triangle.bin"]); + expect(Array.from(new Uint8Array(loadedAsset?.files["tiny-triangle.gltf"].bytes ?? new ArrayBuffer(0)))).toEqual([0, 1, 2, 3, 4]); + expect(Array.from(new Uint8Array(loadedAsset?.files["triangle.bin"].bytes ?? new ArrayBuffer(0)))).toEqual([9, 8, 7]); await storage.deleteAsset(storageKey);