From 620862941f1456c7895d581b197ee5ead81b84b5 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Wed, 15 Apr 2026 04:25:24 +0200 Subject: [PATCH] Add support for starter environment image assets --- src/assets/image-assets.ts | 154 +++++++++++++++++++++++++++++++++---- 1 file changed, 139 insertions(+), 15 deletions(-) diff --git a/src/assets/image-assets.ts b/src/assets/image-assets.ts index 27370ae1..b2721b3e 100644 --- a/src/assets/image-assets.ts +++ b/src/assets/image-assets.ts @@ -9,6 +9,11 @@ import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js"; import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js"; import { createOpaqueId } from "../core/ids"; +import { + getStarterEnvironmentPreviewUrl, + getStarterEnvironmentTextureUrl, + isStarterEnvironmentImageAsset +} from "./starter-environment-assets"; import { createProjectAssetStorageKey, type ImageAssetMetadata, type ImageAssetRecord } from "./project-assets"; import { type ProjectAssetStorage, @@ -22,6 +27,7 @@ export interface LoadedImageAsset { metadata: ImageAssetMetadata; texture: Texture; sourceUrl: string; + previewUrl: string; revokeSourceUrl: () => void; } @@ -115,6 +121,24 @@ function createTransientResourceUrl(file: ProjectAssetStorageFileRecord): { revo }; } +function createPersistentResourceUrl(url: string): { + revoke: () => void; + url: string; +} { + return { + url, + revoke: () => undefined + }; +} + +function combineRevokeHandlers(...handlers: Array<() => void>): () => void { + return () => { + for (const handler of handlers) { + handler(); + } + }; +} + function loadImageElement(sourceUrl: string): Promise { return new Promise((resolve, reject) => { const image = new Image(); @@ -230,6 +254,7 @@ function createLoadedImageAsset( asset: ImageAssetRecord, image: HTMLImageElement, sourceUrl: string, + previewUrl: string, revokeSourceUrl: () => void ): LoadedImageAsset { return { @@ -238,6 +263,7 @@ function createLoadedImageAsset( metadata: asset.metadata, texture: createImageTexture(image), sourceUrl, + previewUrl, revokeSourceUrl }; } @@ -246,6 +272,7 @@ function createLoadedHdrImageAsset( asset: ImageAssetRecord, texture: DataTexture, sourceUrl: string, + previewUrl: string, revokeSourceUrl: () => void ): LoadedImageAsset { return { @@ -254,6 +281,7 @@ function createLoadedHdrImageAsset( metadata: asset.metadata, texture: configureHdrTexture(texture), sourceUrl, + previewUrl, revokeSourceUrl }; } @@ -296,25 +324,105 @@ function getStoredImageAssetFile( return null; } -async function loadImageAssetFromFileRecord( - asset: ImageAssetRecord, - fileRecord: ProjectAssetStorageFileRecord +function getStoredPreviewImageFile( + storedAsset: ProjectAssetStoragePackageRecord +): ProjectAssetStorageFileRecord | null { + for (const previewPath of [ + "preview.jpg", + "preview.jpeg", + "preview.webp", + "preview.png" + ]) { + const previewFile = storedAsset.files[previewPath]; + + if (previewFile !== undefined) { + return previewFile; + } + } + + return null; +} + +async function loadStarterEnvironmentImageAsset( + asset: ImageAssetRecord ): Promise { + const sourceResource = createPersistentResourceUrl( + getStarterEnvironmentTextureUrl(asset) + ); + const previewResource = createPersistentResourceUrl( + getStarterEnvironmentPreviewUrl(asset) + ); + + try { + const texture = await loadHdrTexture(sourceResource.url, asset.sourceName); + + return createLoadedHdrImageAsset( + asset, + texture, + sourceResource.url, + previewResource.url, + combineRevokeHandlers(sourceResource.revoke, previewResource.revoke) + ); + } catch (error) { + throw new Error( + `Image asset reload failed for ${asset.sourceName}: ${getErrorDetail(error)}` + ); + } +} + +async function loadImageAssetFromPackageRecord( + asset: ImageAssetRecord, + storedAsset: ProjectAssetStoragePackageRecord +): Promise { + const fileRecord = getStoredImageAssetFile(asset, storedAsset); + + if (fileRecord === null) { + throw new Error( + `Missing stored image file for imported image asset ${asset.sourceName}.` + ); + } + const transientResourceUrl = createTransientResourceUrl(fileRecord); if (isHdrFormat(asset.sourceName)) { + const previewFileRecord = getStoredPreviewImageFile(storedAsset); + const previewResourceUrl = + previewFileRecord === null + ? transientResourceUrl + : createTransientResourceUrl(previewFileRecord); + try { const texture = await loadHdrTexture(transientResourceUrl.url, asset.sourceName); - return createLoadedHdrImageAsset(asset, texture, transientResourceUrl.url, transientResourceUrl.revoke); + return createLoadedHdrImageAsset( + asset, + texture, + transientResourceUrl.url, + previewResourceUrl.url, + combineRevokeHandlers( + transientResourceUrl.revoke, + previewResourceUrl === transientResourceUrl + ? () => undefined + : previewResourceUrl.revoke + ) + ); } catch (error) { transientResourceUrl.revoke(); + if (previewResourceUrl !== transientResourceUrl) { + previewResourceUrl.revoke(); + } throw new Error(`Image asset reload failed for ${asset.sourceName}: ${getErrorDetail(error)}`); } } try { const image = await loadImageElement(transientResourceUrl.url); - return createLoadedImageAsset(asset, image, transientResourceUrl.url, transientResourceUrl.revoke); + return createLoadedImageAsset( + asset, + image, + transientResourceUrl.url, + transientResourceUrl.url, + transientResourceUrl.revoke + ); } catch (error) { transientResourceUrl.revoke(); throw new Error(`Image asset reload failed for ${asset.sourceName}: ${getErrorDetail(error)}`); @@ -346,7 +454,13 @@ export async function importBackgroundImageAssetFromFile( const metadata = extractHdrTextureMetadata(texture); asset = createImageAssetRecord(sourceName, mimeType, bytes.byteLength, metadata); - loadedAsset = createLoadedHdrImageAsset(asset, texture, transientResourceUrl.url, transientResourceUrl.revoke); + loadedAsset = createLoadedHdrImageAsset( + asset, + texture, + transientResourceUrl.url, + transientResourceUrl.url, + transientResourceUrl.revoke + ); } else { const transientResourceUrl = createTransientResourceUrl(fileRecord); let image: HTMLImageElement; @@ -360,7 +474,13 @@ export async function importBackgroundImageAssetFromFile( const metadata = extractImageAssetMetadata(image); asset = createImageAssetRecord(sourceName, mimeType, bytes.byteLength, metadata); - loadedAsset = createLoadedImageAsset(asset, image, transientResourceUrl.url, transientResourceUrl.revoke); + loadedAsset = createLoadedImageAsset( + asset, + image, + transientResourceUrl.url, + transientResourceUrl.url, + transientResourceUrl.revoke + ); } const packageRecord: ProjectAssetStoragePackageRecord = { @@ -378,9 +498,19 @@ export async function importBackgroundImageAssetFromFile( } export async function loadImageAssetFromStorage( - storage: ProjectAssetStorage, + storage: ProjectAssetStorage | null, asset: ImageAssetRecord ): Promise { + if (isStarterEnvironmentImageAsset(asset)) { + return loadStarterEnvironmentImageAsset(asset); + } + + if (storage === null) { + throw new Error( + `Missing stored binary data for imported image asset ${asset.sourceName}.` + ); + } + let storedAsset: ProjectAssetStoragePackageRecord | null; try { @@ -393,13 +523,7 @@ export async function loadImageAssetFromStorage( throw new Error(`Missing stored binary data for imported image asset ${asset.sourceName}.`); } - const storedImageFile = getStoredImageAssetFile(asset, storedAsset); - - if (storedImageFile === null) { - throw new Error(`Missing stored image file for imported image asset ${asset.sourceName}.`); - } - - return loadImageAssetFromFileRecord(asset, storedImageFile); + return loadImageAssetFromPackageRecord(asset, storedAsset); } export function disposeLoadedImageAsset(asset: LoadedImageAsset) {