From afe6139f756adec5382f56efe0cb000a55772603 Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Fri, 12 Jan 2024 16:35:32 -0500 Subject: [PATCH] Refactor `studio-frontend` (#19) This PR does some rudimentary cleanup / reorganizing to the `studio-frontend` project. ### Changes * Refactored `studio-frontend` to use the `` and `` components from `studio-web` * Added some CSS layout utilities for positioning overlayed UI elements * Extracted `` and `` to their own components * Extracted `` to its own component --- .vscode/extensions.json | 5 + studio-frontend/index.html | 1 - studio-frontend/src/app/app.element.scss | 107 ++------- studio-frontend/src/app/app.element.ts | 205 ++++++------------ .../src/app/toolbar-group.element.scss | 22 ++ studio-frontend/src/app/toolbar.element.scss | 4 + studio-frontend/src/app/toolbar.element.ts | 21 ++ .../src/app/transform-toolbar.element.scss | 57 +++++ .../src/app/transform-toolbar.element.ts | 105 +++++++++ studio-web/src/lib/viewer.element.ts | 2 +- 10 files changed, 296 insertions(+), 233 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 studio-frontend/src/app/toolbar-group.element.scss create mode 100644 studio-frontend/src/app/toolbar.element.scss create mode 100644 studio-frontend/src/app/toolbar.element.ts create mode 100644 studio-frontend/src/app/transform-toolbar.element.scss create mode 100644 studio-frontend/src/app/transform-toolbar.element.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..0441a85 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "runem.lit-plugin" + ] +} diff --git a/studio-frontend/index.html b/studio-frontend/index.html index 009702c..49ce5c6 100644 --- a/studio-frontend/index.html +++ b/studio-frontend/index.html @@ -14,7 +14,6 @@ - diff --git a/studio-frontend/src/app/app.element.scss b/studio-frontend/src/app/app.element.scss index 7d60320..9f80b0b 100644 --- a/studio-frontend/src/app/app.element.scss +++ b/studio-frontend/src/app/app.element.scss @@ -1,112 +1,31 @@ -:host { +.app-root { position: fixed; inset: 0; - pointer-events: none; } -.btn-help { +// Layout utilities +.v-start { position: absolute; top: 16px; - right: 16px; } - -.toolbar { - display: flex; - gap: 16px; +.v-after-toolbar { position: absolute; - top: 16px; + top: 60px; +} +.h-start { left: 16px } +.h-end { right: 16px } +.h-center { left: 50%; transform: translateX(-50%); } -.toolbar-group { - display: flex; - gap: 2px; -} - -.btn-toggle { - display: inline-flex; - align-items: center; - justify-content: center; - position: relative; - font-size: 16px; - padding: 10px 12px; - pointer-events: auto; - outline: 0 solid transparent; - - // TODO: Copypasta - background: #0006; - color: #FFFC; - backdrop-filter: blur(16px); - transition: { - property: background, color, outline-width, outline-color; - duration: 0.1s; - timing-function: linear; - } - - &:first-child { - border: { - top-left-radius: 8px; - bottom-left-radius: 8px; - } - } - - &:last-child { - border: { - top-right-radius: 8px; - bottom-right-radius: 8px; - } - } - - &:hover { - background: #0008; - color: #FFF; - } - - &:has(:focus-visible) { - z-index: 1; - outline: 4px solid #2F83FFBF; - } - - &:has(:checked) { - background: #7A2C34E3; - color: #FFF; - - &:hover { - background: #90363F; - } - } - - &:has(:disabled) { - background: #0003 !important; - color: #FFF6 !important; - } - - input { - display: block; - appearance: none; - border: 0; - margin: 0; - padding: 0; - opacity: 0; - position: absolute; - inset: 0; - width: 100%; - height: 100%; - z-index: 1; - } -} - -.help { - position: absolute; - top: 60px; - right: 16px; +sts-button.active { + background: #7A2C34E3; + color: #FFF; } +// Hierarchy (TODO: Probably move this) .hierarchy { - position: absolute; - top: 16px; - left: 16px; overflow: auto; box-sizing: border-box; diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index be162d3..a2ee738 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -8,19 +8,25 @@ import { type SceneStateEvent, TransformSpace, TransformType, - ViewportSize, SceneState, } from "@storyteller/studio"; +import { CanvasProvider } from "@storyteller/studio-web/canvas-provider"; import { match } from "@storyteller/utility"; -import { LitElement, html, nothing, unsafeCSS } from "lit"; +import { LitElement, html, nothing } from "lit"; import { customElement, state } from "lit/decorators.js"; +import { classMap } from "lit-html/directives/class-map.js"; +import { Ref, createRef, ref } from "lit-html/directives/ref.js"; +import "@storyteller/studio-web/canvas-provider"; +import "@storyteller/studio-web/viewer"; import "./button.element"; import "./help.element"; import "./icon.element"; +import "./toolbar.element"; +import "./transform-toolbar.element"; import "./tree.element"; -import styles from "./app.element.scss?inline"; +import "./app.element.scss"; interface SceneObject extends Omit { children: SceneObject[]; @@ -28,8 +34,6 @@ interface SceneObject extends Omit { @customElement("app-root") export class AppElement extends LitElement { - static override styles = unsafeCSS(styles); - @state() showHelp = false; @state() loading = true; @@ -41,18 +45,17 @@ export class AppElement extends LitElement { #sceneObjects: studio.SceneObject[] = []; #entityMap = new Map(); - #studioCanvas?: HTMLCanvasElement | null; + #canvasProvider: Ref = createRef(); - override connectedCallback(): void { - this.#studioCanvas = this.#initStudio(); - super.connectedCallback(); + get #studioCanvas(): HTMLCanvasElement | null { + const provider = this.#canvasProvider.value; + if (!provider) return null; + + return document.getElementById(provider.canvasId) as HTMLCanvasElement | null; } - @on("window:resize") - onWindowResize(): void { - const width = window.innerWidth; - const height = window.innerHeight; - studio.resize(new ViewportSize(width, height)); + protected override createRenderRoot(): Element | ShadowRoot { + return this; } @on("keydown") @@ -82,7 +85,6 @@ export class AppElement extends LitElement { })); } - @on("window:scene-state") onSceneStateChange({ detail: state }: SceneStateEvent): void { console.log("scene state change:", SceneState[state]); @@ -124,38 +126,14 @@ export class AppElement extends LitElement { this.showHelp = !this.showHelp; } - onTransformSpaceChange(event: InputEvent): void { - const { value } = event.target as HTMLInputElement; - const space = match (value, { - "local": () => TransformSpace.Local, - "world": () => TransformSpace.World, - _: () => { - throw new Error(`Unknown transform space: "${value}"`); - } - }); - + onTransformSpaceChange({ detail: space }: CustomEvent): void { this.transformSpace = space; studio.setTransformSpace(space); } - onTransformTypeChange(event: InputEvent): void { - const { name, checked } = event.target as HTMLInputElement; - const flag = match (name, { - "translate": () => TransformType.Translate, - "rotate": () => TransformType.Rotate, - "scale": () => TransformType.Scale, - _: () => { - throw new Error(`Unknown transform type: "${name}"`); - } - }); - - if (checked) { - this.transformType |= flag; - } else { - this.transformType &= ~flag; - } - - studio.setTransformType(this.transformType); + onTransformTypeChange({ detail: type }: CustomEvent): void { + this.transformType = type; + studio.setTransformType(type); } // TODO: Get some help from someone who knows what an algorithm is @@ -186,105 +164,58 @@ export class AppElement extends LitElement { console.log("Hierarchy:", this.sceneObjects); } - #initStudio(): HTMLCanvasElement | null { - const width = window.innerWidth; - const height = window.innerHeight; - try { - studio.startViewer( - "couch", - "test_scene", - "#studio_canvas", - "Storyteller Studio", - new ViewportSize(width, height), - ); - } catch (err) { - if (!(err as Error).message?.startsWith("Using exceptions for control flow")) - throw err; - } - - return document.querySelector(`canvas[alt="Storyteller Studio"]`); - } - protected override render = () => html` - - - -
- ${this.sceneObjects.map(obj => html` - - `)} -
- - - Help - + - ${this.showHelp ? html` - - ` : nothing} + + + + Help + + + - ${this.loading ? html` - -
-

Loading

+ + +
+ ${this.sceneObjects.map(obj => html` + + `)}
- ` : nothing} + + ${this.showHelp ? html` + + ` : nothing} + + ${this.loading ? html` + +
+

Loading

+
+ ` : nothing} + `; } diff --git a/studio-frontend/src/app/toolbar-group.element.scss b/studio-frontend/src/app/toolbar-group.element.scss new file mode 100644 index 0000000..462ff27 --- /dev/null +++ b/studio-frontend/src/app/toolbar-group.element.scss @@ -0,0 +1,22 @@ +:host { + display: flex; + gap: 2px; +} + +::slotted(*) { + border-radius: 0; +} + +::slotted(:first-child) { + border: { + top-left-radius: 8px; + bottom-left-radius: 8px; + } +} + +::slotted(:last-child) { + border: { + top-right-radius: 8px; + bottom-right-radius: 8px; + } +} diff --git a/studio-frontend/src/app/toolbar.element.scss b/studio-frontend/src/app/toolbar.element.scss new file mode 100644 index 0000000..7d5aa1f --- /dev/null +++ b/studio-frontend/src/app/toolbar.element.scss @@ -0,0 +1,4 @@ +:host { + display: flex; + gap: 16px; +} diff --git a/studio-frontend/src/app/toolbar.element.ts b/studio-frontend/src/app/toolbar.element.ts new file mode 100644 index 0000000..3281b44 --- /dev/null +++ b/studio-frontend/src/app/toolbar.element.ts @@ -0,0 +1,21 @@ +import { LitElement, html, unsafeCSS } from "lit"; +import { customElement } from "lit/decorators.js"; + +import toolbarStyles from "./toolbar.element.scss?inline"; +import toolbarGroupStyles from "./toolbar-group.element.scss?inline"; + +// TODO: A11y - https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/ + +@customElement("sts-toolbar") +export class ToolbarElement extends LitElement { + static override styles = unsafeCSS(toolbarStyles); + override role = "toolbar"; + protected override render = () => html``; +} + +@customElement("sts-toolbar-group") +export class ToolbarGroupElement extends LitElement { + static override styles = unsafeCSS(toolbarGroupStyles); + override role = "group"; + protected override render = () => html``; +} diff --git a/studio-frontend/src/app/transform-toolbar.element.scss b/studio-frontend/src/app/transform-toolbar.element.scss new file mode 100644 index 0000000..43fbb31 --- /dev/null +++ b/studio-frontend/src/app/transform-toolbar.element.scss @@ -0,0 +1,57 @@ +.btn-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + position: relative; + font-size: 16px; + padding: 10px 12px; + pointer-events: auto; + + // TODO: Copypasta + background: #0006; + color: #FFFC; + backdrop-filter: blur(16px); + transition: { + property: background, color; + duration: 0.1s; + timing-function: linear; + } + + &:hover { + background: #0008; + color: #FFF; + } + + &:has(:focus-visible) { + z-index: 1; + outline: 4px solid #2F83FFBF; + } + + &:has(:checked) { + background: #7A2C34E3; + color: #FFF; + + &:hover { + background: #90363F; + } + } + + &:has(:disabled) { + background: #0003 !important; + color: #FFF6 !important; + } + + input { + display: block; + appearance: none; + border: 0; + margin: 0; + padding: 0; + opacity: 0; + position: absolute; + inset: 0; + width: 100%; + height: 100%; + z-index: 1; + } +} diff --git a/studio-frontend/src/app/transform-toolbar.element.ts b/studio-frontend/src/app/transform-toolbar.element.ts new file mode 100644 index 0000000..e6ca3fe --- /dev/null +++ b/studio-frontend/src/app/transform-toolbar.element.ts @@ -0,0 +1,105 @@ +import { TransformSpace, TransformType } from "@storyteller/studio"; +import { match } from "@storyteller/utility"; +import { LitElement, html, unsafeCSS } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +import styles from "./transform-toolbar.element.scss?inline"; + +@customElement("sts-transform-toolbar") +export class TransformToolbarElement extends LitElement { + static override styles = unsafeCSS(styles); + + @property({ type: Number }) space = TransformSpace.Local; + @property({ type: Number }) type = TransformType.Translate; + + #onSpaceChange(event: InputEvent): void { + const { value } = event.target as HTMLInputElement; + const space = match (value, { + "local": () => TransformSpace.Local, + "world": () => TransformSpace.World, + _: () => { + throw new Error(`Unknown transform space: "${value}"`); + } + }); + + this.dispatchEvent(new CustomEvent("space-change", { + detail: space, + bubbles: true, + composed: true, + })); + } + + #onTypeChange(event: InputEvent): void { + const { name, checked } = event.target as HTMLInputElement; + const flag = match (name, { + "translate": () => TransformType.Translate, + "rotate": () => TransformType.Rotate, + "scale": () => TransformType.Scale, + _: () => { + throw new Error(`Unknown transform type: "${name}"`); + } + }); + + const type = checked + ? this.type | flag + : this.type & ~flag; + + this.dispatchEvent(new CustomEvent("type-change", { + detail: type, + bubbles: true, + composed: true, + })); + } + + protected override render = () => html` + + + + + + + + + + + + `; +} diff --git a/studio-web/src/lib/viewer.element.ts b/studio-web/src/lib/viewer.element.ts index d81cd7d..3ae7944 100644 --- a/studio-web/src/lib/viewer.element.ts +++ b/studio-web/src/lib/viewer.element.ts @@ -4,7 +4,7 @@ import { inject } from "@storyteller/framework"; import { LitElement, PropertyValues, html, unsafeCSS } from "lit"; import { customElement, property } from "lit/decorators.js"; -import { CANVAS_PROVIDER, CanvasProvider } from "./canvas-provider.element"; +import { CANVAS_PROVIDER, type CanvasProvider } from "./canvas-provider.element"; import styles from "./viewer.element.scss?inline";