From 2e23a903a4a1045185719dc6879cbf62ed924ab2 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:50:26 +0300 Subject: [PATCH] Point clouds: crops along the view, POINTCLOUDUNCROP, POINTCLOUDLOD A crop is drawn on the view plane: it keeps the view's right and up directions in the cloud's coordinates and its points in 3D, and cuts along the view. POINTCLOUDUNCROP removes a picked cloud's crops. POINTCLOUDLOD scales the points shown in tenths. Co-Authored-By: Claude Opus 5.5 --- src/app/commands/blocks.rs | 6 ++- src/entities/extended.rs | 14 +++-- src/modules/insert/pc_crop.rs | 82 ++++++++++++++++++++++-------- src/scene/model/point_cloud/mod.rs | 15 ++++-- 4 files changed, 81 insertions(+), 36 deletions(-) diff --git a/src/app/commands/blocks.rs b/src/app/commands/blocks.rs index f5f2e0cb1..7f671436b 100644 --- a/src/app/commands/blocks.rs +++ b/src/app/commands/blocks.rs @@ -720,9 +720,11 @@ impl OpenCADStudio { self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); } - "POINTCLOUDCROP" => { + "POINTCLOUDCROP" | "POINTCLOUDUNCROP" => { use crate::command::CadCommand; - let command = crate::modules::insert::pc_crop::PointCloudCropCommand::new(); + 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)); } diff --git a/src/entities/extended.rs b/src/entities/extended.rs index 574b752f3..cd7033929 100644 --- a/src/entities/extended.rs +++ b/src/entities/extended.rs @@ -535,22 +535,20 @@ fn point_cloud_ex_clip_lines(data: &PointCloudExData) -> Vec<[f64; 3]> { if crop.points.len() < 2 { continue; } - // Crop boundaries are kept in the cloud's own coordinates, on - // the crop plane through `plane` spanned by its two directions. - let normal = crop.x_direction.cross(&crop.y_direction); + // Crop points are kept in the cloud's own coordinates; the crop + // runs along the normal of the plane its two directions span. let place = |p: Vector3| { - let local = crop.plane + crop.x_direction * p.x + crop.y_direction * p.y + normal * p.z; - let w = codec::entities::point_cloud_ex_to_world(data, local); + 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 = if crop.crop_type == 3 { - let (c, e) = (crop.points[0], crop.points[1]); - let r = ((e.x - c.x).powi(2) + (e.y - c.y).powi(2)).sqrt(); + 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; - Vector3::new(c.x + r * a.cos(), c.y + r * a.sin(), c.z) + c + crop.x_direction * (r * a.cos()) + crop.y_direction * (r * a.sin()) }) .collect() } else { diff --git a/src/modules/insert/pc_crop.rs b/src/modules/insert/pc_crop.rs index 3460278a9..d9ca86f68 100644 --- a/src/modules/insert/pc_crop.rs +++ b/src/modules/insert/pc_crop.rs @@ -1,4 +1,5 @@ // 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]: @@ -8,9 +9,10 @@ // Specify center point: / Specify radius: (Circular) // Keep points inside or outside? [Inside/Outside] : // -// Crops are kept in the cloud's own coordinates, on the plane of its X and -// Y axes: 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, +// 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; @@ -59,6 +61,10 @@ pub struct PointCloudCropCommand { picked: Option, shape: Shape, points: Vec, + /// POINTCLOUDUNCROP: the picked cloud loses its crops. + uncrop: bool, + /// The view's right and up directions in the drawing. + view: (DVec3, DVec3), } impl PointCloudCropCommand { @@ -70,9 +76,15 @@ impl PointCloudCropCommand { 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 { @@ -108,13 +120,21 @@ impl PointCloudCropCommand { CmdResult::UpdateEntityAndFinish { handle: self.handle, entity: EntityType::Extended(cloud) } } - /// A point of the drawing in the cloud's own coordinates. - fn to_local(data: &PointCloudExData, p: DVec3) -> Vector3 { + fn axes(data: &PointCloudExData) -> glam::DMat3 { let v = |a: Vector3| DVec3::new(a.x, a.y, a.z); - let axes = glam::DMat3::from_cols(v(data.ucs_x_direction), v(data.ucs_y_direction), v(data.ucs_z_direction)); - let local = axes.inverse() * (p - v(data.ucs_origin)); - // On the plane of the cloud's X and Y axes. - Vector3::new(local.x, local.y, 0.0) + 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 { @@ -157,24 +177,26 @@ impl PointCloudCropCommand { let Some(data) = self.data() else { return CmdResult::Cancel; }; - let points: Vec = match self.shape { + let (right, up) = self.view_local(data); + let local: Vec = 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, b) = (self.points[0], self.points[1]); - [DVec3::new(a.x, a.y, a.z), DVec3::new(b.x, a.y, a.z), DVec3::new(b.x, b.y, a.z), DVec3::new(a.x, b.y, a.z)] - .into_iter() - .map(|p| Self::to_local(data, p)) - .collect() + 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] } - _ => self.points.iter().map(|p| Self::to_local(data, *p)).collect(), + _ => 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: Vector3::UNIT_X, - y_direction: Vector3::UNIT_Y, - points, + x_direction: vector(right), + y_direction: vector(up), + points: local.into_iter().map(vector).collect(), }; self.finish(move |data| { data.croppings.push(crop); @@ -190,7 +212,7 @@ impl PointCloudCropCommand { impl CadCommand for PointCloudCropCommand { fn name(&self) -> &'static str { - "POINTCLOUDCROP" + if self.uncrop { "POINTCLOUDUNCROP" } else { "POINTCLOUDCROP" } } fn prompt(&self) -> String { @@ -247,6 +269,21 @@ impl CadCommand for PointCloudCropCommand { 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) { + // 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 } @@ -260,6 +297,9 @@ impl CadCommand for PointCloudCropCommand { 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 } @@ -366,4 +406,4 @@ impl CadCommand for PointCloudCropCommand { } } -inventory::submit!(crate::command::CommandRegistration { names: &["POINTCLOUDCROP"] }); +inventory::submit!(crate::command::CommandRegistration { names: &["POINTCLOUDCROP", "POINTCLOUDUNCROP"] }); diff --git a/src/scene/model/point_cloud/mod.rs b/src/scene/model/point_cloud/mod.rs index 553262dfc..2ae89752e 100644 --- a/src/scene/model/point_cloud/mod.rs +++ b/src/scene/model/point_cloud/mod.rs @@ -260,9 +260,10 @@ pub(crate) fn placed( ) -> Option> { let cloud = load(&resolve_source(document, data)?)?; // The points shown: POINTCLOUDDENSITY per cent of POINTCLOUDPOINTMAX, - // every n-th point of a larger cloud. + // 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).max(1) as usize; + 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; @@ -382,9 +383,13 @@ pub(crate) fn placed( /// is in that plane's coordinates. `inside` keeps the prism's interior and /// `inverted` flips it. fn keeps(crop: &PointCloudExCrop, p: &[f64; 3]) -> bool { - let d = Vector3::new(p[0], p[1], p[2]) - crop.plane; - let (x, y) = (d.dot(&crop.x_direction), d.dot(&crop.y_direction)); - let points = &crop.points; + // 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 = 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]);