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:
Hakan SevenandClaude Opus 5.5 committed 2026-10-01 01:49:25 +03:00
commit 3dcebf0dc6
81 files changed
+9162 -305

No files matched your search

+160 -1
View File
@@ -322,6 +322,21 @@ pub fn register_underlay_sources(doc: &CadDocument, base_dir: &Path) {
crate::scene::model::pdf_raster::register_source(stored, std::sync::Arc::new(bytes));
}
}
// Point cloud scans are large and read lazily: register where they are.
for object in doc.objects.values() {
let codec::objects::ObjectType::ClassObject(object) = object else {
continue;
};
let (codec::objects::ClassObjectData::PointCloudDefinitionEx(def)
| codec::objects::ClassObjectData::PointCloudDefinition(def)) = &object.data
else {
continue;
};
let stored = def.source_filename.trim();
if let Some(found) = (!stored.is_empty()).then(|| resolve_path(stored, base_dir)).flatten() {
crate::scene::model::point_cloud::register_source(stored, found);
}
}
}
/// Layer properties an override can change, as compared for the
@@ -1277,6 +1292,55 @@ pub fn collect_entries_with_prev(
entries.push(entry);
}
// ── Point clouds ──
// Listed under their definitions' names; a definition no cloud uses is
// Unreferenced, as for images and underlays.
let mut referenced_clouds: HashSet<Handle> = HashSet::default();
for e in doc.entities() {
if let EntityType::Extended(extended) = e {
if let codec::entities::ExtendedEntityData::PointCloudEx(data) = &extended.data {
referenced_clouds.insert(data.definition_handle);
}
}
}
let cloud_names: HashMap<Handle, String> = doc
.objects
.values()
.filter_map(|object| match object {
ObjectType::Dictionary(dictionary) => Some(dictionary),
_ => None,
})
.flat_map(|dictionary| dictionary.entries.iter().map(|(name, handle)| (*handle, name.clone())))
.collect();
for (handle, obj) in doc.objects.iter() {
let ObjectType::ClassObject(object) = obj else {
continue;
};
let codec::objects::ClassObjectData::PointCloudDefinitionEx(def) = &object.data else {
continue;
};
let key = handle.value();
let name = cloud_names.get(handle).cloned().unwrap_or_else(|| {
Path::new(&def.source_filename.replace('\\', "/"))
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default()
});
let mut entry = ReferenceEntry::new(key, name, RefKind::PointCloud);
entry.saved_path = def.source_filename.clone();
if !def.is_loaded {
entry.status = RefStatus::Unloaded;
resolve_only(&mut entry, &def.source_filename, base_dir);
} else {
stat_into(&mut entry, &def.source_filename, base_dir);
entry.status = decide_status(entry.status, entry.modified, prev.get(&key).copied(), false);
if !referenced_clouds.contains(handle) {
entry.status = RefStatus::Unreferenced;
}
}
entries.push(entry);
}
// ── Nested enumeration: full transitive closure, read-only, no merge ──
// Paths stored in a nested drawing are relative to *that drawing*, not to
// the host. The work queue carries each parent's resolved file so every
@@ -1369,6 +1433,7 @@ enum RefTarget {
DwgXref { handle: Handle, name: String },
Image { handle: Handle, name: String },
Pdf { handle: Handle, name: String },
PointCloud { handle: Handle, name: String },
}
fn find_target(doc: &CadDocument, key: u64) -> Option<RefTarget> {
@@ -1389,6 +1454,11 @@ fn find_target(doc: &CadDocument, key: u64) -> Option<RefTarget> {
name: def.file_name.clone(),
});
}
ObjectType::ClassObject(object) if handle.value() == key => {
if let codec::objects::ClassObjectData::PointCloudDefinitionEx(def) = &object.data {
return Some(RefTarget::PointCloud { handle: *handle, name: def.source_filename.clone() });
}
}
ObjectType::UnderlayDefinition(def) if handle.value() == key => {
return Some(RefTarget::Pdf {
handle: *handle,
@@ -1462,6 +1532,20 @@ pub fn unload_reference(doc: &mut CadDocument, key: u64) -> Result<String, Strin
}
Ok(name)
}
// So does a point cloud (its definition's loaded flag).
RefTarget::PointCloud { handle, name } => {
set_point_cloud_loaded(doc, handle, false);
Ok(name)
}
}
}
/// A point cloud definition's loaded flag (Unload / Reload).
pub fn set_point_cloud_loaded(doc: &mut CadDocument, handle: Handle, loaded: bool) {
if let Some(codec::objects::ObjectType::ClassObject(object)) = doc.objects.get_mut(&handle) {
if let codec::objects::ClassObjectData::PointCloudDefinitionEx(def) = &mut object.data {
def.is_loaded = loaded;
}
}
}
@@ -1563,6 +1647,34 @@ pub fn detach_reference(doc: &mut CadDocument, key: u64) -> Result<String, Strin
doc.objects.remove(&handle);
Ok(name)
}
// The clouds, their reactors, the definition and its dictionary
// entry.
RefTarget::PointCloud { handle, name } => {
use codec::entities::ExtendedEntityData;
let clouds: Vec<(Handle, Handle)> = doc
.entities()
.filter_map(|e| match e {
EntityType::Extended(extended) => match &extended.data {
ExtendedEntityData::PointCloudEx(data) if data.definition_handle == handle => {
Some((e.common().handle, data.reactor_handle))
}
_ => None,
},
_ => None,
})
.collect();
for (cloud, reactor) in clouds {
doc.remove_entity(cloud);
doc.objects.remove(&reactor);
}
for object in doc.objects.values_mut() {
if let codec::objects::ObjectType::Dictionary(dictionary) = object {
dictionary.entries.retain(|(_, h)| *h != handle);
}
}
doc.objects.remove(&handle);
Ok(name)
}
}
}
@@ -1611,6 +1723,9 @@ pub fn bind_reference(
"XREF: PDF bind (vector import) is not available in this version."
)
.to_string()),
RefTarget::PointCloud { .. } => {
Err(crate::t!("XREF: bind applies to drawing references only.").to_string())
}
}
}
@@ -1881,6 +1996,10 @@ fn same_file(a: &Path, b: &Path) -> bool {
}
}
/// Marks a point cloud definition (by handle) among the references made
/// relative on the drawing's first save.
pub const POINT_CLOUD_KEY: &str = "pointcloud:";
/// Make the named references' paths relative to `host` (their drawing's
/// first save); a reference that cannot be relative keeps its full path.
pub fn make_relative(
@@ -1889,6 +2008,12 @@ pub fn make_relative(
host: &Path,
) {
for name in names {
if let Some(handle) = name.strip_prefix(POINT_CLOUD_KEY) {
if let Ok(handle) = u64::from_str_radix(handle, 16) {
relative_point_cloud_path(doc, Handle::new(handle), host);
}
continue;
}
let Some(key) = doc.block_records.get(name).map(|br| br.handle.value()) else {
continue;
};
@@ -2146,6 +2271,19 @@ pub fn set_ref_path(
_ => Err(crate::t!("XREF: no loaded reference with that key.").to_string()),
}
}
RefTarget::PointCloud { handle, name } => {
use codec::objects::{ClassObjectData, ObjectType};
match doc.objects.get_mut(&handle) {
Some(ObjectType::ClassObject(object)) => match &mut object.data {
ClassObjectData::PointCloudDefinitionEx(def) => {
def.source_filename = new_raw.to_string();
Ok(name)
}
_ => Err(crate::t!("XREF: no loaded reference with that key.").to_string()),
},
_ => Err(crate::t!("XREF: no loaded reference with that key.").to_string()),
}
}
}
}
@@ -2177,6 +2315,7 @@ pub fn apply_pathtype(
_ => String::new(),
}
}
Some(RefTarget::PointCloud { name, .. }) => name,
None => return Err(crate::t!("XREF: no loaded reference with that key.").to_string()),
};
// A stored relative path first has to be resolved against its current host
@@ -2305,7 +2444,7 @@ pub fn set_ref_type(
}
Err(crate::t!("XREF: no loaded reference with that key.").to_string())
}
RefTarget::Image { .. } | RefTarget::Pdf { .. } => {
RefTarget::Image { .. } | RefTarget::Pdf { .. } | RefTarget::PointCloud { .. } => {
Err(crate::t!("XREF: overlays apply to drawing references only.").to_string())
}
}
@@ -3106,3 +3245,23 @@ mod tests {
}
}
}
/// A point cloud definition's full path made relative to the drawing
/// (".\rcs\scan.rcs", as the reference writes it); on another drive it
/// stays full.
fn relative_point_cloud_path(doc: &mut CadDocument, handle: Handle, host: &Path) {
use crate::io::xref_model::{to_pathtype_result, Pathtype};
let Some(codec::objects::ObjectType::ClassObject(object)) = doc.objects.get_mut(&handle) else {
return;
};
let codec::objects::ClassObjectData::PointCloudDefinitionEx(def) = &mut object.data else {
return;
};
if let Ok(relative) = to_pathtype_result(&def.source_filename, host, Pathtype::Relative) {
let relative = relative.replace('/', "\\");
if let Some(found) = std::path::Path::new(&def.source_filename).is_file().then(|| def.source_filename.clone()) {
crate::scene::model::point_cloud::register_source(&relative, found.into());
}
def.source_filename = relative;
}
}
+2
View File
@@ -305,6 +305,8 @@ pub enum RefKind {
Image,
/// A PDF, DWF or DGN underlay.
Underlay,
/// A point cloud scan (.rcs) or project (.rcp).
PointCloud,
}
/// Lifecycle state of a [`ReferenceEntry`].