forked from 1b-agent/opencadstudio-upstream
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 <noreply@anthropic.com>
This commit is contained in:
commit
3dcebf0dc6
81 files changed
+9162
-305
No files matched your search
@@ -124,6 +124,13 @@ impl SelectObjectsCommand {
|
||||
command
|
||||
}
|
||||
|
||||
/// The standard gather with the command's own wording.
|
||||
pub fn with_prompt(prompt_cmd: &str, pending_cmd: &str, prompt_text: &'static str) -> Self {
|
||||
let mut command = Self::plain(prompt_cmd, pending_cmd);
|
||||
command.prompt_text = Some(prompt_text);
|
||||
command
|
||||
}
|
||||
|
||||
pub fn auto_constrain(pending_cmd: &str) -> Self {
|
||||
let mut command = Self::new(pending_cmd);
|
||||
command.auto_constrain_settings = true;
|
||||
@@ -196,6 +203,13 @@ impl CadCommand for SelectObjectsCommand {
|
||||
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
|
||||
// sense of the next box, which dragging would otherwise take from the
|
||||
// direction the corner travels; All takes the whole space; Add and Remove
|
||||
|
||||
@@ -16,7 +16,10 @@ pub(crate) mod picker;
|
||||
pub mod minsert;
|
||||
mod mview_block;
|
||||
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_clip;
|
||||
pub(crate) mod pdf_import;
|
||||
@@ -53,7 +56,8 @@ impl CadModule for InsertModule {
|
||||
title: "Reference",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(xattach::tool()),
|
||||
// PDF, DWF or DGN; the face runs the last one chosen.
|
||||
// PDF, DWF, DGN or a point cloud; the face runs the
|
||||
// last one chosen.
|
||||
RibbonItem::LargeDropdown {
|
||||
id: "UNDERLAY_ATTACH",
|
||||
label: "Attach Underlay",
|
||||
@@ -62,6 +66,7 @@ impl CadModule for InsertModule {
|
||||
("PDFATTACH", "Attach PDF", pdf_attach::ICON),
|
||||
("DWFATTACH", "Attach DWF", pdf_attach::ICON),
|
||||
("DGNATTACH", "Attach DGN", pdf_attach::ICON),
|
||||
("POINTCLOUDATTACH", "Attach Point Cloud", pc_attach::ICON),
|
||||
],
|
||||
default: "PDFATTACH",
|
||||
},
|
||||
@@ -95,11 +100,6 @@ impl CadModule for InsertModule {
|
||||
},
|
||||
],
|
||||
},
|
||||
// ── Point Cloud ───────────────────────────────────────────────────
|
||||
RibbonGroup {
|
||||
title: "Point Cloud",
|
||||
tools: vec![RibbonItem::LargeTool(pc_attach::tool())],
|
||||
},
|
||||
// ── Block ─────────────────────────────────────────────────────────
|
||||
RibbonGroup {
|
||||
title: "Block",
|
||||
|
||||
@@ -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 fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "POINTCLOUDATTACH",
|
||||
label: "Attach",
|
||||
icon: ICON,
|
||||
event: ModuleEvent::Command("POINTCLOUDATTACH".to_string()),
|
||||
|
||||
/// What the Attach Point Cloud dialog settles before the prompts: the
|
||||
/// path to store, the switches, and the scale and rotation not left to
|
||||
/// the command line.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AttachOptions {
|
||||
/// The path stored as given (no path: the file name); None stores the
|
||||
/// full path.
|
||||
pub stored: Option<String>,
|
||||
/// Stored relative to the drawing (once it has a file).
|
||||
pub relative: bool,
|
||||
/// Locked on attach; None follows POINTCLOUDLOCK.
|
||||
pub locked: Option<bool>,
|
||||
/// Zoom to the cloud once attached.
|
||||
pub zoom: bool,
|
||||
pub scale: Option<f64>,
|
||||
pub rotation: Option<f64>,
|
||||
}
|
||||
|
||||
impl Default for AttachOptions {
|
||||
fn default() -> Self {
|
||||
// The command line attaches with a relative path, as the dialog
|
||||
// does by default.
|
||||
Self { stored: None, relative: true, locked: None, zoom: false, scale: None, rotation: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// 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"]
|
||||
});
|
||||
@@ -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
@@ -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"] });
|
||||
Reference in new issue
Block a user