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
- You have completed the Building Your First Plugin guide.
- You understand the basics of Statements and Objects.
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
- Interaction: The user clicks in the 3D viewport.
- Overlay:
MarkerOverlaydetects the click and calculates the 3D position. - Communication: The position is sent to the
MarkerToolvia theToolCtxinbox. - Logic:
MarkerToolreceives the position and builds aSpawnMarkerMutation. - Execution: The mutation is committed to the Runtime.
- 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.