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 b14bd4b..e867a24 100644 --- a/studio/src/debug.rs +++ b/studio/src/debug.rs @@ -8,7 +8,7 @@ use space_editor::{space_editor_ui::{EditorPluginGroup, EditorPickingPlugin, Edi use crate::{ camera_controller::{CameraController, CameraMotionSystem}, input::{Action, InputAction, Lifecycle}, - MainCamera, interaction::gizmos::camera::GizmoCamera, + 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 e88fcfd..552f402 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 b9b3517..e11d880 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::{ @@ -44,6 +44,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")] @@ -119,6 +159,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(); @@ -127,6 +207,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, @@ -139,19 +222,18 @@ pub fn start_viewer( }), ..default() }), - DefaultPickingPlugins, - DebugPlugin, - InteractionPlugin, - InspectorPlugin, - StudioInputPlugin, - InterpolationPlugin, - CameraControllerPlugin, - ScenePlugin, - SkyboxPlugin, - UiLayerPlugin, - WorldGridPlugin, - )) - .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())); @@ -159,10 +241,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();