use crate::kernel_header::RefWidth;
use crate::nurbs::reader::{
construction_marker_positions, is_periodic, marker_at, marker_positions, read_control_points,
read_knots, take_tagged_int, BsplineMarker, KnotLayout, ReadPoles3, INT_WIDTHS, LEN_TO_MM,
};
use crate::nurbs::subtypes;
use crate::nurbs::toks;
use crate::nurbs::toks::Cur;
use crate::sab::Token;
use cadmpeg_ir::geometry::nurbs::{NurbsCurve, NurbsSurface, NurbsSurfaceAxis};
use cadmpeg_ir::math::Point3;
use crate::nurbs::toks::take_knot_table as knots;
const MAX_RECOVERY_SURFACE_POLES: u64 = 200_000;
macro_rules! propagate_resource {
($result:expr) => {
match $result {
Ok(value) => value,
Err(error) => return Some(Err(error)),
}
};
}
fn control_points(cur: &mut Cur<'_>, 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()) {
*comp = cur.take_f64()?;
}
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 surface_block(toks: &[Token], marker_pos: usize) -> Option<(NurbsSurface, usize)> {
let marker = toks::marker_at(toks, marker_pos)?;
let mut cur = Cur::at(toks, marker_pos + 1);
let degree_u = cur.take_long()?;
let degree_v = cur.take_long()?;
if !(1..=20).contains(°ree_u) || !(1..=20).contains(°ree_v) {
return None;
}
if matches!(cur.peek(), Some(Token::Ident(_))) {
cur.bump();
}
let mut enums = [0i64; 4];
for e in &mut enums {
*e = cur.take_enum()?;
}
let n_uniq_u = cur.take_long()?;
let n_uniq_v = cur.take_long()?;
if n_uniq_u < 1 || n_uniq_v < 1 {
return None;
}
let (u_knots, n_poles_u) = knots(&mut cur, usize::try_from(n_uniq_u).ok()?, degree_u)?;
let (v_knots, n_poles_v) = knots(&mut cur, usize::try_from(n_uniq_v).ok()?, degree_v)?;
let pole_count = n_poles_u.checked_mul(n_poles_v)?;
if cadmpeg_core::decode::u64_from_index(pole_count) > MAX_RECOVERY_SURFACE_POLES {
return None;
}
let poles = control_points(&mut cur, pole_count, marker)?;
let grid = poles.into_counted_transposed_grid(n_poles_u, n_poles_v)?;
let surface = NurbsSurface::new(
NurbsSurfaceAxis::new(
u32::try_from(degree_u).ok()?,
u_knots,
is_periodic(enums[0]),
),
NurbsSurfaceAxis::new(
u32::try_from(degree_v).ok()?,
v_knots,
is_periodic(enums[1]),
),
grid,
false,
)
.ok()?;
Some((surface, cur.pos()))
}
pub(super) fn curve_block(toks: &[Token], marker_pos: usize) -> Option<(NurbsCurve, usize)> {
let marker = toks::marker_at(toks, marker_pos)?;
let mut cur = Cur::at(toks, marker_pos + 1);
let degree = cur.take_long()?;
if !(1..=20).contains(°ree) {
return None;
}
let closure = cur.take_enum()?;
let n_uniq = cur.take_long()?;
if n_uniq < 1 {
return None;
}
let (knot_vector, n_poles) = knots(&mut cur, usize::try_from(n_uniq).ok()?, degree)?;
let poles = control_points(&mut cur, n_poles, marker)?;
let curve = NurbsCurve::new(
u32::try_from(degree).ok()?,
knot_vector,
poles.into_lane(),
is_periodic(closure),
)
.ok()?;
Some((curve, cur.pos()))
}
pub(super) fn surface_cache(toks: &[Token]) -> Option<NurbsSurface> {
let scope = toks::cache_scope(toks)?;
let positions = toks::owned_marker_positions(scope)?;
let compound = scope.iter().any(|token| {
matches!(token, Token::Ident(name) | Token::SubIdent(name) if name == "comp_spl_sur")
});
if compound {
positions
.into_iter()
.find_map(|pos| surface_block(scope, pos).map(|(surface, _)| surface))
} else {
positions
.into_iter()
.rev()
.find_map(|pos| surface_block(scope, pos).map(|(surface, _)| surface))
}
}
pub(super) fn owned_surface_cache(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
scope: toks::SubtypeScope<'_>,
table: Option<&toks::SubtypeTable>,
) -> Option<Result<NurbsSurface, cadmpeg_core::CodecError>> {
let tokens = scope.tokens();
if matches!(scope.interior().first(), Some(Token::Ident(name) | Token::SubIdent(name))
if crate::nurbs::blend::VARIABLE_BLEND_NAMES.contains(&name.as_str()))
{
let _depth = propagate_resource!(ctx.enter_nested("admit ASM variable-blend cache"));
let cache_marker = propagate_resource!(crate::nurbs::blend::var_blend_spl_sur(
ctx,
tokens,
table,
crate::nurbs::blend::SupportCaches::Skip,
)
.transpose()
.map(|decoded| decoded.and_then(|decoded| decoded.current_cache_marker)));
return surface_block(tokens, cache_marker?).map(|(surface, _)| Ok(surface));
}
scope
.owned_marker_positions()
.into_iter()
.find_map(|pos| surface_block(tokens, pos).map(|(surface, _)| Ok(surface)))
}
pub(super) fn curve_cache(toks: &[Token]) -> Option<NurbsCurve> {
let scope = toks::cache_scope(toks)?;
toks::owned_marker_positions(scope)?
.into_iter()
.find_map(|pos| curve_block(scope, pos).map(|(curve, _)| curve))
}
pub(super) fn owned_curve_cache(scope: toks::SubtypeScope<'_>) -> Option<NurbsCurve> {
let tokens = scope.tokens();
scope
.owned_marker_positions()
.into_iter()
.find_map(|pos| curve_block(tokens, pos).map(|(curve, _)| curve))
}
enum CacheLookup<T> {
Found(T),
Missing,
Rejected,
}
fn cache_from_subtype_refs<T, E>(
toks: &[Token],
table: &toks::SubtypeTable,
decode_scope: impl Fn(toks::SubtypeScope<'_>) -> Result<CacheLookup<T>, E>,
) -> Option<Result<T, E>> {
let mut seen = std::collections::HashSet::new();
let mut pending = Vec::new();
pending.push(toks::subtype_refs(toks));
while let Some(references) = pending.last_mut() {
let Some(index) = references.next() else {
pending.pop();
continue;
};
if !seen.insert(index) {
continue;
}
let target = table.span(index)?;
match decode_scope(target) {
Ok(CacheLookup::Found(decoded)) => return Some(Ok(decoded)),
Ok(CacheLookup::Rejected) => return None,
Ok(CacheLookup::Missing) => {}
Err(error) => return Some(Err(error)),
}
pending.push(toks::subtype_refs(target.tokens()));
}
None
}
pub fn surface_cache_resolving_refs(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
toks: &[Token],
table: &toks::SubtypeTable,
) -> Option<Result<NurbsSurface, cadmpeg_core::CodecError>> {
if let Some((start, _)) =
toks::find_owned_subtype_marker(toks, crate::nurbs::blend::VARIABLE_BLEND_NAMES)
{
return owned_surface_cache(ctx, toks::subtype_span(toks, start)?, Some(table));
}
surface_cache(toks).map(Ok).or_else(|| {
cache_from_subtype_refs(toks, table, |scope| {
if matches!(scope.interior().first(), Some(Token::Ident(name) | Token::SubIdent(name))
if crate::nurbs::blend::VARIABLE_BLEND_NAMES.contains(&name.as_str()))
{
owned_surface_cache(ctx, scope, Some(table))
.transpose()
.map(|cache| cache.map_or(CacheLookup::Rejected, CacheLookup::Found))
} else {
Ok(surface_cache(scope.tokens()).map_or(CacheLookup::Missing, CacheLookup::Found))
}
})
})
}
pub(super) fn owned_surface_cache_resolving_refs(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
scope: toks::SubtypeScope<'_>,
table: &toks::SubtypeTable,
) -> Option<Result<NurbsSurface, cadmpeg_core::CodecError>> {
if let Some(cache) = owned_surface_cache(ctx, scope, Some(table)) {
return Some(cache);
}
let index = match scope.interior() {
[Token::Ident(name), Token::Long(index)] if name == "ref" => *index,
[Token::Long(index)] => *index,
_ => return None,
};
let target = table.span(usize::try_from(index).ok()?)?;
owned_surface_cache(ctx, target, Some(table))
}
pub fn curve_cache_resolving_refs(
toks: &[Token],
table: &toks::SubtypeTable,
) -> Option<NurbsCurve> {
curve_cache(toks).or_else(|| {
let Ok(curve) = cache_from_subtype_refs(toks, table, |scope| {
Ok::<_, std::convert::Infallible>(
curve_cache(scope.tokens()).map_or(CacheLookup::Missing, CacheLookup::Found),
)
})?;
Some(curve)
})
}
pub(super) fn owned_curve_cache_resolving_refs(
scope: toks::SubtypeScope<'_>,
table: &toks::SubtypeTable,
) -> Option<NurbsCurve> {
owned_curve_cache(scope).or_else(|| {
let Ok(curve) = cache_from_subtype_refs(scope.tokens(), table, |scope| {
Ok::<_, std::convert::Infallible>(
owned_curve_cache(scope).map_or(CacheLookup::Missing, CacheLookup::Found),
)
})?;
Some(curve)
})
}
pub struct SurfacePatchLayout {
pub surface: NurbsSurface,
control_start: usize,
pub u_knots: KnotLayout,
pub v_knots: KnotLayout,
pub periodic_value_offsets: [usize; 2],
pub degree_value_offsets: [usize; 2],
}
impl SurfacePatchLayout {
pub fn end(&self) -> usize {
self.control_start + self.control_value_offsets().len() * 9
}
pub fn control_value_offsets(&self) -> impl ExactSizeIterator<Item = usize> + '_ {
let components = if self.surface.weights().is_some() {
4
} else {
3
};
(0..self.surface.poles().len() * components)
.map(|ordinal| self.control_start + ordinal * 9 + 1)
}
}
pub(super) fn decode_surface_block(
b: &[u8],
marker_pos: usize,
int_width: RefWidth,
) -> Option<SurfacePatchLayout> {
let marker = marker_at(b, marker_pos)?;
let mut pos = marker_pos + marker.byte_len();
let degree_u_offset = pos + 1;
let degree_u = take_tagged_int(b, &mut pos, 0x04, int_width)?;
let degree_v_offset = pos + 1;
let degree_v = take_tagged_int(b, &mut pos, 0x04, int_width)?;
if !(1..=20).contains(°ree_u) || !(1..=20).contains(°ree_v) {
return None;
}
if b.get(pos) == Some(&0x0d) {
let len = usize::from(*b.get(pos + 1)?);
pos += 2 + len;
}
let mut enums = [0i64; 4];
let mut enum_value_offsets = [0usize; 4];
for (ordinal, e) in enums.iter_mut().enumerate() {
enum_value_offsets[ordinal] = pos + 1;
*e = take_tagged_int(b, &mut pos, 0x15, int_width)?;
}
let n_uniq_u = take_tagged_int(b, &mut pos, 0x04, int_width)?;
let n_uniq_v = take_tagged_int(b, &mut pos, 0x04, int_width)?;
if n_uniq_u < 1 || n_uniq_v < 1 {
return None;
}
let (u_knots, n_poles_u, u_knot_layout) = read_knots(
b,
&mut pos,
usize::try_from(n_uniq_u).ok()?,
degree_u,
int_width,
)?;
let (v_knots, n_poles_v, v_knot_layout) = read_knots(
b,
&mut pos,
usize::try_from(n_uniq_v).ok()?,
degree_v,
int_width,
)?;
if n_poles_u.checked_mul(n_poles_v).is_none_or(|n| n > 200_000) {
return None;
}
let control_start = pos;
let poles = read_control_points(b, &mut pos, n_poles_u * n_poles_v, marker)?;
let grid = poles.into_transposed_grid(n_poles_u, n_poles_v)?;
let surface = NurbsSurface::new(
NurbsSurfaceAxis::new(
u32::try_from(degree_u).ok()?,
u_knots,
is_periodic(enums[0]),
),
NurbsSurfaceAxis::new(
u32::try_from(degree_v).ok()?,
v_knots,
is_periodic(enums[1]),
),
grid,
false,
)
.ok()?;
Some(SurfacePatchLayout {
surface,
control_start,
u_knots: u_knot_layout,
v_knots: v_knot_layout,
periodic_value_offsets: [enum_value_offsets[0], enum_value_offsets[1]],
degree_value_offsets: [degree_u_offset, degree_v_offset],
})
}
pub fn final_surface_patch_layout(
record: &[u8],
int_width: RefWidth,
) -> Option<SurfacePatchLayout> {
construction_marker_positions(record, int_width)?
.into_iter()
.filter_map(|position| decode_surface_block(record, position, int_width))
.next_back()
}
pub fn surface_patch_layout_at(
record: &[u8],
ordinal: usize,
int_width: RefWidth,
) -> Option<SurfacePatchLayout> {
construction_marker_positions(record, int_width)?
.into_iter()
.filter_map(|position| decode_surface_block(record, position, int_width))
.nth(ordinal)
}
pub struct CurvePatchLayout {
pub curve: NurbsCurve,
control_start: usize,
pub knots: KnotLayout,
pub periodic_value_offset: usize,
pub degree_value_offset: usize,
}
impl CurvePatchLayout {
pub fn end(&self) -> usize {
self.control_start + self.control_value_offsets().len() * 9
}
pub fn control_value_offsets(&self) -> impl ExactSizeIterator<Item = usize> + '_ {
let components = if self.curve.weights().is_some() { 4 } else { 3 };
(0..self.curve.control_points().len() * components)
.map(|ordinal| self.control_start + ordinal * 9 + 1)
}
}
pub(super) fn decode_curve_block(
b: &[u8],
marker_pos: usize,
int_width: RefWidth,
) -> Option<CurvePatchLayout> {
let marker = marker_at(b, marker_pos)?;
let mut pos = marker_pos + marker.byte_len();
let degree_value_offset = pos + 1;
let degree = take_tagged_int(b, &mut pos, 0x04, int_width)?;
if !(1..=20).contains(°ree) {
return None;
}
let periodic_value_offset = pos + 1;
let closure = take_tagged_int(b, &mut pos, 0x15, int_width)?;
let n_uniq = take_tagged_int(b, &mut pos, 0x04, int_width)?;
if n_uniq < 1 {
return None;
}
let (knots, n_poles, knot_layout) = read_knots(
b,
&mut pos,
usize::try_from(n_uniq).ok()?,
degree,
int_width,
)?;
let control_start = pos;
let poles = read_control_points(b, &mut pos, n_poles, marker)?;
let curve = NurbsCurve::new(
u32::try_from(degree).ok()?,
knots,
poles.into_lane(),
is_periodic(closure),
)
.ok()?;
Some(CurvePatchLayout {
curve,
control_start,
knots: knot_layout,
periodic_value_offset,
degree_value_offset,
})
}
pub fn first_curve_patch_layout(record: &[u8], int_width: RefWidth) -> Option<CurvePatchLayout> {
construction_marker_positions(record, int_width)?
.into_iter()
.find_map(|position| decode_curve_block(record, position, int_width))
}
pub fn final_curve_patch_layout(record: &[u8], int_width: RefWidth) -> Option<CurvePatchLayout> {
construction_marker_positions(record, int_width)?
.into_iter()
.filter_map(|position| decode_curve_block(record, position, int_width))
.next_back()
}
pub fn decode_surface_cache(record_bytes: &[u8]) -> Option<NurbsSurface> {
decode_unique_cache(record_bytes, |bytes, position, width| {
decode_surface_block(bytes, position, width).map(|candidate| candidate.surface)
})
}
pub(super) fn decode_owned_surface_cache_at(
scope: subtypes::SubtypeScope<'_>,
int_width: RefWidth,
) -> Option<NurbsSurface> {
let bytes = scope.bytes();
scope
.owned_marker_positions(int_width)
.into_iter()
.find_map(|pos| decode_surface_block(bytes, pos, int_width).map(|decoded| decoded.surface))
}
pub fn decode_curve_cache(record_bytes: &[u8]) -> Option<NurbsCurve> {
decode_unique_cache(record_bytes, |bytes, position, width| {
decode_curve_block(bytes, position, width).map(|candidate| candidate.curve)
})
}
pub fn decode_owned_curve_cache_at(
scope: subtypes::SubtypeScope<'_>,
int_width: RefWidth,
) -> Option<NurbsCurve> {
let bytes = scope.bytes();
scope
.owned_marker_positions(int_width)
.into_iter()
.find_map(|pos| decode_curve_block(bytes, pos, int_width).map(|decoded| decoded.curve))
}
fn decode_unique_cache<T>(
record_bytes: &[u8],
decode: impl Fn(&[u8], usize, RefWidth) -> Option<T>,
) -> Option<T> {
let positions = marker_positions(record_bytes);
let mut decoded = None;
for width in INT_WIDTHS {
for &position in &positions {
if let Some(candidate) = decode(record_bytes, position, width) {
if decoded.is_some() {
return None;
}
decoded = Some(candidate);
}
}
}
decoded
}
#[cfg(test)]
mod tests {
use crate::nurbs::toks::SubtypeTable;
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
#[test]
fn cacheless_surface_does_not_borrow_a_referenced_support_cache() {
use crate::sab::{Record, Token};
let arena = DecodeArena::new();
let (ctx, _) =
DecodeContext::from_root_bytes(&[], &arena, &DecodePolicy::service()).unwrap();
let mut tokens = vec![
Token::SubtypeOpen,
Token::Ident("exact_spl_sur".into()),
Token::Ident("nubs".into()),
Token::Long(1),
Token::Long(1),
Token::Enum(0),
Token::Enum(0),
Token::Enum(0),
Token::Enum(0),
Token::Long(2),
Token::Long(2),
];
for _ in 0..2 {
tokens.extend([
Token::Double(0.0),
Token::Long(1),
Token::Double(1.0),
Token::Long(1),
]);
}
for point in [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [1., 1., 0.]] {
tokens.extend(point.map(Token::Double));
}
tokens.push(Token::SubtypeClose);
let record = Record {
index: 0,
name: "spline-surface".into(),
tokens: tokens.into(),
offset: 0,
len: 0,
};
let table = SubtypeTable::from_records(&[record]);
for reference in [
vec![
Token::SubtypeOpen,
Token::Ident("ref".into()),
Token::Long(0),
Token::SubtypeClose,
],
vec![Token::SubtypeOpen, Token::Long(0), Token::SubtypeClose],
] {
let scope = crate::nurbs::toks::subtype_span(&reference, 0).unwrap();
assert!(
super::owned_surface_cache_resolving_refs(&ctx, scope, &table)
.unwrap()
.is_ok()
);
let mut owner = vec![
Token::SubtypeOpen,
Token::Ident("srf_srf_v_bl_spl_sur".into()),
];
owner.extend(reference);
owner.push(Token::SubtypeClose);
let scope = crate::nurbs::toks::subtype_span(&owner, 0).unwrap();
assert!(super::owned_surface_cache_resolving_refs(&ctx, scope, &table).is_none());
}
}
#[test]
fn cached_nurbs_recovery_admits_large_knot_tables() {
use crate::sab::Token;
let mut curve = vec![
Token::Ident("nubs".into()),
Token::Long(1),
Token::Enum(0),
Token::Long(1_001),
];
for index in 0_u32..1_001 {
curve.extend([Token::Double(f64::from(index)), Token::Long(1)]);
}
for index in 0_u32..1_001 {
curve.extend([
Token::Double(f64::from(index)),
Token::Double(0.0),
Token::Double(0.0),
]);
}
let (decoded, end) = super::curve_block(&curve, 0).unwrap();
assert_eq!(decoded.control_points().len(), 1_001);
assert_eq!(decoded.knots().len(), 1_003);
assert_eq!(end, curve.len());
for width in [
crate::kernel_header::RefWidth::Four,
crate::kernel_header::RefWidth::Eight,
] {
let bytes = knot_block_bytes(&curve, width);
let decoded = super::decode_curve_block(&bytes, 0, width).unwrap();
assert_eq!(decoded.curve.control_points().len(), 1_001);
assert_eq!(decoded.knots.value_offsets.len(), 1_001);
assert_eq!(decoded.end(), bytes.len());
}
let mut surface = vec![
Token::Ident("nubs".into()),
Token::Long(1),
Token::Long(1),
Token::Enum(0),
Token::Enum(0),
Token::Enum(0),
Token::Enum(0),
Token::Long(2),
Token::Long(1_001),
];
for count in [2_u32, 1_001] {
for index in 0..count {
surface.extend([Token::Double(f64::from(index)), Token::Long(1)]);
}
}
for v in 0_u32..1_001 {
for u in 0_u32..2 {
surface.extend([
Token::Double(f64::from(u)),
Token::Double(f64::from(v)),
Token::Double(0.0),
]);
}
}
let (decoded, end) = super::surface_block(&surface, 0).unwrap();
assert_eq!(decoded.poles().len(), 2_002);
assert_eq!(decoded.v_knots().len(), 1_003);
assert_eq!(end, surface.len());
for width in [
crate::kernel_header::RefWidth::Four,
crate::kernel_header::RefWidth::Eight,
] {
let bytes = knot_block_bytes(&surface, width);
let decoded = super::decode_surface_block(&bytes, 0, width).unwrap();
assert_eq!(decoded.surface.poles().len(), 2_002);
assert_eq!(decoded.v_knots.value_offsets.len(), 1_001);
assert_eq!(decoded.end(), bytes.len());
}
}
#[test]
fn cached_surface_past_the_pole_limit_is_not_recovered() {
use crate::sab::Token;
let mut grid = vec![
Token::Ident("nubs".into()),
Token::Long(1),
Token::Long(1),
Token::Enum(0),
Token::Enum(0),
Token::Enum(0),
Token::Enum(0),
Token::Long(449),
Token::Long(449),
];
for _ in 0..2 {
for index in 0_u32..449 {
grid.extend([Token::Double(f64::from(index)), Token::Long(1)]);
}
}
grid.extend(std::iter::repeat_n(Token::Double(0.0), 449 * 449 * 3));
let arena = DecodeArena::new();
let (ctx, _) =
DecodeContext::from_root_bytes(&[], &arena, &DecodePolicy::service()).expect("root");
assert!(
super::surface_block(&grid, 0).is_none(),
"a grid past the pole ceiling is not recovered"
);
assert!(ctx.resource_refusal().is_none());
}
fn knot_block_bytes(
tokens: &[crate::sab::Token],
width: crate::kernel_header::RefWidth,
) -> Vec<u8> {
use crate::sab::Token;
let mut bytes = Vec::new();
for token in tokens {
match token {
Token::Ident(name) => {
bytes.extend([0x0d, u8::try_from(name.len()).unwrap()]);
bytes.extend(name.as_bytes());
}
Token::Double(value) => {
bytes.push(0x06);
bytes.extend(value.to_le_bytes());
}
Token::Long(value) | Token::Enum(value) => {
bytes.push(if matches!(token, Token::Long(_)) {
0x04
} else {
0x15
});
match width {
crate::kernel_header::RefWidth::Four => {
bytes.extend(i32::try_from(*value).unwrap().to_le_bytes());
}
crate::kernel_header::RefWidth::Eight => bytes.extend(value.to_le_bytes()),
}
}
_ => panic!("not a knot-block token"),
}
}
bytes
}
#[test]
fn knot_counts_without_complete_source_fields_do_not_allocate() {
use crate::sab::Token;
let curve = [
Token::Ident("nubs".into()),
Token::Long(1),
Token::Enum(0),
Token::Long(i64::MAX),
Token::Double(0.0),
Token::Long(1),
];
assert!(super::curve_block(&curve, 0).is_none());
let surface = [
Token::Ident("nubs".into()),
Token::Long(1),
Token::Long(1),
Token::Enum(0),
Token::Enum(0),
Token::Enum(0),
Token::Enum(0),
Token::Long(i64::MAX),
Token::Long(2),
Token::Double(0.0),
Token::Long(1),
];
assert!(super::surface_block(&surface, 0).is_none());
for width in [
crate::kernel_header::RefWidth::Four,
crate::kernel_header::RefWidth::Eight,
] {
let mut truncated = curve.clone();
truncated[3] = Token::Long(i64::from(i32::MAX));
assert!(
super::decode_curve_block(&knot_block_bytes(&truncated, width), 0, width).is_none()
);
}
}
fn variable_blend_tokens(name: &str, current: i64) -> Vec<crate::sab::Token> {
use crate::sab::Token;
let mut tokens = vec![
Token::SubtypeOpen,
Token::Ident(name.into()),
Token::Long(1),
];
for _ in 0..2 {
tokens.extend([
Token::Str("blend_support_zero_curve".into()),
Token::Ident("null_surface".into()),
Token::Ident("null_curve".into()),
Token::Ident("nullbs".into()),
Token::Position([0.0; 3]),
Token::Ident("nullbs".into()),
]);
}
tokens.extend([
Token::Ident("straight".into()),
Token::Position([0.0; 3]),
Token::Vector3([1.0, 0.0, 0.0]),
Token::Double(0.0),
Token::Double(1.0),
Token::Double(0.0),
Token::Double(0.0),
Token::Enum(0),
Token::Str("two_ends".into()),
Token::Enum(0),
Token::True,
Token::Double(0.0),
Token::Double(1.0),
Token::Double(1.0),
Token::Double(1.0),
Token::Enum(7),
Token::Double(1.0),
Token::Double(1.0),
Token::Double(0.0),
Token::Double(1.0),
Token::Double(0.0),
Token::False,
Token::Long(current),
Token::Double(0.001),
Token::Double(0.001),
Token::Long(1),
Token::Enum(0),
Token::Ident("nubs".into()),
Token::Long(1),
Token::Long(1),
Token::Enum(0),
Token::Enum(0),
Token::Enum(0),
Token::Enum(0),
Token::Long(2),
Token::Long(2),
]);
for _ in 0..2 {
tokens.extend([
Token::Double(0.0),
Token::Long(1),
Token::Double(1.0),
Token::Long(1),
]);
}
for point in [[7., 0., 0.], [8., 0., 0.], [7., 1., 0.], [8., 1., 0.]] {
tokens.extend(point.map(Token::Double));
}
tokens.extend([
Token::Double(0.001),
Token::Long(0),
Token::Long(0),
Token::Long(0),
Token::Long(0),
Token::Long(0),
Token::Long(0),
Token::False,
Token::Long(0),
Token::Long(0),
Token::Long(0),
Token::Ident("null_curve".into()),
Token::True,
Token::True,
Token::False,
Token::False,
Token::Ident("nullbs".into()),
Token::Ident("nullbs".into()),
Token::SubtypeClose,
]);
tokens
}
#[test]
fn embedded_variable_blend_cache_admission_matches_top_level() {
use crate::sab::{Record, Token};
use cadmpeg_ir::geometry::{SolvedSurfaceGeometry, SurfaceGeometry};
for name in crate::nurbs::blend::VARIABLE_BLEND_NAMES {
for current in [0, 1, 2] {
let scope = variable_blend_tokens(name, current);
let record = |index, name: &str, tokens: Vec<Token>| Record {
index,
name: name.into(),
tokens: tokens.into(),
offset: 0,
len: 0,
};
let mut surface =
vec![Token::Ref(-1), Token::Long(0), Token::Ref(-1), Token::False];
surface.extend(scope.clone());
surface.extend([Token::False, Token::False, Token::False, Token::False]);
let records = [
record(
0,
"face",
vec![
Token::Ref(-1),
Token::Long(0),
Token::Ref(-1),
Token::Ref(-1),
Token::Ref(-1),
Token::Ref(2),
Token::Ref(-1),
Token::Ref(1),
Token::False,
Token::False,
],
),
record(1, "spline", surface),
record(
2,
"shell",
vec![
Token::Ref(-1),
Token::Long(0),
Token::Ref(-1),
Token::Ref(-1),
Token::Ref(-1),
Token::Ref(0),
Token::Ref(-1),
Token::Ref(3),
],
),
record(
3,
"lump",
vec![
Token::Ref(-1),
Token::Long(0),
Token::Ref(-1),
Token::Ref(-1),
Token::Ref(2),
Token::Ref(4),
],
),
record(
4,
"body",
vec![
Token::Ref(-1),
Token::Long(0),
Token::Ref(-1),
Token::Ref(3),
Token::Ref(-1),
Token::Ref(-1),
],
),
];
let arena = DecodeArena::new();
let (ctx, _) =
DecodeContext::from_root_bytes(&[], &arena, &DecodePolicy::service()).unwrap();
let out = crate::brep::decode_with_header(
&ctx,
&records,
&[],
None,
"source",
crate::asm_format!("sat"),
crate::brep::DecodePurpose::Model,
)
.unwrap();
let carrier = out
.surfaces
.iter()
.find(|s| s.id.as_str() == "sat:brep:entity#1")
.unwrap();
assert_eq!(out.faces.len(), 1);
if current == 0 {
assert!(matches!(
carrier.geometry,
SurfaceGeometry::Procedural { cache: None, .. }
));
} else {
assert!(matches!(
carrier.geometry,
SurfaceGeometry::Solved(SolvedSurfaceGeometry::Nurbs(_))
));
}
let table = SubtypeTable::from_records(&records);
for reference in [
vec![
Token::SubtypeOpen,
Token::Ident("ref".into()),
Token::Long(0),
Token::SubtypeClose,
],
vec![Token::SubtypeOpen, Token::Long(0), Token::SubtypeClose],
scope.clone(),
] {
let mut support = vec![Token::Ident("spline".into()), Token::False];
support.extend(reference);
support.extend([Token::False, Token::False, Token::False, Token::False]);
let mut cursor = crate::nurbs::toks::Cur::at(&support, 0);
let decoded = crate::nurbs::proc_curve::optional_embedded_surface_with_bounds(
&ctx,
&mut cursor,
&table,
)
.unwrap()
.unwrap();
assert_eq!(
decoded.surface.into_surface().is_some(),
current != 0,
"{name}, current={current}"
);
assert_eq!(cursor.pos(), support.len());
}
}
}
}
#[test]
fn variable_blend_current_cache_is_not_an_inline_support_surface() {
use crate::sab::{Record, Token};
for name in crate::nurbs::blend::VARIABLE_BLEND_NAMES {
for current in [0, 1, 2] {
let mut owner = variable_blend_tokens(name, current);
let marker = owner
.iter()
.position(|token| matches!(token, Token::Ident(name) if name == "nubs"))
.unwrap();
let mut support = owner[marker..marker + 29].to_vec();
for token in &mut support[17..] {
if let Token::Double(value) = token {
*value += 100.0;
}
}
let mut surface = vec![Token::Ident("spline".into())];
surface.extend(support);
surface.extend([Token::False, Token::False, Token::False, Token::False]);
owner[3] = Token::Str("blend_support_surface".into());
owner.splice(4..5, surface);
let records = [Record {
index: 0,
name: "spline".into(),
tokens: owner.clone().into(),
offset: 0,
len: 0,
}];
let arena = DecodeArena::new();
let (ctx, _) =
DecodeContext::from_root_bytes(&[], &arena, &DecodePolicy::service()).unwrap();
let table = SubtypeTable::from_records(&records);
assert!(
crate::nurbs::proc_surface::procedural_surface_resolving_refs(
&ctx, &owner, &table
)
.is_some()
);
for payload in [
owner,
vec![
Token::SubtypeOpen,
Token::Ident("ref".into()),
Token::Long(0),
Token::SubtypeClose,
],
vec![Token::SubtypeOpen, Token::Long(0), Token::SubtypeClose],
] {
let cache = super::surface_cache_resolving_refs(&ctx, &payload, &table);
if current == 0 {
assert!(cache.is_none());
} else {
let surface = cache.unwrap().unwrap();
assert_eq!(
surface.poles()[0].get(),
cadmpeg_ir::math::Point3::new(70., 0., 0.)
);
}
}
}
}
}
#[test]
fn bounded_second_interval_keeps_the_variable_blend_cache() {
use crate::sab::{Record, Token};
for name in crate::nurbs::blend::VARIABLE_BLEND_NAMES {
let mut owner = variable_blend_tokens(name, 1);
let upper = owner
.iter()
.position(|token| matches!(token, Token::False))
.unwrap();
owner.splice(upper..=upper, [Token::True, Token::Double(1.0)]);
let records = [Record {
index: 0,
name: "spline".into(),
tokens: owner.clone().into(),
offset: 0,
len: 0,
}];
let arena = DecodeArena::new();
let (ctx, _) =
DecodeContext::from_root_bytes(&[], &arena, &DecodePolicy::service()).unwrap();
let table = SubtypeTable::from_records(&records);
assert!(
crate::nurbs::proc_surface::procedural_surface_resolving_refs(&ctx, &owner, &table)
.is_none(),
"{name}"
);
let surface = super::surface_cache_resolving_refs(&ctx, &owner, &table)
.unwrap()
.unwrap();
assert_eq!(
surface.poles()[0].get(),
cadmpeg_ir::math::Point3::new(70., 0., 0.),
"{name}"
);
}
}
#[test]
fn uncached_spline_support_keeps_the_variable_blend_cache() {
use crate::sab::{Record, Token};
let mut support = vec![
Token::Ident("spline".into()),
Token::True,
Token::SubtypeOpen,
Token::Ident("cyl_spl_sur".into()),
Token::Long(1),
Token::Ident("intcurve".into()),
Token::True,
Token::SubtypeOpen,
Token::Ident("ref".into()),
Token::Long(0),
Token::SubtypeClose,
Token::True,
Token::Double(-1.0),
Token::True,
Token::Double(1.0),
Token::Vector3([0.0, 1.0, 0.0]),
Token::Position([0.0; 3]),
Token::Enum(2),
];
for bound in [-1.0, 1.0, 0.0, 0.1] {
support.extend([Token::True, Token::Double(bound)]);
}
support.extend([
Token::Enum(0),
Token::Enum(0),
Token::Enum(0),
Token::Enum(0),
]);
support.extend([Token::Long(0), Token::Long(1), Token::Double(0.5)]);
support.extend([
Token::Long(0),
Token::Long(0),
Token::Long(0),
Token::Long(0),
]);
support.extend([Token::False, Token::SubtypeClose]);
support.extend([Token::False, Token::False, Token::False, Token::False]);
for name in crate::nurbs::blend::VARIABLE_BLEND_NAMES {
for side in [3, 9] {
let mut owner = variable_blend_tokens(name, 1);
owner[side] = Token::Str("blend_support_surface".into());
owner.splice(side + 1..side + 2, support.clone());
let records = [Record {
index: 0,
name: "spline".into(),
tokens: owner.clone().into(),
offset: 0,
len: 0,
}];
let arena = DecodeArena::new();
let (ctx, _) =
DecodeContext::from_root_bytes(&[], &arena, &DecodePolicy::service()).unwrap();
let table = SubtypeTable::from_records(&records);
assert!(
crate::nurbs::proc_surface::procedural_surface_resolving_refs(
&ctx, &owner, &table
)
.is_none(),
"{name}, side token {side}"
);
let surface = super::surface_cache_resolving_refs(&ctx, &owner, &table)
.unwrap()
.unwrap();
assert_eq!(
surface.poles()[0].get(),
cadmpeg_ir::math::Point3::new(70., 0., 0.),
"{name}, side token {side}"
);
}
}
}
#[test]
fn post_curve_follows_the_null_post_pcurve() {
use crate::sab::{Record, Token};
for name in crate::nurbs::blend::VARIABLE_BLEND_NAMES {
let mut owner = variable_blend_tokens(name, 1);
let post = owner.len() - 2;
owner.splice(
post..=post,
[
Token::Ident("nubs".into()),
Token::Long(1),
Token::Enum(0),
Token::Long(2),
Token::Double(0.0),
Token::Long(1),
Token::Double(1.0),
Token::Long(1),
Token::Double(1.0),
Token::Double(2.0),
Token::Double(3.0),
Token::Double(4.0),
Token::Double(5.0),
Token::Double(6.0),
],
);
let records = [Record {
index: 0,
name: "spline".into(),
tokens: owner.clone().into(),
offset: 0,
len: 0,
}];
let arena = DecodeArena::new();
let (ctx, _) =
DecodeContext::from_root_bytes(&[], &arena, &DecodePolicy::service()).unwrap();
let table = SubtypeTable::from_records(&records);
let surface =
crate::nurbs::proc_surface::procedural_surface_resolving_refs(&ctx, &owner, &table)
.unwrap()
.unwrap();
let crate::nurbs::proc_surface::DecodedProceduralSurfaceDefinition::VariableBlend(
construction,
) = surface.definition()
else {
panic!("{name}: expected a variable blend");
};
assert!(construction.post_pcurve.is_none(), "{name}");
let post_curve = construction.post_curve.as_ref().expect("post curve");
assert_eq!(
post_curve.control_points()[1].get(),
cadmpeg_ir::math::Point3::new(40., 50., 60.),
"{name}"
);
assert!(super::surface_cache_resolving_refs(&ctx, &owner, &table).is_some());
}
}
#[test]
fn untyped_variable_blend_cache_is_rejected_for_owner_and_alias() {
use crate::sab::{Record, Token};
for name in crate::nurbs::blend::VARIABLE_BLEND_NAMES {
for current in [0, 1, 2] {
let mut tokens = variable_blend_tokens(name, current);
let tail = tokens.len() - 2;
tokens[tail] = Token::Ident("unsupported_pcurve".into());
let records = [Record {
index: 0,
name: "spline".into(),
tokens: tokens.clone().into(),
offset: 0,
len: 0,
}];
let arena = DecodeArena::new();
let (ctx, _) =
DecodeContext::from_root_bytes(&[], &arena, &DecodePolicy::service()).unwrap();
let table = SubtypeTable::from_records(&records);
assert!(
crate::nurbs::proc_surface::procedural_surface_resolving_refs(
&ctx, &tokens, &table,
)
.is_none()
);
for payload in [
tokens.clone(),
vec![
Token::SubtypeOpen,
Token::Ident("ref".into()),
Token::Long(0),
Token::SubtypeClose,
],
vec![Token::SubtypeOpen, Token::Long(0), Token::SubtypeClose],
] {
assert!(super::surface_cache_resolving_refs(&ctx, &payload, &table).is_none());
}
}
}
}
#[test]
fn rejected_blend_alias_does_not_substitute_its_support_cache() {
use crate::sab::{Record, Token};
for current in [0, 1] {
for malformed in [false, true] {
let mut owner = variable_blend_tokens("var_blend_spl_sur", current);
if malformed {
let tail = owner.len() - 2;
owner[tail] = Token::Ident("unsupported_pcurve".into());
}
owner.splice(
4..5,
[
Token::Ident("spline".into()),
Token::False,
Token::SubtypeOpen,
Token::Ident("ref".into()),
Token::Long(1),
Token::SubtypeClose,
Token::False,
Token::False,
Token::False,
Token::False,
],
);
let record = |index, tokens: Vec<Token>| Record {
index,
name: "spline".into(),
tokens: tokens.into(),
offset: 0,
len: 0,
};
let records = [
record(0, owner.clone()),
record(1, variable_blend_tokens("var_blend_spl_sur", 1)),
];
let arena = DecodeArena::new();
let (ctx, _) =
DecodeContext::from_root_bytes(&[], &arena, &DecodePolicy::service()).unwrap();
let table = SubtypeTable::from_records(&records);
assert_eq!(
crate::nurbs::proc_surface::procedural_surface_resolving_refs(
&ctx, &owner, &table
)
.is_some(),
!malformed,
);
for payload in [
owner.clone(),
vec![
Token::SubtypeOpen,
Token::Ident("ref".into()),
Token::Long(0),
Token::SubtypeClose,
],
vec![Token::SubtypeOpen, Token::Long(0), Token::SubtypeClose],
] {
assert_eq!(
super::surface_cache_resolving_refs(&ctx, &payload, &table).is_some(),
current != 0 && !malformed,
);
}
}
}
}
#[test]
fn stale_variable_blend_cache_uses_only_temporary_storage() {
let tokens = variable_blend_tokens("var_blend_spl_sur", 0);
let scope = crate::nurbs::toks::subtype_span(&tokens, 0).unwrap();
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_retained_bytes = 0;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
assert!(super::owned_surface_cache(&ctx, scope, None).is_none());
}
#[test]
fn variable_blend_cache_reference_cycles_do_not_recurse() {
use crate::sab::{Record, Token};
fn records(name: &str, side: usize) -> [Record; 1] {
let mut tokens = variable_blend_tokens(name, 1);
tokens.splice(
side + 1..side + 2,
[
Token::Ident("spline".into()),
Token::False,
Token::SubtypeOpen,
Token::Ident("ref".into()),
Token::Long(0),
Token::SubtypeClose,
Token::False,
Token::False,
Token::False,
Token::False,
],
);
[Record {
index: 0,
name: "spline".into(),
tokens: tokens.into(),
offset: 0,
len: 0,
}]
}
let mut policy = DecodePolicy::service();
policy.limits.max_recursion_depth = 3;
for name in crate::nurbs::blend::VARIABLE_BLEND_NAMES {
for side in [3, 9] {
let records = records(name, side);
let table = SubtypeTable::from_records(&records);
let scope = table.span(0).unwrap();
let arena = DecodeArena::new();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let surface = super::owned_surface_cache_resolving_refs(&ctx, scope, &table)
.unwrap()
.unwrap();
assert_eq!(
surface.poles()[0].get(),
cadmpeg_ir::math::Point3::new(70., 0., 0.),
"{name}, side token {side}"
);
if let Some(Err(error)) =
crate::nurbs::proc_surface::procedural_surface_resolving_refs(
&ctx,
&records[0].tokens,
&table,
)
{
panic!("{name}, side token {side}: {error:?}");
}
}
}
let records = records("var_blend_spl_sur", 3);
let table = SubtypeTable::from_records(&records);
let scope = table.span(0).unwrap();
policy.limits.max_recursion_depth = 0;
let arena = DecodeArena::new();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let error = super::owned_surface_cache_resolving_refs(&ctx, scope, &table)
.unwrap()
.unwrap_err();
let CodecError::ResourceLimit(limit) = error else {
panic!("depth refusal: {error:?}");
};
assert_eq!(limit.dimension, ResourceDimension::RecursionDepth);
assert_eq!(limit.operation, "admit ASM variable-blend cache");
}
}