forked from 1b-agent/opencadstudio-upstream
Fix: viewport MSPACE activation and navigation
Four bugs prevented viewports from working like AutoCAD model-space windows: 1. Dead-code double-click block: `!is_down2` was always false because `is_down2 = sel.left_down` is captured while the button is still held (before the selection block clears it). Changed to `is_down` so the double-click handler actually fires. 2. Inverted pan direction: pan_active_viewport used `-=` on the delta returned by screen_delta_to_world, which is the same delta that cam.pan *adds* to its target — causing reversed drag. Changed to `+=`. 3. Zoom not cursor-centered: zoom_active_viewport now accepts an optional paper-space cursor position and adjusts view_target so the model point under the cursor stays fixed (same as cam.zoom_about_point). 4. ESC did not exit MSPACE: CommandEscape now calls ExitViewport when active_viewport is set and no command is running. Also added MS/MSPACE and PSPACE command aliases, and MspaceCommand / PspaceCommand messages so MS auto-enters the first viewport. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
1 parent
0b8f9bfa5b
commit
0beb589f2c
4 files changed
+168
-30
No files matched your search
+9
-1
@@ -901,11 +901,19 @@ impl H7CAD {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── MSPACE / PSPACE ───────────────────────────────────────────
|
||||||
|
"MS"|"MSPACE" => {
|
||||||
|
return Task::done(Message::MspaceCommand);
|
||||||
|
}
|
||||||
|
"PSPACE" => {
|
||||||
|
return Task::done(Message::PspaceCommand);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Plot / Page Setup ──────────────────────────────────────────
|
// ── Plot / Page Setup ──────────────────────────────────────────
|
||||||
"PRINT"|"PLOT"|"EXPORT" => {
|
"PRINT"|"PLOT"|"EXPORT" => {
|
||||||
return Task::done(Message::PlotExport);
|
return Task::done(Message::PlotExport);
|
||||||
}
|
}
|
||||||
"PAGESETUP"|"PS" => {
|
"PAGESETUP" => {
|
||||||
if self.tabs[i].scene.current_layout == "Model" {
|
if self.tabs[i].scene.current_layout == "Model" {
|
||||||
self.command_line.push_error("PAGESETUP: switch to a paper space layout first.");
|
self.command_line.push_error("PAGESETUP: switch to a paper space layout first.");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -224,6 +224,10 @@ pub enum Message {
|
|||||||
EnterViewport(acadrust::Handle),
|
EnterViewport(acadrust::Handle),
|
||||||
/// Exit MSPACE and return to paper-space editing (PSPACE).
|
/// Exit MSPACE and return to paper-space editing (PSPACE).
|
||||||
ExitViewport,
|
ExitViewport,
|
||||||
|
/// MS command: enter MSPACE for the first available viewport.
|
||||||
|
MspaceCommand,
|
||||||
|
/// PS command: exit MSPACE (PSPACE).
|
||||||
|
PspaceCommand,
|
||||||
/// Switch to a named layout ("Model" or paper space layout name).
|
/// Switch to a named layout ("Model" or paper space layout name).
|
||||||
LayoutSwitch(String),
|
LayoutSwitch(String),
|
||||||
/// Create a new paper space layout.
|
/// Create a new paper space layout.
|
||||||
|
|||||||
+75
-19
@@ -312,6 +312,9 @@ impl H7CAD {
|
|||||||
if let Some(r) = result {
|
if let Some(r) = result {
|
||||||
return self.apply_cmd_result(r);
|
return self.apply_cmd_result(r);
|
||||||
}
|
}
|
||||||
|
} else if self.tabs[i].scene.active_viewport.is_some() {
|
||||||
|
// ESC while in MSPACE → exit back to paper space.
|
||||||
|
return Task::done(Message::ExitViewport);
|
||||||
} else {
|
} else {
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
@@ -697,9 +700,10 @@ impl H7CAD {
|
|||||||
let (vw, vh) = vp_size;
|
let (vw, vh) = vp_size;
|
||||||
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
||||||
let cam = self.tabs[i].scene.camera.borrow();
|
let cam = self.tabs[i].scene.camera.borrow();
|
||||||
let raw = cam.pick_on_target_plane(p, bounds);
|
let raw_paper = cam.pick_on_target_plane(p, bounds);
|
||||||
let vp_mat = cam.view_proj(bounds);
|
let vp_mat = cam.view_proj(bounds);
|
||||||
drop(cam);
|
drop(cam);
|
||||||
|
let raw = self.tabs[i].scene.paper_to_model(raw_paper);
|
||||||
|
|
||||||
let edited_name = grip.handle.value().to_string();
|
let edited_name = grip.handle.value().to_string();
|
||||||
let all_wires = self.tabs[i].scene.entity_wires();
|
let all_wires = self.tabs[i].scene.entity_wires();
|
||||||
@@ -738,9 +742,12 @@ impl H7CAD {
|
|||||||
let (vw, vh) = vp_size;
|
let (vw, vh) = vp_size;
|
||||||
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
||||||
let cam = self.tabs[i].scene.camera.borrow();
|
let cam = self.tabs[i].scene.camera.borrow();
|
||||||
let cursor_world = cam.pick_on_target_plane(p, bounds);
|
let cursor_paper = cam.pick_on_target_plane(p, bounds);
|
||||||
let view_proj = cam.view_proj(bounds);
|
let view_proj = cam.view_proj(bounds);
|
||||||
drop(cam);
|
drop(cam);
|
||||||
|
// In MSPACE, map paper-space cursor to model space so that
|
||||||
|
// command previews and snapping work in the correct coordinate space.
|
||||||
|
let cursor_world = self.tabs[i].scene.paper_to_model(cursor_paper);
|
||||||
|
|
||||||
let all_wires = self.tabs[i].scene.entity_wires();
|
let all_wires = self.tabs[i].scene.entity_wires();
|
||||||
let needs_tan = self.tabs[i]
|
let needs_tan = self.tabs[i]
|
||||||
@@ -1075,7 +1082,7 @@ impl H7CAD {
|
|||||||
// ── Double-click: enter/exit MSPACE ───────────────────────
|
// ── Double-click: enter/exit MSPACE ───────────────────────
|
||||||
// Only when no command is running, no drag, and we're in paper space.
|
// Only when no command is running, no drag, and we're in paper space.
|
||||||
if is_click
|
if is_click
|
||||||
&& !is_down2
|
&& is_down // ensures there was a matching left-press
|
||||||
&& self.tabs[i].active_cmd.is_none()
|
&& self.tabs[i].active_cmd.is_none()
|
||||||
&& self.tabs[i].scene.current_layout != "Model"
|
&& self.tabs[i].scene.current_layout != "Model"
|
||||||
{
|
{
|
||||||
@@ -1096,22 +1103,42 @@ impl H7CAD {
|
|||||||
if is_double {
|
if is_double {
|
||||||
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
||||||
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
||||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
|
||||||
let all_wires = self.tabs[i].scene.entity_wires();
|
|
||||||
let hit = scene::hit_test::click_hit(p, &all_wires, vp_mat, bounds)
|
|
||||||
.and_then(|s| Scene::handle_from_wire_name(s));
|
|
||||||
|
|
||||||
if let Some(handle) = hit {
|
// 1) Try direct wire hit — works when the border is clicked.
|
||||||
// Double-clicked on a user viewport → enter MSPACE.
|
let hit_vp: Option<acadrust::Handle> = {
|
||||||
if let Some(AcadEntityType::Viewport(vp)) =
|
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||||
self.tabs[i].scene.document.get_entity(handle)
|
let all_wires = self.tabs[i].scene.entity_wires();
|
||||||
{
|
scene::hit_test::click_hit(p, &all_wires, vp_mat, bounds)
|
||||||
if vp.id > 1 {
|
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||||
return Task::done(Message::EnterViewport(handle));
|
.and_then(|h| {
|
||||||
}
|
if let Some(AcadEntityType::Viewport(vp)) =
|
||||||
}
|
self.tabs[i].scene.document.get_entity(h)
|
||||||
|
{
|
||||||
|
if vp.id > 1 { Some(h) } else { None }
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2) Geometric fallback: check if the cursor is inside any
|
||||||
|
// viewport's bounding rectangle in paper space. This handles
|
||||||
|
// double-clicks on model-entity content wires or empty areas.
|
||||||
|
let hit_vp = hit_vp.or_else(|| {
|
||||||
|
let paper_pt = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.camera
|
||||||
|
.borrow()
|
||||||
|
.pick_on_target_plane(p, bounds);
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.viewport_at_paper_point(paper_pt.x, paper_pt.y)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(handle) = hit_vp {
|
||||||
|
return Task::done(Message::EnterViewport(handle));
|
||||||
} else if self.tabs[i].scene.active_viewport.is_some() {
|
} else if self.tabs[i].scene.active_viewport.is_some() {
|
||||||
// Double-clicked on empty area while in MSPACE → exit.
|
// Double-clicked outside all viewports while in MSPACE → exit.
|
||||||
return Task::done(Message::ExitViewport);
|
return Task::done(Message::ExitViewport);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1189,8 +1216,14 @@ impl H7CAD {
|
|||||||
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
||||||
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
||||||
if self.tabs[i].scene.active_viewport.is_some() {
|
if self.tabs[i].scene.active_viewport.is_some() {
|
||||||
// In MSPACE: zoom the active viewport's model-space view.
|
// In MSPACE: zoom the active viewport's model-space view,
|
||||||
self.tabs[i].scene.zoom_active_viewport(s);
|
// keeping the model point under the cursor stationary.
|
||||||
|
let cursor_paper = cursor.map(|cp| {
|
||||||
|
let pt = self.tabs[i].scene.camera.borrow()
|
||||||
|
.pick_on_target_plane(cp, bounds);
|
||||||
|
glam::Vec2::new(pt.x, pt.y)
|
||||||
|
});
|
||||||
|
self.tabs[i].scene.zoom_active_viewport(s, cursor_paper);
|
||||||
} else {
|
} else {
|
||||||
let mut cam = self.tabs[i].scene.camera.borrow_mut();
|
let mut cam = self.tabs[i].scene.camera.borrow_mut();
|
||||||
if let Some(cursor) = cursor {
|
if let Some(cursor) = cursor {
|
||||||
@@ -1658,6 +1691,29 @@ impl H7CAD {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::MspaceCommand => {
|
||||||
|
let i = self.active_tab;
|
||||||
|
if self.tabs[i].scene.current_layout == "Model" {
|
||||||
|
self.command_line.push_error("MS is only available in paper space layouts.");
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
if self.tabs[i].scene.active_viewport.is_some() {
|
||||||
|
// Already in MSPACE — nothing to do.
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
match self.tabs[i].scene.first_user_viewport() {
|
||||||
|
Some(handle) => Task::done(Message::EnterViewport(handle)),
|
||||||
|
None => {
|
||||||
|
self.command_line.push_error("No viewport found in this layout.");
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::PspaceCommand => {
|
||||||
|
Task::done(Message::ExitViewport)
|
||||||
|
}
|
||||||
|
|
||||||
Message::Undo => { self.undo_active_tab(); Task::none() }
|
Message::Undo => { self.undo_active_tab(); Task::none() }
|
||||||
Message::Redo => { self.redo_active_tab(); Task::none() }
|
Message::Redo => { self.redo_active_tab(); Task::none() }
|
||||||
|
|
||||||
|
|||||||
+80
-10
@@ -744,16 +744,20 @@ impl Scene {
|
|||||||
1.0
|
1.0
|
||||||
};
|
};
|
||||||
if scale.abs() < 1e-12 { return; }
|
if scale.abs() < 1e-12 { return; }
|
||||||
// Paper delta → model delta (divide by viewport scale).
|
// screen_delta_to_world returns the same delta that cam.pan() ADDS to its
|
||||||
vp.view_target.x -= (paper_delta.x / scale as f32) as f64;
|
// target, so we add it here too (dividing by viewport scale to convert from
|
||||||
vp.view_target.y -= (paper_delta.y / scale as f32) as f64;
|
// paper-space to model-space). Using -= would invert the drag direction.
|
||||||
|
vp.view_target.x += (paper_delta.x / scale as f32) as f64;
|
||||||
|
vp.view_target.y += (paper_delta.y / scale as f32) as f64;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Zoom the active viewport's model-space view by `steps` notches.
|
/// Zoom the active viewport's model-space view by `steps` notches.
|
||||||
/// Positive = zoom in (increase detail), negative = zoom out.
|
/// Positive = zoom in (increase detail), negative = zoom out.
|
||||||
|
/// `cursor_paper`: optional paper-space XY of the cursor; when supplied the
|
||||||
|
/// model point under the cursor is kept stationary (AutoCAD-style zoom).
|
||||||
/// No-op when there is no active viewport.
|
/// No-op when there is no active viewport.
|
||||||
pub fn zoom_active_viewport(&mut self, steps: f32) {
|
pub fn zoom_active_viewport(&mut self, steps: f32, cursor_paper: Option<glam::Vec2>) {
|
||||||
let vp_handle = match self.active_viewport {
|
let vp_handle = match self.active_viewport {
|
||||||
Some(h) => h,
|
Some(h) => h,
|
||||||
None => return,
|
None => return,
|
||||||
@@ -761,17 +765,83 @@ impl Scene {
|
|||||||
if let Some(acadrust::EntityType::Viewport(vp)) =
|
if let Some(acadrust::EntityType::Viewport(vp)) =
|
||||||
self.document.get_entity_mut(vp_handle)
|
self.document.get_entity_mut(vp_handle)
|
||||||
{
|
{
|
||||||
// Zoom in = shrink view_height → more of the model fits less area.
|
// Zoom in = shrink view_height → higher scale → objects appear larger.
|
||||||
let factor = (1.0_f64 - 0.15 * steps as f64).clamp(0.1, 10.0);
|
let factor = (1.0_f64 - 0.15 * steps as f64).clamp(0.1, 10.0);
|
||||||
let new_height = (vp.view_height * factor).max(1e-6);
|
|
||||||
vp.view_height = new_height;
|
if let Some(cp) = cursor_paper {
|
||||||
// Keep custom_scale in sync.
|
// Compute the model-space point under the cursor before zoom.
|
||||||
if vp.view_height.abs() > 1e-9 {
|
let scale_before = if vp.custom_scale.abs() > 1e-9 {
|
||||||
vp.custom_scale = vp.height / vp.view_height;
|
vp.custom_scale as f32
|
||||||
|
} else if vp.view_height.abs() > 1e-9 {
|
||||||
|
(vp.height / vp.view_height) as f32
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
let cx = vp.center.x as f32;
|
||||||
|
let cy = vp.center.y as f32;
|
||||||
|
let tx = vp.view_target.x as f32;
|
||||||
|
let ty = vp.view_target.y as f32;
|
||||||
|
let mx = (cp.x - cx) / scale_before + tx;
|
||||||
|
let my = (cp.y - cy) / scale_before + ty;
|
||||||
|
|
||||||
|
// Apply zoom.
|
||||||
|
vp.view_height = (vp.view_height * factor).max(1e-6);
|
||||||
|
if vp.view_height.abs() > 1e-9 {
|
||||||
|
vp.custom_scale = vp.height / vp.view_height;
|
||||||
|
}
|
||||||
|
let scale_after = vp.custom_scale as f32;
|
||||||
|
|
||||||
|
// Adjust view_target so the model point under cursor stays there.
|
||||||
|
let mx_after = (cp.x - cx) / scale_after + vp.view_target.x as f32;
|
||||||
|
let my_after = (cp.y - cy) / scale_after + vp.view_target.y as f32;
|
||||||
|
vp.view_target.x += (mx - mx_after) as f64;
|
||||||
|
vp.view_target.y += (my - my_after) as f64;
|
||||||
|
} else {
|
||||||
|
vp.view_height = (vp.view_height * factor).max(1e-6);
|
||||||
|
if vp.view_height.abs() > 1e-9 {
|
||||||
|
vp.custom_scale = vp.height / vp.view_height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the handle of the user viewport whose bounding rectangle contains
|
||||||
|
/// the given paper-space point, or `None` if no viewport matches.
|
||||||
|
pub fn viewport_at_paper_point(&self, px: f32, py: f32) -> Option<Handle> {
|
||||||
|
let layout_block = self.current_layout_block_handle();
|
||||||
|
self.document
|
||||||
|
.entities()
|
||||||
|
.find_map(|e| {
|
||||||
|
let EntityType::Viewport(vp) = e else { return None; };
|
||||||
|
if vp.id <= 1 || vp.common.owner_handle != layout_block || !vp.status.is_on {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let hw = (vp.width / 2.0) as f32;
|
||||||
|
let hh = (vp.height / 2.0) as f32;
|
||||||
|
let cx = vp.center.x as f32;
|
||||||
|
let cy = vp.center.y as f32;
|
||||||
|
if px >= cx - hw && px <= cx + hw && py >= cy - hh && py <= cy + hh {
|
||||||
|
Some(vp.common.handle)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the handle of the first active user viewport in the current layout,
|
||||||
|
/// or `None` if there are none. Used by the MS command.
|
||||||
|
pub fn first_user_viewport(&self) -> Option<Handle> {
|
||||||
|
let layout_block = self.current_layout_block_handle();
|
||||||
|
self.document.entities().find_map(|e| {
|
||||||
|
let EntityType::Viewport(vp) = e else { return None; };
|
||||||
|
if vp.id > 1 && vp.common.owner_handle == layout_block && vp.status.is_on {
|
||||||
|
Some(vp.common.handle)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ── Layout management ─────────────────────────────────────────────────
|
// ── Layout management ─────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Rename a paper-space layout. Updates the Layout object name in the document.
|
/// Rename a paper-space layout. Updates the Layout object name in the document.
|
||||||
|
|||||||
Reference in new issue
Block a user