use super::types::*;
pub struct SatParser;
impl SatParser {
pub fn parse(text: &str) -> Result<SatDocument, SatParseError> {
let text = text.trim();
if text.is_empty() {
return Err(SatParseError::EmptyInput);
}
let mut lines = SatLines::new(text);
let header_line = lines.next_line().ok_or(SatParseError::EmptyInput)?;
let header = Self::parse_header_line(header_line)?;
let product_line = lines
.next_line()
.ok_or(SatParseError::InvalidProductInfo("missing".to_string()))?;
let (product_id, product_version, date) =
Self::parse_product_line(product_line, &header.version)?;
let tol_line = lines
.next_line()
.ok_or(SatParseError::InvalidTolerances("missing".to_string()))?;
let (spatial_res, normal_tol, resfit_tol) = Self::parse_tolerance_line(tol_line)?;
let sat_header = SatHeader {
version: header.version,
num_records: header.num_records,
num_bodies: header.num_bodies,
has_history: header.has_history,
product_id,
product_version,
date,
spatial_resolution: spatial_res,
normal_tolerance: normal_tol,
resfit_tolerance: resfit_tol,
};
let records = Self::parse_records(&mut lines, &sat_header.version)?;
let mut doc = SatDocument {
header: sat_header,
records,
};
doc.header.num_records = doc.records.len();
Ok(doc)
}
fn parse_header_line(line: &str) -> Result<HeaderInfo, SatParseError> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
return Err(SatParseError::InvalidHeader(
"empty header line".to_string(),
));
}
let version_num: u32 = parts[0]
.parse()
.map_err(|_| SatParseError::InvalidHeader(format!("invalid version: {}", parts[0])))?;
let num_records = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
let num_bodies = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
let has_history = parts.get(3).and_then(|s| s.parse::<u32>().ok()).unwrap_or(0) != 0;
Ok(HeaderInfo {
version: SatVersion::from_sat_number(version_num),
num_records,
num_bodies,
has_history,
})
}
fn parse_product_line(
line: &str,
_version: &SatVersion,
) -> Result<(String, String, String), SatParseError> {
let trimmed = line.trim_start();
if trimmed.starts_with('@') {
Self::parse_product_line_v7(line)
} else {
Self::parse_product_line_legacy(line)
}
}
fn parse_product_line_v7(line: &str) -> Result<(String, String, String), SatParseError> {
let mut pos = 0;
let bytes = line.as_bytes();
let product_id = read_counted_string(bytes, &mut pos).unwrap_or_default();
let product_version = read_counted_string(bytes, &mut pos).unwrap_or_default();
let date = read_counted_string(bytes, &mut pos).unwrap_or_else(|| {
line[pos..].trim().to_string()
});
Ok((product_id, product_version, date))
}
fn parse_product_line_legacy(line: &str) -> Result<(String, String, String), SatParseError> {
let mut pos = 0;
let bytes = line.as_bytes();
let product_id = read_legacy_string(bytes, &mut pos).unwrap_or_default();
let product_version = read_legacy_string(bytes, &mut pos).unwrap_or_default();
let date = read_legacy_string(bytes, &mut pos)
.unwrap_or_else(|| line[pos..].trim().to_string());
Ok((product_id, product_version, date))
}
fn parse_tolerance_line(line: &str) -> Result<(f64, f64, Option<f64>), SatParseError> {
let parts: Vec<&str> = line.split_whitespace().collect();
let spatial = parts
.first()
.and_then(|s| s.parse().ok())
.unwrap_or(1e-06);
let normal = parts
.get(1)
.and_then(|s| s.parse().ok())
.unwrap_or(9.9999999999999995e-07);
let resfit = parts
.get(2)
.and_then(|s| s.parse().ok());
Ok((spatial, normal, resfit))
}
fn parse_records(
lines: &mut SatLines<'_>,
version: &SatVersion,
) -> Result<Vec<SatRecord>, SatParseError> {
let mut records = Vec::new();
let mut auto_index: i32 = 0;
let remaining = lines.remaining();
if remaining.is_empty() {
return Ok(records);
}
let remaining = remaining.replace('\n', "").replace('\r', "");
let record_texts: Vec<&str> = remaining.split('#').collect();
for record_text in record_texts {
let trimmed = record_text.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with("End-of-ACIS-data")
|| trimmed.starts_with("End-of-ASM-data")
{
break;
}
if trimmed.contains("End-of-ACIS-data") || trimmed.contains("End-of-ASM-data") {
let before_end = if let Some(pos) = trimmed.find("End-of-") {
trimmed[..pos].trim()
} else {
""
};
if before_end.is_empty() {
break;
}
if let Ok(record) = Self::parse_single_record(before_end, version, &mut auto_index)
{
records.push(record);
}
break;
}
match Self::parse_single_record(trimmed, version, &mut auto_index) {
Ok(record) => records.push(record),
Err(_) => {
continue;
}
}
}
Ok(records)
}
fn parse_single_record(
text: &str,
version: &SatVersion,
auto_index: &mut i32,
) -> Result<SatRecord, SatParseError> {
let mut tokenizer = SatTokenizer::new(text, version.has_counted_strings());
let index = if version.has_explicit_indices() {
match tokenizer.peek_token() {
Some(tok) if looks_like_negative_index(tok) => {
let idx_str = tokenizer.next_raw_token().unwrap();
let idx: i32 = idx_str.parse().unwrap_or(*auto_index);
let idx = idx.abs();
*auto_index = idx + 1;
idx
}
_ => {
let idx = *auto_index;
*auto_index += 1;
idx
}
}
} else {
let idx = *auto_index;
*auto_index += 1;
idx
};
let entity_type = tokenizer.next_raw_token().ok_or(SatParseError::InvalidRecord {
line: 0,
message: format!("missing entity type in: {}", text),
})?;
let entity_type = entity_type.to_string();
let attribute = match tokenizer.next_token() {
Some(SatToken::Pointer(p)) => p,
Some(SatToken::Integer(v)) => {
if v == -1 {
SatPointer::NULL
} else {
SatPointer::new(v as i32)
}
}
_ => SatPointer::NULL,
};
let subtype_id = if version.major >= 7 {
match tokenizer.peek_token() {
Some(tok) if !tok.starts_with('$') && !tok.starts_with('@') && !tok.starts_with('#') => {
if tok.parse::<i64>().is_ok() && !tok.contains('.') && !tok.contains('e') && !tok.contains('E') {
match tokenizer.next_token() {
Some(SatToken::Integer(v)) => v as i32,
_ => -1,
}
} else {
-1
}
}
_ => -1,
}
} else {
-1
};
let mut tokens = Vec::new();
while let Some(token) = tokenizer.next_token() {
if matches!(token, SatToken::Terminator) {
break;
}
tokens.push(token);
}
if version.major < 7 {
normalize_v400_tokens(&entity_type, &mut tokens);
}
Ok(SatRecord {
index,
entity_type,
sub_type: None,
attribute,
subtype_id,
tokens,
raw_text: Some(text.to_string()),
})
}
}
struct HeaderInfo {
version: SatVersion,
num_records: usize,
num_bodies: usize,
has_history: bool,
}
struct SatLines<'a> {
text: &'a str,
pos: usize,
}
impl<'a> SatLines<'a> {
fn new(text: &'a str) -> Self {
Self { text, pos: 0 }
}
fn next_line(&mut self) -> Option<&'a str> {
if self.pos >= self.text.len() {
return None;
}
let remaining = &self.text[self.pos..];
let end = remaining.find('\n').unwrap_or(remaining.len());
let line = &remaining[..end];
self.pos += end + 1; Some(line.trim_end_matches('\r'))
}
fn remaining(&self) -> &'a str {
if self.pos >= self.text.len() {
""
} else {
&self.text[self.pos..]
}
}
}
struct SatTokenizer<'a> {
text: &'a str,
pos: usize,
use_counted_strings: bool,
}
impl<'a> SatTokenizer<'a> {
fn new(text: &'a str, use_counted_strings: bool) -> Self {
Self {
text,
pos: 0,
use_counted_strings,
}
}
fn skip_whitespace(&mut self) {
while self.pos < self.text.len() {
let ch = self.text.as_bytes()[self.pos];
if ch == b' ' || ch == b'\t' || ch == b'\r' || ch == b'\n' {
self.pos += 1;
} else {
break;
}
}
}
fn peek_token(&self) -> Option<&'a str> {
let mut pos = self.pos;
while pos < self.text.len() {
let ch = self.text.as_bytes()[pos];
if ch == b' ' || ch == b'\t' || ch == b'\r' || ch == b'\n' {
pos += 1;
} else {
break;
}
}
if pos >= self.text.len() {
return None;
}
let start = pos;
while pos < self.text.len() {
let ch = self.text.as_bytes()[pos];
if ch == b' ' || ch == b'\t' || ch == b'\r' || ch == b'\n' {
break;
}
pos += 1;
}
Some(&self.text[start..pos])
}
fn next_raw_token(&mut self) -> Option<&'a str> {
self.skip_whitespace();
if self.pos >= self.text.len() {
return None;
}
let start = self.pos;
while self.pos < self.text.len() {
let ch = self.text.as_bytes()[self.pos];
if ch == b' ' || ch == b'\t' || ch == b'\r' || ch == b'\n' {
break;
}
self.pos += 1;
}
Some(&self.text[start..self.pos])
}
fn next_token(&mut self) -> Option<SatToken> {
self.skip_whitespace();
if self.pos >= self.text.len() {
return None;
}
let ch = self.text.as_bytes()[self.pos];
if ch == b'@' && self.use_counted_strings {
self.pos += 1; let raw = self.next_raw_token()?;
let len: usize = raw.parse().unwrap_or(0);
self.skip_whitespace();
if self.pos + len <= self.text.len() {
let s = &self.text[self.pos..self.pos + len];
self.pos += len;
return Some(SatToken::String(s.to_string()));
} else {
let s = &self.text[self.pos..];
self.pos = self.text.len();
return Some(SatToken::String(s.trim().to_string()));
}
}
if ch == b'$' {
self.pos += 1; let raw = self.next_raw_token()?;
let idx: i32 = raw.parse().unwrap_or(-1);
return Some(SatToken::Pointer(SatPointer::new(idx)));
}
if ch == b'#' {
self.pos += 1;
return Some(SatToken::Terminator);
}
let raw = self.next_raw_token()?;
if let Ok(v) = raw.parse::<i64>() {
return Some(SatToken::Integer(v));
}
if let Ok(v) = raw.parse::<f64>() {
return Some(SatToken::Float(v));
}
if raw.eq_ignore_ascii_case("true") || raw == "T" {
return Some(SatToken::True);
}
if raw.eq_ignore_ascii_case("false") || raw == "F" {
return Some(SatToken::False);
}
match raw.to_lowercase().as_str() {
"forward" | "reversed" | "single" | "double" | "in" | "out" | "unknown" => {
return Some(SatToken::Enum(raw.to_lowercase()));
}
_ => {}
}
Some(SatToken::Ident(raw.to_string()))
}
}
fn looks_like_negative_index(s: &str) -> bool {
if !s.starts_with('-') {
return false;
}
s[1..].chars().all(|c| c.is_ascii_digit())
}
pub(crate) fn normalize_v400_tokens(entity_type: &str, tokens: &mut Vec<SatToken>) {
let sentinel = SatToken::Pointer(SatPointer::NULL);
match entity_type {
"body" | "face" | "loop" | "vertex" | "coedge" | "edge"
| "point" | "transform"
| "plane-surface" | "cone-surface" | "sphere-surface" | "torus-surface"
| "spline-surface" | "meshsurf-surface" | "bs3-surface"
| "straight-curve" | "ellipse-curve" | "intcurve-curve" | "bs2-curve"
| "bs3-curve" | "exactcur-curve" => {
tokens.insert(0, sentinel);
}
"lump" => {
let pos = 1.min(tokens.len());
tokens.insert(pos, sentinel);
}
"shell" => {
let pos = 2.min(tokens.len());
tokens.insert(pos, sentinel);
}
_ => {}
}
}
fn read_counted_string(bytes: &[u8], pos: &mut usize) -> Option<String> {
while *pos < bytes.len() && (bytes[*pos] == b' ' || bytes[*pos] == b'\t') {
*pos += 1;
}
if *pos >= bytes.len() || bytes[*pos] != b'@' {
return None;
}
*pos += 1;
let start = *pos;
while *pos < bytes.len() && bytes[*pos].is_ascii_digit() {
*pos += 1;
}
let len_str = std::str::from_utf8(&bytes[start..*pos]).ok()?;
let len: usize = len_str.parse().ok()?;
if *pos < bytes.len() && bytes[*pos] == b' ' {
*pos += 1;
}
if *pos + len <= bytes.len() {
let s = std::str::from_utf8(&bytes[*pos..*pos + len]).ok()?;
*pos += len;
Some(s.to_string())
} else {
let s = std::str::from_utf8(&bytes[*pos..]).ok()?;
*pos = bytes.len();
Some(s.trim().to_string())
}
}
fn read_legacy_string(bytes: &[u8], pos: &mut usize) -> Option<String> {
while *pos < bytes.len() && (bytes[*pos] == b' ' || bytes[*pos] == b'\t') {
*pos += 1;
}
if *pos >= bytes.len() {
return None;
}
let start = *pos;
while *pos < bytes.len() && bytes[*pos].is_ascii_digit() {
*pos += 1;
}
if start == *pos {
return None;
}
let len_str = std::str::from_utf8(&bytes[start..*pos]).ok()?;
let len: usize = len_str.parse().ok()?;
if *pos < bytes.len() && bytes[*pos] == b' ' {
*pos += 1;
}
if *pos + len <= bytes.len() {
let s = std::str::from_utf8(&bytes[*pos..*pos + len]).ok()?;
*pos += len;
Some(s.to_string())
} else {
let s = std::str::from_utf8(&bytes[*pos..]).ok()?;
*pos = bytes.len();
Some(s.trim().to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_header_line() {
let info = SatParser::parse_header_line("700 0 1 0").unwrap();
assert_eq!(info.version, SatVersion::new(7, 0, 0));
assert_eq!(info.num_records, 0);
assert_eq!(info.num_bodies, 1);
assert!(!info.has_history);
}
#[test]
fn test_parse_header_line_v400() {
let info = SatParser::parse_header_line("400 0 1 0").unwrap();
assert_eq!(info.version, SatVersion::new(4, 0, 0));
}
#[test]
fn test_parse_tolerance_line() {
let (spatial, normal, resfit) =
SatParser::parse_tolerance_line("1e-06 9.9999999999999995e-07").unwrap();
assert!((spatial - 1e-06).abs() < 1e-20);
assert!((normal - 9.9999999999999995e-07).abs() < 1e-20);
assert!(resfit.is_none());
let (spatial, normal, resfit) =
SatParser::parse_tolerance_line("1e-06 9.9999999999999995e-07 1e-10").unwrap();
assert!((spatial - 1e-06).abs() < 1e-20);
assert!((normal - 9.9999999999999995e-07).abs() < 1e-20);
assert!((resfit.unwrap() - 1e-10).abs() < 1e-24);
}
#[test]
fn test_parse_simple_v700_document() {
let sat = "700 0 1 0\n\
@8 acadrust @8 ACIS 7.0 @24 Thu Jan 01 00:00:00 2023\n\
1e-06 9.9999999999999995e-07\n\
-0 asmheader $-1 -1 @12 700 7 0 0 @5 ACIS @3 7.0 @24 Thu Jan 01 00:00:00 2023 #\n\
-1 body $-1 $-1 $-1 $-1 #\n\
End-of-ACIS-data\n";
let doc = SatDocument::parse(sat).unwrap();
assert_eq!(doc.header.version, SatVersion::V7_0);
assert_eq!(doc.records.len(), 2);
assert_eq!(doc.records[0].entity_type, "asmheader");
assert_eq!(doc.records[0].index, 0);
assert_eq!(doc.records[1].entity_type, "body");
assert_eq!(doc.records[1].index, 1);
}
#[test]
fn test_parse_simple_v400_document() {
let sat = "400 0 1 0\n\
19 Spatial ACIS Modeler 7 ACIS 4.0 24 Thu Jan 01 00:00:00 2023\n\
1e-06 9.9999999999999995e-07\n\
body $-1 $1 $-1 $-1 #\n\
lump $-1 $-1 $2 $0 #\n\
shell $-1 $-1 $-1 $3 $-1 $1 #\n\
End-of-ACIS-data\n";
let doc = SatDocument::parse(sat).unwrap();
assert_eq!(doc.header.version, SatVersion::new(4, 0, 0));
assert_eq!(doc.records.len(), 3);
assert_eq!(doc.records[0].entity_type, "body");
assert_eq!(doc.records[0].index, 0);
assert_eq!(doc.records[1].entity_type, "lump");
assert_eq!(doc.records[1].index, 1);
assert_eq!(doc.records[2].entity_type, "shell");
assert_eq!(doc.records[2].index, 2);
}
#[test]
fn test_parse_pointers() {
let sat = "700 0 1 0\n\
@8 acadrust @8 ACIS 7.0 @24 Thu Jan 01 00:00:00 2023\n\
1e-06 9.9999999999999995e-07\n\
-0 body $-1 $1 $-1 $-1 #\n\
-1 lump $-1 $-1 $2 $0 #\n\
End-of-ACIS-data\n";
let doc = SatDocument::parse(sat).unwrap();
let body = &doc.records[0];
assert_eq!(body.attribute, SatPointer::NULL);
assert_eq!(body.token_pointer(0), Some(SatPointer::new(1)));
assert_eq!(body.token_pointer(1), Some(SatPointer::NULL));
}
#[test]
fn test_body_accessor() {
let sat = "700 0 1 0\n\
@8 acadrust @8 ACIS 7.0 @24 Thu Jan 01 00:00:00 2023\n\
1e-06 9.9999999999999995e-07\n\
body $-1 -1 $-1 $1 $-1 $2 #\n\
lump $-1 -1 $-1 $-1 $-1 $0 #\n\
transform $-1 -1 $-1 1 0 0 0 1 0 0 0 1 0 0 0 1 #\n\
End-of-ACIS-data\n";
let doc = SatDocument::parse(sat).unwrap();
let bodies = doc.bodies();
assert_eq!(bodies.len(), 1);
let body = &bodies[0];
assert_eq!(body.lump(), SatPointer::new(1));
assert_eq!(body.wire_body(), SatPointer::NULL);
assert_eq!(body.transform(), SatPointer::new(2));
}
#[test]
fn test_plane_surface_accessor() {
let sat = "700 0 0 0\n\
@8 acadrust @8 ACIS 7.0 @24 Thu Jan 01 00:00:00 2023\n\
1e-06 9.9999999999999995e-07\n\
-0 plane-surface $-1 -1 $-1 1.0 2.0 3.0 0 0 1 1 0 0 forward_v I I I I #\n\
End-of-ACIS-data\n";
let doc = SatDocument::parse(sat).unwrap();
let records = doc.records_of_type("plane-surface");
assert_eq!(records.len(), 1);
let plane = SatPlaneSurface::from_record(records[0]).unwrap();
assert_eq!(plane.root_point(), (1.0, 2.0, 3.0));
assert_eq!(plane.normal(), (0.0, 0.0, 1.0));
assert_eq!(plane.u_direction(), (1.0, 0.0, 0.0));
}
#[test]
fn test_validate_document() {
let sat = "700 0 1 0\n\
@8 acadrust @8 ACIS 7.0 @24 Thu Jan 01 00:00:00 2023\n\
1e-06 9.9999999999999995e-07\n\
-0 body $-1 $99 $-1 $-1 #\n\
End-of-ACIS-data\n";
let doc = SatDocument::parse(sat).unwrap();
let errors = doc.validate();
assert!(!errors.is_empty());
}
#[test]
fn test_version_properties() {
let v7 = SatVersion::V7_0;
assert!(v7.has_explicit_indices());
assert!(v7.has_counted_strings());
assert!(v7.has_asm_header());
assert_eq!(v7.sat_version_number(), 700);
let v4 = SatVersion::V4_0;
assert!(!v4.has_explicit_indices());
assert!(!v4.has_counted_strings());
assert!(!v4.has_asm_header());
assert_eq!(v4.sat_version_number(), 400);
}
#[test]
fn test_counted_string_reading() {
let input = b"@5 hello @3 bye";
let mut pos = 0;
let s1 = read_counted_string(input, &mut pos).unwrap();
assert_eq!(s1, "hello");
let s2 = read_counted_string(input, &mut pos).unwrap();
assert_eq!(s2, "bye");
}
#[test]
fn test_legacy_string_reading() {
let input = b"5 hello 3 bye";
let mut pos = 0;
let s1 = read_legacy_string(input, &mut pos).unwrap();
assert_eq!(s1, "hello");
let s2 = read_legacy_string(input, &mut pos).unwrap();
assert_eq!(s2, "bye");
}
#[test]
fn test_parse_box_sat() {
let sat = "700 0 1 0\n\
@8 acadrust @8 ACIS 7.0 @24 Thu Jan 01 00:00:00 2023\n\
1e-06 9.9999999999999995e-07\n\
asmheader $-1 -1 @12 700 7 0 0 @5 ACIS @3 7.0 @24 Thu Jan 01 00:00:00 2023 #\n\
body $-1 -1 $-1 $2 $-1 $-1 #\n\
lump $-1 -1 $-1 $-1 $3 $1 #\n\
shell $-1 -1 $-1 $-1 $-1 $4 $-1 $2 #\n\
face $-1 -1 $-1 $5 $10 $3 $-1 $20 forward single #\n\
face $-1 -1 $-1 $6 $11 $3 $-1 $21 forward single #\n\
face $-1 -1 $-1 $7 $12 $3 $-1 $22 forward single #\n\
face $-1 -1 $-1 $8 $13 $3 $-1 $23 forward single #\n\
face $-1 -1 $-1 $9 $14 $3 $-1 $24 forward single #\n\
face $-1 -1 $-1 $-1 $15 $3 $-1 $25 forward single #\n\
loop $-1 -1 $-1 $-1 $30 $4 #\n\
loop $-1 -1 $-1 $-1 $31 $5 #\n\
loop $-1 -1 $-1 $-1 $32 $6 #\n\
loop $-1 -1 $-1 $-1 $33 $7 #\n\
loop $-1 -1 $-1 $-1 $34 $8 #\n\
loop $-1 -1 $-1 $-1 $35 $9 #\n\
plane-surface $-1 -1 $-1 0 0 5 0 0 1 1 0 0 forward_v I I I I #\n\
plane-surface $-1 -1 $-1 0 0 -5 0 0 -1 1 0 0 forward_v I I I I #\n\
plane-surface $-1 -1 $-1 5 0 0 1 0 0 0 1 0 forward_v I I I I #\n\
plane-surface $-1 -1 $-1 -5 0 0 -1 0 0 0 1 0 forward_v I I I I #\n\
plane-surface $-1 -1 $-1 0 5 0 0 1 0 0 0 1 forward_v I I I I #\n\
plane-surface $-1 -1 $-1 0 -5 0 0 -1 0 0 0 1 forward_v I I I I #\n\
End-of-ACIS-data\n";
let doc = SatDocument::parse(sat).unwrap();
assert_eq!(doc.header.version, SatVersion::V7_0);
assert_eq!(doc.records.len(), 22);
let bodies = doc.bodies();
assert_eq!(bodies.len(), 1);
assert_eq!(bodies[0].lump(), SatPointer::new(2));
let faces = doc.faces();
assert_eq!(faces.len(), 6);
let planes = doc.records_of_type("plane-surface");
assert_eq!(planes.len(), 6);
let top = SatPlaneSurface::from_record(planes[0]).unwrap();
assert_eq!(top.root_point(), (0.0, 0.0, 5.0));
assert_eq!(top.normal(), (0.0, 0.0, 1.0));
}
#[test]
fn test_entity_classification() {
assert_eq!(classify_entity_type("body"), SatEntityCategory::Topology);
assert_eq!(classify_entity_type("face"), SatEntityCategory::Topology);
assert_eq!(
classify_entity_type("plane-surface"),
SatEntityCategory::Geometry
);
assert_eq!(
classify_entity_type("straight-curve"),
SatEntityCategory::Geometry
);
assert_eq!(
classify_entity_type("transform"),
SatEntityCategory::Transform
);
assert_eq!(
classify_entity_type("asmheader"),
SatEntityCategory::Header
);
}
#[test]
fn test_v700_legacy_product_line() {
let sat = "700 0 2 0\n\
19 TransMagic R7 sp0.0 14 ACIS 16.0.7 NT 24 Tue Mar 20 12:06:10 2007\n\
25.399999999999999 9.9999999999999995e-007 1e-010\n\
body $2 -1 $-1 $3 $-1 $-1 #\n\
lump $4 -1 $-1 $-1 $5 $0 #\n\
End-of-ACIS-data\n";
let doc = SatDocument::parse(sat).unwrap();
assert_eq!(doc.header.version, SatVersion::V7_0);
assert_eq!(doc.header.product_id, "TransMagic R7 sp0.0");
assert_eq!(doc.header.product_version, "ACIS 16.0.7 NT");
assert_eq!(doc.header.date, "Tue Mar 20 12:06:10 2007");
assert_eq!(doc.records.len(), 2);
assert_eq!(doc.records[0].entity_type, "body");
assert_eq!(doc.records[1].entity_type, "lump");
}
#[test]
fn test_parse_sample_sat_1() {
let path = "examples/sat v7 samples/sample_sat_1.sat";
let Ok(sat) = std::fs::read_to_string(path) else { return; };
let doc = SatDocument::parse(&sat).unwrap();
assert_eq!(doc.header.version, SatVersion::V7_0);
assert_eq!(doc.header.product_id, "TransMagic R7 sp0.0");
assert_eq!(doc.header.product_version, "ACIS 16.0.7 NT");
assert_eq!(doc.header.date, "Tue Mar 20 12:06:10 2007");
let bodies = doc.bodies();
assert_eq!(bodies.len(), 2);
let faces = doc.faces();
assert!(!faces.is_empty(), "expected face records");
let shells = doc.records_of_type("shell");
assert!(!shells.is_empty(), "expected shell records");
let lumps = doc.records_of_type("lump");
assert_eq!(lumps.len(), 2);
assert!(
doc.records.len() > 100,
"expected >100 records, got {}",
doc.records.len()
);
println!(
"sample_sat_1: {} records, {} bodies, {} faces, {} shells",
doc.records.len(),
bodies.len(),
faces.len(),
shells.len()
);
}
#[test]
fn test_parse_sample_sat_2() {
let path = "examples/sat v7 samples/sample_sat_2.sat";
let Ok(sat) = std::fs::read_to_string(path) else { return; };
let doc = SatDocument::parse(&sat).unwrap();
assert_eq!(doc.header.version, SatVersion::V7_0);
assert_eq!(doc.header.product_id, "TransMagic R7 sp0.0");
assert_eq!(doc.header.product_version, "ACIS 16.0.7 NT");
assert_eq!(doc.header.date, "Tue Mar 20 12:06:38 2007");
let bodies = doc.bodies();
assert_eq!(bodies.len(), 9);
let lumps = doc.records_of_type("lump");
assert!(lumps.len() >= 9, "expected at least 9 lumps");
let shells = doc.records_of_type("shell");
assert!(!shells.is_empty(), "expected shell records");
let faces = doc.faces();
assert!(!faces.is_empty(), "expected face records");
let cones = doc.records_of_type("cone-surface");
assert!(!cones.is_empty(), "expected cone-surface records");
let planes = doc.records_of_type("plane-surface");
assert!(!planes.is_empty(), "expected plane-surface records");
let splines = doc.records_of_type("spline-surface");
assert!(!splines.is_empty(), "expected spline-surface records");
assert!(
doc.records.len() > 500,
"expected >500 records, got {}",
doc.records.len()
);
println!(
"sample_sat_2: {} records, {} bodies, {} faces, {} cones, {} planes, {} splines",
doc.records.len(),
bodies.len(),
faces.len(),
cones.len(),
planes.len(),
splines.len()
);
}
}