From c2d9ac5ae2d4dd568415fe3fc150a5de57aafcaa Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Wed, 30 Sep 2026 10:14:44 +0300 Subject: [PATCH] Point clouds: attach dialog, crop toolbar, stylize, palette rows POINTCLOUDATTACH opens the Attach Point Cloud dialog after the file is chosen; its Attach button lives in the Attach Underlay list instead of a separate Insert panel. A selected cloud brings the crop tools to the right-edge toolbar. POINTCLOUDSTYLIZE sets the stylization with the reference's prompts, the External References palette lists point clouds (unload, reload, detach, path), and definitions keep the header extents. Co-Authored-By: Claude Opus 5.5 --- locales/ar-SA/opencadstudio.ftl | 33 +++++ locales/bg-BG/opencadstudio.ftl | 33 +++++ locales/cs-CZ/opencadstudio.ftl | 33 +++++ locales/de-DE/opencadstudio.ftl | 33 +++++ locales/el-GR/opencadstudio.ftl | 33 +++++ locales/en-US/opencadstudio.ftl | 33 +++++ locales/es-ES/opencadstudio.ftl | 33 +++++ locales/fi-FI/opencadstudio.ftl | 33 +++++ locales/fr-FR/opencadstudio.ftl | 33 +++++ locales/hi-IN/opencadstudio.ftl | 33 +++++ locales/hu-HU/opencadstudio.ftl | 33 +++++ locales/it-IT/opencadstudio.ftl | 33 +++++ locales/ja-JP/opencadstudio.ftl | 33 +++++ locales/ko-KR/opencadstudio.ftl | 33 +++++ locales/nl-NL/opencadstudio.ftl | 33 +++++ locales/pl-PL/opencadstudio.ftl | 33 +++++ locales/pt-BR/opencadstudio.ftl | 33 +++++ locales/ru-RU/opencadstudio.ftl | 33 +++++ locales/tr-TR/opencadstudio.ftl | 33 +++++ locales/zh-CN/opencadstudio.ftl | 33 +++++ locales/zh-TW/opencadstudio.ftl | 33 +++++ src/app/commands/pdf_dialogs.rs | 187 ++++++++++++++++++++++++++++- src/app/commands/pdf_underlay.rs | 164 +++++++++++++++++++++++++ src/app/commands/point_cloud.rs | 47 +++++--- src/app/commands/xref_attach.rs | 9 +- src/app/control/actions.rs | 1 + src/app/mod.rs | 3 + src/app/update/dialog.rs | 14 +++ src/app/update/file.rs | 6 - src/app/view/mod.rs | 5 + src/app/view/modal.rs | 7 ++ src/entities/extended.rs | 32 ++--- src/io/xref.rs | 159 +++++++++++++++++++++--- src/io/xref_model.rs | 4 +- src/locale_catalog.rs | 19 +++ src/modules/draw/select.rs | 7 ++ src/modules/insert/mod.rs | 10 +- src/modules/insert/pc_attach.rs | 69 +++++++++-- src/modules/insert/pc_crop.rs | 13 ++ src/modules/insert/pc_stylize.rs | 88 ++++++++++++++ src/scene/mod.rs | 8 ++ src/scene/model/point_cloud/mod.rs | 34 +++++- src/scene/model/point_cloud/rcp.rs | 15 +++ src/scene/model/point_cloud/rcs.rs | 7 ++ src/ui/ribbon/context_tools.rs | 27 +++++ src/ui/ribbon/mod.rs | 5 +- src/ui/window/pdf_dialogs.rs | 146 ++++++++++++++++++++++ src/ui/window/xref_manager.rs | 15 ++- 48 files changed, 1711 insertions(+), 83 deletions(-) create mode 100644 src/modules/insert/pc_stylize.rs diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 9e8a602df..5196b9d90 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -3951,6 +3951,39 @@ insert = .block-select-objects = BLOCK حدد الكائنات: .block-specify-insertion-base-point = BLOCK حدد نقطة قاعدة الإدراج: .wblock-specify-insertion-base-point = WBLOCK حدد نقطة أساس الإدراج: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = إنشاء .boolean = عمليات منطقية diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index c8db99ada..6c0668515 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -3938,6 +3938,39 @@ insert = .block-select-objects = BLOCK Изберете обекти: .block-specify-insertion-base-point = BLOCK Укажете базова точка на вмъкване: .wblock-specify-insertion-base-point = WBLOCK Задайте базова точка на вмъкване: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = Създаване .boolean = Булеви операции diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index f037bef15..2c806e8c5 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -3936,6 +3936,39 @@ insert = .block-select-objects = BLOCK Vyberte objekty: .block-specify-insertion-base-point = BLOCK Zadejte základní bod vložení: .wblock-specify-insertion-base-point = WBLOCK Určete základní bod vložení: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = Vytvořit .boolean = Booleovské operace diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 987697176..179c8b1e9 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -3937,6 +3937,39 @@ insert = .block-select-objects = BLOCK Objekte auswählen: .block-specify-insertion-base-point = BLOCK Einfügebasispunkt angeben: .wblock-specify-insertion-base-point = WBLOCK Basispunkt für Einfügung angeben: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = Erstellen .boolean = Boolesche Operationen diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 0b6fc3a5d..343220765 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -3939,6 +3939,39 @@ insert = .block-select-objects = BLOCK Επιλέξτε αντικείμενα: .block-specify-insertion-base-point = BLOCK Καθορίστε σημείο βάσης εισαγωγής: .wblock-specify-insertion-base-point = WBLOCK Καθορίστε σημείο βάσης εισαγωγής: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = Δημιουργία .boolean = Δυαδική πράξη diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index ebc6e6a52..60a144f69 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -3940,6 +3940,39 @@ insert = .block-select-objects = BLOCK Select objects: .block-specify-insertion-base-point = BLOCK Specify insertion base point: .wblock-specify-insertion-base-point = WBLOCK Specify insertion base point: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = Create .boolean = Boolean diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 6f5bb451d..e1605f099 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -3939,6 +3939,39 @@ insert = .block-select-objects = BLOCK Designar objetos: .block-specify-insertion-base-point = BLOCK Precise punto base de inserción: .wblock-specify-insertion-base-point = WBLOCK Precise punto base de inserción: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = Crear .boolean = Booleanas diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 8c7186d33..db4d5846c 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -3938,6 +3938,39 @@ insert = .block-select-objects = BLOCK Valitse kohteet: .block-specify-insertion-base-point = BLOCK Määritä sijoituksen peruspiste: .wblock-specify-insertion-base-point = WBLOCK Määritä lisäyksen peruspiste: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = Luo .boolean = Boolen operaatiot diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 7c0ed134b..f4a842655 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -3939,6 +3939,39 @@ insert = .block-select-objects = BLOCK Sélectionnez les objets : .block-specify-insertion-base-point = BLOCK Spécifiez le point de base d'insertion : .wblock-specify-insertion-base-point = WBLOCK Spécifiez le point de base d'insertion : + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = Créer .boolean = Opérations booléennes diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 696bad1bf..90868ab02 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -3927,6 +3927,39 @@ insert = .block-select-objects = BLOCK ऑब्जेक्ट चुनें: .block-specify-insertion-base-point = BLOCK सम्मिलन आधार बिंदु निर्दिष्ट करें: .wblock-specify-insertion-base-point = WBLOCK सम्मिलन आधार बिंदु निर्दिष्ट करें: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = बनाएँ .boolean = बूलियन संक्रियाएँ diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 4f59af6c2..a4717f1ab 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -3938,6 +3938,39 @@ insert = .block-select-objects = BLOCK Válasszon objektumokat: .block-specify-insertion-base-point = BLOCK Adja meg a beillesztési bázispontot: .wblock-specify-insertion-base-point = WBLOCK Adja meg a beillesztési bázispontot: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = Létrehozás .boolean = Logikai műveletek diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 8ee8680fc..07e857baf 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -3938,6 +3938,39 @@ insert = .block-select-objects = BLOCK Selezionare oggetti: .block-specify-insertion-base-point = BLOCK Specificare punto base di inserimento: .wblock-specify-insertion-base-point = WBLOCK Specificare il punto base di inserimento: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = Crea .boolean = Operazioni booleane diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index d9425b2c7..9703dc815 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -3927,6 +3927,39 @@ insert = .block-select-objects = BLOCK オブジェクトを選択: .block-specify-insertion-base-point = BLOCK 挿入基点を指定: .wblock-specify-insertion-base-point = WBLOCK 挿入基点を指定: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = 作成 .boolean = ブール演算 diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index d26d78125..31f0a9b5b 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -3938,6 +3938,39 @@ insert = .block-select-objects = BLOCK 객체 선택: .block-specify-insertion-base-point = BLOCK 삽입 기준점 지정: .wblock-specify-insertion-base-point = WBLOCK 삽입 기준점 지정: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = 생성 .boolean = 불리언 연산 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 1f3095934..fdf3ee132 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -3937,6 +3937,39 @@ insert = .block-select-objects = BLOCK Objecten selecteren: .block-specify-insertion-base-point = BLOCK Invoegbasispunt opgeven: .wblock-specify-insertion-base-point = WBLOCK Geef basisinvoegpunt op: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = Maken .boolean = Booleaanse bewerkingen diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 681d3fbf3..a39cb9944 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -3938,6 +3938,39 @@ insert = .block-select-objects = BLOCK Wybierz obiekty: .block-specify-insertion-base-point = BLOCK Określ bazowy punkt wstawienia: .wblock-specify-insertion-base-point = WBLOCK Określ punkt bazowy wstawienia: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = Utwórz .boolean = Operacje boolowskie diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 262d1ccc1..802b3acce 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -3939,6 +3939,39 @@ insert = .block-select-objects = BLOCK Selecionar objetos: .block-specify-insertion-base-point = BLOCK Especificar ponto base de inserção: .wblock-specify-insertion-base-point = WBLOCK Especifique o ponto base de inserção: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = Criar .boolean = Operações booleanas diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 9d3455935..d8f4f948a 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -3940,6 +3940,39 @@ insert = .block-select-objects = BLOCK Выберите объекты: .block-specify-insertion-base-point = BLOCK Укажите базовую точку вставки: .wblock-specify-insertion-base-point = WBLOCK Укажите базовую точку вставки: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = Создание .boolean = Булевы операции diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index ee9a3c94d..bd8b2babf 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -3899,6 +3899,39 @@ insert = .block-select-objects = BLOCK Nesneleri seçin: .block-specify-insertion-base-point = BLOCK Ekleme taban noktasını belirtin: .wblock-specify-insertion-base-point = WBLOCK Ekleme taban noktasını belirleyin: + .ul-attach-point-cloud = Nokta Bulutu Bağla + .pc-rect-crop = + Dikdörtgen + Kırp + .pc-poly-crop = + Çokgen + Kırp + .pc-circ-crop = + Dairesel + Kırp + .pc-show-crop = + Kırpmayı + Göster + .pc-invert-crop = + Kırpmayı + Ters Çevir + .pc-uncrop = + Tüm Kırpmaları + Kaldır + .pc-data = Veri: + .pc-size = Boyut: + .pc-unit = Birim: + .pc-options = Seçenekler + .pc-geo = Coğrafi konumu kullan + .pc-lock = Nokta bulutunu kilitle + .pc-zoom = Nokta bulutuna yakınlaştır + .pc-scan-count = __ocs_fmt_0__ tarama + .pc-point-count = __ocs_fmt_0__ nokta + .pc-select-file = Nokta Bulutu Dosyası Seç + .pc-rcp = Nokta Bulutu Projesi + .pc-rcs = Nokta Bulutu Taraması + .pc-clouds = Nokta Bulutları + .pc-help = Bir nokta bulutu taramasını veya projesini bağlar. model = .create = Oluştur .boolean = Boole işlemleri diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index a60d11dd1..440d059fd 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -3922,6 +3922,39 @@ insert = .block-select-objects = BLOCK 选择对象: .block-specify-insertion-base-point = BLOCK 指定插入基点: .wblock-specify-insertion-base-point = WBLOCK 指定插入基点: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = 创建 .boolean = 布尔运算 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index b3681adb5..3b8f71179 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -3934,6 +3934,39 @@ insert = .block-select-objects = BLOCK 選取物件: .block-specify-insertion-base-point = BLOCK 指定插入基準點: .wblock-specify-insertion-base-point = WBLOCK 指定插入基準點: + .ul-attach-point-cloud = Attach Point Cloud + .pc-rect-crop = + Rectangular + Crop + .pc-poly-crop = + Polygonal + Crop + .pc-circ-crop = + Circular + Crop + .pc-show-crop = + Show + Cropping + .pc-invert-crop = + Invert + Cropping + .pc-uncrop = + Remove All + Cropping + .pc-data = Data: + .pc-size = Size: + .pc-unit = Unit: + .pc-options = Options + .pc-geo = Use geographic location + .pc-lock = Lock point cloud + .pc-zoom = Zoom to point cloud + .pc-scan-count = __ocs_fmt_0__ scans + .pc-point-count = __ocs_fmt_0__ points + .pc-select-file = Select Point Cloud File + .pc-rcp = Point Cloud Project + .pc-rcs = Point Cloud Scan + .pc-clouds = Point Clouds + .pc-help = Attaches a point cloud scan or project. model = .create = 建立 .boolean = 布林運算 diff --git a/src/app/commands/pdf_dialogs.rs b/src/app/commands/pdf_dialogs.rs index 85629aed6..e88f69f0a 100644 --- a/src/app/commands/pdf_dialogs.rs +++ b/src/app/commands/pdf_dialogs.rs @@ -6,7 +6,7 @@ use crate::io::xref_model::Pathtype; use crate::modules::insert::pdf_import::{self, PdfFileImport}; use crate::ui::window::pdf_dialogs::{ click_page, item_thumbs, page_thumbs, LayerTarget, PdfAttachState, PdfDialogMsg, PdfImportFileState, - RotationChoice, UnderlayLayersState, + PointCloudAttachState, RotationChoice, UnderlayLayersState, }; /// "7.8740 × 3.9369" (inches) for a page. @@ -102,6 +102,139 @@ impl OpenCADStudio { crate::entities::underlay::display_path(&self.pdf_stored_path(path, state.path_type.0)); } + /// POINTCLOUDATTACH after the file is chosen: the Attach Point Cloud + /// dialog, placed on screen by default. + pub(in crate::app) fn open_point_cloud_attach_dialog(&mut self, path: &std::path::Path) { + let Some(cloud) = crate::scene::model::point_cloud::load(path) else { + return; + }; + let yes_no = |on: bool| if on { crate::t!("Yes") } else { crate::t!("No") }.into_owned(); + let data = format!( + "RGB: {} · {}: {} · {}: {}", + yes_no(cloud.has_rgb), + crate::t!("Intensity"), + yes_no(cloud.has_intensity), + crate::t!("Normals"), + yes_no(cloud.has_normals), + ); + let data_more = format!( + "{}: {} · {}: {}", + crate::t!("Classification"), + yes_no(false), + crate::t!("Segmentation"), + yes_no(false), + ); + let factor = crate::app::properties::insert_unit_scale(self.tabs[self.active_tab].scene.document.header.insertion_units, 6) + .unwrap_or(1.0); + let full = path.to_string_lossy().into_owned(); + let memory = crate::ui::window::pdf_dialogs::ATTACH_MEMORY.lock().ok().and_then(|m| m.clone()).unwrap_or_default(); + let mut state = PointCloudAttachState { + path: full.clone(), + name: file_stem(&full), + preview: cloud.preview.clone().map(iced::widget::image::Handle::from_bytes), + summary: format!( + "{} · {}", + crate::tf!("{count} scans", count = cloud.scans), + crate::tf!("{count} points", count = cloud.positions.len()) + ), + data, + data_more, + size: cloud + .bounds + .map(|[lo, hi]| format!("{:.4} × {:.4} × {:.4}", hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2])) + .unwrap_or_default(), + unit: format!("{} · {} {}", crate::t!("Meters"), crate::t!("Unit factor"), crate::app::properties::format_unit_factor(factor)), + found_in: crate::entities::underlay::display_path(&full), + saved_path: String::new(), + path_type: memory.path_type, + insert_on_screen: true, + insert: ["0.0000".into(), "0.0000".into(), "0.0000".into()], + scale_on_screen: true, + scale: "1.0000".into(), + rotation_on_screen: true, + rotation: "0".into(), + lock: false, + zoom: true, + }; + state.saved_path = crate::entities::underlay::display_path(&self.pdf_stored_path(&full, state.path_type.0)); + self.point_cloud_attach = Some(state); + self.active_modal = Some(crate::app::ModalKind::PointCloudAttach); + } + + /// The dialog's Attach: what it set goes to the command, which asks for + /// the rest. + fn point_cloud_attach_ok(&mut self, i: usize) -> Task { + use crate::command::CadCommand; + let Some(state) = self.point_cloud_attach.as_ref() else { + return Task::none(); + }; + let number = |s: &str| s.trim().parse::().ok(); + let insertion = if state.insert_on_screen { + None + } else { + match (number(&state.insert[0]), number(&state.insert[1]), number(&state.insert[2])) { + (Some(x), Some(y), Some(z)) => Some(glam::DVec3::new(x, y, z)), + _ => { + self.command_line.push_error("Requires numeric value."); + return Task::none(); + } + } + }; + let scale = if state.scale_on_screen { + None + } else { + match number(&state.scale).filter(|v| *v > 0.0) { + Some(v) => Some(v), + None => { + self.command_line.push_error("Value must be positive and nonzero."); + return Task::none(); + } + } + }; + let rotation = if state.rotation_on_screen { + None + } else { + match number(&state.rotation) { + Some(v) => Some(v), + None => { + self.command_line.push_error("Requires numeric value."); + return Task::none(); + } + } + }; + let state = self.point_cloud_attach.take().expect("checked above"); + self.close_active_modal(); + let options = crate::modules::insert::pc_attach::AttachOptions { + stored: (state.path_type.0 == Pathtype::None).then(|| { + std::path::Path::new(&state.path) + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_default() + }), + relative: state.path_type.0 == Pathtype::Relative, + locked: Some(state.lock), + zoom: state.zoom, + scale, + rotation, + }; + let mut command = crate::modules::insert::pc_attach::PointCloudAttachCommand::from_dialog( + std::path::PathBuf::from(&state.path), + options, + ); + match insertion { + Some(point) => { + let result = command.on_point(point); + self.tabs[i].active_cmd = Some(Box::new(command)); + self.apply_cmd_result(result) + } + None => { + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + self.focus_cmd_input() + } + } + } + /// ULAYERS: every underlay of the drawing, the selected one first shown. pub(in crate::app) fn open_underlay_layers_dialog(&mut self, i: usize) { let document = &self.tabs[i].scene.document; @@ -244,6 +377,7 @@ impl OpenCADStudio { crate::t!("Attaches a DGN file as an underlay.") } "attach" => crate::t!("Attaches a PDF file as an underlay."), + "pointcloud" => crate::t!("Attaches a point cloud scan or project."), "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."), }; @@ -343,6 +477,57 @@ impl OpenCADStudio { } PdfDialogMsg::AttachOk => return self.pdf_attach_ok(i), + // ── Attach Point Cloud ──────────────────────────────────────── + PdfDialogMsg::CloudBrowse => return Task::done(Message::PointCloudAttachPick), + PdfDialogMsg::CloudOk => return self.point_cloud_attach_ok(i), + 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) { + s.path_type = choice; + s.saved_path = crate::entities::underlay::display_path(&stored); + } + } + PdfDialogMsg::CloudInsertOnScreen(on) => { + if let Some(s) = self.point_cloud_attach.as_mut() { + s.insert_on_screen = on; + } + } + PdfDialogMsg::CloudInsert(k, value) => { + if let Some(s) = self.point_cloud_attach.as_mut() { + s.insert[k.min(2)] = value; + } + } + PdfDialogMsg::CloudScaleOnScreen(on) => { + if let Some(s) = self.point_cloud_attach.as_mut() { + s.scale_on_screen = on; + } + } + PdfDialogMsg::CloudScale(value) => { + if let Some(s) = self.point_cloud_attach.as_mut() { + s.scale = value; + } + } + PdfDialogMsg::CloudRotationOnScreen(on) => { + if let Some(s) = self.point_cloud_attach.as_mut() { + s.rotation_on_screen = on; + } + } + PdfDialogMsg::CloudRotation(value) => { + if let Some(s) = self.point_cloud_attach.as_mut() { + s.rotation = value; + } + } + PdfDialogMsg::CloudLock(on) => { + if let Some(s) = self.point_cloud_attach.as_mut() { + s.lock = on; + } + } + PdfDialogMsg::CloudZoom(on) => { + if let Some(s) = self.point_cloud_attach.as_mut() { + s.zoom = on; + } + } + // ── Underlay Layers ─────────────────────────────────────────── PdfDialogMsg::LayersUnderlay(name) => { if let Some(s) = self.underlay_layers.as_mut() { diff --git a/src/app/commands/pdf_underlay.rs b/src/app/commands/pdf_underlay.rs index 62e2f0cac..14ce8e662 100644 --- a/src/app/commands/pdf_underlay.rs +++ b/src/app/commands/pdf_underlay.rs @@ -62,6 +62,104 @@ impl OpenCADStudio { }) }; self.ribbon.set_underlay_context(context, xref); + let clouds = if self.tabs[i].is_start { Vec::new() } else { self.selected_point_clouds(i) }; + self.ribbon.set_point_cloud_context(clouds.first().map(|(_, data)| data.show_cropping)); + } + + /// The selected point clouds, or nothing when anything else is selected. + fn selected_point_clouds(&self, i: usize) -> Vec<(codec::Handle, codec::entities::PointCloudExData)> { + let mut out = Vec::new(); + for (handle, entity) in self.tabs[i].scene.selected_entities() { + match entity { + codec::EntityType::Extended(extended) => match &extended.data { + codec::entities::ExtendedEntityData::PointCloudEx(data) => out.push((handle, data.clone())), + _ => return Vec::new(), + }, + _ => return Vec::new(), + } + } + out + } + + /// POINTCLOUDSTYLIZE's result: the clouds that carry the data take the + /// stylization (one undo step); the rest are named. + fn stylize_point_clouds(&mut self, i: usize, handles: &[codec::Handle], stylization: i16) { + let mut done = Vec::new(); + for &handle in handles { + if self.tabs[i].scene.is_layer_locked(handle) { + continue; + } + let document = &self.tabs[i].scene.document; + let Some(codec::EntityType::Extended(extended)) = document.get_entity(handle) else { + continue; + }; + let codec::entities::ExtendedEntityData::PointCloudEx(data) = &extended.data else { + continue; + }; + let cloud = crate::scene::model::point_cloud::resolve_source(document, data) + .and_then(|path| crate::scene::model::point_cloud::load(&path)); + let supported = match (stylization, &cloud) { + (6, _) => false, + (_, None) => true, + (1, Some(c)) => c.has_rgb, + (5, Some(c)) => c.has_intensity, + (3, Some(c)) => c.has_normals, + _ => true, + }; + if supported { + done.push(handle); + } else { + self.command_line.push_info(&format!( + "The selected point cloud({}) does not support this stylization type.", + data.name + )); + } + } + if !done.is_empty() { + self.push_undo_snapshot(i, "POINTCLOUDSTYLIZE"); + for handle in &done { + if let Some(codec::EntityType::Extended(extended)) = self.tabs[i].scene.document.get_entity_mut(*handle) { + if let codec::entities::ExtendedEntityData::PointCloudEx(data) = &mut extended.data { + crate::entities::extended::set_point_cloud_stylization(data, stylization); + } + } + } + let changes: Vec<_> = done.iter().map(|h| (*h, crate::scene::ChangeKind::Modified)).collect(); + self.tabs[i].scene.bump_entities(&changes); + self.tabs[i].dirty = true; + self.refresh_properties(); + } + self.command_line.push_output(&format!("{} point cloud(s) stylized", done.len())); + } + + /// One undo step that edits every selected point cloud not locked. + fn edit_selected_point_clouds( + &mut self, + i: usize, + label: &str, + edit: impl Fn(&mut codec::entities::PointCloudExData), + ) { + let handles: Vec<_> = self + .selected_point_clouds(i) + .into_iter() + .map(|(h, _)| h) + .filter(|h| !self.tabs[i].scene.is_layer_locked(*h)) + .collect(); + if handles.is_empty() { + return; + } + self.push_undo_snapshot(i, label); + for handle in &handles { + if let Some(codec::EntityType::Extended(extended)) = self.tabs[i].scene.document.get_entity_mut(*handle) { + if let codec::entities::ExtendedEntityData::PointCloudEx(data) = &mut extended.data { + edit(data); + } + } + } + let changes: Vec<_> = handles.iter().map(|h| (*h, crate::scene::ChangeKind::Modified)).collect(); + self.tabs[i].scene.bump_entities(&changes); + self.tabs[i].dirty = true; + self.refresh_properties(); } /// One undo step that edits every selected PDF underlay. @@ -156,6 +254,72 @@ impl OpenCADStudio { self.apply_xclip(i, inserts, crate::modules::insert::xclip::XclipAction::Delete); } } + // Point cloud tools: a crop of the first selected cloud, or the + // crops of every selected one. + "_PCCROPRECT" | "_PCCROPPOLY" | "_PCCROPCIRC" => { + use crate::command::CadCommand; + let Some(&(handle, _)) = self.selected_point_clouds(i).first() else { + return Some(Task::none()); + }; + if self.tabs[i].scene.is_layer_locked(handle) { + return Some(Task::none()); + } + let Some(codec::EntityType::Extended(cloud)) = self.tabs[i].scene.document.get_entity(handle).cloned() else { + return Some(Task::none()); + }; + let option = match cmd { + "_PCCROPPOLY" => "P", + "_PCCROPCIRC" => "C", + _ => "", + }; + let command = crate::modules::insert::pc_crop::PointCloudCropCommand::for_cloud(handle, cloud, option); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + "POINTCLOUDSTYLIZE" => { + use crate::command::CadCommand; + let command = crate::modules::draw::select::SelectObjectsCommand::with_prompt( + "POINTCLOUDSTYLIZE", + "_PCSTYLIZE", + "Select point cloud objects:", + ); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + // The chosen objects' point clouds, then the option. + "_PCSTYLIZE" => { + use crate::command::CadCommand; + let handles: Vec = self.tabs[i] + .scene + .selected_entities() + .into_iter() + .filter(|(_, entity)| crate::scene::is_point_cloud(entity)) + .map(|(h, _)| h) + .collect(); + if handles.is_empty() { + return Some(self.finish_dispatch(cmd)); + } + let command = crate::modules::insert::pc_stylize::PointCloudStylizeCommand::new(handles); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + c if c.starts_with("_PCSTYLIZEAPPLY ") => { + let mut parts = c.split_whitespace().skip(1); + let stylization: i16 = parts.next().and_then(|v| v.parse().ok()).unwrap_or(1); + let handles: Vec = + parts.filter_map(|h| u64::from_str_radix(h, 16).ok()).map(codec::Handle::new).collect(); + self.stylize_point_clouds(i, &handles, stylization); + } + "_PCCROPSHOW" => { + let on = !self.selected_point_clouds(i).first().is_some_and(|(_, data)| data.show_cropping); + self.edit_selected_point_clouds(i, "POINTCLOUDCROP", |data| data.show_cropping = on); + } + "_PCCROPINVERT" => self.edit_selected_point_clouds(i, "POINTCLOUDCROP", |data| { + for crop in &mut data.croppings { + crop.inverted = !crop.inverted; + } + }), + "_PCUNCROP" => self.edit_selected_point_clouds(i, "POINTCLOUDUNCROP", |data| data.croppings.clear()), "_PDFULUNCLIP" => { self.edit_selected_underlays(i, "PDFCLIP", |u| { u.clip_boundary_vertices.clear(); diff --git a/src/app/commands/point_cloud.rs b/src/app/commands/point_cloud.rs index 7ff493d58..e7440c4cb 100644 --- a/src/app/commands/point_cloud.rs +++ b/src/app/commands/point_cloud.rs @@ -49,18 +49,8 @@ fn ensure_definition( name: &str, points: &crate::scene::model::point_cloud::CloudPoints, ) -> (Handle, Vector3, Vector3) { - let (mut lo, mut hi) = ([f64::MAX; 3], [f64::MIN; 3]); - for p in &points.positions { - for k in 0..3 { - lo[k] = lo[k].min(p[k]); - hi[k] = hi[k].max(p[k]); - } - } - let (min, max) = if points.positions.is_empty() { - (Vector3::ZERO, Vector3::ZERO) - } else { - (Vector3::new(lo[0], lo[1], lo[2]), Vector3::new(hi[0], hi[1], hi[2])) - }; + let [lo, hi] = points.bounds.unwrap_or_default(); + let (min, max) = (Vector3::new(lo[0], lo[1], lo[2]), Vector3::new(hi[0], hi[1], hi[2])); let same = |stored: &str| stored.replace('/', "\\").eq_ignore_ascii_case(&path.replace('/', "\\")); let existing = document.objects.iter().find_map(|(handle, object)| match object { ObjectType::ClassObject(object) => match &object.data { @@ -107,6 +97,25 @@ impl OpenCADStudio { return; }; let path = placement.path.to_string_lossy().into_owned(); + let options = placement.options.clone(); + // The path stored: as chosen; relative to the drawing once it has a + // file (until then the full path, made relative on the first save). + let host = self.tabs[i].current_path.clone(); + let stored = match &options.stored { + Some(stored) => stored.clone(), + None if options.relative => host + .as_deref() + .and_then(|host| { + crate::io::xref_model::to_pathtype_result(&path, host, crate::io::xref_model::Pathtype::Relative).ok() + }) + .map(|relative| relative.replace('/', "\\")) + .unwrap_or_else(|| path.clone()), + None => path.clone(), + }; + if stored != path { + crate::scene::model::point_cloud::register_source(&stored, placement.path.clone()); + } + let relative_later = options.relative && options.stored.is_none() && host.is_none(); let name = placement .path .file_stem() @@ -115,9 +124,11 @@ impl OpenCADStudio { let pending = self.begin_undo(i, label, 1, false); let document = &mut self.tabs[i].scene.document; // POINTCLOUDLOCK 1 attaches clouds locked. - let locked = crate::scene::model::point_cloud::setting("POINTCLOUDLOCK") - .is_some_and(|setting| crate::scene::model::point_cloud::setting_value(document, setting) == 1); - let (definition, extents_min, extents_max) = ensure_definition(document, &path, &name, &points); + let locked = options.locked.unwrap_or_else(|| { + crate::scene::model::point_cloud::setting("POINTCLOUDLOCK") + .is_some_and(|setting| crate::scene::model::point_cloud::setting_value(document, setting) == 1) + }); + let (definition, extents_min, extents_max) = ensure_definition(document, &stored, &name, &points); let [x, y, z] = placement.axes; // As the reference creates a cloud: scan colours, the intensity // range 0–100 as a gradient, crops shown. @@ -175,6 +186,12 @@ impl OpenCADStudio { } } self.command_line.push_output("1 point cloud attached"); + if relative_later { + self.tabs[i].xref_relative_on_save.insert(format!("{}{:X}", crate::io::xref::POINT_CLOUD_KEY, definition.value())); + } + if options.zoom { + self.tabs[i].scene.zoom_to_entities(&[cloud]); + } } self.tabs[i].dirty = true; if let Some(pd) = pending { diff --git a/src/app/commands/xref_attach.rs b/src/app/commands/xref_attach.rs index 60211567a..3c7c4bc8c 100644 --- a/src/app/commands/xref_attach.rs +++ b/src/app/commands/xref_attach.rs @@ -452,19 +452,14 @@ impl OpenCADStudio { } Task::none() } - // A point cloud goes on to its placement prompts. + // A point cloud goes on to the Attach Point Cloud dialog. "rcp" | "rcs" => { - use crate::command::CadCommand; - let i = self.active_tab; if crate::scene::model::point_cloud::load(&path).is_none() { self.command_line.push_error("All scans are not found or invalid."); self.command_line.push_error("Attach point cloud failed"); return Task::none(); } - let command = - crate::modules::insert::pc_attach::PointCloudAttachCommand::with_file(path); - self.command_line.push_info(&command.prompt()); - self.tabs[i].active_cmd = Some(Box::new(command)); + self.open_point_cloud_attach_dialog(&path); Task::none() } "png" | "jpg" | "jpeg" | "bmp" | "tif" | "tiff" => { diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index b4e73a9f7..954243365 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -713,6 +713,7 @@ impl OpenCADStudio { use crate::ui::window::pdf_dialogs::PdfDialogMsg; Message::PdfDialog(match self.active_modal { Some(crate::app::ModalKind::PdfAttach) => PdfDialogMsg::AttachOk, + Some(crate::app::ModalKind::PointCloudAttach) => PdfDialogMsg::CloudOk, 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 15a5d8b65..11b152e26 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -497,6 +497,7 @@ pub(super) struct OpenCADStudio { block_definition: Option, /// PDF dialogs' working copies; `None` while closed. pdf_attach: Option, + point_cloud_attach: Option, underlay_layers: Option, pdf_import_settings: Option, pdf_import_file: Option, @@ -1901,6 +1902,7 @@ pub enum ModalKind { DrawingUnits, BlockDefinition, PdfAttach, + PointCloudAttach, UnderlayLayers, PdfImportSettings, PdfImportFile, @@ -4031,6 +4033,7 @@ impl OpenCADStudio { drawing_units: None, block_definition: None, pdf_attach: None, + point_cloud_attach: None, underlay_layers: None, pdf_import_settings: None, pdf_import_file: None, diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index 92a555c6d..4cbcf237e 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -995,6 +995,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven } } self.tabs[i].scene.reseed_underlays(); + // Point clouds redraw as unloaded (box and saved path). + self.tabs[i].scene.bump_geometry(); } XrefPaletteOp::Reload => { let base_dir: std::path::PathBuf = host @@ -1036,6 +1038,18 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven self.tabs[i].xref_unloaded.remove(key); } self.tabs[i].scene.reseed_underlays(); + } else if *kind == crate::io::xref_model::RefKind::PointCloud { + // A point cloud reloads by setting its definition loaded. + for (key, row_name, row_kind, _) in &picked { + if row_kind == kind && row_name == name { + crate::io::xref::set_point_cloud_loaded( + &mut self.tabs[i].scene.document, + codec::types::Handle::new(*key), + true, + ); + } + } + self.tabs[i].scene.bump_geometry(); } else if *kind != crate::io::xref_model::RefKind::DwgXref { self.command_line.push_error(crate::tf!( "{}: reload applies to drawing references only.", diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 76d304444..7d9455805 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -2419,12 +2419,6 @@ impl OpenCADStudio { } } } - if path - .extension() - .is_some_and(|extension| extension.eq_ignore_ascii_case("dwg")) - { - crate::io::xref::relative_point_cloud_paths(&mut snapshot, &path); - } let clone_ms = clone_started.elapsed().as_secs_f64() * 1000.0; if crate::perf::enabled() { crate::perf_record!( diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 0c75a7a34..b9ee67da6 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1604,6 +1604,11 @@ bg={bg_ms:.1}ms n={view_count}" None } else if self.ribbon.xref_context() { crate::ui::side_toolbar::view(&crate::ui::ribbon::xref_tools()) + } else if let Some(shown) = self.ribbon.point_cloud_context() { + crate::ui::side_toolbar::view_with_active( + &crate::ui::ribbon::point_cloud_tools(), + &move |id| id == "_PCCROPSHOW" && shown, + ) } else if let Some(ctx) = self.ribbon.underlay_context() { let ctx = ctx.clone(); crate::ui::side_toolbar::view_with_active( diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 2915a28ec..3391d47d9 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -29,6 +29,7 @@ impl OpenCADStudio { Some(codec::entities::UnderlayType::Dgn) => crate::t!("Attach DGN Underlay").into_owned(), _ => crate::t!("Attach PDF Underlay").into_owned(), }, + Some(K::PointCloudAttach) => crate::t!("Attach Point Cloud").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(), @@ -522,6 +523,12 @@ impl OpenCADStudio { crate::ui::window::pdf_dialogs::view_attach(state, flow) }) } + super::super::ModalKind::PointCloudAttach => { + let state = self.point_cloud_attach.as_ref()?; + sized_flow(ex, 880, 560, |flow| { + crate::ui::window::pdf_dialogs::view_point_cloud_attach(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 11684d552..574b752f3 100644 --- a/src/entities/extended.rs +++ b/src/entities/extended.rs @@ -1273,20 +1273,7 @@ fn apply_point_cloud_ex_prop(data: &mut PointCloudExData, field: &str, value: &s .iter() .find(|(name, _)| value == t!(*name).as_ref() || value.eq_ignore_ascii_case(name)) { - data.stylization_type = *stylization; - // Intensity and elevation start from their default schemes, - // Spectrum and Earth. - let default = match stylization { - 5 => Some(POINT_CLOUD_RAMPS[6].1), - 4 => Some(POINT_CLOUD_RAMPS[1].1), - _ => None, - }; - if let Some(default) = default { - let slot = point_cloud_scheme_slot(data); - if slot.is_empty() { - *slot = default.to_string(); - } - } + set_point_cloud_stylization(data, *stylization); } } "ext_pcx_color_scheme" => { @@ -1326,6 +1313,23 @@ fn apply_point_cloud_ex_prop(data: &mut PointCloudExData, field: &str, value: &s } } +/// Sets a cloud's stylization (its stored value); intensity and elevation +/// start from their default schemes, Spectrum and Earth. +pub(crate) fn set_point_cloud_stylization(data: &mut PointCloudExData, stylization: i16) { + data.stylization_type = stylization; + let default = match stylization { + 5 => Some(POINT_CLOUD_RAMPS[6].1), + 4 => Some(POINT_CLOUD_RAMPS[1].1), + _ => None, + }; + if let Some(default) = default { + let slot = point_cloud_scheme_slot(data); + if slot.is_empty() { + *slot = default.to_string(); + } + } +} + /// Rejects what the reference rejects before a point cloud row is written. pub(crate) fn validate_point_cloud_property(field: &str, value: &str) -> Result<(), &'static str> { if field == "ext_pcx_scale" && !parse_f64(value).is_some_and(|scale| scale > 0.0) { diff --git a/src/io/xref.rs b/src/io/xref.rs index 2f1c000ed..f335f4251 100644 --- a/src/io/xref.rs +++ b/src/io/xref.rs @@ -1292,6 +1292,55 @@ pub fn collect_entries_with_prev( entries.push(entry); } + // ── Point clouds ── + // Listed under their definitions' names; a definition no cloud uses is + // Unreferenced, as for images and underlays. + let mut referenced_clouds: HashSet = HashSet::default(); + for e in doc.entities() { + if let EntityType::Extended(extended) = e { + if let codec::entities::ExtendedEntityData::PointCloudEx(data) = &extended.data { + referenced_clouds.insert(data.definition_handle); + } + } + } + let cloud_names: HashMap = doc + .objects + .values() + .filter_map(|object| match object { + ObjectType::Dictionary(dictionary) => Some(dictionary), + _ => None, + }) + .flat_map(|dictionary| dictionary.entries.iter().map(|(name, handle)| (*handle, name.clone()))) + .collect(); + for (handle, obj) in doc.objects.iter() { + let ObjectType::ClassObject(object) = obj else { + continue; + }; + let codec::objects::ClassObjectData::PointCloudDefinitionEx(def) = &object.data else { + continue; + }; + let key = handle.value(); + let name = cloud_names.get(handle).cloned().unwrap_or_else(|| { + Path::new(&def.source_filename.replace('\\', "/")) + .file_stem() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_default() + }); + let mut entry = ReferenceEntry::new(key, name, RefKind::PointCloud); + entry.saved_path = def.source_filename.clone(); + if !def.is_loaded { + entry.status = RefStatus::Unloaded; + resolve_only(&mut entry, &def.source_filename, base_dir); + } else { + stat_into(&mut entry, &def.source_filename, base_dir); + entry.status = decide_status(entry.status, entry.modified, prev.get(&key).copied(), false); + if !referenced_clouds.contains(handle) { + entry.status = RefStatus::Unreferenced; + } + } + entries.push(entry); + } + // ── Nested enumeration: full transitive closure, read-only, no merge ── // Paths stored in a nested drawing are relative to *that drawing*, not to // the host. The work queue carries each parent's resolved file so every @@ -1384,6 +1433,7 @@ enum RefTarget { DwgXref { handle: Handle, name: String }, Image { handle: Handle, name: String }, Pdf { handle: Handle, name: String }, + PointCloud { handle: Handle, name: String }, } fn find_target(doc: &CadDocument, key: u64) -> Option { @@ -1404,6 +1454,11 @@ fn find_target(doc: &CadDocument, key: u64) -> Option { name: def.file_name.clone(), }); } + ObjectType::ClassObject(object) if handle.value() == key => { + if let codec::objects::ClassObjectData::PointCloudDefinitionEx(def) = &object.data { + return Some(RefTarget::PointCloud { handle: *handle, name: def.source_filename.clone() }); + } + } ObjectType::UnderlayDefinition(def) if handle.value() == key => { return Some(RefTarget::Pdf { handle: *handle, @@ -1477,6 +1532,20 @@ pub fn unload_reference(doc: &mut CadDocument, key: u64) -> Result { + set_point_cloud_loaded(doc, handle, false); + Ok(name) + } + } +} + +/// A point cloud definition's loaded flag (Unload / Reload). +pub fn set_point_cloud_loaded(doc: &mut CadDocument, handle: Handle, loaded: bool) { + if let Some(codec::objects::ObjectType::ClassObject(object)) = doc.objects.get_mut(&handle) { + if let codec::objects::ClassObjectData::PointCloudDefinitionEx(def) = &mut object.data { + def.is_loaded = loaded; + } } } @@ -1578,6 +1647,34 @@ pub fn detach_reference(doc: &mut CadDocument, key: u64) -> Result { + use codec::entities::ExtendedEntityData; + let clouds: Vec<(Handle, Handle)> = doc + .entities() + .filter_map(|e| match e { + EntityType::Extended(extended) => match &extended.data { + ExtendedEntityData::PointCloudEx(data) if data.definition_handle == handle => { + Some((e.common().handle, data.reactor_handle)) + } + _ => None, + }, + _ => None, + }) + .collect(); + for (cloud, reactor) in clouds { + doc.remove_entity(cloud); + doc.objects.remove(&reactor); + } + for object in doc.objects.values_mut() { + if let codec::objects::ObjectType::Dictionary(dictionary) = object { + dictionary.entries.retain(|(_, h)| *h != handle); + } + } + doc.objects.remove(&handle); + Ok(name) + } } } @@ -1626,6 +1723,9 @@ pub fn bind_reference( "XREF: PDF bind (vector import) is not available in this version." ) .to_string()), + RefTarget::PointCloud { .. } => { + Err(crate::t!("XREF: bind applies to drawing references only.").to_string()) + } } } @@ -1896,6 +1996,10 @@ fn same_file(a: &Path, b: &Path) -> bool { } } +/// Marks a point cloud definition (by handle) among the references made +/// relative on the drawing's first save. +pub const POINT_CLOUD_KEY: &str = "pointcloud:"; + /// Make the named references' paths relative to `host` (their drawing's /// first save); a reference that cannot be relative keeps its full path. pub fn make_relative( @@ -1904,6 +2008,12 @@ pub fn make_relative( host: &Path, ) { for name in names { + if let Some(handle) = name.strip_prefix(POINT_CLOUD_KEY) { + if let Ok(handle) = u64::from_str_radix(handle, 16) { + relative_point_cloud_path(doc, Handle::new(handle), host); + } + continue; + } let Some(key) = doc.block_records.get(name).map(|br| br.handle.value()) else { continue; }; @@ -2161,6 +2271,19 @@ pub fn set_ref_path( _ => Err(crate::t!("XREF: no loaded reference with that key.").to_string()), } } + RefTarget::PointCloud { handle, name } => { + use codec::objects::{ClassObjectData, ObjectType}; + match doc.objects.get_mut(&handle) { + Some(ObjectType::ClassObject(object)) => match &mut object.data { + ClassObjectData::PointCloudDefinitionEx(def) => { + def.source_filename = new_raw.to_string(); + Ok(name) + } + _ => Err(crate::t!("XREF: no loaded reference with that key.").to_string()), + }, + _ => Err(crate::t!("XREF: no loaded reference with that key.").to_string()), + } + } } } @@ -2192,6 +2315,7 @@ pub fn apply_pathtype( _ => String::new(), } } + Some(RefTarget::PointCloud { name, .. }) => name, None => return Err(crate::t!("XREF: no loaded reference with that key.").to_string()), }; // A stored relative path first has to be resolved against its current host @@ -2320,7 +2444,7 @@ pub fn set_ref_type( } Err(crate::t!("XREF: no loaded reference with that key.").to_string()) } - RefTarget::Image { .. } | RefTarget::Pdf { .. } => { + RefTarget::Image { .. } | RefTarget::Pdf { .. } | RefTarget::PointCloud { .. } => { Err(crate::t!("XREF: overlays apply to drawing references only.").to_string()) } } @@ -3122,23 +3246,22 @@ mod tests { } } -/// Point cloud definitions keep the full path while the drawing is open; a -/// DWG stores it relative to the drawing (".\rcs\scan.rcs"), as the -/// reference writes it. Paths on another drive stay full. -pub fn relative_point_cloud_paths(doc: &mut CadDocument, host: &Path) { - use crate::io::xref_model::{is_relative_path, normalize_lexical, to_pathtype_result, Pathtype}; - for object in doc.objects.values_mut() { - let codec::objects::ObjectType::ClassObject(object) = object else { - continue; - }; - let codec::objects::ClassObjectData::PointCloudDefinitionEx(def) = &mut object.data else { - continue; - }; - if def.source_filename.is_empty() || is_relative_path(&normalize_lexical(&def.source_filename)) { - continue; - } - if let Ok(relative) = to_pathtype_result(&def.source_filename, host, Pathtype::Relative) { - def.source_filename = relative.replace('/', "\\"); +/// A point cloud definition's full path made relative to the drawing +/// (".\rcs\scan.rcs", as the reference writes it); on another drive it +/// stays full. +fn relative_point_cloud_path(doc: &mut CadDocument, handle: Handle, host: &Path) { + use crate::io::xref_model::{to_pathtype_result, Pathtype}; + let Some(codec::objects::ObjectType::ClassObject(object)) = doc.objects.get_mut(&handle) else { + return; + }; + let codec::objects::ClassObjectData::PointCloudDefinitionEx(def) = &mut object.data else { + return; + }; + if let Ok(relative) = to_pathtype_result(&def.source_filename, host, Pathtype::Relative) { + let relative = relative.replace('/', "\\"); + if let Some(found) = std::path::Path::new(&def.source_filename).is_file().then(|| def.source_filename.clone()) { + crate::scene::model::point_cloud::register_source(&relative, found.into()); } + def.source_filename = relative; } } diff --git a/src/io/xref_model.rs b/src/io/xref_model.rs index 64ef98576..7f2010fc5 100644 --- a/src/io/xref_model.rs +++ b/src/io/xref_model.rs @@ -131,7 +131,7 @@ fn root_key(normalized: &str) -> String { String::new() } -pub fn is_relative_path(normalized: &str) -> bool { +fn is_relative_path(normalized: &str) -> bool { let b = normalized.as_bytes(); if b.len() >= 2 && b[0].is_ascii_alphabetic() && b[1] == b':' { return false; @@ -305,6 +305,8 @@ pub enum RefKind { Image, /// A PDF, DWF or DGN underlay. Underlay, + /// A point cloud scan (.rcs) or project (.rcp). + PointCloud, } /// Lifecycle state of a [`ReferenceEntry`]. diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 2dc5c762c..cae6c29c8 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -96,6 +96,25 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "#RRGGBB or blank" => Some(("common", "rrggbb-or-blank")), "%{cmd} Select objects (%{count} selected, Enter to apply):" => Some(("selection", "cmd-select-objects-count-selected-enter-to-app")), "%{cmd} Select objects:" => Some(("selection", "cmd-select-objects")), + "Attach Point Cloud" => Some(("insert", "ul-attach-point-cloud")), + "Rectangular\nCrop" => Some(("insert", "pc-rect-crop")), + "Polygonal\nCrop" => Some(("insert", "pc-poly-crop")), + "Circular\nCrop" => Some(("insert", "pc-circ-crop")), + "Show\nCropping" => Some(("insert", "pc-show-crop")), + "Invert\nCropping" => Some(("insert", "pc-invert-crop")), + "Remove All\nCropping" => Some(("insert", "pc-uncrop")), + "Data:" => Some(("insert", "pc-data")), + "Size:" => Some(("insert", "pc-size")), + "Use geographic location" => Some(("insert", "pc-geo")), + "Lock point cloud" => Some(("insert", "pc-lock")), + "Zoom to point cloud" => Some(("insert", "pc-zoom")), + "{count} scans" => Some(("insert", "pc-scan-count")), + "{count} points" => Some(("insert", "pc-point-count")), + "Select Point Cloud File" => Some(("insert", "pc-select-file")), + "Point Cloud Project" => Some(("insert", "pc-rcp")), + "Point Cloud Scan" => Some(("insert", "pc-rcs")), + "Point Clouds" => Some(("insert", "pc-clouds")), + "Attaches a point cloud scan or project." => Some(("insert", "pc-help")), "Stylization" => Some(("properties", "stylization")), "Color scheme" => Some(("properties", "color-scheme")), "Scan Colors" => Some(("properties", "scan-colors")), diff --git a/src/modules/draw/select.rs b/src/modules/draw/select.rs index c495adf5d..43d6e3873 100644 --- a/src/modules/draw/select.rs +++ b/src/modules/draw/select.rs @@ -124,6 +124,13 @@ impl SelectObjectsCommand { command } + /// The standard gather with the command's own wording. + pub fn with_prompt(prompt_cmd: &str, pending_cmd: &str, prompt_text: &'static str) -> Self { + let mut command = Self::plain(prompt_cmd, pending_cmd); + command.prompt_text = Some(prompt_text); + command + } + pub fn auto_constrain(pending_cmd: &str) -> Self { let mut command = Self::new(pending_cmd); command.auto_constrain_settings = true; diff --git a/src/modules/insert/mod.rs b/src/modules/insert/mod.rs index d8b289ff7..e64cffbc9 100644 --- a/src/modules/insert/mod.rs +++ b/src/modules/insert/mod.rs @@ -18,6 +18,7 @@ mod mview_block; mod open_obj; pub(crate) mod pc_attach; pub(crate) mod pc_crop; +pub(crate) mod pc_stylize; pub(crate) mod pdf_attach; pub(crate) mod pdf_clip; pub(crate) mod pdf_import; @@ -54,7 +55,8 @@ impl CadModule for InsertModule { title: "Reference", tools: vec![ RibbonItem::LargeTool(xattach::tool()), - // PDF, DWF or DGN; the face runs the last one chosen. + // PDF, DWF, DGN or a point cloud; the face runs the + // last one chosen. RibbonItem::LargeDropdown { id: "UNDERLAY_ATTACH", label: "Attach Underlay", @@ -63,6 +65,7 @@ impl CadModule for InsertModule { ("PDFATTACH", "Attach PDF", pdf_attach::ICON), ("DWFATTACH", "Attach DWF", pdf_attach::ICON), ("DGNATTACH", "Attach DGN", pdf_attach::ICON), + ("POINTCLOUDATTACH", "Attach Point Cloud", pc_attach::ICON), ], default: "PDFATTACH", }, @@ -96,11 +99,6 @@ impl CadModule for InsertModule { }, ], }, - // ── Point Cloud ─────────────────────────────────────────────────── - RibbonGroup { - title: "Point Cloud", - tools: vec![RibbonItem::LargeTool(pc_attach::tool())], - }, // ── Block ───────────────────────────────────────────────────────── RibbonGroup { title: "Block", diff --git a/src/modules/insert/pc_attach.rs b/src/modules/insert/pc_attach.rs index ed935e7c1..ab9d3c593 100644 --- a/src/modules/insert/pc_attach.rs +++ b/src/modules/insert/pc_attach.rs @@ -16,15 +16,33 @@ use std::path::PathBuf; use glam::DVec3; use crate::command::{CadCommand, CmdResult, InputKind, WorkingPlane}; -use crate::modules::{IconKind, ModuleEvent, ToolDef}; +use crate::modules::IconKind; pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/pc_attach.svg")); -pub fn tool() -> ToolDef { - ToolDef { - id: "POINTCLOUDATTACH", - label: "Attach", - icon: ICON, - event: ModuleEvent::Command("POINTCLOUDATTACH".to_string()), + +/// What the Attach Point Cloud dialog settles before the prompts: the +/// path to store, the switches, and the scale and rotation not left to +/// the command line. +#[derive(Clone, Debug)] +pub struct AttachOptions { + /// The path stored as given (no path: the file name); None stores the + /// full path. + pub stored: Option, + /// Stored relative to the drawing (once it has a file). + pub relative: bool, + /// Locked on attach; None follows POINTCLOUDLOCK. + pub locked: Option, + /// Zoom to the cloud once attached. + pub zoom: bool, + pub scale: Option, + pub rotation: Option, +} + +impl Default for AttachOptions { + fn default() -> Self { + // The command line attaches with a relative path, as the dialog + // does by default. + Self { stored: None, relative: true, locked: None, zoom: false, scale: None, rotation: None } } } @@ -35,6 +53,7 @@ pub struct PointCloudPlacement { pub insertion: DVec3, /// The placement's axes in the drawing, each as long as the scale. pub axes: [DVec3; 3], + pub options: AttachOptions, } #[derive(Clone, Copy, PartialEq, Eq)] @@ -51,6 +70,7 @@ pub struct PointCloudAttachCommand { insertion: DVec3, scale: f64, plane: WorkingPlane, + options: AttachOptions, } /// A number as the reference echoes a coordinate or an angle: no trailing @@ -67,9 +87,17 @@ impl PointCloudAttachCommand { insertion: DVec3::ZERO, scale: 1.0, plane: WorkingPlane::default(), + options: AttachOptions::default(), } } + /// From the Attach Point Cloud dialog: the file chosen, what it set. + pub fn from_dialog(path: PathBuf, options: AttachOptions) -> Self { + let mut command = Self::with_file(path); + command.options = options; + command + } + /// Starts at the insertion point, the file already chosen. pub fn with_file(path: PathBuf) -> Self { let mut command = Self::new(); @@ -103,12 +131,22 @@ impl PointCloudAttachCommand { self.insertion = point; self.step = Step::Scale; let local = self.plane.to_local(point); - CmdResult::ReportMeasurement(format!( + let message = format!( "Current insertion point: X = {}, Y = {}, Z = {}", plain(local.x), plain(local.y), plain(local.z) - )) + ); + // Scale and rotation the dialog set are not asked for. + let Some(scale) = self.options.scale else { + return CmdResult::ReportMeasurement(message); + }; + self.scale = scale; + self.step = Step::Rotation; + match self.options.rotation { + Some(degrees) => self.placed(degrees, message), + None => CmdResult::ReportMeasurement(message), + } } fn accept_scale(&mut self, text: &str) -> CmdResult { @@ -129,10 +167,18 @@ impl PointCloudAttachCommand { } self.scale = scale; self.step = Step::Rotation; - CmdResult::ReportMeasurement(format!("Current scale factor: {scale:.6}")) + let message = format!("Current scale factor: {scale:.6}"); + match self.options.rotation { + Some(degrees) => self.placed(degrees, message), + None => CmdResult::ReportMeasurement(message), + } } fn accept_rotation(&mut self, degrees: f64) -> CmdResult { + self.placed(degrees, format!("Current rotate angle: {}", plain(degrees))) + } + + fn placed(&mut self, degrees: f64, message: String) -> CmdResult { let (sin, cos) = degrees.to_radians().sin_cos(); let x = (self.plane.x * cos + self.plane.y * sin) * self.scale; let y = (self.plane.y * cos - self.plane.x * sin) * self.scale; @@ -141,8 +187,9 @@ impl PointCloudAttachCommand { path: self.path.clone(), insertion: self.insertion, axes: [x, y, self.plane.z * self.scale], + options: self.options.clone(), }, - message: format!("Current rotate angle: {}", plain(degrees)), + message, } } } diff --git a/src/modules/insert/pc_crop.rs b/src/modules/insert/pc_crop.rs index 4f4575779..3460278a9 100644 --- a/src/modules/insert/pc_crop.rs +++ b/src/modules/insert/pc_crop.rs @@ -73,6 +73,19 @@ impl PointCloudCropCommand { } } + /// The toolbar's crops: the cloud already chosen, the shape too + /// ("P" polygon, "C" circle, "" rectangle). + pub fn for_cloud(handle: Handle, cloud: ExtendedEntity, option: &str) -> Self { + let mut command = Self::new(); + command.handle = handle; + command.cloud = Some(cloud); + command.step = Step::First; + if !option.is_empty() { + command.option(option); + } + command + } + fn data(&self) -> Option<&PointCloudExData> { match &self.cloud.as_ref()?.data { ExtendedEntityData::PointCloudEx(data) => Some(data), diff --git a/src/modules/insert/pc_stylize.rs b/src/modules/insert/pc_stylize.rs new file mode 100644 index 000000000..ebefacc46 --- /dev/null +++ b/src/modules/insert/pc_stylize.rs @@ -0,0 +1,88 @@ +// POINTCLOUDSTYLIZE — colour the chosen point clouds by one stylization. +// +// Select point cloud objects: +// Enter a stylization option [RGB/Object color/Intensity/Elevation/Normal/Classification] : +// 1 point cloud(s) stylized +// +// The option offered is the last one chosen. The host applies it (one undo +// step) and reports clouds that lack the data. + +use std::sync::Mutex; + +use codec::types::Handle; + +use crate::command::{CadCommand, CmdOption, CmdResult, InputKind}; + +/// The options with their stored stylization values. +pub const OPTIONS: [(&str, &str, i16); 6] = [ + ("RGB", "R", 1), + ("Object color", "O", 2), + ("Intensity", "I", 5), + ("Elevation", "E", 4), + ("Normal", "N", 3), + ("Classification", "C", 6), +]; + +/// The option last chosen (RGB to begin with). +static LAST: Mutex = Mutex::new(0); + +pub struct PointCloudStylizeCommand { + handles: Vec, +} + +impl PointCloudStylizeCommand { + pub fn new(handles: Vec) -> Self { + Self { handles } + } +} + +impl CadCommand for PointCloudStylizeCommand { + fn name(&self) -> &'static str { + "POINTCLOUDSTYLIZE" + } + + fn prompt(&self) -> String { + let last = LAST.lock().map(|v| *v).unwrap_or(0); + format!( + "Enter a stylization option [RGB/Object color/Intensity/Elevation/Normal/Classification] <{}>:", + OPTIONS[last].0 + ) + } + + fn options(&self) -> Vec { + OPTIONS.iter().map(|(name, key, _)| CmdOption::new(name, key)).collect() + } + + fn input_kind(&self) -> InputKind { + InputKind::SingleToken + } + + fn on_text_input(&mut self, text: &str) -> Option { + let text = text.trim().to_ascii_uppercase(); + let chosen = if text.is_empty() { + LAST.lock().map(|v| *v).unwrap_or(0) + } else { + match OPTIONS.iter().position(|(name, key, _)| { + text == *key || (text.len() >= key.len() && name.to_ascii_uppercase().starts_with(&text)) + }) { + Some(at) => at, + None => return Some(CmdResult::ReportError("Invalid option keyword.".to_string())), + } + }; + if let Ok(mut last) = LAST.lock() { + *last = chosen; + } + let handles: Vec = self.handles.iter().map(|h| format!("{:X}", h.value())).collect(); + Some(CmdResult::Dispatch(format!("_PCSTYLIZEAPPLY {} {}", OPTIONS[chosen].2, handles.join(" ")))) + } + + fn on_point(&mut self, _pt: glam::DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + self.on_text_input("").unwrap_or(CmdResult::Cancel) + } +} + +inventory::submit!(crate::command::CommandRegistration { names: &["POINTCLOUDSTYLIZE"] }); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 34837001e..c71f877dd 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -13924,6 +13924,14 @@ mod update_entities_batch_tests { } } +pub(crate) fn is_point_cloud(entity: &EntityType) -> bool { + matches!( + entity, + EntityType::Extended(extended) + if matches!(extended.data, codec::entities::ExtendedEntityData::PointCloudEx(_)) + ) +} + /// A point cloud whose Locked property is set: edits leave it alone. pub(crate) fn is_locked_point_cloud(entity: &EntityType) -> bool { matches!( diff --git a/src/scene/model/point_cloud/mod.rs b/src/scene/model/point_cloud/mod.rs index 6a2fd5da4..d35ebbefd 100644 --- a/src/scene/model/point_cloud/mod.rs +++ b/src/scene/model/point_cloud/mod.rs @@ -26,6 +26,14 @@ pub struct CloudPoints { pub intensity: Vec, pub has_rgb: bool, pub has_intensity: bool, + pub has_normals: bool, + /// Scans read (one for a scan file). + pub scans: usize, + /// A project's preview picture (JPEG). + pub preview: Option>, + /// Bounds from the scans' headers, placed like their points: the + /// extents a definition records. + pub bounds: Option<[[f64; 3]; 2]>, } /// Scan points by file path and modification time. A file that fails to @@ -80,14 +88,32 @@ fn decode(path: &Path) -> Option { intensity: Vec::new(), has_rgb: scans.iter().any(|(scan, _)| scan.has_rgb), has_intensity: scans.iter().any(|(scan, _)| scan.has_intensity), + has_normals: scans.iter().any(|(scan, _)| scan.has_normals), + scans: scans.len(), + preview: is_project.then(|| rcp::preview(&bytes)).flatten(), + bounds: None, }; for (scan, [translation, rotation, scale]) in scans { let rotate = euler(rotation); - cloud.positions.extend(scan.local.iter().map(|p| { + let place = |p: &[f64; 3]| { let s = [p[0] * scale[0], p[1] * scale[1], p[2] * scale[2]]; let r = rotate.map(|row| row[0] * s[0] + row[1] * s[1] + row[2] * s[2]); [r[0] + translation[0], r[1] + translation[1], r[2] + translation[2]] - })); + }; + cloud.positions.extend(scan.local.iter().map(place)); + let [lo, hi] = scan.bounds; + for corner in 0..8 { + let p = place(&[ + if corner & 1 == 0 { lo[0] } else { hi[0] }, + if corner & 2 == 0 { lo[1] } else { hi[1] }, + if corner & 4 == 0 { lo[2] } else { hi[2] }, + ]); + let [min, max] = cloud.bounds.get_or_insert([p, p]); + for k in 0..3 { + min[k] = min[k].min(p[k]); + max[k] = max[k].max(p[k]); + } + } if scan.has_rgb || !cloud.has_rgb { cloud.colors.extend_from_slice(&scan.colors); } else { @@ -150,6 +176,10 @@ pub(crate) fn register_source(stored: &str, resolved: PathBuf) { /// is, else where it was found next to the drawing, else relative to the /// working folder. pub(crate) fn resolve_source(document: &CadDocument, data: &PointCloudExData) -> Option { + // An unloaded cloud draws as a missing one: its box and saved path. + if definition(document, data).is_some_and(|def| !def.is_loaded) { + return None; + } let stored = definition(document, data)?.source_filename.trim(); if stored.is_empty() { return None; diff --git a/src/scene/model/point_cloud/rcp.rs b/src/scene/model/point_cloud/rcp.rs index b5ba0ba54..89f1982c5 100644 --- a/src/scene/model/point_cloud/rcp.rs +++ b/src/scene/model/point_cloud/rcp.rs @@ -14,6 +14,21 @@ pub struct ScanEntry { pub scale: [f64; 3], } +/// The project's preview picture (the JPEG it carries), for the attach +/// dialog. +pub fn preview(bytes: &[u8]) -> Option> { + let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).ok()?; + for i in 0..archive.len() { + let file = archive.by_index(i).ok()?; + if file.name().to_ascii_lowercase().ends_with(".jpg") { + let mut picture = Vec::new(); + file.take(32 << 20).read_to_end(&mut picture).ok()?; + return Some(picture); + } + } + None +} + pub fn scans(bytes: &[u8], project: &Path) -> Option> { let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).ok()?; let folder = project.parent().unwrap_or(Path::new("")); diff --git a/src/scene/model/point_cloud/rcs.rs b/src/scene/model/point_cloud/rcs.rs index a06c11289..2efb8b086 100644 --- a/src/scene/model/point_cloud/rcs.rs +++ b/src/scene/model/point_cloud/rcs.rs @@ -23,10 +23,13 @@ pub struct Scan { pub colors: Vec<[u8; 3]>, pub intensity: Vec, pub has_rgb: bool, + pub has_normals: bool, pub has_intensity: bool, pub translation: [f64; 3], pub rotation: [f64; 3], pub scale: [f64; 3], + /// The scan's bounds as its header records them (local frame). + pub bounds: [[f64; 3]; 2], } const NODE_RECORD: usize = 368; @@ -55,6 +58,8 @@ pub fn decode(b: &[u8]) -> Option { let rotation = vec3_at(b, 0x28)?; let scale = vec3_at(b, 0x40)?; let has_rgb = *b.get(0xE0)? != 0; + let bounds = [vec3_at(b, 0xA0)?, vec3_at(b, 0xB8)?]; + let has_normals = *b.get(0xE1)? != 0; let has_intensity = *b.get(0xE2)? != 0; let sections = u32_at(b, 0x129)? as usize; @@ -80,10 +85,12 @@ pub fn decode(b: &[u8]) -> Option { colors: Vec::new(), intensity: Vec::new(), has_rgb, + has_normals, has_intensity, translation, rotation, scale, + bounds, }; for node in 0..nodes { let record = directory + 8 + node * NODE_RECORD; diff --git a/src/ui/ribbon/context_tools.rs b/src/ui/ribbon/context_tools.rs index d0b4a4c86..db50ec683 100644 --- a/src/ui/ribbon/context_tools.rs +++ b/src/ui/ribbon/context_tools.rs @@ -59,6 +59,24 @@ pub fn pdf_underlay_tools(kind: codec::entities::UnderlayType) -> Vec { tools } +/// Point cloud tools: the three crops, showing, inverting and removing +/// them, and the palette. +pub fn point_cloud_tools() -> Vec { + vec![ + tool("_PCCROPRECT", "Rectangular\nCrop", CLIP), + tool("_PCCROPPOLY", "Polygonal\nCrop", CLIP), + tool("_PCCROPCIRC", "Circular\nCrop", CLIP), + tool( + "_PCCROPSHOW", + "Show\nCropping", + include_bytes!("../../../assets/icons/underlay_frames.svg"), + ), + tool("_PCCROPINVERT", "Invert\nCropping", CLIP), + tool("_PCUNCROP", "Remove All\nCropping", UNCLIP), + tool("EXTERNALREFERENCES", "External\nReferences", crate::ui::icons::FOLDER_OPEN), + ] +} + /// External reference tools: edit, open, clipping and the palette. pub fn xref_tools() -> Vec { vec![ @@ -90,6 +108,15 @@ impl Ribbon { self.xref_ctx } + /// Only point clouds are selected: whether the first shows its crops. + pub fn set_point_cloud_context(&mut self, context: Option) { + self.point_cloud_ctx = context; + } + + pub fn point_cloud_context(&self) -> Option { + self.point_cloud_ctx + } + /// Select a tab by its module id. pub fn select_by_id(&mut self, id: &str) -> bool { match self.modules.iter().position(|m| m.id() == id) { diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index 1aea6eccb..dae84c28f 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -26,7 +26,7 @@ pub(crate) use draw_panel::tools as panel_tools; mod modify_panel; mod color_dropdown; mod context_tools; -pub use context_tools::{pdf_underlay_tools, xref_tools, UnderlayContext}; +pub use context_tools::{pdf_underlay_tools, point_cloud_tools, xref_tools, UnderlayContext}; use widgets::{StyleContext, *}; pub(crate) use widgets::{REDO_HISTORY_ID, UNDO_HISTORY_ID}; mod collapse; @@ -109,6 +109,8 @@ pub struct Ribbon { underlay_ctx: Option, /// Only xrefs are selected. xref_ctx: bool, + /// Only point clouds are selected: whether the first shows its crops. + point_cloud_ctx: Option, /// XDWGFADECTL as the Reference slide-out shows it (negative = off). pub xref_fade: i32, } @@ -206,6 +208,7 @@ impl Ribbon { collapse_tight: Arc::new(AtomicBool::new(false)), underlay_ctx: None, xref_ctx: false, + point_cloud_ctx: None, xref_fade: 50, } } diff --git a/src/ui/window/pdf_dialogs.rs b/src/ui/window/pdf_dialogs.rs index 8946be613..5e8ac5eb4 100644 --- a/src/ui/window/pdf_dialogs.rs +++ b/src/ui/window/pdf_dialogs.rs @@ -35,6 +35,18 @@ pub enum PdfDialogMsg { AttachRotation(String), AttachDetails(bool), AttachOk, + // Attach Point Cloud + CloudBrowse, + CloudPathType(PathTypeChoice), + CloudInsertOnScreen(bool), + CloudInsert(usize, String), + CloudScaleOnScreen(bool), + CloudScale(String), + CloudRotationOnScreen(bool), + CloudRotation(String), + CloudLock(bool), + CloudZoom(bool), + CloudOk, // Underlay Layers LayersUnderlay(String), LayersSearch(String), @@ -665,6 +677,140 @@ pub fn view_attach<'a>( .into() } +// ── Attach Point Cloud ───────────────────────────────────────────────────── + +/// The Attach Point Cloud dialog: the scan or project chosen, what it +/// holds, and how it is placed. +pub struct PointCloudAttachState { + /// The file read (absolute). + pub path: String, + pub name: String, + pub preview: Option, + /// "1 scan · 365281 points". + pub summary: String, + /// Which data the scans carry (colour, intensity, normals; then + /// classification and segmentation). + pub data: String, + pub data_more: String, + /// Width × length × height. + pub size: String, + pub unit: String, + pub found_in: String, + pub saved_path: String, + pub path_type: PathTypeChoice, + pub insert_on_screen: bool, + pub insert: [String; 3], + pub scale_on_screen: bool, + pub scale: String, + pub rotation_on_screen: bool, + pub rotation: String, + pub lock: bool, + pub zoom: bool, +} + +/// A switch that cannot be used here: dimmed, not pressable. +fn off_chip<'a>(label: String) -> Element<'a, Message> { + container(text(label).size(11).style(muted_style)).padding([4, 10]).into() +} + +pub fn view_point_cloud_attach<'a>( + state: &'a PointCloudAttachState, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let file = row![ + text_input("", &state.name).size(12).padding([5, 8]).width(Fill).style(field_style), + container(text(state.summary.clone()).size(11).style(accent_text)).padding([4, 10]).style(well_style), + button(text(t!("Browse...")).size(11)) + .on_press(msg(PdfDialogMsg::CloudBrowse)) + .style(button_style(false)) + .padding([5, 12]), + ] + .spacing(8) + .align_y(iced::Center); + + let picture: Element<'a, Message> = match &state.preview { + Some(handle) => image(handle.clone()).width(Fill).height(Fill).into(), + None => Space::new().width(Fill).height(Fill).into(), + }; + let preview = card( + t!("Preview").into_owned(), + container(picture).padding(6).height(Length::Fixed(226.0)).width(Fill).style(well_style), + ); + + let details = card( + t!("File details").into_owned(), + column![ + info_line(t!("Found in:").into_owned(), &state.found_in), + info_line(t!("Saved path:").into_owned(), &state.saved_path), + info_line(t!("Data:").into_owned(), &state.data), + info_line(String::new(), &state.data_more), + info_line(t!("Size:").into_owned(), &state.size), + info_line(t!("Unit:").into_owned(), &state.unit), + ] + .spacing(4), + ); + + let on_screen = t!("On screen").into_owned(); + let heading = |label: std::borrow::Cow<'static, str>, on: bool, message: Message| { + row![text(label).size(11).width(Fill), chip(on_screen.clone(), on, message)].align_y(iced::Center) + }; + let insert_enabled = !state.insert_on_screen; + let xyz = row![ + field("X".into(), &state.insert[0], insert_enabled, 12.0, |v| msg(PdfDialogMsg::CloudInsert(0, v))), + field("Y".into(), &state.insert[1], insert_enabled, 12.0, |v| msg(PdfDialogMsg::CloudInsert(1, v))), + field("Z".into(), &state.insert[2], insert_enabled, 12.0, |v| msg(PdfDialogMsg::CloudInsert(2, v))), + ] + .spacing(8); + let placement = card( + t!("Placement").into_owned(), + column![ + heading( + t!("Insertion point"), + state.insert_on_screen, + msg(PdfDialogMsg::CloudInsertOnScreen(!state.insert_on_screen)) + ), + xyz, + heading(t!("Scale"), state.scale_on_screen, msg(PdfDialogMsg::CloudScaleOnScreen(!state.scale_on_screen))), + field(String::new(), &state.scale, !state.scale_on_screen, 0.0, |v| msg(PdfDialogMsg::CloudScale(v))), + heading( + t!("Rotation"), + state.rotation_on_screen, + msg(PdfDialogMsg::CloudRotationOnScreen(!state.rotation_on_screen)) + ), + field(String::new(), &state.rotation, !state.rotation_on_screen, 0.0, |v| msg(PdfDialogMsg::CloudRotation(v))), + ] + .spacing(6), + ); + let path = card( + t!("Path type").into_owned(), + segmented( + PathTypeChoice::ALL.iter().map(|c| (*c, c.to_string())).collect(), + state.path_type, + PdfDialogMsg::CloudPathType, + ), + ); + let options = card( + t!("Options").into_owned(), + column![ + off_chip(t!("Use geographic location").into_owned()), + chip(t!("Lock point cloud").into_owned(), state.lock, msg(PdfDialogMsg::CloudLock(!state.lock))), + chip(t!("Zoom to point cloud").into_owned(), state.zoom, msg(PdfDialogMsg::CloudZoom(!state.zoom))), + ] + .spacing(6), + ); + + let body = row![ + column![preview, details].spacing(8).width(Length::FillPortion(3)), + column![placement, path, options].spacing(8).width(Length::FillPortion(2)), + ] + .spacing(10); + column![file, body, footer("pointcloud", None, t!("Attach"), PdfDialogMsg::CloudOk)] + .spacing(10) + .padding([10, 12]) + .width(sizing.width) + .into() +} + // ── Underlay Layers ──────────────────────────────────────────────────────── /// One PDF underlay the dialog can show: its name, its file's layers, and diff --git a/src/ui/window/xref_manager.rs b/src/ui/window/xref_manager.rs index c407c16d7..218bd4485 100644 --- a/src/ui/window/xref_manager.rs +++ b/src/ui/window/xref_manager.rs @@ -1103,6 +1103,11 @@ fn direct_identities(doc: &CadDocument) -> HashSet<(u64, String)> { ObjectType::UnderlayDefinition(def) => { ids.insert((handle.value(), def.file_path.clone())); } + ObjectType::ClassObject(object) => { + if let codec::objects::ClassObjectData::PointCloudDefinitionEx(def) = &object.data { + ids.insert((handle.value(), def.source_filename.clone())); + } + } _ => {} } } @@ -1193,6 +1198,14 @@ fn type_text(entry: &ReferenceEntry) -> std::borrow::Cow<'static, str> { _ => crate::t!("PDF"), } } + // The file's own kind: RCP (project) or RCS (scan). + RefKind::PointCloud => { + let ext = std::path::Path::new(&entry.saved_path.replace('\\', "/")) + .extension() + .map(|e| e.to_string_lossy().to_ascii_uppercase()) + .unwrap_or_default(); + if ext == "RCS" { "RCS".into() } else { "RCP".into() } + } } } @@ -2125,7 +2138,7 @@ fn reference_preview(entry: &ReferenceEntry) -> Option { let img = image::open(found).ok()?; Some(img.thumbnail(PREVIEW_MAX, PREVIEW_MAX).to_rgba8()) } - RefKind::Underlay => None, + RefKind::Underlay | RefKind::PointCloud => None, } }