import { drag, on } from "@storyteller/framework"; import * as studio from "@storyteller/studio"; import { type SceneSavedEvent, type SceneStateEvent, SceneState, StudioMode, } from "@storyteller/studio"; import { SceneHierarchyElement, StudioURLParams, } from "@storyteller/studio-web"; import { match } from "@storyteller/utility"; import { LitElement, html, nothing } from "lit"; 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 "@storyteller/studio-ui/button"; import "@storyteller/studio-ui/icon"; import "@storyteller/studio-ui/toolbar"; import "@storyteller/studio-ui/tree"; import "@storyteller/studio-web/canvas-provider"; import "@storyteller/studio-web/entity-inspector"; import "@storyteller/studio-web/iframe-messaging-bridge"; import "@storyteller/studio-web/loading"; import "@storyteller/studio-web/scene-hierarchy"; import "@storyteller/studio-web/studio"; import "@storyteller/studio-web/transform-toolbar"; import "./anim-editor.element"; import "./help.element"; import "./blocking.element"; import "./app.element.scss"; @customElement("app-root") export class AppElement extends LitElement { @state() showHelp = false; @state() showInspector = false; @state() loading = true; @state() resizingPanel = false; @state() inspectorWidth = 320; @state() hierarchyHeight?: number; #enableAnimTimeline = true; // TODO #inspectorRef = createRef(); #hierarchyRef = createRef(); #params = new StudioURLParams(); protected override createRenderRoot(): Element | ShadowRoot { return this; } @on("scene-saved") async onSceneSaved({ detail: scene }: SceneSavedEvent): Promise { 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, }); 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, })); } } } catch (err) { console.error(err); } } @on("scene-state") onSceneStateChange({ detail: state }: SceneStateEvent): void { console.log("scene state change:", SceneState[state]); this.loading = match (state, { [SceneState.Active]: () => false, _: () => true, }); } onPanelResizeStart(): void { this.resizingPanel = true; } onPanelResizeEnd(): void { this.resizingPanel = false; } resizeInspectorWidth(event: PointerEvent): void { const updated = this.inspectorWidth - event.movementX; this.inspectorWidth = Math.max(Math.min(updated, 1024), 128); } resizeHierarchyHeight(event: PointerEvent): void { let availableHeight = this.#inspectorRef.value?.clientHeight; if (availableHeight == null) return; // Account for: // - `section.inspector` padding (2rem ~= 32px) // - resize-handle (0.5rem ~= 8px) // - minimal space for `sts-entity-inspector` (2rem ~= 32px) availableHeight -= 72; const height = this.hierarchyHeight ?? this.#hierarchyRef.value?.offsetHeight; if (height == null) return; const updated = height + event.movementY; this.hierarchyHeight = Math.max(Math.min(updated, availableHeight), 8); } minimizeInspector(): void { this.showInspector = false; } openInspector(): void { this.showInspector = true; } toggleHelp(): void { this.showHelp = !this.showHelp; } protected override render = () => html` ${this.#params.mode === StudioMode.Editor ? html` studio.saveScene()} > Save ` : nothing} Help ${this.#params.mode === StudioMode.Editor ? html` ${this.showInspector ? html`

Inspector

` : html` `} ${this.#enableAnimTimeline ? html` ` : nothing} ` : nothing} ${this.showHelp ? html` ` : nothing} ${this.loading ? html` ` : nothing}
`; }