diff --git a/Cargo.lock b/Cargo.lock index 6770305..ab605c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,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.58", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -339,7 +351,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -404,7 +416,7 @@ checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" dependencies = [ "concurrent-queue", "event-listener 5.2.0", - "event-listener-strategy 0.5.0", + "event-listener-strategy 0.5.1", "futures-core", "pin-project-lite", ] @@ -434,9 +446,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.8.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" +checksum = "10b3e585719c2358d2660232671ca8ca4ddb4be4ce8a1842d6c2dc8685303316" dependencies = [ "async-lock 3.3.0", "async-task", @@ -601,7 +613,7 @@ checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -724,7 +736,7 @@ checksum = "b3c488161a04a123e10273e16d4533945943fcfcf345f066242790e8977aee2d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -835,7 +847,7 @@ checksum = "adda0f1b91d2e34b132ea29a14dbe2adf273073c168950603ebddaf53a23947f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -847,7 +859,7 @@ dependencies = [ "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -935,7 +947,7 @@ checksum = "7de77523d154e220a740e568a89f52fac7de481374bdecbbbeb283a37580ba34" dependencies = [ "bevy_macro_utils", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -983,7 +995,7 @@ dependencies = [ "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -1031,7 +1043,7 @@ checksum = "fa29be733a02a5d7ca4507ef15f294711c1a0884b9a9a2730640ff4e7d0200ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -1081,7 +1093,7 @@ dependencies = [ "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -1207,7 +1219,7 @@ dependencies = [ "proc-macro2", "quote", "rustc-hash", - "syn 2.0.55", + "syn 2.0.58", "toml_edit 0.21.1", ] @@ -1518,7 +1530,7 @@ dependencies = [ "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", "uuid", ] @@ -1576,7 +1588,7 @@ dependencies = [ "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -1754,7 +1766,7 @@ checksum = "014c80f466ed01821a2e602d63cd5076915c1af5de5fa3c074cc4a9ca898ada7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -1838,7 +1850,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e1ef1d5e328abe1b76df974245f78e17fd17867583883d5e77444c6a8223a64" dependencies = [ "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -1858,7 +1870,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2043,7 +2055,7 @@ checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2182,7 +2194,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2614,6 +2626,18 @@ dependencies = [ "tokio", ] +[[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.58", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -2820,7 +2844,7 @@ checksum = "92959a9e8d13eaa13b8ae8c7b583c3bf1669ca7a8e7708a088d12587ba86effc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2850,7 +2874,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2861,7 +2885,7 @@ checksum = "6fd000fd6988e73bbe993ea3db9b1aa64906ab88766d654973924340c8cddb42" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2960,9 +2984,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +checksum = "332f51cb23d20b0de8458b86580878211da09bcd4503cb579c225b3d124cabb3" dependencies = [ "event-listener 5.2.0", "pin-project-lite", @@ -2984,6 +3008,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.58", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -3072,7 +3108,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -3197,7 +3233,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -3416,7 +3452,7 @@ dependencies = [ "inflections", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -3521,9 +3557,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" +checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" dependencies = [ "bytes", "fnv", @@ -3815,9 +3851,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9b4f005360d32e9325029b38ba47ebd7a56f3316df09249368939562d518645" +checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" dependencies = [ "bytemuck", "byteorder", @@ -3858,6 +3894,23 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" +[[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 = "2.2.6" @@ -3920,7 +3973,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -4235,6 +4288,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.58", +] + +[[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.58", +] + +[[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.58", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -4596,7 +4696,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -4658,7 +4758,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -4824,7 +4924,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -4835,9 +4935,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.101" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -4970,14 +5070,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -5148,7 +5248,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -5785,9 +5885,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -5798,9 +5898,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" dependencies = [ "core-foundation-sys", "libc", @@ -5855,7 +5955,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -5888,7 +5988,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -6301,9 +6401,9 @@ dependencies = [ [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "studio" @@ -6323,12 +6423,15 @@ dependencies = [ "bytemuck", "clap", "console_error_panic_hook", + "dashmap", "futures", "futures-intrusive", "futures-lite 2.3.0", - "image 0.25.0", + "image 0.25.1", + "indexed_db_futures", "itertools", "js-sys", + "once_cell", "parking_lot", "reqwest", "serde", @@ -6411,9 +6514,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.55" +version = "2.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ "proc-macro2", "quote", @@ -6428,9 +6531,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sysinfo" -version = "0.30.7" +version = "0.30.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c385888ef380a852a16209afc8cfad22795dd8873d69c9a14d2e2088f118d18" +checksum = "4b1a378e48fb3ce3a5cf04359c456c9c98ff689bcf1c1bc6e6a31f247686f275" dependencies = [ "cfg-if", "core-foundation-sys", @@ -6536,7 +6639,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -6771,7 +6874,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -7013,12 +7116,13 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.8.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ "getrandom 0.2.12", "serde", + "wasm-bindgen", ] [[package]] @@ -7126,7 +7230,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", "wasm-bindgen-shared", ] @@ -7160,7 +7264,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7940,9 +8044,9 @@ checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621" [[package]] name = "xml-rs" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" +checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" [[package]] name = "xmlwriter" @@ -7967,7 +8071,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] diff --git a/package-lock.json b/package-lock.json index 27871e5..b4002e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 55de6be..6962616 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/screenshots/img_0004.png b/screenshots/img_0004.png new file mode 100644 index 0000000..a0c22db Binary files /dev/null and b/screenshots/img_0004.png differ diff --git a/screenshots/img_0005.png b/screenshots/img_0005.png new file mode 100644 index 0000000..a0c22db Binary files /dev/null and b/screenshots/img_0005.png differ diff --git a/screenshots/img_0006.png b/screenshots/img_0006.png new file mode 100644 index 0000000..a0c22db Binary files /dev/null and b/screenshots/img_0006.png differ diff --git a/screenshots/img_0007.png b/screenshots/img_0007.png new file mode 100644 index 0000000..a0c22db Binary files /dev/null and b/screenshots/img_0007.png differ diff --git a/screenshots/img_0008.png b/screenshots/img_0008.png new file mode 100644 index 0000000..a0c22db Binary files /dev/null and b/screenshots/img_0008.png differ diff --git a/screenshots/img_0009.png b/screenshots/img_0009.png new file mode 100644 index 0000000..a0c22db Binary files /dev/null and b/screenshots/img_0009.png differ diff --git a/screenshots/img_0010.png b/screenshots/img_0010.png new file mode 100644 index 0000000..a0c22db Binary files /dev/null and b/screenshots/img_0010.png differ diff --git a/screenshots/img_0011.png b/screenshots/img_0011.png new file mode 100644 index 0000000..a0c22db Binary files /dev/null and b/screenshots/img_0011.png differ diff --git a/screenshots/img_0012.png b/screenshots/img_0012.png new file mode 100644 index 0000000..a0c22db Binary files /dev/null and b/screenshots/img_0012.png differ diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index 11ed178..17c336d 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -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(); #videoStylePreviewBtnRef = createRef(); @@ -76,43 +78,15 @@ export class AppElement extends LitElement { async onSceneSaved({ detail: scene }: SceneSavedEvent): Promise { 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); diff --git a/studio-web/package.json b/studio-web/package.json index 2b55d35..363b3c4 100644 --- a/studio-web/package.json +++ b/studio-web/package.json @@ -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", diff --git a/studio-web/src/lib/storyteller-api.ts b/studio-web/src/lib/storyteller-api.ts index 2df7959..980bb3e 100644 --- a/studio-web/src/lib/storyteller-api.ts +++ b/studio-web/src/lib/storyteller-api.ts @@ -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 + session(): Promise + listMediaFiles(options: ListMediaFilesOptions): Promise + uploadMediaFile( + file: File, + { subtype, mediaClass }: UploadMediaFileOptions, + ): Promise + renameMediaFile(token: MediaFileToken, name: string): Promise + convertFbxToGltf(token: MediaFileToken): Promise + mediaFile(token: MediaFileToken): Promise +} + +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; + 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> { + if (this.database) { + return this.database; + } + this.database = await openDB('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 { + 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 { + 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 { + 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 = {}; + + 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 { + 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 { + 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 { + console.log("not implemented"); + } + + async mediaFile(token: string): Promise { + 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 { + 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 { + return this.apiBase.createScene(file) + } + session(): Promise { + return this.apiBase.session() + } + listMediaFiles(options: ListMediaFilesOptions): Promise { + return this.apiBase.listMediaFiles(options) + } + uploadMediaFile(file: File, { subtype, mediaClass }: UploadMediaFileOptions): Promise { + return this.apiBase.uploadMediaFile(file, { subtype, mediaClass }) + } + renameMediaFile(token: string, name: string): Promise { + return this.apiBase.renameMediaFile(token, name) + } + convertFbxToGltf(token: string): Promise { + return this.apiBase.convertFbxToGltf(token) + } + mediaFile(token: string): Promise { + return this.apiBase.mediaFile(token) + } + apiBase: IStorytellerApi; + + constructor(apiBase: IStorytellerApi) { + this.apiBase = apiBase + } +} + export class ApiError extends Error { detail: T; - constructor (detail: T) { + constructor(detail: T) { super("API Error"); this.detail = detail; } diff --git a/studio/Cargo.toml b/studio/Cargo.toml index fb373b6..7c6e76f 100644 --- a/studio/Cargo.toml +++ b/studio/Cargo.toml @@ -38,6 +38,7 @@ web-sys = { version = "0.3", optional = true, features = [ "Document", "HtmlElement", "Window", + "Location", "WorkerGlobalScope" ] } clap = { version = "4.4.18", features = ["derive"] } @@ -51,10 +52,13 @@ bytemuck = "1.14.1" space_editor = {git = "https://github.com/rewin123/space_editor", rev = "0f15c0d6b6b5864f4a05a2d57dfc2066e96d1a3b"} anyhow.workspace = true serde_repr = "0.1.18" -wasm-bindgen-futures = "0.4" 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" bevy_tweening = "0.10.0" [dependencies.reqwest] @@ -71,6 +75,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 = [] diff --git a/studio/src/headless.rs b/studio/src/headless.rs index 119cd6d..24144d4 100644 --- a/studio/src/headless.rs +++ b/studio/src/headless.rs @@ -209,13 +209,14 @@ fn start_timeline_animation( mut ev: EventWriter, ) { if keyframes.is_empty() { - return; + // return; } commands.remove_resource::(); let total_time = keyframes .iter() .fold(0f32, |previous, keyframe| previous.max(keyframe.time)) - .max(3.0); + .max(3.0) + .min(1.0); let frames = (total_time * headless_settings.fps).ceil() as u32; headless_settings.max_frames = frames; diff --git a/studio/src/remote/reader.rs b/studio/src/remote/reader.rs index 685666d..4c7e23c 100644 --- a/studio/src/remote/reader.rs +++ b/studio/src/remote/reader.rs @@ -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 = 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>, 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 = 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>, 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 }) } } @@ -131,3 +165,129 @@ impl IntoError for reqwest::Error { ))) } } + +#[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> = Lazy::new(DashMap::new); + static FILE_RESPONSES: Lazy, 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, String>; + + fn poll( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + 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, + } + } + } + } + } +} diff --git a/studio/src/scene/mod.rs b/studio/src/scene/mod.rs index cd6261b..d289044 100644 --- a/studio/src/scene/mod.rs +++ b/studio/src/scene/mod.rs @@ -195,7 +195,7 @@ fn check_loading( // `Handle`s. if loading_queue.queue.is_empty() && **r_scene_state == SceneState::Loading - && *frames_without_loading > 100 + && *frames_without_loading > 200 { info!("Transition to active"); st_next.set(SceneState::Active); diff --git a/studio/src/screenshot.rs b/studio/src/screenshot.rs index 4c2edb0..e72d512 100644 --- a/studio/src/screenshot.rs +++ b/studio/src/screenshot.rs @@ -148,7 +148,7 @@ fn extract_screen_capture_components( screen_capure_comps: Extract>>, scene_state: Extract>>, ) { - if scene_state.get() != &SceneState::Active { + if *scene_state.get() != SceneState::Active { return; }; for (entity, screen_capture) in screen_capure_comps.iter() { diff --git a/studio/src/ui/inspector.rs b/studio/src/ui/inspector.rs index 124cc4b..ac0ef28 100644 --- a/studio/src/ui/inspector.rs +++ b/studio/src/ui/inspector.rs @@ -598,7 +598,7 @@ fn serialize_standart_materal( ev_inspect.clear(); - if let Some((_, 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()))) diff --git a/test_headless_mode.sh b/test_headless_mode.sh new file mode 100644 index 0000000..43d61ee --- /dev/null +++ b/test_headless_mode.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Update this to a recent scene to download it. +scene_url="https://storage.googleapis.com/vocodes-public/media/d/2/d/s/e/d2dseec97gw06fk7mdavmk4pv87dk0dt/scene_d2dseec97gw06fk7mdavmk4pv87dk0dt.scn.ron" + +filename="downloaded_scene.scn.ron" +directory="studio/assets/" +full_path="${directory}${filename}" + +if [ ! -f "${full_path}" ]; then + echo "Downloading ${scene_url}" + mkdir -p "${directory}" + wget "${scene_url}" -O "${full_path}" +fi + +cargo run -p studio --bin studio-headless \ + -- \ + scene \ + -o frames/ \ + --frames 100 \ + --skybox "00ff00" \ + $filename +