Point clouds: attach dialog, crop toolbar, stylize, palette rows

POINTCLOUDATTACH opens the Attach Point Cloud dialog after the file is
chosen; its Attach button lives in the Attach Underlay list instead of a
separate Insert panel. A selected cloud brings the crop tools to the
right-edge toolbar. POINTCLOUDSTYLIZE sets the stylization with the
reference's prompts, the External References palette lists point clouds
(unload, reload, detach, path), and definitions keep the header extents.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-09-30 10:14:44 +03:00
1 parent 3afd547e28
commit c2d9ac5ae2
48 files changed
+1711 -83

No files matched your search

+8
View File
@@ -13924,6 +13924,14 @@ mod update_entities_batch_tests {
}
}
pub(crate) fn is_point_cloud(entity: &EntityType) -> bool {
matches!(
entity,
EntityType::Extended(extended)
if matches!(extended.data, codec::entities::ExtendedEntityData::PointCloudEx(_))
)
}
/// A point cloud whose Locked property is set: edits leave it alone.
pub(crate) fn is_locked_point_cloud(entity: &EntityType) -> bool {
matches!(
+32 -2
View File
@@ -26,6 +26,14 @@ pub struct CloudPoints {
pub intensity: Vec<u8>,
pub has_rgb: bool,
pub has_intensity: bool,
pub has_normals: bool,
/// Scans read (one for a scan file).
pub scans: usize,
/// A project's preview picture (JPEG).
pub preview: Option<Vec<u8>>,
/// Bounds from the scans' headers, placed like their points: the
/// extents a definition records.
pub bounds: Option<[[f64; 3]; 2]>,
}
/// Scan points by file path and modification time. A file that fails to
@@ -80,14 +88,32 @@ fn decode(path: &Path) -> Option<CloudPoints> {
intensity: Vec::new(),
has_rgb: scans.iter().any(|(scan, _)| scan.has_rgb),
has_intensity: scans.iter().any(|(scan, _)| scan.has_intensity),
has_normals: scans.iter().any(|(scan, _)| scan.has_normals),
scans: scans.len(),
preview: is_project.then(|| rcp::preview(&bytes)).flatten(),
bounds: None,
};
for (scan, [translation, rotation, scale]) in scans {
let rotate = euler(rotation);
cloud.positions.extend(scan.local.iter().map(|p| {
let place = |p: &[f64; 3]| {
let s = [p[0] * scale[0], p[1] * scale[1], p[2] * scale[2]];
let r = rotate.map(|row| row[0] * s[0] + row[1] * s[1] + row[2] * s[2]);
[r[0] + translation[0], r[1] + translation[1], r[2] + translation[2]]
}));
};
cloud.positions.extend(scan.local.iter().map(place));
let [lo, hi] = scan.bounds;
for corner in 0..8 {
let p = place(&[
if corner & 1 == 0 { lo[0] } else { hi[0] },
if corner & 2 == 0 { lo[1] } else { hi[1] },
if corner & 4 == 0 { lo[2] } else { hi[2] },
]);
let [min, max] = cloud.bounds.get_or_insert([p, p]);
for k in 0..3 {
min[k] = min[k].min(p[k]);
max[k] = max[k].max(p[k]);
}
}
if scan.has_rgb || !cloud.has_rgb {
cloud.colors.extend_from_slice(&scan.colors);
} else {
@@ -150,6 +176,10 @@ pub(crate) fn register_source(stored: &str, resolved: PathBuf) {
/// is, else where it was found next to the drawing, else relative to the
/// working folder.
pub(crate) fn resolve_source(document: &CadDocument, data: &PointCloudExData) -> Option<PathBuf> {
// An unloaded cloud draws as a missing one: its box and saved path.
if definition(document, data).is_some_and(|def| !def.is_loaded) {
return None;
}
let stored = definition(document, data)?.source_filename.trim();
if stored.is_empty() {
return None;
+15
View File
@@ -14,6 +14,21 @@ pub struct ScanEntry {
pub scale: [f64; 3],
}
/// The project's preview picture (the JPEG it carries), for the attach
/// dialog.
pub fn preview(bytes: &[u8]) -> Option<Vec<u8>> {
let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).ok()?;
for i in 0..archive.len() {
let file = archive.by_index(i).ok()?;
if file.name().to_ascii_lowercase().ends_with(".jpg") {
let mut picture = Vec::new();
file.take(32 << 20).read_to_end(&mut picture).ok()?;
return Some(picture);
}
}
None
}
pub fn scans(bytes: &[u8], project: &Path) -> Option<Vec<ScanEntry>> {
let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).ok()?;
let folder = project.parent().unwrap_or(Path::new(""));
+7
View File
@@ -23,10 +23,13 @@ pub struct Scan {
pub colors: Vec<[u8; 3]>,
pub intensity: Vec<u8>,
pub has_rgb: bool,
pub has_normals: bool,
pub has_intensity: bool,
pub translation: [f64; 3],
pub rotation: [f64; 3],
pub scale: [f64; 3],
/// The scan's bounds as its header records them (local frame).
pub bounds: [[f64; 3]; 2],
}
const NODE_RECORD: usize = 368;
@@ -55,6 +58,8 @@ pub fn decode(b: &[u8]) -> Option<Scan> {
let rotation = vec3_at(b, 0x28)?;
let scale = vec3_at(b, 0x40)?;
let has_rgb = *b.get(0xE0)? != 0;
let bounds = [vec3_at(b, 0xA0)?, vec3_at(b, 0xB8)?];
let has_normals = *b.get(0xE1)? != 0;
let has_intensity = *b.get(0xE2)? != 0;
let sections = u32_at(b, 0x129)? as usize;
@@ -80,10 +85,12 @@ pub fn decode(b: &[u8]) -> Option<Scan> {
colors: Vec::new(),
intensity: Vec::new(),
has_rgb,
has_normals,
has_intensity,
translation,
rotation,
scale,
bounds,
};
for node in 0..nodes {
let record = directory + 8 + node * NODE_RECORD;