mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-22 08:06:01 +00:00
Media Library: Importing and spawning assets (#109)
This PR updates the Media Library feature to implement asset imports
from the local file system, and spawning assets from the library into
the scene. Additionally, some placeholder functionality has been
scaffolded for adding animations onto existing characters in the scene.
### Additional Changes
* Dialogs can be set to `nonBlocking` to allow interaction with
background elements (note: this is fraught for accessibility and likely
temporary, to facilitate testing while the UI/UX redesign is in
progress)
* Dialogs can be moved around the screen by dragging their title-bars
* The `scene` module has been moved into a folder with a separate
`wasm.rs` file for its submodule. (`scene/mod.rs` remains one of our
largest single Rust files and could probably stand to be partitioned
further.)
### To-Do
* Support for adding animations is not yet fully implemented, though
some prerequisite functionality has been scaffolded. Namely, the "Add to
Scene" button will be disabled if:
- There is a mix of animations and non-animations selected in the Media
Library
- There is more than one animation selected in the Media Library
- There is one animation selected in the Media Library, but no
animatable character selected in the scene
* Adding full Storyteller-format scenes to the existing scene is not a
use case that has been tested, and may not be a use case we wish to
support in the long-term, as some of the data stored in Storyteller
scenes are intended to be "singleton" resources that may conflict with
existing data in the scene.
**Attempting to spawn a Storyteller scene through the Media Library has
not been explicitly blocked for now, but may cause unexpected
behavior.** This needs additional UX consideration and grooming.
This commit is contained in:
@@ -10,8 +10,9 @@
|
||||
background: #0006;
|
||||
color: #FFFC;
|
||||
backdrop-filter: blur(32px);
|
||||
border-radius: 0.5rem;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.main {
|
||||
@@ -22,13 +23,23 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 auto;
|
||||
flex: 1 0 0;
|
||||
gap: 1rem;
|
||||
border: {
|
||||
inline: 0.5rem solid #0004;
|
||||
block-end: 0.5rem solid #0004;
|
||||
bottom-right-radius: 1rem;
|
||||
bottom-left-radius: 1rem;
|
||||
}
|
||||
padding: 1rem;
|
||||
|
||||
&.with-footer {
|
||||
border: {
|
||||
block-end: none;
|
||||
bottom-right-radius: 0;
|
||||
bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[part="sidebar"]:not(:empty) {
|
||||
@@ -36,6 +47,9 @@
|
||||
background: #0004;
|
||||
|
||||
+ .content {
|
||||
border-inline-start: none;
|
||||
border: {
|
||||
inline-start: none;
|
||||
bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { on } from "@storyteller/framework";
|
||||
import { LitElement, TemplateResult, html, unsafeCSS } from "lit";
|
||||
import { customElement, property, queryAssignedElements } from "lit/decorators.js";
|
||||
import { LitElement, PropertyValues, TemplateResult, html, unsafeCSS } from "lit";
|
||||
import { customElement, property, queryAssignedElements, state } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { type DialogHeaderElement } from "./dialog-header.element";
|
||||
@@ -62,14 +63,37 @@ export class DialogBaseElement extends LitElement {
|
||||
override ariaModal = "true";
|
||||
override tabIndex = -1;
|
||||
|
||||
@state() _hasFooter = false;
|
||||
|
||||
@queryAssignedElements({ slot: "dialog-header" })
|
||||
dialogHeaders!: Element[];
|
||||
|
||||
@queryAssignedElements({ slot: "footer" })
|
||||
dialogFooters!: Element[];
|
||||
|
||||
@state() _translateX?: number;
|
||||
@state() _translateY?: number;
|
||||
|
||||
override connectedCallback(): void {
|
||||
this.focus();
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
protected override update(changes: PropertyValues<this>): void {
|
||||
if (
|
||||
changes.has("_translateX") || changes.has("_translateY")
|
||||
&& (this._translateX != null || this._translateY != null)
|
||||
) {
|
||||
this.style.setProperty(
|
||||
"transform",
|
||||
`translateX(${this._translateX ?? 0}px) ` +
|
||||
`translateY(${this._translateY ?? 0}px)`
|
||||
);
|
||||
}
|
||||
|
||||
super.update(changes);
|
||||
}
|
||||
|
||||
@on("keydown")
|
||||
onKeydown(event: KeyboardEvent): void {
|
||||
if (/^(Escape|Shift)$/.test(event.key))
|
||||
@@ -79,6 +103,12 @@ export class DialogBaseElement extends LitElement {
|
||||
this.dispatchEvent(new CustomEvent("close"));
|
||||
}
|
||||
|
||||
@on("translate", { capture: true })
|
||||
onTranslate({ detail: { x, y }}: CustomEvent<{ x: number; y: number }>): void {
|
||||
this._translateX = x;
|
||||
this._translateY = y;
|
||||
}
|
||||
|
||||
onDialogHeaderChanged(): void {
|
||||
if (this.dialogHeaders.length && !this.ariaLabel) {
|
||||
const header = this.dialogHeaders[0];
|
||||
@@ -90,6 +120,10 @@ export class DialogBaseElement extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
onFooterChanged(): void {
|
||||
this._hasFooter = this.dialogFooters.length > 0;
|
||||
}
|
||||
|
||||
protected override render(): TemplateResult {
|
||||
return html`
|
||||
<slot
|
||||
@@ -107,12 +141,21 @@ export class DialogBaseElement extends LitElement {
|
||||
<section class="main" part="main">
|
||||
<slot name="sidebar" part="sidebar"></slot>
|
||||
|
||||
<section class="content" part="content">
|
||||
<section
|
||||
class=${classMap({
|
||||
"content": true,
|
||||
"with-footer": this._hasFooter,
|
||||
})}
|
||||
part="content"
|
||||
>
|
||||
<slot></slot>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<slot name="footer"></slot>
|
||||
<slot
|
||||
name="footer"
|
||||
@slotchange=${this.onFooterChanged}
|
||||
></slot>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
:host {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
padding: 0 1rem 1rem;
|
||||
border: {
|
||||
inline: 0.5rem solid #0004;
|
||||
block-end: 0.5rem solid #0004;
|
||||
bottom-right-radius: 1rem;
|
||||
bottom-left-radius: 1rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { LitElement, html, unsafeCSS } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import styles from "./dialog-footer.element.scss?inline";
|
||||
|
||||
@customElement("sts-dialog-footer")
|
||||
export class DialogFooterElement extends LitElement {
|
||||
static override styles = unsafeCSS(styles);
|
||||
|
||||
@property({ reflect: true })
|
||||
override slot = "footer";
|
||||
|
||||
protected override render = () => html`
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
@@ -3,18 +3,18 @@
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 3rem;
|
||||
padding: 0 {
|
||||
inline-start: 1rem;
|
||||
}
|
||||
background: #0006;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1 0 0;
|
||||
display: flex;
|
||||
height: 3rem;
|
||||
align-items: center;
|
||||
gap: math.div(12, 16) * 1rem;
|
||||
padding-inline-start: 1rem;
|
||||
user-select: none;
|
||||
|
||||
&__icon {
|
||||
color: #FFFC;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { drag } from "@storyteller/framework";
|
||||
import { LitElement, html, nothing, unsafeCSS } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { v4 as uuid } from "uuid";
|
||||
@@ -53,6 +54,9 @@ export class DialogHeaderElement extends LitElement {
|
||||
|
||||
@property() icon?: string;
|
||||
|
||||
#translateX?: number;
|
||||
#translateY?: number;
|
||||
|
||||
onClose(): void {
|
||||
this.dispatchEvent(new CustomEvent("close", {
|
||||
bubbles: true,
|
||||
@@ -60,10 +64,30 @@ export class DialogHeaderElement extends LitElement {
|
||||
}));
|
||||
}
|
||||
|
||||
onDrag(event: PointerEvent): void {
|
||||
this.#translateX ??= 0;
|
||||
this.#translateY ??= 0;
|
||||
|
||||
this.#translateX += event.movementX;
|
||||
this.#translateY += event.movementY;
|
||||
|
||||
this.dispatchEvent(new CustomEvent("translate", {
|
||||
detail: {
|
||||
x: this.#translateX,
|
||||
y: this.#translateY,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
<div
|
||||
class="title"
|
||||
part="title"
|
||||
${drag({
|
||||
move: this.onDrag,
|
||||
})}
|
||||
>
|
||||
${this.icon ? html`
|
||||
<sts-icon
|
||||
|
||||
@@ -4,6 +4,7 @@ import { LitElement, TemplateResult, type PropertyValues } from "lit";
|
||||
import { html, unsafeStatic } from "lit/static-html.js";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { StyleInfo } from "lit/directives/style-map.js";
|
||||
import { Ref } from "lit/directives/ref.js";
|
||||
|
||||
import {
|
||||
@@ -293,6 +294,14 @@ export class DialogElement extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
ignoreBackdropClick = false;
|
||||
|
||||
/**
|
||||
* If this property is `true`, the dialog backdrop will be completely
|
||||
* transparent and allow the user to interact with background elements while
|
||||
* the dialog is still present.
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
nonBlocking = false;
|
||||
|
||||
/**
|
||||
* ARIA role to pass to the custom element defined by `tag`.
|
||||
*
|
||||
@@ -318,12 +327,18 @@ export class DialogElement extends LitElement {
|
||||
if (!this.tag && !this.template)
|
||||
return;
|
||||
|
||||
const containerStyles = {
|
||||
const containerStyles: StyleInfo = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "#1A1A2766",
|
||||
};
|
||||
|
||||
if (this.nonBlocking) {
|
||||
containerStyles["pointer-events"] = "none";
|
||||
} else {
|
||||
containerStyles["background"] = "#1A1A2766";
|
||||
}
|
||||
|
||||
const onBackdropClick = this.onBackdropClick;
|
||||
|
||||
if (this.template) {
|
||||
@@ -393,7 +408,7 @@ export class DialogElement extends LitElement {
|
||||
}
|
||||
|
||||
onBackdropClick = (): void => {
|
||||
if (!this.ignoreBackdropClick)
|
||||
if (!this.ignoreBackdropClick && !this.nonBlocking)
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import "./dialog-base.element";
|
||||
import "./dialog-footer.element";
|
||||
import "./dialog-header.element";
|
||||
import "./dialog.element";
|
||||
|
||||
export * from "./dialog.element";
|
||||
export * from "./dialog-base.element";
|
||||
export * from "./dialog-footer.element";
|
||||
export * from "./dialog-header.element";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { on } from "@storyteller/framework";
|
||||
import { html, nothing, unsafeCSS } from "lit";
|
||||
import { PropertyValues, html, nothing, unsafeCSS } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@@ -27,6 +27,13 @@ export class TextFieldElement extends FormFieldBaseElement {
|
||||
@property({ attribute: "icon-pre" }) iconPre?: string;
|
||||
@property({ attribute: "icon-post" }) iconPost?: string;
|
||||
|
||||
protected override update(changes: PropertyValues<this>): void {
|
||||
if (changes.has("value"))
|
||||
this.internals.setFormValue(this.value);
|
||||
|
||||
super.update(changes);
|
||||
}
|
||||
|
||||
@on("keydown")
|
||||
@on("keyup")
|
||||
blockKeypressBubbling(event: KeyboardEvent): void {
|
||||
|
||||
@@ -44,6 +44,7 @@ import farArrowRightToBracket from "@fortawesome/fontawesome-pro/svgs/regular/ar
|
||||
import farMagnifyingGlass from "@fortawesome/fontawesome-pro/svgs/regular/magnifying-glass.svg?raw";
|
||||
import fasHouse from "@fortawesome/fontawesome-pro/svgs/solid/house.svg?raw";
|
||||
import fasRecord from "@fortawesome/fontawesome-pro/svgs/solid/circle.svg?raw";
|
||||
import fasUpload from "@fortawesome/fontawesome-pro/svgs/solid/upload.svg?raw";
|
||||
|
||||
import customCylinder from "../assets/icons/cylinder.svg?raw";
|
||||
import customSphere from "../assets/icons/sphere.svg?raw";
|
||||
@@ -105,6 +106,7 @@ function renderSvg(iconId: string) {
|
||||
"pause": () => fasPause,
|
||||
"plus-large": () => fasPlusLarge,
|
||||
"question": () => fasQuestion,
|
||||
"record": () => fasRecord,
|
||||
"rotate": () => farRotate,
|
||||
"scale": () => farScale,
|
||||
"scene": () => farMountainSun,
|
||||
@@ -113,10 +115,10 @@ function renderSvg(iconId: string) {
|
||||
"sphere": () => customSphere,
|
||||
"torus": () => customTorus,
|
||||
"up-down-left-right": () => farUpDownLeftRight,
|
||||
"upload": () => fasUpload,
|
||||
"video": () => farVideo,
|
||||
"volume": () => farVolume,
|
||||
"xmark": () => fasXmark,
|
||||
"record": () => fasRecord,
|
||||
_: () => null,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user