forked from 1b-agent/opencadstudio-upstream
Formats the branch the way main's 9cf9ff2 did, so merging main only
conflicts where the code itself differs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013RyGiMXcsf9HTKfqDxaQGt
244 lines
9.2 KiB
Rust
244 lines
9.2 KiB
Rust
//! 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(Box::new(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);
|
||
}
|
||
}
|
||
}
|