use std::fmt;
use super::parser::SatParser;
use super::writer::SatWriter;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SatVersion {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl SatVersion {
pub fn new(major: u32, minor: u32, patch: u32) -> Self {
Self { major, minor, patch }
}
pub const V4_0: Self = Self { major: 4, minor: 0, patch: 0 };
pub const V7_0: Self = Self { major: 7, minor: 0, patch: 0 };
pub const V21_0: Self = Self { major: 21, minor: 0, patch: 0 };
pub fn sat_version_number(&self) -> u32 {
self.major * 100 + self.minor * 10 + self.patch
}
pub fn from_sat_number(num: u32) -> Self {
Self {
major: num / 100,
minor: (num % 100) / 10,
patch: num % 10,
}
}
pub fn has_explicit_indices(&self) -> bool {
self.major >= 7
}
pub fn has_counted_strings(&self) -> bool {
self.major >= 7
}
pub fn has_asm_header(&self) -> bool {
self.major >= 7
}
}
impl Default for SatVersion {
fn default() -> Self {
Self::V7_0
}
}
impl fmt::Display for SatVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SatHeader {
pub version: SatVersion,
pub num_records: usize,
pub num_bodies: usize,
pub has_history: bool,
pub product_id: String,
pub product_version: String,
pub date: String,
pub spatial_resolution: f64,
pub normal_tolerance: f64,
pub resfit_tolerance: Option<f64>,
}
impl SatHeader {
pub fn new() -> Self {
Self {
version: SatVersion::V7_0,
num_records: 0,
num_bodies: 0,
has_history: false,
product_id: "acadrust".to_string(),
product_version: "ACIS 7.0".to_string(),
date: "Thu Jan 01 00:00:00 2023".to_string(),
spatial_resolution: 10.0,
normal_tolerance: 9.9999999999999995e-07,
resfit_tolerance: Some(1e-10),
}
}
pub fn with_version(version: SatVersion) -> Self {
let mut header = Self::new();
header.version = version;
header.product_version = format!("ACIS {}.{}", version.major, version.minor);
header
}
}
impl Default for SatHeader {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SatPointer(pub i32);
impl SatPointer {
pub const NULL: Self = Self(-1);
pub fn new(index: i32) -> Self {
Self(index)
}
pub fn is_null(&self) -> bool {
self.0 < 0
}
pub fn index(&self) -> Option<usize> {
if self.0 >= 0 {
Some(self.0 as usize)
} else {
None
}
}
}
impl Default for SatPointer {
fn default() -> Self {
Self::NULL
}
}
impl fmt::Display for SatPointer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "${}", self.0)
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SatToken {
Ident(String),
Pointer(SatPointer),
Integer(i64),
Float(f64),
String(String),
Position(f64, f64, f64),
True,
False,
Terminator,
Enum(String),
Sab {
tag: u8,
data: Vec<u8>,
},
}
impl SatToken {
pub fn as_ident(&self) -> Option<&str> {
match self {
SatToken::Ident(s) | SatToken::Enum(s) => Some(s),
SatToken::Sab { tag: 0x0F, .. } => Some("{"),
SatToken::Sab { tag: 0x10, .. } => Some("}"),
SatToken::Sab { tag, data } => sab_string_bytes(*tag, data)
.and_then(|bytes| std::str::from_utf8(bytes).ok()),
_ => None,
}
}
pub fn as_pointer(&self) -> Option<SatPointer> {
match self {
SatToken::Pointer(p) => Some(*p),
_ => None,
}
}
pub fn as_integer(&self) -> Option<i64> {
match self {
SatToken::Integer(v) => Some(*v),
SatToken::Sab { tag: 0x02, data } if data.len() == 1 => {
Some(i8::from_le_bytes([data[0]]) as i64)
}
SatToken::Sab { tag: 0x03, data } if data.len() == 2 => {
Some(i16::from_le_bytes([data[0], data[1]]) as i64)
}
SatToken::Sab { tag: 0x04 | 0x15, data } if data.len() == 4 => {
Some(i32::from_le_bytes([data[0], data[1], data[2], data[3]]) as i64)
}
SatToken::Sab { tag: 0x17, data } if data.len() == 8 => {
Some(i64::from_le_bytes([
data[0], data[1], data[2], data[3],
data[4], data[5], data[6], data[7],
]))
}
_ => None,
}
}
pub fn as_float(&self) -> Option<f64> {
match self {
SatToken::Float(v) => Some(*v),
SatToken::Integer(v) => Some(*v as f64),
SatToken::Sab { tag: 0x05, data } if data.len() == 4 => {
Some(f32::from_le_bytes([data[0], data[1], data[2], data[3]]) as f64)
}
SatToken::Sab { tag: 0x06, data } if data.len() == 8 => {
Some(f64::from_le_bytes([
data[0], data[1], data[2], data[3],
data[4], data[5], data[6], data[7],
]))
}
_ => None,
}
}
pub fn as_string(&self) -> Option<&str> {
match self {
SatToken::String(s) => Some(s),
SatToken::Sab { tag, data } => sab_string_bytes(*tag, data)
.and_then(|bytes| std::str::from_utf8(bytes).ok()),
_ => None,
}
}
pub fn coordinate_components(&self) -> Option<([f64; 3], usize)> {
match self {
SatToken::Position(x, y, z) => Some(([*x, *y, *z], 3)),
SatToken::Sab { tag: 0x13 | 0x14, data } if data.len() == 24 => {
Some(([
f64::from_le_bytes(data[0..8].try_into().ok()?),
f64::from_le_bytes(data[8..16].try_into().ok()?),
f64::from_le_bytes(data[16..24].try_into().ok()?),
], 3))
}
SatToken::Sab { tag: 0x16, data } if data.len() == 16 => {
Some(([
f64::from_le_bytes(data[0..8].try_into().ok()?),
f64::from_le_bytes(data[8..16].try_into().ok()?),
0.0,
], 2))
}
_ => None,
}
}
}
fn sab_string_bytes(tag: u8, data: &[u8]) -> Option<&[u8]> {
let prefix = match tag {
0x07 | 0x0D | 0x0E => 1,
0x08 => 2,
0x09 | 0x12 => 4,
_ => return None,
};
(data.len() >= prefix).then_some(&data[prefix..])
}
impl fmt::Display for SatToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SatToken::Ident(s) => write!(f, "{}", s),
SatToken::Pointer(p) => write!(f, "{}", p),
SatToken::Integer(v) => write!(f, "{}", v),
SatToken::Float(v) => {
if v.fract() == 0.0 && !v.is_infinite() && !v.is_nan() {
write!(f, "{:.1}", v)
} else {
write!(f, "{}", v)
}
}
SatToken::String(s) => write!(f, "@{} {}", s.len(), s),
SatToken::Position(x, y, z) => write!(f, "{} {} {}", x, y, z),
SatToken::True => write!(f, "TRUE"),
SatToken::False => write!(f, "FALSE"),
SatToken::Terminator => write!(f, "#"),
SatToken::Enum(s) => write!(f, "{}", s),
SatToken::Sab { tag, data } => {
if let Some((values, len)) = self.coordinate_components() {
for (index, value) in values[..len].iter().enumerate() {
if index != 0 {
write!(f, " ")?;
}
write!(f, "{}", value)?;
}
Ok(())
} else if let Some(value) = self.as_integer() {
write!(f, "{}", value)
} else if let Some(value) = self.as_float() {
write!(f, "{}", value)
} else if let Some(value) = sab_string_bytes(*tag, data)
.and_then(|bytes| std::str::from_utf8(bytes).ok())
{
if matches!(*tag, 0x0D | 0x0E) {
write!(f, "{}", value)
} else {
write!(f, "@{} {}", value.len(), value)
}
} else {
match *tag {
0x0F => write!(f, "{{"),
0x10 => write!(f, "}}"),
_ => write!(f, "<sab:{:02X}:{}>", tag, data.len()),
}
}
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SatRecord {
pub index: i32,
pub entity_type: String,
pub sub_type: Option<String>,
pub attribute: SatPointer,
pub subtype_id: i32,
pub tokens: Vec<SatToken>,
pub raw_text: Option<String>,
}
impl SatRecord {
pub fn new(index: i32, entity_type: &str) -> Self {
Self {
index,
entity_type: entity_type.to_string(),
sub_type: None,
attribute: SatPointer::NULL,
subtype_id: -1,
tokens: Vec::new(),
raw_text: None,
}
}
pub fn is_a(&self, entity_type: &str) -> bool {
self.entity_type == entity_type || base_entity_type(&self.entity_type) == entity_type
}
pub fn pointers(&self) -> Vec<SatPointer> {
let mut ptrs = vec![self.attribute];
for token in &self.tokens {
if let SatToken::Pointer(p) = token {
ptrs.push(*p);
}
}
ptrs
}
pub fn token(&self, index: usize) -> Option<&SatToken> {
self.tokens.get(index)
}
pub fn token_integer(&self, index: usize) -> Option<i64> {
self.tokens.get(index).and_then(|t| t.as_integer())
}
pub fn token_float(&self, index: usize) -> Option<f64> {
let mut slot = 0usize;
for t in &self.tokens {
if let Some((components, len)) = t.coordinate_components() {
for &c in &components[..len] {
if slot == index {
return Some(c);
}
slot += 1;
}
} else {
if slot == index {
return t.as_float();
}
slot += 1;
}
}
None
}
pub fn token_pointer(&self, index: usize) -> Option<SatPointer> {
self.tokens.get(index).and_then(|t| t.as_pointer())
}
pub fn nth_pointer(&self, index: usize) -> Option<SatPointer> {
self.tokens
.iter()
.filter_map(|t| t.as_pointer())
.nth(index)
}
pub fn token_string(&self, index: usize) -> Option<&str> {
self.tokens
.get(index)
.and_then(|token| token.as_ident().or_else(|| token.as_string()))
}
pub fn token_sense(&self, index: usize) -> Sense {
match self.tokens.get(index) {
Some(token) if token.as_integer().is_some() => {
if token.as_integer().unwrap_or(0) != 0 {
Sense::Reversed
} else {
Sense::Forward
}
}
_ => self.token_string(index).map(Sense::from_str).unwrap_or_default(),
}
}
}
impl fmt::Display for SatRecord {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {} {}", self.index, self.entity_type, self.attribute)?;
for token in &self.tokens {
write!(f, " {}", token)?;
}
write!(f, " #")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Sense {
Forward,
Reversed,
}
impl Sense {
pub fn from_str(s: &str) -> Self {
match s {
"reversed" | "REVERSED" | "reversed_v" | "REVERSED_V" | "reverse_v"
| "REVERSE_V" | "1" => Self::Reversed,
_ => Self::Forward,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Forward => "forward",
Self::Reversed => "reversed",
}
}
}
impl Default for Sense {
fn default() -> Self {
Self::Forward
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Sidedness {
Single,
Double,
}
impl Sidedness {
pub fn from_str(s: &str) -> Self {
match s {
"double" | "DOUBLE" => Self::Double,
_ => Self::Single,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Single => "single",
Self::Double => "double",
}
}
}
impl Default for Sidedness {
fn default() -> Self {
Self::Single
}
}
#[derive(Debug, Clone)]
pub struct SatBody<'a> {
record: &'a SatRecord,
}
impl<'a> SatBody<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.is_a("body") {
Some(Self { record })
} else {
None
}
}
pub fn next_body(&self) -> SatPointer {
self.record.token_pointer(0).unwrap_or(SatPointer::NULL)
}
pub fn lump(&self) -> SatPointer {
self.record.token_pointer(1).unwrap_or(SatPointer::NULL)
}
pub fn wire_body(&self) -> SatPointer {
self.record.token_pointer(2).unwrap_or(SatPointer::NULL)
}
pub fn transform(&self) -> SatPointer {
self.record.token_pointer(3).unwrap_or(SatPointer::NULL)
}
}
#[derive(Debug, Clone)]
pub struct SatLump<'a> {
record: &'a SatRecord,
}
impl<'a> SatLump<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.is_a("lump") {
Some(Self { record })
} else {
None
}
}
pub fn next_lump(&self) -> SatPointer {
self.record.token_pointer(1).unwrap_or(SatPointer::NULL)
}
pub fn shell(&self) -> SatPointer {
self.record.token_pointer(2).unwrap_or(SatPointer::NULL)
}
pub fn body(&self) -> SatPointer {
self.record.token_pointer(3).unwrap_or(SatPointer::NULL)
}
}
#[derive(Debug, Clone)]
pub struct SatShell<'a> {
record: &'a SatRecord,
}
impl<'a> SatShell<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.is_a("shell") {
Some(Self { record })
} else {
None
}
}
pub fn next_shell(&self) -> SatPointer {
self.record.token_pointer(1).unwrap_or(SatPointer::NULL)
}
pub fn subshell(&self) -> SatPointer {
self.record.token_pointer(2).unwrap_or(SatPointer::NULL)
}
pub fn face(&self) -> SatPointer {
self.record.token_pointer(3).unwrap_or(SatPointer::NULL)
}
pub fn wire(&self) -> SatPointer {
self.record.token_pointer(4).unwrap_or(SatPointer::NULL)
}
pub fn lump(&self) -> SatPointer {
self.record.token_pointer(5).unwrap_or(SatPointer::NULL)
}
}
#[derive(Debug, Clone)]
pub struct SatSubshell<'a> {
record: &'a SatRecord,
}
impl<'a> SatSubshell<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.is_a("subshell") {
Some(Self { record })
} else {
None
}
}
pub fn pattern(&self) -> SatPointer {
self.record.token_pointer(0).unwrap_or(SatPointer::NULL)
}
pub fn link(&self, ordinal: usize) -> SatPointer {
self.record
.nth_pointer(ordinal + 1)
.unwrap_or(SatPointer::NULL)
}
pub fn record(&self) -> &'a SatRecord {
self.record
}
}
#[derive(Debug, Clone)]
pub struct SatWire<'a> {
record: &'a SatRecord,
}
impl<'a> SatWire<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.is_a("wire") {
Some(Self { record })
} else {
None
}
}
pub fn pattern(&self) -> SatPointer {
self.record.token_pointer(0).unwrap_or(SatPointer::NULL)
}
pub fn link(&self, ordinal: usize) -> SatPointer {
self.record
.nth_pointer(ordinal + 1)
.unwrap_or(SatPointer::NULL)
}
pub fn record(&self) -> &'a SatRecord {
self.record
}
}
#[derive(Debug, Clone)]
pub struct SatAttribute<'a> {
record: &'a SatRecord,
}
impl<'a> SatAttribute<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if classify_entity_type(&record.entity_type) == SatEntityCategory::Attribute {
Some(Self { record })
} else {
None
}
}
pub fn next(&self) -> SatPointer {
self.record.token_pointer(0).unwrap_or(SatPointer::NULL)
}
pub fn previous(&self) -> SatPointer {
self.record.token_pointer(1).unwrap_or(SatPointer::NULL)
}
pub fn owner(&self) -> SatPointer {
self.record.token_pointer(2).unwrap_or(SatPointer::NULL)
}
pub fn payload(&self) -> &'a [SatToken] {
self.record.tokens.get(3..).unwrap_or(&[])
}
pub fn record(&self) -> &'a SatRecord {
self.record
}
}
#[derive(Debug, Clone)]
pub struct SatFace<'a> {
record: &'a SatRecord,
}
impl<'a> SatFace<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.is_a("face") {
Some(Self { record })
} else {
None
}
}
pub fn attribute(&self) -> SatPointer {
self.record.attribute
}
pub fn record(&self) -> &'a SatRecord {
self.record
}
pub fn next_face(&self) -> SatPointer {
self.record.token_pointer(1).unwrap_or(SatPointer::NULL)
}
pub fn first_loop(&self) -> SatPointer {
self.record.token_pointer(2).unwrap_or(SatPointer::NULL)
}
pub fn shell(&self) -> SatPointer {
self.record.token_pointer(3).unwrap_or(SatPointer::NULL)
}
pub fn subshell(&self) -> SatPointer {
self.record.token_pointer(4).unwrap_or(SatPointer::NULL)
}
pub fn surface(&self) -> SatPointer {
self.record.token_pointer(5).unwrap_or(SatPointer::NULL)
}
pub fn sense(&self) -> Sense {
self.record
.token_string(6)
.map(Sense::from_str)
.unwrap_or_default()
}
pub fn sidedness(&self) -> Sidedness {
self.record
.token_string(7)
.map(Sidedness::from_str)
.unwrap_or_default()
}
}
#[derive(Debug, Clone)]
pub struct SatLoop<'a> {
record: &'a SatRecord,
}
impl<'a> SatLoop<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.is_a("loop") {
Some(Self { record })
} else {
None
}
}
pub fn next_loop(&self) -> SatPointer {
self.record.token_pointer(1).unwrap_or(SatPointer::NULL)
}
pub fn first_coedge(&self) -> SatPointer {
self.record.token_pointer(2).unwrap_or(SatPointer::NULL)
}
pub fn face(&self) -> SatPointer {
self.record.token_pointer(3).unwrap_or(SatPointer::NULL)
}
}
#[derive(Debug, Clone)]
pub struct SatCoedge<'a> {
record: &'a SatRecord,
}
impl<'a> SatCoedge<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.is_a("coedge") {
Some(Self { record })
} else {
None
}
}
pub fn next(&self) -> SatPointer {
self.record.token_pointer(1).unwrap_or(SatPointer::NULL)
}
pub fn prev(&self) -> SatPointer {
self.record.token_pointer(2).unwrap_or(SatPointer::NULL)
}
pub fn partner(&self) -> SatPointer {
self.record.token_pointer(3).unwrap_or(SatPointer::NULL)
}
pub fn edge(&self) -> SatPointer {
self.record.token_pointer(4).unwrap_or(SatPointer::NULL)
}
pub fn sense(&self) -> Sense {
self.record.token_sense(5)
}
pub fn owner_loop(&self) -> SatPointer {
self.record.token_pointer(6).unwrap_or(SatPointer::NULL)
}
pub fn pcurve(&self) -> SatPointer {
self.record.nth_pointer(6).unwrap_or(SatPointer::NULL)
}
}
#[derive(Debug, Clone)]
pub struct SatEdge<'a> {
record: &'a SatRecord,
}
impl<'a> SatEdge<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.is_a("edge") {
Some(Self { record })
} else {
None
}
}
pub fn start_vertex(&self) -> SatPointer {
self.record.token_pointer(1).unwrap_or(SatPointer::NULL)
}
pub fn start_param(&self) -> f64 {
self.record.token_float(2).unwrap_or(0.0)
}
pub fn end_vertex(&self) -> SatPointer {
self.record.token_pointer(3).unwrap_or(SatPointer::NULL)
}
pub fn end_param(&self) -> f64 {
self.record.token_float(4).unwrap_or(0.0)
}
pub fn coedge(&self) -> SatPointer {
self.record.token_pointer(5).unwrap_or(SatPointer::NULL)
}
pub fn curve(&self) -> SatPointer {
self.record.token_pointer(6).unwrap_or(SatPointer::NULL)
}
pub fn sense(&self) -> Sense {
self.record.token_sense(7)
}
}
#[derive(Debug, Clone)]
pub struct SatVertex<'a> {
record: &'a SatRecord,
}
impl<'a> SatVertex<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.is_a("vertex") {
Some(Self { record })
} else {
None
}
}
pub fn edge(&self) -> SatPointer {
self.record.nth_pointer(1).unwrap_or(SatPointer::NULL)
}
pub fn point(&self) -> SatPointer {
self.record.nth_pointer(2).unwrap_or(SatPointer::NULL)
}
}
#[derive(Debug, Clone)]
pub struct SatPoint<'a> {
record: &'a SatRecord,
}
impl<'a> SatPoint<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.entity_type == "point" {
Some(Self { record })
} else {
None
}
}
pub fn position(&self) -> (f64, f64, f64) {
let x = self.record.token_float(1).unwrap_or(0.0);
let y = self.record.token_float(2).unwrap_or(0.0);
let z = self.record.token_float(3).unwrap_or(0.0);
(x, y, z)
}
}
#[derive(Debug, Clone)]
pub struct SatStraightCurve<'a> {
record: &'a SatRecord,
}
impl<'a> SatStraightCurve<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.entity_type == "straight-curve" {
Some(Self { record })
} else {
None
}
}
pub fn root_point(&self) -> (f64, f64, f64) {
let x = self.record.token_float(1).unwrap_or(0.0);
let y = self.record.token_float(2).unwrap_or(0.0);
let z = self.record.token_float(3).unwrap_or(0.0);
(x, y, z)
}
pub fn direction(&self) -> (f64, f64, f64) {
let x = self.record.token_float(4).unwrap_or(0.0);
let y = self.record.token_float(5).unwrap_or(0.0);
let z = self.record.token_float(6).unwrap_or(1.0);
(x, y, z)
}
}
#[derive(Debug, Clone)]
pub struct SatEllipseCurve<'a> {
record: &'a SatRecord,
}
impl<'a> SatEllipseCurve<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.entity_type == "ellipse-curve" {
Some(Self { record })
} else {
None
}
}
pub fn center(&self) -> (f64, f64, f64) {
let x = self.record.token_float(1).unwrap_or(0.0);
let y = self.record.token_float(2).unwrap_or(0.0);
let z = self.record.token_float(3).unwrap_or(0.0);
(x, y, z)
}
pub fn normal(&self) -> (f64, f64, f64) {
let x = self.record.token_float(4).unwrap_or(0.0);
let y = self.record.token_float(5).unwrap_or(0.0);
let z = self.record.token_float(6).unwrap_or(1.0);
(x, y, z)
}
pub fn major_axis(&self) -> (f64, f64, f64) {
let x = self.record.token_float(7).unwrap_or(1.0);
let y = self.record.token_float(8).unwrap_or(0.0);
let z = self.record.token_float(9).unwrap_or(0.0);
(x, y, z)
}
pub fn ratio(&self) -> f64 {
self.record.token_float(10).unwrap_or(1.0)
}
}
#[derive(Debug, Clone)]
pub struct SatIntCurve<'a> {
record: &'a SatRecord,
}
pub type SatBs3Curve<'a> = SatIntCurve<'a>;
impl<'a> SatIntCurve<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
let entity_type = record.entity_type.as_str();
if matches!(entity_type, "intcurve-curve" | "bs3-curve" | "exactcur-curve")
|| entity_type.ends_with("-intcurve-curve")
|| entity_type.ends_with("-bs3-curve")
|| entity_type.ends_with("-exactcur-curve")
{
Some(Self { record })
} else {
None
}
}
pub fn bspline(&self) -> Option<(usize, Vec<f64>, Vec<[f64; 4]>)> {
decode_bspline_curve(&self.record.tokens)
}
pub fn bspline_in(&self, document: &SatDocument) -> Option<(usize, Vec<f64>, Vec<[f64; 4]>)> {
self.bspline().or_else(|| {
let reference = subtype_reference(&self.record.tokens)?;
decode_bspline_curve(document.subtype_tokens(reference)?)
})
}
pub fn is_closed_in(&self, document: &SatDocument) -> bool {
let tokens = if let Some(reference) = subtype_reference(&self.record.tokens) {
match document.subtype_tokens(reference) {
Some(tokens) => tokens,
None => return false,
}
} else {
&self.record.tokens
};
let Some(start) = tokens
.iter()
.position(|token| matches!(token.as_ident(), Some("nubs" | "nurbs")))
else {
return false;
};
tokens.get(start + 2).is_some_and(|token| {
matches!(token.as_ident(), Some("closed" | "periodic"))
|| token.as_integer().is_some_and(|value| value != 0)
})
}
pub fn sample(&self, segs: usize) -> Vec<(f64, f64, f64)> {
let Some((degree, knots, _)) = self.bspline() else {
return Vec::new();
};
self.sample_range(knots[degree], knots[knots.len() - degree - 1], segs)
}
pub fn sample_range(&self, t0: f64, t1: f64, segs: usize) -> Vec<(f64, f64, f64)> {
let Some((degree, knots, ctrl)) = self.bspline() else {
return Vec::new();
};
let segs = segs.max(1);
let (lo, hi) = (knots[degree], knots[knots.len() - degree - 1]);
let (a, b) = (t0.min(t1).max(lo), t0.max(t1).min(hi));
if !(b > a) {
return Vec::new();
}
(0..=segs)
.map(|s| {
let t = a + (b - a) * (s as f64 / segs as f64);
let p = de_boor_homogeneous(degree, &knots, &ctrl, t);
let w = if p[3].abs() > 1e-12 { p[3] } else { 1.0 };
(p[0] / w, p[1] / w, p[2] / w)
})
.collect()
}
}
fn decode_bspline_curve(t: &[SatToken]) -> Option<(usize, Vec<f64>, Vec<[f64; 4]>)> {
let ni = t
.iter()
.position(|x| matches!(x.as_ident(), Some("nubs" | "nurbs")))?;
let rational = t.get(ni)?.as_ident() == Some("nurbs");
let degree = t.get(ni + 1)?.as_integer()? as usize;
let n_knots = t.get(ni + 3)?.as_integer()? as usize;
let mut knots: Vec<f64> = Vec::new();
let mut i = ni + 4;
for _ in 0..n_knots {
let k = t.get(i)?.as_float()?;
let mult = t.get(i + 1)?.as_integer()?.max(0) as usize;
for _ in 0..mult {
knots.push(k);
}
i += 2;
}
if knots.len() < 2 {
return None;
}
let stride = if rational { 4 } else { 3 };
let available = t[i..]
.iter()
.take_while(|token| token.as_float().is_some())
.count()
/ stride;
let base_ctrl = knots.len().checked_sub(degree + 1)?;
let n_ctrl = if available >= base_ctrl + 2 {
let first = *knots.first()?;
let last = *knots.last()?;
knots.insert(0, first);
knots.push(last);
base_ctrl + 2
} else {
base_ctrl
};
if n_ctrl <= degree {
return None;
}
let mut ctrl: Vec<[f64; 4]> = Vec::with_capacity(n_ctrl);
for _ in 0..n_ctrl {
let x = t.get(i)?.as_float()?;
let y = t.get(i + 1)?.as_float()?;
let z = t.get(i + 2)?.as_float()?;
if rational {
let w = t.get(i + 3)?.as_float()?;
ctrl.push([x * w, y * w, z * w, w]);
i += 4;
} else {
ctrl.push([x, y, z, 1.0]);
i += 3;
}
}
Some((degree, knots, ctrl))
}
#[derive(Debug, Clone)]
pub struct SatPCurve<'a> {
record: &'a SatRecord,
}
impl<'a> SatPCurve<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.entity_type == "pcurve" {
Some(Self { record })
} else {
None
}
}
fn bspline_at(
tokens: &[SatToken],
start: usize,
) -> Option<(usize, Vec<f64>, Vec<[f64; 4]>)> {
let rational = tokens.get(start)?.as_ident() == Some("nurbs");
let degree = tokens.get(start + 1)?.as_integer()?.max(0) as usize;
let knot_count = tokens.get(start + 3)?.as_integer()?.max(0) as usize;
let mut position = start + 4;
let mut knots = Vec::new();
for _ in 0..knot_count {
let value = tokens.get(position)?.as_float()?;
let multiplicity = tokens.get(position + 1)?.as_integer()?.max(0) as usize;
for _ in 0..multiplicity {
knots.push(value);
}
position += 2;
}
if knots.len() < 2 {
return None;
}
let stride = if rational { 3 } else { 2 };
let available = tokens[position..]
.iter()
.take_while(|token| token.as_float().is_some())
.count()
/ stride;
let base_count = knots.len().checked_sub(degree + 1)?;
let control_count = if available >= base_count + 2 {
let first = *knots.first()?;
let last = *knots.last()?;
knots.insert(0, first);
knots.push(last);
base_count + 2
} else {
base_count
};
if control_count <= degree {
return None;
}
let mut control = Vec::with_capacity(control_count);
for _ in 0..control_count {
let u = tokens.get(position)?.as_float()?;
let v = tokens.get(position + 1)?.as_float()?;
if rational {
let weight = tokens.get(position + 2)?.as_float()?;
control.push([u * weight, v * weight, 0.0, weight]);
position += 3;
} else {
control.push([u, v, 0.0, 1.0]);
position += 2;
}
}
Some((degree, knots, control))
}
fn sample_bspline(
spline: (usize, Vec<f64>, Vec<[f64; 4]>),
segments: usize,
) -> Vec<(f64, f64)> {
let (degree, knots, control) = spline;
let start = knots[degree];
let end = knots[knots.len() - degree - 1];
if end <= start {
return Vec::new();
}
let segments = segments.max(1);
(0..=segments)
.map(|index| {
let parameter = start + (end - start) * (index as f64 / segments as f64);
let point = de_boor_homogeneous(degree, &knots, &control, parameter);
let weight = if point[3].abs() > 1e-12 {
point[3]
} else {
1.0
};
(point[0] / weight, point[1] / weight)
})
.collect()
}
fn apply_offsets(&self, points: &mut [(f64, f64)]) {
let Some((u_offset, v_offset)) = self.offsets() else {
return;
};
for point in points {
point.0 += u_offset;
point.1 += v_offset;
}
}
fn offsets(&self) -> Option<(f64, f64)> {
let offsets: Vec<f64> = self
.record
.tokens
.iter()
.rev()
.filter_map(SatToken::as_float)
.take(2)
.collect();
(offsets.len() == 2).then_some((offsets[1], offsets[0]))
}
pub fn bspline_in(&self, document: &SatDocument) -> Option<(usize, Vec<f64>, Vec<[f64; 3]>)> {
let mut reversed = false;
let direct = self
.record
.tokens
.iter()
.position(|token| matches!(token.as_ident(), Some("nubs" | "nurbs")))
.and_then(|start| Self::bspline_at(&self.record.tokens, start));
let spline = match direct {
Some(spline) => spline,
None => {
let selector = self
.record
.tokens
.iter()
.find_map(SatToken::as_integer)
.unwrap_or(1);
reversed = selector < 0;
let target = self
.record
.nth_pointer(1)
.and_then(|pointer| document.resolve(pointer))?;
let mut tokens = target.tokens.as_slice();
if let Some(reference) = subtype_reference(tokens) {
tokens = document.subtype_tokens(reference)?;
}
let blocks: Vec<usize> = tokens
.iter()
.enumerate()
.filter_map(|(index, token)| {
matches!(token.as_ident(), Some("nubs" | "nurbs")).then_some(index)
})
.collect();
let support = selector.unsigned_abs().max(1) as usize;
Self::bspline_at(tokens, *blocks.get(support)?)?
}
};
let (degree, mut knots, controls) = spline;
let (u_offset, v_offset) = self.offsets().unwrap_or((0.0, 0.0));
let mut controls: Vec<[f64; 3]> = controls
.into_iter()
.map(|point| {
[
point[0] + u_offset * point[3],
point[1] + v_offset * point[3],
point[3],
]
})
.collect();
if reversed {
controls.reverse();
let sum = knots.first()? + knots.last()?;
knots = knots.into_iter().rev().map(|knot| sum - knot).collect();
}
Some((degree, knots, controls))
}
pub fn sample(&self, segments: usize) -> Vec<(f64, f64)> {
let Some(start) = self
.record
.tokens
.iter()
.position(|token| matches!(token.as_ident(), Some("nubs" | "nurbs")))
else {
return Vec::new();
};
let Some(spline) = Self::bspline_at(&self.record.tokens, start) else {
return Vec::new();
};
let mut points = Self::sample_bspline(spline, segments);
self.apply_offsets(&mut points);
points
}
pub fn sample_in(&self, document: &SatDocument, segments: usize) -> Vec<(f64, f64)> {
let direct = self.sample(segments);
if !direct.is_empty() {
return direct;
}
let selector = self
.record
.tokens
.iter()
.find_map(SatToken::as_integer)
.unwrap_or(1);
let target = self
.record
.nth_pointer(1)
.and_then(|pointer| document.resolve(pointer));
let Some(target) = target else {
return Vec::new();
};
let mut target_tokens = target.tokens.as_slice();
if let Some(reference) = subtype_reference(target_tokens) {
let Some(tokens) = document.subtype_tokens(reference) else {
return Vec::new();
};
target_tokens = tokens;
}
let blocks: Vec<usize> = target_tokens
.iter()
.enumerate()
.filter_map(|(index, token)| {
matches!(token.as_ident(), Some("nubs" | "nurbs")).then_some(index)
})
.collect();
let support_index = selector.unsigned_abs().max(1) as usize;
let Some(&start) = blocks.get(support_index) else {
return Vec::new();
};
let Some(spline) = Self::bspline_at(target_tokens, start) else {
return Vec::new();
};
let mut points = Self::sample_bspline(spline, segments);
if selector < 0 {
points.reverse();
}
self.apply_offsets(&mut points);
points
}
}
fn subtype_reference(tokens: &[SatToken]) -> Option<usize> {
let start = tokens
.iter()
.position(|token| token.as_ident() == Some("{"))?;
if tokens.get(start + 1).and_then(SatToken::as_ident) != Some("ref")
|| tokens.get(start + 3).and_then(SatToken::as_ident) != Some("}")
{
return None;
}
tokens
.get(start + 2)?
.as_integer()
.and_then(|index| usize::try_from(index).ok())
}
#[derive(Debug, Clone, PartialEq)]
pub struct SatBSplineSurface {
pub rational: bool,
pub degree_u: usize,
pub degree_v: usize,
pub u_closure: Option<String>,
pub v_closure: Option<String>,
pub u_singularity: Option<String>,
pub v_singularity: Option<String>,
pub u_knots: Vec<f64>,
pub v_knots: Vec<f64>,
pub control_count_u: usize,
pub control_count_v: usize,
pub control_points: Vec<[f64; 4]>,
pub fit_tolerance: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SatSplineSurfaceKind {
Spline,
Mesh,
Bs3,
}
#[derive(Debug, Clone)]
pub struct SatSplineSurface<'a> {
record: &'a SatRecord,
kind: SatSplineSurfaceKind,
}
pub type SatMeshSurface<'a> = SatSplineSurface<'a>;
pub type SatBs3Surface<'a> = SatSplineSurface<'a>;
impl<'a> SatSplineSurface<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
let entity_type = record.entity_type.as_str();
let kind = if entity_type == "meshsurf-surface"
|| entity_type.ends_with("-meshsurf-surface")
{
SatSplineSurfaceKind::Mesh
} else if entity_type == "bs3-surface"
|| entity_type.ends_with("-bs3-surface")
{
SatSplineSurfaceKind::Bs3
} else if entity_type == "spline-surface"
|| entity_type.ends_with("-spline-surface")
{
SatSplineSurfaceKind::Spline
} else {
return None;
};
Some(Self { record, kind })
}
pub fn kind(&self) -> SatSplineSurfaceKind {
self.kind
}
pub fn sense(&self) -> Sense {
self.record.token_sense(1)
}
pub fn record(&self) -> &'a SatRecord {
self.record
}
pub fn definition_tokens<'b>(&'b self, document: &'b SatDocument) -> Option<&'b [SatToken]> {
if let Some(reference) = subtype_reference(&self.record.tokens) {
document.subtype_tokens(reference)
} else {
Some(&self.record.tokens)
}
}
pub fn bspline(&self, document: &SatDocument) -> Option<SatBSplineSurface> {
decode_bspline_surface(self.definition_tokens(document)?)
}
}
fn decode_bspline_surface(tokens: &[SatToken]) -> Option<SatBSplineSurface> {
let start = tokens
.iter()
.rposition(|token| matches!(token.as_ident(), Some("nubs" | "nurbs")))?;
let rational = tokens.get(start)?.as_ident() == Some("nurbs");
let degree_u = usize::try_from(tokens.get(start + 1)?.as_integer()?).ok()?;
let degree_v = usize::try_from(tokens.get(start + 2)?.as_integer()?).ok()?;
let string_at = |index: usize| {
tokens
.get(index)
.and_then(SatToken::as_ident)
.map(str::to_owned)
};
let mut form_start = start + 3;
if rational
&& matches!(
tokens.get(form_start).and_then(SatToken::as_ident),
Some("u" | "v" | "both" | "none")
)
{
form_start += 1;
}
let u_closure = string_at(form_start);
let v_closure = string_at(form_start + 1);
let u_singularity = string_at(form_start + 2);
let v_singularity = string_at(form_start + 3);
let u_knot_count = usize::try_from(tokens.get(form_start + 4)?.as_integer()?).ok()?;
let v_knot_count = usize::try_from(tokens.get(form_start + 5)?.as_integer()?).ok()?;
let mut position = form_start + 6;
let mut u_knots = Vec::new();
for _ in 0..u_knot_count {
let value = tokens.get(position)?.as_float()?;
let multiplicity =
usize::try_from(tokens.get(position + 1)?.as_integer()?).ok()?;
u_knots.extend(std::iter::repeat(value).take(multiplicity));
position += 2;
}
let mut v_knots = Vec::new();
for _ in 0..v_knot_count {
let value = tokens.get(position)?.as_float()?;
let multiplicity =
usize::try_from(tokens.get(position + 1)?.as_integer()?).ok()?;
v_knots.extend(std::iter::repeat(value).take(multiplicity));
position += 2;
}
if u_knots.len() < 2 || v_knots.len() < 2 {
return None;
}
let mut values = Vec::new();
for token in &tokens[position..] {
if token.as_ident() == Some("}") {
break;
}
if let Some((components, len)) = token.coordinate_components() {
values.extend_from_slice(&components[..len]);
} else if let Some(value) = token.as_float() {
values.push(value);
} else if let Some(value) = token.as_integer() {
values.push(value as f64);
}
}
let stride = if rational { 4 } else { 3 };
let base_u = u_knots.len().checked_sub(degree_u + 1)?;
let base_v = v_knots.len().checked_sub(degree_v + 1)?;
let mut best: Option<(usize, usize, bool, bool, usize)> = None;
for clamp_u in [false, true] {
for clamp_v in [false, true] {
let count_u = base_u + usize::from(clamp_u) * 2;
let count_v = base_v + usize::from(clamp_v) * 2;
if count_u <= degree_u || count_v <= degree_v {
continue;
}
let needed = count_u.checked_mul(count_v)?.checked_mul(stride)?;
if needed > values.len() {
continue;
}
let remaining = values.len() - needed;
if best
.as_ref()
.map(|candidate| remaining < candidate.4)
.unwrap_or(true)
{
best = Some((count_u, count_v, clamp_u, clamp_v, remaining));
}
}
}
let (control_count_u, control_count_v, clamp_u, clamp_v, _) = best?;
if clamp_u {
let first = *u_knots.first()?;
let last = *u_knots.last()?;
u_knots.insert(0, first);
u_knots.push(last);
}
if clamp_v {
let first = *v_knots.first()?;
let last = *v_knots.last()?;
v_knots.insert(0, first);
v_knots.push(last);
}
let control_scalar_count = control_count_u
.checked_mul(control_count_v)?
.checked_mul(stride)?;
let mut control_points = Vec::with_capacity(control_count_u * control_count_v);
for point in values[..control_scalar_count].chunks_exact(stride) {
if rational {
let weight = point[3];
control_points.push([
point[0] * weight,
point[1] * weight,
point[2] * weight,
weight,
]);
} else {
control_points.push([point[0], point[1], point[2], 1.0]);
}
}
Some(SatBSplineSurface {
rational,
degree_u,
degree_v,
u_closure,
v_closure,
u_singularity,
v_singularity,
u_knots,
v_knots,
control_count_u,
control_count_v,
control_points,
fit_tolerance: values.get(control_scalar_count).copied(),
})
}
fn de_boor_homogeneous(
degree: usize,
knots: &[f64],
ctrl: &[[f64; 4]],
t: f64,
) -> [f64; 4] {
let n = ctrl.len();
let mut k = degree;
while k + 1 < knots.len() && knots[k + 1] <= t && k + 1 <= n {
k += 1;
}
k = k.min(n - 1).max(degree);
let mut d: Vec<[f64; 4]> = (0..=degree)
.map(|j| ctrl[(k - degree + j).min(n - 1)])
.collect();
for r in 1..=degree {
for j in (r..=degree).rev() {
let i = k - degree + j;
let denom = knots[i + degree - r + 1] - knots[i];
let a = if denom.abs() > 1e-12 {
(t - knots[i]) / denom
} else {
0.0
};
for c in 0..4 {
d[j][c] = (1.0 - a) * d[j - 1][c] + a * d[j][c];
}
}
}
d[degree]
}
#[derive(Debug, Clone)]
pub struct SatPlaneSurface<'a> {
record: &'a SatRecord,
}
impl<'a> SatPlaneSurface<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.entity_type == "plane-surface" {
Some(Self { record })
} else {
None
}
}
pub fn root_point(&self) -> (f64, f64, f64) {
let x = self.record.token_float(1).unwrap_or(0.0);
let y = self.record.token_float(2).unwrap_or(0.0);
let z = self.record.token_float(3).unwrap_or(0.0);
(x, y, z)
}
pub fn normal(&self) -> (f64, f64, f64) {
let x = self.record.token_float(4).unwrap_or(0.0);
let y = self.record.token_float(5).unwrap_or(0.0);
let z = self.record.token_float(6).unwrap_or(1.0);
(x, y, z)
}
pub fn u_direction(&self) -> (f64, f64, f64) {
let x = self.record.token_float(7).unwrap_or(1.0);
let y = self.record.token_float(8).unwrap_or(0.0);
let z = self.record.token_float(9).unwrap_or(0.0);
(x, y, z)
}
pub fn sense(&self) -> Sense {
self.record.token_sense(10)
}
}
#[derive(Debug, Clone)]
pub struct SatConeSurface<'a> {
record: &'a SatRecord,
}
impl<'a> SatConeSurface<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.entity_type == "cone-surface" {
Some(Self { record })
} else {
None
}
}
pub fn center(&self) -> (f64, f64, f64) {
let x = self.record.token_float(1).unwrap_or(0.0);
let y = self.record.token_float(2).unwrap_or(0.0);
let z = self.record.token_float(3).unwrap_or(0.0);
(x, y, z)
}
pub fn axis(&self) -> (f64, f64, f64) {
let x = self.record.token_float(4).unwrap_or(0.0);
let y = self.record.token_float(5).unwrap_or(0.0);
let z = self.record.token_float(6).unwrap_or(1.0);
(x, y, z)
}
pub fn major_axis(&self) -> (f64, f64, f64) {
let x = self.record.token_float(7).unwrap_or(1.0);
let y = self.record.token_float(8).unwrap_or(0.0);
let z = self.record.token_float(9).unwrap_or(0.0);
(x, y, z)
}
pub fn ratio(&self) -> f64 {
self.record.token_float(10).unwrap_or(1.0)
}
pub fn sin_half_angle(&self) -> f64 {
self.record.token_float(13).unwrap_or(0.0)
}
pub fn cos_half_angle(&self) -> f64 {
self.record.token_float(14).unwrap_or(1.0)
}
pub fn radius(&self) -> f64 {
self.record.token_float(15).unwrap_or(1.0)
}
pub fn sense(&self) -> Sense {
self.record.token_sense(16)
}
}
#[derive(Debug, Clone)]
pub struct SatSphereSurface<'a> {
record: &'a SatRecord,
}
impl<'a> SatSphereSurface<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.entity_type == "sphere-surface" {
Some(Self { record })
} else {
None
}
}
pub fn center(&self) -> (f64, f64, f64) {
let x = self.record.token_float(1).unwrap_or(0.0);
let y = self.record.token_float(2).unwrap_or(0.0);
let z = self.record.token_float(3).unwrap_or(0.0);
(x, y, z)
}
pub fn radius(&self) -> f64 {
self.record.token_float(4).unwrap_or(1.0)
}
pub fn u_direction(&self) -> (f64, f64, f64) {
let x = self.record.token_float(5).unwrap_or(1.0);
let y = self.record.token_float(6).unwrap_or(0.0);
let z = self.record.token_float(7).unwrap_or(0.0);
(x, y, z)
}
pub fn pole(&self) -> (f64, f64, f64) {
let x = self.record.token_float(8).unwrap_or(0.0);
let y = self.record.token_float(9).unwrap_or(0.0);
let z = self.record.token_float(10).unwrap_or(1.0);
(x, y, z)
}
pub fn sense(&self) -> Sense {
self.record.token_sense(11)
}
}
#[derive(Debug, Clone)]
pub struct SatTorusSurface<'a> {
record: &'a SatRecord,
}
impl<'a> SatTorusSurface<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.entity_type == "torus-surface" {
Some(Self { record })
} else {
None
}
}
pub fn center(&self) -> (f64, f64, f64) {
let x = self.record.token_float(1).unwrap_or(0.0);
let y = self.record.token_float(2).unwrap_or(0.0);
let z = self.record.token_float(3).unwrap_or(0.0);
(x, y, z)
}
pub fn normal(&self) -> (f64, f64, f64) {
let x = self.record.token_float(4).unwrap_or(0.0);
let y = self.record.token_float(5).unwrap_or(0.0);
let z = self.record.token_float(6).unwrap_or(1.0);
(x, y, z)
}
pub fn major_radius(&self) -> f64 {
self.record.token_float(7).unwrap_or(1.0)
}
pub fn minor_radius(&self) -> f64 {
self.record.token_float(8).unwrap_or(0.5)
}
pub fn u_direction(&self) -> (f64, f64, f64) {
let x = self.record.token_float(9).unwrap_or(1.0);
let y = self.record.token_float(10).unwrap_or(0.0);
let z = self.record.token_float(11).unwrap_or(0.0);
(x, y, z)
}
pub fn sense(&self) -> Sense {
self.record.token_sense(12)
}
}
#[derive(Debug, Clone)]
pub struct SatTransform<'a> {
record: &'a SatRecord,
}
impl<'a> SatTransform<'a> {
pub fn from_record(record: &'a SatRecord) -> Option<Self> {
if record.entity_type == "transform" {
Some(Self { record })
} else {
None
}
}
pub fn row0(&self) -> (f64, f64, f64) {
(
self.record.token_float(1).unwrap_or(1.0),
self.record.token_float(2).unwrap_or(0.0),
self.record.token_float(3).unwrap_or(0.0),
)
}
pub fn row1(&self) -> (f64, f64, f64) {
(
self.record.token_float(4).unwrap_or(0.0),
self.record.token_float(5).unwrap_or(1.0),
self.record.token_float(6).unwrap_or(0.0),
)
}
pub fn row2(&self) -> (f64, f64, f64) {
(
self.record.token_float(7).unwrap_or(0.0),
self.record.token_float(8).unwrap_or(0.0),
self.record.token_float(9).unwrap_or(1.0),
)
}
pub fn translation(&self) -> (f64, f64, f64) {
(
self.record.token_float(10).unwrap_or(0.0),
self.record.token_float(11).unwrap_or(0.0),
self.record.token_float(12).unwrap_or(0.0),
)
}
pub fn scale(&self) -> f64 {
self.record.token_float(13).unwrap_or(1.0)
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SatDocument {
pub header: SatHeader,
pub records: Vec<SatRecord>,
}
impl SatDocument {
pub fn new() -> Self {
Self {
header: SatHeader::new(),
records: Vec::new(),
}
}
pub fn with_header(header: SatHeader) -> Self {
Self {
header,
records: Vec::new(),
}
}
pub fn parse(text: &str) -> Result<Self, SatParseError> {
SatParser::parse(text)
}
pub fn to_sat_string(&self) -> String {
SatWriter::write(self)
}
pub fn placement(&self) -> ([[f64; 3]; 3], [f64; 3], f64) {
const IDENTITY: ([[f64; 3]; 3], [f64; 3], f64) =
([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], [0.0; 3], 1.0);
let Some(rec) = self.records.iter().find(|r| r.entity_type == "transform") else {
return IDENTITY;
};
let mut v: Vec<f64> = Vec::with_capacity(13);
for tok in &rec.tokens {
if v.len() >= 13 {
break;
}
if let Some((components, len)) = tok.coordinate_components() {
v.extend_from_slice(&components[..len]);
} else if let Some(f) = tok.as_float() {
v.push(f);
} else if let Some(text) = tok.as_string() {
for word in text.split_ascii_whitespace() {
let Ok(value) = word.parse::<f64>() else {
break;
};
v.push(value);
if v.len() >= 13 {
break;
}
}
}
}
if v.len() < 13 {
return IDENTITY;
}
(
[[v[0], v[1], v[2]], [v[3], v[4], v[5]], [v[6], v[7], v[8]]],
[v[9], v[10], v[11]],
v[12],
)
}
pub fn set_placement(&mut self, matrix: [[f64; 3]; 3], translation: [f64; 3], scale: f64) {
let mut tokens = Vec::with_capacity(17);
tokens.push(SatToken::Pointer(SatPointer::NULL)); for row in &matrix {
for &x in row {
tokens.push(SatToken::Float(x));
}
}
for &x in &translation {
tokens.push(SatToken::Float(x));
}
tokens.push(SatToken::Float(scale));
tokens.push(SatToken::Integer(0));
tokens.push(SatToken::Integer(0));
tokens.push(SatToken::Integer(1));
if let Some(rec) = self.records.iter_mut().find(|r| r.entity_type == "transform") {
rec.tokens = tokens;
return;
}
let terminator = self
.records
.iter()
.position(|r| r.entity_type.starts_with("End-of"))
.map(|p| self.records.remove(p));
let index = self.records.len() as i32;
let mut rec = SatRecord::new(index, "transform");
rec.attribute = SatPointer::NULL;
rec.tokens = tokens;
self.records.push(rec);
if let Some(body) = self.records.iter_mut().find(|r| r.entity_type == "body") {
if body.tokens.len() >= 4 {
body.tokens[3] = SatToken::Pointer(SatPointer::new(index));
}
}
if let Some(term) = terminator {
self.records.push(term);
}
self.header.num_records = self.records.len();
}
pub fn record_count(&self) -> usize {
self.records.len()
}
pub fn record(&self, index: usize) -> Option<&SatRecord> {
if let Some(r) = self.records.get(index) {
if r.index == index as i32 {
return Some(r);
}
}
self.records.iter().find(|r| r.index == index as i32)
}
pub fn record_mut(&mut self, index: usize) -> Option<&mut SatRecord> {
if matches!(self.records.get(index), Some(r) if r.index == index as i32) {
return self.records.get_mut(index);
}
self.records.iter_mut().find(|r| r.index == index as i32)
}
pub fn records_of_type(&self, entity_type: &str) -> Vec<&SatRecord> {
self.records
.iter()
.filter(|r| r.entity_type == entity_type)
.collect()
}
pub fn bodies(&self) -> Vec<SatBody<'_>> {
self.records
.iter()
.filter_map(SatBody::from_record)
.collect()
}
pub fn subshells(&self) -> Vec<SatSubshell<'_>> {
self.records
.iter()
.filter_map(SatSubshell::from_record)
.collect()
}
pub fn wires(&self) -> Vec<SatWire<'_>> {
self.records
.iter()
.filter_map(SatWire::from_record)
.collect()
}
pub fn attributes(&self) -> Vec<SatAttribute<'_>> {
self.records
.iter()
.filter_map(SatAttribute::from_record)
.collect()
}
pub fn faces(&self) -> Vec<SatFace<'_>> {
self.records
.iter()
.filter_map(SatFace::from_record)
.collect()
}
pub fn edges(&self) -> Vec<SatEdge<'_>> {
self.records
.iter()
.filter_map(SatEdge::from_record)
.collect()
}
pub fn vertices(&self) -> Vec<SatVertex<'_>> {
self.records
.iter()
.filter_map(SatVertex::from_record)
.collect()
}
pub fn spline_surfaces(&self) -> Vec<SatSplineSurface<'_>> {
self.records
.iter()
.filter_map(SatSplineSurface::from_record)
.collect()
}
pub fn add_record(&mut self, mut record: SatRecord) -> i32 {
let index = self.records.len() as i32;
record.index = index;
self.records.push(record);
self.header.num_records = self.records.len();
index
}
pub fn resolve(&self, ptr: SatPointer) -> Option<&SatRecord> {
if ptr.is_null() {
None
} else {
self.record(ptr.0 as usize)
}
}
pub fn subtype_tokens(&self, requested: usize) -> Option<&[SatToken]> {
let mut current = 0usize;
for record in &self.records {
let tokens = &record.tokens;
for start in 0..tokens.len() {
if tokens[start].as_ident() != Some("{")
|| tokens.get(start + 1).and_then(SatToken::as_ident) == Some("ref")
{
continue;
}
if current == requested {
let mut depth = 1usize;
for end in start + 1..tokens.len() {
match tokens[end].as_ident() {
Some("{") => depth += 1,
Some("}") => {
depth -= 1;
if depth == 0 {
return Some(&tokens[start + 1..end]);
}
}
_ => {}
}
}
return None;
}
current += 1;
}
}
None
}
pub fn validate(&self) -> Vec<SatValidationError> {
let mut errors = Vec::new();
let max_index = self.records.len() as i32;
for record in &self.records {
if !record.attribute.is_null() {
if let Some(idx) = record.attribute.index() {
if idx as i32 >= max_index {
errors.push(SatValidationError::InvalidPointer {
record_index: record.index,
pointer_value: record.attribute.0,
context: "attribute".to_string(),
});
}
}
}
for (i, token) in record.tokens.iter().enumerate() {
if let SatToken::Pointer(p) = token {
if !p.is_null() {
if let Some(idx) = p.index() {
if idx as i32 >= max_index {
errors.push(SatValidationError::InvalidPointer {
record_index: record.index,
pointer_value: p.0,
context: format!("token[{}]", i),
});
}
}
}
}
}
}
errors
}
pub fn strip_for_sab(&mut self) {
let keep: Vec<bool> = self
.records
.iter()
.map(|r| Self::is_core_geometry_type(&r.entity_type))
.collect();
let kept_count = keep.iter().filter(|&&k| k).count();
if kept_count == self.records.len() {
return; }
let mut index_map = vec![-1i32; self.records.len()];
let mut new_idx: i32 = 0;
for (old_idx, &kept) in keep.iter().enumerate() {
if kept {
index_map[old_idx] = new_idx;
new_idx += 1;
}
}
let remap = |p: i32| -> i32 {
if p < 0 || (p as usize) >= index_map.len() {
-1
} else {
index_map[p as usize]
}
};
let mut new_records = Vec::with_capacity(kept_count);
let old_records = std::mem::take(&mut self.records);
for (old_idx, record) in old_records.into_iter().enumerate() {
if !keep[old_idx] {
continue;
}
let mut rec = record;
rec.index = index_map[old_idx];
rec.attribute = SatPointer::new(remap(rec.attribute.0));
for token in &mut rec.tokens {
if let SatToken::Pointer(p) = token {
p.0 = remap(p.0);
}
}
new_records.push(rec);
}
self.records = new_records;
self.header.num_records = self.records.len();
self.header.spatial_resolution = 1.0;
}
fn is_core_geometry_type(entity_type: &str) -> bool {
let base = if let Some(pos) = entity_type.rfind('-') {
&entity_type[pos + 1..]
} else {
entity_type
};
matches!(
base,
"body"
| "lump"
| "shell"
| "subshell"
| "face"
| "loop"
| "coedge"
| "edge"
| "vertex"
| "wire"
| "point"
| "curve"
| "surface"
| "pcurve"
| "transform"
| "asmheader"
)
}
}
impl Default for SatDocument {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SatParseError {
EmptyInput,
InvalidHeader(String),
InvalidProductInfo(String),
InvalidTolerances(String),
InvalidRecord {
line: usize,
message: String,
},
UnexpectedToken {
line: usize,
token: String,
},
}
impl fmt::Display for SatParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyInput => write!(f, "SAT data is empty"),
Self::InvalidHeader(msg) => write!(f, "Invalid SAT header: {}", msg),
Self::InvalidProductInfo(msg) => write!(f, "Invalid product info: {}", msg),
Self::InvalidTolerances(msg) => write!(f, "Invalid tolerances: {}", msg),
Self::InvalidRecord { line, message } => {
write!(f, "Invalid SAT record at line {}: {}", line, message)
}
Self::UnexpectedToken { line, token } => {
write!(f, "Unexpected token '{}' at line {}", token, line)
}
}
}
}
impl std::error::Error for SatParseError {}
#[derive(Debug, Clone, PartialEq)]
pub enum SatValidationError {
InvalidPointer {
record_index: i32,
pointer_value: i32,
context: String,
},
}
impl fmt::Display for SatValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidPointer {
record_index,
pointer_value,
context,
} => write!(
f,
"Record {} has invalid pointer ${} in {}",
record_index, pointer_value, context
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SatEntityCategory {
Header,
Topology,
Geometry,
Transform,
Attribute,
Unknown,
}
pub fn classify_entity_type(entity_type: &str) -> SatEntityCategory {
match entity_type {
"asmheader" => SatEntityCategory::Header,
"body" | "lump" | "shell" | "subshell" | "face" | "loop" | "coedge" | "edge"
| "vertex" | "wire" => SatEntityCategory::Topology,
"point" | "straight-curve" | "ellipse-curve" | "intcurve-curve" | "bs3-curve" | "pcurve"
| "plane-surface" | "cone-surface" | "sphere-surface" | "torus-surface"
| "spline-surface" | "meshsurf-surface" | "bs3-surface" => SatEntityCategory::Geometry,
"transform" => SatEntityCategory::Transform,
_ if matches!(
base_entity_type(entity_type),
"body"
| "lump"
| "shell"
| "subshell"
| "face"
| "loop"
| "coedge"
| "edge"
| "vertex"
| "wire"
) => SatEntityCategory::Topology,
_ if matches!(
base_entity_type(entity_type),
"point" | "curve" | "surface" | "pcurve"
) => SatEntityCategory::Geometry,
_ if entity_type.ends_with("-attrib") || entity_type.starts_with("attrib") => {
SatEntityCategory::Attribute
}
_ => SatEntityCategory::Unknown,
}
}
pub fn base_entity_type(entity_type: &str) -> &str {
entity_type.rsplit('-').next().unwrap_or(entity_type)
}