UI styling, main workspace layout (#119)

This commit is contained in:
Danny McGee
2024-04-05 20:15:09 -04:00
committed by GitHub
parent c5a0250d9f
commit bfc638b644
57 changed files with 2004 additions and 694 deletions
@@ -3,12 +3,9 @@
display: flex;
flex-direction: column;
gap: var(--padding);
gap: 1rem;
overflow: auto;
--background-color: var(--color-panel-background);
color: #FFFC;
color: #FFF;
&::-webkit-scrollbar {
width: 0.5rem;
@@ -32,13 +29,3 @@
*, *::before, *::after {
box-sizing: border-box;
}
.container {
background-color: var(--background-color);
border-radius: var(--corner-radius);
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: var(--padding);
gap: var(--padding);
}
@@ -1,5 +1,5 @@
import { inject } from "@storyteller/framework";
import { LitElement, type PropertyValues, html, unsafeCSS, nothing } from "lit";
import { inject, observe } from "@storyteller/framework";
import { LitElement, html, unsafeCSS, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import { SCENE_DATA_PROVIDER, type SceneDataProvider } from "../scene-data-provider.element";
@@ -16,89 +16,69 @@ import styles from "./entity-inspector.element.scss?inline";
export class EntityInspectorElement extends LitElement {
static override styles = unsafeCSS(styles);
@observe(["selectionComponents"])
@inject(SCENE_DATA_PROVIDER)
_sceneData?: SceneDataProvider;
_sceneData!: SceneDataProvider;
override connectedCallback(): void {
this._sceneData?.addEventListener("property-changes", this.onProviderChanges);
super.connectedCallback();
}
protected override render = () => html`
${this._sceneData
?.selectionComponents
.slice()
.sort((a, b) => {
const ordA = COMPONENT_SORT_ORDER.indexOf(a.name);
const ordB = COMPONENT_SORT_ORDER.indexOf(b.name);
override disconnectedCallback(): void {
this._sceneData?.removeEventListener("property-changes", this.onProviderChanges);
super.disconnectedCallback();
}
if (ordA === -1) return Infinity;
if (ordB === -1) return -Infinity;
onProviderChanges = ({ detail: changes }: CustomEvent<PropertyValues<SceneDataProvider>>) => {
if (changes.has("selectionComponents"))
this.requestUpdate();
}
return ordA - ordB;
})
.map(({ name, value }) => {
switch (name) {
// TODO?
// case "bevy_core::name::Name": {
// return html`
// <sts-name-inspector
// .value=${value.name}
// ></sts-name-inspector>
// `;
// }
case "bevy_transform::components::transform::Transform": {
return html`
<sts-transform-inspector
.translation=${value.translation}
.rotation=${value.rotation}
.scale=${value.scale}
></sts-transform-inspector>
`;
}
case "bevy_pbr::pbr_material::StandardMaterial": {
const { colorSpace, baseColor } = isLinear(value.base_color)
? { colorSpace: "linear", baseColor: value.base_color.RgbaLinear }
: { colorSpace: "srgb", baseColor: value.base_color.Rgba };
protected override render () {
let components = this._sceneData
?.selectionComponents
.slice()
.sort((a, b) => {
const ordA = COMPONENT_SORT_ORDER.indexOf(a.name);
const ordB = COMPONENT_SORT_ORDER.indexOf(b.name);
if (ordA === -1) return Infinity;
if (ordB === -1) return -Infinity;
return ordA - ordB;
})
.map(({ name, value }) => {
switch (name) {
case "bevy_core::name::Name": {
return html`
<sts-name-inspector
.value=${value.name}
></sts-name-inspector>
`;
return html`
<sts-material-inspector
.baseColor=${baseColor}
.colorSpace=${colorSpace}
.roughness=${value.perceptual_roughness}
.metallic=${value.metallic}
.reflectance=${value.reflectance}
></sts-material-inspector>
`;
}
default: {
return nothing;
// return html`
// <h3>Unknown Component: ${name}</h3>
// <pre>${JSON.stringify({ name, value }, null, " ")}</pre>
// `;
}
}
case "bevy_transform::components::transform::Transform": {
return html`
<sts-transform-inspector
.translation=${value.translation}
.rotation=${value.rotation}
.scale=${value.scale}
></sts-transform-inspector>
`;
}
case "bevy_pbr::pbr_material::StandardMaterial": {
const { colorSpace, baseColor } = isLinear(value.base_color)
? { colorSpace: "linear", baseColor: value.base_color.RgbaLinear }
: { colorSpace: "srgb", baseColor: value.base_color.Rgba };
return html`
<sts-material-inspector
.baseColor=${baseColor}
.colorSpace=${colorSpace}
.roughness=${value.perceptual_roughness}
.metallic=${value.metallic}
.reflectance=${value.reflectance}
></sts-material-inspector>
`;
}
default: {
return nothing;
// return html`
// <h3>Unknown Component: ${name}</h3>
// <pre>${JSON.stringify({ name, value }, null, " ")}</pre>
// `;
}
}
}).filter((value) => value !== nothing) || [];
if (components?.length === 0) {
return nothing;
})
?? nothing
}
return html`
<div class="container">
${components
}
</div>
`;
}
}
const COMPONENT_SORT_ORDER: Array<InspectorComponent["name"]> = [
@@ -11,7 +11,7 @@
h4 {
font-weight: 500;
margin: 0 0 1rem;
margin: 0 0 0.5rem;
}
.fields {
@@ -2,30 +2,41 @@
:host {
display: flex;
gap: 0.25rem;
gap: 0.5rem;
}
.field {
display: flex;
width: math.div(1, 3) * 100%;
border-radius: 0.5rem;
overflow: hidden;
outline: transparent 2px solid {
offset: -2px;
}
transition: outline-color 100ms linear;
&--x:focus-within { outline-color: var(--axis-red) }
&--y:focus-within { outline-color: var(--axis-green) }
&--z:focus-within { outline-color: var(--axis-blue) }
}
.label {
display: flex;
align-items: center;
padding: 0 0.375rem;
font: {
size: math.div(14, 16) * 1rem;
weight: 500;
}
&--x { background: var(--axis-red) }
&--y { background: var(--axis-green) }
&--z { background: var(--axis-blue) }
}
.input {
padding: 0.5rem;
width: math.div(1, 3) * 100%;
border-left-width: 4px;
&:nth-of-type(1) {
border-left-color: rgb(220, 53, 69);
&:focus {
border-left-color: rgb(255, 48, 68);
}
}
&:nth-of-type(2) {
border-left-color: rgb(25, 135, 84);
&:focus {
border-left-color: rgb(0, 187, 100);
}
}
&:nth-of-type(3) {
border-left-color: rgb(13, 110, 253);
&:focus {
border-left-color: rgb(47, 131, 255);
}
}
flex: 1 0 0;
border-radius: 0;
border: none;
outline: none !important;
}
@@ -46,36 +46,44 @@ export class QuatInspectorElement extends LitElement {
}
protected override render = (): TemplateResult => html`
<sts-scalar-field
class="input"
aria-label="X"
format="1.1-2 °"
emitOn="change"
.step=${1}
.microStep=${0.1}
.value=${this._eulerX}
@value-change=${this.#emitUpdate(RotationAxis.X)}
></sts-scalar-field>
<sts-scalar-field
class="input"
aria-label="Y"
format="1.1-2 °"
emitOn="change"
.step=${1}
.microStep=${0.1}
.value=${this._eulerY}
@value-change=${this.#emitUpdate(RotationAxis.Y)}
></sts-scalar-field>
<sts-scalar-field
class="input"
aria-label="Z"
format="1.1-2 °"
emitOn="change"
.step=${1}
.microStep=${0.1}
.value=${this._eulerZ}
@value-change=${this.#emitUpdate(RotationAxis.Z)}
></sts-scalar-field>
<label class="field field--x">
<span class="label label--x">X</span>
<sts-scalar-field
class="input"
format="1.2-2 °"
emitOn="change"
.step=${1}
.microStep=${0.1}
.value=${this._eulerX}
@value-change=${this.#emitUpdate(RotationAxis.X)}
></sts-scalar-field>
</label>
<label class="field field--y">
<span class="label label--y">Y</span>
<sts-scalar-field
class="input"
format="1.2-2 °"
emitOn="change"
.step=${1}
.microStep=${0.1}
.value=${this._eulerY}
@value-change=${this.#emitUpdate(RotationAxis.Y)}
></sts-scalar-field>
</label>
<label class="field field--z">
<span class="label label--z">Z</span>
<sts-scalar-field
class="input"
format="1.2-2 °"
emitOn="change"
.step=${1}
.microStep=${0.1}
.value=${this._eulerZ}
@value-change=${this.#emitUpdate(RotationAxis.Z)}
></sts-scalar-field>
</label>
`;
}
@@ -22,43 +22,57 @@ h4 {
position: relative;
width: 100%;
padding: 0 {
block-start: math.div(14, 16) + 0.333rem !important;
block-start: math.div(17, 16) + 0.25rem !important;
}
margin: 0;
gap: 0.25rem;
gap: 0.5rem;
border: none;
legend {
position: absolute;
inset-block-start: 0;
margin-block-end: 0.333rem;
color: #FFF9;
font-size: math.div(14, 16) * 1rem;
padding: 0;
color: #FFF;
font: {
size: math.div(14, 16) * 1rem;
weight: 600;
}
}
.field {
display: flex;
width: math.div(1, 3) * 100%;
border-radius: 0.5rem;
overflow: hidden;
outline: transparent 2px solid {
offset: -2px;
}
transition: outline-color 100ms linear;
&--x:focus-within { outline-color: var(--axis-red) }
&--y:focus-within { outline-color: var(--axis-green) }
&--z:focus-within { outline-color: var(--axis-blue) }
}
.label {
display: flex;
align-items: center;
padding: 0 0.375rem;
font: {
size: math.div(14, 16) * 1rem;
weight: 500;
}
&--x { background: var(--axis-red) }
&--y { background: var(--axis-green) }
&--z { background: var(--axis-blue) }
}
.input {
padding: 0.5rem;
width: math.div(1, 3) * 100%;
border-left-width: 4px;
&:nth-of-type(1) {
border-left-color: rgb(220, 53, 69);
&:focus {
border-left-color: rgb(255, 48, 68);
}
}
&:nth-of-type(2) {
border-left-color: rgb(25, 135, 84);
&:focus {
border-left-color: rgb(0, 187, 100);
}
}
&:nth-of-type(3) {
border-left-color: rgb(13, 110, 253);
&:focus {
border-left-color: rgb(47, 131, 255);
}
}
flex: 1 0 0;
border-radius: 0;
border: none;
outline: none !important;
}
}
@@ -39,7 +39,6 @@ export class TransformInspectorElement extends LitElement {
}
protected override render = () => html`
<h4>Transform</h4>
<div
class="fieldset"
role="group"
@@ -48,31 +47,44 @@ export class TransformInspectorElement extends LitElement {
<legend id=${this.#positionLabelId}>
Position
</legend>
<sts-scalar-field
class="input"
aria-label="X"
.step=${0.01}
.microStep=${0.001}
.value=${this.translation.x}
@value-change=${this.updateField("translation.x")}
></sts-scalar-field>
<sts-scalar-field
class="input"
aria-label="Y"
.step=${0.01}
.microStep=${0.001}
.value=${this.translation.y}
@value-change=${this.updateField("translation.y")}
></sts-scalar-field>
<sts-scalar-field
class="input"
aria-label="Z"
.step=${0.01}
.microStep=${0.001}
.value=${this.translation.z}
@value-change=${this.updateField("translation.z")}
></sts-scalar-field>
<label class="field field--x">
<span class="label label--x">X</span>
<sts-scalar-field
class="input"
format="1.3"
.step=${0.01}
.microStep=${0.001}
.value=${this.translation.x}
@value-change=${this.updateField("translation.x")}
></sts-scalar-field>
</label>
<label class="field field--y">
<span class="label label--y">Y</span>
<sts-scalar-field
class="input"
format="1.3"
.step=${0.01}
.microStep=${0.001}
.value=${this.translation.y}
@value-change=${this.updateField("translation.y")}
></sts-scalar-field>
</label>
<label class="field field--z">
<span class="label label--z">Z</span>
<sts-scalar-field
class="input"
format="1.3"
.step=${0.01}
.microStep=${0.001}
.value=${this.translation.z}
@value-change=${this.updateField("translation.z")}
></sts-scalar-field>
</label>
</div>
<div
class="fieldset"
role="group"
@@ -86,6 +98,7 @@ export class TransformInspectorElement extends LitElement {
@value-change=${this.updateField("rotation")}
></sts-quat-inspector>
</div>
<div
class="fieldset"
role="group"
@@ -94,30 +107,42 @@ export class TransformInspectorElement extends LitElement {
<legend id=${this.#scaleLabelId}>
Scale
</legend>
<sts-scalar-field
class="input"
aria-label="X"
.step=${0.1}
.microStep=${0.01}
.value=${this.scale.x}
@value-change=${this.updateField("scale.x")}
></sts-scalar-field>
<sts-scalar-field
class="input"
aria-label="Y"
.step=${0.1}
.microStep=${0.01}
.value=${this.scale.y}
@value-change=${this.updateField("scale.y")}
></sts-scalar-field>
<sts-scalar-field
class="input"
aria-label="Z"
.step=${0.1}
.microStep=${0.01}
.value=${this.scale.z}
@value-change=${this.updateField("scale.z")}
></sts-scalar-field>
<label class="field field--x">
<span class="label label--x">X</span>
<sts-scalar-field
class="input"
format="1.3"
.step=${0.1}
.microStep=${0.01}
.value=${this.scale.x}
@value-change=${this.updateField("scale.x")}
></sts-scalar-field>
</label>
<label class="field field--y">
<span class="label label--y">Y</span>
<sts-scalar-field
class="input"
format="1.3"
.step=${0.1}
.microStep=${0.01}
.value=${this.scale.y}
@value-change=${this.updateField("scale.y")}
></sts-scalar-field>
</label>
<label class="field field--z">
<span class="label label--z">Z</span>
<sts-scalar-field
class="input"
format="1.3"
.step=${0.1}
.microStep=${0.01}
.value=${this.scale.z}
@value-change=${this.updateField("scale.z")}
></sts-scalar-field>
</label>
</div>
`;
}
@@ -1,6 +1,6 @@
import * as studio from "@storyteller/studio";
import { SceneElement } from "@storyteller/studio";
import { bind, inject } from "@storyteller/framework";
import { bind, inject, observe } from "@storyteller/framework";
import { DialogBaseElement, DialogElement } from "@storyteller/studio-ui/dialog";
import {
type MediaFile as IMediaFile,
@@ -56,6 +56,7 @@ export class MediaLibraryDialogElement extends DialogBaseElement {
@inject(StorytellerApi)
_api!: StorytellerApi;
@observe(["selectedObject"])
@inject(SCENE_DATA_PROVIDER)
_sceneData!: SceneDataProvider;
@@ -104,22 +105,6 @@ export class MediaLibraryDialogElement extends DialogBaseElement {
return oneAnimationSelected && skeletonSelected;
}
override connectedCallback(): void {
this._sceneData?.addEventListener("property-changes", this.onProviderChanges);
this.loadResults();
super.connectedCallback();
}
override disconnectedCallback(): void {
this._sceneData?.removeEventListener("property-changes", this.onProviderChanges);
super.disconnectedCallback();
}
onProviderChanges = ({ detail: changes }: CustomEvent<PropertyValues<SceneDataProvider>>) => {
if (changes.has("selectedObject"))
this.requestUpdate();
}
protected override willUpdate(changes: PropertyValues<this>): void {
if (changes.has("_activeCategory") || changes.has("_searchTerm")) {
this._filters = [
+4 -18
View File
@@ -1,6 +1,6 @@
import { inject } from "@storyteller/framework";
import { inject, observe } from "@storyteller/framework";
import * as studio from "@storyteller/studio";
import { LitElement, type PropertyValues, html, unsafeCSS } from "lit";
import { LitElement, html, unsafeCSS } from "lit";
import { customElement } from "lit/decorators.js";
import { SCENE_DATA_PROVIDER, type SceneDataProvider } from "./scene-data-provider.element";
@@ -13,23 +13,9 @@ import styles from "./scene-hierarchy.element.scss?inline";
export class SceneHierarchyElement extends LitElement {
static override styles = unsafeCSS(styles);
@observe(["tree"])
@inject(SCENE_DATA_PROVIDER)
_sceneData?: SceneDataProvider;
override connectedCallback(): void {
this._sceneData?.addEventListener("property-changes", this.onProviderChanges);
super.connectedCallback();
}
override disconnectedCallback(): void {
this._sceneData?.removeEventListener("property-changes", this.onProviderChanges);
super.disconnectedCallback();
}
onProviderChanges = ({ detail: changes }: CustomEvent<PropertyValues<SceneDataProvider>>) => {
if (changes.has("tree"))
this.requestUpdate();
}
_sceneData!: SceneDataProvider;
protected override render = () => html`
${this._sceneData?.tree.map(obj => html`
+31 -12
View File
@@ -1,6 +1,9 @@
import { UniqueToken } from "@storyteller/framework";
import { StudioMode } from "@storyteller/studio";
import { match } from "@storyteller/utility";
export const STUDIO_PARAMS = UniqueToken.create<StudioParams>();
/**
* A thin wrapper around
* {@linkcode https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams URLSearchParams}
@@ -26,42 +29,37 @@ import { match } from "@storyteller/utility";
* - **remote://&lt;media-token&gt;.&lt;file-extension&gt;** - A token for a
* media file that can be fetched from the Storyteller.ai API
*/
export class StudioURLParams {
#_nativeParams?: URLSearchParams;
get #nativeParams() {
return this.#_nativeParams ??= new URLSearchParams(document.location.search);
}
export interface StudioParams {
/**
* Initialize the application from an asset in the "bundled" asset library.
* Takes a filename, including the extension, that can be found under
* `studio/assets/gltf` (e.g., "base-human-female.gltf").
*/
get objectId() { return this.#nativeParams.get("objectId") ?? undefined; }
readonly objectId?: string;
/**
* Initialize the application by retargeting a BVH animation (currently only
* supports the MocapNET skeleton) onto the bundled `Mannequin.gltf` model.
*/
get bvh() { return this.#nativeParams.get("bvh") ?? undefined; }
readonly bvh?: string;
/**
* Initialize the application by retargeting a Mixamo animation onto the
* bundled `Mannequin.gltf` model.
*/
get mixamo() { return this.#nativeParams.get("mixamo") ?? undefined; }
readonly mixamo?: string;
/**
* Initialize the application by loading an arbitrary glTF scene from any
* source.
*/
get sceneImport() { return this.#nativeParams.get("sceneImport") ?? undefined; }
readonly sceneImport?: string;
/**
* Initialize the application by loading a previously saved Storyteller
* Studio scene in `*.scn.ron` format.
*/
get scene() { return this.#nativeParams.get("scene") ?? undefined; }
readonly scene?: string;
/**
* One of:
@@ -70,7 +68,7 @@ export class StudioURLParams {
* "specular" qualifiers (e.g., "gum_trees_4k").
* - A hex-formatted color, without the leading `#` (e.g., "1A1A27")
*/
get skybox() { return this.#nativeParams.get("skybox") ?? undefined; }
readonly skybox?: string;
/**
* Determines whether the app initializes in a minimal, read-only "viewer"
@@ -82,6 +80,27 @@ export class StudioURLParams {
*
* @default StudioMode.Viewer
*/
readonly mode: StudioMode;
}
export class StudioURLParams implements StudioParams {
#_nativeParams?: URLSearchParams;
get #nativeParams() {
return this.#_nativeParams ??= new URLSearchParams(document.location.search);
}
get objectId() { return this.#nativeParams.get("objectId") ?? undefined; }
get bvh() { return this.#nativeParams.get("bvh") ?? undefined; }
get mixamo() { return this.#nativeParams.get("mixamo") ?? undefined; }
get sceneImport() { return this.#nativeParams.get("sceneImport") ?? undefined; }
get scene() { return this.#nativeParams.get("scene") ?? undefined; }
get skybox() { return this.#nativeParams.get("skybox") ?? undefined; }
get mode(): StudioMode {
return match(this.#nativeParams.get("mode"), {
"studio": () => StudioMode.Editor,