Merge pull request #1743 from ramox81/codex/layout-create-viewport

This commit is contained in:
Hakan Seven committed 2026-10-13 11:27:33 +03:00
commit 7095802e8e
9 files changed
+120 -14

No files matched your search

+1
View File
@@ -712,6 +712,7 @@ inventory::submit!(crate::command::CommandRegistration {
"UOSNAP",
"FIELDDISPLAY",
"SURFACEASSOCIATIVITY",
"LAYOUTCREATEVIEWPORT",
"FIELDEVAL",
"PDFIMPORTMODE",
"PDFIMPORTFILTER",
+34
View File
@@ -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() });
-1
View File
@@ -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
+4
View File
@@ -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,
+2
View File
@@ -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;
+11 -4
View File
@@ -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()])
+6 -1
View File
@@ -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 {
+4
View File
@@ -24,6 +24,7 @@ impl Scene {
/// cached layout-name list is invalidated on insert.
pub fn add_layout(&mut self, name: &str) -> codec::Result<Handle> {
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;
}
+58 -8
View File
@@ -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<String>,
layout_names_cache: RefCell<Option<(u64, std::sync::Arc<Vec<String>>)>>,
/// 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,35 @@ 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;
viewport.status = codec::entities::ViewportStatusFlags::default_on();
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);
self.notify_viewport_changed(handle);
self.bump_entities(&[(handle, ChangeKind::Added)]);
}
}
fn is_content_viewport_in_layout(
@@ -12664,6 +12698,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,
@@ -12682,8 +12722,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);
@@ -12693,6 +12732,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()