From 6ca2a992e5509739e1d3e3cbb0c7e4318a1d604a Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Fri, 23 Feb 2024 21:05:32 -0500 Subject: [PATCH] Storyteller.ai API integration (#49) This PR integrates asset retrieval from the Storyteller.ai back-end API, and begins implementing long-term scene saving and storage. ### Media retrieval Instead of fetching raw asset URLs, going forward, media files from remote sources (e.g., in URL parameters) should be specified with a path like the following: ``` remote://. ``` where * `token` is a media token (e.g., "m_70b77neq9jsgmy5v31mekzchcbd223") * `filetype` is a file extension appropriate for the type of file being fetched (e.g., "gltf") The `remote` protocol will be recognized by a [custom asset reader](https://github.com/storytold/storyteller-bevy/blob/70726db111f6ccafd47feb055c5546540fef1786/studio/src/remote/reader.rs) which will retrieve the file by calling a Storyteller API endpoint. ### Scene storage Additionally, this PR begins laying the groundwork for storing/retrieving Bevy scenes to/from the backend instead of solely in-memory. This work is still incomplete -- as of this PR, a button has been added to the web UI allowing the user to download the scene file locally for testing/debugging purposes. ### Test URL Media retrieval from the backend can be tested by [visiting this URL](https://deploy-preview-49--storyteller-studio.netlify.app/?mode=viewer&mixamo=remote://m_70b77neq9jsgmy5v31mekzchcbd223.gltf) in Chrome with the `--disable-web-security` flag (to bypass CORS errors) --- Cargo.lock | 399 +++++++++++++++++++- studio-frontend/src/app/app.element.ts | 33 +- studio-frontend/src/app/button.element.scss | 9 +- studio-frontend/src/app/icon.element.ts | 28 +- studio/Cargo.toml | 14 +- studio/src/lib.rs | 3 + studio/src/persistance/mod.rs | 68 ++++ studio/src/remote/mod.rs | 22 ++ studio/src/remote/reader.rs | 114 ++++++ studio/src/remote/response.rs | 107 ++++++ 10 files changed, 770 insertions(+), 27 deletions(-) create mode 100644 studio/src/remote/mod.rs create mode 100644 studio/src/remote/reader.rs create mode 100644 studio/src/remote/response.rs diff --git a/Cargo.lock b/Cargo.lock index 66bf9a1..c46c87c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2063,7 +2063,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "socket2", + "socket2 0.4.9", "winapi", ] @@ -2277,6 +2277,16 @@ dependencies = [ "serde", ] +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "error-code" version = "2.3.1" @@ -2783,6 +2793,25 @@ dependencies = [ "svg_fmt", ] +[[package]] +name = "h2" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.0.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.3.1" @@ -2867,6 +2896,66 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "idna" version = "0.4.0" @@ -2955,6 +3044,12 @@ dependencies = [ "mach2", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "isahc" version = "1.7.2" @@ -3219,6 +3314,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + [[package]] name = "lock_api" version = "0.4.10" @@ -3420,6 +3521,24 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.7.0" @@ -3731,6 +3850,32 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.4.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "openssl-probe" version = "0.1.5" @@ -3739,9 +3884,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.99" +version = "0.9.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" dependencies = [ "cc", "libc", @@ -3755,7 +3900,7 @@ version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8378ac0dfbd4e7895f2d2c1f1345cab3836910baf3a300b000d04250f0c8428f" dependencies = [ - "redox_syscall", + "redox_syscall 0.3.5", ] [[package]] @@ -3797,7 +3942,7 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "smallvec", "windows-targets 0.48.5", ] @@ -4097,6 +4242,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "regex" version = "1.9.5" @@ -4147,6 +4301,48 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b" +[[package]] +name = "reqwest" +version = "0.11.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" +dependencies = [ + "base64 0.21.4", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "winreg", +] + [[package]] name = "robust" version = "1.1.0" @@ -4196,6 +4392,28 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.4", +] + [[package]] name = "ruzstd" version = "0.4.0" @@ -4246,6 +4464,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys 0.8.4", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys 0.8.4", + "libc", +] + [[package]] name = "semver" version = "1.0.21" @@ -4294,6 +4535,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -4385,6 +4638,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "space_bevy_xpbd_plugin" version = "0.3.1" @@ -4553,6 +4816,7 @@ dependencies = [ "itertools 0.12.1", "js-sys", "parking_lot", + "reqwest", "serde", "serde-wasm-bindgen", "serde_json", @@ -4601,6 +4865,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "sysinfo" version = "0.29.10" @@ -4615,6 +4885,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys 0.8.4", + "libc", +] + [[package]] name = "taffy" version = "0.3.13" @@ -4633,6 +4924,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand 2.0.1", + "redox_syscall 0.4.1", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "termcolor" version = "1.3.0" @@ -4718,6 +5022,45 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.5.4", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "toml_datetime" version = "0.6.5" @@ -4746,6 +5089,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.37" @@ -4830,6 +5179,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "ttf-parser" version = "0.19.2" @@ -4958,6 +5313,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -5030,6 +5394,19 @@ version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wayland-scanner" version = "0.29.5" @@ -5519,7 +5896,7 @@ dependencies = [ "orbclient", "percent-encoding", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.3.5", "wasm-bindgen", "wayland-scanner", "web-sys", @@ -5536,6 +5913,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "wyz" version = "0.2.0" diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index 2bc2427..5b2158f 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -5,6 +5,7 @@ import { type EntityMultiSpawnEvent, type EntitySelectEvent, type EntitySpawnEvent, + type SceneSavedEvent, type SceneStateEvent, TransformSpace, TransformType, @@ -150,6 +151,22 @@ export class AppElement extends LitElement { this.#updateHierarchy(); } + @on("window:scene-saved") + onSceneSaved({ detail: scene }: SceneSavedEvent): void { + const fileBlob = new Blob([scene], { type: "text/ron" }); + const url = URL.createObjectURL(fileBlob); + + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = "scene.scn.ron"; + anchor.click(); + + setTimeout(() => { + URL.revokeObjectURL(url); + anchor.remove(); + }); + } + toggleHelp(): void { this.showHelp = !this.showHelp; } @@ -211,6 +228,7 @@ export class AppElement extends LitElement { class="app-root" ${ref(this.#canvasProvider as any)} > + ${this.bvhPath ? html` `} + + ${this.mode === StudioMode.Editor ? html` + studio.saveScene()} + > + Save + + ` : nothing} Help @@ -245,6 +272,7 @@ export class AppElement extends LitElement { ${this.mode === StudioMode.Editor ? html` + +
${this.sceneObjects.map(obj => html` ` : nothing} + ${this.mode === StudioMode.Editor ? html` ` : nothing} + ${this.showHelp ? html` ` : nothing} diff --git a/studio-frontend/src/app/button.element.scss b/studio-frontend/src/app/button.element.scss index 085f9a9..2939af9 100644 --- a/studio-frontend/src/app/button.element.scss +++ b/studio-frontend/src/app/button.element.scss @@ -1,8 +1,8 @@ :host { display: inline-flex; align-items: center; - gap: 0.5rem; - padding: 10px 20px; + gap: 0.75rem; + padding: 0.5rem 1rem; border-radius: 0.5rem; font: { @@ -10,14 +10,14 @@ size: 15px; weight: 500; } - line-height: 1; + line-height: 1.5; pointer-events: auto; cursor: pointer; user-select: none; background: #0006; - color: #FFF; + color: #FFFC; backdrop-filter: blur(16px); transition: { property: background, color; @@ -28,6 +28,7 @@ :host(:hover) { background: #0008; + color: #FFFF; } .icon { diff --git a/studio-frontend/src/app/icon.element.ts b/studio-frontend/src/app/icon.element.ts index 668a344..5921924 100644 --- a/studio-frontend/src/app/icon.element.ts +++ b/studio-frontend/src/app/icon.element.ts @@ -6,19 +6,21 @@ import { unsafeSVG } from "lit/directives/unsafe-svg.js"; import farCrosshairs from "@fortawesome/fontawesome-pro/svgs/regular/crosshairs.svg?raw"; import farVideo from "@fortawesome/fontawesome-pro/svgs/regular/video.svg?raw"; -import fasCircleInfo from "@fortawesome/fontawesome-pro/svgs/regular/circle-info.svg?raw"; +import fasCircleQuestion from "@fortawesome/fontawesome-pro/svgs/solid/circle-question.svg?raw"; import fasKey from "@fortawesome/fontawesome-pro/svgs/solid/key.svg?raw"; -import fasCube from "@fortawesome/fontawesome-pro/svgs/regular/cube.svg?raw"; -import fasGlobe from "@fortawesome/fontawesome-pro/svgs/regular/globe.svg?raw"; -import fasUpDownLeftRight from "@fortawesome/fontawesome-pro/svgs/regular/arrows-up-down-left-right.svg?raw"; +import farCube from "@fortawesome/fontawesome-pro/svgs/regular/cube.svg?raw"; +import farGlobe from "@fortawesome/fontawesome-pro/svgs/regular/globe.svg?raw"; +import farUpDownLeftRight from "@fortawesome/fontawesome-pro/svgs/regular/arrows-up-down-left-right.svg?raw"; import fasPlay from "@fortawesome/fontawesome-pro/svgs/solid/play.svg?raw"; import fasFwdStep from "@fortawesome/fontawesome-pro/svgs/solid/forward-step.svg?raw"; import fasFwdFast from "@fortawesome/fontawesome-pro/svgs/solid/forward-fast.svg?raw"; import fasBwdStep from "@fortawesome/fontawesome-pro/svgs/solid/backward-step.svg?raw"; import fasBwdFast from "@fortawesome/fontawesome-pro/svgs/solid/backward-fast.svg?raw"; -import fasRotate from "@fortawesome/fontawesome-pro/svgs/regular/arrows-rotate.svg?raw"; -import fasScale from "@fortawesome/fontawesome-pro/svgs/regular/arrow-up-right-and-arrow-down-left-from-center.svg?raw"; +import fasQuestion from "@fortawesome/fontawesome-pro/svgs/solid/question.svg?raw"; +import farRotate from "@fortawesome/fontawesome-pro/svgs/regular/arrows-rotate.svg?raw"; +import farScale from "@fortawesome/fontawesome-pro/svgs/regular/arrow-up-right-and-arrow-down-left-from-center.svg?raw"; import fasCaretRight from "@fortawesome/fontawesome-pro/svgs/solid/caret-right.svg?raw"; +import fasFloppyDisk from "@fortawesome/fontawesome-pro/svgs/solid/floppy-disk.svg?raw"; import styles from "./icon.element.scss?inline"; @@ -47,18 +49,20 @@ function renderSvg(iconId: string) { const svgSrc = match (iconId, { "bwd-fast": () => fasBwdFast, "bwd-step": () => fasBwdStep, - "circle-info": () => fasCircleInfo, + "circle-question": () => fasCircleQuestion, "caret-right": () => fasCaretRight, "crosshairs": () => farCrosshairs, - "cube": () => fasCube, + "cube": () => farCube, + "floppy-disk": () => fasFloppyDisk, "fwd-fast": () => fasFwdFast, "fwd-step": () => fasFwdStep, "key": () => fasKey, - "globe": () => fasGlobe, + "globe": () => farGlobe, "play": () => fasPlay, - "rotate": () => fasRotate, - "scale": () => fasScale, - "up-down-left-right": () => fasUpDownLeftRight, + "question": () => fasQuestion, + "rotate": () => farRotate, + "scale": () => farScale, + "up-down-left-right": () => farUpDownLeftRight, "video": () => farVideo, _: () => null, }); diff --git a/studio/Cargo.toml b/studio/Cargo.toml index 62dd252..3daa843 100644 --- a/studio/Cargo.toml +++ b/studio/Cargo.toml @@ -27,9 +27,9 @@ console_error_panic_hook = { version = "0.1.7", optional = true } itertools = "0.12.1" js-sys = { version = "0.3.65", optional = true } parking_lot = "0.12.1" -serde = { version = "1.0.192", optional = true } +serde = "1.0.192" serde-wasm-bindgen = { version = "0.6.1", optional = true } -serde_json = { version = "1.0.108", optional = true } +serde_json = "1.0.108" studio-bvh = { path = "../studio-bvh" } wasm-bindgen = "0.2.84" web-sys = { version = "0.3.65", optional = true, features = [ @@ -49,12 +49,18 @@ bytemuck = "1.14.1" space_editor = {git = "https://github.com/rewin123/space_editor", rev = "ce83f86dc902787ab16213f3209276a784654df6"} anyhow.workspace = true +[dependencies.reqwest] +version = "0.11.24" +features = [ + "default", + "json", + "stream", +] + [features] wasm = [ "dep:console_error_panic_hook", "dep:js-sys", - "dep:serde", - "dep:serde_json", "dep:serde-wasm-bindgen", "dep:web-sys", "bevy_rapier3d/wasm-bindgen", diff --git a/studio/src/lib.rs b/studio/src/lib.rs index a449892..3641593 100644 --- a/studio/src/lib.rs +++ b/studio/src/lib.rs @@ -28,6 +28,7 @@ mod interaction; pub mod math; mod mixamo; mod persistance; +mod remote; mod scene; pub mod screenshot; mod skybox; @@ -46,6 +47,7 @@ pub use crate::{ math::InterpolationPlugin, mixamo::{MixamoImportEvent, MixamoPlugin}, persistance::PersistancePlugin, + remote::RemoteAssetPlugin, scene::{LoadSceneEvent, ScenePlugin}, skybox::{SkyboxId, SkyboxPlugin}, ui::UiLayerPlugin, @@ -251,6 +253,7 @@ pub fn start_internal( #[cfg(feature = "wasm")] viewport::wasm::ViewportPlugin, WebAssetPlugin, + RemoteAssetPlugin, DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { resolution: viewport_size.into(), diff --git a/studio/src/persistance/mod.rs b/studio/src/persistance/mod.rs index 6f5842c..d1ea77e 100644 --- a/studio/src/persistance/mod.rs +++ b/studio/src/persistance/mod.rs @@ -10,6 +10,13 @@ impl Plugin for PersistancePlugin { EditorTabName::Other("Persistance".to_string()), DebugPersistanceTab::default(), ); + + #[cfg(feature = "wasm")] + app.add_systems(Update, wasm::forward_save_scene_events) + .add_systems( + OnEnter(SaveState::Idle), + wasm::on_scene_saved.map(bevy::utils::warn), + ); } } @@ -43,3 +50,64 @@ impl EditorTab for DebugPersistanceTab { "Persistance".into() } } + +#[cfg(feature = "wasm")] +mod wasm { + use bevy::{prelude::*, window::PrimaryWindow}; + use space_editor::prelude::{EditorEvent, EditorPrefabPath, PrefabMemoryCache}; + use wasm_bindgen::prelude::*; + + use crate::wasm::{self, StaticBuffer, WasmMessageBuffer}; + + static SAVE_SCENE: WasmMessageBuffer<()> = WasmMessageBuffer::new(); + + #[wasm_bindgen(js_name = saveScene)] + pub fn save_scene() { + SAVE_SCENE.write_message(()); + } + + pub(super) fn forward_save_scene_events(mut ew_editor_events: EventWriter) { + if SAVE_SCENE.take_message().is_some() { + ew_editor_events.send(EditorEvent::Save(EditorPrefabPath::MemoryCahce)); + } + } + + #[wasm_bindgen(typescript_custom_section)] + const SCENE_SAVED_EVENT: &str = r#" +export interface SceneSavedEvent extends CustomEvent { + type: "scene-saved"; + detail: string; +} + +declare global { + export interface GlobalEventHandlersEventMap { + "scene-saved": SceneSavedEvent; + } +} + "#; + + pub(super) fn on_scene_saved( + r_scene_cache: Res, + r_type_registry: Res, + ra_scenes: Res>, + q_window: Query<&Window, With>, + ) -> Result<(), String> { + let handle = r_scene_cache + .scene + .as_ref() + .ok_or_else(|| "Scene cache is empty!".to_string())?; + + let serialized = match ra_scenes.get(handle) { + Some(scene) => scene + .serialize_ron(&r_type_registry) + .map_err(|err| format!("{err}")), + None => Err("No scene found for cached handle!".into()), + }?; + + let Ok(window) = q_window.get_single() else { + return Ok(()); + }; + + wasm::dispatch_to_js(window, "scene-saved", &serialized) + } +} diff --git a/studio/src/remote/mod.rs b/studio/src/remote/mod.rs new file mode 100644 index 0000000..99ce463 --- /dev/null +++ b/studio/src/remote/mod.rs @@ -0,0 +1,22 @@ +use bevy::{ + asset::io::{AssetSource, AssetSourceId}, + prelude::*, +}; + +use self::reader::RemoteAssetReader; + +mod reader; +mod response; + +pub struct RemoteAssetPlugin; + +impl Plugin for RemoteAssetPlugin { + fn build(&self, app: &mut App) { + app.register_asset_source( + AssetSourceId::Name("remote".into()), + AssetSource::build() + .with_reader(|| Box::new(RemoteAssetReader)) + .with_writer(|| None), + ); + } +} diff --git a/studio/src/remote/reader.rs b/studio/src/remote/reader.rs new file mode 100644 index 0000000..1753860 --- /dev/null +++ b/studio/src/remote/reader.rs @@ -0,0 +1,114 @@ +use std::{io::ErrorKind, path::Path}; + +use bevy::{ + asset::io::{AssetReader, AssetReaderError, PathStream, Reader, VecReader}, + utils::BoxedFuture, +}; +use reqwest::StatusCode; + +use super::response::GetMediaFileSuccessResponse; + +pub struct RemoteAssetReader; + +impl RemoteAssetReader { + const API_BASE_URL: &'static str = "https://api.fakeyou.com/v1/media_files/file"; + const BUCKET_BASE_URL: &'static str = "https://storage.googleapis.com/vocodes-public"; +} + +impl AssetReader for RemoteAssetReader { + fn read<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + Box::pin(async move { + let token = path + .file_stem() + .ok_or_else(|| { + AssetReaderError::Io(std::io::Error::new( + ErrorKind::Other, + format!("Failed to parse token from '{path:?}'"), + )) + })? + .to_str() + .unwrap(); + + let response = reqwest::get(format!("{}/{token}", &Self::API_BASE_URL)) + .await + .map_err(IntoError::::into)?; + + let response = match response.status() { + StatusCode::OK => response + .json::() + .await + .map_err(IntoError::::into), + other => Err(AssetReaderError::Io(std::io::Error::new( + ErrorKind::Other, + format!("Status Code from endpoint: {other}"), + ))), + }?; + + let url = format!( + "{}{}", + &Self::BUCKET_BASE_URL, + &response.media_file.public_bucket_path + ); + + let response = reqwest::get(url) + .await + .map_err(IntoError::::into)?; + + let bytes = match response.status() { + // TODO: This is not thread-safe, but some variation of this + // would be better than the current solution once Bevy + // WASM becomes multi-threaded: + // StatusCode::OK => Ok(response.bytes_stream().map(|result| { + // result.map_err(|err| std::io::Error::new(ErrorKind::Other, format!("{err}"))) + // })), + StatusCode::OK => Ok(response + .bytes() + .await + .map_err(IntoError::::into)? + .into_iter() + .collect::>()), + other => Err(AssetReaderError::Io(std::io::Error::new( + ErrorKind::Other, + format!("Status Code from bucket URL: {other}"), + ))), + }?; + + let reader: Box = Box::new(VecReader::new(bytes)); + Ok(reader) + }) + } + + fn read_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + // TODO: Do we want to try to read .meta files? + Box::pin(async { Err(AssetReaderError::NotFound(path.into())) }) + } + + fn read_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result, AssetReaderError>> { + // We won't have to deal with directories + Box::pin(async { Err(AssetReaderError::NotFound(path.into())) }) + } + + fn is_directory<'a>(&'a self, _: &'a Path) -> BoxedFuture<'a, Result> { + // We won't have to deal with directories + Box::pin(async { Ok(false) }) + } +} + +trait IntoError { + fn into(self) -> Output; +} + +impl IntoError for reqwest::Error { + fn into(self) -> AssetReaderError { + AssetReaderError::Io(std::io::Error::new(ErrorKind::Other, format!("{self}"))) + } +} diff --git a/studio/src/remote/response.rs b/studio/src/remote/response.rs new file mode 100644 index 0000000..7225b9b --- /dev/null +++ b/studio/src/remote/response.rs @@ -0,0 +1,107 @@ +//! Response types copy/pasted straight from the backend source code. + +use serde::Deserialize; + +#[derive(Deserialize, Debug)] +pub struct GetMediaFileSuccessResponse { + pub success: bool, + pub media_file: MediaFileInfo, +} + +#[derive(Deserialize, Debug)] +pub struct MediaFileInfo { + pub token: String, + + /// Type of media will dictate which fields are populated and what + /// the frontend should display (eg. video player vs audio player). + pub media_type: String, + + /// If the file was generated as part of a batch, this is the token for the batch. + pub maybe_batch_token: Option, + + /// URL to the media file + pub public_bucket_path: String, + + //// Provenance Data + //pub origin_product: MediaFileOriginProductCategory, + //pub origin_category: MediaFileOriginCategory, + //pub maybe_origin_model_type: Option, + //pub maybe_origin_model_token: Option, + //pub maybe_origin_filename: Option, + /// Possible model info (if generated by inference) + pub maybe_model_weight_info: Option, + + /// User info + pub maybe_creator_user: Option, + + pub creator_set_visibility: String, + + /// Text transcripts for TTS, etc. + pub maybe_text_transcript: Option, + + /// We can simulate media files for "tts_results" records. + /// If this is set as true, this informs the frontend and API callers not to treat + /// the token as a media file and improperly assume it can be used with the rest of + /// the media file APIs. + pub is_emulated_media_file: bool, + + /// Statistics about the media file + pub stats: SimpleEntityStats, + + pub created_at: String, + pub updated_at: String, + //pub maybe_moderator_fields: Option, +} + +#[derive(Deserialize, Debug)] +pub struct GetMediaFileModelInfo { + pub weight_token: String, + pub weight_type: String, + pub weight_category: String, + pub title: String, + + /// Cover images are small descriptive images that can be set for any model. + /// If a cover image is set, this is the path to the asset. + pub maybe_cover_image_public_bucket_path: Option, + + /// NB: Technically this shouldn't be a nullable field, but since there are so many + /// joins we'll relax that this must exist (for now). + pub maybe_weight_creator: Option, +} + +#[derive(Deserialize, Debug)] +pub struct UserDetailsLight { + /// The token for the user + pub user_token: String, + + /// The unique username someone logs in with + /// As of 2023-08-23, this is always lowercase + pub username: String, + + /// As of 2023-08-23, this is the username with capitalization + /// (In the future, a display name can be customized by the user.) + pub display_name: String, + + /// Email hash for Gravatar + /// Always set for now since login is email/username+password. + /// In the future this will need to become an optional *OR* be filled with a bogus hash. + pub gravatar_hash: String, + + /// For users without a gravatar, we show one of our own avatars. + pub default_avatar: DefaultAvatarInfo, + // In the future, we'll also support user-uploaded avatars that we store on our servers. +} + +#[derive(Deserialize, Debug)] +pub struct DefaultAvatarInfo { + pub image_index: u8, + pub color_index: u8, +} + +#[derive(Deserialize, Debug)] +pub struct SimpleEntityStats { + /// Number of positive ratings (or "likes") for this item + pub positive_rating_count: u32, + /// Number of bookmarks for this item + pub bookmark_count: u32, +}