Merge pull request #1553 from ramox81/codex/pointcloud-attach

Point clouds: drawn from their scans, attached, cropped and stylized.

The fork's codec patch is resolved to the upstream pins: the point cloud
records landed in opencadcodec 5ef9376, carried through opencadkernel
68eeda1 and opencadgraph e204966.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-01 01:49:25 +03:00
commit 3dcebf0dc6
81 files changed
+9162 -305

No files matched your search

Generated
+4 -4
View File
@@ -4205,7 +4205,7 @@ dependencies = [
[[package]] [[package]]
name = "opencadcodec" name = "opencadcodec"
version = "0.5.5" version = "0.5.5"
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=fe3984f#fe3984fb8bf36a02ea899ccbf598e3e64a7b2c11" source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=5ef9376#5ef9376e02996097ff649cd9268c343327413787"
dependencies = [ dependencies = [
"ahash 0.8.12", "ahash 0.8.12",
"anyhow", "anyhow",
@@ -4228,7 +4228,7 @@ dependencies = [
[[package]] [[package]]
name = "opencadgraph" name = "opencadgraph"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=dd40a10#dd40a10a72f7837611489f1a01d3d471237603e2" source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=e204966#e2049666de6acd9681d69f7fb16a9f881b4d8b74"
dependencies = [ dependencies = [
"opencadkernel", "opencadkernel",
"serde_json", "serde_json",
@@ -4237,7 +4237,7 @@ dependencies = [
[[package]] [[package]]
name = "opencadkernel" name = "opencadkernel"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=c637716#c637716276095142bf7d1dac61c6f044be3ca662" source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=68eeda1#68eeda1069440f2e990738edb923f37666920dfc"
dependencies = [ dependencies = [
"cavalier_contours", "cavalier_contours",
"opencadcodec", "opencadcodec",
@@ -4248,7 +4248,7 @@ dependencies = [
[[package]] [[package]]
name = "opencadkernel-constraints" name = "opencadkernel-constraints"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=c637716#c637716276095142bf7d1dac61c6f044be3ca662" source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=68eeda1#68eeda1069440f2e990738edb923f37666920dfc"
dependencies = [ dependencies = [
"nalgebra", "nalgebra",
] ]
+4 -4
View File
@@ -34,10 +34,10 @@ rfd = "0.17"
clap = { version = "4", features = ["derive", "string"] } clap = { version = "4", features = ["derive", "string"] }
env_logger = "0.11" env_logger = "0.11"
log = "0.4" log = "0.4"
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "fe3984f", features = ["serde"] } codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "5ef9376", features = ["serde"] }
kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "c637716", features = ["acis", "offset"] } kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "68eeda1", features = ["acis", "offset"] }
kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "c637716" } kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "68eeda1" }
graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "dd40a10" } graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "e204966" }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1" flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
+2 -2
View File
@@ -15,7 +15,7 @@ serde_json = { version = "1", optional = true }
# Pulled in only by the `host` feature, which adds the `opencadcodec`-typed # Pulled in only by the `host` feature, which adds the `opencadcodec`-typed
# `HostApi` runtime surface. The default crate stays dependency-free so engine # `HostApi` runtime surface. The default crate stays dependency-free so engine
# crates and external tooling can depend on the manifest/ribbon contract cheaply. # crates and external tooling can depend on the manifest/ribbon contract cheaply.
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "fe3984f", optional = true, features = ["serde"] } codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "5ef9376", optional = true, features = ["serde"] }
# Runtime IPC and serialization (host feature only). # Runtime IPC and serialization (host feature only).
interprocess = { version = "2", optional = true } interprocess = { version = "2", optional = true }
@@ -39,7 +39,7 @@ serde_json = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
cargo-lock = "11" cargo-lock = "11"
# opencadcodec is scanned at build time to generate the embedded type registry. # opencadcodec is scanned at build time to generate the embedded type registry.
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "fe3984f", features = ["serde"] } codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "5ef9376", features = ["serde"] }
[dev-dependencies] [dev-dependencies]
serde_json = "1" serde_json = "1"
+1 -1
View File
@@ -8,7 +8,7 @@ publish = false
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "fe3984f", features = ["serde"] } codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "5ef9376", features = ["serde"] }
bincode = "1.3" bincode = "1.3"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
console_error_panic_hook = "0.1" console_error_panic_hook = "0.1"
+90
View File
@@ -3951,6 +3951,87 @@ insert =
.block-select-objects = BLOCK حدد الكائنات: .block-select-objects = BLOCK حدد الكائنات:
.block-specify-insertion-base-point = BLOCK حدد نقطة قاعدة الإدراج: .block-specify-insertion-base-point = BLOCK حدد نقطة قاعدة الإدراج:
.wblock-specify-insertion-base-point = WBLOCK حدد نقطة أساس الإدراج: .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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = إنشاء .create = إنشاء
.boolean = عمليات منطقية .boolean = عمليات منطقية
@@ -5038,6 +5119,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = الكتل: __ocs_fmt_0__ .blocks = الكتل: __ocs_fmt_0__
+90
View File
@@ -3938,6 +3938,87 @@ insert =
.block-select-objects = BLOCK Изберете обекти: .block-select-objects = BLOCK Изберете обекти:
.block-specify-insertion-base-point = BLOCK Укажете базова точка на вмъкване: .block-specify-insertion-base-point = BLOCK Укажете базова точка на вмъкване:
.wblock-specify-insertion-base-point = WBLOCK Задайте базова точка на вмъкване: .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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = Създаване .create = Създаване
.boolean = Булеви операции .boolean = Булеви операции
@@ -5026,6 +5107,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = Блокове: __ocs_fmt_0__ .blocks = Блокове: __ocs_fmt_0__
+90
View File
@@ -3936,6 +3936,87 @@ insert =
.block-select-objects = BLOCK Vyberte objekty: .block-select-objects = BLOCK Vyberte objekty:
.block-specify-insertion-base-point = BLOCK Zadejte základní bod vložení: .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í: .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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = Vytvořit .create = Vytvořit
.boolean = Booleovské operace .boolean = Booleovské operace
@@ -5023,6 +5104,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = Bloky: __ocs_fmt_0__ .blocks = Bloky: __ocs_fmt_0__
+90
View File
@@ -3937,6 +3937,87 @@ insert =
.block-select-objects = BLOCK Objekte auswählen: .block-select-objects = BLOCK Objekte auswählen:
.block-specify-insertion-base-point = BLOCK Einfügebasispunkt angeben: .block-specify-insertion-base-point = BLOCK Einfügebasispunkt angeben:
.wblock-specify-insertion-base-point = WBLOCK Basispunkt für Einfügung 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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = Erstellen .create = Erstellen
.boolean = Boolesche Operationen .boolean = Boolesche Operationen
@@ -5024,6 +5105,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = Blöcke: __ocs_fmt_0__ .blocks = Blöcke: __ocs_fmt_0__
+90
View File
@@ -3939,6 +3939,87 @@ insert =
.block-select-objects = BLOCK Επιλέξτε αντικείμενα: .block-select-objects = BLOCK Επιλέξτε αντικείμενα:
.block-specify-insertion-base-point = BLOCK Καθορίστε σημείο βάσης εισαγωγής: .block-specify-insertion-base-point = BLOCK Καθορίστε σημείο βάσης εισαγωγής:
.wblock-specify-insertion-base-point = WBLOCK Καθορίστε σημείο βάσης εισαγωγής: .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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = Δημιουργία .create = Δημιουργία
.boolean = Δυαδική πράξη .boolean = Δυαδική πράξη
@@ -5026,6 +5107,15 @@ properties =
.model-text-height = { common.model-uppercase } · { common.text-height-sentence-case } .model-text-height = { common.model-uppercase } · { common.text-height-sentence-case }
.insert-unit-scale-is-outside-the-supported-range = Η κλίμακα μονάδας INSERT είναι εκτός του υποστηριζόμενου εύρους. .insert-unit-scale-is-outside-the-supported-range = Η κλίμακα μονάδας INSERT είναι εκτός του υποστηριζόμενου εύρους.
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = Μπλοκ: __ocs_fmt_0__ .blocks = Μπλοκ: __ocs_fmt_0__
+90
View File
@@ -3940,6 +3940,87 @@ insert =
.block-select-objects = BLOCK Select objects: .block-select-objects = BLOCK Select objects:
.block-specify-insertion-base-point = BLOCK Specify insertion base point: .block-specify-insertion-base-point = BLOCK Specify insertion base point:
.wblock-specify-insertion-base-point = WBLOCK 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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = Create .create = Create
.boolean = Boolean .boolean = Boolean
@@ -5027,6 +5108,15 @@ properties =
.model-text-height = Model text height .model-text-height = Model text height
.insert-unit-scale-is-outside-the-supported-range = INSERT unit scale is outside the supported range. .insert-unit-scale-is-outside-the-supported-range = INSERT unit scale is outside the supported range.
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = Blocks: __ocs_fmt_0__ .blocks = Blocks: __ocs_fmt_0__
+90
View File
@@ -3939,6 +3939,87 @@ insert =
.block-select-objects = BLOCK Designar objetos: .block-select-objects = BLOCK Designar objetos:
.block-specify-insertion-base-point = BLOCK Precise punto base de inserción: .block-specify-insertion-base-point = BLOCK Precise punto base de inserción:
.wblock-specify-insertion-base-point = WBLOCK 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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = Crear .create = Crear
.boolean = Booleanas .boolean = Booleanas
@@ -5026,6 +5107,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = Bloques: __ocs_fmt_0__ .blocks = Bloques: __ocs_fmt_0__
+90
View File
@@ -3938,6 +3938,87 @@ insert =
.block-select-objects = BLOCK Valitse kohteet: .block-select-objects = BLOCK Valitse kohteet:
.block-specify-insertion-base-point = BLOCK Määritä sijoituksen peruspiste: .block-specify-insertion-base-point = BLOCK Määritä sijoituksen peruspiste:
.wblock-specify-insertion-base-point = WBLOCK Määritä lisäyksen 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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = Luo .create = Luo
.boolean = Boolen operaatiot .boolean = Boolen operaatiot
@@ -5025,6 +5106,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = Lohkot: __ocs_fmt_0__ .blocks = Lohkot: __ocs_fmt_0__
+90
View File
@@ -3939,6 +3939,87 @@ insert =
.block-select-objects = BLOCK Sélectionnez les objets : .block-select-objects = BLOCK Sélectionnez les objets :
.block-specify-insertion-base-point = BLOCK Spécifiez le point de base d'insertion : .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 : .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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = Créer .create = Créer
.boolean = Opérations booléennes .boolean = Opérations booléennes
@@ -5026,6 +5107,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = Blocs : __ocs_fmt_0__ .blocks = Blocs : __ocs_fmt_0__
+90
View File
@@ -3927,6 +3927,87 @@ insert =
.block-select-objects = BLOCK ऑब्जेक्ट चुनें: .block-select-objects = BLOCK ऑब्जेक्ट चुनें:
.block-specify-insertion-base-point = BLOCK सम्मिलन आधार बिंदु निर्दिष्ट करें: .block-specify-insertion-base-point = BLOCK सम्मिलन आधार बिंदु निर्दिष्ट करें:
.wblock-specify-insertion-base-point = WBLOCK सम्मिलन आधार बिंदु निर्दिष्ट करें: .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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = बनाएँ .create = बनाएँ
.boolean = बूलियन संक्रियाएँ .boolean = बूलियन संक्रियाएँ
@@ -5014,6 +5095,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = ब्लॉक: __ocs_fmt_0__ .blocks = ब्लॉक: __ocs_fmt_0__
+90
View File
@@ -3938,6 +3938,87 @@ insert =
.block-select-objects = BLOCK Válasszon objektumokat: .block-select-objects = BLOCK Válasszon objektumokat:
.block-specify-insertion-base-point = BLOCK Adja meg a beillesztési bázispontot: .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: .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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = Létrehozás .create = Létrehozás
.boolean = Logikai műveletek .boolean = Logikai műveletek
@@ -5025,6 +5106,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = Blokkok: __ocs_fmt_0__ .blocks = Blokkok: __ocs_fmt_0__
+90
View File
@@ -3938,6 +3938,87 @@ insert =
.block-select-objects = BLOCK Selezionare oggetti: .block-select-objects = BLOCK Selezionare oggetti:
.block-specify-insertion-base-point = BLOCK Specificare punto base di inserimento: .block-specify-insertion-base-point = BLOCK Specificare punto base di inserimento:
.wblock-specify-insertion-base-point = WBLOCK Specificare il 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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = Crea .create = Crea
.boolean = Operazioni booleane .boolean = Operazioni booleane
@@ -5025,6 +5106,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = Blocchi: __ocs_fmt_0__ .blocks = Blocchi: __ocs_fmt_0__
+90
View File
@@ -3927,6 +3927,87 @@ insert =
.block-select-objects = BLOCK オブジェクトを選択: .block-select-objects = BLOCK オブジェクトを選択:
.block-specify-insertion-base-point = BLOCK 挿入基点を指定: .block-specify-insertion-base-point = BLOCK 挿入基点を指定:
.wblock-specify-insertion-base-point = WBLOCK 挿入基点を指定: .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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = 作成 .create = 作成
.boolean = ブール演算 .boolean = ブール演算
@@ -5014,6 +5095,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = ブロック: __ocs_fmt_0__ .blocks = ブロック: __ocs_fmt_0__
+90
View File
@@ -3938,6 +3938,87 @@ insert =
.block-select-objects = BLOCK 객체 선택: .block-select-objects = BLOCK 객체 선택:
.block-specify-insertion-base-point = BLOCK 삽입 기준점 지정: .block-specify-insertion-base-point = BLOCK 삽입 기준점 지정:
.wblock-specify-insertion-base-point = WBLOCK 삽입 기준점 지정: .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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = 생성 .create = 생성
.boolean = 불리언 연산 .boolean = 불리언 연산
@@ -5025,6 +5106,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = 블록: __ocs_fmt_0__ .blocks = 블록: __ocs_fmt_0__
+90
View File
@@ -3937,6 +3937,87 @@ insert =
.block-select-objects = BLOCK Objecten selecteren: .block-select-objects = BLOCK Objecten selecteren:
.block-specify-insertion-base-point = BLOCK Invoegbasispunt opgeven: .block-specify-insertion-base-point = BLOCK Invoegbasispunt opgeven:
.wblock-specify-insertion-base-point = WBLOCK Geef basisinvoegpunt op: .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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = Maken .create = Maken
.boolean = Booleaanse bewerkingen .boolean = Booleaanse bewerkingen
@@ -5024,6 +5105,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = Symbolen: __ocs_fmt_0__ .blocks = Symbolen: __ocs_fmt_0__
+90
View File
@@ -3938,6 +3938,87 @@ insert =
.block-select-objects = BLOCK Wybierz obiekty: .block-select-objects = BLOCK Wybierz obiekty:
.block-specify-insertion-base-point = BLOCK Określ bazowy punkt wstawienia: .block-specify-insertion-base-point = BLOCK Określ bazowy punkt wstawienia:
.wblock-specify-insertion-base-point = WBLOCK Określ punkt bazowy 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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = Utwórz .create = Utwórz
.boolean = Operacje boolowskie .boolean = Operacje boolowskie
@@ -5025,6 +5106,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = Bloki: __ocs_fmt_0__ .blocks = Bloki: __ocs_fmt_0__
+90
View File
@@ -3939,6 +3939,87 @@ insert =
.block-select-objects = BLOCK Selecionar objetos: .block-select-objects = BLOCK Selecionar objetos:
.block-specify-insertion-base-point = BLOCK Especificar ponto base de inserção: .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: .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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = Criar .create = Criar
.boolean = Operações booleanas .boolean = Operações booleanas
@@ -5026,6 +5107,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = Blocos: __ocs_fmt_0__ .blocks = Blocos: __ocs_fmt_0__
+90
View File
@@ -3940,6 +3940,87 @@ insert =
.block-select-objects = BLOCK Выберите объекты: .block-select-objects = BLOCK Выберите объекты:
.block-specify-insertion-base-point = BLOCK Укажите базовую точку вставки: .block-specify-insertion-base-point = BLOCK Укажите базовую точку вставки:
.wblock-specify-insertion-base-point = WBLOCK Укажите базовую точку вставки: .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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = Создание .create = Создание
.boolean = Булевы операции .boolean = Булевы операции
@@ -5027,6 +5108,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = Блоки: __ocs_fmt_0__ .blocks = Блоки: __ocs_fmt_0__
+90
View File
@@ -3899,6 +3899,87 @@ insert =
.block-select-objects = BLOCK Nesneleri seçin: .block-select-objects = BLOCK Nesneleri seçin:
.block-specify-insertion-base-point = BLOCK Ekleme taban noktasını belirtin: .block-specify-insertion-base-point = BLOCK Ekleme taban noktasını belirtin:
.wblock-specify-insertion-base-point = WBLOCK Ekleme taban noktasını belirleyin: .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.
.pcm-title = Nokta Bulutu Renk Haritası
.pcm-ramp = Renk rampası
.pcm-count = Renk sayısı
.pcm-gradient = Geçişli göster
.pcm-range = Renklendirilen nokta aralığı
.pcm-max-int = En büyük yoğunluk (%)
.pcm-min-int = En küçük yoğunluk (%)
.pcm-max-elev = En büyük yükseklik
.pcm-min-elev = En küçük yükseklik
.pcm-interval = Aralık yüksekliği
.pcm-extents = Nokta bulutu kapsamına uygula
.pcm-oor = Aralık dışı noktalar
.pcm-oor-minmax = En küçük/en büyük renkleri kullan
.pcm-oor-scan = Tarama RGB renklerini kullan
.pcm-oor-hide = Noktaları gizle
.pcm-current = Stilizasyonu geçerli yap
.pcm-name-taken = Bu adda bir renk şeması zaten var.
.pcm-bad-range = Renklendirilen nokta aralığı geçersiz.
.pcm-help = Nokta bulutunun yoğunluğa veya yüksekliğe göre renklendirildiği renk şemalarını ve aralıkları ayarlar.
.pc-manager = Nokta Bulutu Yöneticisi
.pc-regions-scans = BÖLGELER VE TARAMALAR
.pc-regions = Bölgeler
.pc-unassigned = Atanmamış Noktalar
.pc-scans = Taramalar
.pc-search = Ara
.pc-collapse-all = Tümünü daralt
.pc-expand-all = Tümünü genişlet
.pcm-scheme-name = Şema adı
.pcs-title = Nokta Bulutundan Kesit Çizgileri Çıkar
.pcs-extract = Çıkar
.pcs-entire = Tüm kesit
.pcs-perimeter = Yalnız çevre
.pcs-max-points = En fazla işlenecek nokta
.pcs-faster = Daha hızlı
.pcs-accurate = Daha doğru
.pcs-time = Tahmini süre: __ocs_fmt_0__ dk
.pcs-output = Çıktı geometrisi
.pcs-polylines = 2B Çoklu çizgiler
.pcs-width = Çoklu çizgi genişliği
.pcs-tolerances = Çıkarma toleransları
.pcs-min-length = En kısa çizgi uzunluğu
.pcs-connect = Çizgi birleştirme toleransı
.pcs-angle = Doğrusal açı toleransı
.pcs-preview = Sonucu önizle
.pcs-use-current = Geçerli katmanı kullan
.pcs-positive = Pozitif bir sayı gerekir.
.pcs-positive-int = Pozitif bir tamsayı gerekir.
.pcs-help = Canlı kesitin korunan tarafındaki noktaları çizgilerle izler.
model = model =
.create = Oluştur .create = Oluştur
.boolean = Boole işlemleri .boolean = Boole işlemleri
@@ -4966,6 +5047,15 @@ properties =
.model-text-height = { common.model-uppercase } · { common.text-height-sentence-case } .model-text-height = { common.model-uppercase } · { common.text-height-sentence-case }
.insert-unit-scale-is-outside-the-supported-range = INSERT birim ölçeği desteklenen aralığın dışında. .insert-unit-scale-is-outside-the-supported-range = INSERT birim ölçeği desteklenen aralığın dışında.
.stylization = Stilize etme
.color-scheme = Renk şeması
.scan-colors = Tarama renkleri
.object-color = Nesne rengi
.classification = Sınıflandırma
.show-cropped = Kırpılanı göster
.geolocate = Coğrafi konumlandır
.segmentation = Segmentasyon
.saved-path-lower = Kayıtlı yol
styles = styles =
.blocks = Bloklar: __ocs_fmt_0__ .blocks = Bloklar: __ocs_fmt_0__
+90
View File
@@ -3922,6 +3922,87 @@ insert =
.block-select-objects = BLOCK 选择对象: .block-select-objects = BLOCK 选择对象:
.block-specify-insertion-base-point = BLOCK 指定插入基点: .block-specify-insertion-base-point = BLOCK 指定插入基点:
.wblock-specify-insertion-base-point = WBLOCK 指定插入基点: .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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = 创建 .create = 创建
.boolean = 布尔运算 .boolean = 布尔运算
@@ -5009,6 +5090,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = 块 : __ocs_fmt_0__ .blocks = 块 : __ocs_fmt_0__
+90
View File
@@ -3934,6 +3934,87 @@ insert =
.block-select-objects = BLOCK 選取物件: .block-select-objects = BLOCK 選取物件:
.block-specify-insertion-base-point = BLOCK 指定插入基準點: .block-specify-insertion-base-point = BLOCK 指定插入基準點:
.wblock-specify-insertion-base-point = WBLOCK 指定插入基準點: .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.
.pcm-title = Point Cloud Color Map
.pcm-ramp = Color ramp
.pcm-count = Number of colors
.pcm-gradient = Display as gradient
.pcm-range = Range of colorized points
.pcm-max-int = Maximum intensity
.pcm-min-int = Minimum intensity
.pcm-max-elev = Maximum elevation
.pcm-min-elev = Minimum elevation
.pcm-interval = Interval height
.pcm-extents = Apply to extents of point cloud
.pcm-oor = Out of range points
.pcm-oor-minmax = Use min/max colors
.pcm-oor-scan = Use RGB scan colors
.pcm-oor-hide = Hide points
.pcm-current = Make stylization current
.pcm-name-taken = A color scheme with this name already exists.
.pcm-bad-range = Invalid range of colorized points.
.pcm-help = Sets the color schemes and ranges a point cloud is colored by intensity or elevation.
.pc-manager = Point Cloud Manager
.pc-regions-scans = REGIONS AND SCANS
.pc-regions = Regions
.pc-unassigned = Unassigned Points
.pc-scans = Scans
.pc-search = Search
.pc-collapse-all = Collapse all
.pc-expand-all = Expand all
.pcm-scheme-name = Scheme name
.pcs-title = Extract Section Lines from Point Cloud
.pcs-extract = Extract
.pcs-entire = Entire cross section
.pcs-perimeter = Perimeter only
.pcs-max-points = Maximum points to process
.pcs-faster = Faster
.pcs-accurate = More accurate
.pcs-time = Estimated time: __ocs_fmt_0__ minutes
.pcs-output = Output geometry
.pcs-polylines = 2D Polylines
.pcs-width = Polyline width
.pcs-tolerances = Extraction tolerances
.pcs-min-length = Minimum line length
.pcs-connect = Connect lines tolerance
.pcs-angle = Collinear angle tolerance
.pcs-preview = Preview result
.pcs-use-current = Use Current
.pcs-positive = Requires a positive number.
.pcs-positive-int = Requires a positive integer.
.pcs-help = Traces the points on the kept side of a live section as lines.
model = model =
.create = 建立 .create = 建立
.boolean = 布林運算 .boolean = 布林運算
@@ -5021,6 +5102,15 @@ properties =
.show-history = { properties.show } · { model.history } .show-history = { properties.show } · { model.history }
.solid-history = { properties.solid } · { model.history } .solid-history = { properties.solid } · { model.history }
.solid-type = { properties.solid } · { common.type } .solid-type = { properties.solid } · { common.type }
.stylization = Stylization
.color-scheme = Color scheme
.scan-colors = Scan Colors
.object-color = Object Color
.classification = Classification
.show-cropped = Show cropped
.geolocate = Geolocate
.segmentation = Segmentation
.saved-path-lower = Saved path
styles = styles =
.blocks = 圖塊 : __ocs_fmt_0__ .blocks = 圖塊 : __ocs_fmt_0__
+1 -1
View File
@@ -1265,7 +1265,7 @@ dependencies = [
[[package]] [[package]]
name = "opencadcodec" name = "opencadcodec"
version = "0.5.5" version = "0.5.5"
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=fe3984f#fe3984fb8bf36a02ea899ccbf598e3e64a7b2c11" source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=5ef9376#5ef9376e02996097ff649cd9268c343327413787"
dependencies = [ dependencies = [
"ahash 0.8.12", "ahash 0.8.12",
"anyhow", "anyhow",
+12 -1
View File
@@ -615,6 +615,15 @@ impl OpenCADStudio {
crate::app::commands::pdf_import::PdfImportSource::File(import), crate::app::commands::pdf_import::PdfImportSource::File(import),
); );
} }
CmdResult::AttachPointCloud { placement, message } => {
let label = self.history_label_from_active_cmd(i, "POINTCLOUDATTACH");
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.command_line.push_output(&message);
self.attach_point_cloud(i, label, placement);
self.restore_pre_cmd_tangent();
}
CmdResult::AttachPdfPages { kind, path, pages } => { CmdResult::AttachPdfPages { kind, path, pages } => {
let label = self.history_label_from_active_cmd(i, "PDFATTACH"); let label = self.history_label_from_active_cmd(i, "PDFATTACH");
self.tabs[i].scene.clear_preview_wire(); self.tabs[i].scene.clear_preview_wire();
@@ -1094,7 +1103,9 @@ impl OpenCADStudio {
} }
fn handle_cancel_with_message(&mut self, message: String) -> Task<Message> { fn handle_cancel_with_message(&mut self, message: String) -> Task<Message> {
self.command_line.push_error(&message); for line in message.lines() {
self.command_line.push_error(line);
}
self.apply_cmd_result(CmdResult::Cancel) self.apply_cmd_result(CmdResult::Cancel)
} }
+42
View File
@@ -490,6 +490,47 @@ impl OpenCADStudio {
StepInput::SelectionComplete(kept) StepInput::SelectionComplete(kept)
} }
/// Drops locked point clouds from a completed selection when the active
/// command would move them, saying which were locked and how many went.
fn filter_locked_point_cloud_selection(&mut self, input: StepInput) -> StepInput {
let StepInput::SelectionComplete(handles) = input else {
return input;
};
let i = self.active_tab;
if !self.tabs[i]
.active_cmd
.as_ref()
.is_some_and(|command| command.selection_drops_locked_point_clouds())
{
return StepInput::SelectionComplete(handles);
}
let (dropped, kept): (Vec<Handle>, Vec<Handle>) = handles.into_iter().partition(|h| {
self.tabs[i]
.scene
.document
.get_entity(*h)
.is_some_and(crate::scene::is_locked_point_cloud)
});
for handle in &dropped {
self.tabs[i].scene.deselect_entity(*handle);
}
if !dropped.is_empty() {
let count = dropped.len();
self.command_line.push_info(&if count == 1 {
crate::t!("1 point cloud is locked.").into_owned()
} else {
crate::tf!("{count} point clouds are locked.").into_owned()
});
self.command_line.push_info(&if count == 1 {
crate::t!("1 was filtered out.").into_owned()
} else {
crate::tf!("{count} were filtered out.").into_owned()
});
self.refresh_properties();
}
StepInput::SelectionComplete(kept)
}
/// `feed_command`, also reporting whether the step actually took the input. /// `feed_command`, also reporting whether the step actually took the input.
/// Only a `Text` token can come back unclaimed (`on_text_input` returning /// Only a `Text` token can come back unclaimed (`on_text_input` returning
/// `None`), which is what lets the caller read it as something else — a /// `None`), which is what lets the caller read it as something else — a
@@ -500,6 +541,7 @@ impl OpenCADStudio {
// as the reference does while gathering. // as the reference does while gathering.
let input = self.filter_associative_dimension_selection(input); let input = self.filter_associative_dimension_selection(input);
let input = self.filter_block_reference_selection(input); let input = self.filter_block_reference_selection(input);
let input = self.filter_locked_point_cloud_selection(input);
// XCLIP / CLIP learn which chosen references carry a clip already. // XCLIP / CLIP learn which chosen references carry a clip already.
{ {
let i = self.active_tab; let i = self.active_tab;
+8
View File
@@ -720,6 +720,14 @@ impl OpenCADStudio {
self.command_line.push_info(&command.prompt()); self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command)); self.tabs[i].active_cmd = Some(Box::new(command));
} }
"POINTCLOUDCROP" | "POINTCLOUDUNCROP" => {
use crate::command::CadCommand;
use crate::modules::insert::pc_crop::PointCloudCropCommand;
let command =
if cmd == "POINTCLOUDUNCROP" { PointCloudCropCommand::uncrop() } else { PointCloudCropCommand::new() };
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
"IMAGECLIP" => { "IMAGECLIP" => {
use crate::command::CadCommand; use crate::command::CadCommand;
let command = crate::modules::insert::pdf_clip::PdfClipCommand::image(); let command = crate::modules::insert::pdf_clip::PdfClipCommand::image();
+1 -1
View File
@@ -1911,7 +1911,7 @@ impl OpenCADStudio {
} }
"UNDERLAYLAYERS" | "ULAYERS" => self.open_underlay_layers_dialog(i), "UNDERLAYLAYERS" | "ULAYERS" => self.open_underlay_layers_dialog(i),
"POINTCLOUDATTACH" | "RECAP" | "SYNCPVIEWPORTS" => { "RECAP" | "SYNCPVIEWPORTS" => {
self.command_line self.command_line
.push_info(crate::tf!("{cmd}: not yet implemented.").as_ref()); .push_info(crate::tf!("{cmd}: not yet implemented.").as_ref());
} }
+26
View File
@@ -14,6 +14,9 @@ mod inquiry;
mod layerprops; mod layerprops;
mod layers; mod layers;
pub(crate) mod pdf_import; pub(crate) mod pdf_import;
mod point_cloud;
mod pc_colormap;
mod pc_extract;
mod pdf_underlay; mod pdf_underlay;
mod pdf_dialogs; mod pdf_dialogs;
mod xclip; mod xclip;
@@ -269,6 +272,12 @@ impl OpenCADStudio {
if let Some(t) = self.dispatch_pdf_underlay(cmd, i) { if let Some(t) = self.dispatch_pdf_underlay(cmd, i) {
return Some(t); return Some(t);
} }
if let Some(t) = self.dispatch_pc_colormap(cmd, i) {
return Some(t);
}
if let Some(t) = self.dispatch_pc_extract(cmd, i) {
return Some(t);
}
if let Some(t) = self.dispatch_draw(cmd, i) { if let Some(t) = self.dispatch_draw(cmd, i) {
return Some(t); return Some(t);
} }
@@ -636,6 +645,21 @@ inventory::submit!(crate::command::CommandRegistration {
"PDFIMPORTIMAGEPATH", "PDFIMPORTIMAGEPATH",
"XDWGFADECTL", "XDWGFADECTL",
"POINTCLOUDCLIPFRAME", "POINTCLOUDCLIPFRAME",
"POINTCLOUDDENSITY",
"POINTCLOUDPOINTSIZE",
"POINTCLOUDLOCK",
"POINTCLOUDAUTOUPDATE",
"POINTCLOUDBOUNDARY",
"POINTCLOUDRTDENSITY",
"POINTCLOUDLOD",
"POINTCLOUDPOINTMAX",
"POINTCLOUDVISRETAIN",
"POINTCLOUDSHADING",
"POINTCLOUDCACHESIZE",
"POINTCLOUD2DVSDISPLAY",
"POINTCLOUDLIGHTING",
"POINTCLOUDLIGHTSOURCE",
"POINTCLOUDPOINTMAXLEGACY",
"XCLIPFRAME", "XCLIPFRAME",
"WIPEOUTFRAME", "WIPEOUTFRAME",
"FRAMES0", "FRAMES0",
@@ -716,6 +740,8 @@ inventory::submit!(crate::command::CommandRegistration {
"EXPORTSTEP", "EXPORTSTEP",
"EXPORTSTL", "EXPORTSTL",
"EXTERNALREFERENCES", "EXTERNALREFERENCES",
"POINTCLOUDMANAGER",
"POINTCLOUDMANAGERCLOSE",
"EXTRIM", "EXTRIM",
"FILETAB", "FILETAB",
"FIND", "FIND",
+409
View File
@@ -0,0 +1,409 @@
//! POINTCLOUDCOLORMAP: the Point Cloud Color Map dialog for a chosen cloud
//! (or the drawing's default schemes), and what OK / Apply write — the
//! drawing's colour map (its schemes) and the cloud's intensity and
//! elevation colouring — as one undo step.
use super::*;
use codec::entities::{ExtendedEntityData, PointCloudExData};
use codec::objects::{ClassObject, ClassObjectData, Dictionary, ObjectType, PointCloudColorMap, PointCloudColorRamp, PointCloudRampColor};
use codec::types::Handle;
use crate::ui::window::pdf_dialogs::{short, MapRamp, MapSlot, OutOfRange, PdfDialogMsg, PointCloudColorMapState};
const MAP_DICTIONARY: &str = "ACAD_POINTCLOUD_COLORMAP_DICT";
const MAP_ENTRY: &str = "ACAD_POINTCLOUD_COLORMAP";
/// The classification scheme the colour map names by default.
const CLASSIFICATION_SCHEME: &str = "8AF59545-D22F-46B7-923C-912F71ED57DE";
fn cloud_data(document: &codec::CadDocument, handle: Handle) -> Option<&PointCloudExData> {
match document.get_entity(handle)? {
codec::EntityType::Extended(extended) => match &extended.data {
ExtendedEntityData::PointCloudEx(data) => Some(data),
_ => None,
},
_ => None,
}
}
/// The drawing's colour map object, created (with the built-in schemes)
/// under its dictionary when the drawing has none.
fn ensure_color_map(document: &mut codec::CadDocument) -> Option<Handle> {
let existing = document.objects.iter().find_map(|(handle, object)| match object {
ObjectType::ClassObject(object) if matches!(object.data, ClassObjectData::PointCloudColorMap(_)) => Some(*handle),
_ => None,
});
if existing.is_some() {
return existing;
}
let root = document.header.named_objects_dict_handle;
let ObjectType::Dictionary(root_dictionary) = document.objects.get(&root)? else {
return None;
};
let dictionary = match root_dictionary.get(MAP_DICTIONARY) {
Some(handle) if matches!(document.objects.get(&handle), Some(ObjectType::Dictionary(_))) => handle,
_ => {
let handle = document.allocate_handle();
let mut dictionary = Dictionary::new();
dictionary.handle = handle;
dictionary.owner = root;
document.objects.insert(handle, ObjectType::Dictionary(dictionary));
if let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get_mut(&root) {
root_dictionary.add_entry(MAP_DICTIONARY, handle);
}
handle
}
};
let handle = document.allocate_handle();
let ramps = crate::scene::model::point_cloud::schemes(document)
.into_iter()
.map(|(id, name, colors)| to_codec_ramp(&MapRamp { id, name, colors }))
.collect();
let mut map = ClassObject::new(ClassObjectData::PointCloudColorMap(PointCloudColorMap {
class_version: 4,
default_intensity_scheme: crate::entities::extended::point_cloud_ramp_id("Spectrum").to_string(),
default_elevation_scheme: crate::entities::extended::point_cloud_ramp_id("Earth").to_string(),
default_classification_scheme: CLASSIFICATION_SCHEME.to_string(),
color_ramps: ramps,
classification_color_ramps: Vec::new(),
}));
map.handle = handle;
map.owner = dictionary;
document.objects.insert(handle, ObjectType::ClassObject(map));
if let Some(ObjectType::Dictionary(dictionary)) = document.objects.get_mut(&dictionary) {
dictionary.add_entry(MAP_ENTRY, handle);
}
Some(handle)
}
/// A scheme as the colour map stores it: true colours, all shown.
fn to_codec_ramp(ramp: &MapRamp) -> PointCloudColorRamp {
PointCloudColorRamp {
id: ramp.id.clone(),
class_version: 1,
colors: ramp
.colors
.iter()
.map(|[r, g, b]| PointCloudRampColor {
color: (0xC2u32 << 24 | u32::from(*r) << 16 | u32::from(*g) << 8 | u32::from(*b)) as i32,
visible: true,
})
.collect(),
name: ramp.name.clone(),
}
}
/// A new scheme identifier: a random GUID in the stored form.
fn new_scheme_id() -> String {
let mut bytes = [0u8; 16];
getrandom::fill(&mut bytes).expect("OS random source");
let hex: String = bytes.iter().map(|b| format!("{b:02X}")).collect();
format!("{}-{}-{}-{}-{}", &hex[0..8], &hex[8..12], &hex[12..16], &hex[16..20], &hex[20..32])
}
/// `count` colours taken evenly along a scheme's blend.
fn resample(colors: &[[u8; 3]], count: usize) -> Vec<[u8; 3]> {
let count = count.clamp(2, 25);
(0..count)
.map(|k| crate::ui::window::pdf_dialogs::blend(colors, k as f64 / (count - 1) as f64))
.collect()
}
impl OpenCADStudio {
/// POINTCLOUDCOLORMAP after the prompt: the dialog for `cloud`, or for
/// the drawing's default schemes.
pub(in crate::app) fn open_point_cloud_color_map(&mut self, i: usize, cloud: Option<Handle>) {
let document = &self.tabs[i].scene.document;
let ramps: Vec<MapRamp> = crate::scene::model::point_cloud::schemes(document)
.into_iter()
.map(|(id, name, colors)| MapRamp { id, name, colors })
.collect();
let defaults = crate::scene::model::point_cloud::default_schemes(document);
let data = cloud.and_then(|handle| cloud_data(document, handle));
let height = data
.map(|data| {
let corners = codec::entities::point_cloud_ex_corners(data);
let low = corners.iter().map(|c| c.z).fold(f64::MAX, f64::min);
let high = corners.iter().map(|c| c.z).fold(f64::MIN, f64::max);
(low, high)
})
.unwrap_or((0.0, 0.0));
let slot = |scheme: &str, fallback: &str, gradient: bool, max: String, min: String, out: i16| MapSlot {
scheme: if scheme.is_empty() { fallback.to_string() } else { scheme.to_string() },
gradient,
max,
min,
out_of_range: OutOfRange(out),
};
let (intensity, elevation, extents, elevation_tab) = match data {
Some(data) => (
slot(
&data.intensity_color_scheme,
&defaults.0,
data.intensity_as_gradient,
data.intensity_max.to_string(),
data.intensity_min.to_string(),
data.intensity_out_of_range_behavior,
),
slot(
&data.current_color_scheme,
&defaults.1,
data.elevation_as_gradient,
short(if data.elevation_apply_to_fixed_range { data.elevation_max } else { height.1 }),
short(if data.elevation_apply_to_fixed_range { data.elevation_min } else { height.0 }),
data.elevation_out_of_range_behavior,
),
!data.elevation_apply_to_fixed_range,
data.stylization_type == 4,
),
None => (
slot("", &defaults.0, true, String::new(), String::new(), 1),
slot("", &defaults.1, false, String::new(), String::new(), 1),
true,
false,
),
};
let mut state = PointCloudColorMapState {
cloud: cloud.filter(|_| data.is_some()),
elevation_tab,
ramps,
intensity,
elevation,
extents,
height: (clean(height.0), clean(height.1)),
make_current: true,
interval: String::new(),
naming: None,
};
refresh_interval(&mut state);
self.point_cloud_color_map = Some(state);
self.active_modal = Some(crate::app::ModalKind::PointCloudColorMap);
}
pub(in crate::app) fn update_point_cloud_color_map(&mut self, message: PdfDialogMsg) -> Task<Message> {
let i = self.active_tab;
let Some(state) = self.point_cloud_color_map.as_mut() else {
return Task::none();
};
match message {
PdfDialogMsg::MapTab(elevation) => {
state.elevation_tab = elevation;
state.naming = None;
refresh_interval(state);
}
PdfDialogMsg::MapScheme(name) => {
if let Some(ramp) = state.ramps.iter().find(|r| r.name == name) {
state.slot_mut().scheme = ramp.id.clone();
refresh_interval(state);
}
}
PdfDialogMsg::MapCount(count) => {
if let Some(ramp) = state.ramp_mut() {
ramp.colors = resample(&ramp.colors, count);
}
refresh_interval(state);
}
// The colours between the two ends, blended evenly.
PdfDialogMsg::MapEven => {
if let Some(ramp) = state.ramp_mut() {
let (first, last) = (ramp.colors[0], ramp.colors[ramp.colors.len() - 1]);
ramp.colors = resample(&[first, last], ramp.colors.len());
}
}
PdfDialogMsg::MapReverse => {
if let Some(ramp) = state.ramp_mut() {
ramp.colors.reverse();
}
}
PdfDialogMsg::MapGradient(on) => state.slot_mut().gradient = on,
PdfDialogMsg::MapNew => {
// "Color Scheme 0", or the first number not taken.
let name = (0..)
.map(|n| format!("Color Scheme {n}"))
.find(|name| !state.ramps.iter().any(|r| r.name.eq_ignore_ascii_case(name)))
.unwrap_or_default();
state.naming = Some((true, name));
}
PdfDialogMsg::MapRename => {
let name = state.ramp().map(|r| r.name.clone()).unwrap_or_default();
state.naming = Some((false, name));
}
PdfDialogMsg::MapNameInput(value) => {
if let Some((_, name)) = state.naming.as_mut() {
*name = value;
}
}
PdfDialogMsg::MapNameCancel => state.naming = None,
PdfDialogMsg::MapNameOk => {
let Some((new, name)) = state.naming.take() else {
return Task::none();
};
let name = name.trim().to_string();
let taken = state.ramps.iter().any(|r| r.name.eq_ignore_ascii_case(&name))
&& !(!new && state.ramp().is_some_and(|r| r.name == name));
if name.is_empty() || taken {
self.command_line.push_error(crate::t!("A color scheme with this name already exists.").as_ref());
self.point_cloud_color_map.as_mut().map(|s| s.naming = Some((new, name)));
return Task::none();
}
if new {
let colors = state.ramp().map(|r| r.colors.clone()).unwrap_or_else(|| vec![[255; 3], [0; 3]]);
let id = new_scheme_id();
state.ramps.push(MapRamp { id: id.clone(), name, colors });
state.slot_mut().scheme = id;
} else if let Some(ramp) = state.ramp_mut() {
ramp.name = name;
}
}
// The last scheme stays; a slot naming a deleted one takes the first.
PdfDialogMsg::MapDelete => {
if state.ramps.len() > 1 {
let id = state.ramp().map(|r| r.id.clone()).unwrap_or_default();
state.ramps.retain(|r| r.id != id);
let first = state.ramps[0].id.clone();
for slot in [&mut state.intensity, &mut state.elevation] {
if slot.scheme.eq_ignore_ascii_case(&id) {
slot.scheme = first.clone();
}
}
refresh_interval(state);
}
}
PdfDialogMsg::MapMax(value) => {
state.slot_mut().max = value;
refresh_interval(state);
}
PdfDialogMsg::MapMin(value) => {
state.slot_mut().min = value;
refresh_interval(state);
}
// A band height sets the maximum from the minimum.
PdfDialogMsg::MapInterval(value) => {
let count = state.ramp().map_or(1, |r| r.colors.len()) as f64;
if let (Ok(step), Ok(min)) = (value.trim().parse::<f64>(), state.elevation.min.trim().parse::<f64>()) {
state.elevation.max = short(min + step * count);
}
state.interval = value;
}
PdfDialogMsg::MapExtents(on) => {
state.extents = on;
if on {
state.elevation.max = short(state.height.1);
state.elevation.min = short(state.height.0);
}
refresh_interval(state);
}
PdfDialogMsg::MapOutOfRange(choice) => state.slot_mut().out_of_range = choice,
PdfDialogMsg::MapCurrent(on) => state.make_current = on,
PdfDialogMsg::MapApply => {
self.apply_point_cloud_color_map(i);
}
PdfDialogMsg::MapOk => {
if self.apply_point_cloud_color_map(i) {
self.point_cloud_color_map = None;
self.active_modal = None;
}
}
_ => {}
}
Task::none()
}
/// Writes the dialog's schemes and the cloud's colouring as one undo
/// step; `false` (with a message) when a range value is not a number.
fn apply_point_cloud_color_map(&mut self, i: usize) -> bool {
let Some(state) = self.point_cloud_color_map.as_ref() else {
return false;
};
let intensity = (state.intensity.min.trim().parse::<i32>(), state.intensity.max.trim().parse::<i32>());
let elevation = (state.elevation.min.trim().parse::<f64>(), state.elevation.max.trim().parse::<f64>());
let cloud = state.cloud;
if cloud.is_some() {
let ok_intensity = matches!(intensity, (Ok(lo), Ok(hi)) if (0..=100).contains(&lo) && (0..=100).contains(&hi) && lo < hi);
let ok_elevation = state.extents || matches!(elevation, (Ok(lo), Ok(hi)) if lo < hi);
if !ok_intensity || !ok_elevation {
self.command_line.push_error(crate::t!("Invalid range of colorized points.").as_ref());
return false;
}
}
let ramps: Vec<PointCloudColorRamp> = state.ramps.iter().map(to_codec_ramp).collect();
let (intensity_slot, elevation_slot) = (state.intensity.clone(), state.elevation.clone());
let (extents, make_current, elevation_tab) = (state.extents, state.make_current, state.elevation_tab);
self.push_undo_snapshot(i, "POINTCLOUDCOLORMAP");
let document = &mut self.tabs[i].scene.document;
if let Some(handle) = ensure_color_map(document) {
if let Some(ObjectType::ClassObject(object)) = document.objects.get_mut(&handle) {
if let ClassObjectData::PointCloudColorMap(map) = &mut object.data {
map.color_ramps = ramps;
if cloud.is_none() {
map.default_intensity_scheme = intensity_slot.scheme.clone();
map.default_elevation_scheme = elevation_slot.scheme.clone();
}
}
}
}
if let Some(handle) = cloud {
if let Some(codec::EntityType::Extended(extended)) = document.get_entity_mut(handle) {
if let ExtendedEntityData::PointCloudEx(data) = &mut extended.data {
data.intensity_color_scheme = intensity_slot.scheme;
data.intensity_as_gradient = intensity_slot.gradient;
data.intensity_out_of_range_behavior = intensity_slot.out_of_range.0;
if let (Ok(lo), Ok(hi)) = intensity {
(data.intensity_min, data.intensity_max) = (lo, hi);
}
data.current_color_scheme = elevation_slot.scheme;
data.elevation_as_gradient = elevation_slot.gradient;
data.elevation_out_of_range_behavior = elevation_slot.out_of_range.0;
data.elevation_apply_to_fixed_range = !extents;
if let (false, Ok(lo), Ok(hi)) = (extents, elevation.0, elevation.1) {
(data.elevation_min, data.elevation_max) = (lo, hi);
}
if make_current {
data.stylization_type = if elevation_tab { 4 } else { 5 };
}
}
}
}
// Every cloud colours from the schemes: redraw them all.
let changes: Vec<_> = self.tabs[i]
.scene
.document
.entities()
.filter(|entity| crate::scene::is_point_cloud(entity))
.map(|entity| (entity.common().handle, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.tabs[i].dirty = true;
self.refresh_properties();
true
}
pub(super) fn dispatch_pc_colormap(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
use crate::command::CadCommand;
if cmd == "POINTCLOUDCOLORMAP" {
let command = crate::modules::insert::pc_stylize::PointCloudColorMapCommand::new();
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
return Some(self.finish_dispatch(cmd));
}
let rest = cmd.strip_prefix("_PCCOLORMAP")?;
let cloud = u64::from_str_radix(rest.trim(), 16).ok().map(Handle::new);
let task = self.finish_dispatch(cmd);
self.open_point_cloud_color_map(i, cloud);
Some(task)
}
}
/// A height with rounding noise dropped (the extents box's -0.0016 reads 0).
fn clean(value: f64) -> f64 {
(value * 100.0).round() / 100.0
}
fn refresh_interval(state: &mut PointCloudColorMapState) {
let count = state.ramp().map_or(1, |r| r.colors.len()).max(1) as f64;
let lo = state.elevation.min.trim().parse::<f64>();
let hi = state.elevation.max.trim().parse::<f64>();
state.interval = match (lo, hi) {
(Ok(lo), Ok(hi)) => short((hi - lo) / count),
_ => String::new(),
};
}
+199
View File
@@ -0,0 +1,199 @@
//! PCEXTRACTEDGE, PCEXTRACTCORNER, PCEXTRACTCENTERLINE and
//! (-)PCEXTRACTSECTION: the commands get the drawing's shown cloud points
//! and its section objects when they start.
use super::*;
use crate::ui::window::pdf_dialogs::PdfDialogMsg;
use codec::entities::ExtendedEntityData;
use glam::DVec3;
use crate::modules::insert::pc_extract::{
CenterlineCommand, Clouds, PlanesCommand, Section, SectionCommand,
};
fn dvec(v: codec::types::Vector3) -> DVec3 {
DVec3::new(v.x, v.y, v.z)
}
impl OpenCADStudio {
/// The points each point cloud shows (the renderer's placed set).
fn extraction_clouds(&self, i: usize) -> Clouds {
let document = &self.tabs[i].scene.document;
let mut clouds = Vec::new();
for entity in document.entities() {
let codec::EntityType::Extended(extended) = entity else {
continue;
};
let ExtendedEntityData::PointCloudEx(data) = &extended.data else {
continue;
};
let common = entity.common();
// The colour the renderer places with, so its cached points are reused.
let color = if matches!(common.color, codec::types::Color::ByLayer) {
document.layers.get(&common.layer).map(|layer| layer.color.clone()).unwrap_or(common.color.clone())
} else {
common.color.clone()
};
let color = color.rgb().map_or([255; 3], |(r, g, b)| [r, g, b]);
if let Some(placed) = crate::scene::model::point_cloud::placed(
document,
data,
color,
None,
&crate::scene::model::point_cloud::hidden(data),
) {
clouds.push((common.handle, placed));
}
}
Clouds(clouds)
}
fn extraction_sections(&self, i: usize) -> Vec<Section> {
self.tabs[i]
.scene
.document
.entities()
.filter_map(|entity| {
let codec::EntityType::Extended(extended) = entity else {
return None;
};
let ExtendedEntityData::SectionObject(data) = &extended.data else {
return None;
};
let (first, last) = (dvec(*data.vertices.first()?), dvec(*data.vertices.last()?));
let tangent = (last - first).normalize_or(DVec3::X);
let vertical = dvec(data.vertical_direction).normalize_or(DVec3::Z);
let viewing = vertical.cross(tangent).normalize_or(-DVec3::Z)
* if data.flags & 4 != 0 { 1.0 } else { -1.0 };
Some(Section { origin: first, viewing, tangent, live: data.flags & 1 != 0 })
})
.collect()
}
pub(super) fn dispatch_pc_extract(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
use crate::command::CadCommand;
let command: Box<dyn CadCommand> = match cmd {
"PCEXTRACTEDGE" => Box::new(PlanesCommand::new(false, self.extraction_clouds(i))),
"PCEXTRACTCORNER" => Box::new(PlanesCommand::new(true, self.extraction_clouds(i))),
"PCEXTRACTCENTERLINE" => Box::new(CenterlineCommand::new(self.extraction_clouds(i))),
"PCEXTRACTSECTION" | "-PCEXTRACTSECTION" => Box::new(SectionCommand::new(
cmd == "PCEXTRACTSECTION",
self.extraction_clouds(i),
self.extraction_sections(i),
)),
"_PCSECTIONDLG" => {
self.open_pc_section_dialog(i);
return Some(self.finish_dispatch(cmd));
}
_ => return None,
};
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(command);
Some(self.finish_dispatch(cmd))
}
}
impl OpenCADStudio {
/// PCEXTRACTSECTION's settings dialog, filled from the session settings.
fn open_pc_section_dialog(&mut self, i: usize) {
use crate::modules::insert::pc_extract::{has_job, settings};
use crate::ui::window::pdf_dialogs::{LineColor, PcSectionState};
if !has_job() {
return;
}
let s = settings();
let current = crate::t!("Use Current").into_owned();
let mut layers = vec![current.clone()];
let mut names: Vec<String> = self.tabs[i].scene.document.layers.iter().map(|l| l.name.clone()).collect();
names.sort_by_key(|n| n.to_lowercase());
layers.extend(names);
let short = |v: f64| {
let t = format!("{v:.4}");
t.trim_end_matches('0').trim_end_matches('.').to_string()
};
self.pc_section = Some(PcSectionState {
perimeter: s.perimeter,
max_points: s.max_points.to_string(),
layers,
layer: s.layer.clone().unwrap_or(current),
color: LineColor(s.color),
polylines: s.polylines,
width: short(s.width),
min_length: short(s.min_length),
connect: short(s.connect),
angle: short(s.angle),
preview: s.preview,
});
self.active_modal = Some(crate::app::ModalKind::PcSection);
}
pub(in crate::app) fn update_pc_section(&mut self, message: PdfDialogMsg) -> Task<Message> {
use crate::modules::insert::pc_extract::{set_settings, settings, SectionCommand, SectionDistanceCommand};
use crate::command::CadCommand;
let i = self.active_tab;
let Some(state) = self.pc_section.as_mut() else {
return Task::none();
};
match message {
PdfDialogMsg::SecPerimeter(on) => state.perimeter = on,
PdfDialogMsg::SecMaxPoints(v) => state.max_points = v,
PdfDialogMsg::SecLayer(v) => state.layer = v,
PdfDialogMsg::SecColor(c) => state.color = c,
PdfDialogMsg::SecPolylines(on) => state.polylines = on,
PdfDialogMsg::SecWidth(v) => state.width = v,
PdfDialogMsg::SecMinLength(v) => state.min_length = v,
PdfDialogMsg::SecConnect(v) => state.connect = v,
PdfDialogMsg::SecAngle(v) => state.angle = v,
PdfDialogMsg::SecPreview(on) => state.preview = on,
// Measure on screen, then back to the dialog (the extraction waits).
PdfDialogMsg::SecPick(connect) => {
self.pc_section = None;
self.active_modal = None;
let command = SectionDistanceCommand::new(connect);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
PdfDialogMsg::SecCreate => {
let number = |t: &str| crate::entities::common::parse_f64(t.trim());
let (min_length, connect, width) = (number(&state.min_length), number(&state.connect), number(&state.width));
let (Some(min_length), Some(connect)) = (min_length.filter(|v| *v > 0.0), connect.filter(|v| *v > 0.0)) else {
self.command_line.push_error(crate::t!("Requires a positive number.").as_ref());
return Task::none();
};
let Some(angle) = state.angle.trim().parse::<i64>().ok().filter(|v| (0..=10).contains(v)) else {
self.command_line
.push_error("Colinear angle tolerance only accepts integer from 0 to 10.");
return Task::none();
};
let Some(max_points) = state.max_points.trim().parse::<usize>().ok().filter(|v| *v > 0) else {
self.command_line.push_error(crate::t!("Requires a positive integer.").as_ref());
return Task::none();
};
let current = crate::t!("Use Current").into_owned();
let mut s = settings();
s.perimeter = state.perimeter;
s.max_points = max_points;
s.layer = (state.layer != current).then(|| state.layer.clone());
s.color = state.color.0;
s.polylines = state.polylines;
s.width = width.filter(|v| *v >= 0.0).unwrap_or(0.0);
s.min_length = min_length;
s.connect = connect;
s.angle = angle as f64;
s.preview = state.preview;
set_settings(s);
self.pc_section = None;
self.active_modal = None;
let Some(mut command) = SectionCommand::resume() else {
return Task::none();
};
let result = command.run();
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
return self.apply_cmd_result(result);
}
_ => {}
}
Task::none()
}
}
+222 -1
View File
@@ -6,7 +6,7 @@ use crate::io::xref_model::Pathtype;
use crate::modules::insert::pdf_import::{self, PdfFileImport}; use crate::modules::insert::pdf_import::{self, PdfFileImport};
use crate::ui::window::pdf_dialogs::{ use crate::ui::window::pdf_dialogs::{
click_page, item_thumbs, page_thumbs, LayerTarget, PdfAttachState, PdfDialogMsg, PdfImportFileState, click_page, item_thumbs, page_thumbs, LayerTarget, PdfAttachState, PdfDialogMsg, PdfImportFileState,
RotationChoice, UnderlayLayersState, PointCloudAttachState, RotationChoice, UnderlayLayersState,
}; };
/// "7.8740 × 3.9369" (inches) for a page. /// "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)); 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. /// ULAYERS: every underlay of the drawing, the selected one first shown.
pub(in crate::app) fn open_underlay_layers_dialog(&mut self, i: usize) { pub(in crate::app) fn open_underlay_layers_dialog(&mut self, i: usize) {
let document = &self.tabs[i].scene.document; let document = &self.tabs[i].scene.document;
@@ -244,6 +377,11 @@ impl OpenCADStudio {
crate::t!("Attaches a DGN file as an underlay.") crate::t!("Attaches a DGN file as an underlay.")
} }
"attach" => crate::t!("Attaches a PDF file as an underlay."), "attach" => crate::t!("Attaches a PDF file as an underlay."),
"pointcloud" => crate::t!("Attaches a point cloud scan or project."),
"pcsection" => crate::t!("Traces the points on the kept side of a live section as lines."),
"pointcloudcolormap" => {
crate::t!("Sets the color schemes and ranges a point cloud is colored by intensity or elevation.")
}
"layers" => crate::t!("Turns the layers of a PDF underlay on or off."), "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."), _ => crate::t!("Imports the geometry, fills, raster images and text of a PDF file as drawing objects."),
}; };
@@ -343,6 +481,89 @@ impl OpenCADStudio {
} }
PdfDialogMsg::AttachOk => return self.pdf_attach_ok(i), 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),
message @ (PdfDialogMsg::MapTab(_)
| PdfDialogMsg::MapScheme(_)
| PdfDialogMsg::MapCount(_)
| PdfDialogMsg::MapEven
| PdfDialogMsg::MapReverse
| PdfDialogMsg::MapGradient(_)
| PdfDialogMsg::MapNew
| PdfDialogMsg::MapDelete
| PdfDialogMsg::MapRename
| PdfDialogMsg::MapNameInput(_)
| PdfDialogMsg::MapNameOk
| PdfDialogMsg::MapNameCancel
| PdfDialogMsg::MapMax(_)
| PdfDialogMsg::MapMin(_)
| PdfDialogMsg::MapInterval(_)
| PdfDialogMsg::MapExtents(_)
| PdfDialogMsg::MapOutOfRange(_)
| PdfDialogMsg::MapCurrent(_)
| PdfDialogMsg::MapApply
| PdfDialogMsg::MapOk) => return self.update_point_cloud_color_map(message),
message @ (PdfDialogMsg::SecPerimeter(_)
| PdfDialogMsg::SecMaxPoints(_)
| PdfDialogMsg::SecLayer(_)
| PdfDialogMsg::SecColor(_)
| PdfDialogMsg::SecPolylines(_)
| PdfDialogMsg::SecWidth(_)
| PdfDialogMsg::SecMinLength(_)
| PdfDialogMsg::SecConnect(_)
| PdfDialogMsg::SecAngle(_)
| PdfDialogMsg::SecPick(_)
| PdfDialogMsg::SecPreview(_)
| PdfDialogMsg::SecCreate) => return self.update_pc_section(message),
PdfDialogMsg::CloudPathType(choice) => {
let stored = self.point_cloud_attach.as_ref().map(|s| self.pdf_stored_path(&s.path, choice.0));
if let (Some(s), Some(stored)) = (self.point_cloud_attach.as_mut(), stored) {
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 ─────────────────────────────────────────── // ── Underlay Layers ───────────────────────────────────────────
PdfDialogMsg::LayersUnderlay(name) => { PdfDialogMsg::LayersUnderlay(name) => {
if let Some(s) = self.underlay_layers.as_mut() { if let Some(s) = self.underlay_layers.as_mut() {
+230
View File
@@ -62,6 +62,154 @@ impl OpenCADStudio {
}) })
}; };
self.ribbon.set_underlay_context(context, xref); 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();
}
/// A Point Cloud Manager click: a switch writes the cloud's hidden scans
/// and regions as one undo step; the rest is palette state.
pub(in crate::app) fn update_pc_manager(&mut self, message: crate::ui::window::pc_manager::PcManagerMsg) -> Task<Message> {
use crate::scene::model::point_cloud::UNASSIGNED_OFF;
use crate::ui::window::pc_manager::{self, PcManagerMsg, Row};
let i = self.active_tab;
match message {
PcManagerMsg::Toggle(handle, row) => {
let Some(cloud) =
pc_manager::clouds(&self.tabs[i].scene.document, &[]).into_iter().find(|c| c.handle == handle)
else {
return Task::none();
};
if self.tabs[i].scene.is_layer_locked(handle) {
return Task::none();
}
// A scan named by its file name (automation) is the scan with that name.
let row = match row {
Row::Scan(key) => Row::Scan(
cloud.scans.iter().find(|(name, _)| *name == key).map_or(key, |(_, id)| id.clone()),
),
row => row,
};
let hidden = pc_manager::toggled(&cloud.hidden, &cloud.scans, &row);
self.push_undo_snapshot(i, "POINTCLOUDMANAGER");
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 {
data.hidden_regions = if hidden.iter().any(|h| h == UNASSIGNED_OFF) { vec![0] } else { Vec::new() };
data.hidden_scans = hidden.into_iter().filter(|h| h != UNASSIGNED_OFF).collect();
}
}
self.tabs[i].scene.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
self.tabs[i].dirty = true;
}
PcManagerMsg::Expand(key) => {
if !self.pc_manager.collapsed.remove(&key) {
self.pc_manager.collapsed.insert(key);
}
}
PcManagerMsg::Select(key) => self.pc_manager.selected = Some(key),
PcManagerMsg::Search(text) => self.pc_manager.search = text,
PcManagerMsg::CollapseAll => {
let clouds = pc_manager::clouds(&self.tabs[i].scene.document, &[]);
self.pc_manager.collapsed.extend(pc_manager::folding_keys(&clouds));
}
PcManagerMsg::ExpandAll => self.pc_manager.collapsed.clear(),
}
Task::none()
} }
/// One undo step that edits every selected PDF underlay. /// One undo step that edits every selected PDF underlay.
@@ -156,6 +304,88 @@ impl OpenCADStudio {
self.apply_xclip(i, inserts, crate::modules::insert::xclip::XclipAction::Delete); 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;
}
}),
// The Point Cloud Manager docks on the right like External
// References and opens expanded.
"POINTCLOUDMANAGER" => {
let id = crate::ui::dock::PanelId::PointCloudManager;
self.pc_manager.show = true;
if self.dock.location(id).is_none() {
self.dock.dock(id, crate::app::config::DockSide::Right, usize::MAX);
}
self.dock_expanded = Some(id);
}
"POINTCLOUDMANAGERCLOSE" => {
self.pc_manager.show = false;
if self.dock_expanded == Some(crate::ui::dock::PanelId::PointCloudManager) {
self.dock_expanded = None;
}
}
"_PCUNCROP" =>self.edit_selected_point_clouds(i, "POINTCLOUDUNCROP", |data| data.croppings.clear()),
"_PDFULUNCLIP" => { "_PDFULUNCLIP" => {
self.edit_selected_underlays(i, "PDFCLIP", |u| { self.edit_selected_underlays(i, "PDFCLIP", |u| {
u.clip_boundary_vertices.clear(); u.clip_boundary_vertices.clear();
+201
View File
@@ -0,0 +1,201 @@
//! POINTCLOUDATTACH: the point cloud, its definition (one per file, in
//! ACAD_POINTCLOUD_EX_DICT) and the reactor linking them, added as one undo
//! step.
use super::*;
use codec::objects::{
ClassObject, ClassObjectData, Dictionary, ObjectType, PointCloudDefinition,
PointCloudDefinitionReactor,
};
use codec::types::{Handle, Vector3};
use crate::modules::insert::pc_attach::PointCloudPlacement;
const DEFINITIONS: &str = "ACAD_POINTCLOUD_EX_DICT";
fn vector(v: glam::DVec3) -> Vector3 {
Vector3::new(v.x, v.y, v.z)
}
/// The point cloud definitions dictionary under the named-objects root,
/// created when the drawing has none.
fn definitions_dictionary(document: &mut codec::CadDocument) -> Option<Handle> {
let root = document.header.named_objects_dict_handle;
let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get(&root) else {
return None;
};
if let Some(existing) = root_dictionary.get(DEFINITIONS) {
if matches!(document.objects.get(&existing), Some(ObjectType::Dictionary(_))) {
return Some(existing);
}
}
let handle = document.allocate_handle();
let mut dictionary = Dictionary::new();
dictionary.handle = handle;
dictionary.owner = root;
document.objects.insert(handle, ObjectType::Dictionary(dictionary));
if let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get_mut(&root) {
root_dictionary.add_entry(DEFINITIONS, handle);
}
Some(handle)
}
/// The definition for a file: the existing one, or a new one registered
/// under the file's name ("Chair", "Chair(2)" …). Its point count and
/// extents come from the file.
fn ensure_definition(
document: &mut codec::CadDocument,
path: &str,
name: &str,
points: &crate::scene::model::point_cloud::CloudPoints,
) -> (Handle, Vector3, Vector3) {
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 {
ClassObjectData::PointCloudDefinitionEx(def) if same(&def.source_filename) => Some(*handle),
_ => None,
},
_ => None,
});
if let Some(handle) = existing {
return (handle, min, max);
}
let handle = document.allocate_handle();
let mut definition = ClassObject::new(ClassObjectData::PointCloudDefinitionEx(PointCloudDefinition {
class_version: 1,
source_filename: path.to_string(),
is_loaded: true,
point_count: points.positions.len() as i64,
extents_min: min,
extents_max: max,
}));
definition.handle = handle;
if let Some(dictionary) = definitions_dictionary(document) {
definition.owner = dictionary;
definition.reactors.push(dictionary);
if let Some(ObjectType::Dictionary(entries)) = document.objects.get_mut(&dictionary) {
let mut key = name.to_string();
let mut suffix = 1;
while entries.get(&key).is_some() {
suffix += 1;
key = format!("{name}({suffix})");
}
entries.add_entry(key, handle);
}
}
document.objects.insert(handle, ObjectType::ClassObject(definition));
(handle, min, max)
}
impl OpenCADStudio {
pub(crate) fn attach_point_cloud(&mut self, i: usize, label: String, placement: PointCloudPlacement) {
let Some(points) = crate::scene::model::point_cloud::load(&placement.path) else {
self.command_line.push_error("All scans are not found or invalid.");
self.command_line.push_error("Attach point cloud failed");
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()
.map(|stem| stem.to_string_lossy().into_owned())
.unwrap_or_default();
let pending = self.begin_undo(i, label, 1, false);
let document = &mut self.tabs[i].scene.document;
// POINTCLOUDLOCK 1 attaches clouds locked.
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.
let data = codec::entities::PointCloudExData {
class_version: 1,
extents_min,
extents_max,
ucs_origin: vector(placement.insertion),
ucs_x_direction: vector(x),
ucs_y_direction: vector(y),
ucs_z_direction: vector(z),
locked,
definition_handle: definition,
reactor_handle: Handle::NULL,
name,
show_intensity: false,
show_cropping: true,
hidden_scans: Vec::new(),
hidden_regions: Vec::new(),
stylization_type: 1,
intensity_color_scheme: String::new(),
current_color_scheme: String::new(),
classification_color_scheme: String::new(),
elevation_min: 0.0,
elevation_max: 0.0,
intensity_min: 0,
intensity_max: 100,
intensity_out_of_range_behavior: 1,
elevation_out_of_range_behavior: 1,
elevation_apply_to_fixed_range: false,
intensity_as_gradient: true,
elevation_as_gradient: false,
croppings: Vec::new(),
};
let entity = codec::EntityType::Extended(codec::entities::ExtendedEntity {
common: codec::entities::EntityCommon::new(),
data: codec::entities::ExtendedEntityData::PointCloudEx(data),
});
if let Some(cloud) = self.commit_entity_handle(entity) {
// The reactor, owned by the cloud, tells the definition who uses it.
let document = &mut self.tabs[i].scene.document;
let reactor = document.allocate_handle();
let mut object = ClassObject::new(ClassObjectData::PointCloudDefinitionReactorEx(
PointCloudDefinitionReactor { class_version: 1 },
));
object.handle = reactor;
object.owner = cloud;
document.objects.insert(reactor, ObjectType::ClassObject(object));
if let Some(ObjectType::ClassObject(definition)) = document.objects.get_mut(&definition) {
definition.reactors.push(reactor);
}
if let Some(codec::EntityType::Extended(extended)) = document.get_entity_mut(cloud) {
if let codec::entities::ExtendedEntityData::PointCloudEx(data) = &mut extended.data {
data.reactor_handle = reactor;
}
}
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 {
self.commit_undo_delta(i, pd);
}
}
}
+48
View File
@@ -1031,6 +1031,21 @@ impl OpenCADStudio {
| "PDFIMPORTIMAGEPATH" | "PDFIMPORTIMAGEPATH"
| "XDWGFADECTL" | "XDWGFADECTL"
| "POINTCLOUDCLIPFRAME" | "POINTCLOUDCLIPFRAME"
| "POINTCLOUDDENSITY"
| "POINTCLOUDPOINTSIZE"
| "POINTCLOUDLOCK"
| "POINTCLOUDAUTOUPDATE"
| "POINTCLOUDBOUNDARY"
| "POINTCLOUDRTDENSITY"
| "POINTCLOUDLOD"
| "POINTCLOUDPOINTMAX"
| "POINTCLOUDVISRETAIN"
| "POINTCLOUDSHADING"
| "POINTCLOUDCACHESIZE"
| "POINTCLOUD2DVSDISPLAY"
| "POINTCLOUDLIGHTING"
| "POINTCLOUDLIGHTSOURCE"
| "POINTCLOUDPOINTMAXLEGACY"
| "XCLIPFRAME" | "XCLIPFRAME"
| "WIPEOUTFRAME" | "WIPEOUTFRAME"
| "HALOGAP" | "HALOGAP"
@@ -1096,6 +1111,39 @@ impl OpenCADStudio {
crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY CONSTRAINTBARMODE CONSTRAINTNAMEFORMAT DYNCONSTRAINTDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(), crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY CONSTRAINTBARMODE CONSTRAINTNAMEFORMAT DYNCONSTRAINTDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(),
); );
} else { } else {
// Point cloud settings, kept with the drawing.
if let Some(setting) = crate::scene::model::point_cloud::setting(&name) {
let document = &self.tabs[i].scene.document;
let current = crate::scene::model::point_cloud::setting_value(document, setting);
match value.as_deref().map(|value| setting.check(value)) {
Some(Ok(new)) => {
if new != current {
crate::io::set_drawing_variable(
&mut self.tabs[i].scene.document,
setting.name,
&new.to_string(),
);
self.tabs[i].scene.bump_geometry();
self.tabs[i].dirty = true;
}
}
// Refused: say why, and ask again unless the value ends it.
Some(Err(refusal)) => {
for line in &refusal.lines {
self.command_line.push_error(line);
}
if refusal.ask_again {
self.command_line.push_output(&format!("Enter new value for {name} <{current}>:"));
self.pending_setvar = Some(name.clone());
}
}
None => {
self.command_line.push_output(&format!("Enter new value for {name} <{current}>:"));
self.pending_setvar = Some(name.clone());
}
}
return Some(self.finish_dispatch(cmd));
}
if name == "XDWGFADECTL" { if name == "XDWGFADECTL" {
let current = crate::scene::cache::block_cache::xref_fade_ctl(); let current = crate::scene::cache::block_cache::xref_fade_ctl();
if let Some(value) = &value { if let Some(value) = &value {
+40 -1
View File
@@ -57,6 +57,19 @@ impl OpenCADStudio {
self.start_underlay_attach(i, codec::entities::UnderlayType::Dgn, rest); self.start_underlay_attach(i, codec::entities::UnderlayType::Dgn, rest);
Some(Task::none()) Some(Task::none())
} }
// POINTCLOUDATTACH picks the file; with a file, or as
// -POINTCLOUDATTACH, it asks on the command line.
("POINTCLOUDATTACH", true) => Some(Task::done(Message::PointCloudAttachPick)),
("POINTCLOUDATTACH" | "-POINTCLOUDATTACH", _) => {
use crate::command::CadCommand;
let command = crate::modules::insert::pc_attach::PointCloudAttachCommand::new();
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
if !rest.is_empty() {
return Some(self.feed_command(crate::command::StepInput::Text(rest.to_string())));
}
Some(Task::none())
}
("XATTACH", true) => Some(Task::done(Message::XAttachPick)), ("XATTACH", true) => Some(Task::done(Message::XAttachPick)),
// A file named on the command line skips the picker. // A file named on the command line skips the picker.
// Handled at once, so the placement it starts belongs to this // Handled at once, so the placement it starts belongs to this
@@ -355,6 +368,21 @@ impl OpenCADStudio {
}, },
Message::AttachPickResult, Message::AttachPickResult,
), ),
Message::PointCloudAttachPick => Task::perform(
async {
let handle = crate::sys::file_dialog()
.set_title(crate::t!("Select Point Cloud File").as_ref())
.add_filter(crate::t!("Point Cloud Project").as_ref(), &["rcp"])
.add_filter(crate::t!("Point Cloud Scan").as_ref(), &["rcs"])
.pick_file()
.await;
match handle {
Some(h) => Ok(crate::sys::handle_path(&h)),
None => Err("Cancelled".to_string()),
}
},
Message::AttachPickResult,
),
Message::AttachPick => Task::perform( Message::AttachPick => Task::perform(
async { async {
let handle = crate::sys::file_dialog() let handle = crate::sys::file_dialog()
@@ -363,7 +391,7 @@ impl OpenCADStudio {
crate::t!("All Reference Files").as_ref(), crate::t!("All Reference Files").as_ref(),
&[ &[
"dwg", "dxf", "pdf", "dwf", "dwfx", "dgn", "png", "jpg", "jpeg", "dwg", "dxf", "pdf", "dwf", "dwfx", "dgn", "png", "jpg", "jpeg",
"bmp", "tif", "tiff", "bmp", "tif", "tiff", "rcp", "rcs",
], ],
) )
.add_filter(crate::t!("Drawing").as_ref(), &["dwg", "dxf"]) .add_filter(crate::t!("Drawing").as_ref(), &["dwg", "dxf"])
@@ -374,6 +402,7 @@ impl OpenCADStudio {
crate::t!("Images").as_ref(), crate::t!("Images").as_ref(),
&["png", "jpg", "jpeg", "bmp", "tif", "tiff"], &["png", "jpg", "jpeg", "bmp", "tif", "tiff"],
) )
.add_filter(crate::t!("Point Clouds").as_ref(), &["rcp", "rcs"])
.pick_file() .pick_file()
.await; .await;
match handle { match handle {
@@ -423,6 +452,16 @@ impl OpenCADStudio {
} }
Task::none() Task::none()
} }
// A point cloud goes on to the Attach Point Cloud dialog.
"rcp" | "rcs" => {
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();
}
self.open_point_cloud_attach_dialog(&path);
Task::none()
}
"png" | "jpg" | "jpeg" | "bmp" | "tif" | "tiff" => { "png" | "jpg" | "jpeg" | "bmp" | "tif" | "tiff" => {
let result = image::image_dimensions(&path) let result = image::image_dimensions(&path)
.map(|(w, h)| (path.clone(), w, h)) .map(|(w, h)| (path.clone(), w, h))
+83
View File
@@ -71,7 +71,11 @@ pub(super) const NAMES: &[&str] = &[
"close_modal", "close_modal",
"pdf_dialog_ok", "pdf_dialog_ok",
"pdf_layer_toggle", "pdf_layer_toggle",
"pc_manager_toggle",
"pc_manager",
"pdf_page_select", "pdf_page_select",
"pc_colormap",
"pc_section",
"ribbon_tab", "ribbon_tab",
"ribbon_dropdown", "ribbon_dropdown",
"dialog_ok", "dialog_ok",
@@ -688,6 +692,82 @@ impl OpenCADStudio {
"pdf_layer_toggle" => Message::PdfDialog( "pdf_layer_toggle" => Message::PdfDialog(
crate::ui::window::pdf_dialogs::PdfDialogMsg::LayersToggle(string(req, "value")?.into()), crate::ui::window::pdf_dialogs::PdfDialogMsg::LayersToggle(string(req, "value")?.into()),
), ),
// Point Cloud Color Map: one edit, "tab=elevation", "count=3",
// "even", "reverse", "scheme=Earth", "gradient=0", "max=90",
// "min=10", "interval=0.1", "extents=0", "range=2", "current=0".
"pc_colormap" => {
use crate::ui::window::pdf_dialogs::{OutOfRange, PdfDialogMsg as M};
let value = string(req, "value")?;
let (key, arg) = value.split_once('=').unwrap_or((value, ""));
let on = arg != "0";
Message::PdfDialog(match key {
"tab" => M::MapTab(arg == "elevation"),
"scheme" => M::MapScheme(arg.into()),
"count" => M::MapCount(arg.parse().map_err(|_| failure("invalid_value", "count"))?),
"even" => M::MapEven,
"reverse" => M::MapReverse,
"gradient" => M::MapGradient(on),
"max" => M::MapMax(arg.into()),
"min" => M::MapMin(arg.into()),
"interval" => M::MapInterval(arg.into()),
"extents" => M::MapExtents(on),
"range" => M::MapOutOfRange(OutOfRange(arg.parse().unwrap_or(1))),
"current" => M::MapCurrent(on),
"new" => M::MapNew,
"rename" => M::MapRename,
"name" => M::MapNameInput(arg.into()),
"name_ok" => M::MapNameOk,
"delete" => M::MapDelete,
"apply" => M::MapApply,
_ => return Err(failure("invalid_value", "Unknown color map edit")),
})
}
// Section extraction dialog: "min=0.01", "connect=0.02", "angle=5",
// "points=18000", "lines=1", "perimeter=1", "preview=0", "width=0".
"pc_section" => {
use crate::ui::window::pdf_dialogs::PdfDialogMsg as M;
let value = string(req, "value")?;
let (key, arg) = value.split_once('=').unwrap_or((value, ""));
Message::PdfDialog(match key {
"min" => M::SecMinLength(arg.into()),
"connect" => M::SecConnect(arg.into()),
"angle" => M::SecAngle(arg.into()),
"points" => M::SecMaxPoints(arg.into()),
"width" => M::SecWidth(arg.into()),
"lines" => M::SecPolylines(arg == "0"),
"perimeter" => M::SecPerimeter(arg != "0"),
"preview" => M::SecPreview(arg != "0"),
_ => return Err(failure("invalid_value", "Unknown section setting")),
})
}
// Point Cloud Manager: flip a row's switch, "<handle>:<row>" with
// row cloud, unassigned, scans or scan:<name>.
"pc_manager_toggle" => {
use crate::ui::window::pc_manager::{PcManagerMsg, Row};
let value = string(req, "value")?;
let parsed = value.split_once(':').and_then(|(handle, row)| {
Some((codec::Handle::new(u64::from_str_radix(handle, 16).ok()?), Row::parse(row)?))
});
let Some((handle, row)) = parsed else {
return Err(failure("bad_value", "Expected <handle>:<row>"));
};
Message::PcManager(PcManagerMsg::Toggle(handle, row))
}
// Point Cloud Manager tree: "search=<text>", "collapse", "expand",
// "toggle_node=<key>" or "select=<key>".
"pc_manager" => {
use crate::ui::window::pc_manager::PcManagerMsg as M;
let value = string(req, "value")?;
let (key, arg) = value.split_once('=').unwrap_or((value, ""));
Message::PcManager(match key {
"search" => M::Search(arg.into()),
"collapse" => M::CollapseAll,
"expand" => M::ExpandAll,
"toggle_node" => M::Expand(arg.into()),
"select" => M::Select(arg.into()),
_ => return Err(failure("invalid_value", "Unknown point cloud manager edit")),
})
}
// Attach dialog: choose pages by index ("0,2"). // Attach dialog: choose pages by index ("0,2").
"pdf_page_select" => { "pdf_page_select" => {
let pages: Vec<usize> = string(req, "value")? let pages: Vec<usize> = string(req, "value")?
@@ -713,6 +793,9 @@ impl OpenCADStudio {
use crate::ui::window::pdf_dialogs::PdfDialogMsg; use crate::ui::window::pdf_dialogs::PdfDialogMsg;
Message::PdfDialog(match self.active_modal { Message::PdfDialog(match self.active_modal {
Some(crate::app::ModalKind::PdfAttach) => PdfDialogMsg::AttachOk, Some(crate::app::ModalKind::PdfAttach) => PdfDialogMsg::AttachOk,
Some(crate::app::ModalKind::PointCloudAttach) => PdfDialogMsg::CloudOk,
Some(crate::app::ModalKind::PointCloudColorMap) => PdfDialogMsg::MapOk,
Some(crate::app::ModalKind::PcSection) => PdfDialogMsg::SecCreate,
Some(crate::app::ModalKind::UnderlayLayers) => PdfDialogMsg::LayersOk, Some(crate::app::ModalKind::UnderlayLayers) => PdfDialogMsg::LayersOk,
Some(crate::app::ModalKind::PdfImportSettings) => PdfDialogMsg::SettingsOk, Some(crate::app::ModalKind::PdfImportSettings) => PdfDialogMsg::SettingsOk,
Some(crate::app::ModalKind::PdfImportFile) => PdfDialogMsg::ImportOk, Some(crate::app::ModalKind::PdfImportFile) => PdfDialogMsg::ImportOk,
+17
View File
@@ -497,6 +497,9 @@ pub(super) struct OpenCADStudio {
block_definition: Option<crate::ui::window::block_definition::BlockDefinitionState>, block_definition: Option<crate::ui::window::block_definition::BlockDefinitionState>,
/// PDF dialogs' working copies; `None` while closed. /// PDF dialogs' working copies; `None` while closed.
pdf_attach: Option<crate::ui::window::pdf_dialogs::PdfAttachState>, pdf_attach: Option<crate::ui::window::pdf_dialogs::PdfAttachState>,
point_cloud_attach: Option<crate::ui::window::pdf_dialogs::PointCloudAttachState>,
point_cloud_color_map: Option<crate::ui::window::pdf_dialogs::PointCloudColorMapState>,
pc_section: Option<crate::ui::window::pdf_dialogs::PcSectionState>,
underlay_layers: Option<crate::ui::window::pdf_dialogs::UnderlayLayersState>, underlay_layers: Option<crate::ui::window::pdf_dialogs::UnderlayLayersState>,
pdf_import_settings: Option<crate::modules::insert::pdf_import::PdfImportSettings>, pdf_import_settings: Option<crate::modules::insert::pdf_import::PdfImportSettings>,
pdf_import_file: Option<crate::ui::window::pdf_dialogs::PdfImportFileState>, pdf_import_file: Option<crate::ui::window::pdf_dialogs::PdfImportFileState>,
@@ -791,6 +794,8 @@ pub(super) struct OpenCADStudio {
/// Whether the Browser panel is shown. Off until BROWSER opens it, so /// Whether the Browser panel is shown. Off until BROWSER opens it, so
/// the default layout is unchanged for existing users. /// the default layout is unchanged for existing users.
pub(crate) show_browser: bool, pub(crate) show_browser: bool,
/// Point Cloud Manager palette (POINTCLOUDMANAGER).
pub(crate) pc_manager: crate::ui::window::pc_manager::PcManager,
/// Which viewport background the colour wheel is editing, or `None` when /// Which viewport background the colour wheel is editing, or `None` when
/// it is closed. One slot, because only one wheel can be open at a time. /// it is closed. One slot, because only one wheel can be open at a time.
pub(crate) bg_picker: Option<BgTarget>, pub(crate) bg_picker: Option<BgTarget>,
@@ -1903,6 +1908,9 @@ pub enum ModalKind {
DrawingUnits, DrawingUnits,
BlockDefinition, BlockDefinition,
PdfAttach, PdfAttach,
PointCloudAttach,
PointCloudColorMap,
PcSection,
UnderlayLayers, UnderlayLayers,
PdfImportSettings, PdfImportSettings,
PdfImportFile, PdfImportFile,
@@ -3856,6 +3864,8 @@ pub enum Message {
PdfImportPick, PdfImportPick,
/// An edit in one of the PDF dialogs. /// An edit in one of the PDF dialogs.
PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg), PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg),
/// A click or search in the Point Cloud Manager.
PcManager(crate::ui::window::pc_manager::PcManagerMsg),
PdfImportPickResult(Result<(std::path::PathBuf, std::sync::Arc<Vec<u8>>), String>), PdfImportPickResult(Result<(std::path::PathBuf, std::sync::Arc<Vec<u8>>), String>),
/// Result of the PDFATTACH file picker. /// Result of the PDFATTACH file picker.
PdfAttachPickResult(Result<(std::path::PathBuf, std::sync::Arc<Vec<u8>>), String>), PdfAttachPickResult(Result<(std::path::PathBuf, std::sync::Arc<Vec<u8>>), String>),
@@ -3869,6 +3879,9 @@ pub enum Message {
AttachPick, AttachPick,
/// DWFATTACH / DGNATTACH: pick the file (the result goes the ATTACH way). /// DWFATTACH / DGNATTACH: pick the file (the result goes the ATTACH way).
UnderlayAttachPick(codec::entities::UnderlayType), UnderlayAttachPick(codec::entities::UnderlayType),
/// POINTCLOUDATTACH: pick the scan or project (the result goes the
/// ATTACH way).
PointCloudAttachPick,
/// The Reference slide-out's xref fading: amount dragged, drag done, /// The Reference slide-out's xref fading: amount dragged, drag done,
/// switch. /// switch.
XrefFadeSlide(u8), XrefFadeSlide(u8),
@@ -4043,6 +4056,9 @@ impl OpenCADStudio {
drawing_units: None, drawing_units: None,
block_definition: None, block_definition: None,
pdf_attach: None, pdf_attach: None,
point_cloud_attach: None,
point_cloud_color_map: None,
pc_section: None,
underlay_layers: None, underlay_layers: None,
pdf_import_settings: None, pdf_import_settings: None,
pdf_import_file: None, pdf_import_file: None,
@@ -4166,6 +4182,7 @@ impl OpenCADStudio {
graph_undo_open: false, graph_undo_open: false,
show_external_references: false, show_external_references: false,
show_browser: false, show_browser: false,
pc_manager: Default::default(),
bg_picker: None, bg_picker: None,
block_palette: Default::default(), block_palette: Default::default(),
xref_manager: Default::default(), xref_manager: Default::default(),
+63 -2
View File
@@ -1011,6 +1011,18 @@ impl OpenCADStudio {
); );
} }
} }
// Point cloud: the saved path from its definition; the scan's
// metres against the drawing's units.
codec::EntityType::Extended(extended) => {
if let codec::entities::ExtendedEntityData::PointCloudEx(data) = &extended.data {
if let Some(definition) = crate::scene::model::point_cloud::definition(doc, data) {
set_row(&mut sections, "ext_pcx_path", definition.source_filename.clone());
}
set_row(&mut sections, "ext_pcx_unit", t!("Meters").into_owned());
let factor = insert_unit_scale(doc.header.insertion_units, 6).unwrap_or(1.0);
set_row(&mut sections, "ext_pcx_unit_factor", format_unit_factor(factor));
}
}
// Leader: text style / vertical text placement / overall // Leader: text style / vertical text placement / overall
// scale come from its dimension style. // scale come from its dimension style.
codec::EntityType::Leader(ld) => { codec::EntityType::Leader(ld) => {
@@ -2632,7 +2644,15 @@ filter={:.1} local={:.1} aggregate={:.1} entities={}",
.iter() .iter()
.all(|handle| self.tabs[i].scene.is_layer_locked(*handle)); .all(|handle| self.tabs[i].scene.is_layer_locked(*handle));
if locked_only { if locked_only {
make_sections_read_only(&mut panel.sections); // A point cloud locked on its own (not by its layer) still
// takes its Locked row, the way it is unlocked.
let cloud_lock_only = property_handles
.iter()
.all(|handle| self.tabs[i].scene.locked_layer_name(*handle).is_none());
make_sections_read_only(
&mut panel.sections,
if cloud_lock_only { &["ext_pcx_locked"] } else { &[] },
);
// Rows demoted to read-only no longer back an editable field; // Rows demoted to read-only no longer back an editable field;
// drop them from the id→key map so focus can't map onto them. // drop them from the id→key map so focus can't map onto them.
panel.field_key_by_id = crate::ui::properties::build_field_key_map(&panel.sections); panel.field_key_by_id = crate::ui::properties::build_field_key_map(&panel.sections);
@@ -2946,6 +2966,29 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
grips.push(grip); grips.push(grip);
} }
} }
// A lone unlocked point cloud in a 3D visual style carries the
// move gizmo at its insertion point.
let gizmo = single_handle
.filter(|_| {
!is_paper
&& self.tabs[i].scene.active_model_tile_render_mode()
!= codec::entities::ViewportRenderMode::Wireframe2D
})
.and_then(|handle| Some((handle, self.tabs[i].scene.document.get_entity(handle)?)))
.filter(|(_, entity)| !crate::scene::is_locked_point_cloud(entity))
.and_then(|(handle, entity)| match entity {
codec::EntityType::Extended(extended) => match &extended.data {
codec::entities::ExtendedEntityData::PointCloudEx(data) => {
Some((handle, crate::entities::extended::point_cloud_gizmo_grips(data)))
}
_ => None,
},
_ => None,
});
if let Some((handle, gizmo)) = gizmo {
handles.extend(std::iter::repeat(handle).take(gizmo.len()));
grips.extend(gizmo);
}
let (grips, handles) = apply_grip_budget(grips, handles); let (grips, handles) = apply_grip_budget(grips, handles);
(single_handle, grips, handles) (single_handle, grips, handles)
}; };
@@ -2981,6 +3024,20 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
handles handles
} }
/// The point clouds the Locked row applies to: locked ones too, unless
/// their layer is locked.
pub(super) fn point_cloud_lock_targets(&self, i: usize) -> Vec<Handle> {
let mut handles = self.tabs[i].properties.selected_handles();
if handles.is_empty() {
handles = self.tabs[i].properties.source_handles.clone();
}
if handles.is_empty() {
handles.extend(self.tabs[i].selected_handle);
}
handles.retain(|handle| self.tabs[i].scene.locked_layer_name(*handle).is_none());
handles
}
pub(super) fn has_property_selection(&self, i: usize) -> bool { pub(super) fn has_property_selection(&self, i: usize) -> bool {
!self.tabs[i].properties.selected_handles().is_empty() !self.tabs[i].properties.selected_handles().is_empty()
|| !self.tabs[i].properties.source_handles.is_empty() || !self.tabs[i].properties.source_handles.is_empty()
@@ -3438,12 +3495,16 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
} }
} }
fn make_sections_read_only(sections: &mut [crate::scene::model::object::PropSection]) { fn make_sections_read_only(
sections: &mut [crate::scene::model::object::PropSection],
keep: &[&str],
) {
use crate::scene::model::object::PropValue; use crate::scene::model::object::PropValue;
for property in sections for property in sections
.iter_mut() .iter_mut()
.flat_map(|section| section.props.iter_mut()) .flat_map(|section| section.props.iter_mut())
.filter(|property| !keep.contains(&property.field))
{ {
let text = match &property.value { let text = match &property.value {
PropValue::ReadOnly(value) PropValue::ReadOnly(value)
+20
View File
@@ -2369,6 +2369,18 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
return self.on_dynamic_dimension_choice(field, &value); return self.on_dynamic_dimension_choice(field, &value);
} }
let i = self.active_tab; let i = self.active_tab;
if field == "ext_pcx_locked" {
let handles = self.point_cloud_lock_targets(i);
self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
if let Some(codec::EntityType::Extended(extended)) =
app.tabs[i].scene.document.get_entity_mut(handle)
{
crate::entities::traits::PropertyEditable::apply_geom_prop(extended, field, &value);
}
});
self.tabs[i].properties.edit_choice_open = false;
return Task::none();
}
let handles = self.property_target_handles(i); let handles = self.property_target_handles(i);
// The Annotative Yes/No list drives the per-object annotative toggle // The Annotative Yes/No list drives the per-object annotative toggle
// (MTEXT's own flag, the annotation context for the rest). // (MTEXT's own flag, the annotation context for the rest).
@@ -3607,6 +3619,14 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
}; };
// Underlay rows: reject what the reference rejects, and // Underlay rows: reject what the reference rejects, and
// write a Width / Height as the scale that produces it. // write a Width / Height as the scale that produces it.
if field.starts_with("ext_pcx_") {
if let Err(message) =
crate::entities::extended::validate_point_cloud_property(field, &val)
{
self.command_line.push_error(message);
return Task::none();
}
}
if field.starts_with("ul_") { if field.starts_with("ul_") {
if let Err(message) = if let Err(message) =
crate::entities::underlay::validate_property(field, &val) crate::entities::underlay::validate_property(field, &val)
+18
View File
@@ -630,6 +630,9 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
PanelId::NodeGraph => { PanelId::NodeGraph => {
self.show_node_graph = false; self.show_node_graph = false;
} }
PanelId::PointCloudManager => {
self.pc_manager.show = false;
}
PanelId::Properties => { PanelId::Properties => {
self.show_properties = false; self.show_properties = false;
self.ribbon.set_properties(false); self.ribbon.set_properties(false);
@@ -734,6 +737,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
PanelId::ExternalReferences => self.show_external_references, PanelId::ExternalReferences => self.show_external_references,
PanelId::Browser => self.show_browser, PanelId::Browser => self.show_browser,
PanelId::NodeGraph => self.show_node_graph, PanelId::NodeGraph => self.show_node_graph,
PanelId::PointCloudManager => self.pc_manager.show,
} }
} }
@@ -1017,6 +1021,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
} }
} }
self.tabs[i].scene.reseed_underlays(); self.tabs[i].scene.reseed_underlays();
// Point clouds redraw as unloaded (box and saved path).
self.tabs[i].scene.bump_geometry();
} }
XrefPaletteOp::Reload => { XrefPaletteOp::Reload => {
let base_dir: std::path::PathBuf = host let base_dir: std::path::PathBuf = host
@@ -1058,6 +1064,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].xref_unloaded.remove(key);
} }
self.tabs[i].scene.reseed_underlays(); 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 { } else if *kind != crate::io::xref_model::RefKind::DwgXref {
self.command_line.push_error(crate::tf!( self.command_line.push_error(crate::tf!(
"{}: reload applies to drawing references only.", "{}: reload applies to drawing references only.",
+7
View File
@@ -1429,6 +1429,7 @@ impl OpenCADStudio {
} }
message @ (Message::AttachPick message @ (Message::AttachPick
| Message::UnderlayAttachPick(_) | Message::UnderlayAttachPick(_)
| Message::PointCloudAttachPick
| Message::AttachPickResult(_) | Message::AttachPickResult(_)
| Message::XrefAttach(_) | Message::XrefAttach(_)
| Message::XrefAttachBrowseResult(_)) => self.update_xref_attach(message), | Message::XrefAttachBrowseResult(_)) => self.update_xref_attach(message),
@@ -1805,6 +1806,7 @@ impl OpenCADStudio {
} }
Message::PdfDialog(message) => self.update_pdf_dialog(message), Message::PdfDialog(message) => self.update_pdf_dialog(message),
Message::PcManager(message) => self.update_pc_manager(message),
Message::RibbonSelectTab(idx) => { Message::RibbonSelectTab(idx) => {
self.ribbon.select(idx); self.ribbon.select(idx);
Task::none() Task::none()
@@ -8494,6 +8496,11 @@ impl OpenCADStudio {
} }
Message::CloseModal => { Message::CloseModal => {
// Cancelling the section settings drops the waiting extraction.
if self.active_modal == Some(super::ModalKind::PcSection) {
crate::modules::insert::pc_extract::drop_job();
self.pc_section = None;
}
if self.active_modal == Some(super::ModalKind::RecoveryPrompt) { if self.active_modal == Some(super::ModalKind::RecoveryPrompt) {
return self.update(Message::RecoveryDecline); return self.update(Message::RecoveryDecline);
} }
+101 -2
View File
@@ -167,6 +167,29 @@ fn cursor_on_projected_axis(
.then_some(origin + direction * distance as f64) .then_some(origin + direction * distance as f64)
} }
/// The point under `cursor` on the plane through `origin` with `normal`.
fn cursor_on_plane(
cursor: Point,
bounds: iced::Rectangle,
view: glam::Mat4,
eye: glam::DVec3,
origin: glam::DVec3,
normal: glam::DVec3,
) -> Option<glam::DVec3> {
let inverse = view.as_dmat4().inverse();
let x = cursor.x as f64 / bounds.width as f64 * 2.0 - 1.0;
let y = 1.0 - cursor.y as f64 / bounds.height as f64 * 2.0;
let near = inverse.project_point3(glam::DVec3::new(x, y, 0.0));
let far = inverse.project_point3(glam::DVec3::new(x, y, 1.0));
let direction = far - near;
let along = direction.dot(normal);
if along.abs() <= 1e-12 {
return None;
}
let t = ((origin - eye) - near).dot(normal) / along;
t.is_finite().then(|| eye + near + direction * t)
}
fn is_added_polyline_vertex( fn is_added_polyline_vertex(
original: &AcadEntityType, original: &AcadEntityType,
current: &AcadEntityType, current: &AcadEntityType,
@@ -635,6 +658,36 @@ impl OpenCADStudio {
.find(|(owner, grip)| *owner == handle && grip.id == grip_id) .find(|(owner, grip)| *owner == handle && grip.id == grip_id)
.and_then(|(_, grip)| grip.axis); .and_then(|(_, grip)| grip.axis);
// A move-gizmo part moves its cloud alone, along its axis or plane.
let gizmo_shape = self.tabs[i]
.selected_grip_handles
.iter()
.copied()
.zip(self.tabs[i].selected_grips.iter())
.find(|(owner, grip)| *owner == handle && grip.id == grip_id)
.map(|(_, grip)| grip.shape)
.filter(|shape| {
matches!(
shape,
crate::scene::model::object::GripShape::GizmoAxis(_)
| crate::scene::model::object::GripShape::GizmoPlane(_)
)
});
if let Some(shape) = gizmo_shape {
self.tabs[i].hot_grips.clear();
let mut edit = GripEdit::single(handle, grip_id, true, world);
edit.axis = axis;
edit.gizmo = true;
if let crate::scene::model::object::GripShape::GizmoPlane(k) = shape {
let (a, b) = crate::scene::pick::grip::gizmo_plane_axes(k);
edit.plane = Some(
crate::scene::pick::grip::gizmo_axis(a)
.cross(crate::scene::pick::grip::gizmo_axis(b)),
);
}
return edit;
}
let clicked_is_hot = self.tabs[i].hot_grips.contains(&(handle, grip_id)); let clicked_is_hot = self.tabs[i].hot_grips.contains(&(handle, grip_id));
let mut targets: Vec<GripTarget> = if clicked_is_hot { let mut targets: Vec<GripTarget> = if clicked_is_hot {
@@ -716,6 +769,9 @@ impl OpenCADStudio {
axis, axis,
targets, targets,
rectangle_frame: None, rectangle_frame: None,
plane: None,
grab: None,
gizmo: false,
} }
} }
@@ -927,6 +983,10 @@ impl OpenCADStudio {
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.command_line self.command_line
.push_output(crate::tf!("Visual style: {label}").as_ref()); .push_output(crate::tf!("Visual style: {label}").as_ref());
// The point cloud move gizmo follows the visual style.
if i == self.active_tab {
self.refresh_selected_grips();
}
Task::none() Task::none()
} }
@@ -1599,6 +1659,28 @@ impl OpenCADStudio {
} }
} }
if let Some(normal) = grip.plane {
if snap_hit.is_none_or(|hit| hit.snap_type == crate::snap::SnapType::Grid) {
snapped = cursor_on_plane(p, bounds, view_rot, eye, grip.origin_world, normal)
.unwrap_or(snapped);
}
}
if grip.gizmo {
// The cloud follows the cursor by the offset it was grabbed
// at (measured on the press; the first move as a fallback).
let grab = match grip.grab {
Some(grab) => grab,
None => {
let grab = snapped - grip.origin_world;
if let Some(active) = self.tabs[i].active_grip.as_mut() {
active.grab = Some(grab);
}
grab
}
};
snapped -= grab;
}
let snap_ms = snap_started.elapsed().as_secs_f64() * 1000.0; let snap_ms = snap_started.elapsed().as_secs_f64() * 1000.0;
// The overlay builds the active tracking guide from `otrack_active.base` // The overlay builds the active tracking guide from `otrack_active.base`
// to `last_cursor_world`. Keep it synchronized with the actual point used // to `last_cursor_world`. Keep it synchronized with the actual point used
@@ -3557,8 +3639,25 @@ impl OpenCADStudio {
self.grip_popup = None; self.grip_popup = None;
return Task::none(); return Task::none();
} }
self.tabs[i].active_grip = let mut edit = self.grip_edit_for_hit(i, handle, grip_id, is_translate, world);
Some(self.grip_edit_for_hit(i, handle, grip_id, is_translate, world)); if edit.gizmo {
// Where the gizmo part was grabbed: the drag keeps
// that offset from the cloud's insertion point.
let (view_rot, eye) = match &edit_cam {
Some(cam) => (cam.view_proj_rte(bounds), cam.eye()),
None => {
let cam = self.tabs[i].scene.camera.borrow();
(cam.view_proj_rte(bounds), cam.eye())
}
};
let grabbed = match (edit.axis, edit.plane) {
(Some(axis), _) => cursor_on_projected_axis(p, bounds, view_rot, eye, world, axis),
(_, Some(normal)) => cursor_on_plane(p, bounds, view_rot, eye, world, normal),
_ => None,
};
edit.grab = grabbed.map(|point| point - world);
}
self.tabs[i].active_grip = Some(edit);
self.grip_hover = None; self.grip_hover = None;
self.grip_popup = None; self.grip_popup = None;
self.tabs[i] self.tabs[i]
+13
View File
@@ -1604,6 +1604,11 @@ bg={bg_ms:.1}ms n={view_count}"
None None
} else if self.ribbon.xref_context() { } else if self.ribbon.xref_context() {
crate::ui::side_toolbar::view(&crate::ui::ribbon::xref_tools()) 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() { } else if let Some(ctx) = self.ribbon.underlay_context() {
let ctx = ctx.clone(); let ctx = ctx.clone();
crate::ui::side_toolbar::view_with_active( crate::ui::side_toolbar::view_with_active(
@@ -1865,6 +1870,7 @@ bg={bg_ms:.1}ms n={view_count}"
crate::ui::dock::PanelId::ExternalReferences => self.show_external_references, crate::ui::dock::PanelId::ExternalReferences => self.show_external_references,
crate::ui::dock::PanelId::Browser => self.show_browser, crate::ui::dock::PanelId::Browser => self.show_browser,
crate::ui::dock::PanelId::NodeGraph => self.show_node_graph, crate::ui::dock::PanelId::NodeGraph => self.show_node_graph,
crate::ui::dock::PanelId::PointCloudManager => self.pc_manager.show,
} }
}; };
let edge_stack = |side: crate::app::config::DockSide| -> Option<Element<'_, Message>> { let edge_stack = |side: crate::app::config::DockSide| -> Option<Element<'_, Message>> {
@@ -2989,6 +2995,13 @@ impl OpenCADStudio {
auto_collapse, auto_collapse,
), ),
crate::ui::dock::PanelId::NodeGraph => tab.graph.panel(width, auto_collapse), crate::ui::dock::PanelId::NodeGraph => tab.graph.panel(width, auto_collapse),
crate::ui::dock::PanelId::PointCloudManager => crate::ui::window::pc_manager::view(
&self.pc_manager,
&tab.scene.document,
&tab.scene.selected_handles_in_order(),
width,
auto_collapse,
),
}; };
let divider = dock_divider(id); let divider = dock_divider(id);
match side { match side {
+19
View File
@@ -29,6 +29,9 @@ impl OpenCADStudio {
Some(codec::entities::UnderlayType::Dgn) => crate::t!("Attach DGN Underlay").into_owned(), Some(codec::entities::UnderlayType::Dgn) => crate::t!("Attach DGN Underlay").into_owned(),
_ => crate::t!("Attach PDF Underlay").into_owned(), _ => crate::t!("Attach PDF Underlay").into_owned(),
}, },
Some(K::PointCloudAttach) => crate::t!("Attach Point Cloud").into_owned(),
Some(K::PointCloudColorMap) => crate::t!("Point Cloud Color Map").into_owned(),
Some(K::PcSection) => crate::t!("Extract Section Lines from Point Cloud").into_owned(),
Some(K::UnderlayLayers) => crate::t!("Underlay Layers").into_owned(), Some(K::UnderlayLayers) => crate::t!("Underlay Layers").into_owned(),
Some(K::PdfImportSettings) => crate::t!("PDF Import Settings").into_owned(), Some(K::PdfImportSettings) => crate::t!("PDF Import Settings").into_owned(),
Some(K::PdfImportFile) => crate::t!("Import PDF").into_owned(), Some(K::PdfImportFile) => crate::t!("Import PDF").into_owned(),
@@ -522,6 +525,22 @@ impl OpenCADStudio {
crate::ui::window::pdf_dialogs::view_attach(state, flow) 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::PointCloudColorMap => {
let state = self.point_cloud_color_map.as_ref()?;
sized_flow(ex, 760, 560, |flow| {
crate::ui::window::pdf_dialogs::view_point_cloud_color_map(state, flow)
})
}
super::super::ModalKind::PcSection => {
let state = self.pc_section.as_ref()?;
sized_flow(ex, 760, 470, |flow| crate::ui::window::pdf_dialogs::view_pc_section(state, flow))
}
super::super::ModalKind::UnderlayLayers => { super::super::ModalKind::UnderlayLayers => {
let state = self.underlay_layers.as_ref()?; let state = self.underlay_layers.as_ref()?;
sized_flow(ex, 460, 520, |flow| { sized_flow(ex, 460, 520, |flow| {
+13
View File
@@ -1572,6 +1572,13 @@ pub enum CmdResult {
OpenPdfImportSettings, OpenPdfImportSettings,
/// Import a page of a file (PDFIMPORT File); ends the command. /// Import a page of a file (PDFIMPORT File); ends the command.
PdfImportFile(crate::modules::insert::pdf_import::PdfFileImport), PdfImportFile(crate::modules::insert::pdf_import::PdfFileImport),
/// Attach a point cloud: the host creates or reuses its definition,
/// commits the cloud in one undo step, then ends the command. The
/// message is the last prompt's echo.
AttachPointCloud {
placement: crate::modules::insert::pc_attach::PointCloudPlacement,
message: String,
},
/// Attach PDF pages: each page's underlay with its file (as stored) and /// Attach PDF pages: each page's underlay with its file (as stored) and
/// page; the host creates or reuses the definitions and commits them all /// page; the host creates or reuses the definitions and commits them all
/// in one undo step, then ends the command. /// in one undo step, then ends the command.
@@ -2843,6 +2850,12 @@ pub trait CadCommand: Send {
false false
} }
/// The command moves, turns or resizes what it gathers: the host drops
/// locked point clouds from each completed selection and says so.
fn selection_drops_locked_point_clouds(&self) -> bool {
false
}
/// Before each input the host says which block references carry a clip /// Before each input the host says which block references carry a clip
/// boundary. /// boundary.
fn inject_clipped(&mut self, _clipped: &dyn Fn(Handle) -> bool) {} fn inject_clipped(&mut self, _clipped: &dyn Fn(Handle) -> bool) {}
+346 -208
View File
@@ -470,12 +470,77 @@ fn point_cloud_clip_lines(data: &PointCloudData) -> Vec<[f64; 3]> {
points points
} }
fn point_cloud_ex_lines(data: &PointCloudExData) -> Vec<[f64; 3]> { /// The cloud's extents box, placed in the drawing. Its points are drawn from
let mut points = Vec::new(); /// the scan file; the box only picks the cloud, unless the file cannot be
push_box(&mut points, data.extents_min, data.extents_max); /// found: then the box shows, with the saved path written across it.
fn point_cloud_ex_lines(data: &PointCloudExData, document: &codec::CadDocument) -> Vec<[f64; 3]> {
let mut points = point_cloud_ex_box(data);
if crate::scene::model::point_cloud::resolve_source(document, data).is_none() {
let saved = crate::scene::model::point_cloud::definition(document, data)
.map(|definition| definition.source_filename.clone())
.unwrap_or_default();
append_missing_cloud_label(&mut points, data, &saved);
}
points points
} }
fn point_cloud_ex_box(data: &PointCloudExData) -> Vec<[f64; 3]> {
let corners = codec::entities::point_cloud_ex_corners(data).map(|c| [c.x, c.y, c.z]);
let mut points = Vec::new();
for a in 0..8 {
for bit in [1, 2, 4] {
if a & bit == 0 {
push_segment(&mut points, corners[a], corners[a | bit]);
}
}
}
points
}
/// A cloud in the 2D wireframe visual style, which draws no points: its
/// extents box with the reference's message across the base.
pub(crate) fn point_cloud_2d_style_lines(data: &PointCloudExData) -> Vec<[f64; 3]> {
let mut points = point_cloud_ex_box(data);
append_missing_cloud_label(&mut points, data, "Point clouds are not displayed in 2D visual style.");
points
}
/// The saved path, centred on the extents' base and as wide as most of it.
fn append_missing_cloud_label(points: &mut Vec<[f64; 3]>, data: &PointCloudExData, text: &str) {
if text.is_empty() {
return;
}
let (strokes, _) =
crate::scene::text::lff::tessellate_text_ex([0.0, 0.0], 1.0, 0.0, 1.0, 0.0, "standard", text);
let (mut lo, mut hi) = ([f64::MAX; 2], [f64::MIN; 2]);
for [x, y] in strokes.iter().flatten() {
lo = [lo[0].min(*x as f64), lo[1].min(*y as f64)];
hi = [hi[0].max(*x as f64), hi[1].max(*y as f64)];
}
let (min, max) = (data.extents_min, data.extents_max);
let (width, height) = (hi[0] - lo[0], hi[1] - lo[1]);
if !(width > 0.0 && height > 0.0) {
return;
}
let scale = (0.8 * (max.x - min.x) / width).min(0.5 * (max.y - min.y) / height);
let center = [(min.x + max.x) * 0.5, (min.y + max.y) * 0.5];
let mid = [(lo[0] + hi[0]) * 0.5, (lo[1] + hi[1]) * 0.5];
for stroke in strokes {
push_chain(
points,
stroke.into_iter().map(|[x, y]| {
let local = Vector3::new(
center[0] + (x as f64 - mid[0]) * scale,
center[1] + (y as f64 - mid[1]) * scale,
min.z,
);
let p = codec::entities::point_cloud_ex_to_world(data, local);
[p.x, p.y, p.z]
}),
);
}
}
fn point_cloud_ex_clip_lines(data: &PointCloudExData) -> Vec<[f64; 3]> { fn point_cloud_ex_clip_lines(data: &PointCloudExData) -> Vec<[f64; 3]> {
let mut points = Vec::new(); let mut points = Vec::new();
if data.show_cropping { if data.show_cropping {
@@ -483,9 +548,27 @@ fn point_cloud_ex_clip_lines(data: &PointCloudExData) -> Vec<[f64; 3]> {
if crop.points.len() < 2 { if crop.points.len() < 2 {
continue; continue;
} }
let mut chain: Vec<[f64; 3]> = // Crop points are kept in the cloud's own coordinates; the crop
crop.points.iter().map(|p| [p.x, p.y, p.z]).collect(); // runs along the normal of the plane its two directions span.
if crop.points.len() > 2 { let place = |p: Vector3| {
let w = codec::entities::point_cloud_ex_to_world(data, p);
[w.x, w.y, w.z]
};
// A circle is stored as its centre and a point on it.
let outline: Vec<Vector3> = if crop.crop_type == 3 {
let (c, d) = (crop.points[0], crop.points[1] - crop.points[0]);
let r = d.dot(&crop.x_direction).hypot(d.dot(&crop.y_direction));
(0..64)
.map(|k| {
let a = k as f64 / 64.0 * std::f64::consts::TAU;
c + crop.x_direction * (r * a.cos()) + crop.y_direction * (r * a.sin())
})
.collect()
} else {
crop.points.clone()
};
let mut chain: Vec<[f64; 3]> = outline.into_iter().map(place).collect();
if chain.len() > 2 {
chain.push(chain[0]); chain.push(chain[0]);
} }
push_chain(&mut points, chain); push_chain(&mut points, chain);
@@ -605,14 +688,14 @@ fn to_render(entity: &ExtendedEntity, document: &codec::CadDocument) -> Option<R
[data.extents_max.x, data.extents_max.y, data.extents_max.z], [data.extents_max.x, data.extents_max.y, data.extents_max.z],
], ],
), ),
ExtendedEntityData::PointCloudEx(data) => ( ExtendedEntityData::PointCloudEx(data) => {
point_cloud_ex_lines(data), let origin = data.ucs_origin;
Vec::new(), (
vec![ point_cloud_ex_lines(data, document),
[data.extents_min.x, data.extents_min.y, data.extents_min.z], vec![(glam::DVec3::new(origin.x, origin.y, origin.z), SnapHint::Insertion)],
[data.extents_max.x, data.extents_max.y, data.extents_max.z], vec![[origin.x, origin.y, origin.z]],
], )
), }
_ => return None, _ => return None,
}; };
if points.len() < 2 { if points.len() < 2 {
@@ -1048,122 +1131,229 @@ fn point_cloud_properties(data: &PointCloudData) -> Vec<PropSection> {
] ]
} }
fn point_cloud_ex_properties(data: &PointCloudExData) -> Vec<PropSection> { /// The colour ramps every point cloud colour map carries, by their fixed
let crops = data /// identifiers; a cloud names its schemes by identifier.
.croppings pub(crate) const POINT_CLOUD_RAMPS: [(&str, &str); 7] = [
("Blues", "9B45EA40-176A-46B5-9470-1FA6A7880EE3"),
("Earth", "6DFAB669-B096-4842-9F90-F23CBDD9E410"),
("Grayscale", "655FCCB9-2DB2-40BC-86BB-A513D3DF3FEE"),
("Greens", "B5E88F37-A921-45DF-9987-B979CE516EF3"),
("Hydro", "46B3A5C6-4029-4D50-90D3-C49ADB0A2B45"),
("Reds", "E247D320-B4DE-471F-8E5F-B848819BF6F6"),
("Spectrum", "C0A3838B-81E3-4506-B908-13ECF523DB68"),
];
/// A built-in scheme's identifier by name (Blues when unknown).
pub(crate) fn point_cloud_ramp_id(name: &str) -> &'static str {
POINT_CLOUD_RAMPS.iter().find(|(n, _)| *n == name).unwrap_or(&POINT_CLOUD_RAMPS[0]).1
}
/// Stylization choices in the order the reference lists them, with the
/// stored values: scan colours 1, object colour 2, normals 3, intensity 5,
/// elevation 4.
const POINT_CLOUD_STYLIZATIONS: [(&str, i16); 5] = [
("Scan Colors", 1),
("Object Color", 2),
("Normals", 3),
("Intensity", 5),
("Elevation", 4),
];
/// The scheme slot a stylization colours by: intensity and elevation each
/// keep their own; the other stylizations show (and set) the intensity one.
fn point_cloud_scheme_slot(data: &mut PointCloudExData) -> &mut String {
match data.stylization_type {
4 => &mut data.current_color_scheme,
6 => &mut data.classification_color_scheme,
_ => &mut data.intensity_color_scheme,
}
}
/// The name of the scheme a cloud shows; an unset scheme reads as the first
/// of the list, as the reference shows it.
pub(crate) fn point_cloud_scheme_name(data: &PointCloudExData) -> &'static str {
let id = match data.stylization_type {
4 => &data.current_color_scheme,
6 => &data.classification_color_scheme,
_ => &data.intensity_color_scheme,
};
POINT_CLOUD_RAMPS
.iter() .iter()
.enumerate() .find(|(_, guid)| guid.eq_ignore_ascii_case(id))
.map(|(index, crop)| { .unwrap_or(&POINT_CLOUD_RAMPS[0])
format!( .0
"{}: type {}; inside {}; inverted {}; points {}; plane [{}]", }
index + 1,
crop.crop_type, /// A placement's rotation about its Z axis and its scale.
crop.inside, pub(crate) fn point_cloud_rotation_scale(data: &PointCloudExData) -> (f64, f64) {
crop.inverted, let x = data.ucs_x_direction;
crop.points.len(), (x.y.atan2(x.x), x.length())
vector_text(crop.plane) }
)
}) fn plain_row(label: &str, field: &'static str, value: f64) -> Property {
.collect::<Vec<_>>() Property {
.join("\n"); label: label.into(),
field,
value: PropValue::EditText(crate::entities::underlay::plain_number(value)),
}
}
fn yes_no_row(label: &str, field: &'static str, flag: bool) -> Property {
Property {
label: label.into(),
field,
value: PropValue::Choice {
selected: if flag { t!("Yes") } else { t!("No") }.into_owned(),
options: vec![t!("Yes").into_owned(), t!("No").into_owned()],
},
}
}
fn is_yes(value: &str) -> bool {
let value = value.trim();
value == t!("Yes").as_ref() || value.eq_ignore_ascii_case("yes") || value.eq_ignore_ascii_case("true")
}
// Props: General → 3D Visualization → Geometry → Misc, as the reference
// lists a point cloud. Saved path, Unit and Unit factor are filled by the
// Properties panel, where the definition is reachable.
fn point_cloud_ex_properties(data: &PointCloudExData) -> Vec<PropSection> {
let stylization = POINT_CLOUD_STYLIZATIONS
.iter()
.find(|(_, value)| *value == data.stylization_type)
.map_or("Scan Colors", |(name, _)| name);
let (rotation, scale) = point_cloud_rotation_scale(data);
let origin = data.ucs_origin;
vec![ vec![
PropSection { PropSection {
title: t!("Point Cloud Ex").into_owned(), title: t!("3D Visualization").into_owned(),
props: vec![ props: vec![
ro_prop(t!("Class Version").as_ref(), Property {
"ext_pcx_version", label: t!("Stylization").into_owned(),
data.class_version.to_string(), field: "ext_pcx_stylization",
), value: PropValue::Choice {
text_prop(t!("Name").as_ref(), "ext_pcx_name", &data.name), selected: t!(stylization).into_owned(),
ro_prop(t!("Extents Min").as_ref(), "ext_pcx_min", vector_text(data.extents_min)), options: POINT_CLOUD_STYLIZATIONS
ro_prop(t!("Extents Max").as_ref(), "ext_pcx_max", vector_text(data.extents_max)), .iter()
ro_prop(t!("UCS Origin").as_ref(), .map(|(name, _)| t!(*name).into_owned())
"ext_pcx_ucs_origin", .collect(),
vector_text(data.ucs_origin), },
), },
ro_prop(t!("UCS X").as_ref(), choice_prop(
"ext_pcx_ucs_x", t!("Color scheme").as_ref(),
vector_text(data.ucs_x_direction), "ext_pcx_color_scheme",
), point_cloud_scheme_name(data),
ro_prop(t!("UCS Y").as_ref(), &POINT_CLOUD_RAMPS.map(|(name, _)| name),
"ext_pcx_ucs_y",
vector_text(data.ucs_y_direction),
),
ro_prop(t!("UCS Z").as_ref(),
"ext_pcx_ucs_z",
vector_text(data.ucs_z_direction),
),
bool_prop(t!("Locked").as_ref(), "ext_pcx_locked", data.locked),
ro_prop(t!("Definition").as_ref(),
"ext_pcx_definition",
handle_text(data.definition_handle),
),
ro_prop(t!("Reactor").as_ref(),
"ext_pcx_reactor",
handle_text(data.reactor_handle),
), ),
], ],
}, },
PropSection { PropSection {
title: t!("Point Cloud Ex Display").into_owned(), title: t!("Geometry").into_owned(),
props: vec![ props: vec![
bool_prop(t!("Show Intensity").as_ref(), plain_row(t!("Insertion point X").as_ref(), "ext_pcx_ix", origin.x),
"ext_pcx_show_intensity", plain_row(t!("Insertion point Y").as_ref(), "ext_pcx_iy", origin.y),
data.show_intensity, plain_row(t!("Insertion point Z").as_ref(), "ext_pcx_iz", origin.z),
), plain_row(t!("Rotation").as_ref(), "ext_pcx_rotation", rotation.to_degrees()),
bool_prop(t!("Show Cropping").as_ref(), "ext_pcx_show_cropping", data.show_cropping), plain_row(t!("Scale").as_ref(), "ext_pcx_scale", scale),
ro_prop(t!("Unknown Flags").as_ref(), ],
"ext_pcx_unknown", },
format!("{}, {}", data.unknown_bl0, data.unknown_bl1), PropSection {
), title: t!("Misc").into_owned(),
ro_prop(t!("Stylization Type").as_ref(), props: vec![
"ext_pcx_stylization", text_prop(t!("Name").as_ref(), "ext_pcx_name", &data.name),
data.stylization_type.to_string(), ro_prop(t!("Saved path").as_ref(), "ext_pcx_path", String::new()),
), yes_no_row(t!("Show cropped").as_ref(), "ext_pcx_show_cropping", data.show_cropping),
text_prop(t!("Intensity Color Scheme").as_ref(), yes_no_row(t!("Locked").as_ref(), "ext_pcx_locked", data.locked),
"ext_pcx_intensity_scheme", yes_no_row(t!("Geolocate").as_ref(), "ext_pcx_geolocate", data.show_intensity),
&data.intensity_color_scheme, ro_prop(t!("Segmentation").as_ref(), "ext_pcx_segmentation", t!("No").into_owned()),
), ro_prop(t!("Unit").as_ref(), "ext_pcx_unit", String::new()),
text_prop(t!("Current Color Scheme").as_ref(), ro_prop(t!("Unit factor").as_ref(), "ext_pcx_unit_factor", String::new()),
"ext_pcx_current_scheme",
&data.current_color_scheme,
),
text_prop(t!("Classification Scheme").as_ref(),
"ext_pcx_class_scheme",
&data.classification_color_scheme,
),
edit_prop(t!("Elevation Min").as_ref(), "ext_pcx_elevation_min", data.elevation_min),
edit_prop(t!("Elevation Max").as_ref(), "ext_pcx_elevation_max", data.elevation_max),
ro_prop(t!("Intensity Range").as_ref(),
"ext_pcx_intensity_range",
format!("{}..{}", data.intensity_min, data.intensity_max),
),
ro_prop(t!("Out Of Range Behavior").as_ref(),
"ext_pcx_out_of_range",
format!(
"intensity {}; elevation {}",
data.intensity_out_of_range_behavior,
data.elevation_out_of_range_behavior
),
),
bool_prop(t!("Fixed Elevation Range").as_ref(),
"ext_pcx_fixed_range",
data.elevation_apply_to_fixed_range,
),
bool_prop(t!("Intensity Gradient").as_ref(),
"ext_pcx_intensity_gradient",
data.intensity_as_gradient,
),
bool_prop(t!("Elevation Gradient").as_ref(),
"ext_pcx_elevation_gradient",
data.elevation_as_gradient,
),
ro_prop(t!("Croppings").as_ref(), "ext_pcx_croppings", crops),
], ],
}, },
] ]
} }
/// Writes a point cloud row, after `validate_point_cloud_property` passed
/// it; a locked cloud takes only its Locked row (the caller sees to that).
fn apply_point_cloud_ex_prop(data: &mut PointCloudExData, field: &str, value: &str) {
let number = parse_f64(value);
match field {
"ext_pcx_name" => data.name = value.to_string(),
"ext_pcx_locked" => data.locked = is_yes(value),
"ext_pcx_show_cropping" => data.show_cropping = is_yes(value),
// Geolocation needs the drawing's geographic location, which a cloud
// attached without one never gets: the row stays No.
"ext_pcx_geolocate" => {}
"ext_pcx_stylization" => {
if let Some((_, stylization)) = POINT_CLOUD_STYLIZATIONS
.iter()
.find(|(name, _)| value == t!(*name).as_ref() || value.eq_ignore_ascii_case(name))
{
set_point_cloud_stylization(data, *stylization);
}
}
"ext_pcx_color_scheme" => {
if let Some((_, guid)) = POINT_CLOUD_RAMPS.iter().find(|(name, _)| *name == value) {
*point_cloud_scheme_slot(data) = guid.to_string();
}
}
"ext_pcx_ix" | "ext_pcx_iy" | "ext_pcx_iz" => {
if let Some(number) = number {
match field {
"ext_pcx_ix" => data.ucs_origin.x = number,
"ext_pcx_iy" => data.ucs_origin.y = number,
_ => data.ucs_origin.z = number,
}
}
}
// Turns the placement about its own Z axis to the angle given.
"ext_pcx_rotation" => {
if let Some(degrees) = number {
let (current, _) = point_cloud_rotation_scale(data);
let axis = normalized(data.ucs_z_direction, Vector3::UNIT_Z);
let turn = Transform::from_rotation(axis, degrees.to_radians() - current);
data.ucs_x_direction = turn.apply_rotation(data.ucs_x_direction);
data.ucs_y_direction = turn.apply_rotation(data.ucs_y_direction);
}
}
"ext_pcx_scale" => {
let (_, current) = point_cloud_rotation_scale(data);
if let Some(scale) = number.filter(|scale| *scale > 0.0 && current > 0.0) {
let factor = scale / current;
data.ucs_x_direction = data.ucs_x_direction * factor;
data.ucs_y_direction = data.ucs_y_direction * factor;
data.ucs_z_direction = data.ucs_z_direction * factor;
}
}
_ => {}
}
}
/// 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) {
return Err("Value must be positive");
}
Ok(())
}
fn semantic_properties( fn semantic_properties(
properties: &[codec::objects::SemanticProperty], properties: &[codec::objects::SemanticProperty],
) -> String { ) -> String {
@@ -1504,27 +1694,7 @@ fn apply_geom_prop(entity: &mut ExtendedEntity, field: &str, value: &str) {
"ext_pc_high_threshold" => set_f64(value, &mut data.high_intensity_threshold), "ext_pc_high_threshold" => set_f64(value, &mut data.high_intensity_threshold),
_ => {} _ => {}
}, },
ExtendedEntityData::PointCloudEx(data) => match field { ExtendedEntityData::PointCloudEx(data) => apply_point_cloud_ex_prop(data, field, value),
"ext_pcx_name" => data.name = value.to_string(),
"ext_pcx_locked" => data.locked = !data.locked,
"ext_pcx_show_intensity" => data.show_intensity = !data.show_intensity,
"ext_pcx_show_cropping" => data.show_cropping = !data.show_cropping,
"ext_pcx_intensity_scheme" => data.intensity_color_scheme = value.to_string(),
"ext_pcx_current_scheme" => data.current_color_scheme = value.to_string(),
"ext_pcx_class_scheme" => data.classification_color_scheme = value.to_string(),
"ext_pcx_elevation_min" => set_f64(value, &mut data.elevation_min),
"ext_pcx_elevation_max" => set_f64(value, &mut data.elevation_max),
"ext_pcx_fixed_range" => {
data.elevation_apply_to_fixed_range = !data.elevation_apply_to_fixed_range
}
"ext_pcx_intensity_gradient" => {
data.intensity_as_gradient = !data.intensity_as_gradient
}
"ext_pcx_elevation_gradient" => {
data.elevation_as_gradient = !data.elevation_as_gradient
}
_ => {}
},
_ => {} _ => {}
} }
} }
@@ -1657,36 +1827,40 @@ fn grips(entity: &ExtendedEntity) -> Vec<GripDef> {
), ),
), ),
], ],
ExtendedEntityData::PointCloudEx(data) => vec![ // One grip, at the insertion point: it moves the cloud.
square_grip( ExtendedEntityData::PointCloudEx(data) => vec![square_grip(
0, 0,
glam::DVec3::new( glam::DVec3::new(data.ucs_origin.x, data.ucs_origin.y, data.ucs_origin.z),
data.extents_min.x, )],
data.extents_min.y,
data.extents_min.z,
),
),
square_grip(
1,
glam::DVec3::new(
data.extents_max.x,
data.extents_max.y,
data.extents_max.z,
),
),
center_grip(
2,
glam::DVec3::new(
(data.extents_min.x + data.extents_max.x) * 0.5,
(data.extents_min.y + data.extents_max.y) * 0.5,
(data.extents_min.z + data.extents_max.z) * 0.5,
),
),
],
_ => Vec::new(), _ => Vec::new(),
} }
} }
/// The move gizmo at a point cloud's insertion point: arrows 1-3 along X/Y/Z
/// and squares 4-6 for the XY/YZ/ZX planes.
pub(crate) fn point_cloud_gizmo_grips(data: &PointCloudExData) -> Vec<GripDef> {
use crate::scene::model::object::GripShape;
let world = glam::DVec3::new(data.ucs_origin.x, data.ucs_origin.y, data.ucs_origin.z);
(0..3u8)
.map(|k| GripDef {
id: 1 + k as usize,
world,
is_midpoint: true,
shape: GripShape::GizmoAxis(k),
dir: None,
axis: Some(crate::scene::pick::grip::gizmo_axis(k)),
})
.chain((0..3u8).map(|k| GripDef {
id: 4 + k as usize,
world,
is_midpoint: true,
shape: GripShape::GizmoPlane(k),
dir: None,
axis: None,
}))
.collect()
}
fn apply_grip(entity: &mut ExtendedEntity, grip_id: usize, apply: GripApply) { fn apply_grip(entity: &mut ExtendedEntity, grip_id: usize, apply: GripApply) {
match &mut entity.data { match &mut entity.data {
ExtendedEntityData::SectionObject(data) => { ExtendedEntityData::SectionObject(data) => {
@@ -1762,23 +1936,10 @@ fn apply_grip(entity: &mut ExtendedEntity, grip_id: usize, apply: GripApply) {
data.ucs_origin = data.ucs_origin + delta; data.ucs_origin = data.ucs_origin + delta;
} }
} }
// The insertion grip and every move-gizmo part move the cloud.
ExtendedEntityData::PointCloudEx(data) => { ExtendedEntityData::PointCloudEx(data) => {
let before = (data.extents_min + data.extents_max) * 0.5; if grip_id <= 6 {
move_extents( apply_point(&mut data.ucs_origin, apply);
&mut data.extents_min,
&mut data.extents_max,
grip_id,
apply,
);
if grip_id == 2 {
let after = (data.extents_min + data.extents_max) * 0.5;
let delta = after - before;
data.ucs_origin = data.ucs_origin + delta;
for crop in &mut data.croppings {
for point in &mut crop.points {
*point = *point + delta;
}
}
} }
} }
_ => {} _ => {}
@@ -2080,37 +2241,14 @@ fn apply_transform(entity: &mut ExtendedEntity, requested: &EntityTransform) {
} }
} }
} }
// The scan keeps its own coordinates (extents and crop boundaries
// stay as they are); the placement takes the transform, its axes
// carrying the scale.
ExtendedEntityData::PointCloudEx(data) => { ExtendedEntityData::PointCloudEx(data) => {
data.ucs_origin = transform.apply(data.ucs_origin); data.ucs_origin = transform.apply(data.ucs_origin);
data.ucs_x_direction = normalized( data.ucs_x_direction = transform.apply_rotation(data.ucs_x_direction);
transform.apply_rotation(data.ucs_x_direction), data.ucs_y_direction = transform.apply_rotation(data.ucs_y_direction);
Vector3::UNIT_X, data.ucs_z_direction = transform.apply_rotation(data.ucs_z_direction);
);
data.ucs_y_direction = normalized(
transform.apply_rotation(data.ucs_y_direction),
Vector3::UNIT_Y,
);
data.ucs_z_direction = normalized(
transform.apply_rotation(data.ucs_z_direction),
Vector3::UNIT_Z,
);
(data.extents_min, data.extents_max) =
transform_extents(data.extents_min, data.extents_max, &transform);
for crop in &mut data.croppings {
crop.plane =
normalized(transform.apply_rotation(crop.plane), Vector3::UNIT_Z);
crop.x_direction = normalized(
transform.apply_rotation(crop.x_direction),
Vector3::UNIT_X,
);
crop.y_direction = normalized(
transform.apply_rotation(crop.y_direction),
Vector3::UNIT_Y,
);
for point in &mut crop.points {
*point = transform.apply(*point);
}
}
} }
_ => {} _ => {}
} }
+6
View File
@@ -90,6 +90,12 @@ pub fn ui_name_or_class(e: &EntityType) -> String {
) { ) {
return "Section Plane".to_string(); return "Section Plane".to_string();
} }
if matches!(
entity.data,
codec::entities::ExtendedEntityData::PointCloudEx(_)
) {
return "Point Cloud".to_string();
}
return entity.class_name().to_string(); return entity.class_name().to_string();
} }
if let EntityType::Unknown(u) = e { if let EntityType::Unknown(u) = e {
+2 -10
View File
@@ -1432,11 +1432,7 @@ fn class_object_section(
"Ramps", "Ramps",
bounded_join( bounded_join(
value.color_ramps.iter().map(|ramp| { value.color_ramps.iter().map(|ramp| {
format!( format!("{} v{} ({} colours)", ramp.name, ramp.class_version, ramp.colors.len())
"v{} [{}]",
ramp.class_version,
ramp.color_schemes.join(", ")
)
}), }),
32, 32,
), ),
@@ -1445,11 +1441,7 @@ fn class_object_section(
"Classification Ramps", "Classification Ramps",
bounded_join( bounded_join(
value.classification_color_ramps.iter().map(|ramp| { value.classification_color_ramps.iter().map(|ramp| {
format!( format!("{} v{} ({} colours)", ramp.name, ramp.class_version, ramp.colors.len())
"v{} [{}]",
ramp.class_version,
ramp.color_schemes.join(", ")
)
}), }),
32, 32,
), ),
+160 -1
View File
@@ -322,6 +322,21 @@ pub fn register_underlay_sources(doc: &CadDocument, base_dir: &Path) {
crate::scene::model::pdf_raster::register_source(stored, std::sync::Arc::new(bytes)); crate::scene::model::pdf_raster::register_source(stored, std::sync::Arc::new(bytes));
} }
} }
// Point cloud scans are large and read lazily: register where they are.
for object in doc.objects.values() {
let codec::objects::ObjectType::ClassObject(object) = object else {
continue;
};
let (codec::objects::ClassObjectData::PointCloudDefinitionEx(def)
| codec::objects::ClassObjectData::PointCloudDefinition(def)) = &object.data
else {
continue;
};
let stored = def.source_filename.trim();
if let Some(found) = (!stored.is_empty()).then(|| resolve_path(stored, base_dir)).flatten() {
crate::scene::model::point_cloud::register_source(stored, found);
}
}
} }
/// Layer properties an override can change, as compared for the /// Layer properties an override can change, as compared for the
@@ -1277,6 +1292,55 @@ pub fn collect_entries_with_prev(
entries.push(entry); 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 ── // ── Nested enumeration: full transitive closure, read-only, no merge ──
// Paths stored in a nested drawing are relative to *that drawing*, not to // 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 // the host. The work queue carries each parent's resolved file so every
@@ -1369,6 +1433,7 @@ enum RefTarget {
DwgXref { handle: Handle, name: String }, DwgXref { handle: Handle, name: String },
Image { handle: Handle, name: String }, Image { handle: Handle, name: String },
Pdf { handle: Handle, name: String }, Pdf { handle: Handle, name: String },
PointCloud { handle: Handle, name: String },
} }
fn find_target(doc: &CadDocument, key: u64) -> Option<RefTarget> { fn find_target(doc: &CadDocument, key: u64) -> Option<RefTarget> {
@@ -1389,6 +1454,11 @@ fn find_target(doc: &CadDocument, key: u64) -> Option<RefTarget> {
name: def.file_name.clone(), 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 => { ObjectType::UnderlayDefinition(def) if handle.value() == key => {
return Some(RefTarget::Pdf { return Some(RefTarget::Pdf {
handle: *handle, handle: *handle,
@@ -1462,6 +1532,20 @@ pub fn unload_reference(doc: &mut CadDocument, key: u64) -> Result<String, Strin
} }
Ok(name) 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;
}
} }
} }
@@ -1563,6 +1647,34 @@ pub fn detach_reference(doc: &mut CadDocument, key: u64) -> Result<String, Strin
doc.objects.remove(&handle); doc.objects.remove(&handle);
Ok(name) 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)
}
} }
} }
@@ -1611,6 +1723,9 @@ pub fn bind_reference(
"XREF: PDF bind (vector import) is not available in this version." "XREF: PDF bind (vector import) is not available in this version."
) )
.to_string()), .to_string()),
RefTarget::PointCloud { .. } => {
Err(crate::t!("XREF: bind applies to drawing references only.").to_string())
}
} }
} }
@@ -1881,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 /// Make the named references' paths relative to `host` (their drawing's
/// first save); a reference that cannot be relative keeps its full path. /// first save); a reference that cannot be relative keeps its full path.
pub fn make_relative( pub fn make_relative(
@@ -1889,6 +2008,12 @@ pub fn make_relative(
host: &Path, host: &Path,
) { ) {
for name in names { 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 { let Some(key) = doc.block_records.get(name).map(|br| br.handle.value()) else {
continue; continue;
}; };
@@ -2146,6 +2271,19 @@ pub fn set_ref_path(
_ => Err(crate::t!("XREF: no loaded reference with that key.").to_string()), _ => 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()),
}
}
} }
} }
@@ -2177,6 +2315,7 @@ pub fn apply_pathtype(
_ => String::new(), _ => String::new(),
} }
} }
Some(RefTarget::PointCloud { name, .. }) => name,
None => return Err(crate::t!("XREF: no loaded reference with that key.").to_string()), 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 // A stored relative path first has to be resolved against its current host
@@ -2305,7 +2444,7 @@ pub fn set_ref_type(
} }
Err(crate::t!("XREF: no loaded reference with that key.").to_string()) 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()) Err(crate::t!("XREF: overlays apply to drawing references only.").to_string())
} }
} }
@@ -3106,3 +3245,23 @@ mod tests {
} }
} }
} }
/// 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;
}
}
+2
View File
@@ -305,6 +305,8 @@ pub enum RefKind {
Image, Image,
/// A PDF, DWF or DGN underlay. /// A PDF, DWF or DGN underlay.
Underlay, Underlay,
/// A point cloud scan (.rcs) or project (.rcp).
PointCloud,
} }
/// Lifecycle state of a [`ReferenceEntry`]. /// Lifecycle state of a [`ReferenceEntry`].
+77
View File
@@ -96,6 +96,83 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"#RRGGBB or blank" => Some(("common", "rrggbb-or-blank")), "#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 (%{count} selected, Enter to apply):" => Some(("selection", "cmd-select-objects-count-selected-enter-to-app")),
"%{cmd} Select objects:" => Some(("selection", "cmd-select-objects")), "%{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")),
"Point Cloud Color Map" => Some(("insert", "pcm-title")),
"Color ramp" => Some(("insert", "pcm-ramp")),
"Number of colors" => Some(("insert", "pcm-count")),
"Display as gradient" => Some(("insert", "pcm-gradient")),
"Range of colorized points" => Some(("insert", "pcm-range")),
"Maximum intensity" => Some(("insert", "pcm-max-int")),
"Minimum intensity" => Some(("insert", "pcm-min-int")),
"Maximum elevation" => Some(("insert", "pcm-max-elev")),
"Minimum elevation" => Some(("insert", "pcm-min-elev")),
"Interval height" => Some(("insert", "pcm-interval")),
"Apply to extents of point cloud" => Some(("insert", "pcm-extents")),
"Out of range points" => Some(("insert", "pcm-oor")),
"Use min/max colors" => Some(("insert", "pcm-oor-minmax")),
"Use RGB scan colors" => Some(("insert", "pcm-oor-scan")),
"Hide points" => Some(("insert", "pcm-oor-hide")),
"Make stylization current" => Some(("insert", "pcm-current")),
"A color scheme with this name already exists." => Some(("insert", "pcm-name-taken")),
"Invalid range of colorized points." => Some(("insert", "pcm-bad-range")),
"Sets the color schemes and ranges a point cloud is colored by intensity or elevation." => Some(("insert", "pcm-help")),
"Point Cloud Manager" => Some(("insert", "pc-manager")),
"REGIONS AND SCANS" => Some(("insert", "pc-regions-scans")),
"Regions" => Some(("insert", "pc-regions")),
"Unassigned Points" => Some(("insert", "pc-unassigned")),
"Scans" => Some(("insert", "pc-scans")),
"Search" => Some(("insert", "pc-search")),
"Collapse all" => Some(("insert", "pc-collapse-all")),
"Expand all" => Some(("insert", "pc-expand-all")),
"Scheme name" => Some(("insert", "pcm-scheme-name")),
"Extract Section Lines from Point Cloud" => Some(("insert", "pcs-title")),
"Extract" => Some(("insert", "pcs-extract")),
"Entire cross section" => Some(("insert", "pcs-entire")),
"Perimeter only" => Some(("insert", "pcs-perimeter")),
"Maximum points to process" => Some(("insert", "pcs-max-points")),
"Faster" => Some(("insert", "pcs-faster")),
"More accurate" => Some(("insert", "pcs-accurate")),
"Estimated time: {count} minutes" => Some(("insert", "pcs-time")),
"Output geometry" => Some(("insert", "pcs-output")),
"2D Polylines" => Some(("insert", "pcs-polylines")),
"Polyline width" => Some(("insert", "pcs-width")),
"Extraction tolerances" => Some(("insert", "pcs-tolerances")),
"Minimum line length" => Some(("insert", "pcs-min-length")),
"Connect lines tolerance" => Some(("insert", "pcs-connect")),
"Collinear angle tolerance" => Some(("insert", "pcs-angle")),
"Preview result" => Some(("insert", "pcs-preview")),
"Use Current" => Some(("insert", "pcs-use-current")),
"Requires a positive number." => Some(("insert", "pcs-positive")),
"Requires a positive integer." => Some(("insert", "pcs-positive-int")),
"Traces the points on the kept side of a live section as lines." => Some(("insert", "pcs-help")),
"Stylization" => Some(("properties", "stylization")),
"Color scheme" => Some(("properties", "color-scheme")),
"Scan Colors" => Some(("properties", "scan-colors")),
"Object Color" => Some(("properties", "object-color")),
"Classification" => Some(("properties", "classification")),
"Show cropped" => Some(("properties", "show-cropped")),
"Geolocate" => Some(("properties", "geolocate")),
"Segmentation" => Some(("properties", "segmentation")),
"Unit factor" => Some(("properties", "unit-factor")),
"Saved path" => Some(("properties", "saved-path-lower")),
"%{count} objects selected" => Some(("properties", "count-objects-selected")), "%{count} objects selected" => Some(("properties", "count-objects-selected")),
"%{count} selected layers" => Some(("modal", "count-selected-layers")), "%{count} selected layers" => Some(("modal", "count-selected-layers")),
"%{count} steps available" => Some(("ui", "count-steps-available")), "%{count} steps available" => Some(("ui", "count-steps-available")),
+14
View File
@@ -124,6 +124,13 @@ impl SelectObjectsCommand {
command 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 { pub fn auto_constrain(pending_cmd: &str) -> Self {
let mut command = Self::new(pending_cmd); let mut command = Self::new(pending_cmd);
command.auto_constrain_settings = true; command.auto_constrain_settings = true;
@@ -196,6 +203,13 @@ impl CadCommand for SelectObjectsCommand {
self.associative_dimensions_only self.associative_dimensions_only
} }
fn selection_drops_locked_point_clouds(&self) -> bool {
matches!(
self.pending_cmd.as_str(),
"MOVE" | "3DMOVE" | "ROTATE" | "3DROTATE" | "SCALE" | "MIRROR" | "MIRROR3D" | "STRETCH"
)
}
// Clickable selection keywords (#426, #596). Window and Crossing fix the // Clickable selection keywords (#426, #596). Window and Crossing fix the
// sense of the next box, which dragging would otherwise take from the // sense of the next box, which dragging would otherwise take from the
// direction the corner travels; All takes the whole space; Add and Remove // direction the corner travels; All takes the whole space; Add and Remove
+7 -7
View File
@@ -16,7 +16,10 @@ pub(crate) mod picker;
pub mod minsert; pub mod minsert;
mod mview_block; mod mview_block;
mod open_obj; mod open_obj;
mod pc_attach; pub(crate) mod pc_attach;
pub(crate) mod pc_crop;
pub(crate) mod pc_extract;
pub(crate) mod pc_stylize;
pub(crate) mod pdf_attach; pub(crate) mod pdf_attach;
pub(crate) mod pdf_clip; pub(crate) mod pdf_clip;
pub(crate) mod pdf_import; pub(crate) mod pdf_import;
@@ -53,7 +56,8 @@ impl CadModule for InsertModule {
title: "Reference", title: "Reference",
tools: vec![ tools: vec![
RibbonItem::LargeTool(xattach::tool()), 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 { RibbonItem::LargeDropdown {
id: "UNDERLAY_ATTACH", id: "UNDERLAY_ATTACH",
label: "Attach Underlay", label: "Attach Underlay",
@@ -62,6 +66,7 @@ impl CadModule for InsertModule {
("PDFATTACH", "Attach PDF", pdf_attach::ICON), ("PDFATTACH", "Attach PDF", pdf_attach::ICON),
("DWFATTACH", "Attach DWF", pdf_attach::ICON), ("DWFATTACH", "Attach DWF", pdf_attach::ICON),
("DGNATTACH", "Attach DGN", pdf_attach::ICON), ("DGNATTACH", "Attach DGN", pdf_attach::ICON),
("POINTCLOUDATTACH", "Attach Point Cloud", pc_attach::ICON),
], ],
default: "PDFATTACH", default: "PDFATTACH",
}, },
@@ -95,11 +100,6 @@ impl CadModule for InsertModule {
}, },
], ],
}, },
// ── Point Cloud ───────────────────────────────────────────────────
RibbonGroup {
title: "Point Cloud",
tools: vec![RibbonItem::LargeTool(pc_attach::tool())],
},
// ── Block ───────────────────────────────────────────────────────── // ── Block ─────────────────────────────────────────────────────────
RibbonGroup { RibbonGroup {
title: "Block", title: "Block",
+259 -7
View File
@@ -1,10 +1,262 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef}; // -POINTCLOUDATTACH — attach a point cloud: a scan (.rcs) or a project of
// scans (.rcp).
//
// Path to point cloud file to attach:
// Specify insertion point <0,0>: Current insertion point: X = 5.5, Y = 2, Z = 3
// Specify scale factor <1>: Current scale factor: 2.500000
// Specify rotation angle <0>: Current rotate angle: 30
// 1 point cloud attached
//
// The definition (one per file, in ACAD_POINTCLOUD_EX_DICT) is created with
// the cloud when it is placed, so a cancelled attach leaves nothing and undo
// removes both.
use std::path::PathBuf;
use glam::DVec3;
use crate::command::{CadCommand, CmdResult, InputKind, WorkingPlane};
use crate::modules::IconKind;
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/pc_attach.svg")); pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/pc_attach.svg"));
pub fn tool() -> ToolDef {
ToolDef { /// What the Attach Point Cloud dialog settles before the prompts: the
id: "POINTCLOUDATTACH", /// path to store, the switches, and the scale and rotation not left to
label: "Attach", /// the command line.
icon: ICON, #[derive(Clone, Debug)]
event: ModuleEvent::Command("POINTCLOUDATTACH".to_string()), 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 }
} }
} }
/// A point cloud to attach: the file, where and how it is placed.
#[derive(Clone, Debug)]
pub struct PointCloudPlacement {
pub path: PathBuf,
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)]
enum Step {
Path,
Insertion,
Scale,
Rotation,
}
pub struct PointCloudAttachCommand {
step: Step,
path: PathBuf,
insertion: DVec3,
scale: f64,
plane: WorkingPlane,
options: AttachOptions,
}
/// A number as the reference echoes a coordinate or an angle: no trailing
/// zeros.
fn plain(value: f64) -> String {
crate::entities::underlay::plain_number(value)
}
impl PointCloudAttachCommand {
pub fn new() -> Self {
Self {
step: Step::Path,
path: PathBuf::new(),
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();
command.path = path;
command.step = Step::Insertion;
command
}
fn accept_path(&mut self, text: &str) -> CmdResult {
let text = text.trim().trim_matches('"');
if text.is_empty() {
return CmdResult::ReportError("Filename cannot be blank".to_string());
}
let path = PathBuf::from(text);
// A file that is not there is asked for again.
if !path.is_file() {
return CmdResult::NeedPoint;
}
if crate::scene::model::point_cloud::load(&path).is_none() {
// Said, and the command ends.
return CmdResult::Measurement(
"All scans are not found or invalid.\nAttach point cloud failed".to_string(),
);
}
self.path = path;
self.step = Step::Insertion;
CmdResult::NeedPoint
}
fn accept_insertion(&mut self, point: DVec3) -> CmdResult {
self.insertion = point;
self.step = Step::Scale;
let local = self.plane.to_local(point);
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 {
let scale = if text.trim().is_empty() {
1.0
} else {
match crate::entities::common::parse_f64(text) {
Some(scale) => scale,
None => {
return CmdResult::ReportError(
"Requires numeric distance or second point.".to_string(),
)
}
}
};
if scale <= 0.0 {
return CmdResult::ReportError("Value must be positive and nonzero.".to_string());
}
self.scale = scale;
self.step = Step::Rotation;
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;
CmdResult::AttachPointCloud {
placement: PointCloudPlacement {
path: self.path.clone(),
insertion: self.insertion,
axes: [x, y, self.plane.z * self.scale],
options: self.options.clone(),
},
message,
}
}
}
impl CadCommand for PointCloudAttachCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str {
"POINTCLOUDATTACH"
}
fn prompt(&self) -> String {
match self.step {
Step::Path => "Path to point cloud file to attach:",
Step::Insertion => "Specify insertion point <0,0>:",
Step::Scale => "Specify scale factor <1>:",
Step::Rotation => "Specify rotation angle <0>:",
}
.to_string()
}
fn input_kind(&self) -> InputKind {
match self.step {
Step::Path => InputKind::FreeText,
Step::Insertion => InputKind::Point,
_ => InputKind::SingleToken,
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step {
Step::Insertion => self.accept_insertion(pt),
// A second point gives the angle from the insertion point.
Step::Rotation => {
let d = self.plane.vector_to_local(pt - self.insertion);
self.accept_rotation(d.y.atan2(d.x).to_degrees())
}
_ => CmdResult::NeedPoint,
}
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
Some(match self.step {
Step::Path => self.accept_path(text),
Step::Insertion => CmdResult::ReportError("Invalid point.".to_string()),
Step::Scale => self.accept_scale(text),
Step::Rotation => match crate::entities::common::parse_f64(text) {
Some(degrees) => self.accept_rotation(degrees),
None => CmdResult::ReportError(
"Requires valid numeric angle or second point.".to_string(),
),
},
})
}
fn on_enter(&mut self) -> CmdResult {
match self.step {
Step::Path => self.accept_path(""),
Step::Insertion => self.accept_insertion(self.plane.origin),
Step::Scale => self.accept_scale(""),
Step::Rotation => self.accept_rotation(0.0),
}
}
}
inventory::submit!(crate::command::CommandRegistration {
names: &["POINTCLOUDATTACH", "-POINTCLOUDATTACH"]
});
+409
View File
@@ -0,0 +1,409 @@
// POINTCLOUDCROP — crop a point cloud to a rectangle, polygon or circle.
// POINTCLOUDUNCROP — `Select point cloud:` and all its crops go.
//
// Select point cloud:
// Specify first corner point or [Polygon/Circular]:
// (with crops: [Polygon/Circular/Invert/Remove last/ON/OFF])
// Specify other corner point:
// Specify first point: / Specify next point or [Undo]: (Polygon)
// Specify center point: / Specify radius: (Circular)
// Keep points inside or outside? [Inside/Outside] <Inside>:
//
// Crops are kept in the cloud's own coordinates, on the view plane (its
// right and up directions) and running along the view: a rectangle as its
// four corners, a polygon as its points, a circle as its centre and a point
// on it. They add up; Invert flips them,
// Remove last drops the newest, ON / OFF shows or hides them.
use std::sync::Mutex;
use codec::entities::{ExtendedEntity, ExtendedEntityData, PointCloudExCrop, PointCloudExData};
use codec::types::{Handle, Vector3};
use codec::EntityType;
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
use crate::scene::model::wire_model::WireModel;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Step {
Select,
First,
RectSecond,
PolyFirst,
PolyNext,
Center,
Radius,
Keep,
}
/// The crop being drawn: its type and points in the drawing.
#[derive(Clone, Copy, PartialEq, Eq)]
enum Shape {
Rectangle = 1,
Polygon = 2,
Circle = 3,
}
/// Inside or outside, as last chosen: the next crop offers it.
static LAST_INSIDE: Mutex<bool> = Mutex::new(true);
fn last_inside() -> bool {
LAST_INSIDE.lock().map(|v| *v).unwrap_or(true)
}
const REQUIRED: &str = "Point or option keyword required.";
pub struct PointCloudCropCommand {
step: Step,
handle: Handle,
cloud: Option<ExtendedEntity>,
picked: Option<EntityType>,
shape: Shape,
points: Vec<DVec3>,
/// POINTCLOUDUNCROP: the picked cloud loses its crops.
uncrop: bool,
/// The view's right and up directions in the drawing.
view: (DVec3, DVec3),
}
impl PointCloudCropCommand {
pub fn new() -> Self {
Self {
step: Step::Select,
handle: Handle::NULL,
cloud: None,
picked: None,
shape: Shape::Rectangle,
points: Vec::new(),
uncrop: false,
view: (DVec3::X, DVec3::Y),
}
}
pub fn uncrop() -> Self {
Self { uncrop: true, ..Self::new() }
}
/// 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),
_ => None,
}
}
fn has_crops(&self) -> bool {
self.data().is_some_and(|data| !data.croppings.is_empty())
}
/// The cloud with `change` made, and the command done.
fn finish(&self, change: impl FnOnce(&mut PointCloudExData)) -> CmdResult {
let Some(mut cloud) = self.cloud.clone() else {
return CmdResult::Cancel;
};
if let ExtendedEntityData::PointCloudEx(data) = &mut cloud.data {
change(data);
}
CmdResult::UpdateEntityAndFinish { handle: self.handle, entity: EntityType::Extended(cloud) }
}
fn axes(data: &PointCloudExData) -> glam::DMat3 {
let v = |a: Vector3| DVec3::new(a.x, a.y, a.z);
glam::DMat3::from_cols(v(data.ucs_x_direction), v(data.ucs_y_direction), v(data.ucs_z_direction))
}
/// A point of the drawing in the cloud's own coordinates.
fn to_local(data: &PointCloudExData, p: DVec3) -> DVec3 {
let origin = data.ucs_origin;
Self::axes(data).inverse() * (p - DVec3::new(origin.x, origin.y, origin.z))
}
/// The view's right and up directions in the cloud's own coordinates.
fn view_local(&self, data: &PointCloudExData) -> (DVec3, DVec3) {
let inverse = Self::axes(data).inverse();
((inverse * self.view.0).normalize_or(DVec3::X), (inverse * self.view.1).normalize_or(DVec3::Y))
}
fn option(&mut self, text: &str) -> CmdResult {
let crops = self.has_crops();
match text.trim().to_ascii_uppercase().as_str() {
"P" | "POLYGON" => {
self.shape = Shape::Polygon;
self.step = Step::PolyFirst;
CmdResult::NeedPoint
}
"C" | "CIRCULAR" => {
self.shape = Shape::Circle;
self.step = Step::Center;
CmdResult::NeedPoint
}
"I" | "INVERT" if crops => self.finish(|data| {
for crop in &mut data.croppings {
crop.inverted = !crop.inverted;
}
}),
"R" | "REMOVE" | "REMOVE LAST" if crops => self.finish(|data| {
data.croppings.pop();
}),
"ON" if crops => self.finish(|data| data.show_cropping = true),
"OFF" if crops => self.finish(|data| data.show_cropping = false),
_ => CmdResult::ReportError(REQUIRED.to_string()),
}
}
fn keep(&mut self, text: &str) -> CmdResult {
let inside = match text.trim().to_ascii_uppercase().as_str() {
"" => last_inside(),
"I" | "INSIDE" => true,
"O" | "OUTSIDE" => false,
_ => return CmdResult::ReportError("Invalid option keyword.".to_string()),
};
if let Ok(mut last) = LAST_INSIDE.lock() {
*last = inside;
}
let Some(data) = self.data() else {
return CmdResult::Cancel;
};
let (right, up) = self.view_local(data);
let local: Vec<DVec3> = self.points.iter().map(|p| Self::to_local(data, *p)).collect();
let local = match self.shape {
// The corners along the view's right and up directions.
Shape::Rectangle => {
let (a, d) = (local[0], local[1] - local[0]);
let (dx, dy) = (right * d.dot(right), up * d.dot(up));
vec![a, a + dx, a + dx + dy, a + dy]
}
_ => local,
};
let vector = |v: DVec3| Vector3::new(v.x, v.y, v.z);
let crop = PointCloudExCrop {
crop_type: self.shape as i16,
inside,
inverted: false,
plane: Vector3::ZERO,
x_direction: vector(right),
y_direction: vector(up),
points: local.into_iter().map(vector).collect(),
};
self.finish(move |data| {
data.croppings.push(crop);
data.show_cropping = true;
})
}
fn to_keep(&mut self) -> CmdResult {
self.step = Step::Keep;
CmdResult::NeedPoint
}
}
impl CadCommand for PointCloudCropCommand {
fn name(&self) -> &'static str {
if self.uncrop { "POINTCLOUDUNCROP" } else { "POINTCLOUDCROP" }
}
fn prompt(&self) -> String {
match self.step {
Step::Select => "Select point cloud:".to_string(),
Step::First if self.has_crops() => {
"Specify first corner point or [Polygon/Circular/Invert/Remove last/ON/OFF]:".to_string()
}
Step::First => "Specify first corner point or [Polygon/Circular]:".to_string(),
Step::RectSecond => "Specify other corner point:".to_string(),
Step::PolyFirst => "Specify first point:".to_string(),
Step::PolyNext => "Specify next point or [Undo]:".to_string(),
Step::Center => "Specify center point:".to_string(),
Step::Radius => "Specify radius:".to_string(),
Step::Keep => format!(
"Keep points inside or outside? [Inside/Outside] <{}>:",
if last_inside() { "Inside" } else { "Outside" }
),
}
}
fn options(&self) -> Vec<CmdOption> {
match self.step {
Step::First => {
let mut options = vec![CmdOption::new("Polygon", "P"), CmdOption::new("Circular", "C")];
if self.has_crops() {
options.extend([
CmdOption::new("Invert", "I"),
CmdOption::new("Remove last", "R"),
CmdOption::new("ON", "ON"),
CmdOption::new("OFF", "OFF"),
]);
}
options
}
Step::PolyNext => vec![CmdOption::new("Undo", "U")],
Step::Keep => vec![CmdOption::new("Inside", "I"), CmdOption::new("Outside", "O")],
_ => Vec::new(),
}
}
fn input_kind(&self) -> InputKind {
match self.step {
Step::Keep => InputKind::SingleToken,
_ => InputKind::Point,
}
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.step, Step::First | Step::PolyNext | Step::Radius)
}
fn needs_entity_pick(&self) -> bool {
self.step == Step::Select
}
fn wants_point_pick_context(&self) -> bool {
!matches!(self.step, Step::Select | Step::Keep)
}
fn set_point_pick_context(&mut self, context: Option<crate::command::PointPickContext>) {
// The projection's first two rows run along the view's right and up.
if let Some(context) = context {
let row = |i: usize| context.view.row(i).truncate().as_dvec3().normalize_or_zero();
let (right, up) = (row(0), row(1));
if right != DVec3::ZERO && up != DVec3::ZERO {
self.view = (right, up);
}
}
}
fn inject_before_entity_pick(&self) -> bool {
true
}
fn inject_picked_entity(&mut self, entity: EntityType) {
self.picked = Some(entity);
}
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
match self.picked.take() {
Some(EntityType::Extended(cloud)) if matches!(cloud.data, ExtendedEntityData::PointCloudEx(_)) => {
self.handle = handle;
self.cloud = Some(cloud);
if self.uncrop {
return self.finish(|data| data.croppings.clear());
}
self.step = Step::First;
CmdResult::NeedPoint
}
// Anything else: the prompt again.
_ => CmdResult::NeedPoint,
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step {
Step::First => {
self.points = vec![pt];
self.step = Step::RectSecond;
CmdResult::NeedPoint
}
Step::RectSecond => {
self.points.push(pt);
self.to_keep()
}
Step::PolyFirst => {
self.points = vec![pt];
self.step = Step::PolyNext;
CmdResult::NeedPoint
}
Step::PolyNext => {
self.points.push(pt);
CmdResult::NeedPoint
}
Step::Center => {
self.points = vec![pt];
self.step = Step::Radius;
CmdResult::NeedPoint
}
Step::Radius => {
self.points.push(pt);
self.to_keep()
}
_ => CmdResult::NeedPoint,
}
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
Some(match self.step {
Step::First => self.option(text),
Step::PolyNext => match text.trim().to_ascii_uppercase().as_str() {
"U" | "UNDO" if self.points.len() > 1 => {
self.points.pop();
CmdResult::NeedPoint
}
_ => CmdResult::ReportError(REQUIRED.to_string()),
},
// A radius typed: a point that far along the X axis.
Step::Radius => match crate::entities::common::parse_f64(text).filter(|r| *r > 0.0) {
Some(radius) => {
let center = self.points[0];
self.points.push(center + DVec3::new(radius, 0.0, 0.0));
self.to_keep()
}
None => CmdResult::ReportError(REQUIRED.to_string()),
},
Step::Keep => self.keep(text),
_ => return None,
})
}
fn on_enter(&mut self) -> CmdResult {
match self.step {
// A polygon of three points or more is closed by Enter.
Step::PolyNext if self.points.len() >= 3 => self.to_keep(),
Step::Keep => self.keep(""),
Step::Select => CmdResult::Cancel,
_ => CmdResult::ReportError(REQUIRED.to_string()),
}
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
let outline: Vec<[f64; 3]> = match self.step {
Step::RectSecond => {
let a = self.points[0];
[a, DVec3::new(pt.x, a.y, a.z), DVec3::new(pt.x, pt.y, a.z), DVec3::new(a.x, pt.y, a.z), a]
.map(|p| p.to_array())
.to_vec()
}
Step::PolyNext => self
.points
.iter()
.copied()
.chain([pt, self.points[0]])
.map(|p| p.to_array())
.collect(),
Step::Radius => {
let c = self.points[0];
let r = (pt - c).truncate().length();
(0..=64)
.map(|k| {
let a = k as f64 / 64.0 * std::f64::consts::TAU;
(c + DVec3::new(r * a.cos(), r * a.sin(), 0.0)).to_array()
})
.collect()
}
_ => return None,
};
Some(WireModel::solid_f64("pc_crop_boundary".into(), outline, WireModel::CYAN, false))
}
}
inventory::submit!(crate::command::CommandRegistration { names: &["POINTCLOUDCROP", "POINTCLOUDUNCROP"] });
File diff suppressed because it is too large. Load diff
+165
View File
@@ -0,0 +1,165 @@
// 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"] });
// POINTCLOUDCOLORMAP — the Point Cloud Color Map dialog.
//
// Select point cloud or [None] <None>:
//
// A picked cloud opens the dialog for it; None (or Enter) for the
// drawing's default schemes.
pub struct PointCloudColorMapCommand {
picked: Option<codec::EntityType>,
}
impl PointCloudColorMapCommand {
pub fn new() -> Self {
Self { picked: None }
}
}
impl CadCommand for PointCloudColorMapCommand {
fn name(&self) -> &'static str {
"POINTCLOUDCOLORMAP"
}
fn prompt(&self) -> String {
"Select point cloud or [None] <None>:".to_string()
}
fn options(&self) -> Vec<CmdOption> {
vec![CmdOption::new("None", "N")]
}
fn input_kind(&self) -> InputKind {
InputKind::Point
}
fn point_step_accepts_keywords(&self) -> bool {
true
}
fn needs_entity_pick(&self) -> bool {
true
}
fn inject_before_entity_pick(&self) -> bool {
true
}
fn inject_picked_entity(&mut self, entity: codec::EntityType) {
self.picked = Some(entity);
}
fn on_entity_pick(&mut self, handle: Handle, _pt: glam::DVec3) -> CmdResult {
match self.picked.take() {
Some(entity) if crate::scene::is_point_cloud(&entity) => {
CmdResult::Dispatch(format!("_PCCOLORMAP {:X}", handle.value()))
}
// Anything else: the prompt again.
_ => CmdResult::NeedPoint,
}
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
match text.trim().to_ascii_uppercase().as_str() {
"" | "N" | "NONE" => Some(CmdResult::Dispatch("_PCCOLORMAP".to_string())),
_ => Some(CmdResult::ReportError("Invalid option keyword.".to_string())),
}
}
fn on_point(&mut self, _pt: glam::DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Dispatch("_PCCOLORMAP".to_string())
}
}
inventory::submit!(crate::command::CommandRegistration { names: &["POINTCLOUDCOLORMAP"] });
+6 -1
View File
@@ -105,11 +105,16 @@ pub fn visualization_section(entity: &EntityType) -> Option<PropSection> {
| EntityType::Seqend(_) | EntityType::Seqend(_)
| EntityType::Leader(_) | EntityType::Leader(_)
| EntityType::Wipeout(_) | EntityType::Wipeout(_)
// The reference lists no 3D group for an underlay. // The reference lists no 3D group for an underlay; a point
// cloud's is its own (stylization and colour scheme).
| EntityType::Underlay(_) | EntityType::Underlay(_)
| EntityType::Unknown(_) | EntityType::Unknown(_)
// Non-plotting drawing-view border: never rendered, no properties. // Non-plotting drawing-view border: never rendered, no properties.
| EntityType::ViewBorder(_) | EntityType::ViewBorder(_)
) || matches!(
entity,
EntityType::Extended(extended)
if matches!(extended.data, codec::entities::ExtendedEntityData::PointCloudEx(_))
) { ) {
return None; return None;
} }
+19 -4
View File
@@ -564,6 +564,21 @@ fn point_cloud_wires(
let RenderObject::Lines(body_points) = rendered.object else { let RenderObject::Lines(body_points) = rendered.object else {
return None; return None;
}; };
// A cloud whose scan file is found draws its points instead of its
// extents box; the box stays behind, unseen, to pick the cloud, with its
// faces so a click among the points selects it too.
let mut shown = true;
let (mut pick_tris, mut pick_tris_low) = (Vec::new(), Vec::new());
if let codec::entities::ExtendedEntityData::PointCloudEx(data) = &extended.data {
// Its box shows too with every point hidden (the reference does).
shown = crate::scene::model::point_cloud::shows_no_points(document, data);
let c = codec::entities::point_cloud_ex_corners(data).map(|c| [c.x, c.y, c.z]);
let mut tris = Vec::with_capacity(36);
for [a, b, d, e] in [[0, 1, 3, 2], [4, 5, 7, 6], [0, 1, 4, 5], [2, 3, 6, 7], [0, 2, 4, 6], [1, 3, 5, 7]] {
tris.extend([c[a], c[b], c[e], c[a], c[e], c[d]]);
}
(pick_tris, pick_tris_low) = points_to_ds(tris);
}
let (points, points_low) = points_to_ds(body_points); let (points, points_low) = points_to_ds(body_points);
let mut wires = vec![WireModel { let mut wires = vec![WireModel {
bg_adapt: None, bg_adapt: None,
@@ -572,14 +587,14 @@ fn point_cloud_wires(
pattern_stations: Vec::new(), pattern_stations: Vec::new(),
world_width: 0.0, world_width: 0.0,
depth_override: None, depth_override: None,
display_visible: true, display_visible: shown,
snap_only: false, snap_only: false,
plot_visible: true, plot_visible: shown,
fill_is_3d: false, fill_is_3d: false,
fill_is_2d_solid: false, fill_is_2d_solid: false,
render_instance: None, render_instance: None,
pick_tris: Vec::new(), pick_tris,
pick_tris_low: Vec::new(), pick_tris_low,
dash_from_start: false, dash_from_start: false,
dash_align_end: None, dash_align_end: None,
text_verts: Vec::new(), text_verts: Vec::new(),
+164 -7
View File
@@ -2186,6 +2186,11 @@ pub struct Scene {
frozen_hatch_cache: RefCell<HashMap<(Handle, String, u64), (u64, u64, Arc<Vec<HatchModel>>)>>, frozen_hatch_cache: RefCell<HashMap<(Handle, String, u64), (u64, u64, Arc<Vec<HatchModel>>)>>,
frozen_wipeout_cache: RefCell<HashMap<(Handle, String, u64), (u64, Arc<Vec<HatchModel>>)>>, frozen_wipeout_cache: RefCell<HashMap<(Handle, String, u64), (u64, Arc<Vec<HatchModel>>)>>,
frozen_image_cache: RefCell<HashMap<(Handle, u64), (u64, Arc<Vec<ImageModel>>)>>, frozen_image_cache: RefCell<HashMap<(Handle, u64), (u64, Arc<Vec<ImageModel>>)>>,
/// Point clouds shown by the model content, keyed by target block and
/// frozen-layer signature, per geometry epoch.
point_cloud_cache: RefCell<
HashMap<(Handle, u64), (u64, Arc<crate::scene::model::point_cloud::PointCloudSet>)>,
>,
frozen_mesh_cache: RefCell<HashMap<(Handle, String, u64), (u64, Arc<Vec<MeshLodSet>>)>>, frozen_mesh_cache: RefCell<HashMap<(Handle, String, u64), (u64, Arc<Vec<MeshLodSet>>)>>,
/// Viewports that carry layer color/alpha/linetype/lineweight overrides. /// Viewports that carry layer color/alpha/linetype/lineweight overrides.
/// Cached per geometry epoch so ordinary viewports can share render data. /// Cached per geometry epoch so ordinary viewports can share render data.
@@ -2648,6 +2653,7 @@ impl Scene {
frozen_hatch_cache: RefCell::new(HashMap::default()), frozen_hatch_cache: RefCell::new(HashMap::default()),
frozen_wipeout_cache: RefCell::new(HashMap::default()), frozen_wipeout_cache: RefCell::new(HashMap::default()),
frozen_image_cache: RefCell::new(HashMap::default()), frozen_image_cache: RefCell::new(HashMap::default()),
point_cloud_cache: RefCell::new(HashMap::default()),
frozen_mesh_cache: RefCell::new(HashMap::default()), frozen_mesh_cache: RefCell::new(HashMap::default()),
viewport_style_override_cache: RefCell::new(None), viewport_style_override_cache: RefCell::new(None),
insert_hatch_cache: RefCell::new(None), insert_hatch_cache: RefCell::new(None),
@@ -8338,6 +8344,135 @@ impl Scene {
arc arc
} }
/// Point clouds of the model content, with a content viewport's frozen
/// layers removed. The paper sheet shows none. The set is rebuilt per
/// geometry epoch; unchanged clouds keep their placed points (and GPU
/// buffers) by key.
pub(super) fn point_clouds_for_viewport(
&self,
frozen: &HashSet<Handle>,
paper_sheet: bool,
) -> Arc<crate::scene::model::point_cloud::PointCloudSet> {
use crate::scene::model::point_cloud;
static EMPTY: std::sync::OnceLock<Arc<point_cloud::PointCloudSet>> =
std::sync::OnceLock::new();
let empty = || Arc::clone(EMPTY.get_or_init(Default::default));
if paper_sheet {
return empty();
}
let target_block = self.content_render_block_handle();
let key = (target_block, Self::frozen_layers_sig(frozen));
if let Some((epoch, arc)) = self.point_cloud_cache.borrow().get(&key) {
if *epoch == self.geometry_epoch {
return Arc::clone(arc);
}
}
let arc = if point_cloud::any_definition(&self.document) {
let depths = rustc_hash::FxHashMap::default();
let graph = render_graph::RenderSceneGraph::new(
&self.document,
(!frozen.is_empty()).then_some(frozen),
None,
true,
&depths,
);
let mut clouds = Vec::new();
graph.walk_root(
self.render_scene_root(target_block),
|entity, context| {
context.is_instanced() || !self.entity_temporarily_hidden(entity.common().handle)
},
|entity, context| {
let EntityType::Extended(extended) = entity else {
return;
};
let codec::entities::ExtendedEntityData::PointCloudEx(data) = &extended.data
else {
return;
};
let transform = context.is_instanced().then_some(&context.transform);
// Object Color stylization draws the cloud in its own colour.
let common = entity.common();
let color = if matches!(common.color, codec::types::Color::ByLayer) {
self.document
.layers
.get(&common.layer)
.map(|layer| layer.color.clone())
.unwrap_or(common.color.clone())
} else {
common.color.clone()
};
let object_color = color.rgb().map_or([255; 3], |(r, g, b)| [r, g, b]);
clouds.extend(point_cloud::placed(
&self.document,
data,
object_color,
transform,
&point_cloud::hidden(data),
));
},
);
if clouds.is_empty() {
empty()
} else {
Arc::new(point_cloud::PointCloudSet {
point_size: point_cloud::point_size(&self.document),
clouds,
})
}
} else {
empty()
};
self.point_cloud_cache
.borrow_mut()
.insert(key, (self.geometry_epoch, Arc::clone(&arc)));
arc
}
/// Wireframe 2D draws no cloud points: each shown cloud whose file is
/// found draws its extents box and a message instead, unless
/// POINTCLOUD2DVSDISPLAY is 1.
// ponytail: model-space clouds only, rebuilt per frame; cache by epoch if
// drawings with many clouds make it show.
pub(super) fn point_cloud_2d_wires(&self) -> Vec<WireModel> {
use crate::scene::model::point_cloud::{resolve_source, setting, setting_value};
if !crate::scene::model::point_cloud::any_definition(&self.document)
|| setting("POINTCLOUD2DVSDISPLAY").is_some_and(|s| setting_value(&self.document, s) == 1)
{
return Vec::new();
}
let mut wires = Vec::new();
for entity in self.document.entities() {
let EntityType::Extended(extended) = entity else {
continue;
};
let codec::entities::ExtendedEntityData::PointCloudEx(data) = &extended.data else {
continue;
};
let common = entity.common();
if common.invisible
|| self.entity_temporarily_hidden(common.handle)
|| self.document.layers.get(&common.layer).is_some_and(|layer| layer.is_off() || layer.is_frozen())
|| resolve_source(&self.document, data).is_none()
{
continue;
}
let color = if matches!(common.color, codec::types::Color::ByLayer) {
self.document.layers.get(&common.layer).map(|layer| layer.color.clone()).unwrap_or(common.color.clone())
} else {
common.color.clone()
};
let (r, g, b) = color.rgb().unwrap_or((255, 255, 255));
wires.push(WireModel::solid_f64(
common.handle.value().to_string(),
crate::entities::extended::point_cloud_2d_style_lines(data),
[f32::from(r) / 255.0, f32::from(g) / 255.0, f32::from(b) / 255.0, 1.0],
false,
));
}
wires
}
/// Image / OLE models for a content viewport, with its frozen layers removed. /// Image / OLE models for a content viewport, with its frozen layers removed.
pub(super) fn images_for_viewport( pub(super) fn images_for_viewport(
&self, &self,
@@ -8408,14 +8543,19 @@ impl Scene {
arc arc
} }
/// True when `handle`'s entity sits on a locked layer. Locked objects stay /// True when `handle`'s entity sits on a locked layer, or is a point cloud
/// visible, snappable and selectable, but mutation paths must skip them. /// locked on its own. Locked objects stay visible, snappable and
/// selectable, but mutation paths must skip them.
pub fn is_layer_locked(&self, handle: Handle) -> bool { pub fn is_layer_locked(&self, handle: Handle) -> bool {
self.document let Some(entity) = self.document.get_entity(handle) else {
.get_entity(handle) return false;
.map(|e| e.common().layer.clone()) };
.and_then(|name| self.document.layers.get(&name).map(|l| l.is_locked())) is_locked_point_cloud(entity)
.unwrap_or(false) || self
.document
.layers
.get(&entity.common().layer)
.is_some_and(|l| l.is_locked())
} }
/// The name of the locked layer `handle` sits on, if any (for messages). /// The name of the locked layer `handle` sits on, if any (for messages).
@@ -13833,3 +13973,20 @@ 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!(
entity,
EntityType::Extended(extended)
if matches!(&extended.data, codec::entities::ExtendedEntityData::PointCloudEx(data) if data.locked)
)
}
+1
View File
@@ -9,6 +9,7 @@ pub mod ole_pres;
pub mod pdf_layers; pub mod pdf_layers;
pub mod pdf_raster; pub mod pdf_raster;
pub mod pdf_vector; pub mod pdf_vector;
pub mod point_cloud;
pub mod underlay_vector; pub mod underlay_vector;
pub mod mesh_model; pub mod mesh_model;
pub mod solid_model; pub mod solid_model;
+5
View File
@@ -135,6 +135,11 @@ pub enum GripShape {
Dropdown, Dropdown,
/// Menu selector placed immediately beside its anchor grip. /// Menu selector placed immediately beside its anchor grip.
DropdownAdjacent, DropdownAdjacent,
/// Move-gizmo arrow along world X (0), Y (1) or Z (2); the grip's world
/// point is the gizmo centre, the marker sits at the arrow tip.
GizmoAxis(u8),
/// Move-gizmo square between two axes: XY (0), YZ (1) or ZX (2).
GizmoPlane(u8),
} }
/// Describes one grip point for an entity. /// Describes one grip point for an entity.
+734
View File
@@ -0,0 +1,734 @@
// Point cloud scans drawn as points: the scan files a PointCloudEx entity
// names are decoded once per file version, and the points it shows (placed
// in the drawing, crops applied, coloured) are kept per placement so a
// re-tessellate or a new render set reuses them.
mod rcp;
mod rcs;
use codec::entities::{PointCloudExCrop, PointCloudExData};
use codec::objects::{ClassObjectData, ObjectType, PointCloudDefinition};
use codec::types::{Transform, Vector3};
use codec::CadDocument;
use rustc_hash::FxHashMap;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock, Weak};
use std::time::SystemTime;
/// A scan file's points, in the file's own coordinates (the frame a
/// PointCloudEx places with its origin and axes).
pub struct CloudPoints {
/// Unique per decoded file version: keys the placed point sets.
id: u64,
pub positions: Vec<[f64; 3]>,
pub colors: Vec<[u8; 3]>,
pub intensity: Vec<u8>,
/// Unit normals in the file's frame (empty without normals).
pub normals: Vec<[f32; 3]>,
pub has_rgb: bool,
pub has_intensity: bool,
pub has_normals: bool,
/// Scans read (one for a scan file).
pub scans: usize,
/// Each scan's name (its file stem) and its points' index range.
pub scan_ranges: Vec<ScanRange>,
/// 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
/// decode is remembered too, so it is not re-read on every tessellate.
pub fn load(path: &Path) -> Option<Arc<CloudPoints>> {
type Cache = Mutex<FxHashMap<PathBuf, (SystemTime, Option<Arc<CloudPoints>>)>>;
static CACHE: OnceLock<Cache> = OnceLock::new();
let modified = std::fs::metadata(path).and_then(|m| m.modified()).ok()?;
let mut cache = CACHE.get_or_init(Default::default).lock().ok()?;
if let Some((stamp, cloud)) = cache.get(path) {
if *stamp == modified {
return cloud.clone();
}
}
let started = crate::perf::enabled().then(iced::time::Instant::now);
let cloud = decode(path).map(Arc::new);
crate::perf::record(format_args!(
"[perf] point-cloud-load {:>7.1}ms points={} {}",
crate::perf::elapsed_ms(started),
cloud.as_ref().map_or(0, |cloud| cloud.positions.len()),
path.display(),
));
cache.insert(path.to_path_buf(), (modified, cloud.clone()));
cloud
}
fn decode(path: &Path) -> Option<CloudPoints> {
static NEXT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
let bytes = std::fs::read(path).ok()?;
let is_project = path
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("rcp"));
// A project lists its scans with their own transforms; a scan file
// carries the same transform in its header.
let stem = |p: &Path| p.file_stem().map_or_else(String::new, |s| s.to_string_lossy().into_owned());
let scans: Vec<(rcs::Scan, [[f64; 3]; 3], String, String)> = if is_project {
rcp::scans(&bytes, path)?
.into_iter()
.filter_map(|entry| {
let scan = rcs::decode(&std::fs::read(&entry.path).ok()?)?;
// The project's identifier, else the scan file's own.
let id = if entry.id.is_empty() { scan.id.clone() } else { entry.id.clone() };
Some((scan, [entry.translation, entry.rotation, entry.scale], stem(&entry.path), id))
})
.collect()
} else {
let scan = rcs::decode(&bytes)?;
let transform = [scan.translation, scan.rotation, scan.scale];
let id = scan.id.clone();
vec![(scan, transform, stem(path), id)]
};
let mut cloud = CloudPoints {
id: NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
positions: Vec::new(),
colors: Vec::new(),
intensity: Vec::new(),
normals: 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(),
scan_ranges: Vec::new(),
preview: is_project.then(|| rcp::preview(&bytes)).flatten(),
bounds: None,
};
for (scan, [translation, rotation, scale], name, id) in scans {
let rotate = euler(rotation.map(f64::to_radians));
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]]
};
let first = cloud.positions.len();
cloud.positions.extend(scan.local.iter().map(place));
let id = if id.is_empty() { name.clone() } else { id };
cloud.scan_ranges.push(ScanRange { name, id, range: first..cloud.positions.len() });
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 {
// A scan without colours among coloured ones shows its intensity.
cloud.colors.extend(scan.intensity.iter().map(|&i| [i; 3]));
}
cloud.intensity.extend_from_slice(&scan.intensity);
cloud.normals.extend(scan.normals.iter().map(|n| {
let r = rotate.map(|row| row[0] * f64::from(n[0]) + row[1] * f64::from(n[1]) + row[2] * f64::from(n[2]));
r.map(|v| v as f32)
}));
}
Some(cloud)
}
/// Rotation matrix of a scan's rotation angles (radians here; projects and
/// scan headers store degrees), applied about X, then Y, then Z — the
/// reference's extents of rotated scans match this order.
fn euler([x, y, z]: [f64; 3]) -> [[f64; 3]; 3] {
let (sx, cx) = x.sin_cos();
let (sy, cy) = y.sin_cos();
let (sz, cz) = z.sin_cos();
[
[cy * cz, sx * sy * cz - cx * sz, cx * sy * cz + sx * sz],
[cy * sz, sx * sy * sz + cx * cz, cx * sy * sz - sx * cz],
[-sy, sx * cy, cx * cy],
]
}
/// The point cloud definition a PointCloudEx names.
pub(crate) fn definition<'a>(
document: &'a CadDocument,
data: &PointCloudExData,
) -> Option<&'a PointCloudDefinition> {
match document.objects.get(&data.definition_handle)? {
ObjectType::ClassObject(object) => match &object.data {
ClassObjectData::PointCloudDefinitionEx(definition)
| ClassObjectData::PointCloudDefinition(definition) => Some(definition),
_ => None,
},
_ => None,
}
}
type Sources = Mutex<FxHashMap<String, PathBuf>>;
fn sources() -> &'static Sources {
static SOURCES: OnceLock<Sources> = OnceLock::new();
SOURCES.get_or_init(Default::default)
}
/// Where a stored (relative or moved) scan path was found for an opened
/// drawing — the definition names the stored path.
// ponytail: keyed by the stored path alone, like underlay sources; two open
// drawings naming the same relative path in different folders share one.
pub(crate) fn register_source(stored: &str, resolved: PathBuf) {
if let Ok(mut map) = sources().lock() {
map.insert(stored.to_string(), resolved);
}
}
/// The scan file a PointCloudEx shows, when it exists: the stored path as
/// 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;
}
let path = PathBuf::from(stored.replace('\\', "/"));
if path.is_absolute() && path.is_file() {
return Some(path);
}
if let Some(found) = sources().lock().ok().and_then(|map| map.get(stored).cloned()) {
if found.is_file() {
return Some(found);
}
}
path.is_file().then_some(path)
}
/// One drawn point: position as two f32 (high + low, like every other
/// renderer vertex), its colour and its world normal (zero without normals;
/// lit only then).
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct PointInstance {
pub pos: [f32; 3],
pub pos_low: [f32; 3],
pub color: [u8; 4],
pub normal: [i8; 4],
}
/// The points one cloud shows, placed in the drawing.
#[derive(Debug)]
pub struct PlacedCloud {
/// Identity of the source file version, placement and crops: equal keys
/// hold equal points, so GPU buffers are reused by key.
pub key: u64,
pub instances: Vec<PointInstance>,
}
/// Every cloud a view shows, with the POINTCLOUDPOINTSIZE they draw at.
#[derive(Debug, Default)]
pub struct PointCloudSet {
pub point_size: f32,
pub clouds: Vec<Arc<PlacedCloud>>,
}
/// POINTCLOUDPOINTSIZE in pixels (1–10, default 2).
pub(crate) fn point_size(document: &CadDocument) -> f32 {
crate::io::drawing_variable(document, "POINTCLOUDPOINTSIZE")
.and_then(|v| v.trim().parse::<i16>().ok())
.unwrap_or(2)
.clamp(1, 10) as f32
}
/// Whether the drawing defines any point cloud, so drawings without one
/// skip the render walk.
pub(crate) fn any_definition(document: &CadDocument) -> bool {
document.objects.values().any(|object| {
matches!(
object,
ObjectType::ClassObject(object)
if matches!(object.data, ClassObjectData::PointCloudDefinitionEx(_)
| ClassObjectData::PointCloudDefinition(_))
)
})
}
/// One scan's points in a cloud: its name (the file's), its identifier and
/// its index range.
#[derive(Debug, Clone)]
pub struct ScanRange {
pub name: String,
pub id: String,
pub range: std::ops::Range<usize>,
}
/// Stands in a hidden list for the cloud's unassigned points (region 0),
/// which are every point of a cloud without regions.
pub const UNASSIGNED_OFF: &str = "*UNASSIGNED";
/// What a cloud turns off: the identifiers of its hidden scans, and
/// [`UNASSIGNED_OFF`] when its unassigned points are hidden.
pub(crate) fn hidden(data: &PointCloudExData) -> Vec<String> {
let mut hidden = data.hidden_scans.clone();
if data.hidden_regions.contains(&0) {
hidden.push(UNASSIGNED_OFF.to_string());
}
hidden
}
/// Whether a cloud draws none of its points: its file is not found, or its
/// unassigned points or every scan are turned off.
pub(crate) fn shows_no_points(document: &CadDocument, data: &PointCloudExData) -> bool {
let Some(cloud) = resolve_source(document, data).and_then(|path| load(&path)) else {
return true;
};
data.hidden_regions.contains(&0)
|| (!cloud.scan_ranges.is_empty() && cloud.scan_ranges.iter().all(|s| data.hidden_scans.contains(&s.id)))
}
/// The points `data` shows, placed through its origin and axes and then
/// `transform` (a block instance), with its crops applied and the `hidden`
/// scans left out. `None` when the scan file is missing or unreadable.
pub(crate) fn placed(
document: &CadDocument,
data: &PointCloudExData,
object_color: [u8; 3],
transform: Option<&Transform>,
hidden: &[String],
) -> Option<Arc<PlacedCloud>> {
let cloud = load(&resolve_source(document, data)?)?;
let skipped: Vec<std::ops::Range<usize>> =
if hidden.iter().any(|h| h == UNASSIGNED_OFF) {
vec![0..cloud.positions.len()]
} else {
cloud.scan_ranges.iter().filter(|s| hidden.contains(&s.id)).map(|s| s.range.clone()).collect()
};
// The points shown: POINTCLOUDDENSITY per cent of POINTCLOUDPOINTMAX,
// in tenths by POINTCLOUDLOD, every n-th point of a larger cloud.
let shown = |name| setting(name).map_or(0, |s| setting_value(document, s));
let limit =
(shown("POINTCLOUDPOINTMAX") * shown("POINTCLOUDDENSITY") / 100 * shown("POINTCLOUDLOD") / 10).max(1) as usize;
let step = cloud.positions.len().div_ceil(limit).max(1);
let style = Stylization::of(document, data, object_color);
let mut origin = data.ucs_origin;
let mut axes = [data.ucs_x_direction, data.ucs_y_direction, data.ucs_z_direction];
if let Some(transform) = transform {
let placed = transform.apply(origin);
axes = axes.map(|axis| transform.apply(origin + axis) - placed);
origin = placed;
}
let crops: Vec<&PointCloudExCrop> = if data.show_cropping {
data.croppings
.iter()
.filter(|crop| match crop.crop_type {
1 | 2 => crop.points.len() >= 3,
3 => crop.points.len() >= 2,
_ => false,
})
.collect()
} else {
Vec::new()
};
let mut h = rustc_hash::FxHasher::default();
cloud.id.hash(&mut h);
let bits = |v: &Vector3| [v.x, v.y, v.z].map(f64::to_bits);
bits(&origin).hash(&mut h);
axes.iter().map(bits).for_each(|v| v.hash(&mut h));
for crop in &crops {
let frame = [crop.plane, crop.x_direction, crop.y_direction];
frame.iter().chain(&crop.points).map(bits).for_each(|v| v.hash(&mut h));
(crop.inside, crop.inverted).hash(&mut h);
}
step.hash(&mut h);
style.hash(&mut h);
skipped.hash(&mut h);
let key = h.finish();
type Placed = Mutex<FxHashMap<u64, Weak<PlacedCloud>>>;
static PLACED: OnceLock<Placed> = OnceLock::new();
let placed_cache = PLACED.get_or_init(Default::default);
if let Some(hit) = placed_cache.lock().ok()?.get(&key).and_then(Weak::upgrade) {
return Some(hit);
}
let started = crate::perf::enabled().then(iced::time::Instant::now);
let world = |p: &[f64; 3]| origin + axes[0] * p[0] + axes[1] * p[1] + axes[2] * p[2];
// Elevation runs over the cloud's own height unless a fixed range is set.
let (low, high) = if data.elevation_apply_to_fixed_range {
(data.elevation_min, data.elevation_max)
} else {
cloud.positions.iter().fold((f64::MAX, f64::MIN), |(lo, hi), p| {
let z = world(p).z;
(lo.min(z), hi.max(z))
})
};
let scan_color = |i: usize| {
if cloud.has_rgb {
cloud.colors[i]
} else if cloud.has_intensity {
[cloud.intensity[i]; 3]
} else {
object_color
}
};
// A value outside a scheme's range takes the end colour, the scan
// colour, or is hidden (`None`), as the cloud's out-of-range choice says.
let along = |colors: &[[u8; 3]], t: f64, gradient: bool, out_of_range: i16, fixed: bool, i: usize| {
if fixed && !(0.0..=1.0).contains(&t) {
return match out_of_range {
0 => Some(ramp(colors, t, gradient)),
2 => None,
_ => Some(scan_color(i)),
};
}
Some(ramp(colors, t, gradient))
};
let color = |i: usize, z: f64| -> Option<[u8; 4]> {
let [r, g, b] = match &style {
Stylization::Object(c) => *c,
Stylization::Normal => match cloud.normals.get(i) {
Some(n) => {
let w = axes[0] * f64::from(n[0]) + axes[1] * f64::from(n[1]) + axes[2] * f64::from(n[2]);
let w = w / w.length().max(1e-12);
[w.x, w.y, w.z].map(|v| ((v + 1.0) * 127.5).round().clamp(0.0, 255.0) as u8)
}
None => object_color,
},
Stylization::Ramp { colors, gradient, intensity: true, range, out_of_range } => {
let percent = f64::from(cloud.intensity.get(i).copied().unwrap_or(0)) * 100.0 / 255.0;
let (from, to) = (range.0 as f64, range.1 as f64);
along(colors, (percent - from) / (to - from).max(1e-9), *gradient, *out_of_range, true, i)?
}
Stylization::Ramp { colors, gradient, out_of_range, .. } => along(
colors,
(z - low) / (high - low).max(1e-9),
*gradient,
*out_of_range,
data.elevation_apply_to_fixed_range,
i,
)?,
Stylization::Scan => scan_color(i),
};
Some([r, g, b, 255])
};
let mut instances = Vec::with_capacity(cloud.positions.len() / step + 1);
for (i, p) in cloud.positions.iter().enumerate().step_by(step) {
if skipped.iter().any(|r| r.contains(&i)) || !crops.iter().all(|crop| keeps(crop, p)) {
continue;
}
let w = world(p);
let Some(color) = color(i, w.z) else {
continue;
};
let pos = [w.x as f32, w.y as f32, w.z as f32];
let normal = cloud.normals.get(i).map_or([0; 4], |n| {
let v = axes[0] * f64::from(n[0]) + axes[1] * f64::from(n[1]) + axes[2] * f64::from(n[2]);
let v = v / v.length().max(1e-12);
[v.x, v.y, v.z, 0.0].map(|c| (c * 127.0).round() as i8)
});
instances.push(PointInstance {
pos,
pos_low: [
(w.x - pos[0] as f64) as f32,
(w.y - pos[1] as f64) as f32,
(w.z - pos[2] as f64) as f32,
],
color,
normal,
});
}
// Spread the points so any leading part of the list is an even sample:
// a real-time density draws only that part.
let instances = spread(instances);
crate::perf::record(format_args!(
"[perf] point-cloud-place {:>7.1}ms shown={}",
crate::perf::elapsed_ms(started),
instances.len(),
));
let placed = Arc::new(PlacedCloud { key, instances });
let mut cache = placed_cache.lock().ok()?;
cache.retain(|_, weak| weak.strong_count() > 0);
cache.insert(key, Arc::downgrade(&placed));
Some(placed)
}
/// The items in a stride order coprime to their count: every prefix samples
/// the whole list evenly.
fn spread<T: Copy>(items: Vec<T>) -> Vec<T> {
let n = items.len();
if n < 3 {
return items;
}
let gcd = |mut a: usize, mut b: usize| {
while b != 0 {
(a, b) = (b, a % b);
}
a
};
// Near the golden section, so consecutive picks land far apart.
let mut stride = (n as f64 * 0.618_033_988_75) as usize | 1;
while gcd(stride, n) != 1 {
stride += 2;
}
(0..n).map(|k| items[k * stride % n]).collect()
}
/// Whether a crop keeps point `p` (file coordinates). The crop is a prism
/// through the plane at `plane` spanned by its two directions; its polygon
/// is in that plane's coordinates. `inside` keeps the prism's interior and
/// `inverted` flips it.
fn keeps(crop: &PointCloudExCrop, p: &[f64; 3]) -> bool {
// Points and crop outline on the crop plane, along its normal.
let on_plane = |v: Vector3| {
let d = v - crop.plane;
Vector3::new(d.dot(&crop.x_direction), d.dot(&crop.y_direction), 0.0)
};
let Vector3 { x, y, .. } = on_plane(Vector3::new(p[0], p[1], p[2]));
let points: Vec<Vector3> = crop.points.iter().copied().map(on_plane).collect();
// A circle: its centre and a point on it.
if crop.crop_type == 3 {
let (c, e) = (points[0], points[1]);
let r2 = (e.x - c.x).powi(2) + (e.y - c.y).powi(2);
let inside = (x - c.x).powi(2) + (y - c.y).powi(2) <= r2;
return (inside == crop.inside) != crop.inverted;
}
let mut inside = false;
let mut j = points.len() - 1;
for i in 0..points.len() {
let (a, b) = (points[i], points[j]);
if (a.y > y) != (b.y > y) && x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x {
inside = !inside;
}
j = i;
}
(inside == crop.inside) != crop.inverted
}
/// How a cloud's points are coloured (its stylization).
#[derive(Hash)]
enum Stylization {
/// The scan's own colours (intensity as grey when it has none).
Scan,
/// The cloud's object colour.
Object([u8; 3]),
/// Each point's world normal as a colour, (n + 1) / 2 per component.
Normal,
/// A colour scheme over intensity (per cent, in the range given) or
/// elevation, blended or in bands; values out of range per
/// `out_of_range` (0 end colours, 1 scan colours, 2 hidden).
Ramp { colors: Vec<[u8; 3]>, gradient: bool, intensity: bool, range: (i64, i64), out_of_range: i16 },
}
impl Stylization {
fn of(document: &CadDocument, data: &PointCloudExData, object_color: [u8; 3]) -> Self {
let (scheme, intensity, gradient) = match data.stylization_type {
2 => return Self::Object(object_color),
3 => return Self::Normal,
5 => (&data.intensity_color_scheme, true, data.intensity_as_gradient),
4 => (&data.current_color_scheme, false, data.elevation_as_gradient),
// Classification has no data here: scan colours.
_ => return Self::Scan,
};
let colors = ramp_colors(document, scheme, if intensity { "Spectrum" } else { "Earth" });
let range = if intensity && data.intensity_max > data.intensity_min {
(i64::from(data.intensity_min), i64::from(data.intensity_max))
} else {
(0, 100)
};
let out_of_range =
if intensity { data.intensity_out_of_range_behavior } else { data.elevation_out_of_range_behavior };
Self::Ramp { colors, gradient, intensity, range: (range.0, range.1), out_of_range }
}
}
/// The colour schemes a new drawing's colour map carries, high end first.
const BUILTIN_RAMPS: [(&str, &[u32]); 7] = [
("Hydro", &[0x00bfff, 0x007ca5, 0x005f7f, 0x00394c, 0x00264c, 0x00134c, 0x00004c, 0x000026]),
("Grayscale", &[0xffffff, 0xcccccc, 0x999999, 0x666666, 0x333333, 0x000000]),
("Earth", &[0xffffc8, 0xf0d796, 0xe1af64, 0xd28732, 0xc35f00]),
("Blues", &[0x0000ff, 0x0000a5, 0x00007f, 0x00004c, 0x000026]),
("Greens", &[0x00ff00, 0x00a500, 0x007f00, 0x004c00, 0x002600]),
("Spectrum", &[0xff00ff, 0x0000ff, 0x00ffff, 0x00ff00, 0xffff00, 0xff0000]),
("Reds", &[0xff0000, 0xa50000, 0x7f0000, 0x4c0000, 0x260000]),
];
fn rgb(color: u32) -> [u8; 3] {
[(color >> 16) as u8, (color >> 8) as u8, color as u8]
}
/// The colour schemes the drawing offers (identifier, name, colours high end
/// first): its colour map's, or the built-in ones.
pub(crate) fn schemes(document: &CadDocument) -> Vec<(String, String, Vec<[u8; 3]>)> {
for object in document.objects.values() {
let ObjectType::ClassObject(object) = object else {
continue;
};
let ClassObjectData::PointCloudColorMap(map) = &object.data else {
continue;
};
if !map.color_ramps.is_empty() {
return map
.color_ramps
.iter()
.map(|r| {
let colors = r.colors.iter().filter(|c| c.visible).map(|c| rgb(c.color as u32)).collect();
(r.id.clone(), r.name.clone(), colors)
})
.collect();
}
}
BUILTIN_RAMPS
.iter()
.map(|(name, colors)| {
let id = crate::entities::extended::point_cloud_ramp_id(name).to_string();
(id, name.to_string(), colors.iter().map(|c| rgb(*c)).collect())
})
.collect()
}
/// The schemes new intensity and elevation colouring starts from: the
/// colour map's defaults, else Spectrum and Earth.
pub(crate) fn default_schemes(document: &CadDocument) -> (String, String) {
let map = document.objects.values().find_map(|object| match object {
ObjectType::ClassObject(object) => match &object.data {
ClassObjectData::PointCloudColorMap(map) => Some(map),
_ => None,
},
_ => None,
});
let pick = |stored: Option<&String>, name: &str| {
stored
.filter(|s| !s.is_empty())
.cloned()
.unwrap_or_else(|| crate::entities::extended::point_cloud_ramp_id(name).to_string())
};
(
pick(map.map(|m| &m.default_intensity_scheme), "Spectrum"),
pick(map.map(|m| &m.default_elevation_scheme), "Earth"),
)
}
/// A scheme's colours: from the drawing's colour map when it has the scheme,
/// else the built-in one of that name (or `fallback` for an unset scheme).
fn ramp_colors(document: &CadDocument, id: &str, fallback: &str) -> Vec<[u8; 3]> {
for object in document.objects.values() {
let ObjectType::ClassObject(object) = object else {
continue;
};
let ClassObjectData::PointCloudColorMap(map) = &object.data else {
continue;
};
if let Some(found) = map.color_ramps.iter().find(|r| !id.is_empty() && r.id.eq_ignore_ascii_case(id)) {
let colors: Vec<[u8; 3]> =
found.colors.iter().filter(|c| c.visible).map(|c| rgb(c.color as u32)).collect();
if !colors.is_empty() {
return colors;
}
}
}
let name = crate::entities::extended::POINT_CLOUD_RAMPS
.iter()
.find(|(_, guid)| guid.eq_ignore_ascii_case(id))
.map_or(fallback, |(name, _)| name);
BUILTIN_RAMPS
.iter()
.find(|(ramp, _)| *ramp == name)
.map_or_else(|| vec![[255; 3]], |(_, colors)| colors.iter().map(|c| rgb(*c)).collect())
}
/// The colour at `t` (0–1, clamped) along a scheme: blended between its
/// colours, or the band `t` falls in. A scheme lists its high end first
/// (Spectrum: magenta at the highest value, red at the lowest).
fn ramp(colors: &[[u8; 3]], t: f64, gradient: bool) -> [u8; 3] {
let t = 1.0 - if t.is_finite() { t.clamp(0.0, 1.0) } else { 0.0 };
if colors.len() < 2 {
return colors.first().copied().unwrap_or([255; 3]);
}
if !gradient {
return colors[((t * colors.len() as f64) as usize).min(colors.len() - 1)];
}
let x = t * (colors.len() - 1) as f64;
let k = (x as usize).min(colors.len() - 2);
let f = x - k as f64;
let (a, b) = (colors[k], colors[k + 1]);
[0, 1, 2].map(|c| (f64::from(a[c]) + (f64::from(b[c]) - f64::from(a[c])) * f).round() as u8)
}
/// A point cloud system variable: its default and the integers it takes.
#[derive(Clone, Copy, Debug)]
pub(crate) struct Setting {
pub name: &'static str,
pub default: i64,
min: i64,
max: i64,
}
/// The reference's defaults and ranges.
const SETTINGS: [Setting; 15] = [
Setting { name: "POINTCLOUDDENSITY", default: 15, min: 1, max: 100 },
Setting { name: "POINTCLOUDRTDENSITY", default: 5, min: 1, max: 100 },
Setting { name: "POINTCLOUDPOINTSIZE", default: 2, min: 1, max: 10 },
Setting { name: "POINTCLOUDLOD", default: 10, min: 1, max: 10 },
Setting { name: "POINTCLOUDLOCK", default: 0, min: 0, max: 1 },
Setting { name: "POINTCLOUDAUTOUPDATE", default: 0, min: 0, max: 1 },
Setting { name: "POINTCLOUDVISRETAIN", default: 1, min: 0, max: 1 },
Setting { name: "POINTCLOUDSHADING", default: 0, min: 0, max: 1 },
Setting { name: "POINTCLOUD2DVSDISPLAY", default: 0, min: 0, max: 1 },
Setting { name: "POINTCLOUDLIGHTSOURCE", default: 0, min: 0, max: 1 },
Setting { name: "POINTCLOUDLIGHTING", default: 0, min: 0, max: 2 },
Setting { name: "POINTCLOUDBOUNDARY", default: 1, min: 0, max: 2 },
Setting { name: "POINTCLOUDCACHESIZE", default: 512, min: 0, max: 32000 },
Setting { name: "POINTCLOUDPOINTMAX", default: 10_000_000, min: 1_000_000, max: 25_000_000 },
Setting { name: "POINTCLOUDPOINTMAXLEGACY", default: 1_500_000, min: 1_500_000, max: 10_000_000 },
];
/// How a value was refused: the lines to show, and whether the prompt asks
/// again (a small range) or the command ends (the point and cache limits).
pub(crate) struct Refusal {
pub lines: Vec<String>,
pub ask_again: bool,
}
impl Setting {
pub(crate) fn check(&self, text: &str) -> Result<i64, Refusal> {
let Ok(value) = text.trim().parse::<i64>() else {
return Err(Refusal { lines: vec!["Requires an integer value.".to_string()], ask_again: true });
};
if (self.min..=self.max).contains(&value) {
return Ok(value);
}
Err(if self.max > 100 {
Refusal {
lines: vec![format!("Cannot set {} to that value.", self.name), "*Invalid*".to_string()],
ask_again: false,
}
} else if self.min == 0 && self.max == 1 {
Refusal { lines: vec!["Requires 0 or 1 only.".to_string()], ask_again: true }
} else {
Refusal {
lines: vec![format!("Requires an integer between {} and {}.", self.min, self.max)],
ask_again: true,
}
})
}
}
pub(crate) fn setting(name: &str) -> Option<Setting> {
SETTINGS.iter().copied().find(|setting| setting.name.eq_ignore_ascii_case(name))
}
/// A setting's value in the drawing, or its default.
pub(crate) fn setting_value(document: &CadDocument, setting: Setting) -> i64 {
crate::io::drawing_variable(document, setting.name)
.and_then(|value| value.trim().parse::<i64>().ok())
.unwrap_or(setting.default)
}
+100
View File
@@ -0,0 +1,100 @@
// Scan project (.rcp) reader: a zip holding one XML whose
// `<VoxelTreeRunTime>` entries list the project's scans — each with its
// transform (Translation / Rotation / Scale), visibility and the .rcs path
// (`RelativePath` against the project's folder, `Path` absolute).
use std::io::{Cursor, Read};
use std::path::{Path, PathBuf};
/// One visible scan of the project.
pub struct ScanEntry {
pub path: PathBuf,
/// The scan's identifier ("{…}").
pub id: String,
pub translation: [f64; 3],
pub rotation: [f64; 3],
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(""));
for i in 0..archive.len() {
let file = archive.by_index(i).ok()?;
if !file.name().to_ascii_lowercase().ends_with(".xml") {
continue;
}
let mut text = String::new();
// The project XML is small; the cap keeps a crafted archive in check.
if file.take(64 << 20).read_to_string(&mut text).is_err() {
continue;
}
let Ok(doc) = roxmltree::Document::parse(text.trim_start_matches('\u{feff}')) else {
continue;
};
let nodes: Vec<_> = doc
.descendants()
.filter(|n| n.has_tag_name("VoxelTreeRunTime"))
.collect();
if nodes.is_empty() {
continue;
}
return Some(nodes.into_iter().filter_map(|node| entry(node, folder)).collect());
}
None
}
fn child<'a, 'i>(node: roxmltree::Node<'a, 'i>, tag: &str) -> Option<roxmltree::Node<'a, 'i>> {
node.children().find(|n| n.has_tag_name(tag))
}
fn value<'a>(node: roxmltree::Node<'a, '_>, tag: &str) -> Option<&'a str> {
child(node, tag)?.attribute("Value")
}
fn xyz(node: roxmltree::Node, tag: &str, default: f64) -> [f64; 3] {
let n = child(node, tag);
["x", "y", "z"].map(|a| {
n.and_then(|n| n.attribute(a))
.and_then(|v| v.parse::<f64>().ok())
.filter(|v| v.is_finite())
.unwrap_or(default)
})
}
fn entry(node: roxmltree::Node, folder: &Path) -> Option<ScanEntry> {
if value(node, "Visible") == Some("0") {
return None;
}
let relative = value(node, "RelativePath")
.filter(|p| !p.is_empty())
.map(|p| folder.join(p.replace('\\', "/")))
.filter(|p| p.is_file());
let path = relative.or_else(|| {
value(node, "Path")
.map(PathBuf::from)
.filter(|p| p.is_file())
})?;
Some(ScanEntry {
path,
id: node.attribute("Id").unwrap_or_default().to_string(),
translation: xyz(node, "Translation", 0.0),
rotation: xyz(node, "Rotation", 0.0),
scale: xyz(node, "Scale", 1.0),
})
}
+166
View File
@@ -0,0 +1,166 @@
// Indexed scan (.rcs, "ADOCT" version 3) point reader.
//
// Layout (little-endian):
// 0x10 3 × f64 scan translation T
// 0x28 3 × f64 scan rotation
// 0x40 3 × f64 scan scale
// 0xE0 3 × u8 has RGB, has normals, has intensity
// 0x129 u32 section count, then 24-byte entries at 0x12D:
// u64 type, u64 offset, u64 size
// Section 2 is the node directory: u64 node count, then 368-byte records
// (cube min at +0, levels +96, point count +100, block size +104, per-level
// cell counts +108 and point counts +236). Section 1 holds the node blocks
// back to back: the node's octree index (u32 entries), then 16-byte point
// records — a u64 with three 18-bit millimetre offsets from the node cube's
// min corner, the intensity byte, then blue, green, red.
// The octree index is only needed for spatial queries: every record carries
// its own position.
/// One scan, in its own (untransformed) frame.
pub struct Scan {
/// Point positions before the scan transform.
pub local: Vec<[f64; 3]>,
pub colors: Vec<[u8; 3]>,
pub intensity: Vec<u8>,
/// Unit normals (scan frame).
pub normals: Vec<[f32; 3]>,
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],
/// The scan's identifier ("{…}"), as its project and a cloud's hidden
/// scans name it.
pub id: String,
}
const NODE_RECORD: usize = 368;
fn u32_at(b: &[u8], at: usize) -> Option<u32> {
Some(u32::from_le_bytes(b.get(at..at.checked_add(4)?)?.try_into().ok()?))
}
fn u64_at(b: &[u8], at: usize) -> Option<u64> {
Some(u64::from_le_bytes(b.get(at..at.checked_add(8)?)?.try_into().ok()?))
}
fn f64_at(b: &[u8], at: usize) -> Option<f64> {
u64_at(b, at).map(f64::from_bits).filter(|v| v.is_finite())
}
fn vec3_at(b: &[u8], at: usize) -> Option<[f64; 3]> {
Some([f64_at(b, at)?, f64_at(b, at + 8)?, f64_at(b, at + 16)?])
}
/// A normal from its cube-map code: six faces of 52 × 52 equal-angle
/// cells. The face gives the main axis and its sign; the cell the other two
/// components, which flip with the face.
fn normal(code: u16) -> [f32; 3] {
let code = usize::from(code);
if code >= 6 * 52 * 52 {
return [0.0, 0.0, 1.0];
}
let (face, cell) = (code / 2704, code % 2704);
let tan = |i: usize| (((i as f64 + 0.5) / 26.0 - 1.0) * std::f64::consts::FRAC_PI_4).tan();
let axis = face >> 1;
let (b, c) = match axis {
0 => (1, 2),
1 => (0, 2),
_ => (0, 1),
};
let sign = if face % 2 == 0 { 1.0 } else { -1.0 };
let mut n = [0.0f64; 3];
n[axis] = sign;
n[b] = sign * tan(cell % 52);
n[c] = sign * tan(cell / 52);
let length = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
n.map(|v| (v / length) as f32)
}
/// The identifier the header carries after its flags, NUL-terminated.
fn header_id(b: &[u8]) -> String {
let tail = b.get(0xE3..b.len().min(0xE3 + 64)).unwrap_or_default();
let text = &tail[..tail.iter().position(|c| *c == 0).unwrap_or(tail.len())];
match std::str::from_utf8(text) {
Ok(id) if id.starts_with('{') && id.ends_with('}') => id.to_string(),
_ => String::new(),
}
}
pub fn decode(b: &[u8]) -> Option<Scan> {
if b.get(0..5)? != b"ADOCT" {
return None;
}
let translation = vec3_at(b, 0x10)?;
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;
let (mut directory, mut data) = (None, None);
for k in 0..sections.min(64) {
let at = 0x12D + k * 24;
let offset = usize::try_from(u64_at(b, at + 8)?).ok()?;
match u64_at(b, at)? {
1 => data = Some(offset),
2 => directory = Some(offset),
_ => {}
}
}
let (directory, mut block) = (directory?, data?);
let nodes = usize::try_from(u64_at(b, directory)?).ok()?;
// Every node needs its directory record; a count past the file is corrupt.
if nodes.checked_mul(NODE_RECORD)? > b.len() {
return None;
}
let mut scan = Scan {
local: Vec::new(),
colors: Vec::new(),
intensity: Vec::new(),
normals: Vec::new(),
has_rgb,
has_normals,
has_intensity,
translation,
rotation,
scale,
bounds,
id: header_id(b),
};
for node in 0..nodes {
let record = directory + 8 + node * NODE_RECORD;
let cube_min = vec3_at(b, record)?;
let points = u32_at(b, record + 100)? as usize;
let size = u32_at(b, record + 104)? as usize;
let mut entries = points;
for level in 0..32 {
entries = entries.checked_add(u32_at(b, record + 108 + level * 4)? as usize)?;
}
if entries.checked_mul(4)?.checked_add(points.checked_mul(16)?)? != size {
return None;
}
let records = block + entries * 4;
let end = block.checked_add(size)?;
let bytes = b.get(records..end)?;
scan.local.reserve(points);
scan.colors.reserve(points);
scan.intensity.reserve(points);
for rec in bytes.chunks_exact(16) {
let v = u64::from_le_bytes(rec[0..8].try_into().ok()?);
let q = |shift: u32| ((v >> shift) & 0x3FFFF) as f64 * 1e-3;
scan.local.push([cube_min[0] + q(0), cube_min[1] + q(18), cube_min[2] + q(36)]);
scan.intensity.push(rec[8]);
scan.colors.push([rec[11], rec[10], rec[9]]);
scan.normals.push(normal(u16::from_le_bytes([rec[12], rec[13]])));
}
block = end;
}
Some(scan)
}
+88 -32
View File
@@ -29,6 +29,71 @@ fn marker_screen_position(mut point: Point, shape: GripShape) -> Point {
point point
} }
/// Screen length of a move-gizmo arrow.
pub const GIZMO_AXIS_PX: f32 = 72.0;
pub fn gizmo_axis(k: u8) -> DVec3 {
[DVec3::X, DVec3::Y, DVec3::Z][k as usize % 3]
}
pub fn gizmo_plane_axes(k: u8) -> (u8, u8) {
[(0, 1), (1, 2), (2, 0)][k as usize % 3]
}
/// Unit screen directions of the three gizmo axes from `center`; an axis
/// seen nearly end-on is left out.
pub fn gizmo_screen_axes(
center: Vec2,
world: DVec3,
project: impl Fn(DVec3) -> Option<Vec2>,
) -> [Option<Vec2>; 3] {
let spans = [0, 1, 2].map(|k| project(world + gizmo_axis(k)).map(|p| p - center));
let longest = spans.iter().flatten().map(|v| v.length()).fold(0.0, f32::max);
spans.map(|span| span.filter(|v| longest > 0.0 && v.length() > 0.15 * longest).map(|v| v.normalize()))
}
/// Marker position (and direction) of a grip whose centre projects to
/// `projected`: gizmo grips sit off their centre, others keep their offset.
fn placed_marker(
g: &GripDef,
projected: Option<Vec2>,
project: impl Fn(DVec3) -> Option<Vec2>,
) -> Option<(Vec2, Option<[f32; 2]>)> {
let p = projected?;
match g.shape {
GripShape::GizmoAxis(k) => {
let d = gizmo_screen_axes(p, g.world, project)[k as usize % 3]?;
Some((p + d * GIZMO_AXIS_PX, Some([d.x, -d.y])))
}
GripShape::GizmoPlane(k) => {
let axes = gizmo_screen_axes(p, g.world, project);
let (a, b) = gizmo_plane_axes(k);
let (a, b) = (axes[a as usize]?, axes[b as usize]?);
Some((p + (a + b) * 0.3 * GIZMO_AXIS_PX, None))
}
_ => {
let dir = g.dir.and_then(|dir| Some(marker_direction(p, project(g.world + dir)?)));
let s = marker_screen_position(Point::new(p.x, p.y), g.shape);
Some((Vec2::new(s.x, s.y), dir))
}
}
}
/// Pixel distance from `cursor` to a marker; a gizmo arrow counts along its
/// whole shaft, not only at its tip.
fn marker_distance(shape: GripShape, screen: Vec2, dir: Option<[f32; 2]>, cursor: Point) -> f32 {
let cursor = Vec2::new(cursor.x, cursor.y);
match (shape, dir) {
(GripShape::GizmoAxis(_), Some([dx, dy])) => {
let d = Vec2::new(dx, -dy);
let start = screen - d * (GIZMO_AXIS_PX - 14.0);
let t = (cursor - start).dot(d).clamp(0.0, GIZMO_AXIS_PX - 14.0);
cursor.distance(start + d * t)
}
_ => cursor.distance(screen),
}
}
// ── Active drag state ───────────────────────────────────────────────────── // ── Active drag state ─────────────────────────────────────────────────────
/// Stored on `OpenCADStudio` while a grip is being dragged. /// Stored on `OpenCADStudio` while a grip is being dragged.
@@ -50,6 +115,12 @@ pub struct GripEdit {
pub targets: Vec<GripTarget>, pub targets: Vec<GripTarget>,
/// Opposite corner and local width/height axes for rectangle corner resize. /// Opposite corner and local width/height axes for rectangle corner resize.
pub rectangle_frame: Option<(DVec3, DVec3, DVec3)>, pub rectangle_frame: Option<(DVec3, DVec3, DVec3)>,
/// Normal of the plane a move-gizmo square drags in.
pub plane: Option<DVec3>,
/// Move gizmo: offset from the gizmo centre to where its part was
/// grabbed, taken on the first move so the drag does not jump.
pub grab: Option<DVec3>,
pub gizmo: bool,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -108,6 +179,9 @@ impl GripEdit {
last_world: world, last_world: world,
}], }],
rectangle_frame: None, rectangle_frame: None,
plane: None,
grab: None,
gizmo: false,
} }
} }
@@ -181,18 +255,11 @@ pub fn grips_to_screen(
// Project from the f64 grip position via the relative-to-eye path so // Project from the f64 grip position via the relative-to-eye path so
// the grip stays glued to the wire at UTM-scale coordinates (an // the grip stays glued to the wire at UTM-scale coordinates (an
// `as_vec3` cast first would quantize it ~0.5 m off at high zoom). // `as_vec3` cast first would quantize it ~0.5 m off at high zoom).
let projected = camera.project(g.world, bounds); let project = |world: DVec3| camera.project(world, bounds);
let screen = match projected { let (screen, dir) = match placed_marker(g, project(g.world), project) {
Some(p) => Point::new(bounds.x + p.x, bounds.y + p.y), Some((p, dir)) => (Point::new(bounds.x + p.x, bounds.y + p.y), dir),
None => Point::new(f32::NAN, f32::NAN), None => (Point::new(f32::NAN, f32::NAN), None),
}; };
let dir = g.dir.and_then(|dir| {
Some(marker_direction(
projected?,
camera.project(g.world + dir, bounds)?,
))
});
let screen = marker_screen_position(screen, g.shape);
(g.id, screen, g.is_midpoint, g.shape, dir) (g.id, screen, g.is_midpoint, g.shape, dir)
}) })
.collect() .collect()
@@ -274,13 +341,11 @@ pub fn find_hit_grip(
let mut best: Option<(usize, usize, bool, DVec3)> = None; let mut best: Option<(usize, usize, bool, DVec3)> = None;
for (index, g) in grips.iter().enumerate() { for (index, g) in grips.iter().enumerate() {
let Some(screen) = camera.project(g.world, bounds) else { let project = |world: DVec3| camera.project(world, bounds);
let Some((screen, dir)) = placed_marker(g, project(g.world), project) else {
continue; continue;
}; };
let screen = marker_screen_position(Point::new(screen.x, screen.y), g.shape); let d = marker_distance(g.shape, screen, dir, cursor);
let dx = screen.x - cursor.x;
let dy = screen.y - cursor.y;
let d = (dx * dx + dy * dy).sqrt();
if d < best_dist { if d < best_dist {
best_dist = d; best_dist = d;
best = Some((index, g.id, g.is_midpoint, g.world)); best = Some((index, g.id, g.is_midpoint, g.world));
@@ -320,18 +385,11 @@ pub fn grips_to_screen_rte(
grips grips
.iter() .iter()
.map(|g| { .map(|g| {
let projected = project_rte(g.world, view_rot, eye, bounds); let project = |world: DVec3| project_rte(world, view_rot, eye, bounds);
let screen = match projected { let (screen, dir) = match placed_marker(g, project(g.world), project) {
Some(p) => Point::new(bounds.x + p.x, bounds.y + p.y), Some((p, dir)) => (Point::new(bounds.x + p.x, bounds.y + p.y), dir),
None => Point::new(f32::NAN, f32::NAN), None => (Point::new(f32::NAN, f32::NAN), None),
}; };
let dir = g.dir.and_then(|dir| {
Some(marker_direction(
projected?,
project_rte(g.world + dir, view_rot, eye, bounds)?,
))
});
let screen = marker_screen_position(screen, g.shape);
(g.id, screen, g.is_midpoint, g.shape, dir) (g.id, screen, g.is_midpoint, g.shape, dir)
}) })
.collect() .collect()
@@ -350,13 +408,11 @@ pub fn find_hit_grip_rte(
let mut best: Option<(usize, usize, bool, DVec3)> = None; let mut best: Option<(usize, usize, bool, DVec3)> = None;
for (index, g) in grips.iter().enumerate() { for (index, g) in grips.iter().enumerate() {
let Some(screen) = project_rte(g.world, view_rot, eye, bounds) else { let project = |world: DVec3| project_rte(world, view_rot, eye, bounds);
let Some((screen, dir)) = placed_marker(g, project(g.world), project) else {
continue; continue;
}; };
let screen = marker_screen_position(Point::new(screen.x, screen.y), g.shape); let d = marker_distance(g.shape, screen, dir, cursor);
let dx = screen.x - cursor.x;
let dy = screen.y - cursor.y;
let d = (dx * dx + dy * dy).sqrt();
if d < best_dist { if d < best_dist {
best_dist = d; best_dist = d;
best = Some((index, g.id, g.is_midpoint, g.world)); best = Some((index, g.id, g.is_midpoint, g.world));
+115 -2
View File
@@ -26,6 +26,7 @@ pub mod gpu_upload;
pub mod hatch_gpu; pub mod hatch_gpu;
pub mod wipeout_gpu; pub mod wipeout_gpu;
pub mod image_gpu; pub mod image_gpu;
pub mod point_cloud_gpu;
pub mod mesh_gpu; pub mod mesh_gpu;
pub mod text_gpu; pub mod text_gpu;
pub mod uniforms; pub mod uniforms;
@@ -136,6 +137,7 @@ pub struct Pipeline {
/// private backends behind one upload/LOD/draw lifecycle. /// private backends behind one upload/LOD/draw lifecycle.
hatch_gpu: hatch_gpu::HatchGpu, hatch_gpu: hatch_gpu::HatchGpu,
image_pipeline: wgpu::RenderPipeline, image_pipeline: wgpu::RenderPipeline,
point_cloud_pipeline: wgpu::RenderPipeline,
/// SDF text-quad pipeline (Phase 2b): draws per-glyph quads sampling the /// SDF text-quad pipeline (Phase 2b): draws per-glyph quads sampling the
/// shared glyph atlas. Fed only when `OCS_TEXT_SDF` is set (else no verts). /// shared glyph atlas. Fed only when `OCS_TEXT_SDF` is set (else no verts).
text_pipeline: wgpu::RenderPipeline, text_pipeline: wgpu::RenderPipeline,
@@ -313,6 +315,7 @@ pub struct Pipeline {
/// viewport rect. Recomputed by `compute_wipeout_lod`. /// viewport rect. Recomputed by `compute_wipeout_lod`.
wipeout_skip_flags: Vec<bool>, wipeout_skip_flags: Vec<bool>,
gpu_images: Vec<ImageGpu>, gpu_images: Vec<ImageGpu>,
gpu_point_clouds: point_cloud_gpu::PointCloudGpu,
/// Batched mesh geometry — every solid's LOD0 concatenated into a few large /// Batched mesh geometry — every solid's LOD0 concatenated into a few large
/// buffers so the whole set draws in a handful of calls instead of one per /// buffers so the whole set draws in a handful of calls instead of one per
/// solid. Hover / selection never re-pack it. /// solid. Hover / selection never re-pack it.
@@ -344,6 +347,8 @@ pub struct Pipeline {
pub cached_preview_hatch_source: Option<std::sync::Arc<Vec<HatchModel>>>, pub cached_preview_hatch_source: Option<std::sync::Arc<Vec<HatchModel>>>,
pub cached_wipeout_source: Option<std::sync::Arc<Vec<HatchModel>>>, pub cached_wipeout_source: Option<std::sync::Arc<Vec<HatchModel>>>,
pub cached_image_source: Option<std::sync::Arc<Vec<ImageModel>>>, pub cached_image_source: Option<std::sync::Arc<Vec<ImageModel>>>,
pub cached_point_cloud_source:
Option<std::sync::Arc<crate::scene::model::point_cloud::PointCloudSet>>,
pub cached_text_source: Option<std::sync::Arc<Vec<text_gpu::TextVertex>>>, pub cached_text_source: Option<std::sync::Arc<Vec<text_gpu::TextVertex>>>,
pub cached_annotation_highlight_source: Option<std::sync::Arc<Vec<WireModel>>>, pub cached_annotation_highlight_source: Option<std::sync::Arc<Vec<WireModel>>>,
pub cached_mesh_source: Option<std::sync::Arc<Vec<MeshLodSet>>>, pub cached_mesh_source: Option<std::sync::Arc<Vec<MeshLodSet>>>,
@@ -2109,6 +2114,61 @@ impl Pipeline {
cache: None, cache: None,
}); });
// ── Point cloud sprites ────────────────────────────────────────────
let point_cloud_bgl1 = point_cloud_gpu::params_layout(device);
let point_cloud_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("point_cloud.pipeline_layout"),
bind_group_layouts: &[&frame_bgl, &point_cloud_bgl1].map(Some),
immediate_size: 0,
});
let point_cloud_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("point_cloud.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/shaders/point_cloud.wgsl"
)))),
});
let point_cloud_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("point_cloud.pipeline"),
layout: Some(&point_cloud_layout),
vertex: wgpu::VertexState {
module: &point_cloud_shader,
entry_point: Some("vs_main"),
buffers: &[point_cloud_gpu::instance_layout()],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: MSAA_SAMPLES,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &point_cloud_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview_mask: None,
cache: None,
});
let gpu_point_clouds = point_cloud_gpu::PointCloudGpu::new(device, &point_cloud_bgl1);
// ── Text (SDF glyph quads) ───────────────────────────────────────── // ── Text (SDF glyph quads) ─────────────────────────────────────────
let text_atlas_bgl = text_gpu::TextAtlasGpu::bind_group_layout(device); let text_atlas_bgl = text_gpu::TextAtlasGpu::bind_group_layout(device);
let ( let (
@@ -2294,6 +2354,7 @@ impl Pipeline {
wipeout_pipeline, wipeout_pipeline,
hatch_gpu, hatch_gpu,
image_pipeline, image_pipeline,
point_cloud_pipeline,
text_pipeline, text_pipeline,
block_text_pipeline, block_text_pipeline,
text_highlight_pipeline, text_highlight_pipeline,
@@ -2385,6 +2446,7 @@ impl Pipeline {
gpu_wipeouts: vec![], gpu_wipeouts: vec![],
wipeout_skip_flags: vec![], wipeout_skip_flags: vec![],
gpu_images: vec![], gpu_images: vec![],
gpu_point_clouds,
gpu_mesh_batch: vec![], gpu_mesh_batch: vec![],
gpu_mesh_dynamic: vec![], gpu_mesh_dynamic: vec![],
mesh_disabled_chunks: rustc_hash::FxHashSet::default(), mesh_disabled_chunks: rustc_hash::FxHashSet::default(),
@@ -2400,6 +2462,7 @@ impl Pipeline {
cached_preview_hatch_source: None, cached_preview_hatch_source: None,
cached_wipeout_source: None, cached_wipeout_source: None,
cached_image_source: None, cached_image_source: None,
cached_point_cloud_source: None,
cached_text_source: None, cached_text_source: None,
cached_annotation_highlight_source: None, cached_annotation_highlight_source: None,
cached_mesh_source: None, cached_mesh_source: None,
@@ -2750,7 +2813,9 @@ impl Pipeline {
); );
for &i in idxs { for &i in idxs {
if let Some(w) = wires.get(i as usize) { if let Some(w) = wires.get(i as usize) {
if !w.display_visible { // An unseen pick hull (a point cloud's extents box) shows while
// highlighted, as the reference does.
if !w.display_visible && (w.pick_tris.is_empty() || w.snap_only) {
continue; continue;
} }
selected_wires.push(w); selected_wires.push(w);
@@ -2762,7 +2827,9 @@ impl Pipeline {
if let Some(idxs) = self.wire_handle_index.get(&h.value()) { if let Some(idxs) = self.wire_handle_index.get(&h.value()) {
for &i in idxs { for &i in idxs {
if let Some(w) = wires.get(i as usize) { if let Some(w) = wires.get(i as usize) {
if !w.display_visible { // An unseen pick hull (a point cloud's extents box) shows while
// highlighted, as the reference does.
if !w.display_visible && (w.pick_tris.is_empty() || w.snap_only) {
continue; continue;
} }
hover_wires.push(w); hover_wires.push(w);
@@ -3776,6 +3843,19 @@ analytic={:.1} regular={:.1} blocks={:.1}",
self.gpu_images = ImageGpu::from_models(device, queue, images, &self.image_bgl1); self.gpu_images = ImageGpu::from_models(device, queue, images, &self.image_bgl1);
} }
pub fn upload_point_clouds(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
set: &crate::scene::model::point_cloud::PointCloudSet,
) {
self.gpu_point_clouds.upload(device, queue, set);
}
pub fn set_point_cloud_view(&mut self, queue: &wgpu::Queue, light: [f32; 12]) {
self.gpu_point_clouds.set_view(queue, light);
}
/// Upload the frame's SDF text-quad vertices, and (re)build the GPU glyph /// Upload the frame's SDF text-quad vertices, and (re)build the GPU glyph
/// atlas from the shared CPU atlas when it grew (new glyphs baked by the /// atlas from the shared CPU atlas when it grew (new glyphs baked by the
/// text collector). `verts` empty (flag off) leaves nothing to draw. /// text collector). `verts` empty (flag off) leaves nothing to draw.
@@ -4189,6 +4269,38 @@ analytic={:.1} regular={:.1} blocks={:.1}",
} }
} }
// ── Pass 3: point clouds ──────────────────────────────────────────
if !self.gpu_point_clouds.is_empty() {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("point_cloud.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: msaa,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }),
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_pipeline(&self.point_cloud_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
self.gpu_point_clouds.draw(&mut pass);
}
// ── Pass 4: solid meshes (batched) ──────────────────────────────── // ── Pass 4: solid meshes (batched) ────────────────────────────────
if !self.gpu_mesh_batch.is_empty() || !self.gpu_mesh_dynamic.is_empty() { if !self.gpu_mesh_batch.is_empty() || !self.gpu_mesh_dynamic.is_empty() {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
@@ -5043,6 +5155,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
self.cached_preview_hatch_source = None; self.cached_preview_hatch_source = None;
self.cached_wipeout_source = None; self.cached_wipeout_source = None;
self.cached_image_source = None; self.cached_image_source = None;
self.cached_point_cloud_source = None;
self.cached_text_source = None; self.cached_text_source = None;
self.cached_mesh_source = None; self.cached_mesh_source = None;
self.cached_face3d_source = None; self.cached_face3d_source = None;
+149
View File
@@ -0,0 +1,149 @@
// Point cloud GPU buffers — one instance buffer per placed cloud, drawn as
// screen-space square sprites (6 vertices per instance, no vertex buffer).
//
// Group 1 binding 0 — PointParams (sprite size in pixels, lighting mode and
// light source, the two light directions; 48 bytes).
//
// Buffers are keyed by `PlacedCloud::key`: a new render set re-uploads only
// the clouds whose points changed; the rest keep their buffers.
use crate::scene::model::point_cloud::{PointCloudSet, PointInstance};
use iced::wgpu;
pub fn instance_layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute {
offset: std::mem::offset_of!(PointInstance, pos) as u64,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(PointInstance, pos_low) as u64,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(PointInstance, color) as u64,
shader_location: 2,
format: wgpu::VertexFormat::Unorm8x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(PointInstance, normal) as u64,
shader_location: 3,
format: wgpu::VertexFormat::Snorm8x4,
},
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<PointInstance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: ATTRS,
}
}
pub fn params_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("point_cloud.bgl1"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
})
}
struct CloudBuffer {
key: u64,
buffer: wgpu::Buffer,
count: u32,
}
/// The resident point clouds of one viewport pipeline.
pub struct PointCloudGpu {
clouds: Vec<CloudBuffer>,
params: wgpu::Buffer,
bind_group: wgpu::BindGroup,
point_size: f32,
params_written: [f32; 12],
/// The share of each cloud's points drawn (real-time density).
fraction: f32,
}
impl PointCloudGpu {
pub fn new(device: &wgpu::Device, layout: &wgpu::BindGroupLayout) -> Self {
let params = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("point_cloud.params"),
size: 48,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("point_cloud.bind_group1"),
layout,
entries: &[wgpu::BindGroupEntry { binding: 0, resource: params.as_entire_binding() }],
});
Self {
clouds: Vec::new(),
params,
bind_group,
point_size: f32::NAN,
params_written: [f32::NAN; 12],
fraction: 1.0,
}
}
pub fn is_empty(&self) -> bool {
self.clouds.is_empty()
}
/// Make `set` resident, keeping the buffers of clouds it already had.
pub fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, set: &PointCloudSet) {
let mut old = std::mem::take(&mut self.clouds);
for cloud in set.clouds.iter().filter(|cloud| !cloud.instances.is_empty()) {
if let Some(at) = old.iter().position(|resident| resident.key == cloud.key) {
self.clouds.push(old.swap_remove(at));
continue;
}
self.clouds.push(CloudBuffer {
key: cloud.key,
buffer: super::gpu_upload::upload_buffer(
device,
queue,
"point_cloud.instances",
&cloud.instances,
wgpu::BufferUsages::VERTEX,
),
count: cloud.instances.len() as u32,
});
}
self.point_size = set.point_size;
}
/// The view's lighting (`[mode, source, fraction, _, light x, y, z, _]`)
/// with the sprite size, written when they change.
pub fn set_view(&mut self, queue: &wgpu::Queue, light: [f32; 12]) {
self.fraction = light[2].clamp(0.0, 1.0);
let mut params = light;
params[0] = self.point_size;
params[1] = light[0];
params[2] = light[1];
params[3] = 0.0;
if params != self.params_written {
self.params_written = params;
queue.write_buffer(&self.params, 0, bytemuck::cast_slice(&params));
}
}
pub fn draw<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>) {
pass.set_bind_group(1, &self.bind_group, &[]);
for cloud in &self.clouds {
pass.set_vertex_buffer(0, cloud.buffer.slice(..));
let count = ((cloud.count as f32 * self.fraction).ceil() as u32).clamp(1, cloud.count);
pass.draw(0..6, 0..count);
}
}
}
+58 -2
View File
@@ -165,6 +165,11 @@ pub struct ViewportData {
/// Wipeout fills — rendered in a separate pass AFTER wires. /// Wipeout fills — rendered in a separate pass AFTER wires.
pub(in crate::scene) wipeout_hatches: Arc<Vec<HatchModel>>, pub(in crate::scene) wipeout_hatches: Arc<Vec<HatchModel>>,
pub(in crate::scene) images: Arc<Vec<ImageModel>>, pub(in crate::scene) images: Arc<Vec<ImageModel>>,
/// Point cloud sprites; empty on the paper sheet and in 2D wireframe.
pub(in crate::scene) point_clouds: Arc<crate::scene::model::point_cloud::PointCloudSet>,
/// Point cloud lighting and density for this view: `[mode, source,
/// drawn fraction, _, light direction x, y, z, _]`.
pub(in crate::scene) point_cloud_light: [f32; 12],
pub(in crate::scene) meshes: Arc<Vec<MeshLodSet>>, pub(in crate::scene) meshes: Arc<Vec<MeshLodSet>>,
pub(in crate::scene) background_image: pub(in crate::scene) background_image:
Option<crate::scene::model::image_model::DecodedImage>, Option<crate::scene::model::image_model::DecodedImage>,
@@ -554,6 +559,15 @@ impl shader::Primitive for Primitive {
inner.upload_images(device, queue, &vp.images[..]); inner.upload_images(device, queue, &vp.images[..]);
inner.cached_image_source = Some(Arc::clone(&vp.images)); inner.cached_image_source = Some(Arc::clone(&vp.images));
} }
if inner
.cached_point_cloud_source
.as_ref()
.map_or(true, |source| !Arc::ptr_eq(source, &vp.point_clouds))
{
inner.upload_point_clouds(device, queue, &vp.point_clouds);
inner.cached_point_cloud_source = Some(Arc::clone(&vp.point_clouds));
}
inner.set_point_cloud_view(queue, vp.point_cloud_light);
if inner if inner
.cached_text_source .cached_text_source
.as_ref() .as_ref()
@@ -1411,6 +1425,13 @@ fn render_signature(vp: &ViewportData, placement: &PhysicalViewport) -> u64 {
std::sync::Arc::as_ptr(&vp.meshes) as usize std::sync::Arc::as_ptr(&vp.meshes) as usize
} }
.hash(&mut h); .hash(&mut h);
if vp.point_clouds.clouds.is_empty() {
std::ptr::null()
} else {
std::sync::Arc::as_ptr(&vp.point_clouds)
}
.hash(&mut h);
vp.point_cloud_light.map(f32::to_bits).hash(&mut h);
vp.geometry_epoch.hash(&mut h); vp.geometry_epoch.hash(&mut h);
vp.selection_generation.hash(&mut h); vp.selection_generation.hash(&mut h);
vp.selected_sig.hash(&mut h); vp.selected_sig.hash(&mut h);
@@ -4510,21 +4531,31 @@ impl Scene {
} else { } else {
Arc::new(Vec::new()) Arc::new(Vec::new())
}; };
// 2D wireframe draws no cloud points; the clouds show their extents
// and a message there instead (drawn with the live overlays).
let wireframe_2d = inst.render_mode == codec::entities::ViewportRenderMode::Wireframe2D;
let cloud_2d_wires = if wireframe_2d && show_live_overlay && !inst.paper_sheet {
self.point_cloud_2d_wires()
} else {
Vec::new()
};
let preview_wires = if !show_live_overlay let preview_wires = if !show_live_overlay
|| (self.interim_wire.is_none() || (self.interim_wire.is_none()
&& self.preview_wires.is_empty() && self.preview_wires.is_empty()
&& self.constraint_hover_wires.is_empty()) && self.constraint_hover_wires.is_empty()
&& cloud_2d_wires.is_empty())
{ {
Arc::new(Vec::new()) Arc::new(Vec::new())
} else { } else {
let mut v: Vec<WireModel> = Vec::with_capacity( let mut v: Vec<WireModel> = Vec::with_capacity(
self.preview_wires.len() + self.constraint_hover_wires.len() + 1, self.preview_wires.len() + self.constraint_hover_wires.len() + cloud_2d_wires.len() + 1,
); );
if let Some(iw) = &self.interim_wire { if let Some(iw) = &self.interim_wire {
v.push(iw.clone()); v.push(iw.clone());
} }
v.extend(self.preview_wires.iter().cloned()); v.extend(self.preview_wires.iter().cloned());
v.extend(self.constraint_hover_wires.iter().cloned()); v.extend(self.constraint_hover_wires.iter().cloned());
v.extend(cloud_2d_wires);
Arc::new(v) Arc::new(v)
}; };
let preview_hatches = if show_live_overlay { let preview_hatches = if show_live_overlay {
@@ -4720,6 +4751,7 @@ impl Scene {
} else { } else {
self.meshes_for_viewport(inst.handle, &vp_frozen) self.meshes_for_viewport(inst.handle, &vp_frozen)
}; };
let point_clouds = self.point_clouds_for_viewport(&vp_frozen, inst.paper_sheet || wireframe_2d);
// SDF text quads (behind OCS_TEXT_SDF). The glyph quads ride on each // SDF text quads (behind OCS_TEXT_SDF). The glyph quads ride on each
// entity's own wire (produced by the tessellator, transformed for // entity's own wire (produced by the tessellator, transformed for
@@ -4803,6 +4835,28 @@ impl Scene {
Arc::new(vec![]) Arc::new(vec![])
}; };
let navigating = !inst.paper_sheet && self.navigating_lod(); let navigating = !inst.paper_sheet && self.navigating_lod();
let point_cloud_light = {
use crate::scene::model::point_cloud::{setting, setting_value};
let value = |name| setting(name).map_or(0, |s| setting_value(&self.document, s)) as f32;
// Lit only in the shaded styles, as the reference.
let lighting = if flags.mesh_fill && !flags.hidden_line { value("POINTCLOUDLIGHTING") } else { 0.0 };
let source = value("POINTCLOUDLIGHTSOURCE");
// The lights follow the view (measured from the reference in
// view coordinates: right, up, toward the viewer): the default
// lighting is a key light from the upper left and a fill light
// from the lower right, the other source one light from above.
let view = |x: f32, y: f32, z: f32| {
(inst.camera.rotation * glam::Vec3::new(x, y, z)).normalize_or(glam::Vec3::Z)
};
let (first, second) = if source > 0.5 {
(view(-0.034, 0.987, 0.158), glam::Vec3::ZERO)
} else {
(view(-0.512, 0.500, 0.698), view(0.834, -0.451, 0.318))
};
// While the view moves, POINTCLOUDRTDENSITY per cent of the points.
let fraction = if navigating { value("POINTCLOUDRTDENSITY") / 100.0 } else { 1.0 };
[lighting, source, fraction, 0.0, first.x, first.y, first.z, 0.0, second.x, second.y, second.z, 0.0]
};
Some(ViewportData { Some(ViewportData {
instance_id, instance_id,
force_rasterize, force_rasterize,
@@ -4819,6 +4873,8 @@ impl Scene {
hatches, hatches,
wipeout_hatches, wipeout_hatches,
images, images,
point_clouds,
point_cloud_light,
meshes, meshes,
background_image: display.background.image.clone(), background_image: display.background.image.clone(),
environment_image: display.background.environment.clone(), environment_image: display.background.environment.clone(),
+92
View File
@@ -0,0 +1,92 @@
// Point cloud sprites: one instance per point, drawn as a square of
// POINTCLOUDPOINTSIZE pixels around its projected position, depth-tested.
// ── Bind group 0: shared projection uniforms ─────────────────────────────────
// Must match the leading fields of `Uniforms` (scene::pipeline::uniforms).
struct Uniforms {
viewport_size: vec2<f32>,
world_per_pixel: f32,
lwdisplay_enable: f32,
flat_shade: f32,
transparency_enable: f32,
_pad: vec2<f32>,
// Relative-to-eye (double-single): see wire.wgsl.
view_rot: mat4x4<f32>,
eye_high: vec3<f32>,
_pad_eh: f32,
eye_low: vec3<f32>,
_pad_el: f32,
};
@group(0) @binding(0) var<uniform> u: Uniforms;
struct PointParams {
size_px: f32,
// POINTCLOUDLIGHTING (0 off; 1 front faces lit; 2 both faces) and
// POINTCLOUDLIGHTSOURCE (0 key + fill light; 1 one light from above).
lighting: f32,
source: f32,
_pad0: f32,
light_dir: vec3<f32>,
_pad1: f32,
fill_dir: vec3<f32>,
_pad2: f32,
};
@group(1) @binding(0) var<uniform> params: PointParams;
struct PointIn {
@location(0) pos: vec3<f32>,
@location(1) pos_low: vec3<f32>,
@location(2) color: vec4<f32>,
@location(3) normal: vec4<f32>,
};
struct VertOut {
@builtin(position) clip_pos: vec4<f32>,
@location(0) color: vec4<f32>,
};
@vertex
fn vs_main(@builtin(vertex_index) corner: u32, in: PointIn) -> VertOut {
var out: VertOut;
let rel = (in.pos - u.eye_high) + (in.pos_low - u.eye_low);
let clip = u.view_rot * vec4<f32>(rel, 1.0);
// Two triangles covering the square: corners in -1..1.
var corners = array<vec2<f32>, 6>(
vec2<f32>(-1.0, -1.0), vec2<f32>(1.0, -1.0), vec2<f32>(1.0, 1.0),
vec2<f32>(-1.0, -1.0), vec2<f32>(1.0, 1.0), vec2<f32>(-1.0, 1.0),
);
// Half the size in pixels → NDC (2 / viewport), scaled by w to stay in
// clip space.
let half_ndc = params.size_px / max(u.viewport_size, vec2<f32>(1.0, 1.0));
out.clip_pos = vec4<f32>(clip.xy + corners[corner] * half_ndc * clip.w, clip.z, clip.w);
out.color = in.color;
let n = in.normal.xyz;
if params.lighting > 0.5 && dot(n, n) > 0.01 {
// Fitted to the reference's shading of the normals.
let nn = normalize(n);
var d = dot(nn, params.light_dir);
var f = dot(nn, params.fill_dir);
if params.lighting > 1.5 {
d = abs(d);
f = abs(f);
}
var shade: f32;
if params.source > 0.5 {
shade = 0.3 + 0.7 * max(d, 0.0);
} else if params.lighting > 1.5 {
// ponytail: best fit only; the reference also darkens some
// regions that no normal-based term explains.
shade = 0.133 + 0.61 * d + 0.086 * f;
} else {
shade = 0.204 + 0.569 * max(d, 0.0) + 0.329 * max(f, 0.0);
}
out.color = vec4<f32>(in.color.rgb * min(shade, 1.0), in.color.a);
}
return out;
}
@fragment
fn fs_main(in: VertOut) -> @location(0) vec4<f32> {
return in.color;
}
+5
View File
@@ -46,6 +46,8 @@ pub enum PanelId {
Browser, Browser,
/// Node library of the node graph overlay. /// Node library of the node graph overlay.
NodeGraph, NodeGraph,
/// Regions and scans of the drawing's point clouds.
PointCloudManager,
} }
impl PanelId { impl PanelId {
@@ -57,6 +59,7 @@ impl PanelId {
PanelId::ExternalReferences => "External References", PanelId::ExternalReferences => "External References",
PanelId::Browser => "Browser", PanelId::Browser => "Browser",
PanelId::NodeGraph => "Node Graph", PanelId::NodeGraph => "Node Graph",
PanelId::PointCloudManager => "Point Cloud Manager",
} }
} }
@@ -68,6 +71,7 @@ impl PanelId {
PanelId::ExternalReferences => 460.0, PanelId::ExternalReferences => 460.0,
PanelId::Browser => 230.0, PanelId::Browser => 230.0,
PanelId::NodeGraph => 220.0, PanelId::NodeGraph => 220.0,
PanelId::PointCloudManager => 280.0,
} }
} }
@@ -157,6 +161,7 @@ impl DockState {
PanelId::ExternalReferences, PanelId::ExternalReferences,
PanelId::Browser, PanelId::Browser,
PanelId::NodeGraph, PanelId::NodeGraph,
PanelId::PointCloudManager,
] { ] {
self.panels.entry(id).or_insert_with(|| DockPanel::for_id(id)); self.panels.entry(id).or_insert_with(|| DockPanel::for_id(id));
} }
+79 -1
View File
@@ -966,6 +966,80 @@ struct SelectionCanvas {
constraint_cursor_badge: Option<String>, constraint_cursor_badge: Option<String>,
} }
/// The move gizmo: an arrow per axis (X red, Y green, Z blue) from the
/// centre grip and a square per axis pair; the hovered or dragged part is
/// yellow.
fn draw_move_gizmo(frame: &mut canvas::Frame, grips: &[GripMarker]) {
use crate::scene::pick::grip::{gizmo_plane_axes, GIZMO_AXIS_PX};
const COLORS: [Color; 3] = [
Color::from_rgb(0.90, 0.22, 0.20),
Color::from_rgb(0.27, 0.70, 0.29),
Color::from_rgb(0.18, 0.52, 0.93),
];
let active = Color::from_rgb(1.0, 0.84, 0.0);
// Unit screen direction (y down) and tip of each shown arrow.
let mut axes: [Option<(Point, iced::Vector)>; 3] = [None; 3];
for grip in grips {
if let (GripShape::GizmoAxis(k), Some([dx, dy])) = (grip.shape, grip.dir) {
axes[k as usize % 3] = Some((grip.pos, iced::Vector::new(dx, -dy)));
}
}
let Some(center) = axes
.iter()
.flatten()
.next()
.map(|(tip, d)| Point::new(tip.x - d.x * GIZMO_AXIS_PX, tip.y - d.y * GIZMO_AXIS_PX))
else {
return;
};
for grip in grips {
let GripShape::GizmoPlane(k) = grip.shape else {
continue;
};
let (a, b) = gizmo_plane_axes(k);
let (Some((_, a)), Some((_, b))) = (axes[a as usize], axes[b as usize]) else {
continue;
};
let corner = |u: f32, v: f32| {
Point::new(
center.x + (a.x * u + b.x * v) * GIZMO_AXIS_PX,
center.y + (a.y * u + b.y * v) * GIZMO_AXIS_PX,
)
};
let quad = canvas::Path::new(|p| {
p.move_to(corner(0.18, 0.18));
p.line_to(corner(0.42, 0.18));
p.line_to(corner(0.42, 0.42));
p.line_to(corner(0.18, 0.42));
p.close();
});
let lit = grip.is_hovered || grip.is_hot;
let color = if lit { active } else { Color::from_rgb(0.85, 0.85, 0.85) };
frame.fill(&quad, color.scale_alpha(if lit { 0.55 } else { 0.25 }));
frame.stroke(&quad, canvas::Stroke::default().with_width(1.0).with_color(color));
}
for grip in grips {
let (GripShape::GizmoAxis(k), Some((tip, d))) =
(grip.shape, grip.dir.map(|[dx, dy]| (grip.pos, iced::Vector::new(dx, -dy))))
else {
continue;
};
let color = if grip.is_hovered || grip.is_hot { active } else { COLORS[k as usize % 3] };
let base = Point::new(tip.x - d.x * 13.0, tip.y - d.y * 13.0);
frame.stroke(
&canvas::Path::line(center, base),
canvas::Stroke::default().with_width(2.5).with_color(color),
);
let head = canvas::Path::new(|p| {
p.move_to(tip);
p.line_to(Point::new(base.x - d.y * 6.0, base.y + d.x * 6.0));
p.line_to(Point::new(base.x + d.y * 6.0, base.y - d.x * 6.0));
p.close();
});
frame.fill(&head, color);
}
}
fn draw_grip_marker( fn draw_grip_marker(
frame: &mut canvas::Frame, frame: &mut canvas::Frame,
grip: &GripMarker, grip: &GripMarker,
@@ -1019,6 +1093,7 @@ fn draw_grip_marker(
b.close(); b.close();
}), }),
GripShape::Circle => canvas::Path::circle(Point::new(sp.x, sp.y), h), GripShape::Circle => canvas::Path::circle(Point::new(sp.x, sp.y), h),
GripShape::GizmoAxis(_) | GripShape::GizmoPlane(_) => return,
GripShape::Dropdown | GripShape::DropdownAdjacent => canvas::Path::new(|b| { GripShape::Dropdown | GripShape::DropdownAdjacent => canvas::Path::new(|b| {
b.move_to(Point::new(sp.x - h, sp.y - h * 0.5)); b.move_to(Point::new(sp.x - h, sp.y - h * 0.5));
b.line_to(Point::new(sp.x + h, sp.y - h * 0.5)); b.line_to(Point::new(sp.x + h, sp.y - h * 0.5));
@@ -1393,8 +1468,11 @@ impl canvas::Program<Message> for SelectionCanvas {
); );
} }
} }
draw_move_gizmo(frame, &self.grips);
for grip in &self.grips { for grip in &self.grips {
draw_grip_marker(frame, grip, theme, &self.selection_visual); if !matches!(grip.shape, GripShape::GizmoAxis(_) | GripShape::GizmoPlane(_)) {
draw_grip_marker(frame, grip, theme, &self.selection_visual);
}
} }
}); });
} }
+27
View File
@@ -59,6 +59,24 @@ pub fn pdf_underlay_tools(kind: codec::entities::UnderlayType) -> Vec<ToolDef> {
tools 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. /// External reference tools: edit, open, clipping and the palette.
pub fn xref_tools() -> Vec<ToolDef> { pub fn xref_tools() -> Vec<ToolDef> {
vec![ vec![
@@ -90,6 +108,15 @@ impl Ribbon {
self.xref_ctx 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. /// Select a tab by its module id.
pub fn select_by_id(&mut self, id: &str) -> bool { pub fn select_by_id(&mut self, id: &str) -> bool {
match self.modules.iter().position(|m| m.id() == id) { 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 modify_panel;
mod color_dropdown; mod color_dropdown;
mod context_tools; 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, *}; use widgets::{StyleContext, *};
pub(crate) use widgets::{REDO_HISTORY_ID, UNDO_HISTORY_ID}; pub(crate) use widgets::{REDO_HISTORY_ID, UNDO_HISTORY_ID};
mod collapse; mod collapse;
@@ -109,6 +109,8 @@ pub struct Ribbon {
underlay_ctx: Option<UnderlayContext>, underlay_ctx: Option<UnderlayContext>,
/// Only xrefs are selected. /// Only xrefs are selected.
xref_ctx: bool, 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). /// XDWGFADECTL as the Reference slide-out shows it (negative = off).
pub xref_fade: i32, pub xref_fade: i32,
} }
@@ -206,6 +208,7 @@ impl Ribbon {
collapse_tight: Arc::new(AtomicBool::new(false)), collapse_tight: Arc::new(AtomicBool::new(false)),
underlay_ctx: None, underlay_ctx: None,
xref_ctx: false, xref_ctx: false,
point_cloud_ctx: None,
xref_fade: 50, xref_fade: 50,
} }
} }
+1
View File
@@ -24,6 +24,7 @@ pub mod annotation_data;
pub mod alias_editor; pub mod alias_editor;
pub mod find_replace; pub mod find_replace;
pub mod named_parameters; pub mod named_parameters;
pub mod pc_manager;
pub mod pdf_dialogs; pub mod pdf_dialogs;
pub mod xref_attach; pub mod xref_attach;
pub mod xref_help; pub mod xref_help;
+358
View File
@@ -0,0 +1,358 @@
//! Point Cloud Manager (POINTCLOUDMANAGER): the regions and scans of the
//! drawing's point clouds as a tree, each with a switch that shows or hides
//! its points. The switches are the cloud's own hidden scans and regions, so
//! the tree is read straight from the document on every view.
use crate::app::Message;
use crate::scene::model::point_cloud::{self, UNASSIGNED_OFF};
use crate::ui::dock::PanelId;
use codec::entities::ExtendedEntityData;
use codec::{CadDocument, EntityType, Handle};
use iced::widget::{button, checkbox, column, container, mouse_area, row, scrollable, text, text_input, tooltip, Space};
use iced::{Background, Border, Element, Fill, Length, Theme};
use std::collections::HashSet;
const CLOUD_ICON: &[u8] = include_bytes!("../../../assets/icons/pc_attach.svg");
const REGION_ICON: &[u8] = include_bytes!("../../../assets/icons/region.svg");
const UNASSIGNED_ICON: &[u8] = include_bytes!("../../../assets/icons/revcloud.svg");
/// Palette state that is not in the drawing: shown, folded nodes, the
/// highlighted row and the search text.
#[derive(Debug, Default)]
pub struct PcManager {
pub show: bool,
pub collapsed: HashSet<String>,
pub selected: Option<String>,
pub search: String,
}
/// A row with a switch.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Row {
Cloud,
Unassigned,
Scans,
/// A scan, by identifier.
Scan(String),
}
/// A switch's state: a parent is mixed while some children are on.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Switch {
On,
Off,
Mixed,
}
impl Row {
/// `cloud`, `unassigned`, `scans` or `scan:<name or identifier>`
/// (automation).
pub fn parse(text: &str) -> Option<Self> {
match text {
"cloud" => Some(Self::Cloud),
"unassigned" => Some(Self::Unassigned),
"scans" => Some(Self::Scans),
_ => text.strip_prefix("scan:").map(|name| Self::Scan(name.to_string())),
}
}
}
#[derive(Debug, Clone)]
pub enum PcManagerMsg {
Toggle(Handle, Row),
Expand(String),
Select(String),
Search(String),
CollapseAll,
ExpandAll,
}
/// One cloud of the tree.
pub struct Cloud {
pub handle: Handle,
pub name: String,
pub hidden: Vec<String>,
/// Each scan's name and identifier.
pub scans: Vec<(String, String)>,
}
/// The drawing's point clouds; only the selected ones when any is selected.
pub fn clouds(document: &CadDocument, selected: &[Handle]) -> Vec<Cloud> {
let all: Vec<Cloud> = document
.entities()
.filter_map(|entity| {
let EntityType::Extended(extended) = entity else {
return None;
};
let ExtendedEntityData::PointCloudEx(data) = &extended.data else {
return None;
};
let common = entity.common();
let scans = point_cloud::resolve_source(document, data)
.and_then(|path| point_cloud::load(&path))
.map(|cloud| cloud.scan_ranges.iter().map(|s| (s.name.clone(), s.id.clone())).collect())
.unwrap_or_default();
Some(Cloud {
handle: common.handle,
name: if data.name.is_empty() { format!("{:X}", common.handle.value()) } else { data.name.clone() },
hidden: point_cloud::hidden(data),
scans,
})
})
.collect();
if all.iter().any(|cloud| selected.contains(&cloud.handle)) {
all.into_iter().filter(|cloud| selected.contains(&cloud.handle)).collect()
} else {
all
}
}
/// A row's switch. A parent is on with all its children on, off with all
/// off, mixed otherwise (the reference shows the cloud mixed with its only
/// scan off and its unassigned points on).
pub fn switch_state(hidden: &[String], scans: &[(String, String)], row: &Row) -> Switch {
let on = |id: &str| !hidden.iter().any(|h| h == id);
let of = |states: &[bool]| match (states.iter().all(|s| *s), states.iter().any(|s| *s)) {
(true, _) => Switch::On,
(false, false) => Switch::Off,
_ => Switch::Mixed,
};
let scan_states: Vec<bool> = scans.iter().map(|(_, id)| on(id)).collect();
match row {
Row::Scan(id) => of(&[on(id)]),
Row::Scans => of(&scan_states),
Row::Unassigned => of(&[on(UNASSIGNED_OFF)]),
Row::Cloud => {
let mut all = scan_states;
all.push(on(UNASSIGNED_OFF));
of(&all)
}
}
}
/// The hidden list after clicking `row`: an on switch turns its rows off,
/// an off or mixed one turns them all on.
pub fn toggled(hidden: &[String], scans: &[(String, String)], row: &Row) -> Vec<String> {
fn set(out: &mut Vec<String>, id: &str, off: bool) {
out.retain(|h| h != id);
if off {
out.push(id.to_string());
}
}
let off = switch_state(hidden, scans, row) == Switch::On;
let mut out = hidden.to_vec();
match row {
Row::Cloud => {
scans.iter().for_each(|(_, id)| set(&mut out, id, off));
set(&mut out, UNASSIGNED_OFF, off);
}
Row::Unassigned => set(&mut out, UNASSIGNED_OFF, off),
Row::Scans => scans.iter().for_each(|(_, id)| set(&mut out, id, off)),
Row::Scan(id) => set(&mut out, id, off),
}
out
}
/// Keys of the rows that fold: each cloud and its Scans.
pub fn folding_keys(clouds: &[Cloud]) -> Vec<String> {
clouds.iter().flat_map(|c| [key(c.handle), format!("{}/scans", key(c.handle))]).collect()
}
fn key(handle: Handle) -> String {
format!("{:X}", handle.value())
}
fn msg(m: PcManagerMsg) -> Message {
Message::PcManager(m)
}
/// One tree row: indent, fold arrow, icon, label and switch (`None` for a
/// row that has none to give, drawn off and disabled).
fn tree_row<'a>(
state: &PcManager,
key: String,
depth: u16,
fold: Option<bool>,
icon: Element<'a, Message>,
label: String,
switch: Option<(Switch, Message)>,
) -> Element<'a, Message> {
let arrow: Element<'a, Message> = match fold {
Some(open) => button(if open {
crate::ui::icons::themed_arrow_down(10.0)
} else {
crate::ui::icons::themed_arrow_right(10.0)
})
.on_press(msg(PcManagerMsg::Expand(key.clone())))
.style(button::text)
.padding(2)
.into(),
None => Space::new().width(14).into(),
};
let enabled = switch.is_some();
let label = text(label).size(12).width(Fill);
let label = if enabled { label } else { label.style(crate::ui::style::common::muted_style) };
let switch: Element<'a, Message> = match switch {
// Mixed: a dash in the box, as the reference's indeterminate state.
Some((Switch::Mixed, message)) => button(crate::ui::icons::themed_primary(crate::ui::icons::MINUS, 10.0))
.on_press(message)
.style(button::secondary)
.padding(1)
.width(Length::Fixed(16.0))
.height(Length::Fixed(16.0))
.into(),
Some((on, message)) => checkbox(on == Switch::On).on_toggle(move |_| message.clone()).size(14).into(),
None => checkbox(false).size(14).into(),
};
let selected = state.selected.as_deref() == Some(key.as_str());
let cells = row![Space::new().width(Length::Fixed(f32::from(depth) * 16.0)), arrow, icon, label, switch]
.spacing(5)
.align_y(iced::Center);
mouse_area(
container(cells)
.width(Fill)
.padding([3, 6])
.style(move |theme: &Theme| container::Style {
background: selected.then(|| Background::Color(theme.palette().primary.weak.color)),
text_color: selected.then(|| theme.palette().primary.weak.text),
..Default::default()
}),
)
.on_press(msg(PcManagerMsg::Select(key)))
.into()
}
fn fold_button<'a>(icon: &'static [u8], tip: String, message: PcManagerMsg) -> Element<'a, Message> {
let b = button(crate::ui::icons::themed_secondary(icon, 12.0))
.on_press(msg(message))
.style(button::subtle)
.padding([2, 4]);
tooltip(b, text(tip).size(10), tooltip::Position::Bottom).gap(4).into()
}
pub fn view<'a>(
state: &'a PcManager,
document: &'a CadDocument,
selected: &[Handle],
width: f32,
auto_collapse: bool,
) -> Element<'a, Message> {
let title_bar =
crate::ui::dock::title_bar(PanelId::PointCloudManager, crate::t!("Point Cloud Manager").into_owned(), auto_collapse);
let query = state.search.trim().to_lowercase();
let matches = |label: &str| query.is_empty() || label.to_lowercase().contains(&query);
// Searching opens every node so the matches show.
let open = |key: &str| !query.is_empty() || !state.collapsed.contains(key);
let regions = crate::t!("Regions").into_owned();
let unassigned = crate::t!("Unassigned Points").into_owned();
let scans_label = crate::t!("Scans").into_owned();
let mut tree = column![].spacing(1);
for cloud in clouds(document, selected) {
let root = key(cloud.handle);
let scans: Vec<&(String, String)> = cloud.scans.iter().filter(|(name, _)| matches(name)).collect();
let show_scans = matches(&scans_label) || !scans.is_empty();
let children = matches(&regions) || matches(&unassigned) || show_scans;
if !matches(&cloud.name) && !children {
continue;
}
let switch = |row: Row| {
let on = switch_state(&cloud.hidden, &cloud.scans, &row);
Some((on, msg(PcManagerMsg::Toggle(cloud.handle, row))))
};
let cloud_icon = || crate::ui::icons::semantic(CLOUD_ICON, 14.0);
tree = tree.push(tree_row(
state,
root.clone(),
0,
Some(open(&root)),
cloud_icon(),
cloud.name.clone(),
switch(Row::Cloud),
));
if !open(&root) {
continue;
}
if matches(&regions) {
// Region data is not read: the row is there, off and disabled.
tree = tree.push(tree_row(
state,
format!("{root}/regions"),
1,
None,
crate::ui::icons::semantic_disabled(REGION_ICON, 14.0),
regions.clone(),
None,
));
}
if matches(&unassigned) {
tree = tree.push(tree_row(
state,
format!("{root}/unassigned"),
1,
None,
crate::ui::icons::semantic(UNASSIGNED_ICON, 14.0),
unassigned.clone(),
switch(Row::Unassigned),
));
}
if show_scans {
let scans_key = format!("{root}/scans");
let scans_open = open(&scans_key);
tree = tree.push(tree_row(
state,
scans_key.clone(),
1,
Some(scans_open),
cloud_icon(),
scans_label.clone(),
switch(Row::Scans),
));
if scans_open {
for (name, id) in scans {
tree = tree.push(tree_row(
state,
format!("{scans_key}/{id}"),
2,
None,
cloud_icon(),
name.clone(),
switch(Row::Scan(id.clone())),
));
}
}
}
}
let heading = text(crate::t!("REGIONS AND SCANS").into_owned())
.size(11)
.font(iced::Font {
weight: iced::font::Weight::Bold,
style: iced::font::Style::Italic,
..iced::Font::DEFAULT
})
.style(|theme: &Theme| text::Style { color: Some(theme.palette().primary.base.color) })
.width(Fill);
let header = row![
heading,
fold_button(crate::ui::icons::MINUS, crate::t!("Collapse all").into_owned(), PcManagerMsg::CollapseAll),
fold_button(crate::ui::icons::PLUS, crate::t!("Expand all").into_owned(), PcManagerMsg::ExpandAll),
]
.spacing(2)
.align_y(iced::Center);
let card = container(column![header, scrollable(tree).height(Fill)].spacing(6))
.padding(6)
.width(Fill)
.height(Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { radius: 4.0.into(), ..Default::default() },
..Default::default()
});
let search = text_input(&crate::t!("Search"), &state.search)
.on_input(|v| msg(PcManagerMsg::Search(v)))
.padding([4, 8])
.size(12);
crate::ui::dock::frame(column![title_bar, card, search].spacing(6), width)
}
+709 -1
View File
@@ -1,6 +1,7 @@
//! PDF dialogs: Attach PDF Underlay (pages, path type, insertion, scale, //! PDF dialogs: Attach PDF Underlay (pages, path type, insertion, scale,
//! rotation), Underlay Layers, PDF Import Settings and Import PDF, built //! rotation), Underlay Layers, PDF Import Settings and Import PDF, built
//! from cards, segmented choices and toggle chips. //! from cards, segmented choices and toggle chips; the point cloud Attach
//! and Color Map dialogs share them.
use std::fmt; use std::fmt;
@@ -35,6 +36,54 @@ pub enum PdfDialogMsg {
AttachRotation(String), AttachRotation(String),
AttachDetails(bool), AttachDetails(bool),
AttachOk, 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,
// Point Cloud Color Map
/// Elevation tab (else intensity).
MapTab(bool),
MapScheme(String),
MapCount(usize),
MapEven,
MapReverse,
MapGradient(bool),
MapNew,
MapDelete,
MapRename,
MapNameInput(String),
MapNameOk,
MapNameCancel,
MapMax(String),
MapMin(String),
MapInterval(String),
MapExtents(bool),
MapOutOfRange(OutOfRange),
MapCurrent(bool),
MapApply,
MapOk,
// Extract Section Lines from Point Cloud
SecPerimeter(bool),
SecMaxPoints(String),
SecLayer(String),
SecColor(LineColor),
SecPolylines(bool),
SecWidth(String),
SecMinLength(String),
SecConnect(String),
SecAngle(String),
/// Measure the connect tolerance (else the minimum length) on screen.
SecPick(bool),
SecPreview(bool),
SecCreate,
// Underlay Layers // Underlay Layers
LayersUnderlay(String), LayersUnderlay(String),
LayersSearch(String), LayersSearch(String),
@@ -665,6 +714,140 @@ pub fn view_attach<'a>(
.into() .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 ──────────────────────────────────────────────────────── // ── Underlay Layers ────────────────────────────────────────────────────────
/// One PDF underlay the dialog can show: its name, its file's layers, and /// One PDF underlay the dialog can show: its name, its file's layers, and
@@ -962,3 +1145,528 @@ pub fn view_import_file<'a>(
.width(sizing.width) .width(sizing.width)
.into() .into()
} }
// ── Point Cloud Color Map ──────────────────────────────────────────────────
/// A colour scheme as the dialog edits it: identifier, name and colours
/// (high end first).
#[derive(Debug, Clone, PartialEq)]
pub struct MapRamp {
pub id: String,
pub name: String,
pub colors: Vec<[u8; 3]>,
}
/// What a scheme does with values outside its range.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OutOfRange(pub i16);
impl OutOfRange {
pub const ALL: [OutOfRange; 3] = [OutOfRange(0), OutOfRange(1), OutOfRange(2)];
}
impl fmt::Display for OutOfRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self.0 {
0 => t!("Use min/max colors"),
2 => t!("Hide points"),
_ => t!("Use RGB scan colors"),
};
f.write_str(label.as_ref())
}
}
/// One tab's settings: its scheme and range.
#[derive(Debug, Clone)]
pub struct MapSlot {
pub scheme: String,
pub gradient: bool,
pub max: String,
pub min: String,
pub out_of_range: OutOfRange,
}
/// The Point Cloud Color Map dialog: the drawing's colour schemes and, for
/// a chosen cloud, its intensity and elevation colouring.
pub struct PointCloudColorMapState {
/// The cloud edited; `None` edits the drawing's default schemes.
pub cloud: Option<codec::Handle>,
/// Elevation tab (else intensity).
pub elevation_tab: bool,
pub ramps: Vec<MapRamp>,
pub intensity: MapSlot,
pub elevation: MapSlot,
/// Elevation over the cloud's own height (else the fixed range).
pub extents: bool,
/// The cloud's height range.
pub height: (f64, f64),
pub make_current: bool,
/// The height of one band (elevation), as shown and typed.
pub interval: String,
/// A name being typed for a new scheme (`true`) or a rename.
pub naming: Option<(bool, String)>,
}
impl PointCloudColorMapState {
pub fn slot(&self) -> &MapSlot {
if self.elevation_tab { &self.elevation } else { &self.intensity }
}
pub fn slot_mut(&mut self) -> &mut MapSlot {
if self.elevation_tab { &mut self.elevation } else { &mut self.intensity }
}
pub fn ramp(&self) -> Option<&MapRamp> {
let id = &self.slot().scheme;
self.ramps.iter().find(|r| r.id.eq_ignore_ascii_case(id)).or(self.ramps.first())
}
pub fn ramp_mut(&mut self) -> Option<&mut MapRamp> {
let id = self.slot().scheme.clone();
let at = self.ramps.iter().position(|r| r.id.eq_ignore_ascii_case(&id)).unwrap_or(0);
self.ramps.get_mut(at)
}
/// The range the ramp spans on the current tab.
pub fn range(&self) -> Option<(f64, f64)> {
if self.elevation_tab && self.extents {
return Some(self.height);
}
let slot = self.slot();
Some((slot.min.trim().parse().ok()?, slot.max.trim().parse().ok()?))
}
}
/// `t` (0 at the high end) along colours, blended; channels are cut to
/// whole values, as the reference stores resampled schemes.
pub fn blend(colors: &[[u8; 3]], t: f64) -> [u8; 3] {
if colors.len() < 2 {
return colors.first().copied().unwrap_or([255; 3]);
}
let x = t.clamp(0.0, 1.0) * (colors.len() - 1) as f64;
let k = (x as usize).min(colors.len() - 2);
let f = x - k as f64;
let (a, b) = (colors[k], colors[k + 1]);
[0, 1, 2].map(|c| (f64::from(a[c]) + (f64::from(b[c]) - f64::from(a[c])) * f) as u8)
}
/// A number as the dialog labels it: at most two decimals.
pub fn short(value: f64) -> String {
let value = if value.abs() < 0.005 { 0.0 } else { value };
let text = format!("{:.2}", value);
text.trim_end_matches('0').trim_end_matches('.').to_string()
}
fn swatch<'a>(color: [u8; 3], width: f32, height: f32) -> Element<'a, Message> {
let color = iced::Color::from_rgb8(color[0], color[1], color[2]);
container(Space::new().width(width).height(height))
.style(move |_: &Theme| container::Style {
background: Some(Background::Color(color)),
border: Border { width: 1.0, radius: 3.0.into(), color: iced::Color::from_rgb8(0x77, 0x77, 0x77) },
..Default::default()
})
.into()
}
/// The ramp as a bar, high end on top, blended or in bands, with the values
/// where the bands meet (blended: the ends and the inner band centres).
fn ramp_bar<'a>(colors: &[[u8; 3]], gradient: bool, range: Option<(f64, f64)>, percent: bool) -> Element<'a, Message> {
const HEIGHT: f32 = 360.0;
let n = colors.len().max(1);
let strips: Vec<Element<'a, Message>> = if gradient {
(0..72).map(|k| {
let c = blend(colors, (k as f64 + 0.5) / 72.0);
let color = iced::Color::from_rgb8(c[0], c[1], c[2]);
container(Space::new().width(Fill).height(Fill))
.height(Length::FillPortion(1))
.style(move |_: &Theme| container::Style {
background: Some(Background::Color(color)),
..Default::default()
})
.into()
}).collect()
} else {
colors.iter().map(|c| {
let color = iced::Color::from_rgb8(c[0], c[1], c[2]);
container(Space::new().width(Fill).height(Fill))
.height(Length::FillPortion(1))
.style(move |_: &Theme| container::Style {
background: Some(Background::Color(color)),
..Default::default()
})
.into()
}).collect()
};
let bar = container(iced::widget::Column::with_children(strips))
.width(Length::Fixed(56.0))
.height(Length::Fixed(HEIGHT));
// Label positions from the top (0) to the bottom (1).
let marks: Vec<f64> = if gradient {
let mut marks = vec![0.0];
marks.extend((1..n.saturating_sub(1)).map(|k| (k as f64 + 0.5) / n as f64));
marks.push(1.0);
marks
} else {
(0..=n).map(|k| k as f64 / n as f64).collect()
};
let mut labels = column![];
if let Some((low, high)) = range {
// Each label is centred on its mark, kept inside the bar.
let mut used = 0.0;
for mark in marks {
let value = high - (high - low) * mark;
let label = if percent { format!("{}%", value.round()) } else { short(value) };
let top = (mark * f64::from(HEIGHT) - 7.0).clamp(0.0, f64::from(HEIGHT) - 14.0);
labels = labels.push(Space::new().height((top - used).max(0.0) as f32)).push(text(label).size(10).height(14.0));
used = top.max(used) + 14.0;
}
}
row![bar, labels.width(Length::Fixed(44.0))].spacing(4).into()
}
pub fn view_point_cloud_color_map<'a>(
state: &'a PointCloudColorMapState,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let tab = |label: std::borrow::Cow<'static, str>, on: bool, message: Option<Message>| -> Element<'a, Message> {
let mut b = button(text(label).size(11).width(Fill).align_x(iced::Center))
.style(button_style(on))
.padding([5, 6])
.width(Length::Fixed(118.0));
if let Some(message) = message {
b = b.on_press(message);
}
b.into()
};
let tabs = container(
row![
tab(t!("Intensity"), !state.elevation_tab, Some(msg(PdfDialogMsg::MapTab(false)))),
tab(t!("Elevation"), state.elevation_tab, Some(msg(PdfDialogMsg::MapTab(true)))),
tab(t!("Classification"), false, None),
]
.spacing(2),
)
.padding(2)
.style(well_style);
let slot = state.slot();
let ramp = state.ramp();
let colors: Vec<[u8; 3]> = ramp.map(|r| r.colors.clone()).unwrap_or_default();
let ramp_card = card(
t!("Color ramp").into_owned(),
ramp_bar(&colors, slot.gradient, state.cloud.and(state.range()), !state.elevation_tab),
);
// Listed by name, as the reference lists them.
let mut names: Vec<String> = state.ramps.iter().map(|r| r.name.clone()).collect();
names.sort_by_key(|n| n.to_lowercase());
let scheme = pick_list(ramp.map(|r| r.name.clone()), names, |n: &String| n.clone())
.on_select(|n| msg(PdfDialogMsg::MapScheme(n)))
.text_size(12)
.padding([5, 8])
.width(Fill);
let small = |label: std::borrow::Cow<'static, str>, message: PdfDialogMsg| {
button(text(label).size(11).width(Fill).align_x(iced::Center))
.on_press(msg(message))
.style(button_style(false))
.padding([5, 8])
.width(Length::Fixed(118.0))
};
let counts: Vec<usize> = (2..=25).collect();
let count = pick_list(Some(colors.len().max(2)), counts, |n: &usize| n.to_string())
.on_select(|n| msg(PdfDialogMsg::MapCount(n)))
.text_size(12)
.padding([4, 8])
.width(Length::Fixed(70.0));
// Blend: a strip from the first colour to the last.
let ends = [colors.first().copied().unwrap_or([255; 3]), colors.last().copied().unwrap_or([255; 3])];
let blend_strip = row((0..6).map(|k| swatch(blend(&ends, k as f64 / 5.0), 3.0, 14.0)));
let even = button(blend_strip).on_press(msg(PdfDialogMsg::MapEven)).style(button_style(false)).padding([5, 6]);
let reverse = button(
iced::widget::svg(crate::ui::icons::themed_handle(include_bytes!("../../../assets/icons/modify_reverse.svg")))
.width(16.0)
.height(16.0),
)
.on_press(msg(PdfDialogMsg::MapReverse))
.style(button_style(false))
.padding([4, 6]);
let mut scheme_rows = column![
row![scheme, small(t!("New"), PdfDialogMsg::MapNew)].spacing(8).align_y(iced::Center),
row![
text(t!("Number of colors")).size(11).width(Length::Fixed(110.0)),
count,
even,
reverse,
Space::new().width(Fill),
small(t!("Delete"), PdfDialogMsg::MapDelete)
]
.spacing(8)
.align_y(iced::Center),
row![
chip(t!("Display as gradient").into_owned(), slot.gradient, msg(PdfDialogMsg::MapGradient(!slot.gradient))),
Space::new().width(Fill),
small(t!("Rename"), PdfDialogMsg::MapRename)
]
.align_y(iced::Center),
]
.spacing(8);
if let Some((_, name)) = &state.naming {
scheme_rows = scheme_rows.push(
row![
text(t!("Scheme name")).size(11).style(muted_style).width(Length::Fixed(110.0)),
text_input("", name)
.on_input(|v| msg(PdfDialogMsg::MapNameInput(v)))
.on_submit(msg(PdfDialogMsg::MapNameOk))
.size(11)
.padding([4, 8])
.width(Fill)
.style(field_style),
dialog_button(t!("Cancel"), msg(PdfDialogMsg::MapNameCancel), false),
dialog_button(t!("OK"), msg(PdfDialogMsg::MapNameOk), true),
]
.spacing(6)
.align_y(iced::Center),
);
}
let scheme_card = card(t!("Color scheme").into_owned(), scheme_rows);
let has_cloud = state.cloud.is_some();
let top = colors.first().copied().unwrap_or([255; 3]);
let bottom = colors.last().copied().unwrap_or([255; 3]);
let editable = has_cloud && !(state.elevation_tab && state.extents);
let with_swatch = |field: Element<'a, Message>, color: [u8; 3]| -> Element<'a, Message> {
row![field, swatch(color, 22.0, 22.0)].spacing(8).align_y(iced::Center).into()
};
let out_of_range = {
let list = pick_list(Some(slot.out_of_range), OutOfRange::ALL.to_vec(), |o: &OutOfRange| o.to_string())
.text_size(12)
.padding([4, 8])
.width(Fill);
let list = if editable { list.on_select(|o| msg(PdfDialogMsg::MapOutOfRange(o))) } else { list };
row![text(t!("Out of range points")).size(11).style(muted_style).width(Length::Fixed(150.0)), list]
.spacing(6)
.align_y(iced::Center)
};
let range_rows = if state.elevation_tab {
let (low, high) = state.height;
let interval = state.interval.as_str();
column![
chip(
format!("{} ({} ~ {})", t!("Apply to extents of point cloud"), short(low), short(high)),
state.extents,
msg(PdfDialogMsg::MapExtents(!state.extents))
),
field(t!("Interval height").into_owned(), interval, editable, 150.0, |v| msg(PdfDialogMsg::MapInterval(v))),
with_swatch(field(t!("Maximum elevation").into_owned(), &slot.max, editable, 150.0, |v| msg(PdfDialogMsg::MapMax(v))), top),
with_swatch(field(t!("Minimum elevation").into_owned(), &slot.min, editable, 150.0, |v| msg(PdfDialogMsg::MapMin(v))), bottom),
out_of_range,
]
.spacing(8)
} else {
column![
with_swatch(field(t!("Maximum intensity").into_owned(), &slot.max, editable, 150.0, |v| msg(PdfDialogMsg::MapMax(v))), top),
with_swatch(field(t!("Minimum intensity").into_owned(), &slot.min, editable, 150.0, |v| msg(PdfDialogMsg::MapMin(v))), bottom),
out_of_range,
]
.spacing(8)
};
let range_card = card(t!("Range of colorized points").into_owned(), range_rows);
let body = row![
container(ramp_card).width(Length::Fixed(150.0)),
column![scheme_card, range_card].spacing(8).width(Fill),
]
.spacing(10);
let mut left = row![button(text("?").size(12))
.on_press(msg(PdfDialogMsg::Help("pointcloudcolormap")))
.style(button_style(false))
.padding([5, 11])]
.spacing(8)
.align_y(iced::Center);
if has_cloud {
left = left.push(chip(
t!("Make stylization current").into_owned(),
state.make_current,
msg(PdfDialogMsg::MapCurrent(!state.make_current)),
));
}
let footer = row![
left,
Space::new().width(Fill),
dialog_button(t!("Cancel"), Message::CloseModal, false),
dialog_button(t!("Apply"), msg(PdfDialogMsg::MapApply), false),
dialog_button(t!("OK"), msg(PdfDialogMsg::MapOk), true),
]
.spacing(6)
.align_y(iced::Center);
column![tabs, body, footer].spacing(10).padding([10, 12]).width(sizing.width).into()
}
// ── Extract Section Lines from Point Cloud ─────────────────────────────────
/// A colour the extracted lines can take (ACI; 256 ByLayer, 0 ByBlock).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LineColor(pub i16);
impl LineColor {
pub const ALL: [LineColor; 9] = [
LineColor(256),
LineColor(0),
LineColor(1),
LineColor(2),
LineColor(3),
LineColor(4),
LineColor(5),
LineColor(6),
LineColor(7),
];
}
impl fmt::Display for LineColor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self.0 {
256 => t!("ByLayer"),
0 => t!("ByBlock"),
1 => t!("Red"),
2 => t!("Yellow"),
3 => t!("Green"),
4 => t!("Cyan"),
5 => t!("Blue"),
6 => t!("Magenta"),
_ => t!("White"),
};
f.write_str(label.as_ref())
}
}
/// The dialog's fields; numbers as typed.
pub struct PcSectionState {
pub perimeter: bool,
pub max_points: String,
/// "Use Current" first, then the drawing's layers.
pub layers: Vec<String>,
pub layer: String,
pub color: LineColor,
pub polylines: bool,
pub width: String,
pub min_length: String,
pub connect: String,
pub angle: String,
pub preview: bool,
}
pub fn view_pc_section<'a>(state: &'a PcSectionState, sizing: crate::ui::modal::ModalSizing) -> Element<'a, Message> {
let extract = card(
t!("Extract").into_owned(),
segmented(
vec![(false, t!("Entire cross section").into_owned()), (true, t!("Perimeter only").into_owned())],
state.perimeter,
PdfDialogMsg::SecPerimeter,
),
);
let points = state.max_points.trim().parse::<f32>().unwrap_or(18_000.0).clamp(1_000.0, 200_000.0);
// ponytail: the time shown scales with the points (18000 ≈ 1 minute, as
// the reference shows it); the real rate depends on the machine.
let minutes = (points / 18_000.0).ceil().max(1.0) as u32;
let max_points = card(
t!("Maximum points to process").into_owned(),
column![
row![
iced::widget::slider(1_000.0..=200_000.0, points, |v| msg(PdfDialogMsg::SecMaxPoints(
((v / 1_000.0).round() * 1_000.0).to_string()
)))
.step(1_000.0)
.width(Fill),
text_input("", &state.max_points)
.on_input(|v| msg(PdfDialogMsg::SecMaxPoints(v)))
.size(11)
.padding([4, 8])
.width(Length::Fixed(90.0))
.style(field_style),
]
.spacing(8)
.align_y(iced::Center),
row![
text(t!("Faster")).size(10).style(muted_style),
Space::new().width(Fill),
text(t!("More accurate")).size(10).style(muted_style),
Space::new().width(Fill),
text(crate::tf!("Estimated time: {count} minutes", count = minutes)).size(10).style(muted_style),
],
]
.spacing(6),
);
let layer = pick_list(Some(state.layer.clone()), state.layers.clone(), |n: &String| n.clone())
.on_select(|n| msg(PdfDialogMsg::SecLayer(n)))
.text_size(12)
.padding([4, 8])
.width(Fill);
let color = pick_list(Some(state.color), LineColor::ALL.to_vec(), |c: &LineColor| c.to_string())
.on_select(|c| msg(PdfDialogMsg::SecColor(c)))
.text_size(12)
.padding([4, 8])
.width(Fill);
let labelled = |label: std::borrow::Cow<'static, str>, control: Element<'a, Message>| -> Element<'a, Message> {
row![text(label).size(11).style(muted_style).width(Length::Fixed(70.0)), control]
.spacing(6)
.align_y(iced::Center)
.into()
};
let output = card(
t!("Output geometry").into_owned(),
column![
labelled(t!("Layer"), layer.into()),
labelled(t!("Color"), color.into()),
segmented(
vec![(false, t!("Lines").into_owned()), (true, t!("2D Polylines").into_owned())],
state.polylines,
PdfDialogMsg::SecPolylines,
),
field(t!("Polyline width").into_owned(), &state.width, state.polylines, 150.0, |v| msg(PdfDialogMsg::SecWidth(v))),
]
.spacing(8),
);
let pick = |connect: bool| {
button(text("⌖").size(12))
.on_press(msg(PdfDialogMsg::SecPick(connect)))
.style(button_style(false))
.padding([4, 8])
};
let tolerances = card(
t!("Extraction tolerances").into_owned(),
column![
row![field(t!("Minimum line length").into_owned(), &state.min_length, true, 170.0, |v| msg(PdfDialogMsg::SecMinLength(v))), pick(false)]
.spacing(6)
.align_y(iced::Center),
row![field(t!("Connect lines tolerance").into_owned(), &state.connect, true, 170.0, |v| msg(PdfDialogMsg::SecConnect(v))), pick(true)]
.spacing(6)
.align_y(iced::Center),
row![
field(t!("Collinear angle tolerance").into_owned(), &state.angle, true, 170.0, |v| msg(PdfDialogMsg::SecAngle(v))),
text("0–10°").size(10).style(muted_style)
]
.spacing(6)
.align_y(iced::Center),
]
.spacing(8),
);
let body = column![
row![extract, max_points].spacing(10),
row![output, tolerances].spacing(10),
]
.spacing(10);
let footer = row![
button(text("?").size(12))
.on_press(msg(PdfDialogMsg::Help("pcsection")))
.style(button_style(false))
.padding([5, 11]),
chip(t!("Preview result").into_owned(), state.preview, msg(PdfDialogMsg::SecPreview(!state.preview))),
Space::new().width(Fill),
dialog_button(t!("Cancel"), Message::CloseModal, false),
dialog_button(t!("Create"), msg(PdfDialogMsg::SecCreate), true),
]
.spacing(8)
.align_y(iced::Center);
column![body, footer].spacing(10).padding([10, 12]).width(sizing.width).into()
}
+14 -1
View File
@@ -1103,6 +1103,11 @@ fn direct_identities(doc: &CadDocument) -> HashSet<(u64, String)> {
ObjectType::UnderlayDefinition(def) => { ObjectType::UnderlayDefinition(def) => {
ids.insert((handle.value(), def.file_path.clone())); 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"), _ => 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()?; let img = image::open(found).ok()?;
Some(img.thumbnail(PREVIEW_MAX, PREVIEW_MAX).to_rgba8()) Some(img.thumbnail(PREVIEW_MAX, PREVIEW_MAX).to_rgba8())
} }
RefKind::Underlay => None, RefKind::Underlay | RefKind::PointCloud => None,
} }
} }