Point clouds: POINTCLOUDCOLORMAP dialog, out-of-range colouring

POINTCLOUDCOLORMAP asks for a cloud (or None for the drawing's default
schemes) and opens the Point Cloud Color Map dialog: intensity and
elevation tabs with the scheme list, New / Delete / Rename, number of
colours (2-25, resampled along the scheme), blend between the ends,
reverse, gradient or bands, the colorized range, the out-of-range
choice and, for elevation, the cloud's extents or a fixed range with its
band height. OK / Apply write the drawing's colour map (created under
ACAD_POINTCLOUD_COLORMAP_DICT when missing) and the cloud's colouring in
one undo step; Make stylization current sets the tab's stylization.
Values outside a scheme's range take the end colours, the scan colours
or are hidden. Automation action pc_colormap edits the dialog.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-09-30 13:10:45 +03:00
1 parent 7761652a9f
commit 2eaae1ecab
32 files changed
+1449 -21

No files matched your search

+19
View File
@@ -3984,6 +3984,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = إنشاء
.boolean = عمليات منطقية
+19
View File
@@ -3971,6 +3971,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = Създаване
.boolean = Булеви операции
+19
View File
@@ -3969,6 +3969,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = Vytvořit
.boolean = Booleovské operace
+19
View File
@@ -3970,6 +3970,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = Erstellen
.boolean = Boolesche Operationen
+19
View File
@@ -3972,6 +3972,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = Δημιουργία
.boolean = Δυαδική πράξη
+19
View File
@@ -3973,6 +3973,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = Create
.boolean = Boolean
+19
View File
@@ -3972,6 +3972,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = Crear
.boolean = Booleanas
+19
View File
@@ -3971,6 +3971,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = Luo
.boolean = Boolen operaatiot
+19
View File
@@ -3972,6 +3972,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = Créer
.boolean = Opérations booléennes
+19
View File
@@ -3960,6 +3960,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = बनाएँ
.boolean = बूलियन संक्रियाएँ
+19
View File
@@ -3971,6 +3971,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = Létrehozás
.boolean = Logikai műveletek
+19
View File
@@ -3971,6 +3971,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = Crea
.boolean = Operazioni booleane
+19
View File
@@ -3960,6 +3960,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = 作成
.boolean = ブール演算
+19
View File
@@ -3971,6 +3971,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = 생성
.boolean = 불리언 연산
+19
View File
@@ -3970,6 +3970,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = Maken
.boolean = Booleaanse bewerkingen
+19
View File
@@ -3971,6 +3971,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = Utwórz
.boolean = Operacje boolowskie
+19
View File
@@ -3972,6 +3972,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = Criar
.boolean = Operações booleanas
+19
View File
@@ -3973,6 +3973,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = Создание
.boolean = Булевы операции
+19
View File
@@ -3932,6 +3932,25 @@ insert =
.pc-rcs = Nokta Bulutu Taraması
.pc-clouds = Nokta Bulutları
.pc-help = Bir nokta bulutu taramasını veya projesini bağlar.
.pcm-title = Nokta Bulutu Renk Haritası
.pcm-ramp = Renk rampası
.pcm-count = Renk sayısı
.pcm-gradient = Geçişli göster
.pcm-range = Renklendirilen nokta aralığı
.pcm-max-int = En büyük yoğunluk (%)
.pcm-min-int = En küçük yoğunluk (%)
.pcm-max-elev = En büyük yükseklik
.pcm-min-elev = En küçük yükseklik
.pcm-interval = Aralık yüksekliği
.pcm-extents = Nokta bulutu kapsamına uygula
.pcm-oor = Aralık dışı noktalar
.pcm-oor-minmax = En küçük/en büyük renkleri kullan
.pcm-oor-scan = Tarama RGB renklerini kullan
.pcm-oor-hide = Noktaları gizle
.pcm-current = Stilizasyonu geçerli yap
.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.
model =
.create = Oluştur
.boolean = Boole işlemleri
+19
View File
@@ -3955,6 +3955,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = 创建
.boolean = 布尔运算
+19
View File
@@ -3967,6 +3967,25 @@ insert =
.pc-rcs = Point Cloud Scan
.pc-clouds = Point Clouds
.pc-help = Attaches a point cloud scan or project.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.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.
model =
.create = 建立
.boolean = 布林運算
+4
View File
@@ -15,6 +15,7 @@ mod layerprops;
mod layers;
pub(crate) mod pdf_import;
mod point_cloud;
mod pc_colormap;
mod pc_extract;
mod pdf_underlay;
mod pdf_dialogs;
@@ -271,6 +272,9 @@ impl OpenCADStudio {
if let Some(t) = self.dispatch_pdf_underlay(cmd, i) {
return Some(t);
}
if let Some(t) = self.dispatch_pc_colormap(cmd, i) {
return Some(t);
}
if let Some(t) = self.dispatch_pc_extract(cmd, i) {
return Some(t);
}
+405
View File
@@ -0,0 +1,405 @@
//! POINTCLOUDCOLORMAP: the Point Cloud Color Map dialog for a chosen cloud
//! (or the drawing's default schemes), and what OK / Apply write — the
//! drawing's colour map (its schemes) and the cloud's intensity and
//! elevation colouring — as one undo step.
use super::*;
use codec::entities::{ExtendedEntityData, PointCloudExData};
use codec::objects::{ClassObject, ClassObjectData, Dictionary, ObjectType, PointCloudColorMap, PointCloudColorRamp, PointCloudRampColor};
use codec::types::Handle;
use crate::ui::window::pdf_dialogs::{short, MapRamp, MapSlot, OutOfRange, PdfDialogMsg, PointCloudColorMapState};
const MAP_DICTIONARY: &str = "ACAD_POINTCLOUD_COLORMAP_DICT";
const MAP_ENTRY: &str = "ACAD_POINTCLOUD_COLORMAP";
/// The classification scheme the colour map names by default.
const CLASSIFICATION_SCHEME: &str = "8AF59545-D22F-46B7-923C-912F71ED57DE";
fn cloud_data(document: &codec::CadDocument, handle: Handle) -> Option<&PointCloudExData> {
match document.get_entity(handle)? {
codec::EntityType::Extended(extended) => match &extended.data {
ExtendedEntityData::PointCloudEx(data) => Some(data),
_ => None,
},
_ => None,
}
}
/// The drawing's colour map object, created (with the built-in schemes)
/// under its dictionary when the drawing has none.
fn ensure_color_map(document: &mut codec::CadDocument) -> Option<Handle> {
let existing = document.objects.iter().find_map(|(handle, object)| match object {
ObjectType::ClassObject(object) if matches!(object.data, ClassObjectData::PointCloudColorMap(_)) => Some(*handle),
_ => None,
});
if existing.is_some() {
return existing;
}
let root = document.header.named_objects_dict_handle;
let ObjectType::Dictionary(root_dictionary) = document.objects.get(&root)? else {
return None;
};
let dictionary = match root_dictionary.get(MAP_DICTIONARY) {
Some(handle) if matches!(document.objects.get(&handle), Some(ObjectType::Dictionary(_))) => handle,
_ => {
let handle = document.allocate_handle();
let mut dictionary = Dictionary::new();
dictionary.handle = handle;
dictionary.owner = root;
document.objects.insert(handle, ObjectType::Dictionary(dictionary));
if let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get_mut(&root) {
root_dictionary.add_entry(MAP_DICTIONARY, handle);
}
handle
}
};
let handle = document.allocate_handle();
let ramps = crate::scene::model::point_cloud::schemes(document)
.into_iter()
.map(|(id, name, colors)| to_codec_ramp(&MapRamp { id, name, colors }))
.collect();
let mut map = ClassObject::new(ClassObjectData::PointCloudColorMap(PointCloudColorMap {
class_version: 4,
default_intensity_scheme: crate::entities::extended::point_cloud_ramp_id("Spectrum").to_string(),
default_elevation_scheme: crate::entities::extended::point_cloud_ramp_id("Earth").to_string(),
default_classification_scheme: CLASSIFICATION_SCHEME.to_string(),
color_ramps: ramps,
classification_color_ramps: Vec::new(),
}));
map.handle = handle;
map.owner = dictionary;
document.objects.insert(handle, ObjectType::ClassObject(map));
if let Some(ObjectType::Dictionary(dictionary)) = document.objects.get_mut(&dictionary) {
dictionary.add_entry(MAP_ENTRY, handle);
}
Some(handle)
}
/// A scheme as the colour map stores it: true colours, all shown.
fn to_codec_ramp(ramp: &MapRamp) -> PointCloudColorRamp {
PointCloudColorRamp {
id: ramp.id.clone(),
class_version: 1,
colors: ramp
.colors
.iter()
.map(|[r, g, b]| PointCloudRampColor {
color: (0xC2u32 << 24 | u32::from(*r) << 16 | u32::from(*g) << 8 | u32::from(*b)) as i32,
visible: true,
})
.collect(),
name: ramp.name.clone(),
}
}
/// A new scheme identifier: a random GUID in the stored form.
fn new_scheme_id() -> String {
let mut bytes = [0u8; 16];
getrandom::fill(&mut bytes).expect("OS random source");
let hex: String = bytes.iter().map(|b| format!("{b:02X}")).collect();
format!("{}-{}-{}-{}-{}", &hex[0..8], &hex[8..12], &hex[12..16], &hex[16..20], &hex[20..32])
}
/// `count` colours taken evenly along a scheme's blend.
fn resample(colors: &[[u8; 3]], count: usize) -> Vec<[u8; 3]> {
let count = count.clamp(2, 25);
(0..count)
.map(|k| crate::ui::window::pdf_dialogs::blend(colors, k as f64 / (count - 1) as f64))
.collect()
}
impl OpenCADStudio {
/// POINTCLOUDCOLORMAP after the prompt: the dialog for `cloud`, or for
/// the drawing's default schemes.
pub(in crate::app) fn open_point_cloud_color_map(&mut self, i: usize, cloud: Option<Handle>) {
let document = &self.tabs[i].scene.document;
let ramps: Vec<MapRamp> = crate::scene::model::point_cloud::schemes(document)
.into_iter()
.map(|(id, name, colors)| MapRamp { id, name, colors })
.collect();
let defaults = crate::scene::model::point_cloud::default_schemes(document);
let data = cloud.and_then(|handle| cloud_data(document, handle));
let height = data
.map(|data| {
let corners = codec::entities::point_cloud_ex_corners(data);
let low = corners.iter().map(|c| c.z).fold(f64::MAX, f64::min);
let high = corners.iter().map(|c| c.z).fold(f64::MIN, f64::max);
(low, high)
})
.unwrap_or((0.0, 0.0));
let slot = |scheme: &str, fallback: &str, gradient: bool, max: String, min: String, out: i16| MapSlot {
scheme: if scheme.is_empty() { fallback.to_string() } else { scheme.to_string() },
gradient,
max,
min,
out_of_range: OutOfRange(out),
};
let (intensity, elevation, extents, elevation_tab) = match data {
Some(data) => (
slot(
&data.intensity_color_scheme,
&defaults.0,
data.intensity_as_gradient,
data.intensity_max.to_string(),
data.intensity_min.to_string(),
data.intensity_out_of_range_behavior,
),
slot(
&data.current_color_scheme,
&defaults.1,
data.elevation_as_gradient,
short(if data.elevation_apply_to_fixed_range { data.elevation_max } else { height.1 }),
short(if data.elevation_apply_to_fixed_range { data.elevation_min } else { height.0 }),
data.elevation_out_of_range_behavior,
),
!data.elevation_apply_to_fixed_range,
data.stylization_type == 4,
),
None => (
slot("", &defaults.0, true, String::new(), String::new(), 1),
slot("", &defaults.1, false, String::new(), String::new(), 1),
true,
false,
),
};
let mut state = PointCloudColorMapState {
cloud: cloud.filter(|_| data.is_some()),
elevation_tab,
ramps,
intensity,
elevation,
extents,
height: (clean(height.0), clean(height.1)),
make_current: true,
interval: String::new(),
naming: None,
};
refresh_interval(&mut state);
self.point_cloud_color_map = Some(state);
self.active_modal = Some(crate::app::ModalKind::PointCloudColorMap);
}
pub(in crate::app) fn update_point_cloud_color_map(&mut self, message: PdfDialogMsg) -> Task<Message> {
let i = self.active_tab;
let Some(state) = self.point_cloud_color_map.as_mut() else {
return Task::none();
};
match message {
PdfDialogMsg::MapTab(elevation) => {
state.elevation_tab = elevation;
state.naming = None;
refresh_interval(state);
}
PdfDialogMsg::MapScheme(name) => {
if let Some(ramp) = state.ramps.iter().find(|r| r.name == name) {
state.slot_mut().scheme = ramp.id.clone();
refresh_interval(state);
}
}
PdfDialogMsg::MapCount(count) => {
if let Some(ramp) = state.ramp_mut() {
ramp.colors = resample(&ramp.colors, count);
}
refresh_interval(state);
}
// The colours between the two ends, blended evenly.
PdfDialogMsg::MapEven => {
if let Some(ramp) = state.ramp_mut() {
let (first, last) = (ramp.colors[0], ramp.colors[ramp.colors.len() - 1]);
ramp.colors = resample(&[first, last], ramp.colors.len());
}
}
PdfDialogMsg::MapReverse => {
if let Some(ramp) = state.ramp_mut() {
ramp.colors.reverse();
}
}
PdfDialogMsg::MapGradient(on) => state.slot_mut().gradient = on,
PdfDialogMsg::MapNew => {
let name = state.ramp().map(|r| format!("{} (2)", r.name)).unwrap_or_default();
state.naming = Some((true, name));
}
PdfDialogMsg::MapRename => {
let name = state.ramp().map(|r| r.name.clone()).unwrap_or_default();
state.naming = Some((false, name));
}
PdfDialogMsg::MapNameInput(value) => {
if let Some((_, name)) = state.naming.as_mut() {
*name = value;
}
}
PdfDialogMsg::MapNameCancel => state.naming = None,
PdfDialogMsg::MapNameOk => {
let Some((new, name)) = state.naming.take() else {
return Task::none();
};
let name = name.trim().to_string();
let taken = state.ramps.iter().any(|r| r.name.eq_ignore_ascii_case(&name))
&& !(!new && state.ramp().is_some_and(|r| r.name == name));
if name.is_empty() || taken {
self.command_line.push_error(crate::t!("A color scheme with this name already exists.").as_ref());
self.point_cloud_color_map.as_mut().map(|s| s.naming = Some((new, name)));
return Task::none();
}
if new {
let colors = state.ramp().map(|r| r.colors.clone()).unwrap_or_else(|| vec![[255; 3], [0; 3]]);
let id = new_scheme_id();
state.ramps.push(MapRamp { id: id.clone(), name, colors });
state.slot_mut().scheme = id;
} else if let Some(ramp) = state.ramp_mut() {
ramp.name = name;
}
}
// The last scheme stays; a slot naming a deleted one takes the first.
PdfDialogMsg::MapDelete => {
if state.ramps.len() > 1 {
let id = state.ramp().map(|r| r.id.clone()).unwrap_or_default();
state.ramps.retain(|r| r.id != id);
let first = state.ramps[0].id.clone();
for slot in [&mut state.intensity, &mut state.elevation] {
if slot.scheme.eq_ignore_ascii_case(&id) {
slot.scheme = first.clone();
}
}
refresh_interval(state);
}
}
PdfDialogMsg::MapMax(value) => {
state.slot_mut().max = value;
refresh_interval(state);
}
PdfDialogMsg::MapMin(value) => {
state.slot_mut().min = value;
refresh_interval(state);
}
// A band height sets the maximum from the minimum.
PdfDialogMsg::MapInterval(value) => {
let count = state.ramp().map_or(1, |r| r.colors.len()) as f64;
if let (Ok(step), Ok(min)) = (value.trim().parse::<f64>(), state.elevation.min.trim().parse::<f64>()) {
state.elevation.max = short(min + step * count);
}
state.interval = value;
}
PdfDialogMsg::MapExtents(on) => {
state.extents = on;
if on {
state.elevation.max = short(state.height.1);
state.elevation.min = short(state.height.0);
}
refresh_interval(state);
}
PdfDialogMsg::MapOutOfRange(choice) => state.slot_mut().out_of_range = choice,
PdfDialogMsg::MapCurrent(on) => state.make_current = on,
PdfDialogMsg::MapApply => {
self.apply_point_cloud_color_map(i);
}
PdfDialogMsg::MapOk => {
if self.apply_point_cloud_color_map(i) {
self.point_cloud_color_map = None;
self.active_modal = None;
}
}
_ => {}
}
Task::none()
}
/// Writes the dialog's schemes and the cloud's colouring as one undo
/// step; `false` (with a message) when a range value is not a number.
fn apply_point_cloud_color_map(&mut self, i: usize) -> bool {
let Some(state) = self.point_cloud_color_map.as_ref() else {
return false;
};
let intensity = (state.intensity.min.trim().parse::<i32>(), state.intensity.max.trim().parse::<i32>());
let elevation = (state.elevation.min.trim().parse::<f64>(), state.elevation.max.trim().parse::<f64>());
let cloud = state.cloud;
if cloud.is_some() {
let ok_intensity = matches!(intensity, (Ok(lo), Ok(hi)) if (0..=100).contains(&lo) && (0..=100).contains(&hi) && lo < hi);
let ok_elevation = state.extents || matches!(elevation, (Ok(lo), Ok(hi)) if lo < hi);
if !ok_intensity || !ok_elevation {
self.command_line.push_error(crate::t!("Invalid range of colorized points.").as_ref());
return false;
}
}
let ramps: Vec<PointCloudColorRamp> = state.ramps.iter().map(to_codec_ramp).collect();
let (intensity_slot, elevation_slot) = (state.intensity.clone(), state.elevation.clone());
let (extents, make_current, elevation_tab) = (state.extents, state.make_current, state.elevation_tab);
self.push_undo_snapshot(i, "POINTCLOUDCOLORMAP");
let document = &mut self.tabs[i].scene.document;
if let Some(handle) = ensure_color_map(document) {
if let Some(ObjectType::ClassObject(object)) = document.objects.get_mut(&handle) {
if let ClassObjectData::PointCloudColorMap(map) = &mut object.data {
map.color_ramps = ramps;
if cloud.is_none() {
map.default_intensity_scheme = intensity_slot.scheme.clone();
map.default_elevation_scheme = elevation_slot.scheme.clone();
}
}
}
}
if let Some(handle) = cloud {
if let Some(codec::EntityType::Extended(extended)) = document.get_entity_mut(handle) {
if let ExtendedEntityData::PointCloudEx(data) = &mut extended.data {
data.intensity_color_scheme = intensity_slot.scheme;
data.intensity_as_gradient = intensity_slot.gradient;
data.intensity_out_of_range_behavior = intensity_slot.out_of_range.0;
if let (Ok(lo), Ok(hi)) = intensity {
(data.intensity_min, data.intensity_max) = (lo, hi);
}
data.current_color_scheme = elevation_slot.scheme;
data.elevation_as_gradient = elevation_slot.gradient;
data.elevation_out_of_range_behavior = elevation_slot.out_of_range.0;
data.elevation_apply_to_fixed_range = !extents;
if let (false, Ok(lo), Ok(hi)) = (extents, elevation.0, elevation.1) {
(data.elevation_min, data.elevation_max) = (lo, hi);
}
if make_current {
data.stylization_type = if elevation_tab { 4 } else { 5 };
}
}
}
}
// Every cloud colours from the schemes: redraw them all.
let changes: Vec<_> = self.tabs[i]
.scene
.document
.entities()
.filter(|entity| crate::scene::is_point_cloud(entity))
.map(|entity| (entity.common().handle, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.tabs[i].dirty = true;
self.refresh_properties();
true
}
pub(super) fn dispatch_pc_colormap(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
use crate::command::CadCommand;
if cmd == "POINTCLOUDCOLORMAP" {
let command = crate::modules::insert::pc_stylize::PointCloudColorMapCommand::new();
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
return Some(self.finish_dispatch(cmd));
}
let rest = cmd.strip_prefix("_PCCOLORMAP")?;
let cloud = u64::from_str_radix(rest.trim(), 16).ok().map(Handle::new);
let task = self.finish_dispatch(cmd);
self.open_point_cloud_color_map(i, cloud);
Some(task)
}
}
/// A height with rounding noise dropped (the extents box's -0.0016 reads 0).
fn clean(value: f64) -> f64 {
(value * 100.0).round() / 100.0
}
fn refresh_interval(state: &mut PointCloudColorMapState) {
let count = state.ramp().map_or(1, |r| r.colors.len()).max(1) as f64;
let lo = state.elevation.min.trim().parse::<f64>();
let hi = state.elevation.max.trim().parse::<f64>();
state.interval = match (lo, hi) {
(Ok(lo), Ok(hi)) => short((hi - lo) / count),
_ => String::new(),
};
}
+23
View File
@@ -378,6 +378,9 @@ impl OpenCADStudio {
}
"attach" => crate::t!("Attaches a PDF file as an underlay."),
"pointcloud" => crate::t!("Attaches a point cloud scan or project."),
"pointcloudcolormap" => {
crate::t!("Sets the color schemes and ranges a point cloud is colored by intensity or elevation.")
}
"layers" => crate::t!("Turns the layers of a PDF underlay on or off."),
_ => crate::t!("Imports the geometry, fills, raster images and text of a PDF file as drawing objects."),
};
@@ -480,6 +483,26 @@ impl OpenCADStudio {
// ── Attach Point Cloud ────────────────────────────────────────
PdfDialogMsg::CloudBrowse => return Task::done(Message::PointCloudAttachPick),
PdfDialogMsg::CloudOk => return self.point_cloud_attach_ok(i),
message @ (PdfDialogMsg::MapTab(_)
| PdfDialogMsg::MapScheme(_)
| PdfDialogMsg::MapCount(_)
| PdfDialogMsg::MapEven
| PdfDialogMsg::MapReverse
| PdfDialogMsg::MapGradient(_)
| PdfDialogMsg::MapNew
| PdfDialogMsg::MapDelete
| PdfDialogMsg::MapRename
| PdfDialogMsg::MapNameInput(_)
| PdfDialogMsg::MapNameOk
| PdfDialogMsg::MapNameCancel
| PdfDialogMsg::MapMax(_)
| PdfDialogMsg::MapMin(_)
| PdfDialogMsg::MapInterval(_)
| PdfDialogMsg::MapExtents(_)
| PdfDialogMsg::MapOutOfRange(_)
| PdfDialogMsg::MapCurrent(_)
| PdfDialogMsg::MapApply
| PdfDialogMsg::MapOk) => return self.update_point_cloud_color_map(message),
PdfDialogMsg::CloudPathType(choice) => {
let stored = self.point_cloud_attach.as_ref().map(|s| self.pdf_stored_path(&s.path, choice.0));
if let (Some(s), Some(stored)) = (self.point_cloud_attach.as_mut(), stored) {
+32
View File
@@ -72,6 +72,7 @@ pub(super) const NAMES: &[&str] = &[
"pdf_dialog_ok",
"pdf_layer_toggle",
"pdf_page_select",
"pc_colormap",
"ribbon_tab",
"ribbon_dropdown",
"dialog_ok",
@@ -688,6 +689,36 @@ impl OpenCADStudio {
"pdf_layer_toggle" => Message::PdfDialog(
crate::ui::window::pdf_dialogs::PdfDialogMsg::LayersToggle(string(req, "value")?.into()),
),
// Point Cloud Color Map: one edit, "tab=elevation", "count=3",
// "even", "reverse", "scheme=Earth", "gradient=0", "max=90",
// "min=10", "interval=0.1", "extents=0", "range=2", "current=0".
"pc_colormap" => {
use crate::ui::window::pdf_dialogs::{OutOfRange, PdfDialogMsg as M};
let value = string(req, "value")?;
let (key, arg) = value.split_once('=').unwrap_or((value, ""));
let on = arg != "0";
Message::PdfDialog(match key {
"tab" => M::MapTab(arg == "elevation"),
"scheme" => M::MapScheme(arg.into()),
"count" => M::MapCount(arg.parse().map_err(|_| failure("invalid_value", "count"))?),
"even" => M::MapEven,
"reverse" => M::MapReverse,
"gradient" => M::MapGradient(on),
"max" => M::MapMax(arg.into()),
"min" => M::MapMin(arg.into()),
"interval" => M::MapInterval(arg.into()),
"extents" => M::MapExtents(on),
"range" => M::MapOutOfRange(OutOfRange(arg.parse().unwrap_or(1))),
"current" => M::MapCurrent(on),
"new" => M::MapNew,
"rename" => M::MapRename,
"name" => M::MapNameInput(arg.into()),
"name_ok" => M::MapNameOk,
"delete" => M::MapDelete,
"apply" => M::MapApply,
_ => return Err(failure("invalid_value", "Unknown color map edit")),
})
}
// Attach dialog: choose pages by index ("0,2").
"pdf_page_select" => {
let pages: Vec<usize> = string(req, "value")?
@@ -714,6 +745,7 @@ impl OpenCADStudio {
Message::PdfDialog(match self.active_modal {
Some(crate::app::ModalKind::PdfAttach) => PdfDialogMsg::AttachOk,
Some(crate::app::ModalKind::PointCloudAttach) => PdfDialogMsg::CloudOk,
Some(crate::app::ModalKind::PointCloudColorMap) => PdfDialogMsg::MapOk,
Some(crate::app::ModalKind::UnderlayLayers) => PdfDialogMsg::LayersOk,
Some(crate::app::ModalKind::PdfImportSettings) => PdfDialogMsg::SettingsOk,
Some(crate::app::ModalKind::PdfImportFile) => PdfDialogMsg::ImportOk,
+3
View File
@@ -498,6 +498,7 @@ pub(super) struct OpenCADStudio {
/// PDF dialogs' working copies; `None` while closed.
pdf_attach: Option<crate::ui::window::pdf_dialogs::PdfAttachState>,
point_cloud_attach: Option<crate::ui::window::pdf_dialogs::PointCloudAttachState>,
point_cloud_color_map: Option<crate::ui::window::pdf_dialogs::PointCloudColorMapState>,
underlay_layers: Option<crate::ui::window::pdf_dialogs::UnderlayLayersState>,
pdf_import_settings: Option<crate::modules::insert::pdf_import::PdfImportSettings>,
pdf_import_file: Option<crate::ui::window::pdf_dialogs::PdfImportFileState>,
@@ -1903,6 +1904,7 @@ pub enum ModalKind {
BlockDefinition,
PdfAttach,
PointCloudAttach,
PointCloudColorMap,
UnderlayLayers,
PdfImportSettings,
PdfImportFile,
@@ -4034,6 +4036,7 @@ impl OpenCADStudio {
block_definition: None,
pdf_attach: None,
point_cloud_attach: None,
point_cloud_color_map: None,
underlay_layers: None,
pdf_import_settings: None,
pdf_import_file: None,
+7
View File
@@ -30,6 +30,7 @@ impl OpenCADStudio {
_ => crate::t!("Attach PDF Underlay").into_owned(),
},
Some(K::PointCloudAttach) => crate::t!("Attach Point Cloud").into_owned(),
Some(K::PointCloudColorMap) => crate::t!("Point Cloud Color Map").into_owned(),
Some(K::UnderlayLayers) => crate::t!("Underlay Layers").into_owned(),
Some(K::PdfImportSettings) => crate::t!("PDF Import Settings").into_owned(),
Some(K::PdfImportFile) => crate::t!("Import PDF").into_owned(),
@@ -529,6 +530,12 @@ impl OpenCADStudio {
crate::ui::window::pdf_dialogs::view_point_cloud_attach(state, flow)
})
}
super::super::ModalKind::PointCloudColorMap => {
let state = self.point_cloud_color_map.as_ref()?;
sized_flow(ex, 760, 560, |flow| {
crate::ui::window::pdf_dialogs::view_point_cloud_color_map(state, flow)
})
}
super::super::ModalKind::UnderlayLayers => {
let state = self.underlay_layers.as_ref()?;
sized_flow(ex, 460, 520, |flow| {
+5
View File
@@ -1130,6 +1130,11 @@ pub(crate) const POINT_CLOUD_RAMPS: [(&str, &str); 7] = [
("Spectrum", "C0A3838B-81E3-4506-B908-13ECF523DB68"),
];
/// A built-in scheme's identifier by name (Blues when unknown).
pub(crate) fn point_cloud_ramp_id(name: &str) -> &'static str {
POINT_CLOUD_RAMPS.iter().find(|(n, _)| *n == name).unwrap_or(&POINT_CLOUD_RAMPS[0]).1
}
/// Stylization choices in the order the reference lists them, with the
/// stored values: scan colours 1, object colour 2, normals 3, intensity 5,
/// elevation 4.
+19
View File
@@ -115,6 +115,25 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Point Cloud Scan" => Some(("insert", "pc-rcs")),
"Point Clouds" => Some(("insert", "pc-clouds")),
"Attaches a point cloud scan or project." => Some(("insert", "pc-help")),
"Point Cloud Color Map" => Some(("insert", "pcm-title")),
"Color ramp" => Some(("insert", "pcm-ramp")),
"Number of colors" => Some(("insert", "pcm-count")),
"Display as gradient" => Some(("insert", "pcm-gradient")),
"Range of colorized points" => Some(("insert", "pcm-range")),
"Maximum intensity" => Some(("insert", "pcm-max-int")),
"Minimum intensity" => Some(("insert", "pcm-min-int")),
"Maximum elevation" => Some(("insert", "pcm-max-elev")),
"Minimum elevation" => Some(("insert", "pcm-min-elev")),
"Interval height" => Some(("insert", "pcm-interval")),
"Apply to extents of point cloud" => Some(("insert", "pcm-extents")),
"Out of range points" => Some(("insert", "pcm-oor")),
"Use min/max colors" => Some(("insert", "pcm-oor-minmax")),
"Use RGB scan colors" => Some(("insert", "pcm-oor-scan")),
"Hide points" => Some(("insert", "pcm-oor-hide")),
"Make stylization current" => Some(("insert", "pcm-current")),
"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")),
"Stylization" => Some(("properties", "stylization")),
"Color scheme" => Some(("properties", "color-scheme")),
"Scan Colors" => Some(("properties", "scan-colors")),
+77
View File
@@ -86,3 +86,80 @@ impl CadCommand for PointCloudStylizeCommand {
}
inventory::submit!(crate::command::CommandRegistration { names: &["POINTCLOUDSTYLIZE"] });
// POINTCLOUDCOLORMAP — the Point Cloud Color Map dialog.
//
// Select point cloud or [None] <None>:
//
// A picked cloud opens the dialog for it; None (or Enter) for the
// drawing's default schemes.
pub struct PointCloudColorMapCommand {
picked: Option<codec::EntityType>,
}
impl PointCloudColorMapCommand {
pub fn new() -> Self {
Self { picked: None }
}
}
impl CadCommand for PointCloudColorMapCommand {
fn name(&self) -> &'static str {
"POINTCLOUDCOLORMAP"
}
fn prompt(&self) -> String {
"Select point cloud or [None] <None>:".to_string()
}
fn options(&self) -> Vec<CmdOption> {
vec![CmdOption::new("None", "N")]
}
fn input_kind(&self) -> InputKind {
InputKind::Point
}
fn point_step_accepts_keywords(&self) -> bool {
true
}
fn needs_entity_pick(&self) -> bool {
true
}
fn inject_before_entity_pick(&self) -> bool {
true
}
fn inject_picked_entity(&mut self, entity: codec::EntityType) {
self.picked = Some(entity);
}
fn on_entity_pick(&mut self, handle: Handle, _pt: glam::DVec3) -> CmdResult {
match self.picked.take() {
Some(entity) if crate::scene::is_point_cloud(&entity) => {
CmdResult::Dispatch(format!("_PCCOLORMAP {:X}", handle.value()))
}
// Anything else: the prompt again.
_ => CmdResult::NeedPoint,
}
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
match text.trim().to_ascii_uppercase().as_str() {
"" | "N" | "NONE" => Some(CmdResult::Dispatch("_PCCOLORMAP".to_string())),
_ => Some(CmdResult::ReportError("Invalid option keyword.".to_string())),
}
}
fn on_point(&mut self, _pt: glam::DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Dispatch("_PCCOLORMAP".to_string())
}
}
inventory::submit!(crate::command::CommandRegistration { names: &["POINTCLOUDCOLORMAP"] });
+96 -20
View File
@@ -318,7 +318,28 @@ pub(crate) fn placed(
(lo.min(z), hi.max(z))
})
};
let color = |i: usize, z: f64| -> [u8; 4] {
let scan_color = |i: usize| {
if cloud.has_rgb {
cloud.colors[i]
} else if cloud.has_intensity {
[cloud.intensity[i]; 3]
} else {
object_color
}
};
// A value outside a scheme's range takes the end colour, the scan
// colour, or is hidden (`None`), as the cloud's out-of-range choice says.
let along = |colors: &[[u8; 3]], t: f64, gradient: bool, out_of_range: i16, fixed: bool, i: usize| {
if fixed && !(0.0..=1.0).contains(&t) {
return match out_of_range {
0 => Some(ramp(colors, t, gradient)),
2 => None,
_ => Some(scan_color(i)),
};
}
Some(ramp(colors, t, gradient))
};
let color = |i: usize, z: f64| -> Option<[u8; 4]> {
let [r, g, b] = match &style {
Stylization::Object(c) => *c,
Stylization::Normal => match cloud.normals.get(i) {
@@ -329,25 +350,22 @@ pub(crate) fn placed(
}
None => object_color,
},
Stylization::Ramp { colors, gradient, intensity: true, range } => {
Stylization::Ramp { colors, gradient, intensity: true, range, out_of_range } => {
let percent = f64::from(cloud.intensity.get(i).copied().unwrap_or(0)) * 100.0 / 255.0;
let (from, to) = (range.0 as f64, range.1 as f64);
ramp(colors, (percent - from) / (to - from).max(1e-9), *gradient)
}
Stylization::Ramp { colors, gradient, .. } => {
ramp(colors, (z - low) / (high - low).max(1e-9), *gradient)
}
Stylization::Scan => {
if cloud.has_rgb {
cloud.colors[i]
} else if cloud.has_intensity {
[cloud.intensity[i]; 3]
} else {
object_color
}
along(colors, (percent - from) / (to - from).max(1e-9), *gradient, *out_of_range, true, i)?
}
Stylization::Ramp { colors, gradient, out_of_range, .. } => along(
colors,
(z - low) / (high - low).max(1e-9),
*gradient,
*out_of_range,
data.elevation_apply_to_fixed_range,
i,
)?,
Stylization::Scan => scan_color(i),
};
[r, g, b, 255]
Some([r, g, b, 255])
};
let mut instances = Vec::with_capacity(cloud.positions.len() / step + 1);
for (i, p) in cloud.positions.iter().enumerate().step_by(step) {
@@ -355,6 +373,9 @@ pub(crate) fn placed(
continue;
}
let w = world(p);
let Some(color) = color(i, w.z) else {
continue;
};
let pos = [w.x as f32, w.y as f32, w.z as f32];
instances.push(PointInstance {
pos,
@@ -363,7 +384,7 @@ pub(crate) fn placed(
(w.y - pos[1] as f64) as f32,
(w.z - pos[2] as f64) as f32,
],
color: color(i, w.z),
color,
});
}
crate::perf::record(format_args!(
@@ -419,8 +440,9 @@ enum Stylization {
/// Each point's world normal as a colour, (n + 1) / 2 per component.
Normal,
/// A colour scheme over intensity (per cent, in the range given) or
/// elevation, blended or in bands.
Ramp { colors: Vec<[u8; 3]>, gradient: bool, intensity: bool, range: (i64, i64) },
/// elevation, blended or in bands; values out of range per
/// `out_of_range` (0 end colours, 1 scan colours, 2 hidden).
Ramp { colors: Vec<[u8; 3]>, gradient: bool, intensity: bool, range: (i64, i64), out_of_range: i16 },
}
impl Stylization {
@@ -439,7 +461,9 @@ impl Stylization {
} else {
(0, 100)
};
Self::Ramp { colors, gradient, intensity, range: (range.0, range.1) }
let out_of_range =
if intensity { data.intensity_out_of_range_behavior } else { data.elevation_out_of_range_behavior };
Self::Ramp { colors, gradient, intensity, range: (range.0, range.1), out_of_range }
}
}
@@ -458,6 +482,58 @@ fn rgb(color: u32) -> [u8; 3] {
[(color >> 16) as u8, (color >> 8) as u8, color as u8]
}
/// The colour schemes the drawing offers (identifier, name, colours high end
/// first): its colour map's, or the built-in ones.
pub(crate) fn schemes(document: &CadDocument) -> Vec<(String, String, Vec<[u8; 3]>)> {
for object in document.objects.values() {
let ObjectType::ClassObject(object) = object else {
continue;
};
let ClassObjectData::PointCloudColorMap(map) = &object.data else {
continue;
};
if !map.color_ramps.is_empty() {
return map
.color_ramps
.iter()
.map(|r| {
let colors = r.colors.iter().filter(|c| c.visible).map(|c| rgb(c.color as u32)).collect();
(r.id.clone(), r.name.clone(), colors)
})
.collect();
}
}
BUILTIN_RAMPS
.iter()
.map(|(name, colors)| {
let id = crate::entities::extended::point_cloud_ramp_id(name).to_string();
(id, name.to_string(), colors.iter().map(|c| rgb(*c)).collect())
})
.collect()
}
/// The schemes new intensity and elevation colouring starts from: the
/// colour map's defaults, else Spectrum and Earth.
pub(crate) fn default_schemes(document: &CadDocument) -> (String, String) {
let map = document.objects.values().find_map(|object| match object {
ObjectType::ClassObject(object) => match &object.data {
ClassObjectData::PointCloudColorMap(map) => Some(map),
_ => None,
},
_ => None,
});
let pick = |stored: Option<&String>, name: &str| {
stored
.filter(|s| !s.is_empty())
.cloned()
.unwrap_or_else(|| crate::entities::extended::point_cloud_ramp_id(name).to_string())
};
(
pick(map.map(|m| &m.default_intensity_scheme), "Spectrum"),
pick(map.map(|m| &m.default_elevation_scheme), "Earth"),
)
}
/// A scheme's colours: from the drawing's colour map when it has the scheme,
/// else the built-in one of that name (or `fallback` for an unset scheme).
fn ramp_colors(document: &CadDocument, id: &str, fallback: &str) -> Vec<[u8; 3]> {
+379 -1
View File
@@ -1,6 +1,7 @@
//! PDF dialogs: Attach PDF Underlay (pages, path type, insertion, scale,
//! rotation), Underlay Layers, PDF Import Settings and Import PDF, built
//! from cards, segmented choices and toggle chips.
//! from cards, segmented choices and toggle chips; the point cloud Attach
//! and Color Map dialogs share them.
use std::fmt;
@@ -47,6 +48,28 @@ pub enum PdfDialogMsg {
CloudLock(bool),
CloudZoom(bool),
CloudOk,
// Point Cloud Color Map
/// Elevation tab (else intensity).
MapTab(bool),
MapScheme(String),
MapCount(usize),
MapEven,
MapReverse,
MapGradient(bool),
MapNew,
MapDelete,
MapRename,
MapNameInput(String),
MapNameOk,
MapNameCancel,
MapMax(String),
MapMin(String),
MapInterval(String),
MapExtents(bool),
MapOutOfRange(OutOfRange),
MapCurrent(bool),
MapApply,
MapOk,
// Underlay Layers
LayersUnderlay(String),
LayersSearch(String),
@@ -1108,3 +1131,358 @@ pub fn view_import_file<'a>(
.width(sizing.width)
.into()
}
// ── Point Cloud Color Map ──────────────────────────────────────────────────
/// A colour scheme as the dialog edits it: identifier, name and colours
/// (high end first).
#[derive(Debug, Clone, PartialEq)]
pub struct MapRamp {
pub id: String,
pub name: String,
pub colors: Vec<[u8; 3]>,
}
/// What a scheme does with values outside its range.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OutOfRange(pub i16);
impl OutOfRange {
pub const ALL: [OutOfRange; 3] = [OutOfRange(0), OutOfRange(1), OutOfRange(2)];
}
impl fmt::Display for OutOfRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self.0 {
0 => t!("Use min/max colors"),
2 => t!("Hide points"),
_ => t!("Use RGB scan colors"),
};
f.write_str(label.as_ref())
}
}
/// One tab's settings: its scheme and range.
#[derive(Debug, Clone)]
pub struct MapSlot {
pub scheme: String,
pub gradient: bool,
pub max: String,
pub min: String,
pub out_of_range: OutOfRange,
}
/// The Point Cloud Color Map dialog: the drawing's colour schemes and, for
/// a chosen cloud, its intensity and elevation colouring.
pub struct PointCloudColorMapState {
/// The cloud edited; `None` edits the drawing's default schemes.
pub cloud: Option<codec::Handle>,
/// Elevation tab (else intensity).
pub elevation_tab: bool,
pub ramps: Vec<MapRamp>,
pub intensity: MapSlot,
pub elevation: MapSlot,
/// Elevation over the cloud's own height (else the fixed range).
pub extents: bool,
/// The cloud's height range.
pub height: (f64, f64),
pub make_current: bool,
/// The height of one band (elevation), as shown and typed.
pub interval: String,
/// A name being typed for a new scheme (`true`) or a rename.
pub naming: Option<(bool, String)>,
}
impl PointCloudColorMapState {
pub fn slot(&self) -> &MapSlot {
if self.elevation_tab { &self.elevation } else { &self.intensity }
}
pub fn slot_mut(&mut self) -> &mut MapSlot {
if self.elevation_tab { &mut self.elevation } else { &mut self.intensity }
}
pub fn ramp(&self) -> Option<&MapRamp> {
let id = &self.slot().scheme;
self.ramps.iter().find(|r| r.id.eq_ignore_ascii_case(id)).or(self.ramps.first())
}
pub fn ramp_mut(&mut self) -> Option<&mut MapRamp> {
let id = self.slot().scheme.clone();
let at = self.ramps.iter().position(|r| r.id.eq_ignore_ascii_case(&id)).unwrap_or(0);
self.ramps.get_mut(at)
}
/// The range the ramp spans on the current tab.
pub fn range(&self) -> Option<(f64, f64)> {
if self.elevation_tab && self.extents {
return Some(self.height);
}
let slot = self.slot();
Some((slot.min.trim().parse().ok()?, slot.max.trim().parse().ok()?))
}
}
/// `t` (0 at the high end) along colours, blended; channels are cut to
/// whole values, as the reference stores resampled schemes.
pub fn blend(colors: &[[u8; 3]], t: f64) -> [u8; 3] {
if colors.len() < 2 {
return colors.first().copied().unwrap_or([255; 3]);
}
let x = t.clamp(0.0, 1.0) * (colors.len() - 1) as f64;
let k = (x as usize).min(colors.len() - 2);
let f = x - k as f64;
let (a, b) = (colors[k], colors[k + 1]);
[0, 1, 2].map(|c| (f64::from(a[c]) + (f64::from(b[c]) - f64::from(a[c])) * f) as u8)
}
/// A number as the dialog labels it: at most two decimals.
pub fn short(value: f64) -> String {
let value = if value.abs() < 0.005 { 0.0 } else { value };
let text = format!("{:.2}", value);
text.trim_end_matches('0').trim_end_matches('.').to_string()
}
fn swatch<'a>(color: [u8; 3], width: f32, height: f32) -> Element<'a, Message> {
let color = iced::Color::from_rgb8(color[0], color[1], color[2]);
container(Space::new().width(width).height(height))
.style(move |_: &Theme| container::Style {
background: Some(Background::Color(color)),
border: Border { width: 1.0, radius: 3.0.into(), color: iced::Color::from_rgb8(0x77, 0x77, 0x77) },
..Default::default()
})
.into()
}
/// The ramp as a bar, high end on top, blended or in bands, with the values
/// where the bands meet (blended: the ends and the inner band centres).
fn ramp_bar<'a>(colors: &[[u8; 3]], gradient: bool, range: Option<(f64, f64)>, percent: bool) -> Element<'a, Message> {
const HEIGHT: f32 = 360.0;
let n = colors.len().max(1);
let strips: Vec<Element<'a, Message>> = if gradient {
(0..72).map(|k| {
let c = blend(colors, (k as f64 + 0.5) / 72.0);
let color = iced::Color::from_rgb8(c[0], c[1], c[2]);
container(Space::new().width(Fill).height(Fill))
.height(Length::FillPortion(1))
.style(move |_: &Theme| container::Style {
background: Some(Background::Color(color)),
..Default::default()
})
.into()
}).collect()
} else {
colors.iter().map(|c| {
let color = iced::Color::from_rgb8(c[0], c[1], c[2]);
container(Space::new().width(Fill).height(Fill))
.height(Length::FillPortion(1))
.style(move |_: &Theme| container::Style {
background: Some(Background::Color(color)),
..Default::default()
})
.into()
}).collect()
};
let bar = container(iced::widget::Column::with_children(strips))
.width(Length::Fixed(56.0))
.height(Length::Fixed(HEIGHT));
// Label positions from the top (0) to the bottom (1).
let marks: Vec<f64> = if gradient {
let mut marks = vec![0.0];
marks.extend((1..n.saturating_sub(1)).map(|k| (k as f64 + 0.5) / n as f64));
marks.push(1.0);
marks
} else {
(0..=n).map(|k| k as f64 / n as f64).collect()
};
let mut labels = column![];
if let Some((low, high)) = range {
// Each label is centred on its mark, kept inside the bar.
let mut used = 0.0;
for mark in marks {
let value = high - (high - low) * mark;
let label = if percent { format!("{}%", value.round()) } else { short(value) };
let top = (mark * f64::from(HEIGHT) - 7.0).clamp(0.0, f64::from(HEIGHT) - 14.0);
labels = labels.push(Space::new().height((top - used).max(0.0) as f32)).push(text(label).size(10).height(14.0));
used = top.max(used) + 14.0;
}
}
row![bar, labels.width(Length::Fixed(44.0))].spacing(4).into()
}
pub fn view_point_cloud_color_map<'a>(
state: &'a PointCloudColorMapState,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let tab = |label: std::borrow::Cow<'static, str>, on: bool, message: Option<Message>| -> Element<'a, Message> {
let mut b = button(text(label).size(11).width(Fill).align_x(iced::Center))
.style(button_style(on))
.padding([5, 6])
.width(Length::Fixed(118.0));
if let Some(message) = message {
b = b.on_press(message);
}
b.into()
};
let tabs = container(
row![
tab(t!("Intensity"), !state.elevation_tab, Some(msg(PdfDialogMsg::MapTab(false)))),
tab(t!("Elevation"), state.elevation_tab, Some(msg(PdfDialogMsg::MapTab(true)))),
tab(t!("Classification"), false, None),
]
.spacing(2),
)
.padding(2)
.style(well_style);
let slot = state.slot();
let ramp = state.ramp();
let colors: Vec<[u8; 3]> = ramp.map(|r| r.colors.clone()).unwrap_or_default();
let ramp_card = card(
t!("Color ramp").into_owned(),
ramp_bar(&colors, slot.gradient, state.cloud.and(state.range()), !state.elevation_tab),
);
let names: Vec<String> = state.ramps.iter().map(|r| r.name.clone()).collect();
let scheme = pick_list(ramp.map(|r| r.name.clone()), names, |n: &String| n.clone())
.on_select(|n| msg(PdfDialogMsg::MapScheme(n)))
.text_size(12)
.padding([5, 8])
.width(Fill);
let small = |label: std::borrow::Cow<'static, str>, message: PdfDialogMsg| {
button(text(label).size(11).width(Fill).align_x(iced::Center))
.on_press(msg(message))
.style(button_style(false))
.padding([5, 8])
.width(Length::Fixed(118.0))
};
let counts: Vec<usize> = (2..=25).collect();
let count = pick_list(Some(colors.len().max(2)), counts, |n: &usize| n.to_string())
.on_select(|n| msg(PdfDialogMsg::MapCount(n)))
.text_size(12)
.padding([4, 8])
.width(Length::Fixed(70.0));
// Blend: a strip from the first colour to the last.
let ends = [colors.first().copied().unwrap_or([255; 3]), colors.last().copied().unwrap_or([255; 3])];
let blend_strip = row((0..6).map(|k| swatch(blend(&ends, k as f64 / 5.0), 3.0, 14.0)));
let even = button(blend_strip).on_press(msg(PdfDialogMsg::MapEven)).style(button_style(false)).padding([5, 6]);
let reverse = button(
iced::widget::svg(crate::ui::icons::themed_handle(include_bytes!("../../../assets/icons/modify_reverse.svg")))
.width(16.0)
.height(16.0),
)
.on_press(msg(PdfDialogMsg::MapReverse))
.style(button_style(false))
.padding([4, 6]);
let mut scheme_rows = column![
row![scheme, small(t!("New"), PdfDialogMsg::MapNew)].spacing(8).align_y(iced::Center),
row![
text(t!("Number of colors")).size(11).width(Length::Fixed(110.0)),
count,
even,
reverse,
Space::new().width(Fill),
small(t!("Delete"), PdfDialogMsg::MapDelete)
]
.spacing(8)
.align_y(iced::Center),
row![
chip(t!("Display as gradient").into_owned(), slot.gradient, msg(PdfDialogMsg::MapGradient(!slot.gradient))),
Space::new().width(Fill),
small(t!("Rename"), PdfDialogMsg::MapRename)
]
.align_y(iced::Center),
]
.spacing(8);
if let Some((_, name)) = &state.naming {
scheme_rows = scheme_rows.push(
row![
text(t!("Name")).size(11).style(muted_style).width(Length::Fixed(110.0)),
text_input("", name)
.on_input(|v| msg(PdfDialogMsg::MapNameInput(v)))
.on_submit(msg(PdfDialogMsg::MapNameOk))
.size(11)
.padding([4, 8])
.width(Fill)
.style(field_style),
dialog_button(t!("Cancel"), msg(PdfDialogMsg::MapNameCancel), false),
dialog_button(t!("OK"), msg(PdfDialogMsg::MapNameOk), true),
]
.spacing(6)
.align_y(iced::Center),
);
}
let scheme_card = card(t!("Color scheme").into_owned(), scheme_rows);
let has_cloud = state.cloud.is_some();
let top = colors.first().copied().unwrap_or([255; 3]);
let bottom = colors.last().copied().unwrap_or([255; 3]);
let editable = has_cloud && !(state.elevation_tab && state.extents);
let with_swatch = |field: Element<'a, Message>, color: [u8; 3]| -> Element<'a, Message> {
row![field, swatch(color, 22.0, 22.0)].spacing(8).align_y(iced::Center).into()
};
let out_of_range = {
let list = pick_list(Some(slot.out_of_range), OutOfRange::ALL.to_vec(), |o: &OutOfRange| o.to_string())
.text_size(12)
.padding([4, 8])
.width(Fill);
let list = if editable { list.on_select(|o| msg(PdfDialogMsg::MapOutOfRange(o))) } else { list };
row![text(t!("Out of range points")).size(11).style(muted_style).width(Length::Fixed(150.0)), list]
.spacing(6)
.align_y(iced::Center)
};
let range_rows = if state.elevation_tab {
let (low, high) = state.height;
let interval = state.interval.as_str();
column![
chip(
format!("{} ({} ~ {})", t!("Apply to extents of point cloud"), short(low), short(high)),
state.extents,
msg(PdfDialogMsg::MapExtents(!state.extents))
),
field(t!("Interval height").into_owned(), interval, editable, 150.0, |v| msg(PdfDialogMsg::MapInterval(v))),
with_swatch(field(t!("Maximum elevation").into_owned(), &slot.max, editable, 150.0, |v| msg(PdfDialogMsg::MapMax(v))), top),
with_swatch(field(t!("Minimum elevation").into_owned(), &slot.min, editable, 150.0, |v| msg(PdfDialogMsg::MapMin(v))), bottom),
out_of_range,
]
.spacing(8)
} else {
column![
with_swatch(field(t!("Maximum intensity").into_owned(), &slot.max, editable, 150.0, |v| msg(PdfDialogMsg::MapMax(v))), top),
with_swatch(field(t!("Minimum intensity").into_owned(), &slot.min, editable, 150.0, |v| msg(PdfDialogMsg::MapMin(v))), bottom),
out_of_range,
]
.spacing(8)
};
let range_card = card(t!("Range of colorized points").into_owned(), range_rows);
let body = row![
container(ramp_card).width(Length::Fixed(150.0)),
column![scheme_card, range_card].spacing(8).width(Fill),
]
.spacing(10);
let mut left = row![button(text("?").size(12))
.on_press(msg(PdfDialogMsg::Help("pointcloudcolormap")))
.style(button_style(false))
.padding([5, 11])]
.spacing(8)
.align_y(iced::Center);
if has_cloud {
left = left.push(chip(
t!("Make stylization current").into_owned(),
state.make_current,
msg(PdfDialogMsg::MapCurrent(!state.make_current)),
));
}
let footer = row![
left,
Space::new().width(Fill),
dialog_button(t!("Cancel"), Message::CloseModal, false),
dialog_button(t!("Apply"), msg(PdfDialogMsg::MapApply), false),
dialog_button(t!("OK"), msg(PdfDialogMsg::MapOk), true),
]
.spacing(6)
.align_y(iced::Center);
column![tabs, body, footer].spacing(10).padding([10, 12]).width(sizing.width).into()
}