Feat: PSPACE/MSPACE interaction isolation + paper-space drawing
Selection, snapping and editing are now mode-aware: PSPACE (paper layout, no active viewport) - Only paper-space entities (viewport borders, title blocks, etc.) are hit-testable and selectable. Viewport content wires are invisible to the interaction layer. - Drawing commands add entities to the paper-space layout block so annotations/title blocks land on the sheet, not in model space. MSPACE (active viewport entered) - Only model-space entities visible through the active viewport are hit-testable. Other viewports' content and paper entities are ignored. - Drawing commands add entities to model space (correct DXF block). - Snap results from projected viewport-content wires are converted from paper-space back to model-space via paper_to_model so that placed coordinates and command previews are consistent. Entering/exiting MSPACE clears the current selection so no stale cross-space selection is carried over. Implementation: - Scene::hit_test_wires() returns mode-filtered wires for all hit-tests, snaps, and selections (entity_wires() still returns everything for rendering and PDF export). - viewport_content_wires() gains an Option<Handle> filter to render only the active viewport's content for MSPACE hit-testing. - Scene::add_entity() routes to the paper-layout block or model-space block depending on current_layout / active_viewport. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0beb589f2c
commit
9cee12d1c7
2 changed files with 73 additions and 17 deletions
|
|
@ -706,7 +706,7 @@ impl H7CAD {
|
|||
let raw = self.tabs[i].scene.paper_to_model(raw_paper);
|
||||
|
||||
let edited_name = grip.handle.value().to_string();
|
||||
let all_wires = self.tabs[i].scene.entity_wires();
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let snap_wires: Vec<_> = all_wires
|
||||
.iter()
|
||||
.filter(|w| w.name != edited_name)
|
||||
|
|
@ -749,7 +749,7 @@ impl H7CAD {
|
|||
// 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.hit_test_wires();
|
||||
let needs_tan = self.tabs[i]
|
||||
.active_cmd.as_ref().map(|c| c.needs_tangent_pick()).unwrap_or(false);
|
||||
self.tabs[i].snap_result = if needs_tan {
|
||||
|
|
@ -758,7 +758,11 @@ impl H7CAD {
|
|||
self.snapper.snap(cursor_world, p, &all_wires, view_proj, bounds)
|
||||
};
|
||||
let effective = {
|
||||
let mut pt = self.tabs[i].snap_result.map(|s| s.world).unwrap_or(cursor_world);
|
||||
// snap.world is paper-space for viewport-projected wires; convert
|
||||
// to model-space so previews use consistent coordinates.
|
||||
let mut pt = self.tabs[i].snap_result
|
||||
.map(|s| self.tabs[i].scene.paper_to_model(s.world))
|
||||
.unwrap_or(cursor_world);
|
||||
if self.tabs[i].active_cmd.is_some() { pt.z = 0.0; }
|
||||
if let Some(base) = self.last_point {
|
||||
if self.ortho_mode {
|
||||
|
|
@ -895,7 +899,7 @@ impl H7CAD {
|
|||
// Convert paper-space → model-space when inside a viewport.
|
||||
let raw = self.tabs[i].scene.paper_to_model(raw_paper);
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let all_wires = self.tabs[i].scene.entity_wires();
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let needs_tan = self.tabs[i].active_cmd.as_ref()
|
||||
.map(|c| c.needs_tangent_pick()).unwrap_or(false);
|
||||
let snap_hit = if needs_tan {
|
||||
|
|
@ -903,7 +907,11 @@ impl H7CAD {
|
|||
} else {
|
||||
self.snapper.snap(raw, p, &all_wires, vp_mat, bounds)
|
||||
};
|
||||
let mut pt = snap_hit.map(|s| s.world).unwrap_or(raw);
|
||||
// snap.world is in paper-space (projected wire coords in MSPACE);
|
||||
// convert to model-space so commands receive consistent coordinates.
|
||||
let mut pt = snap_hit
|
||||
.map(|s| self.tabs[i].scene.paper_to_model(s.world))
|
||||
.unwrap_or(raw);
|
||||
pt.z = 0.0;
|
||||
if let Some(base) = self.last_point {
|
||||
if self.ortho_mode {
|
||||
|
|
@ -919,7 +927,7 @@ impl H7CAD {
|
|||
.map(|c| c.needs_entity_pick()).unwrap_or(false)
|
||||
{
|
||||
let vp_mat2 = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let all_wires2 = self.tabs[i].scene.entity_wires();
|
||||
let all_wires2 = self.tabs[i].scene.hit_test_wires();
|
||||
let hit = scene::hit_test::click_hit(p, &all_wires2, vp_mat2, bounds)
|
||||
.and_then(|s| Scene::handle_from_wire_name(s));
|
||||
if let Some(handle) = hit {
|
||||
|
|
@ -976,7 +984,7 @@ impl H7CAD {
|
|||
if elapsed_ms < POLY_START_DELAY_MS {
|
||||
if let Some(a) = box_anchor {
|
||||
let crossing = box_crossing;
|
||||
let all_wires = self.tabs[i].scene.entity_wires();
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let mut handles: Vec<Handle> = scene::hit_test::box_hit(
|
||||
a, p, crossing, &all_wires, vp_mat, bounds,
|
||||
|
|
@ -996,7 +1004,7 @@ impl H7CAD {
|
|||
(sel.poly_points.clone(), sel.poly_crossing)
|
||||
};
|
||||
self.tabs[i].scene.selection.borrow_mut().poly_last_crossing = crossing;
|
||||
let all_wires = self.tabs[i].scene.entity_wires();
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let mut handles: Vec<Handle> = scene::hit_test::poly_hit(
|
||||
&poly_pts, crossing, &all_wires, vp_mat, bounds,
|
||||
|
|
@ -1018,7 +1026,7 @@ impl H7CAD {
|
|||
sel.box_current = None;
|
||||
} else {
|
||||
if box_anchor.is_none() {
|
||||
let all_wires = self.tabs[i].scene.entity_wires();
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let hit = scene::hit_test::click_hit(p, &all_wires, vp_mat, bounds)
|
||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||
|
|
@ -1041,7 +1049,7 @@ impl H7CAD {
|
|||
} else {
|
||||
let a = box_anchor.unwrap();
|
||||
let crossing = box_crossing;
|
||||
let all_wires = self.tabs[i].scene.entity_wires();
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let mut handles: Vec<Handle> = scene::hit_test::box_hit(
|
||||
a, p, crossing, &all_wires, vp_mat, bounds,
|
||||
|
|
@ -1107,7 +1115,7 @@ impl H7CAD {
|
|||
// 1) Try direct wire hit — works when the border is clicked.
|
||||
let hit_vp: Option<acadrust::Handle> = {
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let all_wires = self.tabs[i].scene.entity_wires();
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
scene::hit_test::click_hit(p, &all_wires, vp_mat, bounds)
|
||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||
.and_then(|h| {
|
||||
|
|
@ -1679,13 +1687,19 @@ impl H7CAD {
|
|||
|
||||
Message::EnterViewport(handle) => {
|
||||
let i = self.active_tab;
|
||||
// Clear paper-space selection before entering model space.
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].scene.active_viewport = Some(handle);
|
||||
self.command_line.push_output("MSPACE — viewport entered. Middle-drag/scroll to navigate, double-click outside to exit.");
|
||||
self.refresh_properties();
|
||||
self.command_line.push_output("MSPACE");
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ExitViewport => {
|
||||
let i = self.active_tab;
|
||||
// Clear model-space selection before returning to paper space.
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.refresh_properties();
|
||||
self.tabs[i].scene.active_viewport = None;
|
||||
self.command_line.push_output("PSPACE");
|
||||
Task::none()
|
||||
|
|
@ -1801,7 +1815,7 @@ impl H7CAD {
|
|||
Message::PlotExportPath(None) => Task::none(),
|
||||
Message::PlotExportPath(Some(path)) => {
|
||||
let i = self.active_tab;
|
||||
let wires = self.tabs[i].scene.entity_wires();
|
||||
let wires = self.tabs[i].scene.entity_wires(); // full scene for PDF
|
||||
let (paper_w, paper_h, offset_x, offset_y) =
|
||||
if let Some(((x0, y0), (x1, y1))) = self.tabs[i].scene.paper_limits() {
|
||||
(x1 - x0, y1 - y0, -x0, -y0)
|
||||
|
|
|
|||
|
|
@ -288,11 +288,35 @@ impl Scene {
|
|||
if let Some(((x0, y0), (x1, y1))) = self.paper_limits() {
|
||||
wires.insert(0, paper_boundary_wire(x0 as f32, y0 as f32, x1 as f32, y1 as f32));
|
||||
}
|
||||
wires.extend(self.viewport_content_wires(layout_block));
|
||||
wires.extend(self.viewport_content_wires(layout_block, None));
|
||||
}
|
||||
wires
|
||||
}
|
||||
|
||||
/// Wires that should participate in hit-testing, snapping, and selection.
|
||||
///
|
||||
/// - Model layout: all entity wires (same as entity_wires).
|
||||
/// - PSPACE (paper layout, no active viewport): paper-space entities only —
|
||||
/// viewport content is NOT interactive.
|
||||
/// - MSPACE (active viewport set): model-space content of the active viewport
|
||||
/// only — paper-space entities are NOT interactive.
|
||||
pub fn hit_test_wires(&self) -> Vec<WireModel> {
|
||||
if self.current_layout == "Model" {
|
||||
return self.entity_wires();
|
||||
}
|
||||
let layout_block = self.current_layout_block_handle();
|
||||
match self.active_viewport {
|
||||
None => {
|
||||
// PSPACE: only paper-space entities (viewport borders, title blocks…)
|
||||
self.wires_for_block(layout_block)
|
||||
}
|
||||
Some(vp_handle) => {
|
||||
// MSPACE: only model content visible through the active viewport
|
||||
self.viewport_content_wires(layout_block, Some(vp_handle))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tessellate all non-invisible entities owned by `block_handle`.
|
||||
fn wires_for_block(&self, block_handle: Handle) -> Vec<WireModel> {
|
||||
self.document
|
||||
|
|
@ -534,7 +558,9 @@ impl Scene {
|
|||
vp.view_height = vp.height / fit_scale;
|
||||
}
|
||||
|
||||
fn viewport_content_wires(&self, paper_block: Handle) -> Vec<WireModel> {
|
||||
/// Collect model-space wires projected into paper space for all (or one specific)
|
||||
/// user viewports. `only_vp = Some(h)` restricts output to that viewport.
|
||||
fn viewport_content_wires(&self, paper_block: Handle, only_vp: Option<Handle>) -> Vec<WireModel> {
|
||||
use acadrust::entities::Viewport;
|
||||
use std::collections::HashSet as HSet;
|
||||
|
||||
|
|
@ -544,7 +570,12 @@ impl Scene {
|
|||
.filter_map(|e| {
|
||||
if let EntityType::Viewport(vp) = e { Some(vp) } else { None }
|
||||
})
|
||||
.filter(|vp| vp.id > 1 && vp.common.owner_handle == paper_block && vp.status.is_on)
|
||||
.filter(|vp| {
|
||||
vp.id > 1
|
||||
&& vp.common.owner_handle == paper_block
|
||||
&& vp.status.is_on
|
||||
&& only_vp.map_or(true, |h| vp.common.handle == h)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if viewports.is_empty() {
|
||||
|
|
@ -911,7 +942,18 @@ impl Scene {
|
|||
None
|
||||
};
|
||||
|
||||
let handle = self.document.add_entity(entity).unwrap_or(Handle::NULL);
|
||||
// Route to the correct block based on current editing mode:
|
||||
// - PSPACE (paper layout, no active viewport): paper-space layout block.
|
||||
// - MSPACE or model layout: model space (document default).
|
||||
let handle = if self.current_layout != "Model" && self.active_viewport.is_none() {
|
||||
let layout_name = self.current_layout.clone();
|
||||
self.document
|
||||
.add_entity_to_layout(entity, &layout_name)
|
||||
.unwrap_or(Handle::NULL)
|
||||
} else {
|
||||
self.document.add_entity(entity).unwrap_or(Handle::NULL)
|
||||
};
|
||||
|
||||
if !handle.is_null() {
|
||||
if let Some(model) = hatch_seed {
|
||||
self.hatches.insert(handle, model);
|
||||
|
|
|
|||
Loading…
Reference in a new issue