feat(layout): copy a layout from its tab menu (#1822)

The layout tab's right-click menu gains New Layout, Copy, Page Setup
and Plot beside Rename and Delete. Copy makes "<name> (2)" with the
source's page setup, sheet view and every paper-space object,
viewports included, and opens it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-16 01:08:07 +03:00
1 parent 38d81bf838
commit 0126ea212f
28 files changed
+172 -16

No files matched your search

+6
View File
@@ -3765,6 +3765,12 @@ pub enum Message {
},
/// Create a new paper space layout.
LayoutCreate,
/// Copy the named layout, its page setup and paper-space objects.
LayoutCopy(String),
/// Open the named layout's page setup.
LayoutPageSetup(String),
/// Plot the named layout.
LayoutPlot(String),
/// Delete the named paper space layout (Model cannot be deleted).
LayoutDelete(String),
/// Begin inline rename for the given layout tab.
+12
View File
@@ -8297,6 +8297,18 @@ impl OpenCADStudio {
Message::LayoutCreate => self.on_layout_create(),
Message::LayoutCopy(name) => self.on_layout_copy(name),
Message::LayoutPageSetup(name) => {
let switch_task = self.on_layout_switch(name);
Task::batch([switch_task, self.on_plot_dialog_open()])
}
Message::LayoutPlot(name) => {
let switch_task = self.on_layout_switch(name);
Task::batch([switch_task, Task::done(Message::Command("PLOT".into()))])
}
Message::LayoutDelete(name) => {
let i = self.active_tab;
let deleting_current = self.tabs[i].scene.current_layout == name;
+41
View File
@@ -6920,6 +6920,47 @@ properties={:.1}ms picked={}",
cancel_task
}
/// Copy layout `source` as "source (2)": its page setup and every
/// paper-space object, viewports included. The copy opens.
pub(super) fn on_layout_copy(&mut self, source: String) -> Task<Message> {
let i = self.active_tab;
if self.tabs[i].is_start || source == "Model" {
return Task::none();
}
let existing = self.tabs[i].scene.layout_names();
let Some(name) = (2..)
.map(|n| format!("{source} ({n})"))
.find(|candidate| !existing.contains(candidate))
else {
return Task::none();
};
let cancel_task = self.cancel_active_command_for_space_change();
self.push_undo_snapshot(i, "LAYOUT");
let scene = &mut self.tabs[i].scene;
if let Err(e) = scene.add_layout(&name) {
self.command_line
.push_error(crate::tf!("Failed to create layout: {e}").as_ref());
return cancel_task;
}
scene.copy_layout_page(&source, &name);
let handles = scene.layout_paper_entities(&source);
let (entities, deps) = crate::app::copy_to_clipboard_kernel(&scene.document, &handles);
crate::app::paste_entities_kernel(
&mut scene.document,
&entities,
&deps,
None,
crate::app::PasteSpace::Layout(&name),
);
scene.populate_meshes_from_document();
scene.bump_geometry();
self.tabs[i].dirty = true;
self.command_line
.push_output(crate::tf!("Layout \"{source}\" copied as \"{name}\"").as_ref());
let switch_task = self.on_layout_switch(name);
Task::batch([cancel_task, switch_task])
}
pub(super) fn on_layout_rename_commit(&mut self) -> Task<Message> {
if let Some((orig, new_name)) = self.layout_rename_state.take() {
let new_name = new_name.trim().to_string();
+1
View File
@@ -1963,6 +1963,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Layout" => Some(("common", "layout")),
"Layout\nTabs" => Some(("view", "layout-tabs")),
"Layout \"{name}\" silindi" => Some(("common", "layout-name-silindi")),
"Layout \"{source}\" copied as \"{name}\"" => Some(("common", "layout-copied")),
"Layout \"{new_name}\" created — use MVIEW to add a viewport" => Some(("common", "layout-new-name-created-use-mview-to-add-a-vie")),
"Layout \"{orig}\" → \"{new_name}\"" => Some(("common", "layout-orig-new-name")),
"Layout '{name}' created." => Some(("common", "layout-name-created")),
+72
View File
@@ -32,6 +32,78 @@ impl Scene {
Ok(handle)
}
/// Give layout `name` the page of layout `source`: its page setup,
/// limits and UCS, and the sheet viewport's view. It opens as it is, so
/// it does not get the first viewport a new layout gets. The paper-space
/// objects are copied by the caller.
pub fn copy_layout_page(&mut self, source: &str, name: &str) {
let find = |document: &codec::CadDocument, wanted: &str| {
document.objects.values().find_map(|object| match object {
ObjectType::Layout(layout) if layout.name == wanted => Some(layout.clone()),
_ => None,
})
};
let (Some(from), Some(to)) = (find(&self.document, source), find(&self.document, name))
else {
return;
};
if let Some(ObjectType::Layout(layout)) = self.document.objects.get_mut(&to.handle) {
*layout = codec::objects::Layout {
handle: to.handle,
owner: to.owner,
name: to.name.clone(),
tab_order: to.tab_order,
block_record: to.block_record,
viewport: to.viewport,
viewports: to.viewports.clone(),
reactors: to.reactors.clone(),
xdictionary_handle: to.xdictionary_handle,
..from.clone()
};
}
if let Some(EntityType::Viewport(sheet)) = self.document.get_entity(from.viewport).cloned()
{
if let Some(EntityType::Viewport(target)) = self.document.get_entity_mut(to.viewport) {
*target = codec::entities::Viewport {
common: target.common.clone(),
..sheet
};
}
}
self.unopened_layouts.remove(name);
self.bump_layout_epoch();
}
/// The paper-space objects of layout `name`, without its sheet viewport.
pub fn layout_paper_entities(&self, name: &str) -> Vec<Handle> {
let Some((record, sheet)) = self.document.objects.values().find_map(|object| match object {
ObjectType::Layout(layout) if layout.name == name => {
Some((layout.block_record, layout.viewport))
}
_ => None,
}) else {
return Vec::new();
};
self.document
.block_records
.iter()
.find(|br| br.handle == record)
.map(|br| {
br.entity_handles
.iter()
.copied()
.filter(|handle| *handle != sheet)
.filter(|handle| {
!matches!(
self.document.get_entity(*handle),
None | Some(EntityType::Block(_) | EntityType::BlockEnd(_))
)
})
.collect()
})
.unwrap_or_default()
}
/// Rename a paper-space layout. Updates the Layout object name in the document.
pub fn rename_layout(&mut self, old_name: &str, new_name: &str) {
for obj in self.document.objects.values_mut() {
+18 -16
View File
@@ -937,23 +937,25 @@ fn osnap_btn<'a>(
// ── Helpers ───────────────────────────────────────────────────────────────
fn layout_tab_context_menu(name: String) -> Element<'static, Message> {
let rename = mouse_area(
container(text(t!("Rename")).size(12))
.padding([4, 12])
.width(Length::Fill),
)
.on_press(Message::LayoutRenameStart(name.clone()))
.interaction(iced::mouse::Interaction::Pointer);
let item = |label: std::borrow::Cow<'static, str>, message: Message| {
mouse_area(
container(text(label).size(12))
.padding([4, 12])
.width(Length::Fill),
)
.on_press(message)
.interaction(iced::mouse::Interaction::Pointer)
};
let items = column![
item(t!("New Layout"), Message::LayoutCreate),
item(t!("Copy"), Message::LayoutCopy(name.clone())),
item(t!("Rename"), Message::LayoutRenameStart(name.clone())),
item(t!("Delete"), Message::LayoutDelete(name.clone())),
item(t!("Page Setup"), Message::LayoutPageSetup(name.clone())),
item(t!("Plot"), Message::LayoutPlot(name)),
];
let delete = mouse_area(
container(text(t!("Delete")).size(12))
.padding([4, 12])
.width(Length::Fill),
)
.on_press(Message::LayoutDelete(name))
.interaction(iced::mouse::Interaction::Pointer);
container(column![rename, delete].spacing(0).width(160))
container(items.spacing(0).width(160))
.style(container::bordered_box)
.padding([4, 0])
.into()