diff --git a/src/app/commands/view_presets.rs b/src/app/commands/view_presets.rs index 0ce257051..fc17eddad 100644 --- a/src/app/commands/view_presets.rs +++ b/src/app/commands/view_presets.rs @@ -80,6 +80,12 @@ impl OpenCADStudio { visual_styles: visual_styles.into_iter().map(|(name, _)| name).collect(), live_sections: live_sections.into_iter().map(|(name, _)| name).collect(), sheet_viewports, + // PICKBOX pixels in sheet units, at the sheet camera's zoom. + sheet_pick_tolerance: { + let height = scene.camera.borrow().ortho_size() as f64 * 2.0; + let (_, pixels) = scene.selection.borrow().view.vp_size; + f64::from(self.pick_box) * height / f64::from(pixels).max(1.0) + }, } } diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 644478cef..397be1f48 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -6784,6 +6784,7 @@ properties={:.1}ms picked={}", .as_ref() .map(|style| style.name.clone()) .unwrap_or_default(); + self.tabs[i].scene.new_layout_plot_style = plot_style.clone(); for obj in self.tabs[i].scene.document.objects.values_mut() { if let codec::objects::ObjectType::Layout(l) = obj { if l.name == new_name { diff --git a/src/modules/view/view_cmd.rs b/src/modules/view/view_cmd.rs index a439e61ad..29144a4bf 100644 --- a/src/modules/view/view_cmd.rs +++ b/src/modules/view/view_cmd.rs @@ -71,6 +71,9 @@ pub struct ViewNames { /// (handle, [min x, min y, max x, max y]), one of which takes a /// model-space view being restored. pub sheet_viewports: Option>, + /// The pick box (PICKBOX pixels) in sheet units: a viewport is selected + /// by its frame, within this distance of it. + pub sheet_pick_tolerance: f64, } #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -513,9 +516,14 @@ impl CadCommand for DashViewCommand { name, first.x, first.y, pt.x, pt.y )), Step::RestoreViewport(name) => { - // The topmost viewport under the pick. + // The topmost viewport whose frame the pick box touches; a + // pick well inside a viewport selects nothing, as in the + // reference. + let tol = self.names.sheet_pick_tolerance.max(0.0); let hit = self.names.sheet_viewports.iter().flatten().rev().find(|(_, [x0, y0, x1, y1])| { - pt.x >= *x0 && pt.x <= *x1 && pt.y >= *y0 && pt.y <= *y1 + let outside = pt.x < x0 - tol || pt.x > x1 + tol || pt.y < y0 - tol || pt.y > y1 + tol; + let inside = pt.x > x0 + tol && pt.x < x1 - tol && pt.y > y0 + tol && pt.y < y1 - tol; + !outside && !inside }); match hit { Some((handle, _)) => CmdResult::Dispatch(format!("-VIEW >RESTOREVP {name}\u{1f}{handle:X}")), diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index 6f51e10b5..5012e5f17 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -1378,10 +1378,15 @@ impl Scene { // Every paper layout has a full-screen sheet viewport that holds // its view; create one if a loaded file lacks it. let layout = self.current_layout.clone(); - // A layout opening for the first time: one whose sheet had to be - // made, or one created in this session. - let created = self.ensure_sheet_viewport(&layout); - if self.unopened_layouts.remove(&layout) || created { + // A layout opening for the first time, as the reference tells + // it: its page setup was never initialized (a file's layout + // with no paper), or it was created in this session and not + // opened yet. Its page setup is initialized and, with + // LAYOUTCREATEVIEWPORT, it gets its first viewport. + let unopened = self.unopened_layouts.remove(&layout); + let initialized = self.initialize_layout_page_setup(&layout); + self.ensure_sheet_viewport(&layout); + if unopened || initialized { self.add_layout_default_viewport(&layout); } self.apply_sheet_viewport_camera() diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 6713fa092..741400e9d 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -3122,6 +3122,7 @@ impl Scene { if let codec::objects::ObjectType::Layout(l) = obj { if l.name != "Model" { crate::scene::apply_default_page_setup(l, ""); + self.unopened_layouts.insert(l.name.clone()); } } } diff --git a/src/scene/layout.rs b/src/scene/layout.rs index 9036499ec..80e4a6d01 100644 --- a/src/scene/layout.rs +++ b/src/scene/layout.rs @@ -24,6 +24,15 @@ impl Scene { /// cached layout-name list is invalidated on insert. pub fn add_layout(&mut self, name: &str) -> codec::Result { let handle = self.document.add_layout(name)?; + // The layout has no viewports until it first opens (the sheet the + // document makes for it is placed at the origin, not on its page); + // opening it makes the sheet and, with LAYOUTCREATEVIEWPORT, its + // first viewport. + let block = match self.document.objects.get(&handle) { + Some(ObjectType::Layout(layout)) => layout.block_record, + _ => codec::Handle::NULL, + }; + crate::scene::drop_layout_viewports(&mut self.document, block); self.unopened_layouts.insert(name.to_string()); self.bump_layout_epoch(); Ok(handle) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index d0f0dd210..0eb634827 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2437,6 +2437,9 @@ pub struct Scene { /// Layouts created in this session and not opened yet: the first time /// one opens it gets the LAYOUTCREATEVIEWPORT viewport. pub(crate) unopened_layouts: std::collections::HashSet, + /// The plot style table a layout's page setup takes when it is + /// initialized on first opening. + pub(crate) new_layout_plot_style: String, layout_names_cache: RefCell>)>>, /// Bump on any annotation-scale list mutation. scale_epoch: u64, @@ -2857,6 +2860,7 @@ impl Scene { layout_type_names_cache: RefCell::new(None), layout_epoch: 0, unopened_layouts: Default::default(), + new_layout_plot_style: String::new(), layout_names_cache: RefCell::new(None), scale_epoch: 0, scale_picker_cache: RefCell::new(None), @@ -4832,6 +4836,30 @@ impl Scene { true } + /// Gives a layout whose page setup was never initialized (no paper size + /// and no media name, as the reference leaves a layout until it is first + /// opened) the default page setup; true when it did. + pub fn initialize_layout_page_setup(&mut self, layout_name: &str) -> bool { + let plot_style = self.new_layout_plot_style.clone(); + for object in self.document.objects.values_mut() { + if let ObjectType::Layout(layout) = object { + if layout.name == layout_name { + if !page_setup_uninitialized(layout) { + return false; + } + crate::scene::apply_default_page_setup(layout, &plot_style); + let block = layout.block_record; + // A sheet written for a layout never set up is not the + // page's; it is made again for the page. + drop_layout_viewports(&mut self.document, block); + self.bump_layout_epoch(); + return true; + } + } + } + false + } + /// The viewport a layout gets when it first opens (LAYOUTCREATEVIEWPORT), /// as the reference makes it: centred on the printable area, 0.8 of its /// width and height, showing the model's extents. @@ -13053,6 +13081,71 @@ vis_index={:.1} visible_probe={:.1}", } } +/// A page setup never initialized: no paper size and no media name (the +/// reference writes a layout so until it is first opened). +pub fn page_setup_uninitialized(layout: &codec::objects::Layout) -> bool { + layout.paper_size.trim().is_empty() && layout.paper_width <= 0.0 && layout.paper_height <= 0.0 +} + +/// A layout written as never opened: the page setup fields back to the +/// uninitialized state and no viewports, as the reference stores a layout +/// it has not opened yet. +pub(crate) fn write_layout_unopened(document: &mut codec::CadDocument, layout_name: &str) { + let fresh = codec::objects::Layout::new(layout_name); + let mut block = Handle::NULL; + for object in document.objects.values_mut() { + if let ObjectType::Layout(layout) = object { + if layout.name == layout_name { + block = layout.block_record; + layout.paper_size = fresh.paper_size.clone(); + layout.paper_width = fresh.paper_width; + layout.paper_height = fresh.paper_height; + layout.plot_rotation = fresh.plot_rotation; + layout.plot_paper_units = fresh.plot_paper_units; + layout.min_limits = fresh.min_limits; + layout.max_limits = fresh.max_limits; + layout.min_extents = fresh.min_extents; + layout.max_extents = fresh.max_extents; + layout.viewports.clear(); + layout.viewport = Handle::NULL; + } + } + } + drop_layout_viewports(document, block); +} + +/// Takes every viewport out of a layout's block (entity and membership). +pub(crate) fn drop_layout_viewports(document: &mut codec::CadDocument, block: Handle) { + if block.is_null() { + return; + } + let viewports: Vec = document + .entities() + .filter_map(|entity| match entity { + EntityType::Viewport(viewport) if viewport.common.owner_handle == block => { + Some(viewport.common.handle) + } + _ => None, + }) + .collect(); + for handle in &viewports { + document.remove_entity(*handle); + } + if let Some(record) = document.block_records.iter_mut().find(|record| record.handle == block) { + record.entity_handles.retain(|handle| !viewports.contains(handle)); + } + for object in document.objects.values_mut() { + if let ObjectType::Layout(layout) = object { + if layout.block_record == block { + layout.viewports.retain(|handle| !viewports.contains(handle)); + if viewports.contains(&layout.viewport) { + layout.viewport = Handle::NULL; + } + } + } + } +} + /// LAYOUTCREATEVIEWPORT: a layout opened for the first time gets a viewport. static LAYOUT_CREATE_VIEWPORT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true); diff --git a/src/scene/parametric_constraints.rs b/src/scene/parametric_constraints.rs index 7037908e9..833cf42ca 100644 --- a/src/scene/parametric_constraints.rs +++ b/src/scene/parametric_constraints.rs @@ -2535,6 +2535,9 @@ impl super::Scene { } crate::io::xref::strip_resolved_xref_content(&mut document); crate::entities::field::stamp_save_dates(&mut document); + for name in &self.unopened_layouts { + crate::scene::write_layout_unopened(&mut document, name); + } document }