forked from 1b-agent/opencadstudio-upstream
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:
1 parent
3afd547e28
commit
c2d9ac5ae2
48 files changed
+1711
-83
No files matched your search
@@ -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!(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(""));
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user