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 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-09-30 10:14:44 +03:00
1 parent 3afd547e28
commit c2d9ac5ae2
48 files changed
+1711 -83

No files matched your search

+33
View File
@@ -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 = عمليات منطقية
+33
View File
@@ -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 = Булеви операции
+33
View File
@@ -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
+33
View File
@@ -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
+33
View File
@@ -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 = Δυαδική πράξη
+33
View File
@@ -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
+33
View File
@@ -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
+33
View File
@@ -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
+33
View File
@@ -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
+33
View File
@@ -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 = बूलियन संक्रियाएँ
+33
View File
@@ -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
+33
View File
@@ -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
+33
View File
@@ -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 = ブール演算
+33
View File
@@ -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 = 불리언 연산
+33
View File
@@ -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
+33
View File
@@ -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
+33
View File
@@ -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
+33
View File
@@ -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 = Булевы операции
+33
View File
@@ -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
+33
View File
@@ -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 = 布尔运算
+33
View File
@@ -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 = 布林運算
+186 -1
View File
@@ -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<Message> {
use crate::command::CadCommand;
let Some(state) = self.point_cloud_attach.as_ref() else {
return Task::none();
};
let number = |s: &str| s.trim().parse::<f64>().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() {
+164
View File
@@ -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<codec::Handle> = 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<codec::Handle> =
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();
+32 -15
View File
@@ -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 {
+2 -7
View File
@@ -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" => {
+1
View File
@@ -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,
+3
View File
@@ -497,6 +497,7 @@ pub(super) struct OpenCADStudio {
block_definition: Option<crate::ui::window::block_definition::BlockDefinitionState>,
/// PDF dialogs' working copies; `None` while closed.
pdf_attach: Option<crate::ui::window::pdf_dialogs::PdfAttachState>,
point_cloud_attach: Option<crate::ui::window::pdf_dialogs::PointCloudAttachState>,
underlay_layers: Option<crate::ui::window::pdf_dialogs::UnderlayLayersState>,
pdf_import_settings: Option<crate::modules::insert::pdf_import::PdfImportSettings>,
pdf_import_file: Option<crate::ui::window::pdf_dialogs::PdfImportFileState>,
@@ -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,
+14
View File
@@ -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.",
-6
View File
@@ -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!(
+5
View File
@@ -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(
+7
View File
@@ -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| {
+18 -14
View File
@@ -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) {
+141 -18
View File
@@ -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<Handle> = 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<Handle, String> = 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<RefTarget> {
@@ -1404,6 +1454,11 @@ fn find_target(doc: &CadDocument, key: u64) -> Option<RefTarget> {
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<String, Strin
}
Ok(name)
}
// So does a point cloud (its definition's loaded flag).
RefTarget::PointCloud { handle, name } => {
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<String, Strin
doc.objects.remove(&handle);
Ok(name)
}
// The clouds, their reactors, the definition and its dictionary
// entry.
RefTarget::PointCloud { handle, name } => {
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;
}
}
+3 -1
View File
@@ -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`].
+19
View File
@@ -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")),
+7
View File
@@ -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;
+4 -6
View File
@@ -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",
+58 -11
View File
@@ -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<String>,
/// Stored relative to the drawing (once it has a file).
pub relative: bool,
/// Locked on attach; None follows POINTCLOUDLOCK.
pub locked: Option<bool>,
/// Zoom to the cloud once attached.
pub zoom: bool,
pub scale: Option<f64>,
pub rotation: Option<f64>,
}
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,
}
}
}
+13
View File
@@ -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),
+88
View File
@@ -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] <RGB>:
// 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<usize> = Mutex::new(0);
pub struct PointCloudStylizeCommand {
handles: Vec<Handle>,
}
impl PointCloudStylizeCommand {
pub fn new(handles: Vec<Handle>) -> 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<CmdOption> {
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<CmdResult> {
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<String> = 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"] });
+8
View File
@@ -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!(
+32 -2
View File
@@ -26,6 +26,14 @@ pub struct CloudPoints {
pub intensity: Vec<u8>,
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<Vec<u8>>,
/// 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<CloudPoints> {
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<PathBuf> {
// 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;
+15
View File
@@ -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<Vec<u8>> {
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<Vec<ScanEntry>> {
let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).ok()?;
let folder = project.parent().unwrap_or(Path::new(""));
+7
View File
@@ -23,10 +23,13 @@ pub struct Scan {
pub colors: Vec<[u8; 3]>,
pub intensity: Vec<u8>,
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<Scan> {
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<Scan> {
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;
+27
View File
@@ -59,6 +59,24 @@ pub fn pdf_underlay_tools(kind: codec::entities::UnderlayType) -> Vec<ToolDef> {
tools
}
/// Point cloud tools: the three crops, showing, inverting and removing
/// them, and the palette.
pub fn point_cloud_tools() -> Vec<ToolDef> {
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<ToolDef> {
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<bool>) {
self.point_cloud_ctx = context;
}
pub fn point_cloud_context(&self) -> Option<bool> {
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) {
+4 -1
View File
@@ -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<UnderlayContext>,
/// Only xrefs are selected.
xref_ctx: bool,
/// Only point clouds are selected: whether the first shows its crops.
point_cloud_ctx: Option<bool>,
/// 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,
}
}
+146
View File
@@ -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<image::Handle>,
/// "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
+14 -1
View File
@@ -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<image::RgbaImage> {
let img = image::open(found).ok()?;
Some(img.thumbnail(PREVIEW_MAX, PREVIEW_MAX).to_rgba8())
}
RefKind::Underlay => None,
RefKind::Underlay | RefKind::PointCloud => None,
}
}