From Viewport Click to Project Mutation

From Viewport Click to Project Mutation

This tutorial walks you through building a “Full Stack” the Foresight SDK feature. We will create a tool that allows a user to click on a point in the 3D viewport and spawn a new versioned object at that location.

Prerequisites

1. Define the Object

First, we define a simple object to represent the spawned point.

#[derive(Clone, Debug, Serialize, Deserialize, FseObject)]
pub struct SpawnedMarker {
    pub position: Vec3,
}

2. Create the Mutation

Next, we create a serializable Mutation that handles the actual addition of the object to the project state.

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SpawnMarkerMutation {
    pub position: Vec3,
}

#[typetag::serde]
impl Mutation for SpawnMarkerMutation {
    fn apply<'a, 'b>(
        &self,
        fse: &'a mut FseFacade<'_>,
    ) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send + 'b>>
    where
        'a: 'b,
    {
        let position = self.position;
        Box::pin(async move {
            let id = fse.new_object::<SpawnedMarker>(())?;
            fse.set_object(id, SpawnedMarker { position })?;
            Ok(())
        })
    }
}

3. Build the Tool

The Tool is responsible for capturing the viewport click and executing the mutation.

pub struct MarkerTool;

impl Tool for MarkerTool {
    type FromViewportOverlay = Vec3; // We expect a Vec3 from the overlay

    async fn run(&self, tool: &mut ToolCtx<Self>) -> anyhow::Result<()> {
        // 1. Set up the viewport overlay to capture clicks
        tool.set_viewport_overlay(MarkerOverlay);

        // 2. Wait for a click (Vec3 position) from the overlay
        while let Ok(click_pos) = tool.overlay_inbox().async_read().await {
            // 3. Commit a transaction containing our mutation
            tool.commit_transaction(|builder, _| {
                async move {
                    builder.push(SpawnMarkerMutation { position: click_pos }).await
                }.boxed()
            }).await?;
        }
        
        Ok(())
    }
}

4. Implement the Viewport Overlay

The overlay handles the low-level interaction with the 3D scene.

struct MarkerOverlay;

impl ViewportOverlayWidget for MarkerOverlay {
    fn show_overlay(&mut self, ctx: &mut Ctx, viewport_ctx: &mut ViewportOverlayCtx, ui: &mut Ui) -> WidgetResponse {
        // Capture mouse clicks and convert them to 3D world positions
        if ui.input(|i| i.pointer.primary_clicked()) {
            if let Some(pos) = viewport_ctx.screen_to_world(ui.input(|i| i.pointer.hover_pos().unwrap())) {
                // Send the position back to the Tool's inbox
                viewport_ctx.send_to_tool(pos);
            }
        }
        ui.response()
    }
}

Summary of the Flow

  1. Interaction: The user clicks in the 3D viewport.
  2. Overlay: MarkerOverlay detects the click and calculates the 3D position.
  3. Communication: The position is sent to the MarkerTool via the ToolCtx inbox.
  4. Logic: MarkerTool receives the position and builds a SpawnMarkerMutation.
  5. Execution: The mutation is committed to the Runtime.
  6. Persistence: The Runtime updates the versioned project state, making the new marker persistent and undoable.

This flow ensures that user actions are decoupled from the authoritative state changes, enabling features like undo/redo, macros, and collaborative editing.