Local assets (#114)

To use it, add "&mock-api" to the URL.
This commit is contained in:
Lee-Orr
2024-04-02 14:04:53 -04:00
committed by GitHub
parent 05eccbf69e
commit ec4809a509
9 changed files with 701 additions and 74 deletions
Generated
+141 -23
View File
@@ -71,6 +71,18 @@ dependencies = [
"winit",
]
[[package]]
name = "accessory"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "850bb534b9dc04744fbbb71d30ad6d25a7e4cf6dc33e223c81ef3a92ebab4e0b"
dependencies = [
"macroific",
"proc-macro2",
"quote",
"syn 2.0.51",
]
[[package]]
name = "addr2line"
version = "0.21.0"
@@ -2117,6 +2129,19 @@ dependencies = [
"winapi",
]
[[package]]
name = "dashmap"
version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if",
"hashbrown 0.14.0",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "dasp_sample"
version = "0.11.0"
@@ -2129,6 +2154,18 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "delegate-display"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98a85201f233142ac819bbf6226e36d0b5e129a47bd325084674261c82d4cd66"
dependencies = [
"macroific",
"proc-macro2",
"quote",
"syn 2.0.51",
]
[[package]]
name = "derive_more"
version = "0.99.17"
@@ -2351,6 +2388,18 @@ dependencies = [
"zune-inflate",
]
[[package]]
name = "fancy_constructor"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f71f317e4af73b2f8f608fac190c52eac4b1879d2145df1db2fe48881ca69435"
dependencies = [
"macroific",
"proc-macro2",
"quote",
"syn 2.0.51",
]
[[package]]
name = "fastrand"
version = "1.9.0"
@@ -2959,7 +3008,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"socket2 0.5.4",
"socket2 0.5.6",
"tokio",
"tower-service",
"tracing",
@@ -3007,6 +3056,23 @@ dependencies = [
"tiff",
]
[[package]]
name = "indexed_db_futures"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cc2083760572ee02385ab8b7c02c20925d2dd1f97a1a25a8737a238608f1152"
dependencies = [
"accessory",
"cfg-if",
"delegate-display",
"fancy_constructor",
"js-sys",
"uuid",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -3194,9 +3260,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.65"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8"
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
dependencies = [
"wasm-bindgen",
]
@@ -3275,9 +3341,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.148"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "libloading"
@@ -3368,6 +3434,53 @@ dependencies = [
"libc",
]
[[package]]
name = "macroific"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05c00ac596022625d01047c421a0d97d7f09a18e429187b341c201cb631b9dd"
dependencies = [
"macroific_attr_parse",
"macroific_core",
"macroific_macro",
]
[[package]]
name = "macroific_attr_parse"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd94d5da95b30ae6e10621ad02340909346ad91661f3f8c0f2b62345e46a2f67"
dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 2.0.51",
]
[[package]]
name = "macroific_core"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.51",
]
[[package]]
name = "macroific_macro"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c9853143cbed7f1e41dc39fee95f9b361bec65c8dc2a01bf609be01b61f5ae"
dependencies = [
"macroific_attr_parse",
"macroific_core",
"proc-macro2",
"quote",
"syn 2.0.51",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@@ -3465,9 +3578,9 @@ checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
[[package]]
name = "mio"
version = "0.8.8"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
@@ -3711,9 +3824,9 @@ dependencies = [
[[package]]
name = "num-traits"
version = "0.2.16"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
dependencies = [
"autocfg",
"libm",
@@ -4674,12 +4787,12 @@ dependencies = [
[[package]]
name = "socket2"
version = "0.5.4"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
dependencies = [
"libc",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -4845,12 +4958,15 @@ dependencies = [
"bytemuck",
"clap",
"console_error_panic_hook",
"dashmap",
"futures",
"futures-intrusive",
"futures-lite 2.2.0",
"image",
"indexed_db_futures",
"itertools 0.12.1",
"js-sys",
"once_cell",
"parking_lot",
"reqwest",
"serde",
@@ -4861,6 +4977,7 @@ dependencies = [
"studio-bvh",
"uuid",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"wgpu",
]
@@ -4986,9 +5103,9 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.49"
version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4"
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
dependencies = [
"thiserror-impl",
]
@@ -5015,9 +5132,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "1.0.49"
version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc"
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [
"proc-macro2",
"quote",
@@ -5062,16 +5179,16 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.33.0"
version = "1.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"pin-project-lite",
"socket2 0.5.4",
"socket2 0.5.6",
"windows-sys 0.48.0",
]
@@ -5303,12 +5420,13 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.4.1"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
dependencies = [
"getrandom",
"serde",
"wasm-bindgen",
]
[[package]]
@@ -5458,9 +5576,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.65"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85"
checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
dependencies = [
"js-sys",
"wasm-bindgen",
+7
View File
@@ -37,6 +37,7 @@
"@vitest/ui": "~0.32.0",
"chalk": "^5.3.0",
"glob": "^10.3.10",
"idb": "8.0.0",
"jsdom": "~22.1.0",
"nx": "16.9.1",
"prettier": "^2.6.2",
@@ -7025,6 +7026,12 @@
"node": ">=0.10.0"
}
},
"node_modules/idb": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz",
"integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==",
"dev": true
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+2 -1
View File
@@ -39,7 +39,8 @@
"vite-plugin-wasm": "^3.2.2",
"vite-plugin-web-components-hmr": "^0.1.3",
"vitest": "~0.32.0",
"which": "^4.0.0"
"which": "^4.0.0",
"idb": "8.0.0"
},
"dependencies": {
"@fortawesome/fontawesome-pro": "^6.4.2",
+13 -39
View File
@@ -11,11 +11,14 @@ import { DialogElement } from "@storyteller/studio-ui/dialog";
import {
SceneHierarchyElement,
StorytellerApi,
StorytellerLocalApi,
StorytellerRemoteApi,
StudioURLParams,
mockApi,
} from "@storyteller/studio-web";
import { match } from "@storyteller/utility";
import { LitElement, TemplateResult, html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js";
import { customElement, property, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { createRef, ref } from "lit/directives/ref.js";
import { styleMap } from "lit/directives/style-map.js";
@@ -40,7 +43,6 @@ import "./help.element";
import "./temp-video-preview.element";
import "./app.element.scss";
import { mock_save_load } from "./util";
@customElement("app-root")
@provide({
@@ -58,7 +60,7 @@ export class AppElement extends LitElement {
@state() currentSidebar : string = 'hierarchy';
private api = new StorytellerApi();
@property({ attribute: false }) api: StorytellerApi = new StorytellerApi(mockApi ? new StorytellerLocalApi() : new StorytellerRemoteApi());
#addToSceneButtonRef = createRef<ButtonElement>();
#videoStylePreviewBtnRef = createRef<ButtonElement>();
@@ -76,43 +78,15 @@ export class AppElement extends LitElement {
async onSceneSaved({ detail: scene }: SceneSavedEvent): Promise<void> {
try {
const file = new File([scene], "scene.scn.ron", { type: "text/ron" });
if (mock_save_load()) {
/*
// NB(brandon, 2024-03-22): Hey Lee-Orr, I'm commenting this out since the file
picker isn't defined and the Netlify builds are breaking. Feel free to uncomment
this for your testing. We probably just need a local definition file so the
builds are happy.
const token = await this.api.createScene(file);
let handle = await window.showSaveFilePicker({
suggestedName: "scene",
types: [{
description: "Scene Files",
accept: {
"text/ron": [".scn.ron"]
}
}]
});
let writable = await handle.createWritable();
await writable.write(file);
await writable.close();
let token = handle.name;
this.dispatchEvent(new CustomEvent("scene-uploaded", {
detail: token,
bubbles: true,
composed: true,
}));
*/
} else {
const token = await this.api.createScene(file);
this.dispatchEvent(new CustomEvent("scene-uploaded", {
detail: token,
bubbles: true,
composed: true,
}));
}
this.dispatchEvent(new CustomEvent("scene-uploaded", {
detail: token,
bubbles: true,
composed: true,
}));
}
catch (err) {
console.error(err);
+2 -1
View File
@@ -4,7 +4,8 @@
"dependencies": {
"@storyteller/framework": "0.0.1",
"@storyteller/studio": "0.0.1",
"lit": "^2.8.0"
"lit": "^2.8.0",
"idb": "8.0.0"
},
"main": "./index.js",
"module": "./index.mjs",
+363 -4
View File
@@ -1,7 +1,24 @@
import axios, { AxiosResponse } from "axios";
import { v4 as uuid } from "uuid";
import { DBSchema, IDBPDatabase, openDB } from "idb";
import * as studio from "@storyteller/studio";
export class StorytellerApi {
export const mockApi = window.location.search.includes("mock-api");
interface IStorytellerApi {
createScene(file: File): Promise<MediaFileToken>
session(): Promise<Session>
listMediaFiles(options: ListMediaFilesOptions): Promise<ListMediaFilesResult>
uploadMediaFile(
file: File,
{ subtype, mediaClass }: UploadMediaFileOptions,
): Promise<MediaFileToken>
renameMediaFile(token: MediaFileToken, name: string): Promise<void>
convertFbxToGltf(token: MediaFileToken): Promise<void>
mediaFile(token: MediaFileToken): Promise<MediaFile>
}
export class StorytellerRemoteApi implements IStorytellerApi {
#api = axios.create({
baseURL: "https://api.fakeyou.com",
withCredentials: true,
@@ -14,8 +31,8 @@ export class StorytellerApi {
uuid_idempotency_token: uuid(),
file,
}, {
headers: { Accept: "text/plain" },
},
headers: { Accept: "text/plain" },
},
);
this.#validateResponse(response);
@@ -105,10 +122,352 @@ export class StorytellerApi {
}
}
interface LocalDbSchema extends DBSchema {
'media-files': {
key: MediaFileToken,
value: MediaFile,
indexes: {
'media-type': MediaFileType,
'media-class': MediaFileClass
}
},
'file-blobs': {
key: string,
value: Uint8Array
}
}
export class StorytellerLocalApi implements IStorytellerApi {
database?: IDBPDatabase<LocalDbSchema>;
dateFormat: Intl.DateTimeFormat = new Intl.DateTimeFormat("en-us")
constructor() {
document.addEventListener("request-file", async (e: studio.RequestFileEvent) => {
const token = e.detail;
console.log("Loading Asset: ", token);
try {
let file = await this.mediaFile(token);
console.log("Got Asset File: ", token, file);
let path = file.public_bucket_path;
let blob = await this.mediaBlob(path);
console.log("Got Asset Blob", token);
studio.requestedFileReceived(token, blob);
console.log("Set File to Bevy", token)
} catch (e) {
console.error("Error Loading File", token, e);
studio.requestedFileFailed(token, e);
}
})
}
async getDatabase(): Promise<IDBPDatabase<LocalDbSchema>> {
if (this.database) {
return this.database;
}
this.database = await openDB<LocalDbSchema>('storyteller-local', 1, {
upgrade(db) {
const media_files = db.createObjectStore('media-files');
media_files.createIndex('media-class', 'media_class');
media_files.createIndex('media-type', 'media_type');
db.createObjectStore('file-blobs');
}
});
return this.database
}
async createScene(file: File): Promise<string> {
let token = uuid();
let original_file_name = file.name;
let filename = token + "-" + file.name;
let media_file: MediaFile = {
token: token,
media_type: MediaFileType.ScnRon,
maybe_media_subtype: MediaFileSubtype.StorytellerScene,
media_class: MediaFileClass.Scene,
public_bucket_path: filename,
creator_set_visibility: Visibility.Public,
is_emulated_media_file: false,
maybe_title: filename,
maybe_original_filename: original_file_name,
stats: {
positive_rating_count: 0,
bookmark_count: 0
},
created_at: (new Date()).toLocaleString("en-us"),
updated_at: (new Date()).toLocaleString("en-us"),
};
let buffer = new Uint8Array(await file.arrayBuffer(), 0);
let db = await this.getDatabase();
let transaction = db.transaction(['media-files', 'file-blobs'], 'readwrite');
await Promise.all([
transaction.objectStore('media-files').add(media_file, token),
transaction.objectStore('file-blobs').add(buffer, filename),
transaction.done,
]);
return token;
}
async session(): Promise<Session> {
return {
success: true,
logged_in: true,
user: {
username: "local_user",
can_access_studio: true,
can_approve_w21_templates: true,
can_ban_users: true,
can_delete_other_users_tts_models: true,
can_delete_other_users_tts_results: true,
can_delete_other_users_w2l_results: true,
can_delete_other_users_w2l_templates: true,
can_delete_own_account: true,
can_delete_own_tts_models: true,
can_delete_own_tts_results: true,
can_delete_own_w2l_results: true,
can_delete_own_w2l_templates: true,
can_delete_users: true,
can_edit_other_users_profiles: true,
can_edit_other_users_tts_models: true,
can_edit_other_users_w2l_templates: true,
can_upload_tts_models: true,
can_upload_w2l_templates: true,
can_use_tts: true,
can_use_w2l: true,
core_info: {
default_avatar: {
color_index: 0,
image_index: 0
},
display_name: "Local User",
gravatar_hash: "",
user_token: "",
username: "local_user"
},
display_name: "Local User",
email_gravatar_hash: "",
fakeyou_plan: FakeYouPlan.Pro,
maybe_feature_flags: {},
storyteller_stream_plan: StorytellerStreamPlan.Pro,
user_token: "user_token"
}
}
}
async listMediaFiles({ filterMediaClasses, filterMediaType }: ListMediaFilesOptions): Promise<ListMediaFilesResult> {
let db = await this.getDatabase();
if (!filterMediaClasses && !filterMediaType) {
let mediaFiles = await db.getAll('media-files');
let media: ListMediaFilesResult = {
pagination: {
current: 0,
total_page_count: 1
},
results: mediaFiles.map((mediaFile) => {
let listItem: MediaFileListItem = {
created_at: mediaFile.created_at,
creator_set_visibility: Visibility.Public,
media_type: mediaFile.media_type,
origin: {
origin_category: MediaFileOriginCategory.Upload,
product_category: MediaFileOriginProductCategory.Unknown,
},
origin_category: MediaFileOriginCategory.Upload,
origin_product_category: MediaFileOriginProductCategory.Unknown,
public_bucket_path: mediaFile.public_bucket_path,
token: mediaFile.token,
updated_at: mediaFile.created_at
};
return listItem
}),
success: true
};
return media;
}
let media: Record<string, MediaFileListItem> = {};
let filteredResults = (await Promise.all([(filterMediaClasses ?? []).map((filter) =>
db.getAllFromIndex('media-files', 'media-class', filter)
), (filterMediaType ?? []).map((filter) =>
db.getAllFromIndex('media-files', 'media-type', filter)
)].flat())).flat();
for (const mediaFile of filteredResults) {
if (media[mediaFile.token]) {
continue;
}
media[mediaFile.token] = {
created_at: mediaFile.created_at,
creator_set_visibility: Visibility.Public,
media_type: mediaFile.media_type,
origin: {
origin_category: MediaFileOriginCategory.Upload,
product_category: MediaFileOriginProductCategory.Unknown,
},
origin_category: MediaFileOriginCategory.Upload,
origin_product_category: MediaFileOriginProductCategory.Unknown,
public_bucket_path: mediaFile.public_bucket_path,
token: mediaFile.token,
updated_at: mediaFile.created_at
};
}
let result: ListMediaFilesResult = {
pagination: {
current: 0,
total_page_count: 1
},
results: Object.keys(media).map((key) => media[key]),
success: true
};
return result;
}
async uploadMediaFile(file: File, { subtype, mediaClass }: UploadMediaFileOptions): Promise<string> {
let token = uuid();
let original_file_name = file.name;
let filename = token + "-" + file.name;
let media_type: MediaFileType | false = false;
if (mediaClass === MediaFileClass.Audio) {
media_type = MediaFileType.Audio;
} else if (mediaClass === MediaFileClass.Video) {
media_type = MediaFileType.Video;
} else if (mediaClass === MediaFileClass.Image) {
media_type = MediaFileType.Image;
} else if (mediaClass === MediaFileClass.Scene) {
if (subtype === MediaFileSubtype.StorytellerScene) {
media_type = MediaFileType.ScnRon;
}
} else if (mediaClass === MediaFileClass.Animation) {
media_type = MediaFileType.Mocap
} else if (file.name.endsWith("gltf")) {
media_type = MediaFileType.Gltf;
} else if (file.name.endsWith("glb")) {
media_type = MediaFileType.Glb;
} else if (file.name.endsWith("bvh")) {
media_type = MediaFileType.Bvh;
} else if (file.name.endsWith("fbx")) {
media_type = MediaFileType.Fbx;
}
if (!media_type) {
throw new Error("Couldn't determine media file type");
}
let media_file: MediaFile = {
token: token,
media_type,
media_class: mediaClass,
public_bucket_path: filename,
creator_set_visibility: Visibility.Public,
is_emulated_media_file: false,
maybe_title: original_file_name,
maybe_original_filename: original_file_name,
maybe_media_subtype: subtype,
stats: {
positive_rating_count: 0,
bookmark_count: 0
},
created_at: (new Date()).toLocaleString("en-us"),
updated_at: (new Date()).toLocaleString("en-us"),
};
let buffer = new Uint8Array(await file.arrayBuffer(), 0);
let db = await this.getDatabase();
let transaction = db.transaction(['media-files', 'file-blobs'], 'readwrite');
await Promise.all([
transaction.objectStore('media-files').add(media_file, token),
transaction.objectStore('file-blobs').add(buffer, filename),
transaction.done,
]);
return token;
}
async renameMediaFile(token: string, name: string): Promise<void> {
let db = await this.getDatabase();
let transaction = db.transaction('media-files', 'readwrite');
if (transaction.store !== undefined) {
let store = transaction.store;
let item = await store.get(token);
if (!item) { throw new Error("No Such File"); }
item.maybe_title = name;
await store.put(item, token);
}
await transaction.done;
}
async convertFbxToGltf(token: string): Promise<void> {
console.log("not implemented");
}
async mediaFile(token: string): Promise<MediaFile> {
let db = await this.getDatabase();
let file = await db.get('media-files', token);
if (!file) {
throw new Error("No Such File");
}
return file;
}
async mediaBlob(path: string): Promise<Uint8Array> {
let db = await this.getDatabase();
let file = await db.get('file-blobs', path);
if (!file) {
throw new Error("No Such File");
}
return file;
}
}
export class StorytellerApi implements IStorytellerApi {
createScene(file: File): Promise<string> {
return this.apiBase.createScene(file)
}
session(): Promise<Session> {
return this.apiBase.session()
}
listMediaFiles(options: ListMediaFilesOptions): Promise<ListMediaFilesResult> {
return this.apiBase.listMediaFiles(options)
}
uploadMediaFile(file: File, { subtype, mediaClass }: UploadMediaFileOptions): Promise<string> {
return this.apiBase.uploadMediaFile(file, { subtype, mediaClass })
}
renameMediaFile(token: string, name: string): Promise<void> {
return this.apiBase.renameMediaFile(token, name)
}
convertFbxToGltf(token: string): Promise<void> {
return this.apiBase.convertFbxToGltf(token)
}
mediaFile(token: string): Promise<MediaFile> {
return this.apiBase.mediaFile(token)
}
apiBase: IStorytellerApi;
constructor(apiBase: IStorytellerApi) {
this.apiBase = apiBase
}
}
export class ApiError<T extends AxiosResponse = AxiosResponse> extends Error {
detail: T;
constructor (detail: T) {
constructor(detail: T) {
super("API Error");
this.detail = detail;
}
+7
View File
@@ -38,6 +38,7 @@ web-sys = { version = "0.3.65", optional = true, features = [
"Document",
"HtmlElement",
"Window",
"Location"
] }
clap = { version = "4.4.18", features = ["derive"] }
futures-intrusive = "0.5.0"
@@ -52,6 +53,10 @@ serde_repr = "0.1.18"
bevy_tweening = "0.9"
async-compat = "0.2"
uuid = { version = "1", features = ["v4", "serde"]}
indexed_db_futures = { version = "0.4", optional = true}
wasm-bindgen-futures = { version = "0.4", optional = true}
dashmap = "5"
once_cell = "1"
[dependencies.reqwest]
version = "0.11.24"
@@ -67,6 +72,8 @@ wasm = [
"dep:js-sys",
"dep:serde-wasm-bindgen",
"dep:web-sys",
"dep:indexed_db_futures",
"dep:wasm-bindgen-futures",
"bevy_rapier3d/wasm-bindgen",
]
desktop = []
+165 -5
View File
@@ -6,6 +6,7 @@ use bevy::{
prelude::*,
utils::BoxedFuture,
};
use reqwest::StatusCode;
use super::response::GetMediaFileSuccessResponse;
@@ -78,6 +79,29 @@ impl RemoteAssetReader {
let reader: Box<Reader> = Box::new(VecReader::new(bytes));
Ok(reader)
}
#[cfg(all(feature = "wasm", target_family = "wasm"))]
async fn load_local<'a>(&'a self, path: &'a Path) -> Result<Box<Reader<'a>>, AssetReaderError> {
let token = path
.ext_file_prefix()
.ok_or_else(|| {
AssetReaderError::Io(std::io::Error::new(
ErrorKind::Other,
format!("Failed to parse token from '{path:?}'"),
))
})?
.to_str()
.unwrap();
let bytes = wasm::LoadLocalFromIdb::new(token);
info!("Sent request to bevy");
let bytes = bytes
.await
.map_err(|e| std::io::Error::new(ErrorKind::Other, format!("{e:?}")))?;
let reader: Box<Reader> = Box::new(VecReader::new(bytes));
Ok(reader)
}
}
impl AssetReader for RemoteAssetReader {
@@ -85,15 +109,25 @@ impl AssetReader for RemoteAssetReader {
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
let unboxed = async move { self.load(path).await };
if cfg!(not(feature = "wasm")) {
#[cfg(not(all(feature = "wasm", target_family = "wasm")))]
{
info!("wasm mode not detected");
// NB: Headless mode (server rendering) is running without a Tokio runtime.
// The reqwest HTTP client requires async-compat to function at runtime.
Box::pin(Compat::new(unboxed))
} else {
Box::pin(Compat::new(async move { self.load(path).await }))
}
#[cfg(all(feature = "wasm", target_family = "wasm"))]
{
info!("wasm mode");
Box::pin(unboxed)
if let Some(window) = web_sys::window() {
if let Ok(search) = window.location().search() {
if search.contains("mock-api") {
return Box::pin(async move { self.load_local(path).await });
}
}
}
Box::pin(async move { self.load(path).await })
}
}
@@ -128,3 +162,129 @@ impl IntoError<AssetReaderError> for reqwest::Error {
AssetReaderError::Io(std::io::Error::new(ErrorKind::Other, format!("{self}")))
}
}
#[cfg(all(feature = "wasm", target_family = "wasm"))]
mod wasm {
use std::task::Waker;
use bevy::log::info;
use dashmap::DashMap;
use futures::Future;
use js_sys::Uint8Array;
use once_cell::sync::Lazy;
use wasm_bindgen::{prelude::*, JsValue};
use web_sys::{CustomEvent, CustomEventInit};
#[wasm_bindgen(typescript_custom_section)]
const TIMELINE_TYPES: &str = r#"
export interface RequestFileEvent extends CustomEvent {
type: "request-file";
detail: string;
}
declare global {
export interface GlobalEventHandlersEventMap {
"request-file": RequestFileEvent;
}
}
"#;
// DashMap + Lazy allow us to create static, global hashmaps that work well concurrently
static FILE_WAKERS: Lazy<DashMap<String, Waker>> = Lazy::new(DashMap::new);
static FILE_RESPONSES: Lazy<DashMap<String, Result<Vec<u8>, String>>> = Lazy::new(DashMap::new);
/// Notifies us that the requested file couldn't be received
#[wasm_bindgen(js_name = requestedFileFailed)]
pub fn requested_file_failed(token: String, error: JsValue) {
FILE_RESPONSES.insert(token.clone(), Err(format!("Error: {error:?}")));
let waker = FILE_WAKERS.remove(&token);
if let Some((_, waker)) = waker {
waker.wake();
}
}
/// Provides us with the requested file's contents
#[wasm_bindgen(js_name = requestedFileReceived)]
pub fn requested_file_received(token: String, file: Uint8Array) {
info!("Received File From Mock API {token}");
FILE_RESPONSES.insert(token.clone(), Ok(file.to_vec()));
let waker = FILE_WAKERS.remove(&token);
if let Some((_, waker)) = waker {
waker.wake();
}
}
pub enum LoadLocalFromIdb {
RequestingFile { token: String },
CheckingForFile { token: String },
}
impl LoadLocalFromIdb {
pub fn new(file: impl ToString) -> Self {
Self::RequestingFile {
token: file.to_string(),
}
}
}
// We are implementing Future ourselves here because all the existing IndexedDb rust libraries
// rely on JsValue, which is not Send - preventing us from using it in a BoxedFuture.
// Instead, we ask the JS front end to grab the data from our mock API, and return it to us.
impl Future for LoadLocalFromIdb {
type Output = Result<Vec<u8>, String>;
fn poll(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
match self.as_ref().get_ref() {
LoadLocalFromIdb::RequestingFile { token } => {
info!("Requesting file {token}");
let Some(window) = web_sys::window() else {
return std::task::Poll::Ready(Err(
"Couldn't find browser window".to_string()
));
};
let event = CustomEvent::new_with_event_init_dict(
"request-file",
CustomEventInit::new()
.bubbles(true)
.cancelable(true)
.detail(&JsValue::from_str(token)),
)
.map_err(|err| format!("{err:?}"));
let event = match event {
Ok(e) => e,
Err(err) => {
return std::task::Poll::Ready(Err(err));
}
};
FILE_WAKERS.insert(token.clone(), cx.waker().clone());
if let Err(err) = window
.document()
.ok_or(JsValue::null())
.and_then(|document| document.dispatch_event(&event))
{
return std::task::Poll::Ready(Err(format!(
"Error Dispatching Message: {err:?}"
)));
}
self.set(LoadLocalFromIdb::CheckingForFile {
token: token.clone(),
});
std::task::Poll::Pending
}
LoadLocalFromIdb::CheckingForFile { token } => {
info!("Checking on file {token}");
match FILE_RESPONSES.remove(token) {
Some((_, value)) => {
info!("Found {token}");
std::task::Poll::Ready(value)
}
None => std::task::Poll::Pending,
}
}
}
}
}
}
+1 -1
View File
@@ -588,7 +588,7 @@ fn serialize_standart_materal(
ev_inspect.clear();
if let Some((target, handle)) = r_selection
if let Some((_target, handle)) = r_selection
.as_ref()
.and_then(|selection| find_handle_in_self_or_children(***selection, &q_children, &q_m))
.and_then(|target| q_m.get(target).ok().map(|h| (target, h.clone())))