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://<token>.<filetype>
```
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)
This commit is contained in:
Danny McGee
2024-02-23 21:05:32 -05:00
committed by GitHub
parent fffcb38ec6
commit 6ca2a992e5
10 changed files with 770 additions and 27 deletions
+32 -1
View File
@@ -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)}
>
<!-- Scene view -->
${this.bvhPath ? html`
<sts-bvh-viewer
bvhPath=${this.bvhPath}
@@ -232,11 +250,20 @@ export class AppElement extends LitElement {
></sts-viewer>
`}
<!-- Actions toolbar -->
<sts-toolbar class="v-start h-start">
<sts-toolbar-group>
${this.mode === StudioMode.Editor ? html`
<sts-button
icon="floppy-disk"
@click=${() => studio.saveScene()}
>
Save
</sts-button>
` : nothing}
<sts-button
class=${classMap({ active: this.showHelp })}
icon="circle-info"
icon="question"
@click=${this.toggleHelp}
>
Help
@@ -245,6 +272,7 @@ export class AppElement extends LitElement {
</sts-toolbar>
${this.mode === StudioMode.Editor ? html`
<!-- Transform toolbar -->
<sts-transform-toolbar
class="v-start h-center"
.type=${this.transformType}
@@ -253,6 +281,7 @@ export class AppElement extends LitElement {
@space-change=${this.onTransformSpaceChange}
></sts-transform-toolbar>
<!-- Hierarchy and inspector -->
<div class="hierarchy v-start h-end">
${this.sceneObjects.map(obj => html`
<sts-tree class="scene-object"
@@ -264,10 +293,12 @@ export class AppElement extends LitElement {
</div>
` : nothing}
<!-- Animation timeline -->
${this.mode === StudioMode.Editor ? html`
<sts-anim-editor class="timeline v-end h-start h-before-hierarchy"></sts-anim-editor>
` : nothing}
<!-- Dialogs and modals -->
${this.showHelp ? html`
<sts-help class="help v-after-toolbar h-start"></sts-help>
` : nothing}
+5 -4
View File
@@ -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 {
+16 -12
View File
@@ -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,
});