use crate::kernel_header::RefWidth;
use crate::sab::{int_le_at, vec3_le_at};
use cadmpeg_core::decode::View;
use cadmpeg_ir::geometry::nurbs::{NurbsPoleGrid, NurbsPoles3, WeightedPole3};
use cadmpeg_ir::math::{Point3, Vector3};
use cadmpeg_ir::scalar::NonZeroReal;
#[derive(Debug, Clone)]
pub(super) enum ReadPoles3 {
Polynomial(Vec<Point3>),
Rational(Vec<WeightedPole3>),
}
impl ReadPoles3 {
pub(super) fn empty(rational: bool) -> Self {
if rational {
Self::Rational(Vec::new())
} else {
Self::Polynomial(Vec::new())
}
}
pub(super) fn with_counted_capacity(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
count: usize,
rational: bool,
) -> Result<Self, cadmpeg_core::CodecError> {
if rational {
Ok(Self::Rational(
ctx.collection_vec(count, "ASM rational NURBS poles")?,
))
} else {
Ok(Self::Polynomial(
ctx.collection_vec(count, "ASM polynomial NURBS poles")?,
))
}
}
pub(super) fn push(&mut self, point: Point3, weight: f64) -> Option<()> {
match self {
Self::Polynomial(points) => points.push(point),
Self::Rational(points) => points.push(WeightedPole3 {
point,
weight: NonZeroReal::new(weight)?,
}),
}
Some(())
}
pub(super) fn into_lane(self) -> NurbsPoles3 {
match self {
Self::Polynomial(points) => NurbsPoles3::Polynomial { points },
Self::Rational(points) => NurbsPoles3::Rational { points },
}
}
pub(super) fn into_transposed_grid(
self,
u_count: usize,
v_count: usize,
) -> Option<NurbsPoleGrid> {
fn transpose<T: Clone>(flat: &[T], u_count: usize, v_count: usize) -> Option<Vec<Vec<T>>> {
(flat.len() == u_count.checked_mul(v_count)?).then_some(())?;
(0..u_count)
.map(|u| {
(0..v_count)
.map(|v| flat.get(v.checked_mul(u_count)?.checked_add(u)?).cloned())
.collect::<Option<Vec<_>>>()
})
.collect()
}
match self {
Self::Polynomial(points) => Some(NurbsPoleGrid::Polynomial {
rows: transpose(&points, u_count, v_count)?,
}),
Self::Rational(points) => Some(NurbsPoleGrid::Rational {
rows: transpose(&points, u_count, v_count)?,
}),
}
}
pub(super) fn into_counted_transposed_grid(
self,
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
u_count: usize,
v_count: usize,
) -> Option<Result<NurbsPoleGrid, cadmpeg_core::CodecError>> {
fn transpose<T: Clone>(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
flat: &[T],
u_count: usize,
v_count: usize,
) -> Option<Result<Vec<Vec<T>>, cadmpeg_core::CodecError>> {
(flat.len() == u_count.checked_mul(v_count)?).then_some(())?;
let mut rows = match ctx.collection_vec(u_count, "ASM NURBS grid rows") {
Ok(rows) => rows,
Err(error) => return Some(Err(error)),
};
for u in 0..u_count {
let mut row = match ctx.collection_vec(v_count, "ASM NURBS grid row poles") {
Ok(row) => row,
Err(error) => return Some(Err(error)),
};
for v in 0..v_count {
row.push(flat.get(v.checked_mul(u_count)?.checked_add(u)?)?.clone());
}
rows.push(row);
}
Some(Ok(rows))
}
match self {
Self::Polynomial(points) => transpose(ctx, &points, u_count, v_count)
.map(|result| result.map(|rows| NurbsPoleGrid::Polynomial { rows })),
Self::Rational(points) => transpose(ctx, &points, u_count, v_count)
.map(|result| result.map(|rows| NurbsPoleGrid::Rational { rows })),
}
}
}
pub const LEN_TO_MM: f64 = 10.0;
pub(crate) const NUBS_MARKER: &[u8] = b"\x0d\x04nubs";
const NURBS_MARKER: &[u8] = b"\x0d\x05nurbs";
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) enum BsplineMarker {
Nubs,
Nurbs,
}
impl BsplineMarker {
pub(super) fn byte_len(self) -> usize {
match self {
Self::Nubs => NUBS_MARKER.len(),
Self::Nurbs => NURBS_MARKER.len(),
}
}
pub(super) fn cp_dims(self) -> usize {
match self {
Self::Nubs => 3,
Self::Nurbs => 4,
}
}
pub(super) fn rational(self) -> bool {
self == Self::Nurbs
}
}
pub(super) const INT_WIDTHS: [RefWidth; 2] = [RefWidth::Eight, RefWidth::Four];
pub(super) fn take_tagged_int(
b: &[u8],
pos: &mut usize,
tag: u8,
int_width: RefWidth,
) -> Option<i64> {
if *b.get(*pos)? != tag {
return None;
}
let v = int_le_at(b, *pos + 1, int_width)?;
*pos += 1 + int_width.bytes();
Some(v)
}
pub(super) fn marker_at(b: &[u8], pos: usize) -> Option<BsplineMarker> {
if b[pos..].starts_with(NUBS_MARKER) {
Some(BsplineMarker::Nubs)
} else if b[pos..].starts_with(NURBS_MARKER) {
Some(BsplineMarker::Nurbs)
} else {
None
}
}
pub(super) fn marker_positions(b: &[u8]) -> Vec<usize> {
let mut out = Vec::new();
if b.len() < NUBS_MARKER.len() {
return out;
}
for pos in 0..=b.len() - NUBS_MARKER.len() {
if marker_at(b, pos).is_some() {
out.push(pos);
}
}
out
}
fn owned_marker_positions(b: &[u8], int_width: RefWidth) -> Option<Vec<usize>> {
let (out, balanced) = walk_owned_markers(b, int_width);
balanced.then_some(out)
}
impl crate::nurbs::subtypes::SubtypeScope<'_> {
pub(super) fn owned_marker_positions(&self, int_width: RefWidth) -> Vec<usize> {
walk_owned_markers(self.bytes(), int_width).0
}
}
fn walk_owned_markers(b: &[u8], int_width: RefWidth) -> (Vec<usize>, bool) {
let mut out = Vec::new();
let mut depth = 0usize;
let mut outer = usize::from(b.first() == Some(&0x0f));
let mut pos = outer;
while pos < b.len() {
match b[pos] {
0x0f => depth += 1,
0x10 => match depth.checked_sub(1) {
Some(next) => depth = next,
None => match outer.checked_sub(1) {
Some(next) => outer = next,
None => return (out, false),
},
},
_ => {
if depth == 0 && marker_at(b, pos).is_some() {
out.push(pos);
}
}
}
match crate::nurbs::subtypes::next_token(b, pos, int_width) {
Some(next) => pos = next,
None => return (out, false),
}
}
(out, depth == 0 && outer == 0)
}
pub(super) fn construction_marker_positions(b: &[u8], int_width: RefWidth) -> Option<Vec<usize>> {
let candidates = crate::nurbs::subtypes::owned_subtype_defs(b, int_width)?
.into_iter()
.filter(|(_, name)| *name != b"ref")
.filter_map(|(start, _)| {
let scope = crate::nurbs::subtypes::subtype_span(b, start, int_width)?;
let positions = scope
.owned_marker_positions(int_width)
.into_iter()
.map(|position| start + position)
.collect::<Vec<_>>();
(!positions.is_empty()).then_some(positions)
})
.collect::<Vec<_>>();
if candidates.is_empty() {
return owned_marker_positions(b, int_width);
}
if candidates.len() != 1 {
return Some(Vec::new());
}
candidates.into_iter().next()
}
const MAX_NURBS_POLES: usize = 100_000;
const MAX_NURBS_DEGREE: usize = 20;
const MAX_EXPANDED_NURBS_KNOTS: usize = MAX_NURBS_POLES + MAX_NURBS_DEGREE + 1;
pub(in crate::nurbs) struct KnotExpansionLayout {
pub(super) n_poles: usize,
expanded_len: usize,
}
impl KnotExpansionLayout {
pub(super) fn expanded_len(&self) -> usize {
self.expanded_len
}
}
pub(super) fn checked_knot_layout(
multiplicities: &[i64],
degree: i64,
) -> Option<KnotExpansionLayout> {
let degree = usize::try_from(degree)
.ok()
.filter(|degree| (1..=MAX_NURBS_DEGREE).contains(degree))?;
let mut sum = 0usize;
let mut expanded_len = 0usize;
for (index, &multiplicity) in multiplicities.iter().enumerate() {
let multiplicity = usize::try_from(multiplicity).ok()?;
sum = sum.checked_add(multiplicity)?;
let endpoint_extra = usize::from(index == 0 || index + 1 == multiplicities.len());
let run_length = multiplicity.checked_add(endpoint_extra)?;
expanded_len = expanded_len.checked_add(run_length)?;
if expanded_len > MAX_EXPANDED_NURBS_KNOTS {
return None;
}
}
let n_poles = sum.checked_sub(degree - 1)?;
if !(2..=MAX_NURBS_POLES).contains(&n_poles) {
return None;
}
let derived_max = n_poles.checked_add(degree)?.checked_add(1)?;
(expanded_len <= derived_max).then_some(KnotExpansionLayout {
n_poles,
expanded_len,
})
}
pub struct KnotLayout {
pub value_offsets: Vec<usize>,
}
pub(super) fn read_knots(
b: &[u8],
pos: &mut usize,
n: usize,
degree: i64,
int_width: RefWidth,
) -> Option<(Vec<f64>, usize, KnotLayout)> {
let pair_bytes = 9 + 1 + int_width.bytes();
if n > b.len().checked_sub(*pos)? / pair_bytes {
return None;
}
let mut knots = Vec::new();
let mut mults = Vec::new();
let mut value_offsets = Vec::new();
for _ in 0..n {
if *b.get(*pos)? != 0x06 {
return None;
}
value_offsets.push(*pos + 1);
knots.push(View::f64_le_at(b, *pos + 1)?);
*pos += 9;
mults.push(take_tagged_int(b, pos, 0x04, int_width)?);
}
let expansion = checked_knot_layout(&mults, degree)?;
let mut expanded = Vec::new();
for (index, (kv, multiplicity)) in knots.iter().zip(&mults).enumerate() {
let run_length = usize::try_from(*multiplicity).ok()?
+ usize::from(index == 0 || index + 1 == mults.len());
for _ in 0..run_length {
expanded.push(*kv);
}
}
Some((expanded, expansion.n_poles, KnotLayout { value_offsets }))
}
pub(super) fn read_control_points(
b: &[u8],
pos: &mut usize,
count: usize,
marker: BsplineMarker,
) -> Option<ReadPoles3> {
let mut poles = ReadPoles3::empty(marker.rational());
for _ in 0..count {
let mut comps = [0.0f64; 4];
for comp in comps.iter_mut().take(marker.cp_dims()) {
if *b.get(*pos)? != 0x06 {
return None;
}
*comp = View::f64_le_at(b, *pos + 1)?;
*pos += 9;
}
poles.push(
Point3::new(
comps[0] * LEN_TO_MM,
comps[1] * LEN_TO_MM,
comps[2] * LEN_TO_MM,
),
comps[3],
)?;
}
Some(poles)
}
pub(super) fn is_periodic(enum_val: i64) -> bool {
enum_val == 2
}
pub(super) enum Nullable<T> {
Null,
Value(T),
}
impl<T> Nullable<T> {
pub(super) fn value(self) -> Option<T> {
match self {
Self::Null => None,
Self::Value(value) => Some(value),
}
}
}
pub(super) fn take_double_payload(bytes: &[u8], position: &mut usize) -> Option<usize> {
(*bytes.get(*position)? == 0x06).then_some(())?;
let payload = *position + 1;
bytes.get(payload..payload + 8)?;
*position = payload + 8;
Some(payload)
}
pub(super) fn take_float_array_payloads(
bytes: &[u8],
position: &mut usize,
int_width: RefWidth,
) -> Option<Vec<usize>> {
(*bytes.get(*position)? == 0x04).then_some(())?;
let count = usize::try_from(int_le_at(bytes, *position + 1, int_width)?).ok()?;
*position += 1 + int_width.bytes();
(0..count)
.map(|_| take_double_payload(bytes, position))
.collect()
}
pub(super) fn take_f64(bytes: &[u8], position: &mut usize) -> Option<f64> {
if bytes.get(*position) != Some(&0x06) {
return None;
}
let value = View::f64_le_at(bytes, *position + 1)?;
*position += 9;
Some(value)
}
pub(super) fn take_bool(bytes: &[u8], position: &mut usize) -> Option<bool> {
let value = match bytes.get(*position)? {
0x0a => true,
0x0b => false,
_ => return None,
};
*position += 1;
Some(value)
}
pub(super) fn normalized(value: [f64; 3]) -> Option<cadmpeg_ir::units::UnitVector3> {
cadmpeg_ir::units::UnitVector3::normalized_nonzero(cadmpeg_ir::features::FiniteVector3::new(
Vector3::from(value),
)?)
}
pub(super) fn take_native_ident(bytes: &[u8], position: &mut usize) -> Option<String> {
if !matches!(bytes.get(*position), Some(0x0d | 0x0e)) {
return None;
}
let length = usize::from(*bytes.get(*position + 1)?);
let start = *position + 2;
let end = start.checked_add(length)?;
let value = String::from_utf8(bytes.get(start..end)?.to_vec()).ok()?;
*position = end;
Some(value)
}
pub(super) fn take_native_string(
bytes: &[u8],
position: &mut usize,
int_width: RefWidth,
) -> Option<String> {
let (length, header) = match *bytes.get(*position)? {
0x07 => (usize::from(*bytes.get(*position + 1)?), 2),
0x08 => (usize::from(View::u16_le_at(bytes, *position + 1)?), 3),
0x09 => (
usize::try_from(int_le_at(bytes, *position + 1, int_width)?).ok()?,
1 + int_width.bytes(),
),
_ => return None,
};
let start = *position + header;
let end = start.checked_add(length)?;
let value = String::from_utf8(bytes.get(start..end)?.to_vec()).ok()?;
*position = end;
Some(value)
}
pub(super) fn take_range_value(bytes: &[u8], position: &mut usize) -> Option<f64> {
if matches!(bytes.get(*position), Some(0x0a | 0x0b)) {
*position += 1;
}
if bytes.get(*position) != Some(&0x06) {
return None;
}
let value = View::f64_le_at(bytes, *position + 1)?;
*position += 9;
Some(value)
}
pub(super) fn take_optional_range_value(
bytes: &[u8],
position: &mut usize,
) -> Option<Nullable<f64>> {
match bytes.get(*position)? {
0x0a => {
*position += 1;
take_f64(bytes, position).map(Nullable::Value)
}
0x0b => {
*position += 1;
Some(Nullable::Null)
}
0x06 => take_f64(bytes, position).map(Nullable::Value),
_ => None,
}
}
pub(super) fn take_native_vec3(bytes: &[u8], position: &mut usize, tag: u8) -> Option<[f64; 3]> {
if bytes.get(*position) != Some(&tag) {
return None;
}
let values = vec3_le_at(bytes, *position + 1)?;
*position += 25;
Some(values)
}
#[cfg(test)]
mod marker_ownership_tests {
use super::{owned_marker_positions, NUBS_MARKER};
use crate::kernel_header::RefWidth;
#[test]
fn raw_marker_walk_refuses_unclosed_own_and_nested_scopes() {
let mut bytes = vec![0x0f];
bytes.extend_from_slice(NUBS_MARKER);
assert_eq!(owned_marker_positions(&bytes, RefWidth::Four), None);
bytes.push(0x10);
assert_eq!(
owned_marker_positions(&bytes, RefWidth::Four),
Some(vec![1])
);
bytes.push(0x0f);
assert_eq!(owned_marker_positions(&bytes, RefWidth::Four), None);
}
}
#[cfg(test)]
mod string_width_tests {
use super::{take_native_string, ReadPoles3};
use crate::kernel_header::RefWidth;
use cadmpeg_ir::geometry::nurbs::NurbsPoleGrid;
use cadmpeg_ir::math::Point3;
#[test]
fn token_surface_grid_rows_refuse_collection_limit() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_collection_items = 5;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let points = (0..4)
.map(|index| Point3::new(f64::from(index), 0.0, 0.0))
.collect();
let error = ReadPoles3::Polynomial(points)
.into_counted_transposed_grid(&ctx, 2, 2)
.expect("valid grid")
.expect_err("two rows and four poles need six items");
let CodecError::ResourceLimit(limit) = error else {
panic!("expected collection refusal: {error:?}");
};
assert_eq!(limit.dimension, ResourceDimension::CollectionItems);
}
fn long_string_bytes(payload: &str, int_width: RefWidth) -> Vec<u8> {
let mut bytes = vec![0x09];
let mut length = (cadmpeg_core::decode::u64_from_index(payload.len()))
.to_le_bytes()
.to_vec();
length.truncate(int_width.bytes());
bytes.extend_from_slice(&length);
bytes.extend_from_slice(payload.as_bytes());
bytes
}
#[test]
fn long_string_length_prefix_is_the_stream_int_width() {
for int_width in [RefWidth::Four, RefWidth::Eight] {
let bytes = long_string_bytes("#TS0200\ndegree 3", int_width);
let mut position = 0;
let value = take_native_string(&bytes, &mut position, int_width)
.unwrap_or_else(|| panic!("string at width {int_width}"));
assert_eq!(value, "#TS0200\ndegree 3");
assert_eq!(position, bytes.len());
}
}
#[test]
fn long_string_read_at_the_wrong_width_never_yields_the_payload() {
let bytes = long_string_bytes("#TS0200\ndegree 3", RefWidth::Eight);
let mut position = 0;
let value = take_native_string(&bytes, &mut position, RefWidth::Four);
assert_ne!(value.as_deref(), Some("#TS0200\ndegree 3"));
}
#[test]
fn read_poles_state_rows_and_refuse_an_unusable_weight() {
let mut poles = ReadPoles3::empty(true);
assert!(poles.push(Point3::new(0.0, 0.0, 0.0), 1.0).is_some());
assert!(poles.push(Point3::new(1.0, 0.0, 0.0), 0.0).is_none());
let ReadPoles3::Rational(rows) = poles else {
panic!("a rational read states weighted rows");
};
assert_eq!(rows.len(), 1);
let mut grid = ReadPoles3::empty(false);
for index in 0..4 {
assert!(grid
.push(Point3::new(f64::from(index), 0.0, 0.0), 1.0)
.is_some());
}
let NurbsPoleGrid::Polynomial { rows } = grid
.into_transposed_grid(2, 2)
.expect("a four-pole read states a two-by-two grid")
else {
panic!("a polynomial read states unweighted rows");
};
assert_eq!(rows[0][0].x, 0.0);
assert_eq!(rows[0][1].x, 2.0);
assert_eq!(rows[1][0].x, 1.0);
assert_eq!(rows[1][1].x, 3.0);
}
}