forked from 1b-agent/opencadstudio-upstream
fix(scene): isolate drawing-space state
Cancel stale interaction state across space changes and keep live previews and zoom extents in their active paper/model context.\n\nRefs #539\nRefs #540\nRefs #541
This commit is contained in:
1 parent
8050fc79fd
commit
00cc17ea55
10 files changed
+424
-96
No files matched your search
+135
-3
@@ -4,6 +4,133 @@ use acadrust::Handle;
|
|||||||
use iced::Task;
|
use iced::Task;
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
|
/// Drop cursor-relative state that was computed in the drawing space being
|
||||||
|
/// left. This is also used by MVIEW, whose command object survives its
|
||||||
|
/// intentional paper/model round-trip while its old-space overlays cannot.
|
||||||
|
pub(super) fn reset_space_interaction_state(&mut self) {
|
||||||
|
let i = self.active_tab;
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.last_point = None;
|
||||||
|
self.snapper.from_point = None;
|
||||||
|
self.snapper.clear_tracking();
|
||||||
|
self.otrack_active = None;
|
||||||
|
self.axis_lock_dir = None;
|
||||||
|
self.dyn_user_reshaped = false;
|
||||||
|
self.grip_hover = None;
|
||||||
|
self.grip_popup = None;
|
||||||
|
self.grip_pending = None;
|
||||||
|
self.visibility_popup = None;
|
||||||
|
self.hover_dwell = None;
|
||||||
|
self.ucs_grip_drag = None;
|
||||||
|
self.ucs_icon_selected = false;
|
||||||
|
self.ucs_icon_hover = false;
|
||||||
|
self.tabs[i].pan_mode = false;
|
||||||
|
let _ = self.on_viewport_exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Roll a hot grip back to its pre-drag image and remove every grip-owned
|
||||||
|
/// overlay. Shared by Escape and drawing-space transitions.
|
||||||
|
pub(super) fn cancel_active_grip_edit(&mut self) -> bool {
|
||||||
|
let i = self.active_tab;
|
||||||
|
let had_grip = self.tabs[i].active_grip.take().is_some()
|
||||||
|
|| self.grip_add_provisional.is_some()
|
||||||
|
|| self.grip_preview_handle.is_some();
|
||||||
|
if !had_grip {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Add-Leader arrow being placed is still provisional.
|
||||||
|
if let Some((handle, grip_id)) = self.grip_add_provisional.take() {
|
||||||
|
use crate::entities::traits::EntityTypeOps;
|
||||||
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
||||||
|
entity.apply_grip_menu(
|
||||||
|
grip_id,
|
||||||
|
crate::scene::model::object::GripMenuAction::RemoveLeader,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(handle) = self.grip_preview_handle.take() {
|
||||||
|
if let Some(original) = self.grip_original.take() {
|
||||||
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
||||||
|
*entity = original;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.tabs[i].scene.preview_hidden.remove(&handle);
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
||||||
|
} else {
|
||||||
|
self.grip_original = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.grip_text_verts.clear();
|
||||||
|
self.grip_text_slide = false;
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.refresh_selected_grips();
|
||||||
|
self.refresh_properties();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// End an interactive command before its drawing coordinate context
|
||||||
|
/// changes. This is a full interaction boundary, not only an `active_cmd`
|
||||||
|
/// check: grip drags, suspended editor commands, snaps, tracking, dynamic
|
||||||
|
/// input and pointer gestures all carry coordinates from the old space.
|
||||||
|
pub(super) fn cancel_active_command_for_space_change(&mut self) -> Task<Message> {
|
||||||
|
let i = self.active_tab;
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
let mut cancellation_reported = false;
|
||||||
|
|
||||||
|
if let Some(result) = self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_mut()
|
||||||
|
.map(|command| command.on_space_change())
|
||||||
|
{
|
||||||
|
let (result, message_pending) = match result {
|
||||||
|
CmdResult::Cancel => (CmdResult::CancelForSpaceChange, false),
|
||||||
|
other => (other, true),
|
||||||
|
};
|
||||||
|
tasks.push(self.apply_cmd_result(result));
|
||||||
|
|
||||||
|
// `on_space_change` must be terminal. Force a plain cancellation
|
||||||
|
// if an external/plugin command violates that contract.
|
||||||
|
if self.tabs[i].active_cmd.is_some() {
|
||||||
|
tasks.push(self.apply_cmd_result(CmdResult::CancelForSpaceChange));
|
||||||
|
} else if message_pending {
|
||||||
|
self.command_line
|
||||||
|
.push_info("Command cancelled because the active drawing space changed.");
|
||||||
|
}
|
||||||
|
cancellation_reported = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let grip_cancelled = self.cancel_active_grip_edit();
|
||||||
|
let suspended_cancelled = self.tabs[i].suspended_cmd.take().is_some();
|
||||||
|
let editor_cancelled = self.text_inline.is_some() || self.mtext_editor.is_some();
|
||||||
|
if editor_cancelled {
|
||||||
|
self.text_inline_cancel();
|
||||||
|
self.mtext_cancel();
|
||||||
|
}
|
||||||
|
if !cancellation_reported && (grip_cancelled || suspended_cancelled || editor_cancelled) {
|
||||||
|
self.command_line
|
||||||
|
.push_info("Command cancelled because the active drawing space changed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.command_line.input.clear();
|
||||||
|
self.command_line.autocomplete_cursor = None;
|
||||||
|
self.command_line.close_history();
|
||||||
|
self.reset_space_interaction_state();
|
||||||
|
if tasks.is_empty() {
|
||||||
|
Task::none()
|
||||||
|
} else {
|
||||||
|
Task::batch(tasks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply LIMCHECK/PLIMCHECK to a point before an interactive command
|
/// Apply LIMCHECK/PLIMCHECK to a point before an interactive command
|
||||||
/// consumes it. LIMITS itself must be able to redefine a rectangle beyond
|
/// consumes it. LIMITS itself must be able to redefine a rectangle beyond
|
||||||
/// the old boundary, so it is the sole bypass.
|
/// the old boundary, so it is the sole bypass.
|
||||||
@@ -611,7 +738,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
CmdResult::MviewSwitchLayout(layout) => {
|
CmdResult::MviewSwitchLayout(layout) => {
|
||||||
let task = self.on_layout_switch(layout);
|
let task = self.on_layout_switch_preserving_command(layout);
|
||||||
if let Some(prompt) =
|
if let Some(prompt) =
|
||||||
self.tabs[i].active_cmd.as_ref().map(|command| command.prompt())
|
self.tabs[i].active_cmd.as_ref().map(|command| command.prompt())
|
||||||
{
|
{
|
||||||
@@ -1149,12 +1276,17 @@ impl OpenCADStudio {
|
|||||||
self.command_line.push_info(&p);
|
self.command_line.push_info(&p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CmdResult::Cancel => {
|
cancel @ (CmdResult::Cancel | CmdResult::CancelForSpaceChange) => {
|
||||||
|
let space_changed = matches!(cancel, CmdResult::CancelForSpaceChange);
|
||||||
self.tabs[i].scene.clear_preview_wire();
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
self.restore_pre_cmd_tangent();
|
self.restore_pre_cmd_tangent();
|
||||||
self.command_line.push_info("Command cancelled.");
|
self.command_line.push_info(if space_changed {
|
||||||
|
"Command cancelled because the active drawing space changed."
|
||||||
|
} else {
|
||||||
|
"Command cancelled."
|
||||||
|
});
|
||||||
}
|
}
|
||||||
CmdResult::Relaunch(cmd, handles) => {
|
CmdResult::Relaunch(cmd, handles) => {
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
|
|||||||
@@ -524,46 +524,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
self.command_line.input.clear();
|
self.command_line.input.clear();
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
// A hot grip (click-move-click placement in progress) ends on
|
// A hot grip (click-move-click placement in progress) rolls
|
||||||
// Escape, leaving the entity at its last previewed position.
|
// back to its pre-drag image on Escape.
|
||||||
if self.tabs[self.active_tab].active_grip.take().is_some() {
|
if self.cancel_active_grip_edit() {
|
||||||
// An Add-Leader arrow being placed: Esc removes it again.
|
|
||||||
if let Some((h, gid)) = self.grip_add_provisional.take() {
|
|
||||||
let i = self.active_tab;
|
|
||||||
use crate::entities::traits::EntityTypeOps;
|
|
||||||
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(h) {
|
|
||||||
e.apply_grip_menu(
|
|
||||||
gid,
|
|
||||||
crate::scene::model::object::GripMenuAction::RemoveLeader,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
self.tabs[i]
|
|
||||||
.scene
|
|
||||||
.bump_entities(&[(h, crate::scene::ChangeKind::Modified)]);
|
|
||||||
self.refresh_selected_grips();
|
|
||||||
}
|
|
||||||
// Cancel an in-progress grip drag: restore the edited
|
|
||||||
// entity from its pre-drag backup, un-hide it, re-tessellate
|
|
||||||
// once, and drop the preview.
|
|
||||||
if let Some(h) = self.grip_preview_handle.take() {
|
|
||||||
let i = self.active_tab;
|
|
||||||
self.grip_text_verts = Vec::new();
|
|
||||||
self.grip_text_slide = false;
|
|
||||||
if let Some(orig) = self.grip_original.take() {
|
|
||||||
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(h) {
|
|
||||||
*e = orig;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.tabs[i].scene.preview_hidden.remove(&h);
|
|
||||||
self.tabs[i].scene.clear_preview_wire();
|
|
||||||
// Geometry restored to the backup — re-tessellate just it.
|
|
||||||
self.tabs[i]
|
|
||||||
.scene
|
|
||||||
.bump_entities(&[(h, crate::scene::ChangeKind::Modified)]);
|
|
||||||
self.refresh_selected_grips();
|
|
||||||
}
|
|
||||||
self.tabs[self.active_tab].snap_result = None;
|
|
||||||
self.refresh_properties();
|
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
// Cancel layout rename first, then fall through.
|
// Cancel layout rename first, then fall through.
|
||||||
|
|||||||
+57
-13
@@ -3393,14 +3393,25 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
Message::LayoutDelete(name) => {
|
Message::LayoutDelete(name) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
let deleting_current = self.tabs[i].scene.current_layout == name;
|
||||||
|
let cancel_task = if deleting_current {
|
||||||
|
self.cancel_active_command_for_space_change()
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
};
|
||||||
self.push_undo_snapshot(i, "LAYOUT DEL");
|
self.push_undo_snapshot(i, "LAYOUT DEL");
|
||||||
|
let switch_task = if deleting_current {
|
||||||
|
self.on_layout_switch("Model".to_string())
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
};
|
||||||
if self.tabs[i].scene.delete_layout(&name) {
|
if self.tabs[i].scene.delete_layout(&name) {
|
||||||
self.layout_rename_state = None;
|
self.layout_rename_state = None;
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("Layout \"{name}\" silindi"));
|
.push_output(&format!("Layout \"{name}\" silindi"));
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::batch([cancel_task, switch_task])
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::LayoutRenameStart(name) => {
|
Message::LayoutRenameStart(name) => {
|
||||||
@@ -3520,20 +3531,28 @@ impl OpenCADStudio {
|
|||||||
if name == "Model" {
|
if name == "Model" {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_error("Cannot delete the Model layout.");
|
.push_error("Cannot delete the Model layout.");
|
||||||
|
Task::none()
|
||||||
} else {
|
} else {
|
||||||
|
let deleting_current = self.tabs[i].scene.current_layout == name;
|
||||||
|
let cancel_task = if deleting_current {
|
||||||
|
self.cancel_active_command_for_space_change()
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
};
|
||||||
self.push_undo_snapshot(i, "LAYOUT DELETE");
|
self.push_undo_snapshot(i, "LAYOUT DELETE");
|
||||||
|
let switch_task = if deleting_current {
|
||||||
|
self.on_layout_switch("Model".to_string())
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
};
|
||||||
self.tabs[i].scene.delete_layout(&name);
|
self.tabs[i].scene.delete_layout(&name);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
// Switch to Model if active layout was deleted.
|
|
||||||
if self.tabs[i].scene.current_layout == name {
|
|
||||||
self.tabs[i].scene.set_current_layout("Model".to_string());
|
|
||||||
}
|
|
||||||
self.layout_manager_selected = "Model".to_string();
|
self.layout_manager_selected = "Model".to_string();
|
||||||
self.layout_manager_rename_buf = String::new();
|
self.layout_manager_rename_buf = String::new();
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("Layout '{name}' deleted."));
|
.push_output(&format!("Layout '{name}' deleted."));
|
||||||
|
Task::batch([cancel_task, switch_task])
|
||||||
}
|
}
|
||||||
Task::none()
|
|
||||||
}
|
}
|
||||||
Message::LayoutManagerMoveLeft => {
|
Message::LayoutManagerMoveLeft => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
@@ -3571,12 +3590,13 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::LayoutManagerSetCurrent => {
|
Message::LayoutManagerSetCurrent => {
|
||||||
let i = self.active_tab;
|
|
||||||
let name = self.layout_manager_selected.clone();
|
let name = self.layout_manager_selected.clone();
|
||||||
self.tabs[i].scene.set_current_layout(name.clone());
|
let task = self.on_layout_switch(name.clone());
|
||||||
self.command_line
|
if self.tabs[self.active_tab].scene.current_layout == name {
|
||||||
.push_output(&format!("Switched to layout '{name}'."));
|
self.command_line
|
||||||
Task::none()
|
.push_output(&format!("Switched to layout '{name}'."));
|
||||||
|
}
|
||||||
|
task
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::SetTheme(theme) => {
|
Message::SetTheme(theme) => {
|
||||||
@@ -4084,8 +4104,17 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
Message::EnterViewport(handle) => {
|
Message::EnterViewport(handle) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
let context_changed = self.tabs[i].scene.active_viewport != Some(handle);
|
||||||
|
let cancel_task = if context_changed {
|
||||||
|
self.cancel_active_command_for_space_change()
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
};
|
||||||
let perf = crate::perf::enabled();
|
let perf = crate::perf::enabled();
|
||||||
let total = Instant::now();
|
let total = Instant::now();
|
||||||
|
if context_changed {
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
}
|
||||||
// Clear paper-space selection before entering model space.
|
// Clear paper-space selection before entering model space.
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
self.tabs[i].scene.active_viewport = Some(handle);
|
self.tabs[i].scene.active_viewport = Some(handle);
|
||||||
@@ -4119,11 +4148,23 @@ impl OpenCADStudio {
|
|||||||
handle.value(),
|
handle.value(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Task::none()
|
if context_changed {
|
||||||
|
self.sync_dyn_fields();
|
||||||
|
}
|
||||||
|
cancel_task
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::ExitViewport => {
|
Message::ExitViewport => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
let context_changed = self.tabs[i].scene.active_viewport.is_some();
|
||||||
|
let cancel_task = if context_changed {
|
||||||
|
self.cancel_active_command_for_space_change()
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
};
|
||||||
|
if context_changed {
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
}
|
||||||
// Clear model-space selection before returning to paper space.
|
// Clear model-space selection before returning to paper space.
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
self.tabs[i].scene.active_viewport = None;
|
self.tabs[i].scene.active_viewport = None;
|
||||||
@@ -4133,7 +4174,10 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].refresh_active_ucs();
|
self.tabs[i].refresh_active_ucs();
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
self.command_line.push_output("PSPACE");
|
self.command_line.push_output("PSPACE");
|
||||||
Task::none()
|
if context_changed {
|
||||||
|
self.sync_dyn_fields();
|
||||||
|
}
|
||||||
|
cancel_task
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::MspaceCommand => {
|
Message::MspaceCommand => {
|
||||||
|
|||||||
@@ -1488,7 +1488,7 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn on_viewport_exit(&mut self) -> Task<Message> {
|
pub(crate) fn on_viewport_exit(&mut self) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||||
sel.left_down = false;
|
sel.left_down = false;
|
||||||
@@ -3676,6 +3676,24 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn on_layout_switch(&mut self, name: String) -> Task<Message> {
|
pub(crate) fn on_layout_switch(&mut self, name: String) -> Task<Message> {
|
||||||
|
self.on_layout_switch_inner(name, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MVIEW's "Insert view > New" flow deliberately visits Model space to
|
||||||
|
/// define a view and then returns to its paper layout. This is the only
|
||||||
|
/// layout transition allowed to preserve an active command.
|
||||||
|
pub(crate) fn on_layout_switch_preserving_command(
|
||||||
|
&mut self,
|
||||||
|
name: String,
|
||||||
|
) -> Task<Message> {
|
||||||
|
self.on_layout_switch_inner(name, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_layout_switch_inner(
|
||||||
|
&mut self,
|
||||||
|
name: String,
|
||||||
|
preserve_active_command: bool,
|
||||||
|
) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
if self.tabs[i].is_start {
|
if self.tabs[i].is_start {
|
||||||
self.command_line
|
self.command_line
|
||||||
@@ -3693,6 +3711,25 @@ impl OpenCADStudio {
|
|||||||
let perf = crate::perf::enabled();
|
let perf = crate::perf::enabled();
|
||||||
let perf_total = Instant::now();
|
let perf_total = Instant::now();
|
||||||
let perf_from = self.tabs[i].scene.current_layout.clone();
|
let perf_from = self.tabs[i].scene.current_layout.clone();
|
||||||
|
let context_changed =
|
||||||
|
self.tabs[i].scene.current_layout != name || self.tabs[i].scene.active_viewport.is_some();
|
||||||
|
let preserve_active_command = preserve_active_command
|
||||||
|
&& self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|command| command.name() == "MVIEW");
|
||||||
|
if context_changed {
|
||||||
|
if preserve_active_command {
|
||||||
|
// MVIEW keeps its command-owned step data, but all host-owned
|
||||||
|
// cursor/snap/dynamic-input state belongs to the old space.
|
||||||
|
self.reset_space_interaction_state();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cancel_task = if context_changed && !preserve_active_command {
|
||||||
|
self.cancel_active_command_for_space_change()
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
};
|
||||||
let going_to_paper = name != "Model";
|
let going_to_paper = name != "Model";
|
||||||
// Persist the camera of the layout we're leaving BEFORE switching
|
// Persist the camera of the layout we're leaving BEFORE switching
|
||||||
// so returning to it restores where the user left off (the
|
// so returning to it restores where the user left off (the
|
||||||
@@ -3715,6 +3752,9 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].refresh_active_ucs();
|
self.tabs[i].refresh_active_ucs();
|
||||||
self.tabs[i].scene.restore_saved_camera();
|
self.tabs[i].scene.restore_saved_camera();
|
||||||
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
|
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
|
||||||
|
// `deselect_all` invalidates the scene highlight, but grips and the
|
||||||
|
// Properties panel are separate caches owned by the app.
|
||||||
|
self.refresh_properties();
|
||||||
// Grid/snap are per-view: load the layout we just entered (its
|
// Grid/snap are per-view: load the layout we just entered (its
|
||||||
// sheet viewport in paper space, the model tile in model space)
|
// sheet viewport in paper space, the model tile in model space)
|
||||||
// so model and each layout keep independent grid state.
|
// so model and each layout keep independent grid state.
|
||||||
@@ -3744,7 +3784,10 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].scene.geometry_epoch,
|
self.tabs[i].scene.geometry_epoch,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Task::none()
|
if context_changed {
|
||||||
|
self.sync_dyn_fields();
|
||||||
|
}
|
||||||
|
cancel_task
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn on_layout_create(&mut self) -> Task<Message> {
|
pub(super) fn on_layout_create(&mut self) -> Task<Message> {
|
||||||
@@ -3754,6 +3797,7 @@ impl OpenCADStudio {
|
|||||||
.push_info("Open or create a drawing to add a layout.");
|
.push_info("Open or create a drawing to add a layout.");
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
let cancel_task = self.cancel_active_command_for_space_change();
|
||||||
// Find a unique name (e.g. Layout2, Layout3, ...).
|
// Find a unique name (e.g. Layout2, Layout3, ...).
|
||||||
let existing = self.tabs[i].scene.layout_names();
|
let existing = self.tabs[i].scene.layout_names();
|
||||||
let mut idx = existing.len();
|
let mut idx = existing.len();
|
||||||
@@ -3779,22 +3823,22 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.set_current_layout(new_name.clone());
|
|
||||||
// Safety net — `add_layout` already creates the overall
|
// Safety net — `add_layout` already creates the overall
|
||||||
// sheet viewport; this covers any path that doesn't.
|
// sheet viewport; this covers any path that doesn't.
|
||||||
self.tabs[i].scene.ensure_sheet_viewport(&new_name);
|
self.tabs[i].scene.ensure_sheet_viewport(&new_name);
|
||||||
self.tabs[i].scene.deselect_all();
|
let switch_task = self.on_layout_switch(new_name.clone());
|
||||||
self.tabs[i].scene.fit_all();
|
self.tabs[i].scene.fit_all();
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"Layout \"{new_name}\" created — use MVIEW to add a viewport"
|
"Layout \"{new_name}\" created — use MVIEW to add a viewport"
|
||||||
));
|
));
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
return Task::batch([cancel_task, switch_task]);
|
||||||
}
|
}
|
||||||
Err(e) => self
|
Err(e) => self
|
||||||
.command_line
|
.command_line
|
||||||
.push_error(&format!("Failed to create layout: {e}")),
|
.push_error(&format!("Failed to create layout: {e}")),
|
||||||
}
|
}
|
||||||
Task::none()
|
cancel_task
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn on_layout_rename_commit(&mut self) -> Task<Message> {
|
pub(super) fn on_layout_rename_commit(&mut self) -> Task<Message> {
|
||||||
|
|||||||
@@ -848,6 +848,9 @@ pub enum CmdResult {
|
|||||||
ReplaceMany(Vec<(Handle, Vec<EntityType>)>, Vec<EntityType>),
|
ReplaceMany(Vec<(Handle, Vec<EntityType>)>, Vec<EntityType>),
|
||||||
/// Cancel: discard any preview and end the command.
|
/// Cancel: discard any preview and end the command.
|
||||||
Cancel,
|
Cancel,
|
||||||
|
/// Cancel because the active drawing space changed. Cleanup is identical
|
||||||
|
/// to `Cancel`, but the host reports the context change explicitly.
|
||||||
|
CancelForSpaceChange,
|
||||||
/// End the selection-gather phase and re-dispatch the named command
|
/// End the selection-gather phase and re-dispatch the named command
|
||||||
/// with the gathered handles installed as the active scene selection.
|
/// with the gathered handles installed as the active scene selection.
|
||||||
Relaunch(String, Vec<Handle>),
|
Relaunch(String, Vec<Handle>),
|
||||||
@@ -1279,6 +1282,15 @@ pub trait CadCommand: Send {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Abort because the active drawing coordinate space is about to change.
|
||||||
|
/// Unlike `on_escape`, the default never interprets cancellation as
|
||||||
|
/// "finish with the points collected so far" (SPLINE does that for a
|
||||||
|
/// deliberate Escape). Commands owning a live document entity can
|
||||||
|
/// override this to close that entity's deferred history safely.
|
||||||
|
fn on_space_change(&mut self) -> CmdResult {
|
||||||
|
CmdResult::Cancel
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns `true` when the command needs entity picking (hit-test) instead of point picking.
|
/// Returns `true` when the command needs entity picking (hit-test) instead of point picking.
|
||||||
fn needs_entity_pick(&self) -> bool {
|
fn needs_entity_pick(&self) -> bool {
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -362,6 +362,13 @@ impl CadCommand for PlineCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_space_change(&mut self) -> CmdResult {
|
||||||
|
match self.live_handle {
|
||||||
|
Some(handle) => CmdResult::FinalizeLiveEntity(handle),
|
||||||
|
None => CmdResult::Cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn wants_text_input(&self) -> bool {
|
||||||
// Accept A / L / C once we have at least the first point.
|
// Accept A / L / C once we have at least the first point.
|
||||||
!self.vertices.is_empty()
|
!self.vertices.is_empty()
|
||||||
|
|||||||
+101
-3
@@ -655,7 +655,108 @@ impl Scene {
|
|||||||
self.camera.borrow_mut().fit_depth_to_bounds(min, max);
|
self.camera.borrow_mut().fit_depth_to_bounds(min, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fit_paper_space_extents(&mut self) {
|
||||||
|
let layout_block = self.current_layout_block_handle();
|
||||||
|
let mut min = glam::Vec3::splat(f32::INFINITY);
|
||||||
|
let mut max = glam::Vec3::splat(f32::NEG_INFINITY);
|
||||||
|
{
|
||||||
|
let mut include = |x: f64, y: f64, z: f64| {
|
||||||
|
const SANE_EXTENT: f64 = 1.0e16;
|
||||||
|
if x.is_finite()
|
||||||
|
&& y.is_finite()
|
||||||
|
&& z.is_finite()
|
||||||
|
&& x.abs() < SANE_EXTENT
|
||||||
|
&& y.abs() < SANE_EXTENT
|
||||||
|
&& z.abs() < SANE_EXTENT
|
||||||
|
{
|
||||||
|
let point = glam::Vec3::new(x as f32, y as f32, z as f32);
|
||||||
|
min = min.min(point);
|
||||||
|
max = max.max(point);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// The physical sheet is always part of Paper Space extents, even
|
||||||
|
// when the layout contains no entities (#539).
|
||||||
|
if let Some(((x0, y0), (x1, y1))) = self.paper_limits() {
|
||||||
|
include(x0, y0, 0.0);
|
||||||
|
include(x1, y1, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paper entities and viewport borders belong to the sheet. Model
|
||||||
|
// content projected through those viewports deliberately does not.
|
||||||
|
for wire in self.wires_for_block_culled(layout_block, None, None, None, None) {
|
||||||
|
let is_infinite = Self::handle_from_wire_name(&wire.name)
|
||||||
|
.and_then(|handle| self.document.get_entity(handle))
|
||||||
|
.is_some_and(|entity| {
|
||||||
|
matches!(entity, EntityType::XLine(_) | EntityType::Ray(_))
|
||||||
|
});
|
||||||
|
if is_infinite {
|
||||||
|
for &[x, y, z] in &wire.key_vertices {
|
||||||
|
include(x, y, z);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (index, &[x, y, z]) in wire.points.iter().enumerate() {
|
||||||
|
let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
|
||||||
|
include(
|
||||||
|
x as f64 + low[0] as f64,
|
||||||
|
y as f64 + low[1] as f64,
|
||||||
|
z as f64 + low[2] as f64,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hatches, wipeouts and raster images are GPU-only and may carry
|
||||||
|
// no normal wire outline, so include their visible paper bounds.
|
||||||
|
let (hatches, wipeouts, images) = self.paper_sheet_render_models();
|
||||||
|
for hatch in hatches.iter().chain(wipeouts.iter()) {
|
||||||
|
for &[x, y] in hatch.boundary.iter() {
|
||||||
|
include(
|
||||||
|
hatch.world_origin[0] + x as f64,
|
||||||
|
hatch.world_origin[1] + y as f64,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for image in images.iter() {
|
||||||
|
for (corner, low) in image.corners.iter().zip(image.corners_low.iter()) {
|
||||||
|
include(
|
||||||
|
corner[0] as f64 + low[0] as f64,
|
||||||
|
corner[1] as f64 + low[1] as f64,
|
||||||
|
corner[2] as f64 + low[2] as f64,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !min.is_finite() || !max.is_finite() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if min == max {
|
||||||
|
max += glam::Vec3::splat(1.0);
|
||||||
|
}
|
||||||
|
self.camera
|
||||||
|
.borrow_mut()
|
||||||
|
.fit_to_bounds(min, max, self.last_render_aspect.get().max(0.01));
|
||||||
|
self.camera_generation += 1;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn fit_all(&mut self) {
|
pub fn fit_all(&mut self) {
|
||||||
|
if self.active_viewport.is_some() {
|
||||||
|
if let Some((mut min, mut max)) = self.model_space_extents() {
|
||||||
|
if min == max {
|
||||||
|
max += glam::Vec3::splat(1.0);
|
||||||
|
min -= glam::Vec3::splat(1.0);
|
||||||
|
}
|
||||||
|
self.fit_active_viewport_to_bounds(min, max);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.current_layout != "Model" {
|
||||||
|
self.fit_paper_space_extents();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Use the FULL, un-culled wire set — not `entity_wires()`, which is
|
// Use the FULL, un-culled wire set — not `entity_wires()`, which is
|
||||||
// frustum-culled to the current view. Culled input would fit only the
|
// frustum-culled to the current view. Culled input would fit only the
|
||||||
// entities already on screen, so each call would zoom out a little and
|
// entities already on screen, so each call would zoom out a little and
|
||||||
@@ -664,9 +765,6 @@ impl Scene {
|
|||||||
// the bounds don't drift with zoom-adaptive curve sampling.
|
// the bounds don't drift with zoom-adaptive curve sampling.
|
||||||
let layout_block = self.current_layout_block_handle();
|
let layout_block = self.current_layout_block_handle();
|
||||||
let mut wires = self.wires_for_block_culled(layout_block, None, None, None, None);
|
let mut wires = self.wires_for_block_culled(layout_block, None, None, None, None);
|
||||||
if self.current_layout != "Model" {
|
|
||||||
wires.extend(self.viewport_content_wires(layout_block, None, None));
|
|
||||||
}
|
|
||||||
// Ray / XLine tessellate as ±DISPLAY_EXTENT display segments
|
// Ray / XLine tessellate as ±DISPLAY_EXTENT display segments
|
||||||
// (entities/ray.rs) — their endpoints are rendering artifacts, not
|
// (entities/ray.rs) — their endpoints are rendering artifacts, not
|
||||||
// drawing extent. A construction line through the drawing defeats
|
// drawing extent. A construction line through the drawing defeats
|
||||||
|
|||||||
+2
-30
@@ -132,36 +132,8 @@ impl Scene {
|
|||||||
let max = glam::Vec3::new(limit_max.x as f32, limit_max.y as f32, 0.0);
|
let max = glam::Vec3::new(limit_max.x as f32, limit_max.y as f32, 0.0);
|
||||||
|
|
||||||
// MSPACE owns a camera encoded on the active viewport entity.
|
// MSPACE owns a camera encoded on the active viewport entity.
|
||||||
if let Some(viewport_handle) = self.active_viewport {
|
if self.active_viewport.is_some() {
|
||||||
let (width, height, locked) = match self.document.get_entity(viewport_handle) {
|
self.fit_active_viewport_to_bounds(min, max);
|
||||||
Some(EntityType::Viewport(viewport)) => {
|
|
||||||
(viewport.width, viewport.height, viewport.status.locked)
|
|
||||||
}
|
|
||||||
_ => return,
|
|
||||||
};
|
|
||||||
if locked {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let aspect = (width / height.max(1e-9)) as f32;
|
|
||||||
let mut camera = match self.viewport_edit_frame(self.selection.borrow().vp_size) {
|
|
||||||
Some((camera, _)) => camera,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
camera.fit_to_bounds(min, max, aspect.max(0.01));
|
|
||||||
if let Some(EntityType::Viewport(viewport)) =
|
|
||||||
self.document.get_entity_mut(viewport_handle)
|
|
||||||
{
|
|
||||||
viewport.view_target.x = camera.target.x;
|
|
||||||
viewport.view_target.y = camera.target.y;
|
|
||||||
viewport.view_target.z = camera.target.z;
|
|
||||||
viewport.view_center.x = 0.0;
|
|
||||||
viewport.view_center.y = 0.0;
|
|
||||||
viewport.view_height = camera.ortho_size() as f64 * 2.0;
|
|
||||||
if viewport.view_height > 1e-9 {
|
|
||||||
viewport.custom_scale = viewport.height / viewport.view_height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.camera_generation += 1;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,46 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fit model-space bounds into the active floating viewport without moving
|
||||||
|
/// the surrounding paper-space camera.
|
||||||
|
pub fn fit_active_viewport_to_bounds(
|
||||||
|
&mut self,
|
||||||
|
min: glam::Vec3,
|
||||||
|
max: glam::Vec3,
|
||||||
|
) -> bool {
|
||||||
|
let Some(viewport_handle) = self.active_viewport else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let (width, height, locked) = match self.document.get_entity(viewport_handle) {
|
||||||
|
Some(acadrust::EntityType::Viewport(viewport)) => {
|
||||||
|
(viewport.width, viewport.height, viewport.status.locked)
|
||||||
|
}
|
||||||
|
_ => return false,
|
||||||
|
};
|
||||||
|
if locked {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some(mut camera) = self.camera_for_viewport(viewport_handle) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
camera.fit_to_bounds(min, max, (width / height.max(1e-9)) as f32);
|
||||||
|
if let Some(acadrust::EntityType::Viewport(viewport)) =
|
||||||
|
self.document.get_entity_mut(viewport_handle)
|
||||||
|
{
|
||||||
|
viewport.view_target.x = camera.target.x;
|
||||||
|
viewport.view_target.y = camera.target.y;
|
||||||
|
viewport.view_target.z = camera.target.z;
|
||||||
|
viewport.view_center.x = 0.0;
|
||||||
|
viewport.view_center.y = 0.0;
|
||||||
|
viewport.view_height = camera.ortho_size() as f64 * 2.0;
|
||||||
|
if viewport.view_height > 1e-9 {
|
||||||
|
viewport.custom_scale = viewport.height / viewport.view_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.camera_generation += 1;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// Pan the active viewport's model-space view by `(screen_dx, screen_dy)` pixels.
|
/// Pan the active viewport's model-space view by `(screen_dx, screen_dy)` pixels.
|
||||||
/// The delta is converted to model-space units using the camera and viewport scale.
|
/// The delta is converted to model-space units using the camera and viewport scale.
|
||||||
/// No-op when there is no active viewport.
|
/// No-op when there is no active viewport.
|
||||||
|
|||||||
@@ -1991,7 +1991,21 @@ impl Scene {
|
|||||||
// per-frame buffer so the (potentially huge) base buffer stays resident
|
// per-frame buffer so the (potentially huge) base buffer stays resident
|
||||||
// and unchanged while a command preview or grip drag is live.
|
// and unchanged while a command preview or grip drag is live.
|
||||||
let all_wires = other_arc;
|
let all_wires = other_arc;
|
||||||
let preview_wires = if self.interim_wire.is_none() && self.preview_wires.is_empty() {
|
// A live overlay belongs to exactly one drawing context. In a paper
|
||||||
|
// layout, feeding model-space preview coordinates to the full-canvas
|
||||||
|
// sheet pass draws a second copy outside the floating viewport (#540).
|
||||||
|
// The inverse is equally wrong: paper-space coordinates must not be
|
||||||
|
// interpreted by a content viewport's model camera.
|
||||||
|
let show_live_overlay = if self.current_layout == "Model" {
|
||||||
|
inst.active
|
||||||
|
} else if let Some(active_viewport) = self.active_viewport {
|
||||||
|
!inst.paper_sheet && inst.handle == active_viewport
|
||||||
|
} else {
|
||||||
|
inst.paper_sheet
|
||||||
|
};
|
||||||
|
let preview_wires = if !show_live_overlay
|
||||||
|
|| (self.interim_wire.is_none() && self.preview_wires.is_empty())
|
||||||
|
{
|
||||||
Arc::new(Vec::new())
|
Arc::new(Vec::new())
|
||||||
} else {
|
} else {
|
||||||
let mut v: Vec<WireModel> = Vec::with_capacity(self.preview_wires.len() + 1);
|
let mut v: Vec<WireModel> = Vec::with_capacity(self.preview_wires.len() + 1);
|
||||||
@@ -2150,7 +2164,9 @@ impl Scene {
|
|||||||
pv.extend_from_slice(&w.text_verts);
|
pv.extend_from_slice(&w.text_verts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pv.extend_from_slice(&self.preview_text);
|
if show_live_overlay {
|
||||||
|
pv.extend_from_slice(&self.preview_text);
|
||||||
|
}
|
||||||
Arc::new(pv)
|
Arc::new(pv)
|
||||||
};
|
};
|
||||||
// Stable per-viewport identity (tagged so tile / sheet / content /
|
// Stable per-viewport identity (tagged so tile / sheet / content /
|
||||||
|
|||||||
Reference in new issue
Block a user