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 <noreply@anthropic.com>
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

+116
View File
@@ -0,0 +1,116 @@
// Indexed scan (.rcs, "ADOCT" version 3) point reader.
//
// Layout (little-endian):
// 0x10 3 × f64 scan translation T
// 0x28 3 × f64 scan rotation
// 0x40 3 × f64 scan scale
// 0xE0 3 × u8 has RGB, has normals, has intensity
// 0x129 u32 section count, then 24-byte entries at 0x12D:
// u64 type, u64 offset, u64 size
// Section 2 is the node directory: u64 node count, then 368-byte records
// (cube min at +0, levels +96, point count +100, block size +104, per-level
// cell counts +108 and point counts +236). Section 1 holds the node blocks
// back to back: the node's octree index (u32 entries), then 16-byte point
// records — a u64 with three 18-bit millimetre offsets from the node cube's
// min corner, the intensity byte, then blue, green, red.
// The octree index is only needed for spatial queries: every record carries
// its own position.
/// One scan, in its own (untransformed) frame.
pub struct Scan {
/// Point positions before the scan transform.
pub local: Vec<[f64; 3]>,
pub colors: Vec<[u8; 3]>,
pub intensity: Vec<u8>,
pub has_rgb: bool,
pub has_intensity: bool,
pub translation: [f64; 3],
pub rotation: [f64; 3],
pub scale: [f64; 3],
}
const NODE_RECORD: usize = 368;
fn u32_at(b: &[u8], at: usize) -> Option<u32> {
Some(u32::from_le_bytes(b.get(at..at.checked_add(4)?)?.try_into().ok()?))
}
fn u64_at(b: &[u8], at: usize) -> Option<u64> {
Some(u64::from_le_bytes(b.get(at..at.checked_add(8)?)?.try_into().ok()?))
}
fn f64_at(b: &[u8], at: usize) -> Option<f64> {
u64_at(b, at).map(f64::from_bits).filter(|v| v.is_finite())
}
fn vec3_at(b: &[u8], at: usize) -> Option<[f64; 3]> {
Some([f64_at(b, at)?, f64_at(b, at + 8)?, f64_at(b, at + 16)?])
}
pub fn decode(b: &[u8]) -> Option<Scan> {
if b.get(0..5)? != b"ADOCT" {
return None;
}
let translation = vec3_at(b, 0x10)?;
let rotation = vec3_at(b, 0x28)?;
let scale = vec3_at(b, 0x40)?;
let has_rgb = *b.get(0xE0)? != 0;
let has_intensity = *b.get(0xE2)? != 0;
let sections = u32_at(b, 0x129)? as usize;
let (mut directory, mut data) = (None, None);
for k in 0..sections.min(64) {
let at = 0x12D + k * 24;
let offset = usize::try_from(u64_at(b, at + 8)?).ok()?;
match u64_at(b, at)? {
1 => data = Some(offset),
2 => directory = Some(offset),
_ => {}
}
}
let (directory, mut block) = (directory?, data?);
let nodes = usize::try_from(u64_at(b, directory)?).ok()?;
// Every node needs its directory record; a count past the file is corrupt.
if nodes.checked_mul(NODE_RECORD)? > b.len() {
return None;
}
let mut scan = Scan {
local: Vec::new(),
colors: Vec::new(),
intensity: Vec::new(),
has_rgb,
has_intensity,
translation,
rotation,
scale,
};
for node in 0..nodes {
let record = directory + 8 + node * NODE_RECORD;
let cube_min = vec3_at(b, record)?;
let points = u32_at(b, record + 100)? as usize;
let size = u32_at(b, record + 104)? as usize;
let mut entries = points;
for level in 0..32 {
entries = entries.checked_add(u32_at(b, record + 108 + level * 4)? as usize)?;
}
if entries.checked_mul(4)?.checked_add(points.checked_mul(16)?)? != size {
return None;
}
let records = block + entries * 4;
let end = block.checked_add(size)?;
let bytes = b.get(records..end)?;
scan.local.reserve(points);
scan.colors.reserve(points);
scan.intensity.reserve(points);
for rec in bytes.chunks_exact(16) {
let v = u64::from_le_bytes(rec[0..8].try_into().ok()?);
let q = |shift: u32| ((v >> shift) & 0x3FFFF) as f64 * 1e-3;
scan.local.push([cube_min[0] + q(0), cube_min[1] + q(18), cube_min[2] + q(36)]);
scan.intensity.push(rec[8]);
scan.colors.push([rec[11], rec[10], rec[9]]);
}
block = end;
}
Some(scan)
}