From 5f62a4024b8b1f87ba01435b731dd86dd252b0d9 Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Thu, 18 Jan 2024 13:56:46 -0500 Subject: [PATCH] `StudioMode` switch for full-editor capabilities (#23) This PR adds a `StudioMode` resource to toggle editing capabilities, and configures the frontend application to read the desired mode from a URL parameter. For now, setting the app to `StudioMode::Viewer` with `?mode=viewer` hides the Transform Toolbar and disables most of the interactive features of the app, but otherwise exhibits the same behavior as `StudioMode::Editor` / `?mode=studio`. In the future, the "Viewer" and "Editor" modes will receive different initialization inputs to better serve the needs of their use cases. In particular: * The "Viewer" mode will accept a BVH animation handle, which should be played in a loop on a simple mannequin asset, allowing a user to preview a given animation before using it within the full studio workflow. * The "Editor" mode will accept a full Bevy scene handle which can be edited and saved to the user's account. These scenes will serve as the basis for the generative-AI workflows Storyteller Studio is ultimately seeking to facilitate. * It will likely be useful for "Editor" mode to alternatively accept only a BVH animation handle, which would be used to initialize a new scene for editing similar to "Viewer" mode, but with editing capabilities enabled. --- .gitignore | 3 +- studio-frontend/src/app/app.element.ts | 36 ++++++-- studio-web/src/lib/viewer.element.ts | 16 +++- studio/src/debug.rs | 6 +- studio/src/input.rs | 4 +- studio/src/lib.rs | 115 +++++++++++++++++++++---- 6 files changed, 148 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 2857433..d453397 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ node_modules # misc /.sass-cache +/.nx /connect.lock /coverage /libpeerconnection.log @@ -37,4 +38,4 @@ testem.log # System Files .DS_Store Thumbs.db -/target \ No newline at end of file +/target diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index a2ee738..dc965b5 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -9,6 +9,7 @@ import { TransformSpace, TransformType, SceneState, + StudioMode, } from "@storyteller/studio"; import { CanvasProvider } from "@storyteller/studio-web/canvas-provider"; import { match } from "@storyteller/utility"; @@ -43,6 +44,8 @@ export class AppElement extends LitElement { @state() sceneObjects: SceneObject[] = []; @state() selection?: string | null; + @state() mode?: StudioMode; + #sceneObjects: studio.SceneObject[] = []; #entityMap = new Map(); #canvasProvider: Ref = createRef(); @@ -54,6 +57,18 @@ export class AppElement extends LitElement { return document.getElementById(provider.canvasId) as HTMLCanvasElement | null; } + override connectedCallback(): void { + super.connectedCallback(); + + const params = new URLSearchParams(document.location.search); + + this.mode = match (params.get("mode"), { + "studio": () => StudioMode.Editor, + "viewer": () => StudioMode.Viewer, + _: () => StudioMode.Viewer, + }); + } + protected override createRenderRoot(): Element | ShadowRoot { return this; } @@ -169,9 +184,14 @@ export class AppElement extends LitElement { class="app-root" ${ref(this.#canvasProvider as any)} > + ${ + // TODO: See notes in studio-web/src/lib/viewer.element.ts + nothing + } @@ -187,13 +207,15 @@ export class AppElement extends LitElement { - + ${this.mode === StudioMode.Editor ? html` + + ` : nothing}
${this.sceneObjects.map(obj => html` diff --git a/studio-web/src/lib/viewer.element.ts b/studio-web/src/lib/viewer.element.ts index 3ae7944..7ee3aea 100644 --- a/studio-web/src/lib/viewer.element.ts +++ b/studio-web/src/lib/viewer.element.ts @@ -1,5 +1,5 @@ import * as studio from "@storyteller/studio"; -import { ViewportSize } from "@storyteller/studio"; +import { StudioMode, ViewportSize } from "@storyteller/studio"; import { inject } from "@storyteller/framework"; import { LitElement, PropertyValues, html, unsafeCSS } from "lit"; import { customElement, property } from "lit/decorators.js"; @@ -12,8 +12,18 @@ import styles from "./viewer.element.scss?inline"; export class ViewerElement extends LitElement { static override styles = unsafeCSS(styles); + // TODO: `` and `` should be separate element + // types with different inputs. `` should take a BVH + // animation and load a simple mannequin asset playing the given + // animation in a loop, while `` should take a scene + // asset handle and load it with full editing capabilities. + // + // Both elements should share most of the core functionality here, + // which could easily be extracted to a base class for inheritance by + // making `#startViewer` an abstract method. @property() objectId?: string; @property() skyboxId?: string; + @property({ type: Number }) mode?: StudioMode; @inject(CANVAS_PROVIDER) _canvasProvider?: CanvasProvider; @@ -68,15 +78,17 @@ export class ViewerElement extends LitElement { if (!this.#canvas) return; if (!this.objectId) return; if (!this.skyboxId) return; + if (this.mode == null) return; const { width, height } = this.getBoundingClientRect(); try { - studio.startViewer( + studio.start( this.objectId, this.skyboxId, `#${this._canvasProvider!.canvasId}`, "Storyteller Studio", new ViewportSize(width, height), + this.mode, ); } catch (err) { if (!(err as Error).message?.startsWith("Using exceptions for control flow")) diff --git a/studio/src/debug.rs b/studio/src/debug.rs index c1a0d07..2d9b5dd 100644 --- a/studio/src/debug.rs +++ b/studio/src/debug.rs @@ -7,7 +7,7 @@ use bevy_mod_picking::debug::DebugPickingMode; use crate::{ camera_controller::{CameraController, CameraMotionSystem}, input::{Action, InputAction, Lifecycle}, - MainCamera, + is_editor, MainCamera, }; #[derive(Resource, Deref, DerefMut)] @@ -36,7 +36,9 @@ impl Plugin for DebugPlugin { Update, ( toggle_debug_mode, - update_debug_picking_mode.run_if(resource_changed::()), + update_debug_picking_mode + .run_if(is_editor) + .run_if(resource_changed::()), update_gizmo_line_thickness, (draw_world_axes, draw_debug_points) .after(CameraMotionSystem) diff --git a/studio/src/input.rs b/studio/src/input.rs index 65f4370..f507afc 100644 --- a/studio/src/input.rs +++ b/studio/src/input.rs @@ -5,6 +5,8 @@ use bevy::{ }; use space_editor::prelude::space_undo::UndoRedo; +use crate::is_editor; + #[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)] pub struct StudioInputSystem; @@ -31,7 +33,7 @@ impl Plugin for StudioInputPlugin { translate_debug_toggle, translate_dcc_camera_events, translate_fps_camera_events, - send_undo_event, + send_undo_event.run_if(is_editor), ) .in_set(StudioInputSystem) .after(bevy::input::InputSystem) diff --git a/studio/src/lib.rs b/studio/src/lib.rs index 3d7a8b7..eedeab6 100644 --- a/studio/src/lib.rs +++ b/studio/src/lib.rs @@ -1,6 +1,6 @@ #![allow(clippy::type_complexity, clippy::too_many_arguments)] -use bevy::prelude::*; +use bevy::{app::PluginGroupBuilder, prelude::*}; use bevy_mod_picking::DefaultPickingPlugins; use space_editor::prelude::space_undo::UndoPlugin; @@ -40,6 +40,46 @@ mod wasm; #[derive(Component)] pub(crate) struct MainCamera; +#[cfg_attr(feature = "wasm", wasm_bindgen)] +#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug)] +pub enum StudioMode { + Editor, + Viewer, +} + +pub(crate) fn is_editor(r_studio_mode: Res) -> bool { + matches!(*r_studio_mode, StudioMode::Editor) +} + +/// The collection of plugins that should be loaded for both viewer-mode and +/// editor-mode instances of the app. +struct CommonPlugins; +impl PluginGroup for CommonPlugins { + fn build(self) -> PluginGroupBuilder { + PluginGroupBuilder::start::() + .add(CameraControllerPlugin) + .add(DebugPlugin) + .add(InspectorPlugin) + .add(InterpolationPlugin) + .add(ScenePlugin) + .add(SkyboxPlugin) + .add(StudioInputPlugin) + .add(UiLayerPlugin) + .add(WorldGridPlugin) + } +} + +/// The collection of plugins that should be loaded only for editor-mode +/// instances of the app. +struct EditorPlugins; +impl PluginGroup for EditorPlugins { + fn build(self) -> PluginGroupBuilder { + PluginGroupBuilder::start::() + .add(InteractionPlugin) + .add(UndoPlugin) + } +} + #[cfg_attr(feature = "wasm", wasm_bindgen)] pub fn init(viewport_size: ViewportSize) { #[cfg(feature = "wasm")] @@ -116,6 +156,46 @@ pub fn start_viewer( canvas_selector: &str, canvas_alt: &str, viewport_size: ViewportSize, +) { + start_internal( + object_id, + skybox_id, + canvas_selector, + canvas_alt, + viewport_size, + StudioMode::Viewer, + ); +} + +#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = startEditor))] +pub fn start_editor( + object_id: &str, + skybox_id: &str, + canvas_selector: &str, + canvas_alt: &str, + viewport_size: ViewportSize, +) { + start_internal( + object_id, + skybox_id, + canvas_selector, + canvas_alt, + viewport_size, + StudioMode::Editor, + ); +} + +// TODO: These `start_` functiions should be refactored to better support the +// needs of the "reaodnly viewer" vs "full studio editor" use cases. See +// notes in `studio-web/src/lib/viewer.element.ts` for more info. +#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = start))] +pub fn start_internal( + object_id: &str, + skybox_id: &str, + canvas_selector: &str, + canvas_alt: &str, + viewport_size: ViewportSize, + studio_mode: StudioMode, ) { #[cfg(feature = "wasm")] console_error_panic_hook::set_once(); @@ -124,6 +204,9 @@ pub fn start_viewer( let skybox_id = skybox_id.to_string(); let mut app = App::new(); + + app.insert_resource(studio_mode); + app.add_plugins(( #[cfg(feature = "wasm")] viewport::wasm::ViewportPlugin, @@ -136,20 +219,18 @@ pub fn start_viewer( }), ..default() }), - DefaultPickingPlugins, - DebugPlugin, - InteractionPlugin, - InspectorPlugin, - StudioInputPlugin, - InterpolationPlugin, - CameraControllerPlugin, - ScenePlugin, - SkyboxPlugin, - UiLayerPlugin, - WorldGridPlugin, - UndoPlugin, - )) - .add_systems( + CommonPlugins, + )); + + if matches!(studio_mode, StudioMode::Editor) { + app.add_plugins((DefaultPickingPlugins, EditorPlugins)); + + // Only for registering components for undo + use space_editor::prelude::space_undo::AppAutoUndo; + app.auto_reflected_undo::(); + } + + app.add_systems( Startup, move |mut cmd: Commands, mut ew: EventWriter| { cmd.insert_resource(SkyboxId(skybox_id.clone())); @@ -157,10 +238,6 @@ pub fn start_viewer( }, ); - // Only for registering components for undo - use space_editor::prelude::space_undo::AppAutoUndo; - app.auto_reflected_undo::(); - #[cfg(feature = "wasm")] wasm::initialize();