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:
ramox81andClaude Opus 5.5 committed 2026-09-29 17:08:46 +03:00
1 parent 07f25f4bca
commit e9c4e501db
58 files changed
+2711 -237

No files matched your search

+184
View File
@@ -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);
}
}
}