forked from 1b-agent/opencadstudio-upstream
Point clouds: drawn from their scans, placed, cropped and attached as the reference does
Scans (.rcs) and projects (.rcp) are read and their points drawn as screen-sized dots, coloured by the cloud's stylization. The cloud's placement carries its scale and rotation; its extents and crops stay in the scan's coordinates. A missing scan shows the extents with the saved path. -POINTCLOUDATTACH / POINTCLOUDATTACH, POINTCLOUDCROP and the POINTCLOUD* settings follow the reference's prompts and messages, the Properties list is the reference's, and a locked cloud is left alone by edits. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
07f25f4bca
commit
e9c4e501db
58 files changed
+2711
-237
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,12 @@ impl OpenCADStudio {
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
"POINTCLOUDCROP" => {
|
||||
use crate::command::CadCommand;
|
||||
let command = crate::modules::insert::pc_crop::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,7 @@ mod inquiry;
|
||||
mod layerprops;
|
||||
mod layers;
|
||||
pub(crate) mod pdf_import;
|
||||
mod point_cloud;
|
||||
mod pdf_underlay;
|
||||
mod pdf_dialogs;
|
||||
mod xclip;
|
||||
@@ -636,6 +637,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",
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
//! 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 (mut lo, mut hi) = ([f64::MAX; 3], [f64::MIN; 3]);
|
||||
for p in &points.positions {
|
||||
for k in 0..3 {
|
||||
lo[k] = lo[k].min(p[k]);
|
||||
hi[k] = hi[k].max(p[k]);
|
||||
}
|
||||
}
|
||||
let (min, max) = if points.positions.is_empty() {
|
||||
(Vector3::ZERO, Vector3::ZERO)
|
||||
} else {
|
||||
(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 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 = 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, &path, &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,
|
||||
unknown_bl0: 0,
|
||||
unknown_bl1: 0,
|
||||
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");
|
||||
}
|
||||
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,21 @@ impl OpenCADStudio {
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
// A point cloud goes on to its placement prompts.
|
||||
"rcp" | "rcs" => {
|
||||
use crate::command::CadCommand;
|
||||
let i = self.active_tab;
|
||||
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();
|
||||
}
|
||||
let command =
|
||||
crate::modules::insert::pc_attach::PointCloudAttachCommand::with_file(path);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
Task::none()
|
||||
}
|
||||
"png" | "jpg" | "jpeg" | "bmp" | "tif" | "tiff" => {
|
||||
let result = image::image_dimensions(&path)
|
||||
.map(|(w, h)| (path.clone(), w, h))
|
||||
|
||||
@@ -3854,6 +3854,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),
|
||||
|
||||
+40
-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);
|
||||
@@ -2981,6 +3001,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 +3472,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)
|
||||
|
||||
@@ -2368,6 +2368,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).
|
||||
@@ -3606,6 +3618,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)
|
||||
|
||||
@@ -2419,6 +2419,12 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
if path
|
||||
.extension()
|
||||
.is_some_and(|extension| extension.eq_ignore_ascii_case("dwg"))
|
||||
{
|
||||
crate::io::xref::relative_point_cloud_paths(&mut snapshot, &path);
|
||||
}
|
||||
let clone_ms = clone_started.elapsed().as_secs_f64() * 1000.0;
|
||||
if crate::perf::enabled() {
|
||||
crate::perf_record!(
|
||||
|
||||
@@ -1429,6 +1429,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
message @ (Message::AttachPick
|
||||
| Message::UnderlayAttachPick(_)
|
||||
| Message::PointCloudAttachPick
|
||||
| Message::AttachPickResult(_)
|
||||
| Message::XrefAttach(_)
|
||||
| Message::XrefAttachBrowseResult(_)) => self.update_xref_attach(message),
|
||||
|
||||
Reference in new issue
Block a user