diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index b64addfeb..8f824e0c3 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -4003,6 +4003,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = إنشاء .boolean = عمليات منطقية diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 45afb7176..656076e43 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -3990,6 +3990,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = Създаване .boolean = Булеви операции diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index bad0db24e..817a1b1d1 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -3988,6 +3988,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = Vytvořit .boolean = Booleovské operace diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index af6cfdccb..10434f924 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -3989,6 +3989,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = Erstellen .boolean = Boolesche Operationen diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 455568e9e..8058b678e 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -3991,6 +3991,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = Δημιουργία .boolean = Δυαδική πράξη diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 3c3d14c22..21c6dd4df 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -3992,6 +3992,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = Create .boolean = Boolean diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index c43d88cd4..de8a0737a 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -3991,6 +3991,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = Crear .boolean = Booleanas diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index aac2495e6..090bd5e2f 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -3990,6 +3990,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = Luo .boolean = Boolen operaatiot diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 931a98dd4..9e6f9311e 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -3991,6 +3991,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = Créer .boolean = Opérations booléennes diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index a423ffb71..c80494f30 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -3979,6 +3979,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = बनाएँ .boolean = बूलियन संक्रियाएँ diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index b2a8467b9..7c37e76bf 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -3990,6 +3990,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = Létrehozás .boolean = Logikai műveletek diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 14abe736a..954d775cd 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -3990,6 +3990,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = Crea .boolean = Operazioni booleane diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index dd184826d..88caf86eb 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -3979,6 +3979,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = 作成 .boolean = ブール演算 diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index b2841a0c3..156e0146b 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -3990,6 +3990,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = 생성 .boolean = 불리언 연산 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 92bd93efc..08a40f997 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -3989,6 +3989,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = Maken .boolean = Booleaanse bewerkingen diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index bfb0bb2ba..611ee84d6 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -3990,6 +3990,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = Utwórz .boolean = Operacje boolowskie diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 0f5738b5a..b823c8f5d 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -3991,6 +3991,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = Criar .boolean = Operações booleanas diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index d08e86631..d49dfa068 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -3992,6 +3992,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = Создание .boolean = Булевы операции diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 2a1726b6b..4d018e78c 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -3951,6 +3951,14 @@ insert = .pcm-name-taken = Bu adda bir renk şeması zaten var. .pcm-bad-range = Renklendirilen nokta aralığı geçersiz. .pcm-help = Nokta bulutunun yoğunluğa veya yüksekliğe göre renklendirildiği renk şemalarını ve aralıkları ayarlar. + .pc-manager = Nokta Bulutu Yöneticisi + .pc-regions-scans = BÖLGELER VE TARAMALAR + .pc-regions = Bölgeler + .pc-unassigned = Atanmamış Noktalar + .pc-scans = Taramalar + .pc-search = Ara + .pc-collapse-all = Tümünü daralt + .pc-expand-all = Tümünü genişlet model = .create = Oluştur .boolean = Boole işlemleri diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index dde729f25..a0021231e 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -3974,6 +3974,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = 创建 .boolean = 布尔运算 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index a83ad05d8..39de64ead 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -3986,6 +3986,14 @@ insert = .pcm-name-taken = A color scheme with this name already exists. .pcm-bad-range = Invalid range of colorized points. .pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation. + .pc-manager = Point Cloud Manager + .pc-regions-scans = REGIONS AND SCANS + .pc-regions = Regions + .pc-unassigned = Unassigned Points + .pc-scans = Scans + .pc-search = Search + .pc-collapse-all = Collapse all + .pc-expand-all = Expand all model = .create = 建立 .boolean = 布林運算 diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index 6c3db7091..c39a58a6a 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -740,6 +740,8 @@ inventory::submit!(crate::command::CommandRegistration { "EXPORTSTEP", "EXPORTSTL", "EXTERNALREFERENCES", + "POINTCLOUDMANAGER", + "POINTCLOUDMANAGERCLOSE", "EXTRIM", "FILETAB", "FIND", diff --git a/src/app/commands/pc_extract.rs b/src/app/commands/pc_extract.rs index d00a7aa21..53f9b6ab0 100644 --- a/src/app/commands/pc_extract.rs +++ b/src/app/commands/pc_extract.rs @@ -34,7 +34,13 @@ impl OpenCADStudio { common.color.clone() }; let color = color.rgb().map_or([255; 3], |(r, g, b)| [r, g, b]); - if let Some(placed) = crate::scene::model::point_cloud::placed(document, data, color, None) { + if let Some(placed) = crate::scene::model::point_cloud::placed( + document, + data, + color, + None, + &crate::scene::model::point_cloud::hidden(common), + ) { clouds.push((common.handle, placed)); } } diff --git a/src/app/commands/pdf_underlay.rs b/src/app/commands/pdf_underlay.rs index 14ce8e662..dcd5f839c 100644 --- a/src/app/commands/pdf_underlay.rs +++ b/src/app/commands/pdf_underlay.rs @@ -162,6 +162,59 @@ impl OpenCADStudio { self.refresh_properties(); } + /// A Point Cloud Manager click: a switch writes the cloud's visibility + /// extended data as one undo step; the rest is palette state. + pub(in crate::app) fn update_pc_manager(&mut self, message: crate::ui::window::pc_manager::PcManagerMsg) -> Task { + use crate::scene::model::point_cloud::VISIBILITY_APP; + use crate::ui::window::pc_manager::{self, PcManagerMsg}; + let i = self.active_tab; + match message { + PcManagerMsg::Toggle(handle, row) => { + let Some(cloud) = + pc_manager::clouds(&self.tabs[i].scene.document, &[]).into_iter().find(|c| c.handle == handle) + else { + return Task::none(); + }; + if self.tabs[i].scene.is_layer_locked(handle) { + return Task::none(); + } + let hidden = pc_manager::toggled(&cloud.hidden, &cloud.scans, &row); + self.push_undo_snapshot(i, "POINTCLOUDMANAGER"); + self.tabs[i].scene.ensure_app_id(VISIBILITY_APP); + let app_handle = self.tabs[i].scene.document.app_ids.get(VISIBILITY_APP).map(|a| a.handle.value()); + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { + let data = &mut entity.common_mut().extended_data; + if let Some(app) = app_handle { + data.raw_dwg_eed.retain(|(a, _)| *a != app); + } + data.remove_record(VISIBILITY_APP); + if !hidden.is_empty() { + let mut record = codec::xdata::ExtendedDataRecord::new(VISIBILITY_APP); + for name in hidden { + record.add_value(codec::xdata::XDataValue::String(name)); + } + data.add_record(record); + } + } + self.tabs[i].scene.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]); + self.tabs[i].dirty = true; + } + PcManagerMsg::Expand(key) => { + if !self.pc_manager.collapsed.remove(&key) { + self.pc_manager.collapsed.insert(key); + } + } + PcManagerMsg::Select(key) => self.pc_manager.selected = Some(key), + PcManagerMsg::Search(text) => self.pc_manager.search = text, + PcManagerMsg::CollapseAll => { + let clouds = pc_manager::clouds(&self.tabs[i].scene.document, &[]); + self.pc_manager.collapsed.extend(pc_manager::folding_keys(&clouds)); + } + PcManagerMsg::ExpandAll => self.pc_manager.collapsed.clear(), + } + Task::none() + } + /// One undo step that edits every selected PDF underlay. fn edit_selected_underlays(&mut self, i: usize, label: &str, edit: impl Fn(&mut Underlay)) { let handles: Vec<_> = self @@ -319,7 +372,23 @@ impl OpenCADStudio { crop.inverted = !crop.inverted; } }), - "_PCUNCROP" => self.edit_selected_point_clouds(i, "POINTCLOUDUNCROP", |data| data.croppings.clear()), + // The Point Cloud Manager docks on the right like External + // References and opens expanded. + "POINTCLOUDMANAGER" => { + let id = crate::ui::dock::PanelId::PointCloudManager; + self.pc_manager.show = true; + if self.dock.location(id).is_none() { + self.dock.dock(id, crate::app::config::DockSide::Right, usize::MAX); + } + self.dock_expanded = Some(id); + } + "POINTCLOUDMANAGERCLOSE" => { + self.pc_manager.show = false; + if self.dock_expanded == Some(crate::ui::dock::PanelId::PointCloudManager) { + self.dock_expanded = None; + } + } + "_PCUNCROP" =>self.edit_selected_point_clouds(i, "POINTCLOUDUNCROP", |data| data.croppings.clear()), "_PDFULUNCLIP" => { self.edit_selected_underlays(i, "PDFCLIP", |u| { u.clip_boundary_vertices.clear(); diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index 2885aa7d3..62ea3f2b3 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -71,6 +71,7 @@ pub(super) const NAMES: &[&str] = &[ "close_modal", "pdf_dialog_ok", "pdf_layer_toggle", + "pc_manager_toggle", "pdf_page_select", "pc_colormap", "ribbon_tab", @@ -719,6 +720,19 @@ impl OpenCADStudio { _ => return Err(failure("invalid_value", "Unknown color map edit")), }) } + // Point Cloud Manager: flip a row's switch, ":" with + // row cloud, unassigned, scans or scan:. + "pc_manager_toggle" => { + use crate::ui::window::pc_manager::{PcManagerMsg, Row}; + let value = string(req, "value")?; + let parsed = value.split_once(':').and_then(|(handle, row)| { + Some((codec::Handle::new(u64::from_str_radix(handle, 16).ok()?), Row::parse(row)?)) + }); + let Some((handle, row)) = parsed else { + return Err(failure("bad_value", "Expected :")); + }; + Message::PcManager(PcManagerMsg::Toggle(handle, row)) + } // Attach dialog: choose pages by index ("0,2"). "pdf_page_select" => { let pages: Vec = string(req, "value")? diff --git a/src/app/mod.rs b/src/app/mod.rs index 1d7ab9361..231d83b98 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -793,6 +793,8 @@ pub(super) struct OpenCADStudio { /// Whether the Browser panel is shown. Off until BROWSER opens it, so /// the default layout is unchanged for existing users. pub(crate) show_browser: bool, + /// Point Cloud Manager palette (POINTCLOUDMANAGER). + pub(crate) pc_manager: crate::ui::window::pc_manager::PcManager, /// Which viewport background the colour wheel is editing, or `None` when /// it is closed. One slot, because only one wheel can be open at a time. pub(crate) bg_picker: Option, @@ -3845,6 +3847,8 @@ pub enum Message { PdfImportPick, /// An edit in one of the PDF dialogs. PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg), + /// A click or search in the Point Cloud Manager. + PcManager(crate::ui::window::pc_manager::PcManagerMsg), PdfImportPickResult(Result<(std::path::PathBuf, std::sync::Arc>), String>), /// Result of the PDFATTACH file picker. PdfAttachPickResult(Result<(std::path::PathBuf, std::sync::Arc>), String>), @@ -4160,6 +4164,7 @@ impl OpenCADStudio { graph_undo_open: false, show_external_references: false, show_browser: false, + pc_manager: Default::default(), bg_picker: None, block_palette: Default::default(), xref_manager: Default::default(), diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index 4cbcf237e..1ced6a977 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -608,6 +608,9 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven PanelId::NodeGraph => { self.show_node_graph = false; } + PanelId::PointCloudManager => { + self.pc_manager.show = false; + } PanelId::Properties => { self.show_properties = false; self.ribbon.set_properties(false); @@ -712,6 +715,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven PanelId::ExternalReferences => self.show_external_references, PanelId::Browser => self.show_browser, PanelId::NodeGraph => self.show_node_graph, + PanelId::PointCloudManager => self.pc_manager.show, } } diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index e1f58e1f7..355f65358 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -1806,6 +1806,7 @@ impl OpenCADStudio { } Message::PdfDialog(message) => self.update_pdf_dialog(message), + Message::PcManager(message) => self.update_pc_manager(message), Message::RibbonSelectTab(idx) => { self.ribbon.select(idx); Task::none() diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index b9ee67da6..e0452c29e 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1870,6 +1870,7 @@ bg={bg_ms:.1}ms n={view_count}" crate::ui::dock::PanelId::ExternalReferences => self.show_external_references, crate::ui::dock::PanelId::Browser => self.show_browser, crate::ui::dock::PanelId::NodeGraph => self.show_node_graph, + crate::ui::dock::PanelId::PointCloudManager => self.pc_manager.show, } }; let edge_stack = |side: crate::app::config::DockSide| -> Option> { @@ -2994,6 +2995,13 @@ impl OpenCADStudio { auto_collapse, ), crate::ui::dock::PanelId::NodeGraph => tab.graph.panel(width, auto_collapse), + crate::ui::dock::PanelId::PointCloudManager => crate::ui::window::pc_manager::view( + &self.pc_manager, + &tab.scene.document, + &tab.scene.selected_handles_in_order(), + width, + auto_collapse, + ), }; let divider = dock_divider(id); match side { diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index ff97a8732..6498ca12e 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -134,6 +134,14 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "A color scheme with this name already exists." => Some(("insert", "pcm-name-taken")), "Invalid range of colorized points." => Some(("insert", "pcm-bad-range")), "Sets the color schemes and ranges a point cloud is colored by intensity or elevation." => Some(("insert", "pcm-help")), + "Point Cloud Manager" => Some(("insert", "pc-manager")), + "REGIONS AND SCANS" => Some(("insert", "pc-regions-scans")), + "Regions" => Some(("insert", "pc-regions")), + "Unassigned Points" => Some(("insert", "pc-unassigned")), + "Scans" => Some(("insert", "pc-scans")), + "Search" => Some(("insert", "pc-search")), + "Collapse all" => Some(("insert", "pc-collapse-all")), + "Expand all" => Some(("insert", "pc-expand-all")), "Stylization" => Some(("properties", "stylization")), "Color scheme" => Some(("properties", "color-scheme")), "Scan Colors" => Some(("properties", "scan-colors")), diff --git a/src/scene/mod.rs b/src/scene/mod.rs index c71f877dd..ed97a2923 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -8403,7 +8403,13 @@ impl Scene { common.color.clone() }; let object_color = color.rgb().map_or([255; 3], |(r, g, b)| [r, g, b]); - clouds.extend(point_cloud::placed(&self.document, data, object_color, transform)); + clouds.extend(point_cloud::placed( + &self.document, + data, + object_color, + transform, + &point_cloud::hidden(common), + )); }, ); if clouds.is_empty() { diff --git a/src/scene/model/point_cloud/mod.rs b/src/scene/model/point_cloud/mod.rs index 7c1da4378..6503fbcee 100644 --- a/src/scene/model/point_cloud/mod.rs +++ b/src/scene/model/point_cloud/mod.rs @@ -31,6 +31,8 @@ pub struct CloudPoints { pub has_normals: bool, /// Scans read (one for a scan file). pub scans: usize, + /// Each scan's name (its file stem) and its points' index range. + pub scan_ranges: Vec<(String, std::ops::Range)>, /// A project's preview picture (JPEG). pub preview: Option>, /// Bounds from the scans' headers, placed like their points: the @@ -70,18 +72,19 @@ fn decode(path: &Path) -> Option { .is_some_and(|e| e.eq_ignore_ascii_case("rcp")); // A project lists its scans with their own transforms; a scan file // carries the same transform in its header. - let scans: Vec<(rcs::Scan, [[f64; 3]; 3])> = if is_project { + let stem = |p: &Path| p.file_stem().map_or_else(String::new, |s| s.to_string_lossy().into_owned()); + let scans: Vec<(rcs::Scan, [[f64; 3]; 3], String)> = if is_project { rcp::scans(&bytes, path)? .into_iter() .filter_map(|entry| { let scan = rcs::decode(&std::fs::read(&entry.path).ok()?)?; - Some((scan, [entry.translation, entry.rotation, entry.scale])) + Some((scan, [entry.translation, entry.rotation, entry.scale], stem(&entry.path))) }) .collect() } else { let scan = rcs::decode(&bytes)?; let transform = [scan.translation, scan.rotation, scan.scale]; - vec![(scan, transform)] + vec![(scan, transform, stem(path))] }; let mut cloud = CloudPoints { id: NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed), @@ -89,21 +92,24 @@ fn decode(path: &Path) -> Option { colors: Vec::new(), intensity: Vec::new(), normals: Vec::new(), - has_rgb: scans.iter().any(|(scan, _)| scan.has_rgb), - has_intensity: scans.iter().any(|(scan, _)| scan.has_intensity), - has_normals: scans.iter().any(|(scan, _)| scan.has_normals), + has_rgb: scans.iter().any(|(scan, ..)| scan.has_rgb), + has_intensity: scans.iter().any(|(scan, ..)| scan.has_intensity), + has_normals: scans.iter().any(|(scan, ..)| scan.has_normals), scans: scans.len(), + scan_ranges: Vec::new(), preview: is_project.then(|| rcp::preview(&bytes)).flatten(), bounds: None, }; - for (scan, [translation, rotation, scale]) in scans { + for (scan, [translation, rotation, scale], name) in scans { let rotate = euler(rotation.map(f64::to_radians)); let place = |p: &[f64; 3]| { let s = [p[0] * scale[0], p[1] * scale[1], p[2] * scale[2]]; let r = rotate.map(|row| row[0] * s[0] + row[1] * s[1] + row[2] * s[2]); [r[0] + translation[0], r[1] + translation[1], r[2] + translation[2]] }; + let first = cloud.positions.len(); cloud.positions.extend(scan.local.iter().map(place)); + cloud.scan_ranges.push((name, first..cloud.positions.len())); let [lo, hi] = scan.bounds; for corner in 0..8 { let p = place(&[ @@ -249,16 +255,46 @@ pub(crate) fn any_definition(document: &CadDocument) -> bool { }) } +/// What the Point Cloud Manager turned off on a cloud: extended data under +/// this application, one string per hidden scan plus the markers below. +pub const VISIBILITY_APP: &str = "OCS_POINTCLOUD_VISIBILITY"; +/// The whole cloud is off. +pub const CLOUD_OFF: &str = "*CLOUD_OFF"; +/// Its unassigned points (every point of a cloud without regions) are off. +pub const UNASSIGNED_OFF: &str = "*UNASSIGNED_OFF"; + +/// The scans and markers a cloud entity hides. +pub(crate) fn hidden(common: &codec::entities::EntityCommon) -> Vec { + common + .extended_data + .records() + .iter() + .filter(|r| r.application_name.eq_ignore_ascii_case(VISIBILITY_APP)) + .flat_map(|r| r.values.iter()) + .filter_map(|v| match v { + codec::xdata::XDataValue::String(s) => Some(s.clone()), + _ => None, + }) + .collect() +} + /// The points `data` shows, placed through its origin and axes and then -/// `transform` (a block instance), with its crops applied. `None` when the -/// scan file is missing or unreadable. +/// `transform` (a block instance), with its crops applied and the `hidden` +/// scans left out. `None` when the scan file is missing or unreadable. pub(crate) fn placed( document: &CadDocument, data: &PointCloudExData, object_color: [u8; 3], transform: Option<&Transform>, + hidden: &[String], ) -> Option> { let cloud = load(&resolve_source(document, data)?)?; + let skipped: Vec> = + if hidden.iter().any(|h| h == CLOUD_OFF || h == UNASSIGNED_OFF) { + vec![0..cloud.positions.len()] + } else { + cloud.scan_ranges.iter().filter(|(name, _)| hidden.contains(name)).map(|(_, r)| r.clone()).collect() + }; // The points shown: POINTCLOUDDENSITY per cent of POINTCLOUDPOINTMAX, // in tenths by POINTCLOUDLOD, every n-th point of a larger cloud. let shown = |name| setting(name).map_or(0, |s| setting_value(document, s)); @@ -298,6 +334,7 @@ pub(crate) fn placed( } step.hash(&mut h); style.hash(&mut h); + skipped.hash(&mut h); let key = h.finish(); type Placed = Mutex>>; @@ -369,7 +406,7 @@ pub(crate) fn placed( }; let mut instances = Vec::with_capacity(cloud.positions.len() / step + 1); for (i, p) in cloud.positions.iter().enumerate().step_by(step) { - if !crops.iter().all(|crop| keeps(crop, p)) { + if skipped.iter().any(|r| r.contains(&i)) || !crops.iter().all(|crop| keeps(crop, p)) { continue; } let w = world(p); diff --git a/src/ui/dock.rs b/src/ui/dock.rs index e379a3f71..1e62a58cf 100644 --- a/src/ui/dock.rs +++ b/src/ui/dock.rs @@ -46,6 +46,8 @@ pub enum PanelId { Browser, /// Node library of the node graph overlay. NodeGraph, + /// Regions and scans of the drawing's point clouds. + PointCloudManager, } impl PanelId { @@ -57,6 +59,7 @@ impl PanelId { PanelId::ExternalReferences => "External References", PanelId::Browser => "Browser", PanelId::NodeGraph => "Node Graph", + PanelId::PointCloudManager => "Point Cloud Manager", } } @@ -68,6 +71,7 @@ impl PanelId { PanelId::ExternalReferences => 460.0, PanelId::Browser => 230.0, PanelId::NodeGraph => 220.0, + PanelId::PointCloudManager => 280.0, } } @@ -157,6 +161,7 @@ impl DockState { PanelId::ExternalReferences, PanelId::Browser, PanelId::NodeGraph, + PanelId::PointCloudManager, ] { self.panels.entry(id).or_insert_with(|| DockPanel::for_id(id)); } diff --git a/src/ui/window/mod.rs b/src/ui/window/mod.rs index 20146b6cd..260b93cde 100644 --- a/src/ui/window/mod.rs +++ b/src/ui/window/mod.rs @@ -24,6 +24,7 @@ pub mod annotation_data; pub mod alias_editor; pub mod find_replace; pub mod named_parameters; +pub mod pc_manager; pub mod pdf_dialogs; pub mod xref_attach; pub mod xref_help; diff --git a/src/ui/window/pc_manager.rs b/src/ui/window/pc_manager.rs new file mode 100644 index 000000000..75f1bf6fa --- /dev/null +++ b/src/ui/window/pc_manager.rs @@ -0,0 +1,326 @@ +//! Point Cloud Manager (POINTCLOUDMANAGER): the regions and scans of the +//! drawing's point clouds as a tree, each with a switch that shows or hides +//! its points. The switches are the cloud's own visibility extended data, so +//! the tree is read straight from the document on every view. + +use crate::app::Message; +use crate::scene::model::point_cloud::{self, CLOUD_OFF, UNASSIGNED_OFF}; +use crate::ui::dock::PanelId; +use codec::entities::ExtendedEntityData; +use codec::{CadDocument, EntityType, Handle}; +use iced::widget::{button, checkbox, column, container, mouse_area, row, scrollable, text, text_input, tooltip, Space}; +use iced::{Background, Border, Element, Fill, Length, Theme}; +use std::collections::HashSet; + +const CLOUD_ICON: &[u8] = include_bytes!("../../../assets/icons/pc_attach.svg"); +const REGION_ICON: &[u8] = include_bytes!("../../../assets/icons/region.svg"); +const UNASSIGNED_ICON: &[u8] = include_bytes!("../../../assets/icons/revcloud.svg"); + +/// Palette state that is not in the drawing: shown, folded nodes, the +/// highlighted row and the search text. +#[derive(Debug, Default)] +pub struct PcManager { + pub show: bool, + pub collapsed: HashSet, + pub selected: Option, + pub search: String, +} + +/// A row with a switch. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Row { + Cloud, + Unassigned, + Scans, + Scan(String), +} + +impl Row { + /// `cloud`, `unassigned`, `scans` or `scan:` (automation). + pub fn parse(text: &str) -> Option { + match text { + "cloud" => Some(Self::Cloud), + "unassigned" => Some(Self::Unassigned), + "scans" => Some(Self::Scans), + _ => text.strip_prefix("scan:").map(|name| Self::Scan(name.to_string())), + } + } +} + +#[derive(Debug, Clone)] +pub enum PcManagerMsg { + Toggle(Handle, Row), + Expand(String), + Select(String), + Search(String), + CollapseAll, + ExpandAll, +} + +/// One cloud of the tree. +pub struct Cloud { + pub handle: Handle, + pub name: String, + pub hidden: Vec, + pub scans: Vec, +} + +/// The drawing's point clouds; only the selected ones when any is selected. +pub fn clouds(document: &CadDocument, selected: &[Handle]) -> Vec { + let all: Vec = document + .entities() + .filter_map(|entity| { + let EntityType::Extended(extended) = entity else { + return None; + }; + let ExtendedEntityData::PointCloudEx(data) = &extended.data else { + return None; + }; + let common = entity.common(); + let scans = point_cloud::resolve_source(document, data) + .and_then(|path| point_cloud::load(&path)) + .map(|cloud| cloud.scan_ranges.iter().map(|(name, _)| name.clone()).collect()) + .unwrap_or_default(); + Some(Cloud { + handle: common.handle, + name: if data.name.is_empty() { format!("{:X}", common.handle.value()) } else { data.name.clone() }, + hidden: point_cloud::hidden(common), + scans, + }) + }) + .collect(); + if all.iter().any(|cloud| selected.contains(&cloud.handle)) { + all.into_iter().filter(|cloud| selected.contains(&cloud.handle)).collect() + } else { + all + } +} + +/// Whether a row's switch is on. A parent is on while any child is. +pub fn is_on(hidden: &[String], scans: &[String], row: &Row) -> bool { + let off = |name: &str| hidden.iter().any(|h| h == name); + let any_scan = scans.is_empty() || scans.iter().any(|s| !off(s)); + match row { + Row::Scan(name) => !off(name), + Row::Scans => any_scan, + Row::Unassigned => !off(UNASSIGNED_OFF), + Row::Cloud => !off(CLOUD_OFF) && (!off(UNASSIGNED_OFF) || any_scan), + } +} + +/// The hidden list after flipping `row`. Switching the cloud back on shows +/// everything under it. +pub fn toggled(hidden: &[String], scans: &[String], row: &Row) -> Vec { + fn set(out: &mut Vec, name: &str, off: bool) { + out.retain(|h| h != name); + if off { + out.push(name.to_string()); + } + } + let on = is_on(hidden, scans, row); + let mut out = hidden.to_vec(); + match row { + Row::Cloud if on => set(&mut out, CLOUD_OFF, true), + Row::Cloud => out.clear(), + Row::Unassigned => set(&mut out, UNASSIGNED_OFF, on), + Row::Scans => scans.iter().for_each(|s| set(&mut out, s, on)), + Row::Scan(name) => set(&mut out, name, on), + } + out +} + +/// Keys of the rows that fold: each cloud and its Scans. +pub fn folding_keys(clouds: &[Cloud]) -> Vec { + clouds.iter().flat_map(|c| [key(c.handle), format!("{}/scans", key(c.handle))]).collect() +} + +fn key(handle: Handle) -> String { + format!("{:X}", handle.value()) +} + +fn msg(m: PcManagerMsg) -> Message { + Message::PcManager(m) +} + +/// One tree row: indent, fold arrow, icon, label and switch (`None` for a +/// row that has none to give, drawn off and disabled). +fn tree_row<'a>( + state: &PcManager, + key: String, + depth: u16, + fold: Option, + icon: Element<'a, Message>, + label: String, + switch: Option<(bool, Message)>, +) -> Element<'a, Message> { + let arrow: Element<'a, Message> = match fold { + Some(open) => button(if open { + crate::ui::icons::themed_arrow_down(10.0) + } else { + crate::ui::icons::themed_arrow_right(10.0) + }) + .on_press(msg(PcManagerMsg::Expand(key.clone()))) + .style(button::text) + .padding(2) + .into(), + None => Space::new().width(14).into(), + }; + let enabled = switch.is_some(); + let label = text(label).size(12).width(Fill); + let label = if enabled { label } else { label.style(crate::ui::style::common::muted_style) }; + let switch: Element<'a, Message> = match switch { + Some((on, message)) => checkbox(on).on_toggle(move |_| message.clone()).size(14).into(), + None => checkbox(false).size(14).into(), + }; + let selected = state.selected.as_deref() == Some(key.as_str()); + let cells = row![Space::new().width(Length::Fixed(f32::from(depth) * 16.0)), arrow, icon, label, switch] + .spacing(5) + .align_y(iced::Center); + mouse_area( + container(cells) + .width(Fill) + .padding([3, 6]) + .style(move |theme: &Theme| container::Style { + background: selected.then(|| Background::Color(theme.palette().primary.weak.color)), + text_color: selected.then(|| theme.palette().primary.weak.text), + ..Default::default() + }), + ) + .on_press(msg(PcManagerMsg::Select(key))) + .into() +} + +fn fold_button<'a>(icon: &'static [u8], tip: String, message: PcManagerMsg) -> Element<'a, Message> { + let b = button(crate::ui::icons::themed_secondary(icon, 12.0)) + .on_press(msg(message)) + .style(button::subtle) + .padding([2, 4]); + tooltip(b, text(tip).size(10), tooltip::Position::Bottom).gap(4).into() +} + +pub fn view<'a>( + state: &'a PcManager, + document: &'a CadDocument, + selected: &[Handle], + width: f32, + auto_collapse: bool, +) -> Element<'a, Message> { + let title_bar = + crate::ui::dock::title_bar(PanelId::PointCloudManager, crate::t!("Point Cloud Manager").into_owned(), auto_collapse); + let query = state.search.trim().to_lowercase(); + let matches = |label: &str| query.is_empty() || label.to_lowercase().contains(&query); + // Searching opens every node so the matches show. + let open = |key: &str| !query.is_empty() || !state.collapsed.contains(key); + let regions = crate::t!("Regions").into_owned(); + let unassigned = crate::t!("Unassigned Points").into_owned(); + let scans_label = crate::t!("Scans").into_owned(); + + let mut tree = column![].spacing(1); + for cloud in clouds(document, selected) { + let root = key(cloud.handle); + let scans: Vec<&String> = cloud.scans.iter().filter(|s| matches(s)).collect(); + let show_scans = matches(&scans_label) || !scans.is_empty(); + let children = matches(®ions) || matches(&unassigned) || show_scans; + if !matches(&cloud.name) && !children { + continue; + } + let switch = |row: Row| { + let on = is_on(&cloud.hidden, &cloud.scans, &row); + Some((on, msg(PcManagerMsg::Toggle(cloud.handle, row)))) + }; + let cloud_icon = || crate::ui::icons::semantic(CLOUD_ICON, 14.0); + tree = tree.push(tree_row( + state, + root.clone(), + 0, + Some(open(&root)), + cloud_icon(), + cloud.name.clone(), + switch(Row::Cloud), + )); + if !open(&root) { + continue; + } + if matches(®ions) { + // Region data is not read: the row is there, off and disabled. + tree = tree.push(tree_row( + state, + format!("{root}/regions"), + 1, + None, + crate::ui::icons::semantic_disabled(REGION_ICON, 14.0), + regions.clone(), + None, + )); + } + if matches(&unassigned) { + tree = tree.push(tree_row( + state, + format!("{root}/unassigned"), + 1, + None, + crate::ui::icons::semantic(UNASSIGNED_ICON, 14.0), + unassigned.clone(), + switch(Row::Unassigned), + )); + } + if show_scans { + let scans_key = format!("{root}/scans"); + let scans_open = open(&scans_key); + tree = tree.push(tree_row( + state, + scans_key.clone(), + 1, + Some(scans_open), + cloud_icon(), + scans_label.clone(), + switch(Row::Scans), + )); + if scans_open { + for name in scans { + tree = tree.push(tree_row( + state, + format!("{scans_key}/{name}"), + 2, + None, + cloud_icon(), + name.clone(), + switch(Row::Scan(name.clone())), + )); + } + } + } + } + + let heading = text(crate::t!("REGIONS AND SCANS").into_owned()) + .size(11) + .font(iced::Font { + weight: iced::font::Weight::Bold, + style: iced::font::Style::Italic, + ..iced::Font::DEFAULT + }) + .style(|theme: &Theme| text::Style { color: Some(theme.palette().primary.base.color) }) + .width(Fill); + let header = row![ + heading, + fold_button(crate::ui::icons::MINUS, crate::t!("Collapse all").into_owned(), PcManagerMsg::CollapseAll), + fold_button(crate::ui::icons::PLUS, crate::t!("Expand all").into_owned(), PcManagerMsg::ExpandAll), + ] + .spacing(2) + .align_y(iced::Center); + let card = container(column![header, scrollable(tree).height(Fill)].spacing(6)) + .padding(6) + .width(Fill) + .height(Fill) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.weak.color)), + border: Border { radius: 4.0.into(), ..Default::default() }, + ..Default::default() + }); + let search = text_input(&crate::t!("Search"), &state.search) + .on_input(|v| msg(PcManagerMsg::Search(v))) + .padding([4, 8]) + .size(12); + + crate::ui::dock::frame(column![title_bar, card, search].spacing(6), width) +}