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 <[email protected]>
This commit is contained in:
commit
3dcebf0dc6
81 files changed
+9162
-305
No files matched your search
@@ -615,6 +615,15 @@ impl OpenCADStudio {
|
||||
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 } => {
|
||||
let label = self.history_label_from_active_cmd(i, "PDFATTACH");
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
@@ -1094,7 +1103,9 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -490,6 +490,47 @@ impl OpenCADStudio {
|
||||
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.
|
||||
/// 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
|
||||
@@ -500,6 +541,7 @@ impl OpenCADStudio {
|
||||
// as the reference does while gathering.
|
||||
let input = self.filter_associative_dimension_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.
|
||||
{
|
||||
let i = self.active_tab;
|
||||
|
||||
@@ -720,6 +720,14 @@ impl OpenCADStudio {
|
||||
self.command_line.push_info(&command.prompt());
|
||||
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" => {
|
||||
use crate::command::CadCommand;
|
||||
let command = crate::modules::insert::pdf_clip::PdfClipCommand::image();
|
||||
|
||||
@@ -1911,7 +1911,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
"UNDERLAYLAYERS" | "ULAYERS" => self.open_underlay_layers_dialog(i),
|
||||
"POINTCLOUDATTACH" | "RECAP" | "SYNCPVIEWPORTS" => {
|
||||
"RECAP" | "SYNCPVIEWPORTS" => {
|
||||
self.command_line
|
||||
.push_info(crate::tf!("{cmd}: not yet implemented.").as_ref());
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ mod inquiry;
|
||||
mod layerprops;
|
||||
mod layers;
|
||||
pub(crate) mod pdf_import;
|
||||
mod point_cloud;
|
||||
mod pc_colormap;
|
||||
mod pc_extract;
|
||||
mod pdf_underlay;
|
||||
mod pdf_dialogs;
|
||||
mod xclip;
|
||||
@@ -269,6 +272,12 @@ impl OpenCADStudio {
|
||||
if let Some(t) = self.dispatch_pdf_underlay(cmd, i) {
|
||||
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) {
|
||||
return Some(t);
|
||||
}
|
||||
@@ -636,6 +645,21 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"PDFIMPORTIMAGEPATH",
|
||||
"XDWGFADECTL",
|
||||
"POINTCLOUDCLIPFRAME",
|
||||
"POINTCLOUDDENSITY",
|
||||
"POINTCLOUDPOINTSIZE",
|
||||
"POINTCLOUDLOCK",
|
||||
"POINTCLOUDAUTOUPDATE",
|
||||
"POINTCLOUDBOUNDARY",
|
||||
"POINTCLOUDRTDENSITY",
|
||||
"POINTCLOUDLOD",
|
||||
"POINTCLOUDPOINTMAX",
|
||||
"POINTCLOUDVISRETAIN",
|
||||
"POINTCLOUDSHADING",
|
||||
"POINTCLOUDCACHESIZE",
|
||||
"POINTCLOUD2DVSDISPLAY",
|
||||
"POINTCLOUDLIGHTING",
|
||||
"POINTCLOUDLIGHTSOURCE",
|
||||
"POINTCLOUDPOINTMAXLEGACY",
|
||||
"XCLIPFRAME",
|
||||
"WIPEOUTFRAME",
|
||||
"FRAMES0",
|
||||
@@ -716,6 +740,8 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"EXPORTSTEP",
|
||||
"EXPORTSTL",
|
||||
"EXTERNALREFERENCES",
|
||||
"POINTCLOUDMANAGER",
|
||||
"POINTCLOUDMANAGERCLOSE",
|
||||
"EXTRIM",
|
||||
"FILETAB",
|
||||
"FIND",
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use crate::io::xref_model::Pathtype;
|
||||
use crate::modules::insert::pdf_import::{self, PdfFileImport};
|
||||
use crate::ui::window::pdf_dialogs::{
|
||||
click_page, item_thumbs, page_thumbs, LayerTarget, PdfAttachState, PdfDialogMsg, PdfImportFileState,
|
||||
RotationChoice, UnderlayLayersState,
|
||||
PointCloudAttachState, RotationChoice, UnderlayLayersState,
|
||||
};
|
||||
|
||||
/// "7.8740 × 3.9369" (inches) for a page.
|
||||
@@ -102,6 +102,139 @@ impl OpenCADStudio {
|
||||
crate::entities::underlay::display_path(&self.pdf_stored_path(path, state.path_type.0));
|
||||
}
|
||||
|
||||
/// POINTCLOUDATTACH after the file is chosen: the Attach Point Cloud
|
||||
/// dialog, placed on screen by default.
|
||||
pub(in crate::app) fn open_point_cloud_attach_dialog(&mut self, path: &std::path::Path) {
|
||||
let Some(cloud) = crate::scene::model::point_cloud::load(path) else {
|
||||
return;
|
||||
};
|
||||
let yes_no = |on: bool| if on { crate::t!("Yes") } else { crate::t!("No") }.into_owned();
|
||||
let data = format!(
|
||||
"RGB: {} · {}: {} · {}: {}",
|
||||
yes_no(cloud.has_rgb),
|
||||
crate::t!("Intensity"),
|
||||
yes_no(cloud.has_intensity),
|
||||
crate::t!("Normals"),
|
||||
yes_no(cloud.has_normals),
|
||||
);
|
||||
let data_more = format!(
|
||||
"{}: {} · {}: {}",
|
||||
crate::t!("Classification"),
|
||||
yes_no(false),
|
||||
crate::t!("Segmentation"),
|
||||
yes_no(false),
|
||||
);
|
||||
let factor = crate::app::properties::insert_unit_scale(self.tabs[self.active_tab].scene.document.header.insertion_units, 6)
|
||||
.unwrap_or(1.0);
|
||||
let full = path.to_string_lossy().into_owned();
|
||||
let memory = crate::ui::window::pdf_dialogs::ATTACH_MEMORY.lock().ok().and_then(|m| m.clone()).unwrap_or_default();
|
||||
let mut state = PointCloudAttachState {
|
||||
path: full.clone(),
|
||||
name: file_stem(&full),
|
||||
preview: cloud.preview.clone().map(iced::widget::image::Handle::from_bytes),
|
||||
summary: format!(
|
||||
"{} · {}",
|
||||
crate::tf!("{count} scans", count = cloud.scans),
|
||||
crate::tf!("{count} points", count = cloud.positions.len())
|
||||
),
|
||||
data,
|
||||
data_more,
|
||||
size: cloud
|
||||
.bounds
|
||||
.map(|[lo, hi]| format!("{:.4} × {:.4} × {:.4}", hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]))
|
||||
.unwrap_or_default(),
|
||||
unit: format!("{} · {} {}", crate::t!("Meters"), crate::t!("Unit factor"), crate::app::properties::format_unit_factor(factor)),
|
||||
found_in: crate::entities::underlay::display_path(&full),
|
||||
saved_path: String::new(),
|
||||
path_type: memory.path_type,
|
||||
insert_on_screen: true,
|
||||
insert: ["0.0000".into(), "0.0000".into(), "0.0000".into()],
|
||||
scale_on_screen: true,
|
||||
scale: "1.0000".into(),
|
||||
rotation_on_screen: true,
|
||||
rotation: "0".into(),
|
||||
lock: false,
|
||||
zoom: true,
|
||||
};
|
||||
state.saved_path = crate::entities::underlay::display_path(&self.pdf_stored_path(&full, state.path_type.0));
|
||||
self.point_cloud_attach = Some(state);
|
||||
self.active_modal = Some(crate::app::ModalKind::PointCloudAttach);
|
||||
}
|
||||
|
||||
/// The dialog's Attach: what it set goes to the command, which asks for
|
||||
/// the rest.
|
||||
fn point_cloud_attach_ok(&mut self, i: usize) -> Task<Message> {
|
||||
use crate::command::CadCommand;
|
||||
let Some(state) = self.point_cloud_attach.as_ref() else {
|
||||
return Task::none();
|
||||
};
|
||||
let number = |s: &str| s.trim().parse::<f64>().ok();
|
||||
let insertion = if state.insert_on_screen {
|
||||
None
|
||||
} else {
|
||||
match (number(&state.insert[0]), number(&state.insert[1]), number(&state.insert[2])) {
|
||||
(Some(x), Some(y), Some(z)) => Some(glam::DVec3::new(x, y, z)),
|
||||
_ => {
|
||||
self.command_line.push_error("Requires numeric value.");
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
};
|
||||
let scale = if state.scale_on_screen {
|
||||
None
|
||||
} else {
|
||||
match number(&state.scale).filter(|v| *v > 0.0) {
|
||||
Some(v) => Some(v),
|
||||
None => {
|
||||
self.command_line.push_error("Value must be positive and nonzero.");
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
};
|
||||
let rotation = if state.rotation_on_screen {
|
||||
None
|
||||
} else {
|
||||
match number(&state.rotation) {
|
||||
Some(v) => Some(v),
|
||||
None => {
|
||||
self.command_line.push_error("Requires numeric value.");
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
};
|
||||
let state = self.point_cloud_attach.take().expect("checked above");
|
||||
self.close_active_modal();
|
||||
let options = crate::modules::insert::pc_attach::AttachOptions {
|
||||
stored: (state.path_type.0 == Pathtype::None).then(|| {
|
||||
std::path::Path::new(&state.path)
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_default()
|
||||
}),
|
||||
relative: state.path_type.0 == Pathtype::Relative,
|
||||
locked: Some(state.lock),
|
||||
zoom: state.zoom,
|
||||
scale,
|
||||
rotation,
|
||||
};
|
||||
let mut command = crate::modules::insert::pc_attach::PointCloudAttachCommand::from_dialog(
|
||||
std::path::PathBuf::from(&state.path),
|
||||
options,
|
||||
);
|
||||
match insertion {
|
||||
Some(point) => {
|
||||
let result = command.on_point(point);
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
self.apply_cmd_result(result)
|
||||
}
|
||||
None => {
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
self.focus_cmd_input()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ULAYERS: every underlay of the drawing, the selected one first shown.
|
||||
pub(in crate::app) fn open_underlay_layers_dialog(&mut self, i: usize) {
|
||||
let document = &self.tabs[i].scene.document;
|
||||
@@ -244,6 +377,11 @@ impl OpenCADStudio {
|
||||
crate::t!("Attaches a DGN file as an underlay.")
|
||||
}
|
||||
"attach" => crate::t!("Attaches a PDF file as an underlay."),
|
||||
"pointcloud" => crate::t!("Attaches a point cloud scan or project."),
|
||||
"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."),
|
||||
_ => 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),
|
||||
|
||||
// ── 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 ───────────────────────────────────────────
|
||||
PdfDialogMsg::LayersUnderlay(name) => {
|
||||
if let Some(s) = self.underlay_layers.as_mut() {
|
||||
|
||||
@@ -62,6 +62,154 @@ impl OpenCADStudio {
|
||||
})
|
||||
};
|
||||
self.ribbon.set_underlay_context(context, xref);
|
||||
let clouds = if self.tabs[i].is_start { Vec::new() } else { self.selected_point_clouds(i) };
|
||||
self.ribbon.set_point_cloud_context(clouds.first().map(|(_, data)| data.show_cropping));
|
||||
}
|
||||
|
||||
/// The selected point clouds, or nothing when anything else is selected.
|
||||
fn selected_point_clouds(&self, i: usize) -> Vec<(codec::Handle, codec::entities::PointCloudExData)> {
|
||||
let mut out = Vec::new();
|
||||
for (handle, entity) in self.tabs[i].scene.selected_entities() {
|
||||
match entity {
|
||||
codec::EntityType::Extended(extended) => match &extended.data {
|
||||
codec::entities::ExtendedEntityData::PointCloudEx(data) => out.push((handle, data.clone())),
|
||||
_ => return Vec::new(),
|
||||
},
|
||||
_ => return Vec::new(),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// POINTCLOUDSTYLIZE's result: the clouds that carry the data take the
|
||||
/// stylization (one undo step); the rest are named.
|
||||
fn stylize_point_clouds(&mut self, i: usize, handles: &[codec::Handle], stylization: i16) {
|
||||
let mut done = Vec::new();
|
||||
for &handle in handles {
|
||||
if self.tabs[i].scene.is_layer_locked(handle) {
|
||||
continue;
|
||||
}
|
||||
let document = &self.tabs[i].scene.document;
|
||||
let Some(codec::EntityType::Extended(extended)) = document.get_entity(handle) else {
|
||||
continue;
|
||||
};
|
||||
let codec::entities::ExtendedEntityData::PointCloudEx(data) = &extended.data else {
|
||||
continue;
|
||||
};
|
||||
let cloud = crate::scene::model::point_cloud::resolve_source(document, data)
|
||||
.and_then(|path| crate::scene::model::point_cloud::load(&path));
|
||||
let supported = match (stylization, &cloud) {
|
||||
(6, _) => false,
|
||||
(_, None) => true,
|
||||
(1, Some(c)) => c.has_rgb,
|
||||
(5, Some(c)) => c.has_intensity,
|
||||
(3, Some(c)) => c.has_normals,
|
||||
_ => true,
|
||||
};
|
||||
if supported {
|
||||
done.push(handle);
|
||||
} else {
|
||||
self.command_line.push_info(&format!(
|
||||
"The selected point cloud({}) does not support this stylization type.",
|
||||
data.name
|
||||
));
|
||||
}
|
||||
}
|
||||
if !done.is_empty() {
|
||||
self.push_undo_snapshot(i, "POINTCLOUDSTYLIZE");
|
||||
for handle in &done {
|
||||
if let Some(codec::EntityType::Extended(extended)) = self.tabs[i].scene.document.get_entity_mut(*handle) {
|
||||
if let codec::entities::ExtendedEntityData::PointCloudEx(data) = &mut extended.data {
|
||||
crate::entities::extended::set_point_cloud_stylization(data, stylization);
|
||||
}
|
||||
}
|
||||
}
|
||||
let changes: Vec<_> = done.iter().map(|h| (*h, crate::scene::ChangeKind::Modified)).collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
self.command_line.push_output(&format!("{} point cloud(s) stylized", done.len()));
|
||||
}
|
||||
|
||||
/// One undo step that edits every selected point cloud not locked.
|
||||
fn edit_selected_point_clouds(
|
||||
&mut self,
|
||||
i: usize,
|
||||
label: &str,
|
||||
edit: impl Fn(&mut codec::entities::PointCloudExData),
|
||||
) {
|
||||
let handles: Vec<_> = self
|
||||
.selected_point_clouds(i)
|
||||
.into_iter()
|
||||
.map(|(h, _)| h)
|
||||
.filter(|h| !self.tabs[i].scene.is_layer_locked(*h))
|
||||
.collect();
|
||||
if handles.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.push_undo_snapshot(i, label);
|
||||
for handle in &handles {
|
||||
if let Some(codec::EntityType::Extended(extended)) = self.tabs[i].scene.document.get_entity_mut(*handle) {
|
||||
if let codec::entities::ExtendedEntityData::PointCloudEx(data) = &mut extended.data {
|
||||
edit(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
let changes: Vec<_> = handles.iter().map(|h| (*h, crate::scene::ChangeKind::Modified)).collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -156,6 +304,88 @@ impl OpenCADStudio {
|
||||
self.apply_xclip(i, inserts, crate::modules::insert::xclip::XclipAction::Delete);
|
||||
}
|
||||
}
|
||||
// Point cloud tools: a crop of the first selected cloud, or the
|
||||
// crops of every selected one.
|
||||
"_PCCROPRECT" | "_PCCROPPOLY" | "_PCCROPCIRC" => {
|
||||
use crate::command::CadCommand;
|
||||
let Some(&(handle, _)) = self.selected_point_clouds(i).first() else {
|
||||
return Some(Task::none());
|
||||
};
|
||||
if self.tabs[i].scene.is_layer_locked(handle) {
|
||||
return Some(Task::none());
|
||||
}
|
||||
let Some(codec::EntityType::Extended(cloud)) = self.tabs[i].scene.document.get_entity(handle).cloned() else {
|
||||
return Some(Task::none());
|
||||
};
|
||||
let option = match cmd {
|
||||
"_PCCROPPOLY" => "P",
|
||||
"_PCCROPCIRC" => "C",
|
||||
_ => "",
|
||||
};
|
||||
let command = crate::modules::insert::pc_crop::PointCloudCropCommand::for_cloud(handle, cloud, option);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
"POINTCLOUDSTYLIZE" => {
|
||||
use crate::command::CadCommand;
|
||||
let command = crate::modules::draw::select::SelectObjectsCommand::with_prompt(
|
||||
"POINTCLOUDSTYLIZE",
|
||||
"_PCSTYLIZE",
|
||||
"Select point cloud objects:",
|
||||
);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
// The chosen objects' point clouds, then the option.
|
||||
"_PCSTYLIZE" => {
|
||||
use crate::command::CadCommand;
|
||||
let handles: Vec<codec::Handle> = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.into_iter()
|
||||
.filter(|(_, entity)| crate::scene::is_point_cloud(entity))
|
||||
.map(|(h, _)| h)
|
||||
.collect();
|
||||
if handles.is_empty() {
|
||||
return Some(self.finish_dispatch(cmd));
|
||||
}
|
||||
let command = crate::modules::insert::pc_stylize::PointCloudStylizeCommand::new(handles);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
c if c.starts_with("_PCSTYLIZEAPPLY ") => {
|
||||
let mut parts = c.split_whitespace().skip(1);
|
||||
let stylization: i16 = parts.next().and_then(|v| v.parse().ok()).unwrap_or(1);
|
||||
let handles: Vec<codec::Handle> =
|
||||
parts.filter_map(|h| u64::from_str_radix(h, 16).ok()).map(codec::Handle::new).collect();
|
||||
self.stylize_point_clouds(i, &handles, stylization);
|
||||
}
|
||||
"_PCCROPSHOW" => {
|
||||
let on = !self.selected_point_clouds(i).first().is_some_and(|(_, data)| data.show_cropping);
|
||||
self.edit_selected_point_clouds(i, "POINTCLOUDCROP", |data| data.show_cropping = on);
|
||||
}
|
||||
"_PCCROPINVERT" => self.edit_selected_point_clouds(i, "POINTCLOUDCROP", |data| {
|
||||
for crop in &mut data.croppings {
|
||||
crop.inverted = !crop.inverted;
|
||||
}
|
||||
}),
|
||||
// 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" => {
|
||||
self.edit_selected_underlays(i, "PDFCLIP", |u| {
|
||||
u.clip_boundary_vertices.clear();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1031,6 +1031,21 @@ impl OpenCADStudio {
|
||||
| "PDFIMPORTIMAGEPATH"
|
||||
| "XDWGFADECTL"
|
||||
| "POINTCLOUDCLIPFRAME"
|
||||
| "POINTCLOUDDENSITY"
|
||||
| "POINTCLOUDPOINTSIZE"
|
||||
| "POINTCLOUDLOCK"
|
||||
| "POINTCLOUDAUTOUPDATE"
|
||||
| "POINTCLOUDBOUNDARY"
|
||||
| "POINTCLOUDRTDENSITY"
|
||||
| "POINTCLOUDLOD"
|
||||
| "POINTCLOUDPOINTMAX"
|
||||
| "POINTCLOUDVISRETAIN"
|
||||
| "POINTCLOUDSHADING"
|
||||
| "POINTCLOUDCACHESIZE"
|
||||
| "POINTCLOUD2DVSDISPLAY"
|
||||
| "POINTCLOUDLIGHTING"
|
||||
| "POINTCLOUDLIGHTSOURCE"
|
||||
| "POINTCLOUDPOINTMAXLEGACY"
|
||||
| "XCLIPFRAME"
|
||||
| "WIPEOUTFRAME"
|
||||
| "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(),
|
||||
);
|
||||
} 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" {
|
||||
let current = crate::scene::cache::block_cache::xref_fade_ctl();
|
||||
if let Some(value) = &value {
|
||||
|
||||
@@ -57,6 +57,19 @@ impl OpenCADStudio {
|
||||
self.start_underlay_attach(i, codec::entities::UnderlayType::Dgn, rest);
|
||||
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)),
|
||||
// A file named on the command line skips the picker.
|
||||
// Handled at once, so the placement it starts belongs to this
|
||||
@@ -355,6 +368,21 @@ impl OpenCADStudio {
|
||||
},
|
||||
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(
|
||||
async {
|
||||
let handle = crate::sys::file_dialog()
|
||||
@@ -363,7 +391,7 @@ impl OpenCADStudio {
|
||||
crate::t!("All Reference Files").as_ref(),
|
||||
&[
|
||||
"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"])
|
||||
@@ -374,6 +402,7 @@ impl OpenCADStudio {
|
||||
crate::t!("Images").as_ref(),
|
||||
&["png", "jpg", "jpeg", "bmp", "tif", "tiff"],
|
||||
)
|
||||
.add_filter(crate::t!("Point Clouds").as_ref(), &["rcp", "rcs"])
|
||||
.pick_file()
|
||||
.await;
|
||||
match handle {
|
||||
@@ -423,6 +452,16 @@ impl OpenCADStudio {
|
||||
}
|
||||
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" => {
|
||||
let result = image::image_dimensions(&path)
|
||||
.map(|(w, h)| (path.clone(), w, h))
|
||||
|
||||
@@ -71,7 +71,11 @@ pub(super) const NAMES: &[&str] = &[
|
||||
"close_modal",
|
||||
"pdf_dialog_ok",
|
||||
"pdf_layer_toggle",
|
||||
"pc_manager_toggle",
|
||||
"pc_manager",
|
||||
"pdf_page_select",
|
||||
"pc_colormap",
|
||||
"pc_section",
|
||||
"ribbon_tab",
|
||||
"ribbon_dropdown",
|
||||
"dialog_ok",
|
||||
@@ -688,6 +692,82 @@ impl OpenCADStudio {
|
||||
"pdf_layer_toggle" => Message::PdfDialog(
|
||||
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").
|
||||
"pdf_page_select" => {
|
||||
let pages: Vec<usize> = string(req, "value")?
|
||||
@@ -713,6 +793,9 @@ impl OpenCADStudio {
|
||||
use crate::ui::window::pdf_dialogs::PdfDialogMsg;
|
||||
Message::PdfDialog(match self.active_modal {
|
||||
Some(crate::app::ModalKind::PdfAttach) => PdfDialogMsg::AttachOk,
|
||||
Some(crate::app::ModalKind::PointCloudAttach) => PdfDialogMsg::CloudOk,
|
||||
Some(crate::app::ModalKind::PointCloudColorMap) => PdfDialogMsg::MapOk,
|
||||
Some(crate::app::ModalKind::PcSection) => PdfDialogMsg::SecCreate,
|
||||
Some(crate::app::ModalKind::UnderlayLayers) => PdfDialogMsg::LayersOk,
|
||||
Some(crate::app::ModalKind::PdfImportSettings) => PdfDialogMsg::SettingsOk,
|
||||
Some(crate::app::ModalKind::PdfImportFile) => PdfDialogMsg::ImportOk,
|
||||
|
||||
@@ -497,6 +497,9 @@ pub(super) struct OpenCADStudio {
|
||||
block_definition: Option<crate::ui::window::block_definition::BlockDefinitionState>,
|
||||
/// PDF dialogs' working copies; `None` while closed.
|
||||
pdf_attach: Option<crate::ui::window::pdf_dialogs::PdfAttachState>,
|
||||
point_cloud_attach: Option<crate::ui::window::pdf_dialogs::PointCloudAttachState>,
|
||||
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>,
|
||||
pdf_import_settings: Option<crate::modules::insert::pdf_import::PdfImportSettings>,
|
||||
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
|
||||
/// the default layout is unchanged for existing users.
|
||||
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
|
||||
/// it is closed. One slot, because only one wheel can be open at a time.
|
||||
pub(crate) bg_picker: Option<BgTarget>,
|
||||
@@ -1903,6 +1908,9 @@ pub enum ModalKind {
|
||||
DrawingUnits,
|
||||
BlockDefinition,
|
||||
PdfAttach,
|
||||
PointCloudAttach,
|
||||
PointCloudColorMap,
|
||||
PcSection,
|
||||
UnderlayLayers,
|
||||
PdfImportSettings,
|
||||
PdfImportFile,
|
||||
@@ -3856,6 +3864,8 @@ pub enum Message {
|
||||
PdfImportPick,
|
||||
/// An edit in one of the PDF dialogs.
|
||||
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>),
|
||||
/// Result of the PDFATTACH file picker.
|
||||
PdfAttachPickResult(Result<(std::path::PathBuf, std::sync::Arc<Vec<u8>>), String>),
|
||||
@@ -3869,6 +3879,9 @@ pub enum Message {
|
||||
AttachPick,
|
||||
/// DWFATTACH / DGNATTACH: pick the file (the result goes the ATTACH way).
|
||||
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,
|
||||
/// switch.
|
||||
XrefFadeSlide(u8),
|
||||
@@ -4043,6 +4056,9 @@ impl OpenCADStudio {
|
||||
drawing_units: None,
|
||||
block_definition: None,
|
||||
pdf_attach: None,
|
||||
point_cloud_attach: None,
|
||||
point_cloud_color_map: None,
|
||||
pc_section: None,
|
||||
underlay_layers: None,
|
||||
pdf_import_settings: None,
|
||||
pdf_import_file: None,
|
||||
@@ -4166,6 +4182,7 @@ impl OpenCADStudio {
|
||||
graph_undo_open: false,
|
||||
show_external_references: false,
|
||||
show_browser: false,
|
||||
pc_manager: Default::default(),
|
||||
bg_picker: None,
|
||||
block_palette: Default::default(),
|
||||
xref_manager: Default::default(),
|
||||
|
||||
+63
-2
@@ -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
|
||||
// scale come from its dimension style.
|
||||
codec::EntityType::Leader(ld) => {
|
||||
@@ -2632,7 +2644,15 @@ filter={:.1} local={:.1} aggregate={:.1} entities={}",
|
||||
.iter()
|
||||
.all(|handle| self.tabs[i].scene.is_layer_locked(*handle));
|
||||
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;
|
||||
// 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);
|
||||
@@ -2946,6 +2966,29 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
|
||||
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);
|
||||
(single_handle, grips, handles)
|
||||
};
|
||||
@@ -2981,6 +3024,20 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
|
||||
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 {
|
||||
!self.tabs[i].properties.selected_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;
|
||||
|
||||
for property in sections
|
||||
.iter_mut()
|
||||
.flat_map(|section| section.props.iter_mut())
|
||||
.filter(|property| !keep.contains(&property.field))
|
||||
{
|
||||
let text = match &property.value {
|
||||
PropValue::ReadOnly(value)
|
||||
|
||||
@@ -2369,6 +2369,18 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
return self.on_dynamic_dimension_choice(field, &value);
|
||||
}
|
||||
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);
|
||||
// The Annotative Yes/No list drives the per-object annotative toggle
|
||||
// (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
|
||||
// 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 let Err(message) =
|
||||
crate::entities::underlay::validate_property(field, &val)
|
||||
|
||||
@@ -630,6 +630,9 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
PanelId::NodeGraph => {
|
||||
self.show_node_graph = false;
|
||||
}
|
||||
PanelId::PointCloudManager => {
|
||||
self.pc_manager.show = false;
|
||||
}
|
||||
PanelId::Properties => {
|
||||
self.show_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::Browser => self.show_browser,
|
||||
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();
|
||||
// Point clouds redraw as unloaded (box and saved path).
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
}
|
||||
XrefPaletteOp::Reload => {
|
||||
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].scene.reseed_underlays();
|
||||
} else if *kind == crate::io::xref_model::RefKind::PointCloud {
|
||||
// A point cloud reloads by setting its definition loaded.
|
||||
for (key, row_name, row_kind, _) in &picked {
|
||||
if row_kind == kind && row_name == name {
|
||||
crate::io::xref::set_point_cloud_loaded(
|
||||
&mut self.tabs[i].scene.document,
|
||||
codec::types::Handle::new(*key),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
} else if *kind != crate::io::xref_model::RefKind::DwgXref {
|
||||
self.command_line.push_error(crate::tf!(
|
||||
"{}: reload applies to drawing references only.",
|
||||
|
||||
@@ -1429,6 +1429,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
message @ (Message::AttachPick
|
||||
| Message::UnderlayAttachPick(_)
|
||||
| Message::PointCloudAttachPick
|
||||
| Message::AttachPickResult(_)
|
||||
| Message::XrefAttach(_)
|
||||
| Message::XrefAttachBrowseResult(_)) => self.update_xref_attach(message),
|
||||
@@ -1805,6 +1806,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
Message::PdfDialog(message) => self.update_pdf_dialog(message),
|
||||
Message::PcManager(message) => self.update_pc_manager(message),
|
||||
Message::RibbonSelectTab(idx) => {
|
||||
self.ribbon.select(idx);
|
||||
Task::none()
|
||||
@@ -8494,6 +8496,11 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
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) {
|
||||
return self.update(Message::RecoveryDecline);
|
||||
}
|
||||
|
||||
+101
-2
@@ -167,6 +167,29 @@ fn cursor_on_projected_axis(
|
||||
.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(
|
||||
original: &AcadEntityType,
|
||||
current: &AcadEntityType,
|
||||
@@ -635,6 +658,36 @@ impl OpenCADStudio {
|
||||
.find(|(owner, grip)| *owner == handle && grip.id == grip_id)
|
||||
.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 mut targets: Vec<GripTarget> = if clicked_is_hot {
|
||||
@@ -716,6 +769,9 @@ impl OpenCADStudio {
|
||||
axis,
|
||||
targets,
|
||||
rectangle_frame: None,
|
||||
plane: None,
|
||||
grab: None,
|
||||
gizmo: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -927,6 +983,10 @@ impl OpenCADStudio {
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.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()
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
// The overlay builds the active tracking guide from `otrack_active.base`
|
||||
// to `last_cursor_world`. Keep it synchronized with the actual point used
|
||||
@@ -3557,8 +3639,25 @@ impl OpenCADStudio {
|
||||
self.grip_popup = None;
|
||||
return Task::none();
|
||||
}
|
||||
self.tabs[i].active_grip =
|
||||
Some(self.grip_edit_for_hit(i, handle, grip_id, is_translate, world));
|
||||
let mut edit = 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_popup = None;
|
||||
self.tabs[i]
|
||||
|
||||
@@ -1604,6 +1604,11 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
None
|
||||
} else if self.ribbon.xref_context() {
|
||||
crate::ui::side_toolbar::view(&crate::ui::ribbon::xref_tools())
|
||||
} else if let Some(shown) = self.ribbon.point_cloud_context() {
|
||||
crate::ui::side_toolbar::view_with_active(
|
||||
&crate::ui::ribbon::point_cloud_tools(),
|
||||
&move |id| id == "_PCCROPSHOW" && shown,
|
||||
)
|
||||
} else if let Some(ctx) = self.ribbon.underlay_context() {
|
||||
let ctx = ctx.clone();
|
||||
crate::ui::side_toolbar::view_with_active(
|
||||
@@ -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::Browser => self.show_browser,
|
||||
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>> {
|
||||
@@ -2989,6 +2995,13 @@ impl OpenCADStudio {
|
||||
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);
|
||||
match side {
|
||||
|
||||
@@ -29,6 +29,9 @@ impl OpenCADStudio {
|
||||
Some(codec::entities::UnderlayType::Dgn) => crate::t!("Attach DGN Underlay").into_owned(),
|
||||
_ => crate::t!("Attach PDF Underlay").into_owned(),
|
||||
},
|
||||
Some(K::PointCloudAttach) => crate::t!("Attach Point Cloud").into_owned(),
|
||||
Some(K::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::PdfImportSettings) => crate::t!("PDF Import Settings").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)
|
||||
})
|
||||
}
|
||||
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 => {
|
||||
let state = self.underlay_layers.as_ref()?;
|
||||
sized_flow(ex, 460, 520, |flow| {
|
||||
|
||||
Reference in new issue
Block a user