forked from 1b-agent/opencadstudio-upstream
Point clouds: normal stylization colours, project rotations in degrees
Scan normals are decoded from their cube-map codes and drawn as world normal colours under the Normals stylization; project and scan rotation angles are read as degrees. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8fd993f016
commit
da25c45fdd
2 files changed
+52
-6
No files matched your search
@@ -24,6 +24,8 @@ pub struct CloudPoints {
|
|||||||
pub positions: Vec<[f64; 3]>,
|
pub positions: Vec<[f64; 3]>,
|
||||||
pub colors: Vec<[u8; 3]>,
|
pub colors: Vec<[u8; 3]>,
|
||||||
pub intensity: Vec<u8>,
|
pub intensity: Vec<u8>,
|
||||||
|
/// Unit normals in the file's frame (empty without normals).
|
||||||
|
pub normals: Vec<[f32; 3]>,
|
||||||
pub has_rgb: bool,
|
pub has_rgb: bool,
|
||||||
pub has_intensity: bool,
|
pub has_intensity: bool,
|
||||||
pub has_normals: bool,
|
pub has_normals: bool,
|
||||||
@@ -86,6 +88,7 @@ fn decode(path: &Path) -> Option<CloudPoints> {
|
|||||||
positions: Vec::new(),
|
positions: Vec::new(),
|
||||||
colors: Vec::new(),
|
colors: Vec::new(),
|
||||||
intensity: Vec::new(),
|
intensity: Vec::new(),
|
||||||
|
normals: Vec::new(),
|
||||||
has_rgb: scans.iter().any(|(scan, _)| scan.has_rgb),
|
has_rgb: scans.iter().any(|(scan, _)| scan.has_rgb),
|
||||||
has_intensity: scans.iter().any(|(scan, _)| scan.has_intensity),
|
has_intensity: scans.iter().any(|(scan, _)| scan.has_intensity),
|
||||||
has_normals: scans.iter().any(|(scan, _)| scan.has_normals),
|
has_normals: scans.iter().any(|(scan, _)| scan.has_normals),
|
||||||
@@ -94,7 +97,7 @@ fn decode(path: &Path) -> Option<CloudPoints> {
|
|||||||
bounds: None,
|
bounds: None,
|
||||||
};
|
};
|
||||||
for (scan, [translation, rotation, scale]) in scans {
|
for (scan, [translation, rotation, scale]) in scans {
|
||||||
let rotate = euler(rotation);
|
let rotate = euler(rotation.map(f64::to_radians));
|
||||||
let place = |p: &[f64; 3]| {
|
let place = |p: &[f64; 3]| {
|
||||||
let s = [p[0] * scale[0], p[1] * scale[1], p[2] * scale[2]];
|
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]);
|
let r = rotate.map(|row| row[0] * s[0] + row[1] * s[1] + row[2] * s[2]);
|
||||||
@@ -121,14 +124,17 @@ fn decode(path: &Path) -> Option<CloudPoints> {
|
|||||||
cloud.colors.extend(scan.intensity.iter().map(|&i| [i; 3]));
|
cloud.colors.extend(scan.intensity.iter().map(|&i| [i; 3]));
|
||||||
}
|
}
|
||||||
cloud.intensity.extend_from_slice(&scan.intensity);
|
cloud.intensity.extend_from_slice(&scan.intensity);
|
||||||
|
cloud.normals.extend(scan.normals.iter().map(|n| {
|
||||||
|
let r = rotate.map(|row| row[0] * f64::from(n[0]) + row[1] * f64::from(n[1]) + row[2] * f64::from(n[2]));
|
||||||
|
r.map(|v| v as f32)
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
Some(cloud)
|
Some(cloud)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rotation matrix of a scan's rotation angles (radians, applied about X,
|
/// Rotation matrix of a scan's rotation angles (radians here; projects and
|
||||||
/// then Y, then Z).
|
/// scan headers store degrees), applied about X, then Y, then Z — the
|
||||||
// ponytail: the angle convention is unverified — every sample scan has a
|
/// reference's extents of rotated scans match this order.
|
||||||
// zero rotation; check against a rotated scan when one turns up.
|
|
||||||
fn euler([x, y, z]: [f64; 3]) -> [[f64; 3]; 3] {
|
fn euler([x, y, z]: [f64; 3]) -> [[f64; 3]; 3] {
|
||||||
let (sx, cx) = x.sin_cos();
|
let (sx, cx) = x.sin_cos();
|
||||||
let (sy, cy) = y.sin_cos();
|
let (sy, cy) = y.sin_cos();
|
||||||
@@ -314,6 +320,14 @@ pub(crate) fn placed(
|
|||||||
let color = |i: usize, z: f64| -> [u8; 4] {
|
let color = |i: usize, z: f64| -> [u8; 4] {
|
||||||
let [r, g, b] = match &style {
|
let [r, g, b] = match &style {
|
||||||
Stylization::Object(c) => *c,
|
Stylization::Object(c) => *c,
|
||||||
|
Stylization::Normal => match cloud.normals.get(i) {
|
||||||
|
Some(n) => {
|
||||||
|
let w = axes[0] * f64::from(n[0]) + axes[1] * f64::from(n[1]) + axes[2] * f64::from(n[2]);
|
||||||
|
let w = w / w.length().max(1e-12);
|
||||||
|
[w.x, w.y, w.z].map(|v| ((v + 1.0) * 127.5).round().clamp(0.0, 255.0) as u8)
|
||||||
|
}
|
||||||
|
None => object_color,
|
||||||
|
},
|
||||||
Stylization::Ramp { colors, gradient, intensity: true, range } => {
|
Stylization::Ramp { colors, gradient, intensity: true, range } => {
|
||||||
let percent = f64::from(cloud.intensity.get(i).copied().unwrap_or(0)) * 100.0 / 255.0;
|
let percent = f64::from(cloud.intensity.get(i).copied().unwrap_or(0)) * 100.0 / 255.0;
|
||||||
let (from, to) = (range.0 as f64, range.1 as f64);
|
let (from, to) = (range.0 as f64, range.1 as f64);
|
||||||
@@ -397,6 +411,8 @@ enum Stylization {
|
|||||||
Scan,
|
Scan,
|
||||||
/// The cloud's object colour.
|
/// The cloud's object colour.
|
||||||
Object([u8; 3]),
|
Object([u8; 3]),
|
||||||
|
/// Each point's world normal as a colour, (n + 1) / 2 per component.
|
||||||
|
Normal,
|
||||||
/// A colour scheme over intensity (per cent, in the range given) or
|
/// A colour scheme over intensity (per cent, in the range given) or
|
||||||
/// elevation, blended or in bands.
|
/// elevation, blended or in bands.
|
||||||
Ramp { colors: Vec<[u8; 3]>, gradient: bool, intensity: bool, range: (i64, i64) },
|
Ramp { colors: Vec<[u8; 3]>, gradient: bool, intensity: bool, range: (i64, i64) },
|
||||||
@@ -406,9 +422,10 @@ impl Stylization {
|
|||||||
fn of(document: &CadDocument, data: &PointCloudExData, object_color: [u8; 3]) -> Self {
|
fn of(document: &CadDocument, data: &PointCloudExData, object_color: [u8; 3]) -> Self {
|
||||||
let (scheme, intensity, gradient) = match data.stylization_type {
|
let (scheme, intensity, gradient) = match data.stylization_type {
|
||||||
2 => return Self::Object(object_color),
|
2 => return Self::Object(object_color),
|
||||||
|
3 => return Self::Normal,
|
||||||
5 => (&data.intensity_color_scheme, true, data.intensity_as_gradient),
|
5 => (&data.intensity_color_scheme, true, data.intensity_as_gradient),
|
||||||
4 => (&data.current_color_scheme, false, data.elevation_as_gradient),
|
4 => (&data.current_color_scheme, false, data.elevation_as_gradient),
|
||||||
// Normals and classification have no data here: scan colours.
|
// Classification has no data here: scan colours.
|
||||||
_ => return Self::Scan,
|
_ => return Self::Scan,
|
||||||
};
|
};
|
||||||
let colors = ramp_colors(document, scheme, if intensity { "Spectrum" } else { "Earth" });
|
let colors = ramp_colors(document, scheme, if intensity { "Spectrum" } else { "Earth" });
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ pub struct Scan {
|
|||||||
pub local: Vec<[f64; 3]>,
|
pub local: Vec<[f64; 3]>,
|
||||||
pub colors: Vec<[u8; 3]>,
|
pub colors: Vec<[u8; 3]>,
|
||||||
pub intensity: Vec<u8>,
|
pub intensity: Vec<u8>,
|
||||||
|
/// Unit normals (scan frame).
|
||||||
|
pub normals: Vec<[f32; 3]>,
|
||||||
pub has_rgb: bool,
|
pub has_rgb: bool,
|
||||||
pub has_normals: bool,
|
pub has_normals: bool,
|
||||||
pub has_intensity: bool,
|
pub has_intensity: bool,
|
||||||
@@ -50,6 +52,31 @@ 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)?])
|
Some([f64_at(b, at)?, f64_at(b, at + 8)?, f64_at(b, at + 16)?])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A normal from its cube-map code: six faces of 52 × 52 equal-angle
|
||||||
|
/// cells. The face gives the main axis and its sign; the cell the other two
|
||||||
|
/// components, which flip with the face.
|
||||||
|
fn normal(code: u16) -> [f32; 3] {
|
||||||
|
let code = usize::from(code);
|
||||||
|
if code >= 6 * 52 * 52 {
|
||||||
|
return [0.0, 0.0, 1.0];
|
||||||
|
}
|
||||||
|
let (face, cell) = (code / 2704, code % 2704);
|
||||||
|
let tan = |i: usize| (((i as f64 + 0.5) / 26.0 - 1.0) * std::f64::consts::FRAC_PI_4).tan();
|
||||||
|
let axis = face >> 1;
|
||||||
|
let (b, c) = match axis {
|
||||||
|
0 => (1, 2),
|
||||||
|
1 => (0, 2),
|
||||||
|
_ => (0, 1),
|
||||||
|
};
|
||||||
|
let sign = if face % 2 == 0 { 1.0 } else { -1.0 };
|
||||||
|
let mut n = [0.0f64; 3];
|
||||||
|
n[axis] = sign;
|
||||||
|
n[b] = sign * tan(cell % 52);
|
||||||
|
n[c] = sign * tan(cell / 52);
|
||||||
|
let length = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
|
||||||
|
n.map(|v| (v / length) as f32)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn decode(b: &[u8]) -> Option<Scan> {
|
pub fn decode(b: &[u8]) -> Option<Scan> {
|
||||||
if b.get(0..5)? != b"ADOCT" {
|
if b.get(0..5)? != b"ADOCT" {
|
||||||
return None;
|
return None;
|
||||||
@@ -84,6 +111,7 @@ pub fn decode(b: &[u8]) -> Option<Scan> {
|
|||||||
local: Vec::new(),
|
local: Vec::new(),
|
||||||
colors: Vec::new(),
|
colors: Vec::new(),
|
||||||
intensity: Vec::new(),
|
intensity: Vec::new(),
|
||||||
|
normals: Vec::new(),
|
||||||
has_rgb,
|
has_rgb,
|
||||||
has_normals,
|
has_normals,
|
||||||
has_intensity,
|
has_intensity,
|
||||||
@@ -116,6 +144,7 @@ pub fn decode(b: &[u8]) -> Option<Scan> {
|
|||||||
scan.local.push([cube_min[0] + q(0), cube_min[1] + q(18), cube_min[2] + q(36)]);
|
scan.local.push([cube_min[0] + q(0), cube_min[1] + q(18), cube_min[2] + q(36)]);
|
||||||
scan.intensity.push(rec[8]);
|
scan.intensity.push(rec[8]);
|
||||||
scan.colors.push([rec[11], rec[10], rec[9]]);
|
scan.colors.push([rec[11], rec[10], rec[9]]);
|
||||||
|
scan.normals.push(normal(u16::from_le_bytes([rec[12], rec[13]])));
|
||||||
}
|
}
|
||||||
block = end;
|
block = end;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user