Merge commit 'ac9a6b07' into codex/pointcloud-attach

# Conflicts:
#	locales/ar-SA/opencadstudio.ftl
#	locales/bg-BG/opencadstudio.ftl
#	locales/cs-CZ/opencadstudio.ftl
#	locales/de-DE/opencadstudio.ftl
#	locales/el-GR/opencadstudio.ftl
#	locales/en-US/opencadstudio.ftl
#	locales/es-ES/opencadstudio.ftl
#	locales/fi-FI/opencadstudio.ftl
#	locales/fr-FR/opencadstudio.ftl
#	locales/hi-IN/opencadstudio.ftl
#	locales/hu-HU/opencadstudio.ftl
#	locales/it-IT/opencadstudio.ftl
#	locales/ja-JP/opencadstudio.ftl
#	locales/ko-KR/opencadstudio.ftl
#	locales/nl-NL/opencadstudio.ftl
#	locales/pl-PL/opencadstudio.ftl
#	locales/pt-BR/opencadstudio.ftl
#	locales/ru-RU/opencadstudio.ftl
#	locales/tr-TR/opencadstudio.ftl
#	locales/zh-CN/opencadstudio.ftl
#	locales/zh-TW/opencadstudio.ftl
#	src/app/control/actions.rs
#	src/locale_catalog.rs
This commit is contained in:
ramox81 committed 2026-09-30 13:26:52 +03:00
commit ee0d4aedb5
35 files changed
+673 -13

No files matched your search

+8
View File
@@ -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 = عمليات منطقية
+8
View File
@@ -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 = Булеви операции
+8
View File
@@ -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
+8
View File
@@ -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
+8
View File
@@ -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 = Δυαδική πράξη
+8
View File
@@ -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
+8
View File
@@ -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
+8
View File
@@ -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
+8
View File
@@ -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
+8
View File
@@ -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 = बूलियन संक्रियाएँ
+8
View File
@@ -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
+8
View File
@@ -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
+8
View File
@@ -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 = ブール演算
+8
View File
@@ -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 = 불리언 연산
+8
View File
@@ -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
+8
View File
@@ -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
+8
View File
@@ -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
+8
View File
@@ -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 = Булевы операции
+8
View File
@@ -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
+8
View File
@@ -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 = 布尔运算
+8
View File
@@ -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 = 布林運算
+2
View File
@@ -740,6 +740,8 @@ inventory::submit!(crate::command::CommandRegistration {
"EXPORTSTEP",
"EXPORTSTL",
"EXTERNALREFERENCES",
"POINTCLOUDMANAGER",
"POINTCLOUDMANAGERCLOSE",
"EXTRIM",
"FILETAB",
"FIND",
+7 -1
View File
@@ -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));
}
}
+70 -1
View File
@@ -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<Message> {
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();
+14
View File
@@ -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, "<handle>:<row>" with
// row cloud, unassigned, scans or scan:<name>.
"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 <handle>:<row>"));
};
Message::PcManager(PcManagerMsg::Toggle(handle, row))
}
// Attach dialog: choose pages by index ("0,2").
"pdf_page_select" => {
let pages: Vec<usize> = string(req, "value")?
+5
View File
@@ -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<BgTarget>,
@@ -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<Vec<u8>>), String>),
/// Result of the PDFATTACH file picker.
PdfAttachPickResult(Result<(std::path::PathBuf, std::sync::Arc<Vec<u8>>), 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(),
+4
View File
@@ -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,
}
}
+1
View File
@@ -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()
+8
View File
@@ -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<Element<'_, Message>> {
@@ -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 {
+8
View File
@@ -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")),
+7 -1
View File
@@ -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() {
+47 -10
View File
@@ -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<usize>)>,
/// A project's preview picture (JPEG).
pub preview: Option<Vec<u8>>,
/// Bounds from the scans' headers, placed like their points: the
@@ -70,18 +72,19 @@ fn decode(path: &Path) -> Option<CloudPoints> {
.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<CloudPoints> {
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<String> {
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<Arc<PlacedCloud>> {
let cloud = load(&resolve_source(document, data)?)?;
let skipped: Vec<std::ops::Range<usize>> =
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<FxHashMap<u64, Weak<PlacedCloud>>>;
@@ -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);
+5
View File
@@ -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));
}
+1
View File
@@ -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;
+326
View File
@@ -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<String>,
pub selected: Option<String>,
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:<name>` (automation).
pub fn parse(text: &str) -> Option<Self> {
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<String>,
pub scans: Vec<String>,
}
/// The drawing's point clouds; only the selected ones when any is selected.
pub fn clouds(document: &CadDocument, selected: &[Handle]) -> Vec<Cloud> {
let all: Vec<Cloud> = 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<String> {
fn set(out: &mut Vec<String>, 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<String> {
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<bool>,
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(&regions) || 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(&regions) {
// 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)
}