Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/brush_drag_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,18 @@ pub(crate) fn face_drag_invoke_trigger(
keybind_focus: KeybindFocus,
modal: Res<ModalTransformState>,
draw_state: Res<DrawBrushState>,
vp: ViewportCursor,
mut commands: Commands,
) {
if !mouse.just_pressed(MouseButton::Left) || drag_state.active || drag_state.pending.is_some() {
return;
}

// Only trigger when inside the viewport
if vp.viewport_entity().is_none() {
return;
}

let in_face_edit = matches!(*edit_mode, EditMode::BrushEdit(BrushEditMode::Face));
let shift = keyboard.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]);
let alt = keyboard.any_pressed([KeyCode::AltLeft, KeyCode::AltRight]);
Expand Down Expand Up @@ -686,13 +693,15 @@ pub(crate) fn vertex_drag_invoke_trigger(
edit_mode: Res<EditMode>,
drag_state: Res<VertexDragState>,
keybind_focus: KeybindFocus,
vp: ViewportCursor,
mut commands: Commands,
) {
if !mouse.just_pressed(MouseButton::Left)
|| !matches!(*edit_mode, EditMode::BrushEdit(BrushEditMode::Vertex))
|| drag_state.active
|| drag_state.pending.is_some()
|| keybind_focus.is_typing()
|| vp.viewport_entity().is_none()
{
return;
}
Expand Down Expand Up @@ -1068,13 +1077,15 @@ pub(crate) fn edge_drag_invoke_trigger(
edit_mode: Res<EditMode>,
drag_state: Res<EdgeDragState>,
keybind_focus: KeybindFocus,
vp: ViewportCursor,
mut commands: Commands,
) {
if !mouse.just_pressed(MouseButton::Left)
|| !matches!(*edit_mode, EditMode::BrushEdit(BrushEditMode::Edge))
|| drag_state.active
|| drag_state.pending.is_some()
|| keybind_focus.is_typing()
|| vp.viewport_entity().is_none()
{
return;
}
Expand Down
16 changes: 6 additions & 10 deletions src/core_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,12 @@ fn dispatch_button_operator_call(
.map(|(k, v)| (k.to_string(), v.clone()))
.collect();
commands.queue(move |world: &mut World| {
// If the target is a modal operator, cancel any in-flight
// modal first. Lets the user switch tools (Draw Brush,
// Measure Distance, brush-element drags, terrain sculpt, ...)
// by clicking another toolbar button without reaching for
// Escape, and keeps the second dispatch from failing with
// `ModalAlreadyActive`. Extensions that wire their own
// operators to buttons inherit this behavior for free.
if let Ok(true) = world.operator(id.clone()).is_modal() {
let _ = world.operator("modal.cancel").call();
}
// Treat every toolbar button as a peer of any active tool: cancel
// whatever modal is running first, then dispatch. Without this,
// clicking Object Mode (or any other non-modal mode button) while
// Draw Brush / Measure / etc. owns the modal slot is silently
// blocked by their `is_available` checks.
let _ = world.operator("modal.cancel").call();

let mut call = world.operator(id.clone()).settings(CallOperatorSettings {
execution_context: ExecutionContext::Invoke,
Expand Down
55 changes: 30 additions & 25 deletions src/measure_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,20 +100,40 @@ pub(crate) fn measure_distance(
vp: crate::viewport::ViewportCursor,
mut ray_cast: MeshRayCast,
) -> OperatorResult {
if !state.initialized {
// Viewport capture is deferred so the modal can start from a
// toolbar button click where the cursor is over the toolbar.
state.initialized = true;
state.active = true;
state.has_start = false;
return OperatorResult::Running;
}

if !state.active {
// Confirm triggered finish, clean up and exit the modal.
state.initialized = false;
state.has_start = false;
state.camera = None;
state.viewport = None;
return OperatorResult::Finished;
}

// Capture the viewport the modal was started on; subsequent
// frames stick to it even if the cursor strays elsewhere.
let camera_entity = state.camera.or_else(|| vp.camera_entity());
let viewport_entity = state.viewport.or_else(|| vp.viewport_entity());
let (Some(camera_entity), Some(viewport_entity)) = (camera_entity, viewport_entity) else {
return OperatorResult::Cancelled;
return OperatorResult::Running;
};
if state.camera.is_none() {
state.camera = Some(camera_entity);
}
if state.viewport.is_none() {
state.viewport = Some(viewport_entity);
}
let (camera, cam_tf) = vp.camera_for(camera_entity)?;
let Some((camera, cam_tf)) = vp.camera_for(camera_entity) else {
return OperatorResult::Running;
};

// Try to get a world-space point under the cursor.
let current_point = vp.cursor().and_then(|cursor_pos| {
Expand All @@ -126,26 +146,6 @@ pub(crate) fn measure_distance(
)
});

if !state.initialized {
// First invocation: enter modal mode. Nothing is drawn until the first
// confirm click sets the start point.
let fallback = cam_tf.translation() + cam_tf.forward().as_vec3() * 5.0;
state.initialized = true;
state.active = true;
state.has_start = false;
state.end_point = current_point.unwrap_or(fallback);
return OperatorResult::Running;
}

if !state.active {
// Confirm triggered finish; clean up and exit modal.
state.initialized = false;
state.has_start = false;
state.camera = None;
state.viewport = None;
return OperatorResult::Finished;
}

// Track cursor while waiting for the first click or while measuring.
if let Some(point) = current_point {
state.end_point = point;
Expand All @@ -162,15 +162,20 @@ fn cancel_measure_distance(mut state: ResMut<MeasureToolState>) {
state.viewport = None;
}

fn measure_tool_active(state: Res<MeasureToolState>) -> bool {
state.active
/// Without the viewport check the toolbar click that activates the
/// modal would also be picked up as the first confirm.
fn confirm_measure_available(
state: Res<MeasureToolState>,
vp: crate::viewport::ViewportCursor,
) -> bool {
state.active && vp.viewport_entity().is_some()
}

#[operator(
id = "tools.measure_distance.confirm",
label = "Confirm Measurement",
description = "First click sets the start point, second click finishes",
is_available = measure_tool_active,
is_available = confirm_measure_available,
allows_undo = false,
)]
fn confirm_measure_distance(
Expand Down
14 changes: 13 additions & 1 deletion src/viewport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,14 +234,26 @@ impl UiCursorPos<'_, '_> {
/// Read-only guard resources checked by many interaction systems before acting.
/// If any guard is active, the system should bail early.
#[derive(SystemParam)]
pub(crate) struct InteractionGuards<'w> {
pub(crate) struct InteractionGuards<'w, 's> {
pub gizmo_drag: Res<'w, crate::gizmos::GizmoDragState>,
pub gizmo_hover: Res<'w, crate::gizmos::GizmoHoverState>,
pub modal: Res<'w, crate::modal_transform::ModalTransformState>,
pub viewport_drag: Res<'w, crate::modal_transform::ViewportDragState>,
pub draw_state: Res<'w, crate::draw_brush::DrawBrushState>,
pub edit_mode: Res<'w, crate::brush::EditMode>,
pub terrain_edit_mode: Res<'w, crate::terrain::TerrainEditMode>,
pub active_modal: ActiveModalQuery<'w, 's>,
}

impl InteractionGuards<'_, '_> {
pub fn is_any_interaction_active(&self) -> bool {
self.gizmo_drag.active
|| self.modal.active.is_some()
|| self.viewport_drag.active.is_some()
|| self.draw_state.active.is_some()
|| matches!(*self.edit_mode, crate::brush::EditMode::BrushEdit(_))
|| self.active_modal.is_modal_running()
}
}

/// Tracks whether a right-click fly session started inside the viewport.
Expand Down
46 changes: 11 additions & 35 deletions src/viewport_select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,22 +155,11 @@ pub(crate) fn handle_viewport_click(
let just_finished_draw = *was_drawing && !drawing_now;
*was_drawing = drawing_now;

// Don't select during gizmo drag, modal ops, viewport drag,
// brush edit mode, draw mode, or terrain sculpt mode. Physics
// mode IS allowed: the user needs to click-select entities to
// drag them in the physics tool.
//
// Plain LMB-down still fires here for the immediate click case;
// if the user starts dragging, `box_select_promote_pending`
// dispatches `BoxSelectOp` which then overrides whatever
// selection this handler set on press.
// Physics mode is intentionally not blocked: the user needs to
// click-select entities to drag them in the physics tool.
if !mouse.just_pressed(MouseButton::Left)
|| guards.gizmo_drag.active
|| guards.is_any_interaction_active()
|| guards.gizmo_hover.hovered_axis.is_some()
|| guards.modal.active.is_some()
|| guards.viewport_drag.active.is_some()
|| matches!(*guards.edit_mode, crate::brush::EditMode::BrushEdit(_))
|| drawing_now
|| just_finished_draw
|| matches!(
*guards.terrain_edit_mode,
Expand Down Expand Up @@ -329,20 +318,14 @@ fn box_select_pending_trigger(
face_entities: Query<(Entity, &BrushFaceEntity, &GlobalTransform)>,
brush_caches: Query<&BrushMeshCache>,
) {
// `gizmo_drag.active` doesn't flip until next frame because the
// gizmo invoke-trigger queues its dispatch — `gizmo_hover` covers
// the same-frame case.
if box_state.active
|| box_state.pending.is_some()
|| !mouse.just_pressed(MouseButton::Left)
|| guards.gizmo_drag.active
// The gizmo invoke-trigger queues the drag operator via
// `commands.queue`, so `gizmo_drag.active` doesn't flip until
// after this Update frame ends. Without checking the hover
// state here the press both arms a pending box-select and
// starts the gizmo drag, so the marquee draws while the user
// is dragging the gizmo.
|| guards.is_any_interaction_active()
|| guards.gizmo_hover.hovered_axis.is_some()
|| matches!(*guards.edit_mode, crate::brush::EditMode::BrushEdit(_))
|| guards.draw_state.active.is_some()
|| guards.modal.active.is_some()
{
return;
}
Expand Down Expand Up @@ -401,17 +384,10 @@ fn box_select_promote_pending(
box_state.pending = None;
return;
}
// A modal that started in the same frame as the press (gizmo
// drag, viewport drag, transform shortcut, draw brush) won't have
// shown up in the trigger system's guard check, but it has by
// now. Drop the pending press so we don't dispatch a competing
// box-select on top of the active gesture.
if guards.gizmo_drag.active
|| guards.viewport_drag.active.is_some()
|| guards.modal.active.is_some()
|| guards.draw_state.active.is_some()
|| matches!(*guards.edit_mode, crate::brush::EditMode::BrushEdit(_))
{
// An interaction that started in the same frame as the press
// wouldn't have shown up in the trigger's guard check, but has by
// now. Drop the pending press so we don't fight it.
if guards.is_any_interaction_active() {
box_state.pending = None;
return;
}
Expand Down
Loading