From 2eaae1ecab8868314e95078e183ce2c7b64d7cdb Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:10:45 +0300 Subject: [PATCH] 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 --- locales/ar-SA/opencadstudio.ftl | 19 ++ locales/bg-BG/opencadstudio.ftl | 19 ++ locales/cs-CZ/opencadstudio.ftl | 19 ++ locales/de-DE/opencadstudio.ftl | 19 ++ locales/el-GR/opencadstudio.ftl | 19 ++ locales/en-US/opencadstudio.ftl | 19 ++ locales/es-ES/opencadstudio.ftl | 19 ++ locales/fi-FI/opencadstudio.ftl | 19 ++ locales/fr-FR/opencadstudio.ftl | 19 ++ locales/hi-IN/opencadstudio.ftl | 19 ++ locales/hu-HU/opencadstudio.ftl | 19 ++ locales/it-IT/opencadstudio.ftl | 19 ++ locales/ja-JP/opencadstudio.ftl | 19 ++ locales/ko-KR/opencadstudio.ftl | 19 ++ locales/nl-NL/opencadstudio.ftl | 19 ++ locales/pl-PL/opencadstudio.ftl | 19 ++ locales/pt-BR/opencadstudio.ftl | 19 ++ locales/ru-RU/opencadstudio.ftl | 19 ++ locales/tr-TR/opencadstudio.ftl | 19 ++ locales/zh-CN/opencadstudio.ftl | 19 ++ locales/zh-TW/opencadstudio.ftl | 19 ++ src/app/commands/mod.rs | 4 + src/app/commands/pc_colormap.rs | 405 +++++++++++++++++++++++++++++ src/app/commands/pdf_dialogs.rs | 23 ++ src/app/control/actions.rs | 32 +++ src/app/mod.rs | 3 + src/app/view/modal.rs | 7 + src/entities/extended.rs | 5 + src/locale_catalog.rs | 19 ++ src/modules/insert/pc_stylize.rs | 77 ++++++ src/scene/model/point_cloud/mod.rs | 116 +++++++-- src/ui/window/pdf_dialogs.rs | 380 ++++++++++++++++++++++++++- 32 files changed, 1449 insertions(+), 21 deletions(-) create mode 100644 src/app/commands/pc_colormap.rs diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 5196b9d90..b64addfeb 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -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 = عمليات منطقية diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 6c0668515..45afb7176 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -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 = Булеви операции diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 2c806e8c5..bad0db24e 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -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 diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 179c8b1e9..af6cfdccb 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -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 diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 343220765..455568e9e 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -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 = Δυαδική πράξη diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 60a144f69..3c3d14c22 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -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 diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index e1605f099..c43d88cd4 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -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 diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index db4d5846c..aac2495e6 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -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 diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index f4a842655..931a98dd4 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -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 diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 90868ab02..a423ffb71 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -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 = बूलियन संक्रियाएँ diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index a4717f1ab..b2a8467b9 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -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 diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 07e857baf..14abe736a 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -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 diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 9703dc815..dd184826d 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -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 = ブール演算 diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 31f0a9b5b..b2841a0c3 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -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 = 불리언 연산 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index fdf3ee132..92bd93efc 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -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 diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index a39cb9944..bfb0bb2ba 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -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 diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 802b3acce..0f5738b5a 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -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 diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index d8f4f948a..d08e86631 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -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 = Булевы операции diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index bd8b2babf..2a1726b6b 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -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 diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 440d059fd..dde729f25 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -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 = 布尔运算 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 3b8f71179..a83ad05d8 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -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 = 布林運算 diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index 3c77807fd..6c3db7091 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -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); } diff --git a/src/app/commands/pc_colormap.rs b/src/app/commands/pc_colormap.rs new file mode 100644 index 000000000..64501758b --- /dev/null +++ b/src/app/commands/pc_colormap.rs @@ -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 { + 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) { + let document = &self.tabs[i].scene.document; + let ramps: Vec = 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 { + 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::(), state.elevation.min.trim().parse::()) { + 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::(), state.intensity.max.trim().parse::()); + let elevation = (state.elevation.min.trim().parse::(), state.elevation.max.trim().parse::()); + 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 = 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> { + 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::(); + let hi = state.elevation.max.trim().parse::(); + state.interval = match (lo, hi) { + (Ok(lo), Ok(hi)) => short((hi - lo) / count), + _ => String::new(), + }; +} diff --git a/src/app/commands/pdf_dialogs.rs b/src/app/commands/pdf_dialogs.rs index e88f69f0a..47435b4aa 100644 --- a/src/app/commands/pdf_dialogs.rs +++ b/src/app/commands/pdf_dialogs.rs @@ -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) { diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index 954243365..2885aa7d3 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -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 = 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, diff --git a/src/app/mod.rs b/src/app/mod.rs index 11b152e26..1d7ab9361 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -498,6 +498,7 @@ pub(super) struct OpenCADStudio { /// PDF dialogs' working copies; `None` while closed. pdf_attach: Option, point_cloud_attach: Option, + point_cloud_color_map: Option, underlay_layers: Option, pdf_import_settings: Option, pdf_import_file: Option, @@ -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, diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 3391d47d9..6eec7b710 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -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| { diff --git a/src/entities/extended.rs b/src/entities/extended.rs index cd7033929..d7f4fb6c9 100644 --- a/src/entities/extended.rs +++ b/src/entities/extended.rs @@ -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. diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index cae6c29c8..ff97a8732 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -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")), diff --git a/src/modules/insert/pc_stylize.rs b/src/modules/insert/pc_stylize.rs index ebefacc46..1445d0ed0 100644 --- a/src/modules/insert/pc_stylize.rs +++ b/src/modules/insert/pc_stylize.rs @@ -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] : +// +// A picked cloud opens the dialog for it; None (or Enter) for the +// drawing's default schemes. +pub struct PointCloudColorMapCommand { + picked: Option, +} + +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] :".to_string() + } + + fn options(&self) -> Vec { + 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 { + 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"] }); diff --git a/src/scene/model/point_cloud/mod.rs b/src/scene/model/point_cloud/mod.rs index 2ae89752e..7c1da4378 100644 --- a/src/scene/model/point_cloud/mod.rs +++ b/src/scene/model/point_cloud/mod.rs @@ -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]> { diff --git a/src/ui/window/pdf_dialogs.rs b/src/ui/window/pdf_dialogs.rs index 5e8ac5eb4..08aaef4df 100644 --- a/src/ui/window/pdf_dialogs.rs +++ b/src/ui/window/pdf_dialogs.rs @@ -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, + /// Elevation tab (else intensity). + pub elevation_tab: bool, + pub ramps: Vec, + 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> = 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 = 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| -> 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 = 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 = (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() +}