From a3c79be278a0eef7fbc66503e6a33ffc9c8c3f11 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:05:47 +0300 Subject: [PATCH] Sheet sets: fly-out submenus, block previews, window-aspect named views - Row menus open Set category / Place Callout Block as fly-out submenus (hover or right press; leftwards when the palette is docked right). - List of Blocks and Select Block show the Blocks palette's preview. - VIEW SAVE keeps the window's width/height ratio. - Placing view content in model space is refused with the reference message. - Drawings read for views, placement and blocks are cached by modification time. Co-Authored-By: Claude Opus 5.5 --- locales/ar-SA/opencadstudio.ftl | 2 + locales/bg-BG/opencadstudio.ftl | 2 + locales/cs-CZ/opencadstudio.ftl | 2 + locales/de-DE/opencadstudio.ftl | 2 + locales/el-GR/opencadstudio.ftl | 2 + locales/en-US/opencadstudio.ftl | 2 + locales/es-ES/opencadstudio.ftl | 2 + locales/fi-FI/opencadstudio.ftl | 2 + locales/fr-FR/opencadstudio.ftl | 2 + locales/hi-IN/opencadstudio.ftl | 2 + locales/hu-HU/opencadstudio.ftl | 2 + locales/it-IT/opencadstudio.ftl | 2 + locales/ja-JP/opencadstudio.ftl | 2 + locales/ko-KR/opencadstudio.ftl | 2 + locales/nl-NL/opencadstudio.ftl | 2 + locales/pl-PL/opencadstudio.ftl | 2 + locales/pt-BR/opencadstudio.ftl | 2 + locales/ru-RU/opencadstudio.ftl | 2 + locales/tr-TR/opencadstudio.ftl | 2 + locales/zh-CN/opencadstudio.ftl | 2 + locales/zh-TW/opencadstudio.ftl | 2 + src/app/commands/sheet_set.rs | 103 ++++++++++++++-- src/app/view/mod.rs | 2 +- src/locale_catalog.rs | 2 + src/scene/camera_ops.rs | 3 +- src/ui/window/block_palette.rs | 5 + src/ui/window/sheet_set.rs | 201 ++++++++++++++++++++++++-------- 27 files changed, 296 insertions(+), 62 deletions(-) diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 41567568c..0ae5d73de 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -4407,6 +4407,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 6f7495bcd..ab15d932f 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -4394,6 +4394,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index a58827e80..63e9ca38f 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -4392,6 +4392,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 340317851..c79f391e1 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -4393,6 +4393,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index db6b247ef..39d72c969 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -4395,6 +4395,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 15e519cee..4128886b8 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -4396,6 +4396,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 0eaaea507..550e14aaa 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -4395,6 +4395,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index b4ccc4c7d..0125df9df 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -4394,6 +4394,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 6fb8ac0b3..63df1df75 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -4395,6 +4395,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index d80f62fb9..44b3dec61 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -4383,6 +4383,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 695be5f54..ee1d0d144 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -4394,6 +4394,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 508b9031d..ea2938074 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -4394,6 +4394,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index cc0bfdc1a..d665753ca 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -4383,6 +4383,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index c5d43cada..4cea5fb8b 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -4394,6 +4394,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index c3dd02ee0..105fa4833 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -4393,6 +4393,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 6dac7e7c0..52584e53e 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -4394,6 +4394,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index e8980d72f..6e1b5d464 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -4395,6 +4395,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 14816ca6b..3179ecc33 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -4396,6 +4396,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index fd7d85d19..6065f397f 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -4355,6 +4355,8 @@ view = .ss-new-sheet-location = Yeni sayfa konumu .ss-remove-subset = Alt Kümeyi Kaldır .ss-rename-subset = Alt Kümeyi Yeniden Adlandır... + .ss-content-model-space = İçerik model uzayına yerleştirilemez. Kağıt uzayına geçip yeniden deneyin. + .ss-cannot-read = okunamıyor .ss-rename-renumber-view = Görünümü Yeniden Adlandır ve Numaralandır .ss-list-of-blocks = Blok Listesi .ss-select-block = Blok Seç diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 6e84dd631..f6710359a 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -4378,6 +4378,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index c9066a585..c5c4e42e6 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -4390,6 +4390,8 @@ view = .ss-new-sheet-location = New sheet location .ss-remove-subset = Remove Subset .ss-rename-subset = Rename Subset... + .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. + .ss-cannot-read = cannot be read .ss-rename-renumber-view = Rename & Renumber View .ss-list-of-blocks = List of Blocks .ss-select-block = Select Block diff --git a/src/app/commands/sheet_set.rs b/src/app/commands/sheet_set.rs index c14522311..1adc56b9f 100644 --- a/src/app/commands/sheet_set.rs +++ b/src/app/commands/sheet_set.rs @@ -705,13 +705,24 @@ impl OpenCADStudio { SheetSetMsg::AddBlocks => { if let Some(SsDialog::Category(c)) = self.sheet_set.dialog.take() { let blocks = c.blocks.iter().map(|(id, label, _)| (id.clone(), label.clone())).collect(); - self.sheet_set.dialog = Some(SsDialog::BlockList(BlockList { category: Box::new(c), blocks, selected: None })); + self.sheet_set.dialog = Some(SsDialog::BlockList(BlockList { category: Box::new(c), blocks, selected: None, preview: None })); } } SheetSetMsg::BlocksPicked(_) | SheetSetMsg::LocationPicked(None) => {} + SheetSetMsg::Submenu(key) => self.sheet_set.submenu = key, SheetSetMsg::BlockListSelect(k) => { + let target = match self.sheet_set.dialog.as_ref() { + Some(SsDialog::BlockList(b)) => b.blocks.get(k).and_then(|(id, _)| self.listed_block(b.category.set, id)), + _ => None, + }; + // A whole drawing taken as a block shows its model space. + let preview = target.and_then(|(file, name)| { + let whole = self.sheet_set.drawings.get(&file).is_some_and(|d| d.block_records.get(&name).is_none()); + self.block_preview(&file, if whole { "" } else { &name }) + }); if let Some(SsDialog::BlockList(b)) = self.sheet_set.dialog.as_mut() { b.selected = Some(k); + b.preview = preview; } } SheetSetMsg::BlockListDelete => { @@ -728,6 +739,7 @@ impl OpenCADStudio { file: String::new(), whole: true, names: Vec::new(), + preview: None, error: None, })); } @@ -748,15 +760,28 @@ impl OpenCADStudio { SheetSetMsg::SelectBlockPicked(None) => {} SheetSetMsg::SelectBlockPicked(Some(path)) => self.select_block_file(path), SheetSetMsg::SelectBlockWhole(v) => { + let file = match self.sheet_set.dialog.as_ref() { + Some(SsDialog::SelectBlock(b)) => b.file.clone(), + _ => String::new(), + }; + let preview = if v && !file.is_empty() { self.block_preview(&file, "") } else { None }; if let Some(SsDialog::SelectBlock(b)) = self.sheet_set.dialog.as_mut() { b.whole = v; + b.preview = preview; } } SheetSetMsg::SelectBlockCheck(k, v) => { + let picked = match self.sheet_set.dialog.as_ref() { + Some(SsDialog::SelectBlock(b)) => b.names.get(k).map(|n| (b.file.clone(), n.0.clone())), + _ => None, + }; + // The block just checked is previewed. + let preview = picked.and_then(|(file, name)| self.block_preview(&file, &name)); if let Some(SsDialog::SelectBlock(b)) = self.sheet_set.dialog.as_mut() { if let Some(n) = b.names.get_mut(k) { n.2 = v; } + b.preview = preview; } } SheetSetMsg::ToggleDrawing(path) => { @@ -771,7 +796,10 @@ impl OpenCADStudio { self.sheet_set.open_drawings.insert(ss::path_key(&path)); } SheetSetMsg::PlaceOnSheet(path, view) => self.start_place_view(path, view), - SheetSetMsg::ViewMenu(id, action) => return self.view_menu(id, action), + SheetSetMsg::ViewMenu(id, action) => { + self.sheet_set.submenu = None; + return self.view_menu(id, action); + } SheetSetMsg::AddLocation => { return Task::perform( async { @@ -1540,13 +1568,55 @@ impl OpenCADStudio { self.save_current_sheet_set(); } + /// A block preview (`name` empty: the drawing's model space), cached by + /// `file|name`; returns its key. + fn block_preview(&mut self, file: &str, name: &str) -> Option { + let key = format!("{}|{}", ss::path_key(file), name); + if self.sheet_set.previews.contains_key(&key) { + return Some(key); + } + let doc = self.sheet_set.drawings.get(file)?; + let mut scene = crate::scene::Scene::new(); + scene.document = (*doc).clone(); + let wires = if name.is_empty() { + let model = scene.document.header.model_space_block_handle; + let entities: Vec = scene + .document + .entities() + .filter(|e| e.common().owner_handle == model) + .filter(|e| !matches!(e, codec::EntityType::Block(_) | codec::EntityType::BlockEnd(_))) + .take(20_000) + .cloned() + .collect(); + scene.wires_for_entities(&entities) + } else { + scene.block_preview_wires(name) + }; + self.sheet_set.previews.insert(key.clone(), wires); + Some(key) + } + + /// The file and block name behind a List of Blocks id. + fn listed_block(&self, set: usize, id: &str) -> Option<(String, String)> { + if let Some(spec) = id.strip_prefix("new:") { + let mut p = spec.splitn(3, '|'); + return Some((p.next()?.to_string(), p.next()?.to_string())); + } + let db = self.sheet_set.sets.get(set)?; + let b = db.find(id)?; + Some((db.resolve_file(b), b.prop("Name").unwrap_or("").to_string())) + } + /// Named model views of a drawing (read once, when its row is opened). fn load_model_views(&mut self, path: &str) { let key = ss::path_key(path); if self.sheet_set.model_views.contains_key(&key) { return; } - let views = crate::io::load_file(Path::new(path)) + let views = self + .sheet_set + .drawings + .get(path) .map(|d| d.views.iter().filter(|v| !v.paper_space).map(|v| v.name.clone()).collect()) .unwrap_or_default(); self.sheet_set.model_views.insert(key, views); @@ -1589,13 +1659,20 @@ impl OpenCADStudio { /// Place on Sheet: only on a sheet of this set open at its layout. fn start_place_view(&mut self, drawing: String, view: Option) { + if self.tabs[self.active_tab].scene.current_layout.eq_ignore_ascii_case("Model") { + self.command_line + .push_error(crate::t!("Content cannot be placed into model space. Please switch to paper space and try again.").as_ref()); + return; + } let Some((set, sheet)) = self.current_sheet() else { self.command_line .push_error(crate::t!("Open a sheet of this sheet set at its layout to place a view on it.").as_ref()); return; }; - let size = crate::io::load_file(Path::new(&drawing)) - .ok() + let size = self + .sheet_set + .drawings + .get(&drawing) .and_then(|d| model_view(&d, view.as_deref())) .map(|v| (v.width, v.height)); let Some(size) = size else { @@ -1637,6 +1714,10 @@ impl OpenCADStudio { } self.save_current_sheet_set(); } + ViewAction::Callout(_) | ViewAction::Label if self.tabs[self.active_tab].scene.current_layout.eq_ignore_ascii_case("Model") => { + self.command_line + .push_error(crate::t!("Content cannot be placed into model space. Please switch to paper space and try again.").as_ref()); + } ViewAction::Callout(_) | ViewAction::Label if self.current_sheet().map(|s| s.1) != Some(sheet.clone()) => { self.command_line .push_error(crate::t!("Open the view's sheet at its layout to place a block on it.").as_ref()); @@ -1662,7 +1743,7 @@ impl OpenCADStudio { /// insert it at `at` with its attributes; their `?View.` / `?Sheet.` /// fields become the view's and sheet's navigation fields. fn insert_set_block(&mut self, i: usize, file: &str, name: &str, at: codec::types::Vector3, view: &str, sheet: &str, set: usize) { - let Ok(source) = crate::io::load_file(Path::new(file)) else { + let Some(source) = self.sheet_set.drawings.get(file) else { self.command_line.push_error(crate::tf!("{}: cannot be read.", file).as_ref()); return; }; @@ -1719,7 +1800,7 @@ impl OpenCADStudio { self.insert_set_block(i, &file, &name, Vector3::new(x, y, 0.0), &view, &sheet, set); } Placing::View { set, sheet, drawing, view } => { - let Some(mv) = crate::io::load_file(Path::new(&drawing)).ok().and_then(|d| model_view(&d, view.as_deref())) else { + let Some(mv) = self.sheet_set.drawings.get(&drawing).and_then(|d| model_view(&d, view.as_deref())) else { return; }; let title = view.clone().unwrap_or_else(|| { @@ -1791,7 +1872,8 @@ impl OpenCADStudio { /// Select Block: read a drawing's named blocks. fn select_block_file(&mut self, path: PathBuf) { - let names: Vec<(String, String, bool)> = match crate::io::load_file(&path) { + let file = path.to_string_lossy().to_string(); + let names: Vec<(String, String, bool)> = match self.sheet_set.drawings.get(&file).ok_or_else(|| crate::t!("cannot be read").into_owned()) { Ok(doc) => { let mut n: Vec<(String, String, bool)> = doc .block_records @@ -1807,9 +1889,12 @@ impl OpenCADStudio { return; } }; + // The whole drawing is the first preview. + let preview = self.block_preview(&file, ""); if let Some(SsDialog::SelectBlock(b)) = self.sheet_set.dialog.as_mut() { - b.file = path.to_string_lossy().to_string(); + b.file = file; b.names = names; + b.preview = preview; b.error = None; } } diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index f875ffd8f..b78547705 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -3041,7 +3041,7 @@ impl OpenCADStudio { auto_collapse, ), crate::ui::dock::PanelId::SheetSetManager => { - crate::ui::window::sheet_set::view(&self.sheet_set, width, auto_collapse) + crate::ui::window::sheet_set::view(&self.sheet_set, width, auto_collapse, side == crate::app::config::DockSide::Right) } }; let divider = dock_divider(id); diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 204e15eec..6814ed061 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -50,6 +50,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "New sheet location" => Some(("view", "ss-new-sheet-location")), "Remove Subset" => Some(("view", "ss-remove-subset")), "Rename Subset..." => Some(("view", "ss-rename-subset")), + "Content cannot be placed into model space. Please switch to paper space and try again." => Some(("view", "ss-content-model-space")), + "cannot be read" => Some(("view", "ss-cannot-read")), "Rename & Renumber View" => Some(("view", "ss-rename-renumber-view")), "List of Blocks" => Some(("view", "ss-list-of-blocks")), "Select Block" => Some(("view", "ss-select-block")), diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index bac7b6622..b216219be 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -227,7 +227,8 @@ impl Scene { let cam = self.camera.borrow(); let eye_dir = cam.rotation * glam::Vec3::Z; let height = cam.ortho_size() * 2.0; - let width = height; // caller can adjust; rough square + // The window's shape: the last rendered aspect ratio. + let width = height * self.active_camera_aspect(); let mut view = codec::tables::View::new(name); // The centre is in view (DCS) coordinates, relative to the target. view.center = Vector3 { x: 0.0, y: 0.0, z: 0.0 }; diff --git a/src/ui/window/block_palette.rs b/src/ui/window/block_palette.rs index 17527437a..a84fda8e9 100644 --- a/src/ui/window/block_palette.rs +++ b/src/ui/window/block_palette.rs @@ -378,6 +378,11 @@ impl<'a> Program for BlockPreviewCanvas<'a> { } } +/// A block preview (the palette's canvas) for other dialogs. +pub(crate) fn preview_canvas<'a>(wires: &'a [WireModel], height: f32) -> Element<'a, Message> { + canvas(BlockPreviewCanvas { wires }).width(Fill).height(Length::Fixed(height)).into() +} + fn pm(m: BlockPaletteMsg) -> Message { Message::BlockPalette(m) } diff --git a/src/ui/window/sheet_set.rs b/src/ui/window/sheet_set.rs index 15bdec0c3..7f49066e1 100644 --- a/src/ui/window/sheet_set.rs +++ b/src/ui/window/sheet_set.rs @@ -82,6 +82,47 @@ pub struct SheetSetManager { pub placing: Option, /// A named view to show once the sheet drawing being opened is up. pub pending_view: Option, + /// The fly-out submenu open in a row menu (its key). + pub submenu: Option, + /// Block previews (`file|name`; no name: the whole drawing). + pub previews: std::collections::HashMap>, + /// Drawings read for Model Views, placement and blocks, by path key. + pub drawings: DrawingCache, +} + +/// Drawings read from disk with their modification time; a changed file is +/// read again. +#[derive(Default)] +pub struct DrawingCache(pub std::collections::HashMap)>); + +impl std::fmt::Debug for DrawingCache { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "DrawingCache({} drawings)", self.0.len()) + } +} + +impl DrawingCache { + /// The drawing at `path`, read again when its modification time changed. + pub fn get(&mut self, path: &str) -> Option> { + let modified = std::fs::metadata(path).and_then(|m| m.modified()).ok()?; + let key = ss::path_key(path); + if let Some((t, doc)) = self.0.get(&key) { + if *t == modified { + return Some(doc.clone()); + } + } + let doc = std::sync::Arc::new(crate::io::load_file(std::path::Path::new(path)).ok()?); + self.0.insert(key, (modified, doc.clone())); + Some(doc) + } +} + +/// A row menu item: an entry, a separator, or a fly-out submenu (label, key, items). +#[derive(Debug, Clone)] +pub enum Mi { + Entry(String, Option), + Sep, + Sub(String, String, Vec<(String, Option)>), } /// What a sheet set point pick places on the current sheet. @@ -176,6 +217,8 @@ pub enum SheetSetMsg { ViewMenu(String, ViewAction), /// List of Blocks / Select Block. BlockListSelect(usize), + /// A row menu's fly-out submenu opened (hovered) or closed. + Submenu(Option), BlockListAdd, BlockListDelete, SelectBlockBrowse, @@ -407,6 +450,8 @@ pub struct BlockList { pub category: Box, pub blocks: Vec<(String, String)>, pub selected: Option, + /// Preview of the selected block (`file|name`). + pub preview: Option, } /// Select Block: a drawing, taken whole or by its named blocks. @@ -417,6 +462,8 @@ pub struct SelectBlock { pub whole: bool, /// Block name, handle, checked. pub names: Vec<(String, String, bool)>, + /// Preview shown (`file|name`). + pub preview: Option, pub error: Option, } @@ -564,7 +611,8 @@ fn plain_row<'a>( label: String, fold: Option<(bool, Message)>, double: Option, - menu: Vec<(String, Option)>, + menu: Vec, + open_sub: Option<(String, bool)>, ) -> Element<'a, Message> { let arrow: Element<'a, Message> = match fold { Some((open, m)) => button(if open { crate::ui::icons::themed_arrow_down(10.0) } else { crate::ui::icons::themed_arrow_right(10.0) }) @@ -589,27 +637,72 @@ fn plain_row<'a>( if menu.is_empty() { return area.into(); } - iced_aw::ContextMenu::new(area, move || { - let items: Vec> = menu - .iter() - .map(|(label, m)| if label.is_empty() { menu_separator() } else { menu_entry(label.clone(), m.clone()) }) - .collect(); - container(iced::widget::Column::with_children(items).spacing(1)) - .padding(4) - .width(Length::Fixed(240.0)) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color(theme.palette().background.weak.color)), - border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 4.0.into() }, - ..Default::default() - }) - .into() - }) - .into() + iced_aw::ContextMenu::new(area, move || menu_panel(&menu, open_sub.as_ref())).into() +} + +/// Height of a menu entry and of a separator (with the column spacing). +const MENU_ENTRY_H: f32 = 27.0; +const MENU_SEP_H: f32 = 2.0; + +fn menu_box<'b>(items: Vec>) -> Element<'b, Message> { + container(iced::widget::Column::with_children(items).spacing(1)) + .padding(4) + .width(Length::Fixed(240.0)) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.weak.color)), + border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 4.0.into() }, + ..Default::default() + }) + .into() +} + +/// A row menu; hovering a submenu entry opens its items beside it (`open`: +/// key and whether the fly-out goes on the left). +fn menu_panel(menu: &[Mi], open: Option<&(String, bool)>) -> Element<'static, Message> { + let mut items: Vec> = Vec::new(); + let mut flyout: Option<(f32, Element<'static, Message>)> = None; + let mut y = 0.0; + for item in menu { + match item { + Mi::Sep => { + items.push(menu_separator()); + y += MENU_SEP_H; + } + Mi::Entry(label, m) => { + items.push(mouse_area(menu_entry(label.clone(), m.clone())).on_enter(msg(SheetSetMsg::Submenu(None))).into()); + y += MENU_ENTRY_H; + } + Mi::Sub(label, key, sub) => { + let open_here = open.is_some_and(|(k, _)| k == key); + // Hover (or a right press) opens the fly-out; a click closes the menu. + let entry = menu_entry(format!("{label} ▸"), Some(msg(SheetSetMsg::Submenu(None)))); + let open_msg = msg(SheetSetMsg::Submenu(Some(key.clone()))); + items.push(mouse_area(entry).on_enter(open_msg.clone()).on_right_press(open_msg).into()); + if open_here { + let subs = sub.iter().map(|(l, m)| menu_entry(l.clone(), m.clone())).collect(); + flyout = Some((y, menu_box(subs))); + } + y += MENU_ENTRY_H; + } + } + } + let main = menu_box(items); + match flyout { + Some((offset, sub)) => { + let sub = column![Space::new().height(Length::Fixed(offset)), sub]; + // A menu opened from a right-docked palette sits left of the cursor + // (the overlay flips it at the window edge); its fly-out opens to + // the left so the menu itself does not move. + // ponytail: side from the dock, not the menu's actual position. + if open.is_some_and(|(_, left)| *left) { row![sub, main] } else { row![main, sub] }.spacing(2).into() + } + None => main, + } } /// A menu item: translated label and message (an empty label is a separator). -fn mi(label: &str, m: Option) -> (String, Option) { - (if label.is_empty() { String::new() } else { t!(label).into_owned() }, m) +fn mi(label: &str, m: Option) -> Mi { + if label.is_empty() { Mi::Sep } else { Mi::Entry(t!(label).into_owned(), m) } } fn tree_row<'a>( @@ -689,32 +782,32 @@ fn push_tree<'a>( /// A sheet view row and its menu: Display, Rename & Renumber, Set category /// (the categories as sub-items), Place Callout Block (the set's callout /// blocks as sub-items) and Place View Label Block. -fn view_row<'a>(db: &SheetSetDatabase, view: &SsElement, depth: u16) -> Element<'a, Message> { +fn view_row<'a>(state: &SheetSetManager, db: &SheetSetDatabase, view: &SsElement, depth: u16, left: bool) -> Element<'a, Message> { let id = view.id().to_string(); let act = |a: ViewAction| Some(msg(SheetSetMsg::ViewMenu(id.clone(), a))); - let mut menu = vec![mi("Display", act(ViewAction::Display)), mi("Rename & Renumber...", act(ViewAction::Rename)), mi("", None)]; - menu.push((format!("{} ▸", t!("Set category")), None)); - for c in db.view_categories() { - menu.push((format!(" {}", c.prop("Name").unwrap_or("")), act(ViewAction::Category(c.id().to_string())))); - } - menu.push((format!("{} ▸", t!("Place Callout Block")), None)); - for b in db.callout_blocks() { - menu.push((format!(" {}", b.prop("Name").unwrap_or("")), act(ViewAction::Callout(b.id().to_string())))); - } - menu.push(mi("Place View Label Block", act(ViewAction::Label))); - plain_row(depth, crate::ui::icons::DOC, ss::number_and_title(view), None, act(ViewAction::Display), menu) + let categories = db.view_categories().iter().map(|c| (c.prop("Name").unwrap_or("").to_string(), act(ViewAction::Category(c.id().to_string())))).collect(); + let callouts = db.callout_blocks().iter().map(|b| (b.prop("Name").unwrap_or("").to_string(), act(ViewAction::Callout(b.id().to_string())))).collect(); + let menu = vec![ + mi("Display", act(ViewAction::Display)), + mi("Rename & Renumber...", act(ViewAction::Rename)), + Mi::Sep, + Mi::Sub(t!("Set category").into_owned(), format!("{id}|category"), categories), + Mi::Sub(t!("Place Callout Block").into_owned(), format!("{id}|callout"), callouts), + mi("Place View Label Block", act(ViewAction::Label)), + ]; + plain_row(depth, crate::ui::icons::DOC, ss::number_and_title(view), None, act(ViewAction::Display), menu, state.submenu.clone().map(|k| (k, left))) } /// The Sheet Views page: the set, then its view categories (or sheets) and their views. -fn sheet_views_rows<'a>(state: &SheetSetManager, db: &SheetSetDatabase) -> Vec> { +fn sheet_views_rows<'a>(state: &SheetSetManager, db: &SheetSetDatabase, left: bool) -> Vec> { let new_category = || mi("New View Category...", Some(msg(SheetSetMsg::NewCategory))); - let mut rows = vec![plain_row(0, SET_ICON, db.name().to_string(), None, None, vec![new_category()])]; + let mut rows = vec![plain_row(0, SET_ICON, db.name().to_string(), None, None, vec![new_category()], None)]; let views: Vec<&SsElement> = db.sheets().into_iter().flat_map(|s| db.sheet_views(s)).collect(); if state.by_category { let named = db.view_categories(); // Views of the unnamed (default) category sit directly under the set. for v in views.iter().filter(|v| !SheetSetDatabase::view_category_of(v).is_some_and(|c| named.iter().any(|n| n.id() == c))) { - rows.push(view_row(db, v, 1)); + rows.push(view_row(state, db, v, 1, left)); } for c in named { let id = c.id().to_string(); @@ -729,9 +822,10 @@ fn sheet_views_rows<'a>(state: &SheetSetManager, db: &SheetSetDatabase) -> Vec(state: &SheetSetManager, db: &SheetSetDatabase) -> Vec(state: &SheetSetManager) -> Vec> { mi("Add New Location...", Some(msg(SheetSetMsg::AddLocation))), mi("Remove Location", Some(msg(SheetSetMsg::RemoveLocation(id.clone())))), ], + None, )); for file in files { let path = std::path::Path::new(folder).join(file).to_string_lossy().to_string(); @@ -784,6 +879,7 @@ fn model_views_rows<'a>(state: &SheetSetManager) -> Vec> { mi("Place on Sheet", Some(msg(SheetSetMsg::PlaceOnSheet(path.clone(), None)))), mi("See Model Space Views", Some(msg(SheetSetMsg::SeeViews(path.clone())))), ], + None, )); if open { for v in state.model_views.get(&key).cloned().unwrap_or_default() { @@ -798,6 +894,7 @@ fn model_views_rows<'a>(state: &SheetSetManager) -> Vec> { mi("", None), mi("Place on Sheet", Some(msg(SheetSetMsg::PlaceOnSheet(path.clone(), Some(v))))), ], + None, )); } } @@ -822,7 +919,8 @@ fn heading(label: &str) -> Element<'static, Message> { .into() } -pub fn view<'a>(state: &'a SheetSetManager, width: f32, auto_collapse: bool) -> Element<'a, Message> { +/// `dock_right`: the palette is docked on the right (fly-outs open leftwards). +pub fn view<'a>(state: &'a SheetSetManager, width: f32, auto_collapse: bool, dock_right: bool) -> Element<'a, Message> { let title_bar = crate::ui::dock::title_bar(PanelId::SheetSetManager, t!("Sheet Set Manager").into_owned(), auto_collapse); let mut picks: Vec = @@ -888,7 +986,7 @@ pub fn view<'a>(state: &'a SheetSetManager, width: f32, auto_collapse: bool) -> ] .spacing(4) .align_y(iced::Center); - column![header, scrollable(iced::widget::Column::with_children(sheet_views_rows(state, db)).spacing(1)).height(Fill)] + column![header, scrollable(iced::widget::Column::with_children(sheet_views_rows(state, db, dock_right)).spacing(1)).height(Fill)] .spacing(6) .into() } @@ -1537,15 +1635,16 @@ fn category_view<'a>(c: &'a Category) -> Element<'a, Message> { col.push(ok_cancel("OK")).into() } -fn preview_box<'a>() -> Element<'a, Message> { - container(text(t!("preview").into_owned()).size(11).style(muted_style)) - .center_x(Fill) - .center_y(Fill) - .style(card_style) - .into() +/// A block preview (the Blocks palette's), or the empty preview box. +fn preview_box<'a>(previews: &'a std::collections::HashMap>, key: Option<&String>, height: f32) -> Element<'a, Message> { + let inner: Element<'a, Message> = match key.and_then(|k| previews.get(k)) { + Some(w) if !w.is_empty() => crate::ui::window::block_palette::preview_canvas(w, height - 8.0), + _ => text(t!("preview").into_owned()).size(11).style(muted_style).into(), + }; + container(inner).center_x(Fill).center_y(Length::Fixed(height)).padding(4).style(card_style).into() } -fn block_list_view<'a>(b: &'a BlockList) -> Element<'a, Message> { +fn block_list_view<'a>(b: &'a BlockList, state: &'a SheetSetManager) -> Element<'a, Message> { let rows = b.blocks.iter().enumerate().map(|(i, (_, label))| { button(text(label.clone()).size(12)) .on_press(msg(SheetSetMsg::BlockListSelect(i))) @@ -1565,7 +1664,7 @@ fn block_list_view<'a>(b: &'a BlockList) -> Element<'a, Message> { .style(button_style(false)) .padding([5, 12]) .width(Fill), - preview_box(), + preview_box(&state.previews, b.preview.as_ref(), 84.0), ] .spacing(6) .width(Length::Fixed(130.0)); @@ -1593,7 +1692,7 @@ fn block_list_view<'a>(b: &'a BlockList) -> Element<'a, Message> { .into() } -fn select_block_view<'a>(b: &'a SelectBlock) -> Element<'a, Message> { +fn select_block_view<'a>(b: &'a SelectBlock, state: &'a SheetSetManager) -> Element<'a, Message> { let names = b.names.iter().enumerate().map(|(i, (name, _, on))| { let c = checkbox(*on).label(name.clone()).text_size(12).size(14); if b.whole { c.into() } else { c.on_toggle(move |v| msg(SheetSetMsg::SelectBlockCheck(i, v))).into() } @@ -1615,7 +1714,7 @@ fn select_block_view<'a>(b: &'a SelectBlock) -> Element<'a, Message> { .height(Length::Fixed(110.0)) .width(Fill) .style(card_style), - container(preview_box()).width(Length::Fixed(130.0)).height(Length::Fixed(110.0)), + container(preview_box(&state.previews, b.preview.as_ref(), 110.0)).width(Length::Fixed(130.0)), ] .spacing(10), ] @@ -1658,8 +1757,8 @@ pub fn dialog_view<'a>(state: &'a SheetSetManager, sizing: crate::ui::modal::Mod Some(SsDialog::Confirm(_, _, q)) => confirm_view(q), Some(SsDialog::Template(t)) => template_view(t), Some(SsDialog::Category(c)) => category_view(c), - Some(SsDialog::BlockList(b)) => block_list_view(b), - Some(SsDialog::SelectBlock(b)) => select_block_view(b), + Some(SsDialog::BlockList(b)) => block_list_view(b, state), + Some(SsDialog::SelectBlock(b)) => select_block_view(b, state), None => Space::new().into(), }; container(body).padding([10, 12]).width(sizing.width).into()