From c55dbb886ea174640e71ff3e2d8085aee391b3ab Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:02:26 +0300 Subject: [PATCH] Layouts: first opening follows the page setup; viewports picked by their frame - A layout opens for the first time when its page setup was never initialized (no paper size, no media name) or it was created in this session; its page setup is initialized then, and with LAYOUTCREATEVIEWPORT it gets its first viewport. A layout never opened is saved with an uninitialized page setup and no viewports, so it gets its viewport when the file is opened again, here or in the reference. - Select Viewport for view: a viewport is selected when the pick box (PICKBOX pixels at the sheet's zoom) touches its frame; a pick well inside or outside selects nothing and the prompt repeats. Co-Authored-By: Claude Opus 5.5 --- src/app/commands/view_presets.rs | 6 +++ src/app/update/viewport.rs | 1 + src/modules/view/view_cmd.rs | 12 +++++- src/scene/camera_ops.rs | 13 +++++-- src/scene/entity.rs | 1 + src/scene/mod.rs | 58 +++++++++++++++++++++++++++++ src/scene/parametric_constraints.rs | 3 ++ 7 files changed, 88 insertions(+), 6 deletions(-) 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 20cbdb597..5d4dbd743 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -6538,6 +6538,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 df09813c2..50e196be2 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -3098,6 +3098,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/mod.rs b/src/scene/mod.rs index 85a819e1e..0c929d14a 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2360,6 +2360,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, @@ -2773,6 +2776,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), @@ -4737,6 +4741,26 @@ 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); + 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. @@ -12732,6 +12756,40 @@ 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 viewports = Vec::new(); + for object in document.objects.values_mut() { + if let ObjectType::Layout(layout) = object { + if layout.name == layout_name { + 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; + viewports = std::mem::take(&mut layout.viewports); + layout.viewport = Handle::NULL; + } + } + } + for handle in viewports { + document.remove_entity(handle); + } +} + /// 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 }