forked from 1b-agent/opencadstudio-upstream
Point clouds: attach dialog, crop toolbar, stylize, palette rows
POINTCLOUDATTACH opens the Attach Point Cloud dialog after the file is chosen; its Attach button lives in the Attach Underlay list instead of a separate Insert panel. A selected cloud brings the crop tools to the right-edge toolbar. POINTCLOUDSTYLIZE sets the stylization with the reference's prompts, the External References palette lists point clouds (unload, reload, detach, path), and definitions keep the header extents. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
3afd547e28
commit
c2d9ac5ae2
48 files changed
+1711
-83
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;
|
||||
|
||||
@@ -18,6 +18,7 @@ mod mview_block;
|
||||
mod open_obj;
|
||||
pub(crate) mod pc_attach;
|
||||
pub(crate) mod pc_crop;
|
||||
pub(crate) mod pc_stylize;
|
||||
pub(crate) mod pdf_attach;
|
||||
pub(crate) mod pdf_clip;
|
||||
pub(crate) mod pdf_import;
|
||||
@@ -54,7 +55,8 @@ impl CadModule for InsertModule {
|
||||
title: "Reference",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(xattach::tool()),
|
||||
// PDF, DWF or DGN; the face runs the last one chosen.
|
||||
// PDF, DWF, DGN or a point cloud; the face runs the
|
||||
// last one chosen.
|
||||
RibbonItem::LargeDropdown {
|
||||
id: "UNDERLAY_ATTACH",
|
||||
label: "Attach Underlay",
|
||||
@@ -63,6 +65,7 @@ impl CadModule for InsertModule {
|
||||
("PDFATTACH", "Attach PDF", pdf_attach::ICON),
|
||||
("DWFATTACH", "Attach DWF", pdf_attach::ICON),
|
||||
("DGNATTACH", "Attach DGN", pdf_attach::ICON),
|
||||
("POINTCLOUDATTACH", "Attach Point Cloud", pc_attach::ICON),
|
||||
],
|
||||
default: "PDFATTACH",
|
||||
},
|
||||
@@ -96,11 +99,6 @@ impl CadModule for InsertModule {
|
||||
},
|
||||
],
|
||||
},
|
||||
// ── Point Cloud ───────────────────────────────────────────────────
|
||||
RibbonGroup {
|
||||
title: "Point Cloud",
|
||||
tools: vec![RibbonItem::LargeTool(pc_attach::tool())],
|
||||
},
|
||||
// ── Block ─────────────────────────────────────────────────────────
|
||||
RibbonGroup {
|
||||
title: "Block",
|
||||
|
||||
@@ -16,15 +16,33 @@ use std::path::PathBuf;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, InputKind, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::modules::IconKind;
|
||||
|
||||
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/pc_attach.svg"));
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "POINTCLOUDATTACH",
|
||||
label: "Attach",
|
||||
icon: ICON,
|
||||
event: ModuleEvent::Command("POINTCLOUDATTACH".to_string()),
|
||||
|
||||
/// What the Attach Point Cloud dialog settles before the prompts: the
|
||||
/// path to store, the switches, and the scale and rotation not left to
|
||||
/// the command line.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AttachOptions {
|
||||
/// The path stored as given (no path: the file name); None stores the
|
||||
/// full path.
|
||||
pub stored: Option<String>,
|
||||
/// Stored relative to the drawing (once it has a file).
|
||||
pub relative: bool,
|
||||
/// Locked on attach; None follows POINTCLOUDLOCK.
|
||||
pub locked: Option<bool>,
|
||||
/// Zoom to the cloud once attached.
|
||||
pub zoom: bool,
|
||||
pub scale: Option<f64>,
|
||||
pub rotation: Option<f64>,
|
||||
}
|
||||
|
||||
impl Default for AttachOptions {
|
||||
fn default() -> Self {
|
||||
// The command line attaches with a relative path, as the dialog
|
||||
// does by default.
|
||||
Self { stored: None, relative: true, locked: None, zoom: false, scale: None, rotation: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +53,7 @@ pub struct PointCloudPlacement {
|
||||
pub insertion: DVec3,
|
||||
/// The placement's axes in the drawing, each as long as the scale.
|
||||
pub axes: [DVec3; 3],
|
||||
pub options: AttachOptions,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
@@ -51,6 +70,7 @@ pub struct PointCloudAttachCommand {
|
||||
insertion: DVec3,
|
||||
scale: f64,
|
||||
plane: WorkingPlane,
|
||||
options: AttachOptions,
|
||||
}
|
||||
|
||||
/// A number as the reference echoes a coordinate or an angle: no trailing
|
||||
@@ -67,9 +87,17 @@ impl PointCloudAttachCommand {
|
||||
insertion: DVec3::ZERO,
|
||||
scale: 1.0,
|
||||
plane: WorkingPlane::default(),
|
||||
options: AttachOptions::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// From the Attach Point Cloud dialog: the file chosen, what it set.
|
||||
pub fn from_dialog(path: PathBuf, options: AttachOptions) -> Self {
|
||||
let mut command = Self::with_file(path);
|
||||
command.options = options;
|
||||
command
|
||||
}
|
||||
|
||||
/// Starts at the insertion point, the file already chosen.
|
||||
pub fn with_file(path: PathBuf) -> Self {
|
||||
let mut command = Self::new();
|
||||
@@ -103,12 +131,22 @@ impl PointCloudAttachCommand {
|
||||
self.insertion = point;
|
||||
self.step = Step::Scale;
|
||||
let local = self.plane.to_local(point);
|
||||
CmdResult::ReportMeasurement(format!(
|
||||
let message = format!(
|
||||
"Current insertion point: X = {}, Y = {}, Z = {}",
|
||||
plain(local.x),
|
||||
plain(local.y),
|
||||
plain(local.z)
|
||||
))
|
||||
);
|
||||
// Scale and rotation the dialog set are not asked for.
|
||||
let Some(scale) = self.options.scale else {
|
||||
return CmdResult::ReportMeasurement(message);
|
||||
};
|
||||
self.scale = scale;
|
||||
self.step = Step::Rotation;
|
||||
match self.options.rotation {
|
||||
Some(degrees) => self.placed(degrees, message),
|
||||
None => CmdResult::ReportMeasurement(message),
|
||||
}
|
||||
}
|
||||
|
||||
fn accept_scale(&mut self, text: &str) -> CmdResult {
|
||||
@@ -129,10 +167,18 @@ impl PointCloudAttachCommand {
|
||||
}
|
||||
self.scale = scale;
|
||||
self.step = Step::Rotation;
|
||||
CmdResult::ReportMeasurement(format!("Current scale factor: {scale:.6}"))
|
||||
let message = format!("Current scale factor: {scale:.6}");
|
||||
match self.options.rotation {
|
||||
Some(degrees) => self.placed(degrees, message),
|
||||
None => CmdResult::ReportMeasurement(message),
|
||||
}
|
||||
}
|
||||
|
||||
fn accept_rotation(&mut self, degrees: f64) -> CmdResult {
|
||||
self.placed(degrees, format!("Current rotate angle: {}", plain(degrees)))
|
||||
}
|
||||
|
||||
fn placed(&mut self, degrees: f64, message: String) -> CmdResult {
|
||||
let (sin, cos) = degrees.to_radians().sin_cos();
|
||||
let x = (self.plane.x * cos + self.plane.y * sin) * self.scale;
|
||||
let y = (self.plane.y * cos - self.plane.x * sin) * self.scale;
|
||||
@@ -141,8 +187,9 @@ impl PointCloudAttachCommand {
|
||||
path: self.path.clone(),
|
||||
insertion: self.insertion,
|
||||
axes: [x, y, self.plane.z * self.scale],
|
||||
options: self.options.clone(),
|
||||
},
|
||||
message: format!("Current rotate angle: {}", plain(degrees)),
|
||||
message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,19 @@ impl PointCloudCropCommand {
|
||||
}
|
||||
}
|
||||
|
||||
/// The toolbar's crops: the cloud already chosen, the shape too
|
||||
/// ("P" polygon, "C" circle, "" rectangle).
|
||||
pub fn for_cloud(handle: Handle, cloud: ExtendedEntity, option: &str) -> Self {
|
||||
let mut command = Self::new();
|
||||
command.handle = handle;
|
||||
command.cloud = Some(cloud);
|
||||
command.step = Step::First;
|
||||
if !option.is_empty() {
|
||||
command.option(option);
|
||||
}
|
||||
command
|
||||
}
|
||||
|
||||
fn data(&self) -> Option<&PointCloudExData> {
|
||||
match &self.cloud.as_ref()?.data {
|
||||
ExtendedEntityData::PointCloudEx(data) => Some(data),
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// POINTCLOUDSTYLIZE — colour the chosen point clouds by one stylization.
|
||||
//
|
||||
// Select point cloud objects:
|
||||
// Enter a stylization option [RGB/Object color/Intensity/Elevation/Normal/Classification] <RGB>:
|
||||
// 1 point cloud(s) stylized
|
||||
//
|
||||
// The option offered is the last one chosen. The host applies it (one undo
|
||||
// step) and reports clouds that lack the data.
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use codec::types::Handle;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
|
||||
|
||||
/// The options with their stored stylization values.
|
||||
pub const OPTIONS: [(&str, &str, i16); 6] = [
|
||||
("RGB", "R", 1),
|
||||
("Object color", "O", 2),
|
||||
("Intensity", "I", 5),
|
||||
("Elevation", "E", 4),
|
||||
("Normal", "N", 3),
|
||||
("Classification", "C", 6),
|
||||
];
|
||||
|
||||
/// The option last chosen (RGB to begin with).
|
||||
static LAST: Mutex<usize> = Mutex::new(0);
|
||||
|
||||
pub struct PointCloudStylizeCommand {
|
||||
handles: Vec<Handle>,
|
||||
}
|
||||
|
||||
impl PointCloudStylizeCommand {
|
||||
pub fn new(handles: Vec<Handle>) -> Self {
|
||||
Self { handles }
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for PointCloudStylizeCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"POINTCLOUDSTYLIZE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
let last = LAST.lock().map(|v| *v).unwrap_or(0);
|
||||
format!(
|
||||
"Enter a stylization option [RGB/Object color/Intensity/Elevation/Normal/Classification] <{}>:",
|
||||
OPTIONS[last].0
|
||||
)
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
OPTIONS.iter().map(|(name, key, _)| CmdOption::new(name, key)).collect()
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
InputKind::SingleToken
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let text = text.trim().to_ascii_uppercase();
|
||||
let chosen = if text.is_empty() {
|
||||
LAST.lock().map(|v| *v).unwrap_or(0)
|
||||
} else {
|
||||
match OPTIONS.iter().position(|(name, key, _)| {
|
||||
text == *key || (text.len() >= key.len() && name.to_ascii_uppercase().starts_with(&text))
|
||||
}) {
|
||||
Some(at) => at,
|
||||
None => return Some(CmdResult::ReportError("Invalid option keyword.".to_string())),
|
||||
}
|
||||
};
|
||||
if let Ok(mut last) = LAST.lock() {
|
||||
*last = chosen;
|
||||
}
|
||||
let handles: Vec<String> = self.handles.iter().map(|h| format!("{:X}", h.value())).collect();
|
||||
Some(CmdResult::Dispatch(format!("_PCSTYLIZEAPPLY {} {}", OPTIONS[chosen].2, handles.join(" "))))
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: glam::DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
self.on_text_input("").unwrap_or(CmdResult::Cancel)
|
||||
}
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["POINTCLOUDSTYLIZE"] });
|
||||
Reference in new issue
Block a user