forked from 1b-agent/opencadstudio-upstream
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:
1 parent
3afd547e28
commit
c2d9ac5ae2
48 files changed
+1711
-83
No files matched your search
@@ -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 = عمليات منطقية
|
||||
|
||||
@@ -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 = Булеви операции
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = Δυαδική πράξη
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = बूलियन संक्रियाएँ
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ブール演算
|
||||
|
||||
@@ -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 = 불리언 연산
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = Булевы операции
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = 布尔运算
|
||||
|
||||
@@ -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 = 布林運算
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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`].
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"] });
|
||||
@@ -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!(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(""));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user