mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-17 00:13:17 +00:00
Merge branch 'add-stuff-to-scene' into apply_animations
This commit is contained in:
Generated
+12
-20
@@ -10,12 +10,12 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-pro": "^6.4.2",
|
||||
"axios": "^1.6.7",
|
||||
"lit": "^2.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"tslib": "^2.3.0",
|
||||
"uuid": "^9.0.1"
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nx/js": "16.9.1",
|
||||
@@ -4976,8 +4976,7 @@
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/available-typed-arrays": {
|
||||
"version": "1.0.5",
|
||||
@@ -4992,12 +4991,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
|
||||
"integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
|
||||
"dev": true,
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
|
||||
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"follow-redirects": "^1.15.4",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
@@ -5606,7 +5604,6 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
@@ -5950,7 +5947,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
@@ -6531,10 +6527,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.3",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
|
||||
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
|
||||
"dev": true,
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
||||
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -6591,7 +6586,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
@@ -8041,7 +8035,6 @@
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -8050,7 +8043,6 @@
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
@@ -8834,8 +8826,7 @@
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/psl": {
|
||||
"version": "1.9.0",
|
||||
@@ -10202,6 +10193,7 @@
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
|
||||
+2
-2
@@ -42,11 +42,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-pro": "^6.4.2",
|
||||
"axios": "^1.6.7",
|
||||
"lit": "^2.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"tslib": "^2.3.0",
|
||||
"uuid": "^9.0.1"
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,3 +165,9 @@ sts-button.active {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move this
|
||||
.menu-button-icon {
|
||||
font-size: 12px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { drag, on } from "@storyteller/framework";
|
||||
import { drag, on, provide } from "@storyteller/framework";
|
||||
import * as studio from "@storyteller/studio";
|
||||
import {
|
||||
type SceneSavedEvent,
|
||||
@@ -6,8 +6,10 @@ import {
|
||||
SceneState,
|
||||
StudioMode,
|
||||
} from "@storyteller/studio";
|
||||
import { ButtonElement } from "@storyteller/studio-ui/button";
|
||||
import {
|
||||
SceneHierarchyElement,
|
||||
StorytellerApi,
|
||||
StudioURLParams,
|
||||
} from "@storyteller/studio-web";
|
||||
import { match } from "@storyteller/utility";
|
||||
@@ -16,12 +18,14 @@ import { customElement, 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";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { MediaLibraryElement } from "./media-library.element";
|
||||
|
||||
import "@storyteller/studio-ui/button";
|
||||
import "@storyteller/studio-ui/icon";
|
||||
import "@storyteller/studio-ui/overlay-provider";
|
||||
import "@storyteller/studio-ui/toolbar";
|
||||
import "@storyteller/studio-ui/tree";
|
||||
import "@storyteller/studio-web/add-to-scene-menu";
|
||||
import "@storyteller/studio-web/canvas-provider";
|
||||
import "@storyteller/studio-web/entity-inspector";
|
||||
import "@storyteller/studio-web/iframe-messaging-bridge";
|
||||
@@ -33,10 +37,15 @@ import "./anim-editor.element";
|
||||
import "./help.element";
|
||||
|
||||
import "./blocking.element";
|
||||
import "./media-library.element";
|
||||
|
||||
import "./app.element.scss";
|
||||
|
||||
@customElement("app-root")
|
||||
@provide({
|
||||
token: StorytellerApi,
|
||||
provide() { return this.api; }
|
||||
})
|
||||
export class AppElement extends LitElement {
|
||||
@state() showHelp = false;
|
||||
@state() showInspector = false;
|
||||
@@ -46,8 +55,11 @@ export class AppElement extends LitElement {
|
||||
@state() inspectorWidth = 320;
|
||||
@state() hierarchyHeight?: number;
|
||||
|
||||
private api = new StorytellerApi();
|
||||
#enableAnimTimeline = false; // TODO
|
||||
|
||||
#addToSceneButtonRef = createRef<ButtonElement>();
|
||||
#mediaLibraryRef = createRef<MediaLibraryElement>();
|
||||
#inspectorRef = createRef<HTMLDivElement>();
|
||||
#hierarchyRef = createRef<SceneHierarchyElement>();
|
||||
|
||||
@@ -59,34 +71,15 @@ export class AppElement extends LitElement {
|
||||
|
||||
@on("scene-saved")
|
||||
async onSceneSaved({ detail: scene }: SceneSavedEvent): Promise<void> {
|
||||
const file = new File([scene], "scene.scn.ron", { type: "text/ron" });
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append("uuid_idempotency_token", uuid());
|
||||
formData.append("file", file);
|
||||
|
||||
try {
|
||||
const response = await fetch("https://api.fakeyou.com/v1/engine/create_scene", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Accept": "text/plain" },
|
||||
body: formData,
|
||||
});
|
||||
const file = new File([scene], "scene.scn.ron", { type: "text/ron" });
|
||||
const token = await this.api.createScene(file);
|
||||
|
||||
if (response.ok) {
|
||||
const resultJson: {
|
||||
success: boolean;
|
||||
media_file_token: string;
|
||||
} = await response.json();
|
||||
|
||||
if (resultJson.success) {
|
||||
this.dispatchEvent(new CustomEvent("scene-uploaded", {
|
||||
detail: resultJson.media_file_token,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent("scene-uploaded", {
|
||||
detail: token,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
@@ -145,6 +138,7 @@ export class AppElement extends LitElement {
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
<sts-overlay-provider>
|
||||
<sts-canvas-provider class="app-root">
|
||||
<sts-scene-data-provider>
|
||||
<sts-iframe-messaging-bridge></sts-iframe-messaging-bridge>
|
||||
@@ -174,6 +168,16 @@ export class AppElement extends LitElement {
|
||||
>
|
||||
Save
|
||||
</sts-button>
|
||||
<sts-button
|
||||
${ref(this.#addToSceneButtonRef)}
|
||||
icon="plus-large"
|
||||
>
|
||||
Add to Scene
|
||||
<sts-icon
|
||||
class="menu-button-icon"
|
||||
icon="chevron-down"
|
||||
></sts-icon>
|
||||
</sts-button>
|
||||
` : nothing}
|
||||
<sts-button
|
||||
class=${classMap({ active: this.showHelp })}
|
||||
@@ -187,7 +191,13 @@ export class AppElement extends LitElement {
|
||||
|
||||
<!-- Studio-mode-only elements -->
|
||||
${this.#params.mode === StudioMode.Editor ? html`
|
||||
<sts-blocking></sts-blocking>
|
||||
<sts-add-to-scene-menu
|
||||
.trigger=${this.#addToSceneButtonRef}
|
||||
@open-media-library=${() => {
|
||||
this.#mediaLibraryRef.value?.open();
|
||||
}}
|
||||
></sts-add-to-scene-menu>
|
||||
<sts-media-library ${ref(this.#mediaLibraryRef)}></sts-media-library>
|
||||
|
||||
<sts-transform-toolbar class="v-start h-center"></sts-transform-toolbar>
|
||||
|
||||
@@ -276,5 +286,6 @@ export class AppElement extends LitElement {
|
||||
` : nothing}
|
||||
</sts-scene-data-provider>
|
||||
</sts-canvas-provider>
|
||||
</sts-overlay-provider>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
:host {
|
||||
display: block;
|
||||
min-width: 512px;
|
||||
min-height: 256px;
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
padding: 1rem;
|
||||
overflow: auto;
|
||||
|
||||
background: #0006;
|
||||
color: #FFFC;
|
||||
backdrop-filter: blur(16px);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { inject, on } from "@storyteller/framework";
|
||||
import {
|
||||
OVERLAY_PROVIDER,
|
||||
type OverlayHandle,
|
||||
type OverlayProvider,
|
||||
} from "@storyteller/studio-ui/overlay-provider";
|
||||
import {
|
||||
type MediaFile,
|
||||
MediaFileType,
|
||||
StorytellerApi,
|
||||
} from "@storyteller/studio-web";
|
||||
import { LitElement, unsafeCSS, type PropertyValues, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
import { until } from "lit/directives/until.js";
|
||||
|
||||
import dialogStyles from "./media-library.element.scss?inline";
|
||||
|
||||
@customElement("sts-media-library")
|
||||
export class MediaLibraryElement extends LitElement {
|
||||
@property({ attribute: false })
|
||||
trigger = createRef<HTMLElement>();
|
||||
|
||||
@inject(OVERLAY_PROVIDER)
|
||||
overlay!: OverlayProvider;
|
||||
|
||||
#overlayHandle?: WeakRef<OverlayHandle>;
|
||||
#dialogRef = createRef<MediaLibraryDialogElement>();
|
||||
|
||||
open(): void {
|
||||
this.#overlayHandle = this.overlay.attach(this, html`
|
||||
<sts-media-library-dialog
|
||||
${ref(this.#dialogRef)}
|
||||
@close=${() => this.close()}
|
||||
></sts-media-library-dialog>
|
||||
`, {
|
||||
containerStyles: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this.#overlayHandle)
|
||||
this.overlay.detach(this.#overlayHandle);
|
||||
}
|
||||
|
||||
protected override updated(changes: PropertyValues<this>): void {
|
||||
if (changes.has("trigger")) {
|
||||
const prevTrigger = changes.get("trigger");
|
||||
if (prevTrigger?.value) {
|
||||
prevTrigger.value.removeEventListener("click", this.onTriggerClick);
|
||||
}
|
||||
|
||||
const trigger = this.trigger.value;
|
||||
if (!trigger) return;
|
||||
|
||||
trigger.addEventListener("click", this.onTriggerClick);
|
||||
}
|
||||
|
||||
super.updated(changes);
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
this.trigger.value?.removeEventListener("click", this.onTriggerClick);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
onTriggerClick = (): void => {
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("sts-media-library-dialog")
|
||||
export class MediaLibraryDialogElement extends LitElement {
|
||||
static override styles = unsafeCSS(dialogStyles);
|
||||
|
||||
@property({ reflect: true })
|
||||
override role = "dialog";
|
||||
|
||||
@state()
|
||||
mediaFiles?: Promise<MediaFile[]>;
|
||||
|
||||
@inject(StorytellerApi)
|
||||
_api!: StorytellerApi;
|
||||
|
||||
override async connectedCallback(): Promise<void> {
|
||||
this.mediaFiles = this._api
|
||||
.session()
|
||||
.then(session => {
|
||||
if (!session.logged_in || !session.user?.username)
|
||||
return Promise.reject(new Error("Not logged in"));
|
||||
|
||||
return this._api.listMediaFiles({
|
||||
username: session.user.username,
|
||||
filterMediaType: [
|
||||
MediaFileType.Bvh,
|
||||
MediaFileType.Glb,
|
||||
MediaFileType.Gltf,
|
||||
MediaFileType.ScnRon,
|
||||
],
|
||||
});
|
||||
})
|
||||
.then(result => Promise.all(
|
||||
result.results.map(({ token }) => this._api.mediaFile(token))
|
||||
))
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
return [];
|
||||
});
|
||||
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
@on("window:click")
|
||||
onWindowClick(event: PointerEvent): void {
|
||||
if (!event.composedPath().includes(this))
|
||||
this.dispatchEvent(new CustomEvent("close"));
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
${until(
|
||||
this.mediaFiles?.then(files => html`
|
||||
<pre>${JSON.stringify(files, null, " ")}</pre>
|
||||
`),
|
||||
html`<span>Loading...</span>`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
"moduleResolution": "Bundler",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M256 0C201.996 0 151.917 10.8937 114.452 29.6264C78.4224 47.6412 47 76.9689 47 116.5C47 116.667 47.0006 116.834 47.0017 117H47V394.5C47 434.031 78.4224 463.359 114.452 481.374C151.917 500.106 201.996 511 256 511C310.004 511 360.083 500.106 397.548 481.374C433.578 463.359 465 434.031 465 394.5V117H464.998C464.999 116.834 465 116.667 465 116.5C465 76.9689 433.578 47.6412 397.548 29.6264C360.083 10.8937 310.004 0 256 0ZM95 116.5C95 104.945 104.991 88.0224 135.918 72.5589C165.41 57.8131 207.832 48 256 48C304.168 48 346.59 57.8131 376.082 72.5589C407.009 88.0224 417 104.945 417 116.5C417 128.055 407.009 144.978 376.082 160.441C346.59 175.187 304.168 185 256 185C207.832 185 165.41 175.187 135.918 160.441C104.991 144.978 95 128.055 95 116.5ZM417 192.155C410.79 196.281 404.249 200.023 397.548 203.374C360.083 222.106 310.004 233 256 233C201.996 233 151.917 222.106 114.452 203.374C107.751 200.023 101.21 196.281 95 192.155V394.5C95 406.055 104.991 422.978 135.918 438.441C165.41 453.187 207.832 463 256 463C304.168 463 346.59 453.187 376.082 438.441C407.009 422.978 417 406.055 417 394.5V192.155Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs><style>.fa-secondary{opacity:.4}</style></defs>
|
||||
<path class="fa-primary" fill-rule="evenodd" clip-rule="evenodd" d="M512 256C512 397.385 397.385 512 256 512C114.615 512 0 397.385 0 256C0 114.615 114.615 0 256 0C397.385 0 512 114.615 512 256ZM186.928 426.524C189.796 436.76 193.011 446.466 196.6 455.395C129.489 435.432 76.5679 382.511 56.6051 315.4C65.5341 318.989 75.24 322.204 85.4764 325.072C109.953 331.93 139.137 337.259 171.326 340.674C174.741 372.863 180.07 402.047 186.928 426.524ZM220.032 344.246C223.188 370.597 227.691 394.094 233.148 413.574C238.705 433.409 244.882 447.719 250.708 456.515C254.094 461.627 256.33 463.434 257.191 463.997C350.905 463.471 429.941 400.971 455.395 315.4C446.466 318.989 436.76 322.204 426.524 325.072C381.892 337.577 321.611 345 256 345C243.805 345 231.793 344.743 220.032 344.246ZM463.997 254.809C463.434 255.67 461.627 257.906 456.515 261.292C447.719 267.118 433.409 273.295 413.574 278.852C374.238 289.873 318.519 297 256 297C242.304 297 228.935 296.658 215.992 296.008C215.342 283.065 215 269.696 215 256C215 193.481 222.127 137.762 233.148 98.426C238.705 78.5909 244.882 64.2806 250.708 55.4854C254.094 50.3726 256.33 48.5664 257.191 48.0033C371.121 48.6421 463.358 140.879 463.997 254.809ZM48.0033 254.809C48.5664 255.67 50.3726 257.906 55.4854 261.292C64.2806 267.118 78.5909 273.295 98.426 278.852C117.906 284.31 141.403 288.812 167.754 291.968C167.256 280.207 167 268.195 167 256C167 190.389 174.423 130.108 186.928 85.4764C189.796 75.24 193.011 65.5341 196.6 56.6051C111.029 82.0589 48.5287 161.095 48.0033 254.809Z"/>
|
||||
<g class="fa-secondary">
|
||||
<path d="M315.4 56.6051C296.941 51.1141 277.407 48.1167 257.191 48.0034C256.924 48.1779 256.525 48.4721 256 48.9472C257.167 50.0045 258.956 51.958 261.292 55.4854C267.118 64.2806 273.295 78.591 278.852 98.4261C284.31 117.906 288.812 141.403 291.968 167.754C280.207 167.256 268.195 167 256 167C243.805 167 231.793 167.256 220.032 167.754C218.212 182.958 216.84 199.112 215.992 215.992C228.935 215.342 242.304 215 256 215C269.696 215 283.065 215.342 296.008 215.992C296.658 228.935 297 242.304 297 256C297 269.696 296.658 283.065 296.008 296.008C312.888 295.16 329.042 293.788 344.246 291.968C344.744 280.207 345 268.195 345 256C345 243.805 344.744 231.793 344.246 220.032C370.597 223.188 394.094 227.691 413.574 233.148C433.409 238.705 447.719 244.882 456.515 250.708C460.042 253.044 461.996 254.833 463.053 256C463.528 255.475 463.822 255.076 463.997 254.809C463.883 234.593 460.886 215.059 455.395 196.6C446.466 193.011 436.76 189.796 426.524 186.928C402.047 180.07 372.863 174.741 340.674 171.326C337.259 139.137 331.93 109.953 325.072 85.4764C322.204 75.24 318.989 65.5341 315.4 56.6051Z"/>
|
||||
<path d="M340.674 340.674C325.08 342.329 308.779 343.534 291.968 344.246C288.812 370.597 284.31 394.094 278.852 413.574C273.295 433.409 267.118 447.719 261.292 456.515C258.956 460.042 257.167 461.996 256 463.053C256.525 463.528 256.924 463.822 257.191 463.997C277.407 463.883 296.941 460.886 315.4 455.395C318.989 446.466 322.204 436.76 325.072 426.524C331.93 402.047 337.259 372.863 340.674 340.674Z"/>
|
||||
<path d="M167.754 220.032C168.466 203.221 169.671 186.921 171.326 171.326C139.137 174.741 109.953 180.07 85.4764 186.928C75.24 189.796 65.5341 193.011 56.6051 196.6C51.1141 215.059 48.1167 234.593 48.0034 254.809C48.1779 255.076 48.4721 255.475 48.9472 256C50.0045 254.833 51.958 253.044 55.4854 250.708C64.2806 244.882 78.591 238.705 98.4261 233.148C117.906 227.691 141.403 223.188 167.754 220.032Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M414.075 374.952C451.433 350.649 464 324.197 464 306V206C464 187.706 451.322 160.985 413.919 136.446C377.543 112.58 322.985 95 256 95C189.015 95 134.457 112.58 98.0807 136.446C60.6776 160.985 48 187.706 48 206V306C48 324.197 60.5666 350.649 97.9251 374.952C134.288 398.608 188.89 416 256 416C323.11 416 377.712 398.608 414.075 374.952ZM256 464C406 464 512 386.5 512 306V206C512 125.5 406 47 256 47C106 47 0 125.5 0 206V306C0 386.5 106 464 256 464Z"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M322.721 287.56C303.166 297.338 279.509 302 256 302C232.491 302 208.834 297.338 189.279 287.56C172.595 279.218 144 258.812 144 222C144 185.188 172.595 164.782 189.279 156.44C208.834 146.662 232.491 142 256 142C279.509 142 303.166 146.662 322.721 156.44C339.405 164.782 368 185.188 368 222C368 258.812 339.405 279.218 322.721 287.56ZM256 254C291.346 254 320 239.673 320 222C320 204.327 291.346 190 256 190C220.654 190 192 204.327 192 222C192 239.673 220.654 254 256 254Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,5 +1,7 @@
|
||||
export * from "./lib/button.element";
|
||||
export * from "./lib/icon.element";
|
||||
export * from "./lib/menu.element";
|
||||
export * from "./lib/overlay-provider.element";
|
||||
export * from "./lib/progress.element";
|
||||
export * from "./lib/toolbar.element";
|
||||
export * from "./lib/tree.element";
|
||||
|
||||
@@ -10,10 +10,12 @@ import "./icon.element";
|
||||
export class ButtonElement extends LitElement {
|
||||
static override styles = unsafeCSS(styles);
|
||||
|
||||
@property({ reflect: true })
|
||||
override role = "button";
|
||||
|
||||
@property() icon?: string;
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
|
||||
override role = "button";
|
||||
override tabIndex = 0;
|
||||
|
||||
protected override willUpdate(changes: PropertyValues<this>): void {
|
||||
@@ -35,9 +37,13 @@ export class ButtonElement extends LitElement {
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
${this.icon
|
||||
? html`<sts-icon icon=${this.icon} aria-hidden="true"></sts-icon>`
|
||||
: nothing}
|
||||
${this.icon ? html`
|
||||
<sts-icon
|
||||
part="icon"
|
||||
icon=${this.icon}
|
||||
aria-hidden="true"
|
||||
></sts-icon>
|
||||
` : nothing}
|
||||
<slot></slot>
|
||||
`
|
||||
}
|
||||
|
||||
@@ -31,6 +31,14 @@ import fasCircleXmark from "@fortawesome/fontawesome-pro/svgs/solid/circle-xmark
|
||||
import fasMinus from "@fortawesome/fontawesome-pro/svgs/solid/minus.svg?raw";
|
||||
import fasListTree from "@fortawesome/fontawesome-pro/svgs/solid/list-tree.svg?raw";
|
||||
import fasBars from "@fortawesome/fontawesome-pro/svgs/solid/bars.svg?raw";
|
||||
import fasPlusLarge from "@fortawesome/fontawesome-pro/svgs/solid/plus-large.svg?raw";
|
||||
import farFileImport from "@fortawesome/fontawesome-pro/svgs/regular/file-import.svg?raw";
|
||||
import farPhotoFilmMusic from "@fortawesome/fontawesome-pro/svgs/regular/photo-film-music.svg?raw";
|
||||
import fasChevronDown from "@fortawesome/fontawesome-pro/svgs/solid/chevron-down.svg?raw";
|
||||
|
||||
import customCylinder from "../assets/icons/cylinder.svg?raw";
|
||||
import customSphere from "../assets/icons/sphere.svg?raw";
|
||||
import customTorus from "../assets/icons/torus.svg?raw";
|
||||
|
||||
import styles from "./icon.element.scss?inline";
|
||||
|
||||
@@ -61,13 +69,16 @@ function renderSvg(iconId: string) {
|
||||
"bone": () => farBone,
|
||||
"bwd-fast": () => fasBwdFast,
|
||||
"bwd-step": () => fasBwdStep,
|
||||
"caret-right": () => fasCaretRight,
|
||||
"check": () => fasCheck,
|
||||
"chevron-down": () => fasChevronDown,
|
||||
"circle-check": () => fasCircleCheck,
|
||||
"circle-question": () => fasCircleQuestion,
|
||||
"circle-xmark": () => fasCircleXmark,
|
||||
"caret-right": () => fasCaretRight,
|
||||
"crosshairs": () => farCrosshairs,
|
||||
"cube": () => farCube,
|
||||
"cylinder": () => customCylinder,
|
||||
"file-import": () => farFileImport,
|
||||
"floppy-disk": () => fasFloppyDisk,
|
||||
"fwd-fast": () => fasFwdFast,
|
||||
"fwd-step": () => fasFwdStep,
|
||||
@@ -76,11 +87,15 @@ function renderSvg(iconId: string) {
|
||||
"list-tree": () => fasListTree,
|
||||
"minus": () => fasMinus,
|
||||
"globe": () => farGlobe,
|
||||
"photo-film-music": () => farPhotoFilmMusic,
|
||||
"play": () => fasPlay,
|
||||
"plus-large": () => fasPlusLarge,
|
||||
"question": () => fasQuestion,
|
||||
"rotate": () => farRotate,
|
||||
"scale": () => farScale,
|
||||
"skeleton": () => farSkeleton,
|
||||
"sphere": () => customSphere,
|
||||
"torus": () => customTorus,
|
||||
"up-down-left-right": () => farUpDownLeftRight,
|
||||
"video": () => farVideo,
|
||||
"xmark": () => fasXmark,
|
||||
@@ -89,7 +104,7 @@ function renderSvg(iconId: string) {
|
||||
|
||||
if (!svgSrc) return nothing;
|
||||
|
||||
const [, attrsString, body] = svgSrc.match(/<svg(.+?)>(.+?)<\/svg>/)!;
|
||||
const [, attrsString, body] = svgSrc.match(/<svg(.+?)>(.+?)<\/svg>/s)!;
|
||||
const attrs = Object.fromEntries(
|
||||
Array.from(attrsString.matchAll(/([-:_a-zA-Z0-9]+)(?:=['"]?([^'"]*)['"]?)/g))
|
||||
.map(([, attr, value]) => [attr, value])
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 0;
|
||||
min-width: 220px;
|
||||
|
||||
background: #0006;
|
||||
backdrop-filter: blur(16px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::slotted(sts-button) {
|
||||
background: unset;
|
||||
backdrop-filter: unset;
|
||||
border-radius: unset;
|
||||
}
|
||||
|
||||
::slotted(sts-button:hover) {
|
||||
background: #0004;
|
||||
|
||||
// FIXME: https://github.com/w3c/csswg-drafts/issues/3896
|
||||
&::part(icon) {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
::slotted(hr) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: none;
|
||||
height: 1px;
|
||||
margin: 0.5rem 0;
|
||||
background: #FFF1;
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { inject, on } from "@storyteller/framework";
|
||||
import { LitElement, PropertyValues, css, html, unsafeCSS } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
import { styleMap } from "lit/directives/style-map.js";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import panelStyles from "./menu.element.scss?inline";
|
||||
|
||||
import {
|
||||
OVERLAY_PROVIDER,
|
||||
type OverlayHandle,
|
||||
type OverlayProvider,
|
||||
} from "./overlay-provider.element";
|
||||
|
||||
@customElement("sts-menu")
|
||||
export class MenuElement extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
visibility: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ attribute: false })
|
||||
trigger = createRef<HTMLElement>();
|
||||
|
||||
/** Close the menu when one of its menuitems is pressed */
|
||||
@property({ type: Boolean })
|
||||
autoClose = false;
|
||||
|
||||
@inject(OVERLAY_PROVIDER)
|
||||
overlay!: OverlayProvider;
|
||||
|
||||
#menuId = uuid();
|
||||
#slotRef = createRef<HTMLSlotElement>();
|
||||
#panelRef = createRef<MenuPanelElement>();
|
||||
#handle?: WeakRef<OverlayHandle>;
|
||||
|
||||
show(): void {
|
||||
const trigger = this.trigger.value;
|
||||
if (!trigger) return;
|
||||
|
||||
trigger.classList.toggle("active", true);
|
||||
trigger.ariaExpanded = "true";
|
||||
|
||||
const rect = trigger.getBoundingClientRect();
|
||||
|
||||
this.#handle = this.overlay.attach(this, html`
|
||||
<sts-menu-panel
|
||||
${ref(this.#panelRef)}
|
||||
id=${this.#menuId}
|
||||
class=${ifDefined(this.className)}
|
||||
style=${styleMap({
|
||||
position: "absolute",
|
||||
top: `${rect.bottom + 4}px`,
|
||||
left: `${rect.left}px`,
|
||||
})}
|
||||
?autoClose=${this.autoClose}
|
||||
@hide=${this.hide}
|
||||
>${
|
||||
// Move the nodes assigned to the <sts-menu>'s slot into the
|
||||
// <sts-menu-panel>'s slot
|
||||
this.#slotRef.value?.assignedNodes()
|
||||
}</sts-menu-panel>
|
||||
`);
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
const trigger = this.trigger.value;
|
||||
if (trigger) {
|
||||
trigger.classList.toggle("active", false);
|
||||
trigger.ariaExpanded = "false";
|
||||
}
|
||||
|
||||
if (this.#panelRef.value) {
|
||||
// Move the nodes assigned to the <sts-menu-panel>'s slot back into the
|
||||
// <sts-menu>'s slot
|
||||
for (let node of this.#panelRef.value.getSlot()!.assignedNodes())
|
||||
this.appendChild(node);
|
||||
}
|
||||
|
||||
if (this.#handle)
|
||||
this.overlay.detach(this.#handle);
|
||||
|
||||
this.#handle = undefined;
|
||||
}
|
||||
|
||||
protected override update(changes: PropertyValues<this>): void {
|
||||
super.update(changes);
|
||||
|
||||
if (this.#handle)
|
||||
this.overlay.reRender(this.#handle);
|
||||
}
|
||||
|
||||
protected override updated(changes: PropertyValues<this>): void {
|
||||
if (changes.has("trigger")) {
|
||||
const prevTrigger = changes.get("trigger");
|
||||
if (prevTrigger?.value) {
|
||||
prevTrigger.value.removeEventListener("click", this.#onTriggerClick);
|
||||
}
|
||||
|
||||
const trigger = this.trigger.value;
|
||||
if (!trigger) return;
|
||||
|
||||
trigger.setAttribute("aria-owns", this.#menuId);
|
||||
trigger.ariaHasPopup = "true";
|
||||
trigger.ariaExpanded = "false";
|
||||
|
||||
trigger.addEventListener("click", this.#onTriggerClick);
|
||||
}
|
||||
|
||||
super.updated(changes);
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
this.trigger.value?.removeEventListener("click", this.#onTriggerClick);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
#onTriggerClick = (event: Event): void => {
|
||||
event.stopPropagation();
|
||||
this.show();
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
<slot ${ref(this.#slotRef)}></slot>
|
||||
`
|
||||
}
|
||||
|
||||
// TODO: ARIA keyboard navigation: https://www.w3.org/WAI/ARIA/apg/patterns/menubar/
|
||||
@customElement("sts-menu-panel")
|
||||
export class MenuPanelElement extends LitElement {
|
||||
static override styles = unsafeCSS(panelStyles);
|
||||
|
||||
@property({ reflect: true })
|
||||
override role = "menu";
|
||||
|
||||
@property({ type: Boolean })
|
||||
autoClose = false;
|
||||
|
||||
#slotRef = createRef<HTMLSlotElement>();
|
||||
getSlot(): HTMLSlotElement | undefined { return this.#slotRef.value; }
|
||||
|
||||
@on("click")
|
||||
onClick(event: PointerEvent): void {
|
||||
event.stopPropagation();
|
||||
|
||||
if (
|
||||
this.autoClose
|
||||
&& event.target
|
||||
&& event.target instanceof HTMLElement
|
||||
&& event.target.role === "menuitem"
|
||||
) {
|
||||
this.dispatchEvent(new CustomEvent("hide"));
|
||||
}
|
||||
}
|
||||
|
||||
@on("window:click")
|
||||
onWindowClick(event: PointerEvent): void {
|
||||
if (!event.composedPath().includes(this))
|
||||
this.dispatchEvent(new CustomEvent("hide"));
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
<slot ${ref(this.#slotRef)}></slot>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { UniqueToken, provide } from "@storyteller/framework";
|
||||
import { LitElement, TemplateResult, html, render } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { Ref, createRef, ref } from "lit/directives/ref.js";
|
||||
import { StyleInfo, styleMap } from "lit/directives/style-map.js";
|
||||
|
||||
export const OVERLAY_PROVIDER = UniqueToken.create<OverlayProvider>();
|
||||
|
||||
// TODO: Document this
|
||||
export interface OverlayProvider {
|
||||
attach(
|
||||
host: LitElement,
|
||||
template: TemplateResult,
|
||||
options?: AttachOptions,
|
||||
): WeakRef<OverlayHandle>;
|
||||
detach(handle: WeakRef<OverlayHandle>): void;
|
||||
reRender(handle: WeakRef<OverlayHandle>): void;
|
||||
}
|
||||
|
||||
export interface AttachOptions {
|
||||
containerStyles?: Readonly<StyleInfo>;
|
||||
}
|
||||
|
||||
// TODO: Document this
|
||||
export interface OverlayHandle {
|
||||
readonly id: number;
|
||||
}
|
||||
|
||||
class OverlayHandleInternal implements OverlayHandle {
|
||||
#id: number;
|
||||
get id() { return this.#id; }
|
||||
|
||||
constructor (id: number) {
|
||||
this.#id = id;
|
||||
}
|
||||
_updateId(id: number): void {
|
||||
this.#id = id;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("sts-overlay-provider")
|
||||
@provide(OVERLAY_PROVIDER)
|
||||
export class OverlayProviderElement
|
||||
extends LitElement
|
||||
implements OverlayProvider
|
||||
{
|
||||
#handles: OverlayHandleInternal[] = [];
|
||||
#containerRefs: Ref<HTMLElement>[] = [];
|
||||
#containerTemplates: TemplateResult[] = [];
|
||||
#templates: [LitElement, TemplateResult][] = [];
|
||||
|
||||
attach(
|
||||
host: LitElement,
|
||||
template: TemplateResult,
|
||||
options: AttachOptions = {},
|
||||
): WeakRef<OverlayHandle> {
|
||||
const index = this.#templates.length;
|
||||
const handle = new OverlayHandleInternal(index);
|
||||
|
||||
const containerStyles = options.containerStyles ?? {};
|
||||
|
||||
this.#handles.push(handle);
|
||||
this.#containerRefs.push(createRef());
|
||||
this.#containerTemplates.push(html`
|
||||
<div
|
||||
${ref(this.#containerRefs[index])}
|
||||
slot="overlay"
|
||||
style=${styleMap({
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
...containerStyles,
|
||||
})}
|
||||
></div>
|
||||
`);
|
||||
this.#templates.push([host, template]);
|
||||
|
||||
this.#updateLightDom();
|
||||
|
||||
return new WeakRef(handle);
|
||||
}
|
||||
|
||||
detach(handle: WeakRef<OverlayHandle>): void {
|
||||
const index = handle.deref()?.id;
|
||||
if (index == null) return;
|
||||
|
||||
this.#handles.splice(index, 1);
|
||||
this.#containerRefs.splice(index, 1);
|
||||
this.#containerTemplates.splice(index, 1);
|
||||
this.#templates.splice(index, 1);
|
||||
|
||||
for (let i = index; i < this.#handles.length; ++i)
|
||||
this.#handles[i]._updateId(i);
|
||||
|
||||
this.#updateLightDom();
|
||||
}
|
||||
|
||||
reRender(handle: WeakRef<OverlayHandle>): void {
|
||||
const index = handle.deref()?.id;
|
||||
if (index == null) return;
|
||||
|
||||
const container = this.#containerRefs[index]?.value;
|
||||
if (!container) return;
|
||||
|
||||
const tuple = this.#templates[index];
|
||||
if (!tuple) return;
|
||||
|
||||
const [host, template] = tuple;
|
||||
render(template, container, { host });
|
||||
}
|
||||
|
||||
#updateLightDom(): void {
|
||||
render(this.#containerTemplates, this, { host: this });
|
||||
|
||||
for (let i = 0; i < this.#handles.length; ++i) {
|
||||
const container = this.#containerRefs[i].value;
|
||||
if (!container) throw new Error("Attempted to retrieve an invalid container ref!");
|
||||
|
||||
const [host, template] = this.#templates[i];
|
||||
render(template, container, { host });
|
||||
}
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
<slot></slot>
|
||||
<slot name="overlay"></slot>
|
||||
`;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./lib/add-to-scene-menu.element";
|
||||
export * from "./lib/canvas-provider.element";
|
||||
export * from "./lib/iframe-messaging-bridge.element";
|
||||
export * from "./lib/inspector/entity-inspector.element";
|
||||
@@ -5,6 +6,7 @@ export * from "./lib/inspector/inspector.types";
|
||||
export * from "./lib/loading.element";
|
||||
export * from "./lib/scene-data-provider.element";
|
||||
export * from "./lib/scene-hierarchy.element";
|
||||
export * from "./lib/storyteller-api";
|
||||
export * from "./lib/studio.element";
|
||||
export * from "./lib/transform-toolbar.element";
|
||||
export * from "./lib/url-params";
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as studio from "@storyteller/studio";
|
||||
import { BlockingCommand } from "@storyteller/studio";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { createRef } from "lit/directives/ref.js"
|
||||
|
||||
import "@storyteller/studio-ui/button";
|
||||
import "@storyteller/studio-ui/menu";
|
||||
|
||||
@customElement("sts-add-to-scene-menu")
|
||||
export class AddToSceneMenuElement extends LitElement {
|
||||
@property({ attribute: false })
|
||||
trigger = createRef<HTMLElement>();
|
||||
|
||||
protected override render = () => html`
|
||||
<sts-menu autoClose .trigger=${this.trigger}>
|
||||
<sts-button
|
||||
role="menuitem"
|
||||
icon="cube"
|
||||
@click=${() => {
|
||||
studio.dispatchBlockingCommand(BlockingCommand.Cube);
|
||||
}}
|
||||
>
|
||||
Cube
|
||||
</sts-button>
|
||||
<sts-button
|
||||
role="menuitem"
|
||||
icon="sphere"
|
||||
@click=${() => {
|
||||
studio.dispatchBlockingCommand(BlockingCommand.Sphere);
|
||||
}}
|
||||
>
|
||||
Sphere
|
||||
</sts-button>
|
||||
<sts-button
|
||||
role="menuitem"
|
||||
icon="torus"
|
||||
@click=${() => {
|
||||
studio.dispatchBlockingCommand(BlockingCommand.Torus);
|
||||
}}
|
||||
>
|
||||
Torus
|
||||
</sts-button>
|
||||
<sts-button
|
||||
role="menuitem"
|
||||
icon="cylinder"
|
||||
@click=${() => {
|
||||
studio.dispatchBlockingCommand(BlockingCommand.Cylinder);
|
||||
}}
|
||||
>
|
||||
Cylinder
|
||||
</sts-button>
|
||||
<hr />
|
||||
<sts-button
|
||||
role="menuitem"
|
||||
icon="file-import"
|
||||
>
|
||||
Import asset...
|
||||
</sts-button>
|
||||
<sts-button
|
||||
role="menuitem"
|
||||
icon="photo-film-music"
|
||||
@click=${() => {
|
||||
this.dispatchEvent(new CustomEvent("open-media-library"));
|
||||
}}
|
||||
>
|
||||
Media library...
|
||||
</sts-button>
|
||||
</sts-menu>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
import { match } from "@storyteller/utility";
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
export class StorytellerApi {
|
||||
#api = axios.create({
|
||||
baseURL: "https://api.fakeyou.com",
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
async createScene(file: File): Promise<MediaFileToken> {
|
||||
const response = await this.#api.postForm<MediaUploadResponse>(
|
||||
"v1/engine/create_scene",
|
||||
{
|
||||
uuid_idempotency_token: uuid(),
|
||||
file,
|
||||
}, {
|
||||
headers: { Accept: "text/plain" },
|
||||
},
|
||||
);
|
||||
this.#validateResponse(response);
|
||||
|
||||
return response.data.media_file_token;
|
||||
}
|
||||
|
||||
async session(): Promise<Session> {
|
||||
const response = await this.#api.get<Session>("v1/session");
|
||||
this.#validateResponse(response);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async listMediaFiles(options: ListMediaFilesOptions = {}): Promise<ListMediaFilesResult> {
|
||||
const params = {} as any;
|
||||
|
||||
if (options.filterMediaType)
|
||||
params.filter_media_type = options.filterMediaType.join(",");
|
||||
if (options.sortAscending != null)
|
||||
params.soer_ascending = options.sortAscending;
|
||||
if (options.pageSize != null)
|
||||
params.page_size = options.pageSize;
|
||||
if (options.pageIndex != null)
|
||||
params.page_index = options.pageIndex;
|
||||
|
||||
const endpoint = options.username
|
||||
? `v1/media_files/list/user/${options.username}`
|
||||
: "v1/media_files/list";
|
||||
|
||||
const response = await this.#api.get<ListMediaFilesResult>(endpoint, { params });
|
||||
this.#validateResponse(response);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async uploadMediaFile(file: File, subtype: MediaFileSubtype): Promise<MediaFileToken> {
|
||||
const [, _basename, extension] = file.name.match(/(.+?)\.(.+)/) ?? [];
|
||||
const mimeType = match (extension, {
|
||||
"fbx": () => "application/fbx",
|
||||
"gltf": () => "model/gltf+json",
|
||||
"glb": () => "model/gltf-binary",
|
||||
"scn.ron": () => "text/ron",
|
||||
_: () => "application/octet-stream",
|
||||
});
|
||||
|
||||
const response = await this.#api.postForm<MediaUploadResponse>(
|
||||
"v1/media_files/upload/engine_asset",
|
||||
{
|
||||
uuid_idempotency_token: uuid(),
|
||||
file,
|
||||
media_file_subtype: subtype,
|
||||
},
|
||||
{
|
||||
headers: { Accept: mimeType },
|
||||
},
|
||||
);
|
||||
this.#validateResponse(response);
|
||||
|
||||
return response.data.media_file_token;
|
||||
}
|
||||
|
||||
async mediaFile(token: MediaFileToken): Promise<MediaFile> {
|
||||
const response = await this.#api.get<MediaFileResult>(`v1/media_files/file/${token}`);
|
||||
this.#validateResponse(response);
|
||||
|
||||
return response.data.media_file;
|
||||
}
|
||||
|
||||
#validateResponse<R extends AxiosResponse>(response: R): asserts response is OkResponse<R> {
|
||||
if (response.status !== 200)
|
||||
throw new Error(`API responded with status ${response.status}: "${response.statusText}"`);
|
||||
|
||||
if (
|
||||
response.data
|
||||
&& "success" in response.data
|
||||
&& response.data.success === false
|
||||
) {
|
||||
throw new ApiError(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiError<T extends AxiosResponse = AxiosResponse> extends Error {
|
||||
detail: T;
|
||||
|
||||
constructor (detail: T) {
|
||||
super("API Error");
|
||||
this.detail = detail;
|
||||
}
|
||||
}
|
||||
|
||||
type OkResponse<T extends AxiosResponse> = T & { status: 200 }
|
||||
|
||||
export interface ListMediaFilesOptions {
|
||||
username?: string;
|
||||
filterMediaType?: MediaFileType[];
|
||||
pageSize?: number;
|
||||
pageIndex?: number;
|
||||
sortAscending?: boolean;
|
||||
}
|
||||
|
||||
export interface ListMediaFilesResult {
|
||||
pagination: PaginationPage;
|
||||
results: MediaFileForUserListItem[];
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface MediaUploadResponse {
|
||||
success: boolean;
|
||||
media_file_token: MediaFileToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination by page id.
|
||||
*
|
||||
* This type of pagination is by "page id" and "page count". This should never
|
||||
* be used to walk the entire public database as competitors and investors can
|
||||
* use it to glean information about the scale of our service. This is best used
|
||||
* for user profiles and scoped down results.
|
||||
*/
|
||||
export interface PaginationPage {
|
||||
current: number;
|
||||
total_page_count: number;
|
||||
}
|
||||
|
||||
export interface MediaFileResult {
|
||||
success: boolean;
|
||||
media_file: MediaFile;
|
||||
}
|
||||
|
||||
export interface MediaFile {
|
||||
token: MediaFileToken;
|
||||
media_type: MediaFileType;
|
||||
maybe_media_subtype?: MediaFileSubtype;
|
||||
maybe_batch_token?: unknown;
|
||||
public_bucket_path: string;
|
||||
maybe_model_weight_info?: unknown;
|
||||
maybe_creator_user?: UserDetailsLight;
|
||||
creator_set_visibility: Visibility;
|
||||
maybe_text_transcript?: string;
|
||||
maybe_prompt_token?: unknown;
|
||||
maybe_original_filename?: string;
|
||||
is_emulated_media_file: boolean;
|
||||
stats: Stats;
|
||||
/** Date-time string */
|
||||
created_at: string;
|
||||
/** Date-time string */
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface MediaFileListItem {
|
||||
/** Date-time string */
|
||||
created_at: string;
|
||||
creator_set_visibility: Visibility;
|
||||
maybe_creator?: UserDetailsLight;
|
||||
/** @deprecated */
|
||||
maybe_origin_model_token?: string;
|
||||
maybe_origin_model_type?: MediaFileOriginModelType;
|
||||
/** Text transcripts for TTS, etc. */
|
||||
maybe_text_transcript?: string;
|
||||
media_type: MediaFileType;
|
||||
maybe_media_subtype?: MediaFileSubtype;
|
||||
origin: MediaFileOriginDetails;
|
||||
origin_category: MediaFileOriginCategory;
|
||||
origin_product_category: MediaFileOriginProductCategory;
|
||||
/** URL to the media file. */
|
||||
public_bucket_path: string;
|
||||
/** The primary key for Media Files */
|
||||
token: MediaFileToken;
|
||||
/** Date-time string */
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Stats {
|
||||
positive_rating_count: number;
|
||||
bookmark_count: number;
|
||||
}
|
||||
|
||||
export type MediaFileForUserListItem = Omit<MediaFileListItem, "maybe_creator">;
|
||||
|
||||
export enum Visibility {
|
||||
Public = "public",
|
||||
Hidden = "hidden",
|
||||
Private = "private",
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in the `media_files` table in a `VARCHAR` field.
|
||||
*
|
||||
* DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY.
|
||||
*/
|
||||
export enum MediaFileOriginModelType {
|
||||
RvcV2 = "rvc_v2",
|
||||
SadTalker = "sad_talker",
|
||||
SoVitsSvc = "so_vits_svc",
|
||||
Tacotron2 = "tacotron2",
|
||||
VallEX = "vall_e_x",
|
||||
Rerender = "rerender",
|
||||
MocapNet = "mocap_net",
|
||||
ComfyUi = "comfy_ui",
|
||||
StyleTts2 = "styletts2",
|
||||
StableDiffusion_1_5 = "stable_diffusion_1_5",
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in the `media_files` table in a `VARCHAR(16)` field.
|
||||
*
|
||||
* DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY.
|
||||
*/
|
||||
export enum MediaFileType {
|
||||
Audio = "audio",
|
||||
Image = "image",
|
||||
Video = "video",
|
||||
Mocap = "mocap",
|
||||
Bvh = "bvh",
|
||||
Fbx = "fbx",
|
||||
Glb = "glb",
|
||||
Gltf = "gltf",
|
||||
ScnRon = "scene_ron",
|
||||
}
|
||||
|
||||
export enum MediaFileSubtype {
|
||||
/**
|
||||
* Animation file from Mixamo.
|
||||
* Primarily used for FBX and GLB.
|
||||
*/
|
||||
Mixamo = "mixamo",
|
||||
/**
|
||||
* Animation file from MocapNet.
|
||||
* Primarily used for BVH.
|
||||
*/
|
||||
MocapNet = "mocap_net",
|
||||
/**
|
||||
* Generic animation case.
|
||||
* Used for BVH files, but can also pertain to animation-only files of other
|
||||
* types.
|
||||
*/
|
||||
AnimationOnly = "animation_only",
|
||||
/**
|
||||
* @deprecated Use `SceneImport` instead.
|
||||
*/
|
||||
Scene = "scene",
|
||||
/**
|
||||
* Generic 3D scene file.
|
||||
* Can pertain to glTF, glB, FBX, etc.
|
||||
*/
|
||||
SceneImport = "scene_import",
|
||||
/**
|
||||
* Native Storyteller scene format.
|
||||
* Typically stored in a `.scn.ron` file.
|
||||
*/
|
||||
StorytellerScene = "storyteller_scene",
|
||||
}
|
||||
|
||||
/** Fields useful for enriching media file listings */
|
||||
export interface MediaFileOriginDetails {
|
||||
maybe_model?: Model;
|
||||
origin_category: MediaFileOriginCategory;
|
||||
product_category: MediaFileOriginProductCategory;
|
||||
}
|
||||
|
||||
export interface Model {
|
||||
model_type: MediaFileOriginModelType;
|
||||
/**
|
||||
* The model title (typically only populated for model_weights models, not
|
||||
* legacy tables such as tts_models.)
|
||||
*/
|
||||
title?: string;
|
||||
/** The primary key for the "model_weights" table. */
|
||||
token?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in the `media_files` table in a `VARCHAR` field.
|
||||
*
|
||||
* DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY.
|
||||
*/
|
||||
export enum MediaFileOriginCategory {
|
||||
Inference = "inference",
|
||||
Processed = "processed",
|
||||
Upload = "upload",
|
||||
DeviceApi = "device_api",
|
||||
StoryEngine = "story_engine",
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in the `media_files` table in `VARCHAR(16)` field
|
||||
* `origin_product_category`.
|
||||
*
|
||||
* This value indicates what product originally created the media file. (Not the
|
||||
* ML model or user upload process.) This will let us scope media files to the
|
||||
* product that generated them and filter them out of unrelated products if
|
||||
* necessary (eg. a user probably doesn't want "Voice Designer" dataset samples
|
||||
* in a video generation flow.)
|
||||
*
|
||||
* DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY.
|
||||
*/
|
||||
export enum MediaFileOriginProductCategory {
|
||||
Unknown = "unknown",
|
||||
FaceAnimator = "face_animator",
|
||||
Tts = "tts",
|
||||
VoiceConversion = "voice_conversion",
|
||||
ZsVoice = "zs_voice",
|
||||
VideoFilter = "video_filter",
|
||||
Mocap = "mocap",
|
||||
ImageGen = "image_gen",
|
||||
Workflow = "workflow",
|
||||
}
|
||||
|
||||
export type MediaFileToken = string;
|
||||
|
||||
export interface Session {
|
||||
logged_in: boolean;
|
||||
success: boolean;
|
||||
user?: {
|
||||
can_access_studio: boolean;
|
||||
can_approve_w21_templates: boolean;
|
||||
can_ban_users: boolean;
|
||||
can_delete_other_users_tts_models: boolean;
|
||||
can_delete_other_users_tts_results: boolean;
|
||||
can_delete_other_users_w2l_results: boolean;
|
||||
can_delete_other_users_w2l_templates: boolean;
|
||||
can_delete_own_account: boolean;
|
||||
can_delete_own_tts_models: boolean;
|
||||
can_delete_own_tts_results: boolean;
|
||||
can_delete_own_w2l_results: boolean;
|
||||
can_delete_own_w2l_templates: boolean;
|
||||
can_delete_users: boolean;
|
||||
can_edit_other_users_profiles: boolean;
|
||||
can_edit_other_users_tts_models: boolean;
|
||||
can_edit_other_users_w2l_templates: boolean;
|
||||
can_upload_tts_models: boolean;
|
||||
can_upload_w2l_templates: boolean;
|
||||
can_use_tts: boolean;
|
||||
can_use_w2l: boolean;
|
||||
core_info: UserDetailsLight;
|
||||
display_name: string;
|
||||
email_gravatar_hash: string;
|
||||
fakeyou_plan: FakeYouPlan;
|
||||
maybe_feature_flags: unknown;
|
||||
storyteller_stream_plan: StorytellerStreamPlan;
|
||||
user_token: string;
|
||||
username: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Everything we need to refer to a user on the public web interface.
|
||||
*/
|
||||
export interface UserDetailsLight {
|
||||
default_avatar: DefaultAvatarInfo;
|
||||
/**
|
||||
* As of 2023-08-23, this is the username with capitalization (In the future,
|
||||
* a display name can be customized by the user.)
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
gravatar_hash: string;
|
||||
/** The primary key for users. */
|
||||
user_token: string;
|
||||
/**
|
||||
* The unique username someone logs in with. As of 2023-08-23, this is always
|
||||
* lowercase.
|
||||
*/
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface DefaultAvatarInfo {
|
||||
color_index: number;
|
||||
image_index: number;
|
||||
}
|
||||
|
||||
export enum FakeYouPlan {
|
||||
Free = "free",
|
||||
Basic = "basic",
|
||||
Standard = "standard",
|
||||
Pro = "pro",
|
||||
}
|
||||
|
||||
export enum StorytellerStreamPlan {
|
||||
Free = "free",
|
||||
Basic = "basic",
|
||||
Standard = "standard",
|
||||
Pro = "pro",
|
||||
}
|
||||
+4
-1
@@ -10,7 +10,7 @@
|
||||
"importHelpers": true,
|
||||
"target": "es2015",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"lib": ["es2021", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
@@ -20,11 +20,14 @@
|
||||
"@storyteller/studio-ui": ["studio-ui/src/index.ts"],
|
||||
"@storyteller/studio-ui/button": ["studio-ui/src/lib/button.element.ts"],
|
||||
"@storyteller/studio-ui/icon": ["studio-ui/src/lib/icon.element.ts"],
|
||||
"@storyteller/studio-ui/menu": ["studio-ui/src/lib/menu.element.ts"],
|
||||
"@storyteller/studio-ui/overlay-provider": ["studio-ui/src/lib/overlay-provider.element.ts"],
|
||||
"@storyteller/studio-ui/progress": ["studio-ui/src/lib/progress.element.ts"],
|
||||
"@storyteller/studio-ui/toolbar": ["studio-ui/src/lib/toolbar.element.ts"],
|
||||
"@storyteller/studio-ui/tree": ["studio-ui/src/lib/tree.element.ts"],
|
||||
"@storyteller/studio-ui/forms/text-field": ["studio-ui/src/lib/forms/text-field.element.ts"],
|
||||
"@storyteller/studio-web": ["studio-web/src/index.ts"],
|
||||
"@storyteller/studio-web/add-to-scene-menu": ["studio-web/src/lib/add-to-scene-menu.element.ts"],
|
||||
"@storyteller/studio-web/canvas-provider": ["studio-web/src/lib/canvas-provider.element.ts"],
|
||||
"@storyteller/studio-web/entity-inspector": ["studio-web/src/lib/inspector/entity-inspector.element.ts"],
|
||||
"@storyteller/studio-web/iframe-messaging-bridge": ["studio-web/src/lib/iframe-messaging-bridge.element.ts"],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./lib/assert";
|
||||
export * from "./lib/exists";
|
||||
export * from "./lib/match";
|
||||
export * from "./lib/types";
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export function assert(expression: unknown, message?: string): asserts expression {
|
||||
if (!expression) {
|
||||
let err = "Assertion failed";
|
||||
if (message) err += `: ${message}`;
|
||||
|
||||
throw new Error(err);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user