From ba0bab13560da9720a7510657781f8a557360afb Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Tue, 10 Sep 2024 22:55:48 +0300 Subject: [PATCH] editor plugins improvements --- src/code/snippets/src/editor/plugins.rs | 244 ++++++++++++++++++++++-- src/editor/plugins.md | 92 ++++++++- 2 files changed, 320 insertions(+), 16 deletions(-) diff --git a/src/code/snippets/src/editor/plugins.rs b/src/code/snippets/src/editor/plugins.rs index 14b95f04..a3927c21 100644 --- a/src/code/snippets/src/editor/plugins.rs +++ b/src/code/snippets/src/editor/plugins.rs @@ -7,14 +7,21 @@ use fyrox::{ visitor::prelude::*, }, engine::Engine, + graph::BaseSceneGraph, gui::{BuildContext, UiNode}, - scene::node::Node, + scene::{base::BaseBuilder, graph::Graph, node::Node, sprite::SpriteBuilder}, script::ScriptTrait, }; use fyroxed_base::{ - interaction::{gizmo::move_gizmo::MoveGizmo, make_interaction_mode_button, InteractionMode}, + camera::PickingOptions, + command::{CommandContext, CommandTrait}, + interaction::{ + gizmo::move_gizmo::MoveGizmo, make_interaction_mode_button, plane::PlaneKind, + InteractionMode, + }, + message::MessageSender, plugin::EditorPlugin, - scene::{controller::SceneController, GameScene, Selection}, + scene::{commands::GameSceneContext, controller::SceneController, GameScene, Selection}, settings::Settings, Editor, Message, }; @@ -86,12 +93,12 @@ impl EditorPlugin for MyPlugin { // ANCHOR_END: selection_2 // ANCHOR: interaction_mode_create - let move_gizmo = MoveGizmo::new(game_scene, &mut editor.engine); - - entry.interaction_modes.add(MyInteractionMode { - move_gizmo, - drag_context: None, - }); + entry.interaction_modes.add(MyInteractionMode::new( + game_scene, + &mut editor.engine, + editor.message_sender.clone(), + *node_handle, + )); // ANCHOR_END: interaction_mode_create // ANCHOR: selection_3 @@ -106,29 +113,45 @@ impl EditorPlugin for MyPlugin { } // ANCHOR_END: plugin_impl_2 +// ANCHOR: interaction_mode_definition struct DragContext { point_index: usize, initial_position: Vector3, + plane_kind: PlaneKind, } -// ANCHOR: interaction_mode #[derive(TypeUuidProvider)] #[type_uuid(id = "d7f56947-a106-408a-9c18-d0191ef89925")] pub struct MyInteractionMode { move_gizmo: MoveGizmo, + node_handle: Handle, drag_context: Option, + message_sender: MessageSender, + line_points_gizmo: LinePointsGizmo, + selected_point_index: Option, } impl MyInteractionMode { - pub fn new(game_scene: &GameScene, engine: &mut Engine) -> Self { + pub fn new( + game_scene: &GameScene, + engine: &mut Engine, + message_sender: MessageSender, + node_handle: Handle, + ) -> Self { Self { move_gizmo: MoveGizmo::new(game_scene, engine), + node_handle, drag_context: None, + message_sender, + line_points_gizmo: LinePointsGizmo::default(), + selected_point_index: None, } } } +// ANCHOR_END: interaction_mode_definition impl InteractionMode for MyInteractionMode { + // ANCHOR: on_left_mouse_button_down fn on_left_mouse_button_down( &mut self, editor_selection: &Selection, @@ -138,8 +161,47 @@ impl InteractionMode for MyInteractionMode { frame_size: Vector2, settings: &Settings, ) { + let Some(game_scene) = controller.downcast_mut::() else { + return; + }; + + let scene = &mut engine.scenes[game_scene.scene]; + + // Pick scene entity at the cursor position. + if let Some(result) = game_scene.camera_controller.pick( + &scene.graph, + PickingOptions { + cursor_pos: mouse_pos, + editor_only: true, + filter: Some(&mut |handle, _| handle != self.move_gizmo.origin), + ..Default::default() + }, + ) { + // The gizmo needs to be fed with input events as well, so it can react to the cursor. + if let Some(plane_kind) = self.move_gizmo.handle_pick(result.node, &mut scene.graph) { + // Start point dragging if there's any point selected. + if let Some(selected_point_index) = self.selected_point_index { + self.drag_context = Some(DragContext { + point_index: selected_point_index, + initial_position: scene.graph + [self.line_points_gizmo.point_nodes[selected_point_index]] + .global_position(), + plane_kind, + }) + } + } else { + // Handle point picking and remember a selected point. + for (index, point_handle) in self.line_points_gizmo.point_nodes.iter().enumerate() { + if result.node == *point_handle { + self.selected_point_index = Some(index); + } + } + } + } } + // ANCHOR_END: on_left_mouse_button_down + // ANCHOR: on_left_mouse_button_up fn on_left_mouse_button_up( &mut self, editor_selection: &Selection, @@ -149,8 +211,36 @@ impl InteractionMode for MyInteractionMode { frame_size: Vector2, settings: &Settings, ) { + let Some(game_scene) = controller.downcast_mut::() else { + return; + }; + + let scene = &mut engine.scenes[game_scene.scene]; + + if let Some(drag_context) = self.drag_context.take() { + if let Some(script) = scene + .graph + .try_get_script_of_mut::(self.node_handle) + { + // Restore the position of the point and use its new position as the value for + // the command below. + let new_position = std::mem::replace( + &mut script.points[drag_context.point_index], + drag_context.initial_position, + ); + + // Confirm the action by creating respective command. + self.message_sender.do_command(SetPointPositionCommand { + node_handle: self.node_handle, + point_index: drag_context.point_index, + point_position: new_position, + }); + } + } } + // ANCHOR_END: on_left_mouse_button_up + // ANCHOR: on_mouse_move fn on_mouse_move( &mut self, mouse_offset: Vector2, @@ -161,22 +251,152 @@ impl InteractionMode for MyInteractionMode { frame_size: Vector2, settings: &Settings, ) { + let Some(game_scene) = controller.downcast_mut::() else { + return; + }; + + let scene = &mut engine.scenes[game_scene.scene]; + + if let Some(drag_context) = self.drag_context.as_ref() { + let global_offset = self.move_gizmo.calculate_offset( + &scene.graph, + game_scene.camera_controller.camera, + mouse_offset, + mouse_position, + frame_size, + drag_context.plane_kind, + ); + + if let Some(script) = scene + .graph + .try_get_script_of_mut::(self.node_handle) + { + script.points[drag_context.point_index] = + drag_context.initial_position + global_offset; + } + } } + // ANCHOR_END: on_mouse_move + + // ANCHOR: update + fn update( + &mut self, + editor_selection: &Selection, + controller: &mut dyn SceneController, + engine: &mut Engine, + settings: &Settings, + ) { + let Some(game_scene) = controller.downcast_mut::() else { + return; + }; + + let scene = &mut engine.scenes[game_scene.scene]; + + self.line_points_gizmo + .sync_to_model(self.node_handle, game_scene, &mut scene.graph); + } + // ANCHOR_END: update fn deactivate(&mut self, controller: &dyn SceneController, engine: &mut Engine) {} + // ANCHOR: make_button fn make_button(&mut self, ctx: &mut BuildContext, selected: bool) -> Handle { make_interaction_mode_button(ctx, include_bytes!("icon.png"), "Line Edit Mode", selected) } + // ANCHOR_END: make_button fn uuid(&self) -> Uuid { Self::type_uuid() } } -// ANCHOR_END: interaction_mode fn add_my_plugin(editor: &mut Editor) { // ANCHOR: plugin_registration editor.add_editor_plugin(MyPlugin::default()); // ANCHOR_END: plugin_registration } + +// ANCHOR: line_points_gizmo +#[derive(Default)] +struct LinePointsGizmo { + point_nodes: Vec>, +} + +impl LinePointsGizmo { + fn sync_to_model( + &mut self, + node_handle: Handle, + game_scene: &GameScene, + graph: &mut Graph, + ) { + let Some(script) = graph.try_get_script_of::(node_handle) else { + return; + }; + let points = script.points.clone(); + + if self.point_nodes.len() != points.len() { + self.remove_points(graph); + for point in points { + // Point could be represented via sprite - it will always be facing towards editor's + // camera. + let point_node = SpriteBuilder::new(BaseBuilder::new()) + .with_size(0.1) + .build(graph); + + self.point_nodes.push(point_node); + + // Link the sprite with the special scene node - the name of it should clearly state + // its purpose. + graph.link_nodes(point_node, game_scene.editor_objects_root); + } + } + } + + fn remove_points(&mut self, graph: &mut Graph) { + for handle in self.point_nodes.drain(..) { + graph.remove_node(handle); + } + } +} + +// ANCHOR_END: line_points_gizmo + +// ANCHOR: command +#[derive(Debug)] +struct SetPointPositionCommand { + node_handle: Handle, + point_index: usize, + point_position: Vector3, +} + +impl SetPointPositionCommand { + fn swap(&mut self, context: &mut dyn CommandContext) { + // Get typed version of the context, it could also be UiSceneContext for + // UI scenes. + let context = context.get_mut::(); + // Get a reference to the script instance. + let script = context.scene.graph[self.node_handle] + .try_get_script_mut::() + .unwrap(); + // Swap the position of the point with the one stored in the command. + std::mem::swap( + &mut script.points[self.point_index], + &mut self.point_position, + ); + } +} + +impl CommandTrait for SetPointPositionCommand { + fn name(&mut self, context: &dyn CommandContext) -> String { + "Set Point Position".to_owned() + } + + fn execute(&mut self, context: &mut dyn CommandContext) { + self.swap(context) + } + + fn revert(&mut self, context: &mut dyn CommandContext) { + self.swap(context) + } +} +// ANCHOR_END: command diff --git a/src/editor/plugins.md b/src/editor/plugins.md index 94dc4d53..4d1f040a 100644 --- a/src/editor/plugins.md +++ b/src/editor/plugins.md @@ -46,6 +46,13 @@ about other methods. Typical plugin definition could look like this: {{#include ../code/snippets/src/editor/plugins.rs:plugin_impl_2}} ``` +Every plugin must be registered in the editor, it could be done from `editor` crate of your project. Simply add the +following code after editor's initialization: + +```rust +{{#include ../code/snippets/src/editor/plugins.rs:plugin_registration}} +``` + Our plugin will work with scene nodes that has particular script type, and we need to know a handle of object that is suitable for editing via our plugin, this is where `on_message` could be useful: @@ -62,12 +69,45 @@ one of them has our script. Once node selection is done, we can write our own in ## Interaction Modes and Visualization -All interaction with scene nodes should be performed using interaction modes. Interaction mode is tiny abstraction layer, +We need a way to show the points of the line in the scene previewer. The editor uses standard scene nodes for this, and +they all live under a "secret" root node (it is hidden in World Viewer, that's why you can't see it there). The good +approach for visualization is just a custom structure with a few methods: + +```rust +{{#include ../code/snippets/src/editor/plugins.rs:line_points_gizmo}} +``` + +`sync_to_model` method can be called on every frame in `update` method of the interaction mode (see below) - it tracks +the amount of scene nodes representing points of the line and if there's mismatch, it recreates the entire set. +`remove_points` should be used when the gizmo is about to be deleted (usually together with the interaction mode). + +All interaction with scene nodes should be performed using interaction modes. Interaction mode is a tiny abstraction layer, that re-routes input from the scene previewer to the modes. We'll create our own interaction mode that will allow -us to move points of the line. Typical interaction mode looks like this: +us to move points of the line. Every interaction mode must implement `InteractionMode` +[trait](https://docs.rs/fyroxed_base/latest/fyroxed_base/interaction/trait.InteractionMode.html). Unfortunately, the +editor's still mostly undocumented, due to its unstable API. There are quite a lot of methods in this trait: + +- `on_left_mouse_button_down` - called when left mouse button was pressed in the scene viewer. +- `on_left_mouse_button_up` - called when left mouse button was released in the scene viewer. +- `on_mouse_move` - called when mouse cursor moves in the scene viewer. +- `update` - called every frame (only for active mode, inactive modes does are not updated). +- `activate` - called when an interaction mode became active. +- `deactivate` - called when an interaction mode became inactive (i.e. when you're switched to another mode). +- `on_key_down` - called when a key was pressed. +- `on_key_up` - called when a key was released. +- `handle_ui_message` - called when the editor receives a UI message +- `on_drop` - called on every interaction mode before the current scene is destroyed. +- `on_hot_key_pressed` - called when a hotkey was pressed. Could be used to switch sub-modes of interaction mode. +For example, tile map editor has single interaction mode, but the mode itself has draw/erase/pick/etc. sub modes which +could be switched using `Ctrl`/`Alt`/etc. hotkeys. +- `on_hot_key_released` - called when a hotkey was released. +- `make_button` - used to create a button, that will be placed. +- `uuid` - must return type UUID of the mode. + +Every method has its particular use case, but we'll use only a handful of them. Let's create a new interaction mode: ```rust -{{#include ../code/snippets/src/editor/plugins.rs:interaction_mode}} +{{#include ../code/snippets/src/editor/plugins.rs:interaction_mode_definition}} ``` To create an interaction mode all that is needed is to add the following lines in `on_message`, right after @@ -83,7 +123,51 @@ The mode must be deleted when we deselect something else, it could be done on `M {{#include ../code/snippets/src/editor/plugins.rs:gizmo_destroy}} ``` -(TODO) +Now onto the `InteractionMode` trait implementation, let's start by adding implementation for `make_button` method: + +```rust +{{#include ../code/snippets/src/editor/plugins.rs:make_button}} +``` + +There's nothing special about it - it uses built-in function, that creates a button with an image and a tooltip. You +could use any UI widget here that sends `ButtonMessage::Click` messages on interaction. Now onto the `on_left_mouse_button_down` +method: + +```rust +{{#include ../code/snippets/src/editor/plugins.rs:on_left_mouse_button_down}} +``` + +It is responsible for two things: it handles picking of scene nodes at the cursor position, and it is also changes +currently selected point. Additionally, it creates dragging context if one of the axes of the movement gizmo was clicked +and there's some point selected. + +When there's something to drag, we must use new mouse position to determine new location for points in 3D space. There's +`on_mouse_move` for that: + +```rust +{{#include ../code/snippets/src/editor/plugins.rs:on_mouse_move}} +``` + +The dragging could be finished simply by releasing the left mouse button: + +```rust +{{#include ../code/snippets/src/editor/plugins.rs:on_left_mouse_button_up}} +``` + +This is where the action must be "confirmed" - we're creating a new command and sending it for execution in the +command stack of the current scene. The command used in this method could be defined like so: + +```rust +{{#include ../code/snippets/src/editor/plugins.rs:command}} +``` + +See the next section for more info about commands and how they interact with the editor. + +The next step is to update the gizmo on each frame: + +```rust +{{#include ../code/snippets/src/editor/plugins.rs:update}} +``` ## Commands