From e3a0ff1bad8d0c787f0bad8bab47f05e36e28adb Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:09:03 +0300 Subject: [PATCH 1/2] Layouts: a viewport on first opening (LAYOUTCREATEVIEWPORT) A layout opened for the first time - one whose sheet viewport had to be made, or one created in this session - gets a viewport centred on the printable area, 0.8 of its width and height, zoomed to the model's extents, while LAYOUTCREATEVIEWPORT is on. The profile variable takes ON/OFF or 1/0 and is kept with the settings. Co-Authored-By: Claude Opus 5.5 --- src/app/commands/mod.rs | 1 + src/app/commands/styleprops.rs | 34 ++++++++++++++++++ src/app/control/sheets.rs | 1 - src/app/settings.rs | 4 +++ src/app/update/file.rs | 2 ++ src/app/update/viewport.rs | 15 +++++--- src/scene/camera_ops.rs | 7 +++- src/scene/layout.rs | 4 +++ src/scene/mod.rs | 63 +++++++++++++++++++++++++++++----- 9 files changed, 117 insertions(+), 14 deletions(-) diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index c33336eda..515022b9a 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -712,6 +712,7 @@ inventory::submit!(crate::command::CommandRegistration { "UOSNAP", "FIELDDISPLAY", "SURFACEASSOCIATIVITY", + "LAYOUTCREATEVIEWPORT", "FIELDEVAL", "PDFIMPORTMODE", "PDFIMPORTFILTER", diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index a31daad7d..c556efa81 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -1213,6 +1213,7 @@ impl OpenCADStudio { | "SURFACEASSOCIATIVITY" | "UCSORTHO" | "UCSVIEW" + | "LAYOUTCREATEVIEWPORT" | "PDFIMPORTMODE" | "PDFIMPORTFILTER" | "PDFIMPORTLAYERS" @@ -1451,6 +1452,39 @@ impl OpenCADStudio { } return Some(self.finish_dispatch(cmd)); } + if name == "LAYOUTCREATEVIEWPORT" { + // A profile switch shown and taken as ON / OFF or 1 / 0. + let current = if crate::scene::layout_create_viewport() { "ON" } else { "OFF" }; + let parsed = value.as_deref().map(|v| { + match v.trim().trim_start_matches('_').to_ascii_uppercase().as_str() { + "1" | "ON" => Some(true), + "0" | "OFF" => Some(false), + _ => None, + } + }); + match parsed { + Some(Some(on)) => { + if crate::scene::layout_create_viewport() != on { + crate::scene::set_layout_create_viewport(on); + self.save_config(); + } + } + Some(None) => { + self.command_line.push_error( + crate::t!("Requires 0 or 1 only, or option keyword.").as_ref(), + ); + self.command_line + .push_output(&format!("Enter new value for {name} <{current}>:")); + self.pending_setvar = Some(name.clone()); + } + None => { + self.command_line + .push_output(&format!("Enter new value for {name} <{current}>:")); + self.pending_setvar = Some(name.clone()); + } + } + return Some(self.finish_dispatch(cmd)); + } if matches!(name.as_str(), "UCSORTHO" | "UCSVIEW") { use crate::modules::view::view_cmd; let current = i16::from(if name == "UCSORTHO" { view_cmd::ucs_ortho() } else { view_cmd::ucs_view() }); diff --git a/src/app/control/sheets.rs b/src/app/control/sheets.rs index a9c32b788..42cdb2f2c 100644 --- a/src/app/control/sheets.rs +++ b/src/app/control/sheets.rs @@ -136,7 +136,6 @@ impl OpenCADStudio { self.push_undo_snapshot(i, "LAYOUTCREATE"); self.tabs[i] .scene - .document .add_layout(&name) .map_err(|e| failure("layout_failed", e))?; let plot_style = self diff --git a/src/app/settings.rs b/src/app/settings.rs index b62c6b072..960a2a54c 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -423,6 +423,9 @@ pub struct UserSettings { pub ucs_ortho: bool, /// UCSVIEW: a saved named view keeps the current UCS. pub ucs_view: bool, + /// LAYOUTCREATEVIEWPORT: a layout opened for the first time gets a + /// viewport. + pub layout_create_viewport: bool, /// Nested-copy symbol handling: false inserts, true binds. pub ncopy_bind: bool, /// Last Options page; unknown saved names fall back without rejecting the config. @@ -842,6 +845,7 @@ impl Default for UserSettings { surface_associativity: true, ucs_ortho: true, ucs_view: true, + layout_create_viewport: true, ncopy_bind: false, cursor_type: CursorType::Crosshair, crosshair_color: None, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 837c1a765..9d78560ef 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -796,6 +796,7 @@ impl OpenCADStudio { surface_associativity: crate::scene::model::sweep_model::surface_associativity(), ucs_ortho: crate::modules::view::view_cmd::ucs_ortho(), ucs_view: crate::modules::view::view_cmd::ucs_view(), + layout_create_viewport: crate::scene::layout_create_viewport(), ncopy_bind: self.ncopy_bind, cursor_type: self.cursor_type, crosshair_color: self.crosshair_color, @@ -911,6 +912,7 @@ impl OpenCADStudio { crate::scene::model::sweep_model::set_surface_associativity(s.surface_associativity); crate::modules::view::view_cmd::set_ucs_ortho(s.ucs_ortho); crate::modules::view::view_cmd::set_ucs_view(s.ucs_view); + crate::scene::set_layout_create_viewport(s.layout_create_viewport); self.ncopy_bind = s.ncopy_bind; self.cursor_type = s.cursor_type; self.crosshair_color = s.crosshair_color; diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 57e627446..684b7a164 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -6549,10 +6549,17 @@ properties={:.1}ms picked={}", self.tabs[i].scene.ensure_sheet_viewport(&new_name); let switch_task = self.on_layout_switch(new_name.clone()); self.tabs[i].scene.fit_all(); - self.command_line.push_output( - crate::tf!("Layout \"{new_name}\" created — use MVIEW to add a viewport") - .as_ref(), - ); + // Opened, it already has its first viewport unless + // LAYOUTCREATEVIEWPORT is off. + if crate::scene::layout_create_viewport() { + let name = new_name.clone(); + self.command_line.push_output(crate::tf!("Layout '{name}' created.").as_ref()); + } else { + self.command_line.push_output( + crate::tf!("Layout \"{new_name}\" created — use MVIEW to add a viewport") + .as_ref(), + ); + } // The option that greets every new layout with its page setup. let switch_task = if self.plot_dialog.page_setup_on_new_layout { Task::batch([switch_task, self.on_plot_dialog_open()]) diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index e9dcbcc6b..d814b429e 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -1363,7 +1363,12 @@ 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(); - self.ensure_sheet_viewport(&layout); + // 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 { + self.add_layout_default_viewport(&layout); + } self.apply_sheet_viewport_camera() }; if restored { diff --git a/src/scene/layout.rs b/src/scene/layout.rs index e61291440..9036499ec 100644 --- a/src/scene/layout.rs +++ b/src/scene/layout.rs @@ -24,6 +24,7 @@ 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)?; + self.unopened_layouts.insert(name.to_string()); self.bump_layout_epoch(); Ok(handle) } @@ -34,6 +35,9 @@ impl Scene { if let ObjectType::Layout(l) = obj { if l.name == old_name { l.name = new_name.to_string(); + if self.unopened_layouts.remove(old_name) { + self.unopened_layouts.insert(new_name.to_string()); + } self.bump_layout_epoch(); return; } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 918b58757..1cb88d3d8 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2357,6 +2357,9 @@ pub struct Scene { >, /// Bump on any Layout object insert/remove/rename/reorder. layout_epoch: u64, + /// 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, layout_names_cache: RefCell>)>>, /// Bump on any annotation-scale list mutation. scale_epoch: u64, @@ -2769,6 +2772,7 @@ impl Scene { unindexable_cache: RefCell::new(None), layout_type_names_cache: RefCell::new(None), layout_epoch: 0, + unopened_layouts: Default::default(), layout_names_cache: RefCell::new(None), scale_epoch: 0, scale_picker_cache: RefCell::new(None), @@ -4662,10 +4666,11 @@ impl Scene { .unwrap_or(Handle::NULL) } - /// Ensure a paper layout has its overall (`id == 1`) viewport. - pub fn ensure_sheet_viewport(&mut self, layout_name: &str) { + /// Ensure a paper layout has its overall (`id == 1`) viewport; true when + /// it had none (the layout opens for the first time). + pub fn ensure_sheet_viewport(&mut self, layout_name: &str) -> bool { if layout_name == "Model" { - return; + return false; } // Locate the layout and its paper limits. let info = self.document.objects.iter().find_map(|(h, obj)| { @@ -4683,10 +4688,10 @@ impl Scene { None }); let Some((layout_handle, block_record, sheet, min_lim, max_lim)) = info else { - return; + return false; }; if block_record.is_null() { - return; + return false; } if sheet.is_valid() @@ -4695,7 +4700,7 @@ impl Scene { Some(EntityType::Viewport(vp)) if vp.common.owner_handle == block_record ) { - return; + return false; } // Create the full-screen overall viewport covering the paper limits. @@ -4729,6 +4734,32 @@ impl Scene { l.viewports.insert(0, handle); } } + true + } + + /// 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. + pub fn add_layout_default_viewport(&mut self, layout_name: &str) { + if !layout_create_viewport() || !self.layout_content_viewports().is_empty() { + return; + } + let Some(((x0, y0), (x1, y1))) = self.printable_area_limits() else { + return; + }; + let mut viewport = codec::entities::Viewport::new(); + viewport.center = codec::types::Vector3::new((x0 + x1) / 2.0, (y0 + y1) / 2.0, 0.0); + viewport.width = (x1 - x0).abs() * 0.8; + viewport.height = (y1 - y0).abs() * 0.8; + viewport.id = 2; + if let Ok(handle) = self + .document + .add_entity_to_layout(EntityType::Viewport(viewport), layout_name) + { + // The reference zooms the new viewport to the model's extents + // with a margin of under one percent. + self.fit_viewport_to_model(handle, 1.0075); + } } fn is_content_viewport_in_layout( @@ -12663,6 +12694,12 @@ vis_index={:.1} visible_probe={:.1}", /// Set a newly created viewport's `view_target` and `view_height` so that /// all model-space content is visible at a reasonable scale. pub fn auto_fit_viewport(&mut self, vp_handle: Handle) { + self.fit_viewport_to_model(vp_handle, 1.1); + } + + /// Fit a viewport's view to the model-space content, with `margin` (1 is + /// tight) around it. + pub fn fit_viewport_to_model(&mut self, vp_handle: Handle, margin: f64) { let extents = self.model_space_extents(); let (min, max) = match extents { Some(e) => e, @@ -12681,8 +12718,7 @@ vis_index={:.1} visible_probe={:.1}", vp.view_target.y = center.y as f64; vp.view_target.z = 0.0; - // Choose the scale that fits both dimensions with a small margin. - let margin = 1.1_f64; + // Choose the scale that fits both dimensions with the margin. let scale_w = vp.width / (content_w as f64 * margin); let scale_h = vp.height / (content_h as f64 * margin); let fit_scale = scale_w.min(scale_h).min(1000.0).max(1e-6); @@ -12692,6 +12728,17 @@ vis_index={:.1} visible_probe={:.1}", } } +/// 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); + +pub fn layout_create_viewport() -> bool { + LAYOUT_CREATE_VIEWPORT.load(std::sync::atomic::Ordering::Relaxed) +} + +pub fn set_layout_create_viewport(on: bool) { + LAYOUT_CREATE_VIEWPORT.store(on, std::sync::atomic::Ordering::Relaxed); +} + impl Default for Scene { fn default() -> Self { Self::new() From bd80d96ce2eff9079a06a6e6bb5aee0efdf783eb Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:19:35 +0300 Subject: [PATCH 2/2] Layouts: the first-open viewport is on and drawn at once Co-Authored-By: Claude Opus 5.5 --- src/scene/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 1cb88d3d8..ec11a97ef 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -4752,6 +4752,7 @@ impl Scene { viewport.width = (x1 - x0).abs() * 0.8; viewport.height = (y1 - y0).abs() * 0.8; viewport.id = 2; + viewport.status = codec::entities::ViewportStatusFlags::default_on(); if let Ok(handle) = self .document .add_entity_to_layout(EntityType::Viewport(viewport), layout_name) @@ -4759,6 +4760,8 @@ impl Scene { // The reference zooms the new viewport to the model's extents // with a margin of under one percent. self.fit_viewport_to_model(handle, 1.0075); + self.notify_viewport_changed(handle); + self.bump_entities(&[(handle, ChangeKind::Added)]); } }