mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-18 00:23:14 +00:00
Refactor studio-frontend (#19)
This PR does some rudimentary cleanup / reorganizing to the `studio-frontend` project. ### Changes * Refactored `studio-frontend` to use the `<sts-canvas-provider/>` and `<sts-viewer/>` components from `studio-web` * Added some CSS layout utilities for positioning overlayed UI elements * Extracted `<sts-toolbar/>` and `<sts-toolbar-group/>` to their own components * Extracted `<sts-transform-toolbar/>` to its own component
This commit is contained in:
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"runem.lit-plugin"
|
||||
]
|
||||
}
|
||||
@@ -14,7 +14,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
<canvas id="studio_canvas"></canvas>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<studio.SceneObject, "children"> {
|
||||
children: SceneObject[];
|
||||
@@ -28,8 +34,6 @@ interface SceneObject extends Omit<studio.SceneObject, "children"> {
|
||||
|
||||
@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<string, SceneObject>();
|
||||
#studioCanvas?: HTMLCanvasElement | null;
|
||||
#canvasProvider: Ref<CanvasProvider> = 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<TransformSpace>): 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<TransformType>): 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<HTMLCanvasElement>(`canvas[alt="Storyteller Studio"]`);
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
<!-- TODO: What's a "component"? -->
|
||||
<div class="toolbar" role="toolbar">
|
||||
<div class="toolbar-group" role="group">
|
||||
<label class="btn-toggle">
|
||||
<sts-icon icon="cube">Local</sts-icon>
|
||||
<input type="radio"
|
||||
name="transform-space"
|
||||
value="local"
|
||||
?checked=${this.transformSpace === TransformSpace.Local}
|
||||
@input=${this.onTransformSpaceChange}
|
||||
/>
|
||||
</label>
|
||||
<label class="btn-toggle">
|
||||
<sts-icon icon="globe">Global</sts-icon>
|
||||
<input type="radio"
|
||||
name="transform-space"
|
||||
value="world"
|
||||
?checked=${this.transformSpace === TransformSpace.World}
|
||||
@input=${this.onTransformSpaceChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="toolbar-group" role="group">
|
||||
<label class="btn-toggle">
|
||||
<sts-icon icon="up-down-left-right">Translate</sts-icon>
|
||||
<input type="checkbox"
|
||||
name="translate"
|
||||
?checked=${this.transformType & TransformType.Translate}
|
||||
@input=${this.onTransformTypeChange}
|
||||
/>
|
||||
</label>
|
||||
<!-- disabled -->
|
||||
<label class="btn-toggle">
|
||||
<sts-icon icon="rotate">Rotate</sts-icon>
|
||||
<input type="checkbox"
|
||||
name="rotate"
|
||||
?checked=${this.transformType & TransformType.Rotate}
|
||||
@input=${this.onTransformTypeChange}
|
||||
/>
|
||||
</label>
|
||||
<label class="btn-toggle">
|
||||
<sts-icon icon="scale">Scale</sts-icon>
|
||||
<input type="checkbox"
|
||||
name="scale"
|
||||
disabled
|
||||
?checked=${this.transformType & TransformType.Scale}
|
||||
@input=${this.onTransformTypeChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hierarchy">
|
||||
${this.sceneObjects.map(obj => html`
|
||||
<sts-tree class="scene-object"
|
||||
id=${obj.id}
|
||||
.name=${obj.name}
|
||||
.treeChildren=${obj.children}
|
||||
></sts-tree>
|
||||
`)}
|
||||
</div>
|
||||
|
||||
<sts-button class="btn-help"
|
||||
icon="circle-info"
|
||||
@click=${this.toggleHelp}
|
||||
<sts-canvas-provider
|
||||
class="app-root"
|
||||
${ref(this.#canvasProvider as any)}
|
||||
>
|
||||
Help
|
||||
</sts-button>
|
||||
<sts-viewer
|
||||
objectId="sample-room"
|
||||
skyboxId="test_scene"
|
||||
@scene-state=${this.onSceneStateChange}
|
||||
></sts-viewer>
|
||||
|
||||
${this.showHelp ? html`
|
||||
<sts-help class="help"></sts-help>
|
||||
` : nothing}
|
||||
<sts-toolbar class="v-start h-start">
|
||||
<sts-toolbar-group>
|
||||
<sts-button
|
||||
class=${classMap({ active: this.showHelp })}
|
||||
icon="circle-info"
|
||||
@click=${this.toggleHelp}
|
||||
>
|
||||
Help
|
||||
</sts-button>
|
||||
</sts-toolbar-group>
|
||||
</sts-toolbar>
|
||||
|
||||
${this.loading ? html`
|
||||
<!-- TODO: Animated spinner or something -- talk to our designers -->
|
||||
<div class="loading">
|
||||
<h1>Loading</h1>
|
||||
<sts-transform-toolbar
|
||||
class="v-start h-center"
|
||||
.type=${this.transformType}
|
||||
@type-change=${this.onTransformTypeChange}
|
||||
.space=${this.transformSpace}
|
||||
@space-change=${this.onTransformSpaceChange}
|
||||
></sts-transform-toolbar>
|
||||
|
||||
<div class="hierarchy v-start h-end">
|
||||
${this.sceneObjects.map(obj => html`
|
||||
<sts-tree class="scene-object"
|
||||
id=${obj.id}
|
||||
.name=${obj.name}
|
||||
.treeChildren=${obj.children}
|
||||
></sts-tree>
|
||||
`)}
|
||||
</div>
|
||||
` : nothing}
|
||||
|
||||
${this.showHelp ? html`
|
||||
<sts-help class="help v-after-toolbar h-start"></sts-help>
|
||||
` : nothing}
|
||||
|
||||
${this.loading ? html`
|
||||
<!-- TODO: Animated spinner or something -- talk to our designers -->
|
||||
<div class="loading">
|
||||
<h1>Loading</h1>
|
||||
</div>
|
||||
` : nothing}
|
||||
</sts-canvas-provider>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
:host {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
@@ -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`<slot></slot>`;
|
||||
}
|
||||
|
||||
@customElement("sts-toolbar-group")
|
||||
export class ToolbarGroupElement extends LitElement {
|
||||
static override styles = unsafeCSS(toolbarGroupStyles);
|
||||
override role = "group";
|
||||
protected override render = () => html`<slot></slot>`;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<TransformSpace>("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<TransformType>("type-change", {
|
||||
detail: type,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
<sts-toolbar>
|
||||
<sts-toolbar-group>
|
||||
<label class="btn-toggle">
|
||||
<sts-icon icon="cube">Local</sts-icon>
|
||||
<input type="radio"
|
||||
name="transform-space"
|
||||
value="local"
|
||||
?checked=${this.space === TransformSpace.Local}
|
||||
@input=${this.#onSpaceChange}
|
||||
/>
|
||||
</label>
|
||||
<label class="btn-toggle">
|
||||
<sts-icon icon="globe">Global</sts-icon>
|
||||
<input type="radio"
|
||||
name="transform-space"
|
||||
value="world"
|
||||
?checked=${this.space === TransformSpace.World}
|
||||
@input=${this.#onSpaceChange}
|
||||
/>
|
||||
</label>
|
||||
</sts-toolbar-group>
|
||||
<sts-toolbar-group>
|
||||
<label class="btn-toggle">
|
||||
<sts-icon icon="up-down-left-right">Translate</sts-icon>
|
||||
<input type="checkbox"
|
||||
name="translate"
|
||||
?checked=${Boolean(this.type & TransformType.Translate)}
|
||||
@input=${this.#onTypeChange}
|
||||
/>
|
||||
</label>
|
||||
<label class="btn-toggle">
|
||||
<sts-icon icon="rotate">Rotate</sts-icon>
|
||||
<input type="checkbox"
|
||||
name="rotate"
|
||||
?checked=${Boolean(this.type & TransformType.Rotate)}
|
||||
@input=${this.#onTypeChange}
|
||||
/>
|
||||
</label>
|
||||
<label class="btn-toggle">
|
||||
<sts-icon icon="scale">Scale</sts-icon>
|
||||
<input type="checkbox"
|
||||
name="scale"
|
||||
disabled
|
||||
?checked=${Boolean(this.type & TransformType.Scale)}
|
||||
@input=${this.#onTypeChange}
|
||||
/>
|
||||
</label>
|
||||
</sts-toolbar-group>
|
||||
</sts-toolbar>
|
||||
`;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user