use cadmpeg_ir::geometry::{CurveGeometry, SurfaceGeometry};
use cadmpeg_ir::le::{u16_at as u16_le, u32_at as u32_le};
use cadmpeg_ir::math::Point3;
use crate::wire::bytes::{f64_le, f64_point, f64_vector, read_f64_array, u32_le_24};
use crate::wire::records::scan_vertex_records;
#[derive(Debug, Clone)]
pub struct E5Circle {
pub pos: usize,
pub geometry: CurveGeometry,
}
#[derive(Debug, Clone)]
pub struct E5Plane {
pub pos: usize,
pub record_id: u32,
pub origin: [f64; 3],
#[cfg(test)]
pub u_range: [f64; 2],
#[cfg(test)]
pub v_range: [f64; 2],
}
#[derive(Debug, Clone)]
pub struct E5Surface {
pub pos: usize,
pub record_id: u32,
pub geometry: SurfaceGeometry,
pub uv_scale: [f64; 2],
}
#[derive(Clone, Copy)]
struct E5Record {
pos: usize,
end: usize,
class: u8,
size: usize,
}
fn e5_records(data: &[u8]) -> Vec<E5Record> {
const MARKER: &[u8; 3] = b"\xe5\x0d\x03";
let mut records = Vec::new();
let mut position = 0;
while position + 13 <= data.len() {
let Some(relative) = data[position..]
.windows(MARKER.len())
.position(|bytes| bytes == MARKER)
else {
break;
};
let pos = position + relative;
let Some(size) = u16_le(data, pos + 5).map(usize::from) else {
break;
};
let Some(end) = pos.checked_add(size + 13) else {
break;
};
if end > data.len() {
break;
}
records.push(E5Record {
pos,
end,
class: data[pos + 3],
size,
});
position = end;
}
records
}
#[must_use]
pub fn e5_vertices(data: &[u8], vertex_count: usize) -> Vec<Point3> {
if vertex_count == 0 {
return Vec::new();
}
let records = e5_records(data);
let mut runs = Vec::new();
let mut region_start = 0usize;
for record in records {
runs.extend(vertex_runs(&data[region_start..record.pos]));
region_start = record.end;
}
runs.extend(vertex_runs(&data[region_start..]));
let Some(run_count) = runs
.iter()
.try_fold(0usize, |count, run| count.checked_add(run.len()))
else {
return Vec::new();
};
if run_count != vertex_count {
return Vec::new();
}
runs.into_iter().flatten().collect()
}
fn vertex_runs(bytes: &[u8]) -> Vec<Vec<Point3>> {
let mut runs = Vec::new();
let mut position = 0usize;
while position + 15 <= bytes.len() {
if bytes[position..position + 3] != [0x05, 0x08, 0x01] {
position += 1;
continue;
}
let start = position;
while position + 15 <= bytes.len() && bytes[position..position + 3] == [0x05, 0x08, 0x01] {
position += 15;
}
let vertices = scan_vertex_records(&bytes[start..position]);
if !vertices.is_empty() {
runs.push(vertices);
}
}
runs
}
pub fn e5_circles(data: &[u8]) -> Vec<E5Circle> {
let mut out = Vec::new();
for record in e5_records(data) {
let pos = record.pos;
if record.class == 0xc9 && record.size >= 81 {
let origin = f64_point(data, pos + 14);
let frame_u = f64_vector(data, pos + 38);
let frame_v = f64_vector(data, pos + 62);
let radius = f64_le(data, pos + 86);
if let (Some(origin), Some(frame_u), Some(frame_v), Some(radius)) =
(origin, frame_u, frame_v, radius)
{
if radius > 0.05 && radius < 1e3 {
if let Some(axis) = frame_u.cross(frame_v).unit() {
out.push(E5Circle {
pos,
geometry: CurveGeometry::Circle {
center: origin,
axis,
ref_direction: frame_u.unit().unwrap_or_else(|| {
cadmpeg_ir::geometry::derive_reference_direction(axis)
}),
radius,
},
});
}
}
}
}
}
out
}
#[must_use]
pub fn e5_planes(data: &[u8]) -> Vec<E5Plane> {
let mut out = Vec::new();
for record in e5_records(data) {
let pos = record.pos;
if record.class != 0xc8 || record.size < 90 || (record.size - 90) % 8 != 0 {
continue;
}
let Some(origin) = read_f64_array::<3>(data, pos + 14) else {
continue;
};
let scalar_count = (record.size - 58) / 8;
let scalars_finite = (0..scalar_count)
.all(|index| f64_le(data, pos + 39 + 8 * index).is_some_and(f64::is_finite));
let Some(bounds) = read_f64_array::<4>(data, record.end - 32) else {
continue;
};
if !scalars_finite || origin.iter().chain(&bounds).any(|value| !value.is_finite()) {
continue;
}
out.push(E5Plane {
pos,
record_id: u32_le(data, pos + 9).unwrap_or(0),
origin,
#[cfg(test)]
u_range: [bounds[0], bounds[1]],
#[cfg(test)]
v_range: [bounds[2], bounds[3]],
});
}
out
}
#[derive(Debug, Clone)]
pub struct E5Edge {
pub start_vertex_id: u32,
pub end_vertex_id: u32,
}
pub fn e5_edges(data: &[u8]) -> Vec<E5Edge> {
let mut out = Vec::new();
for record in e5_records(data) {
let pos = record.pos;
if record.class == 0xff && data.get(pos + 13) == Some(&0x85) {
let payload = &data[pos + 13..record.end];
if let Some((_, next)) = e5_ref(payload, 1) {
if let Some((start_vertex_id, next)) = e5_ref(payload, next) {
if let Some((end_vertex_id, _)) = e5_ref(payload, next) {
out.push(E5Edge {
start_vertex_id,
end_vertex_id,
});
}
}
}
}
}
out
}
pub fn e5_surfaces(data: &[u8]) -> Vec<E5Surface> {
let mut out = Vec::new();
for record in e5_records(data) {
let pos = record.pos;
let decoded = match record.class {
0xc9 => e5_cylinder(data, pos).map(|geometry| {
let SurfaceGeometry::Cylinder { radius, .. } = geometry else {
unreachable!()
};
(geometry, [1.0 / radius, 1.0])
}),
0xca => e5_cone(data, pos).and_then(|geometry| {
let SurfaceGeometry::Cone { half_angle, .. } = geometry else {
unreachable!()
};
let u_scale = f64_le(data, pos + 158)?;
let v_scale = f64_le(data, pos + 166)?;
(u_scale.is_finite()
&& u_scale.abs() > 1e-12
&& v_scale.is_finite()
&& v_scale.abs() > 1e-12)
.then_some((geometry, [1.0 / u_scale, half_angle.cos() / v_scale]))
}),
0xcc => e5_torus(data, pos).map(|geometry| {
let SurfaceGeometry::Torus {
major_radius,
minor_radius,
..
} = geometry
else {
unreachable!()
};
(geometry, [1.0 / major_radius, 1.0 / minor_radius])
}),
_ => None,
};
if let Some((geometry, uv_scale)) = decoded {
out.push(E5Surface {
pos,
record_id: u32_le(data, pos + 9).unwrap_or(0),
geometry,
uv_scale,
});
}
}
out
}
fn e5_cylinder(data: &[u8], pos: usize) -> Option<SurfaceGeometry> {
let mut c = crate::wire::cursor::Cursor::new_at(data, pos + 14);
let origin = c.point3()?;
let (geometry, radius) = crate::analytic::cylinder_uvr(&mut c, origin)?;
if !radius.is_finite() || !(0.05..1e3).contains(&radius) {
return None;
}
Some(geometry)
}
fn e5_cone(data: &[u8], pos: usize) -> Option<SurfaceGeometry> {
let mut c = crate::wire::cursor::Cursor::new_at(data, pos + 14);
let (geometry, radius, half_angle) = crate::analytic::cone_ozra(&mut c)?;
if !(radius > 0.0
&& radius < 1e6
&& half_angle > 0.0
&& half_angle < std::f64::consts::FRAC_PI_2)
{
return None;
}
Some(geometry)
}
fn e5_torus(data: &[u8], pos: usize) -> Option<SurfaceGeometry> {
let mut c = crate::wire::cursor::Cursor::new_at(data, pos + 14);
let (geometry, major_radius, minor_radius) = crate::analytic::torus_ozrr(&mut c)?;
if !(major_radius > 0.0 && major_radius < 1e6 && minor_radius > 0.0 && minor_radius < 1e6) {
return None;
}
Some(geometry)
}
fn e5_ref(bytes: &[u8], at: usize) -> Option<(u32, usize)> {
match *bytes.get(at)? {
0x38 => Some((u32_le_24(bytes, at + 1)?, at + 4)),
0x18 => Some((u16_le(bytes, at + 1)? as u32, at + 3)),
0x10 => Some((u32::from(*bytes.get(at + 1)?) << 8, at + 2)),
0x08 => Some((*bytes.get(at + 1)? as u32, at + 2)),
byte if byte >= 0x80 => Some(((byte - 0x80) as u32, at + 1)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::e5_ref;
#[test]
fn e5_width_coded_reference_widens_before_shifting() {
assert_eq!(e5_ref(&[0x10, 0xff], 0), Some((0xff00, 2)));
}
}