diff --git a/packages/fakeyou.com/src/components/common/Scene3D/EngineMode.ts b/packages/fakeyou.com/src/components/common/Scene3D/EngineMode.ts new file mode 100644 index 0000000000..d24186fb7e --- /dev/null +++ b/packages/fakeyou.com/src/components/common/Scene3D/EngineMode.ts @@ -0,0 +1,5 @@ + +export enum EngineMode { + Viewer = "viewer", + Studio = "studio", +} diff --git a/packages/fakeyou.com/src/components/common/Scene3D/GetEngineUrl.test.ts b/packages/fakeyou.com/src/components/common/Scene3D/GetEngineUrl.test.ts new file mode 100644 index 0000000000..050a12bae0 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/Scene3D/GetEngineUrl.test.ts @@ -0,0 +1,119 @@ +import { MediaFileType } from "@storyteller/components/src/api/_common/enums/MediaFileType"; +import { EngineMode } from "./EngineMode"; +import { GetEngineUrl } from "./GetEngineUrl"; +import { WeightCategory } from "@storyteller/components/src/api/_common/enums/WeightCategory"; +import { WeightType } from "@storyteller/components/src/api/_common/enums/WeightType"; +import { MediaFileSubtype } from "@storyteller/components/src/api/enums/MediaFileSubtype"; + +describe('mode', () => { + test('studio', () => { + const url = GetEngineUrl({mode: EngineMode.Studio, load: { objectId: "foo" } }); + expect(url).toEqual("https://engine.fakeyou.com/?mode=studio&objectId=foo"); + }); + + test('viewer', () => { + const url = GetEngineUrl({mode: EngineMode.Viewer, load: { objectId: "foo" } }); + expect(url).toEqual("https://engine.fakeyou.com/?mode=viewer&objectId=foo"); + }); +}); + +describe('skybox', () => { + test('from named skybox', () => { + const url = GetEngineUrl({mode: EngineMode.Studio, load: { objectId: "foo" }, skybox: "gum_trees_4k" }); + expect(url).toEqual("https://engine.fakeyou.com/?mode=studio&skybox=gum_trees_4k&objectId=foo"); + }); + + test('from hex color', () => { + const url = GetEngineUrl({mode: EngineMode.Studio, load: { objectId: "foo" }, skybox: "ff0000" }); + expect(url).toEqual("https://engine.fakeyou.com/?mode=studio&skybox=ff0000&objectId=foo"); + }); +}); + +describe('object ids', () => { + test('object id urls should work', () => { + const url = GetEngineUrl({mode: EngineMode.Studio, load: { objectId: "foo" } }); + expect(url).toEqual("https://engine.fakeyou.com/?mode=studio&objectId=foo"); + }); +}); + +// TODO(bt,2024-03-11): We're only testing one type of media file +describe('media files', () => { + let mediaFile = { + token: "MEDIA_FILE_TOKEN", + media_type: MediaFileType.GLB, + maybe_media_subtype: null, + public_bucket_path: "path/to/file", + maybe_batch_token: "BATCH_TOKEN", + created_at: new Date(), + updated_at: new Date(), + maybe_creator_user: null, + creator_set_visibility: "public", + // TODO(bt,2024-03-11): Make optional + maybe_model_weight_info: { + title: "title", + weight_token: "WEIGHT_TOKEN", + weight_category: WeightCategory.SD, + weight_type: WeightType.HIFIGAN_TT2, + maybe_cover_image_public_bucket_path: "image", + maybe_weight_creator: { + user_token: "USER_TOKEN", + username: "username", + display_name: "display_name", + gravatar_hash: "foo", + default_avatar: { + image_index: 1, + color_index: 2, + } + }, + } + }; + + describe('scene files', () => { + test('from media type', () => { + mediaFile.media_type = MediaFileType.SceneRon; + + // NB: Not the real subtype; forcing test to act on type. + mediaFile.maybe_media_subtype = null; + + const url = GetEngineUrl({mode: EngineMode.Studio, load: mediaFile }); + expect(url).toEqual("https://engine.fakeyou.com/?mode=studio&scene=remote://MEDIA_FILE_TOKEN.scn.ron"); + }); + + test('from media subtype', () => { + // NB: Not the real time; forcing test to act on subtype. + mediaFile.media_type = MediaFileType.Audio; + + // TODO(bt,2024-03-11): Why does the IDE complain here? + mediaFile.maybe_media_subtype = MediaFileSubtype.StorytellerScene as any; + + const url = GetEngineUrl({mode: EngineMode.Studio, load: mediaFile }); + expect(url).toEqual("https://engine.fakeyou.com/?mode=studio&scene=remote://MEDIA_FILE_TOKEN.scn.ron"); + }); + }); + + describe('mixamo animations', () => { + test('from media subtype', () => { + // NB: Not the real time; forcing test to act on subtype. + mediaFile.media_type = MediaFileType.Audio; + + // TODO(bt,2024-03-11): Why does the IDE complain here? + mediaFile.maybe_media_subtype = MediaFileSubtype.Mixamo as any; + + const url = GetEngineUrl({mode: EngineMode.Studio, load: mediaFile }); + expect(url).toEqual("https://engine.fakeyou.com/?mode=studio&mixamo=https://storage.googleapis.com/dev-vocodes-public/path/to/file"); + }); + }); + + describe('bvh animations', () => { + test('from media subtype', () => { + // NB: Not the real time; forcing test to act on subtype. + mediaFile.media_type = MediaFileType.Audio; + + // TODO(bt,2024-03-11): Why does the IDE complain here? + mediaFile.maybe_media_subtype = MediaFileSubtype.MocapNet as any; + + const url = GetEngineUrl({mode: EngineMode.Studio, load: mediaFile }); + expect(url).toEqual("https://engine.fakeyou.com/?mode=studio&bvh=https://storage.googleapis.com/dev-vocodes-public/path/to/file"); + }); + }); +}); diff --git a/packages/fakeyou.com/src/components/common/Scene3D/GetEngineUrl.ts b/packages/fakeyou.com/src/components/common/Scene3D/GetEngineUrl.ts new file mode 100644 index 0000000000..eb0d167663 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/Scene3D/GetEngineUrl.ts @@ -0,0 +1,87 @@ +import { MediaFile as MediaFileOne } from "@storyteller/components/src/api/media_files/GetMedia"; +import { MediaFile as MediaFileTwo } from "@storyteller/components/src/api/media_files/GetMediaFile"; +import { EngineMode } from "./EngineMode"; +import { MediaFileSubtype } from "@storyteller/components/src/api/enums/MediaFileSubtype"; +import { MediaFileType } from "@storyteller/components/src/api/_common/enums/MediaFileType"; +import { BucketConfig } from "@storyteller/components/src/api/BucketConfig"; + +// Storyteller Engine parameters +// These are documented here: +// https://www.notion.so/storytellerai/Studio-Iframe-Query-Params-a748a9929ec3404780c3884e7fb89bdb + +interface GetEngineUrlArgs { + mode: EngineMode, + + // Polymorphic type of asset we're loading into the engine. + load: LoadableAsset + + // Optional skybox color (hex) or name + // - Predefined skyboxes, eg. `gum_trees_4k`, `kloofendal_28d_misty_4k`, `meadow_4k`, `promenade_de_vidy_4k`, `scythian_tombs_4k` + // - Hex colors, eg. `ff0000` or `1a1a27`. + skybox?: string, +} + +// Support multiple "Media File" API response payloads. +type MediaFile = MediaFileOne | MediaFileTwo ; + +// Used to load raw built-in objects into the engine. +interface EngineObject { + objectId: string; +} + +type LoadableAsset = MediaFile | EngineObject; + +const ENGINE_BASE_URL = "https://engine.fakeyou.com"; + +export function GetEngineUrl(args: GetEngineUrlArgs) : string { + let engineUrl = `${ENGINE_BASE_URL}/?mode=${args.mode}`; + + + if (args.skybox) { + engineUrl += `&skybox=${args.skybox}`; + } + + if (isMediaFile(args.load)) { + + const bucketAssetUrl = new BucketConfig().getGcsUrl(args.load.public_bucket_path); + + if ( + args.load.maybe_media_subtype === MediaFileSubtype.StorytellerScene || + args.load.media_type === MediaFileType.SceneRon + ) { + // Storyteller Engine Scenes + // NB: Storyteller Engine makes the API call to load the scene. + // We don't need to pass the bucket path. + // The engine, does, however, need a `.scn.ron` file extension. + const sceneUrlRef = `remote://${args.load.token}.scn.ron`; + engineUrl += `&scene=${sceneUrlRef}`; + } else if ( + args.load.maybe_media_subtype === MediaFileSubtype.Mixamo + ) { + engineUrl += `&mixamo=${bucketAssetUrl}`; + } else if ( + args.load.maybe_media_subtype === MediaFileSubtype.MocapNet + ) { + engineUrl += `&bvh=${bucketAssetUrl}`; + } else { + engineUrl += `&sceneImport=${bucketAssetUrl}`; + } + + } else if (isEngineObject(args.load)) { + // NB: This is an engine built-in, eg. `couch.gltf` or `sample-room.gltf`. + engineUrl += `&objectId=${args.load.objectId}`; + } + + return engineUrl; +} + +// Type guard for media files +const isMediaFile = (loadable: LoadableAsset) : loadable is MediaFile => { + return "token" in loadable; +} + +// Type guard for engine objects +const isEngineObject = (loadable: LoadableAsset) : loadable is EngineObject => { + return "objectId" in loadable; +} +