Skip to content

Commit

Permalink
Add more extensive docs
Browse files Browse the repository at this point in the history
Signed-off-by: Michael X. Grey <mxgrey@intrinsic.ai>
  • Loading branch information
mxgrey committed Aug 28, 2024
1 parent d9ef9b4 commit 423239d
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 11 deletions.
2 changes: 1 addition & 1 deletion rmf_site_editor/src/interaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ impl Plugin for InteractionPlugin {
.add_plugins((CameraControlsPlugin, ModelPreviewPlugin));

if !self.headless {
app.add_plugins(SelectPlugin::default())
app.add_plugins(SelectionPlugin::default())
.add_systems(
Update,
(
Expand Down
63 changes: 54 additions & 9 deletions rmf_site_editor/src/interaction/select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ pub use select_anchor::*;
pub const SELECT_ANCHOR_MODE_LABEL: &'static str = "select_anchor";

#[derive(Default)]
pub struct SelectPlugin {}
pub struct SelectionPlugin {}

impl Plugin for SelectPlugin {
impl Plugin for SelectionPlugin {
fn build(&self, app: &mut App) {
app.configure_sets(
Update,
Expand Down Expand Up @@ -114,31 +114,69 @@ impl Plugin for SelectPlugin {

let inspector_service = app.world.resource::<InspectorService>().inspector_service;
let new_selector_service = app.spawn_event_streaming_service::<RunSelector>(Update);
let select_workflow = app.world.spawn_io_workflow(build_selection_workflow(
let selection_workflow = app.world.spawn_io_workflow(build_selection_workflow(
inspector_service,
new_selector_service,
));

// Get the selection workflow running
app.world.command(|commands| {
commands.request((), select_workflow).detach();
commands.request((), selection_workflow).detach();
});
}
}

/// This builder function creates the high-level selection
/// This builder function creates the high-level workflow that manages "selection"
/// behavior, which is largely driven by mouse interactions. "Selection" behaviors
/// determine how the application responds to the mouse cursor hovering over
/// objects in the scene and what happens when the mouse is clicked.
///
/// The default selection behavior is the "inspector" service which allows the
/// user to select objects in the scene so that their properties get displayed
/// in the inspector panel. The inspector service will automatically be run by
/// this workflow at startup.
///
/// When the user asks for some other type of mouse interaction to begin, such as
/// drawing walls and floors, or placing models in the scene, this workflow will
/// "trim" (stop) the inspector workflow and inject the new requested interaction
/// mode into the workflow. The requested interaction mode is represented by a
/// service specified by the `selector` field of [`RunSelector`]. When that
/// service terminates, this workflow will resume running the inspector service
/// until the user requests some other mouse interaction service to run.
///
/// In most cases downstream users will not need to call this function since the
/// [`SelectionPlugin`] will use this to build and run the default selection
/// workflow. If you are not using the [`SelectionPlugin`] that we provide and
/// want to customize the inspector service, then you could use this to build a
/// customized selection workflow by passingin a custom inspector service.
pub fn build_selection_workflow(
inspector_service: Service<(), ()>,
new_selector_service: Service<(), (), StreamOf<RunSelector>>,
) -> impl FnOnce(Scope<(), ()>, &mut Builder) -> DeliverySettings {
move |scope, builder| {
// This creates a service that will listen to run_service_buffer.
// The job of this service is to atomically pull the most recent item
// out of the buffer and also close the buffer gate to ensure that we
// never have multiple selector services racing to be injected. If we
// don't bother to close the gate after pulling exactly one selection
// service, then it's theoretically possible for multiple selection
// services to get simultaneously injected after the trim operation
// finishes.
let process_new_selector_service = builder
.commands()
.spawn_service(process_new_selector.into_blocking_service());

// The run_service_buffer queues up the most recent RunSelector request
// sent in by the user. That request will be held in this buffer while
// we wait for any ongoing mouse interaction services to cleanly exit
// after we trigger the trim operation.
let run_service_buffer = builder.create_buffer::<RunSelector>(BufferSettings::keep_last(1));
let input = scope.input.fork_clone(builder);
// Run the default inspector service
let inspector = input.clone_chain(builder).then_node(inspector_service);

// Create a node that reads RunSelector events from the world and streams
// them into the workflow.
let new_selector_node = input.clone_chain(builder).then_node(new_selector_service);
builder.connect(new_selector_node.output, scope.terminate);
new_selector_node
Expand All @@ -148,15 +186,24 @@ pub fn build_selection_workflow(
.connect(run_service_buffer.input_slot());

let open_gate = builder.create_gate_open(run_service_buffer);
// Create an operation that trims the gate opening operation, the injected
// selector service, and the default inspector service.
let trim = builder.create_trim([TrimBranch::between(open_gate.input, inspector.input)]);
builder.connect(trim.output, open_gate.input);

// Create a sequence where we listen for updates in the run service buffer,
// then pull an item out of the buffer (if available), then begin the
// trim of all ongoing selection services, then opening the gate of the
// buffer to allow new selection services to be started.
builder
.listen(run_service_buffer)
.then(process_new_selector_service)
.dispose_on_none()
.connect(trim.input);

// After we open the gate it is safe to inject the user-requested selecion
// service. Once that service finishes, we will trigger the inspector to
// resume.
open_gate
.output
.chain(builder)
Expand All @@ -165,6 +212,7 @@ pub fn build_selection_workflow(
.trigger()
.connect(inspector.input);

// This workflow only makes sense to run in serial.
DeliverySettings::Serial
}
}
Expand Down Expand Up @@ -515,10 +563,7 @@ impl Plugin for InspectorServicePlugin {
}
}

pub fn deselect_on_esc(
In(code): In<KeyCode>,
mut select: EventWriter<Select>,
) {
pub fn deselect_on_esc(In(code): In<KeyCode>, mut select: EventWriter<Select>) {
if matches!(code, KeyCode::Escape) {
select.send(Select::new(None));
}
Expand Down
41 changes: 40 additions & 1 deletion rmf_site_editor/src/interaction/select/select_anchor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,49 @@ pub struct HiddenSelectAnchorEntities {
}

/// The first five services should be customized for the State data. The services
/// that return NodeResult should return `Ok(())` if it is okay for the
/// that return [`SelectionNodeResult`] should return `Ok(())` if it is okay for the
/// workflow to continue as normal, and they should return `Err(None)` if it's
/// time for the workflow to terminate as normal. If the workflow needs to
/// terminate because of an error, return `Err(Some(_))`.
///
/// In most cases you should use [`AnchorSelectionHelpers::spawn_anchor_selection_workflow`]
/// instead of running this function yourself directly, unless you know that you
/// need to customize the last four services.
///
/// * `anchor_setup`: This is run once at the start of the workflow to prepare the
/// world to select anchors from the right kind of scope for the request. This
/// is usually just [`anchor_selection_setup`] instantiated for the right type
/// of state.
/// * `state_setup`: This is for any additional custom setup that is relevant to
/// the state information for your selection workflow. This gets run exactly once
/// immediately after `anchor_setup`
/// * `update_preview`: This is run each time a [`Hover`] signal arrives. This
/// is where you should put the logic to update the preview that's being displayed
/// for users.
/// * `update_current`: This is run each time a [`Select`] signal containing `Some`
/// value is sent. This is where you should put the logic to make a persistent
/// (rather than just a preview) modification to the world.
/// * `handle_key_code`: This is where you should put the logic for how your
/// workflow responds to various key codes. For example, should the workflow
/// exit?
/// * `cleanup_state`: This is where you should run anything that's needed to
/// clean up the state of the world after your workflow is finished running.
/// This will be run no matter whether your workflow terminates with a success,
/// terminates with a failure, or cancels prematurely.
///
/// ### The remaining parameters can all be provided by [`AnchorSelectionHelpers`] in most cases:
///
/// * `anchor_cursor_transform`: This service should update the 3D cursor transform.
/// A suitable service for this is available from [`AnchorSelectionHelpers`].
/// * `anchor_select_stream`: This service should produce the [`Hover`] and [`Select`]
/// streams that hook into `update_preview` and `update_current` respectively.
/// A suitable service for this is provided by [`AnchorSelectionHelpers`].
/// * `keyobard_just_pressed`: This service should produce [`KeyCode`] streams
/// when the keyboard gets pressed. A suitable service for this is provided by
/// [`AnchorSelectionHelpers`].
/// * `cleanup_anchor_selection`: This service will run during the cleanup phase
/// and should cleanup any anchor-related modifications to the world. A suitable
/// service for this is provided by [`AnchorSelectionHelpers`].
pub fn build_anchor_selection_workflow<State: 'static + Send + Sync>(
anchor_setup: Service<BufferKey<State>, SelectionNodeResult>,
state_setup: Service<BufferKey<State>, SelectionNodeResult>,
Expand Down

0 comments on commit 423239d

Please sign in to comment.