mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
STEP geometry as plain data: read and write ISO 10303-42 curves and surfaces without truck
A new crate, solidcraft-step (L1, no modelling backend), holds the Part 21 parser (moved from the kernel, which now reads through it) and STEP geometry as plain data: lines, conics, B-splines (rational and implied knots), polylines, trimmed curves, surface curves with their pcurves, 3D offset curves; planes, cylinders, cones, spheres, tori (degenerate too), B-spline surfaces, revolutions, linear extrusions, offset and rectangular trimmed surfaces. Each keeps STEP's own definition and parameterisation, in mm and radians, with pcurves scaled by their surface's parameter units. Shared entities stay shared. A writer emits the same entities back, deduplicated. Tests per entity check values against analytic points, units, round trips and hostile input; an ignored test reads and round-trips every edge curve and face surface of the tangency-labs/models STEP files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Generated
+5
@@ -4367,6 +4367,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-smlib",
|
||||
"solidcraft-step",
|
||||
"spade",
|
||||
"thiserror 2.0.21",
|
||||
"truck-meshalgo",
|
||||
@@ -4426,6 +4427,10 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-step"
|
||||
version = "0.18.1"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-egui"
|
||||
version = "0.24.0"
|
||||
|
||||
@@ -19,6 +19,7 @@ solidcraft-geom = { path = "crates/geom" }
|
||||
solidcraft-sketch = { path = "crates/sketch" }
|
||||
solidcraft-kernel = { path = "crates/kernel" }
|
||||
solidcraft-smlib = { path = "crates/smlib" }
|
||||
solidcraft-step = { path = "crates/step" }
|
||||
solidcraft-render = { path = "crates/render" }
|
||||
solidcraft-gpu = { path = "crates/gpu" }
|
||||
solidcraft-doc = { path = "crates/doc" }
|
||||
|
||||
@@ -16,6 +16,7 @@ smlib = ["dep:solidcraft-smlib", "solidcraft-smlib/native"]
|
||||
[dependencies]
|
||||
solidcraft-geom = { workspace = true }
|
||||
solidcraft-smlib = { workspace = true, optional = true }
|
||||
solidcraft-step = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@@ -1,601 +1,20 @@
|
||||
//! ISO 10303-21 ("Part 21") exchange structure parser: the HEADER and the DATA section's entity
|
||||
//! instances, simple (`#1=NAME(...)`) and complex (`#2=(A(...) B(...))`). Written from the
|
||||
//! public standard. Input is hostile: sizes, entity counts and nesting depth are capped and every
|
||||
//! error is a message, never a panic.
|
||||
//! The Part 21 parser lives in `solidcraft-step`; this reads with the kernel's progress watch.
|
||||
|
||||
use std::collections::HashMap;
|
||||
pub use solidcraft_step::p21::{Entity, Exchange, Param, Record};
|
||||
|
||||
/// Largest STEP text we parse.
|
||||
pub const MAX_BYTES: usize = 512 << 20;
|
||||
/// Most entity instances in one file.
|
||||
pub const MAX_ENTITIES: usize = 20_000_000;
|
||||
/// Deepest parameter list nesting.
|
||||
const MAX_DEPTH: usize = 64;
|
||||
struct Watched(std::sync::Arc<dyn crate::watch::Watch>);
|
||||
|
||||
/// A parameter value.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Param {
|
||||
Int(i64),
|
||||
Real(f64),
|
||||
Str(String),
|
||||
/// `.NAME.` (`.T.`/`.F.` included).
|
||||
Enum(String),
|
||||
Ref(u64),
|
||||
List(Vec<Param>),
|
||||
/// `NAME(params)` (a typed value such as `LENGTH_MEASURE(1.)`).
|
||||
Typed(String, Vec<Param>),
|
||||
Binary(String),
|
||||
/// `$`
|
||||
Null,
|
||||
/// `*`
|
||||
Derived,
|
||||
}
|
||||
|
||||
impl Param {
|
||||
pub fn as_f64(&self) -> Option<f64> {
|
||||
match self {
|
||||
Param::Real(x) => Some(*x).filter(|x| x.is_finite()),
|
||||
Param::Int(i) => Some(*i as f64),
|
||||
Param::Typed(_, v) if v.len() == 1 => v.first().and_then(Param::as_f64),
|
||||
_ => None,
|
||||
}
|
||||
impl solidcraft_step::p21::Progress for Watched {
|
||||
fn cancelled(&self) -> bool {
|
||||
self.0.cancelled()
|
||||
}
|
||||
pub fn as_i64(&self) -> Option<i64> {
|
||||
match self {
|
||||
Param::Int(i) => Some(*i),
|
||||
Param::Real(x) if x.fract() == 0.0 && x.abs() < 9e15 => Some(*x as i64),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn as_ref_id(&self) -> Option<u64> {
|
||||
match self {
|
||||
Param::Ref(r) => Some(*r),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn as_str(&self) -> Option<&str> {
|
||||
match self {
|
||||
Param::Str(s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn as_enum(&self) -> Option<&str> {
|
||||
match self {
|
||||
Param::Enum(s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// `.T.` → true, `.F.` → false (`.U.` and anything else → None).
|
||||
pub fn as_bool(&self) -> Option<bool> {
|
||||
match self.as_enum()? {
|
||||
"T" => Some(true),
|
||||
"F" => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn as_list(&self) -> Option<&[Param]> {
|
||||
match self {
|
||||
Param::List(v) => Some(v),
|
||||
_ => None,
|
||||
}
|
||||
fn progress(&self, stage: &'static str, done: usize, total: usize) {
|
||||
self.0.progress(stage, done, total);
|
||||
}
|
||||
}
|
||||
|
||||
/// One record of an entity instance: a simple instance has one, a complex one several.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Record {
|
||||
pub name: String,
|
||||
pub params: Vec<Param>,
|
||||
}
|
||||
|
||||
/// An entity instance.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Entity {
|
||||
pub records: Vec<Record>,
|
||||
}
|
||||
|
||||
impl Entity {
|
||||
pub fn is_complex(&self) -> bool {
|
||||
self.records.len() > 1
|
||||
}
|
||||
/// Name of a simple instance (the first record of a complex one).
|
||||
pub fn name(&self) -> &str {
|
||||
self.records.first().map(|r| r.name.as_str()).unwrap_or("")
|
||||
}
|
||||
/// Parameters of the record called `name`.
|
||||
pub fn record(&self, name: &str) -> Option<&[Param]> {
|
||||
self.records.iter().find(|r| r.name == name).map(|r| r.params.as_slice())
|
||||
}
|
||||
pub fn has(&self, name: &str) -> bool {
|
||||
self.records.iter().any(|r| r.name == name)
|
||||
}
|
||||
/// Parameters of a simple instance.
|
||||
pub fn params(&self) -> &[Param] {
|
||||
self.records.first().map(|r| r.params.as_slice()).unwrap_or(&[])
|
||||
}
|
||||
}
|
||||
|
||||
/// A parsed exchange file.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Exchange {
|
||||
/// FILE_SCHEMA names.
|
||||
pub schemas: Vec<String>,
|
||||
/// FILE_NAME originating system.
|
||||
pub originating_system: String,
|
||||
/// FILE_NAME preprocessor version.
|
||||
pub preprocessor: String,
|
||||
/// FILE_DESCRIPTION text, its lines joined by a space.
|
||||
pub description: String,
|
||||
pub entities: HashMap<u64, Entity>,
|
||||
/// Ids defined more than once (the first definition is kept).
|
||||
pub duplicates: Vec<u64>,
|
||||
/// [`Exchange::vertex_extent`], worked out on first use.
|
||||
extent: std::sync::OnceLock<f64>,
|
||||
}
|
||||
|
||||
impl Exchange {
|
||||
pub fn get(&self, id: u64) -> Option<&Entity> {
|
||||
self.entities.get(&id)
|
||||
}
|
||||
|
||||
/// The size of the model in the file's own coordinates: the diagonal of the box around every
|
||||
/// VERTEX_POINT (0 for a file without one). Non-finite coordinates are skipped.
|
||||
pub fn vertex_extent(&self) -> f64 {
|
||||
*self.extent.get_or_init(|| {
|
||||
let (mut lo, mut hi) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
|
||||
for e in self.entities.values().filter(|e| e.name() == "VERTEX_POINT") {
|
||||
let Some(p) = e.params().get(1).and_then(Param::as_ref_id).and_then(|id| self.get(id)) else { continue };
|
||||
let Some(xyz) = p.params().get(1).and_then(Param::as_list) else { continue };
|
||||
let c: Vec<f64> = xyz.iter().take(3).filter_map(Param::as_f64).collect();
|
||||
if c.len() != 3 || !c.iter().all(|x| x.is_finite()) {
|
||||
continue;
|
||||
}
|
||||
for ((l, h), x) in lo.iter_mut().zip(hi.iter_mut()).zip(c) {
|
||||
*l = l.min(x);
|
||||
*h = h.max(x);
|
||||
}
|
||||
}
|
||||
let d = lo.iter().zip(&hi).map(|(l, h)| (h - l).powi(2)).sum::<f64>().sqrt();
|
||||
if d.is_finite() { d } else { 0.0 }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct Lexer<'a> {
|
||||
s: &'a [u8],
|
||||
i: usize,
|
||||
}
|
||||
|
||||
fn err<T>(msg: impl Into<String>) -> Result<T, String> {
|
||||
Err(msg.into())
|
||||
}
|
||||
|
||||
impl<'a> Lexer<'a> {
|
||||
fn line(&self) -> usize {
|
||||
self.s.get(..self.i).map(|b| b.iter().filter(|c| **c == b'\n').count() + 1).unwrap_or(0)
|
||||
}
|
||||
fn fail<T>(&self, msg: &str) -> Result<T, String> {
|
||||
err(format!("STEP syntax error at line {}: {msg}", self.line()))
|
||||
}
|
||||
fn peek(&self) -> Option<u8> {
|
||||
self.s.get(self.i).copied()
|
||||
}
|
||||
/// Skip whitespace and comments.
|
||||
fn ws(&mut self) -> Result<(), String> {
|
||||
loop {
|
||||
match self.peek() {
|
||||
Some(c) if c.is_ascii_whitespace() => self.i += 1,
|
||||
Some(b'/') if self.s.get(self.i + 1) == Some(&b'*') => {
|
||||
let rest = self.s.get(self.i + 2..).unwrap_or(&[]);
|
||||
match rest.windows(2).position(|w| w == b"*/") {
|
||||
Some(p) => self.i += p + 4,
|
||||
None => return self.fail("unterminated comment"),
|
||||
}
|
||||
}
|
||||
_ => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn eat(&mut self, c: u8) -> Result<bool, String> {
|
||||
self.ws()?;
|
||||
if self.peek() == Some(c) {
|
||||
self.i += 1;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
fn expect(&mut self, c: u8) -> Result<(), String> {
|
||||
if self.eat(c)? { Ok(()) } else { self.fail(&format!("expected `{}`", c as char)) }
|
||||
}
|
||||
fn keyword(&mut self) -> Result<String, String> {
|
||||
self.ws()?;
|
||||
let st = self.i;
|
||||
if self.peek() == Some(b'!') {
|
||||
self.i += 1;
|
||||
}
|
||||
while let Some(c) = self.peek() {
|
||||
if c.is_ascii_alphanumeric() || c == b'_' || c == b'-' {
|
||||
self.i += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if self.i == st {
|
||||
return self.fail("expected a keyword");
|
||||
}
|
||||
Ok(String::from_utf8_lossy(self.s.get(st..self.i).unwrap_or(&[])).to_ascii_uppercase())
|
||||
}
|
||||
fn number(&mut self) -> Result<Param, String> {
|
||||
let st = self.i;
|
||||
while let Some(c) = self.peek() {
|
||||
if c.is_ascii_digit() || matches!(c, b'+' | b'-' | b'.' | b'E' | b'e') {
|
||||
self.i += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let t = std::str::from_utf8(self.s.get(st..self.i).unwrap_or(&[])).unwrap_or("");
|
||||
if !t.contains(['.', 'E', 'e'])
|
||||
&& let Ok(i) = t.parse::<i64>()
|
||||
{
|
||||
return Ok(Param::Int(i));
|
||||
}
|
||||
// STEP allows `1.` and `1.E-3`.
|
||||
let fixed = t.replace(".E", ".0E").replace(".e", ".0e");
|
||||
let fixed = if fixed.ends_with('.') { format!("{fixed}0") } else { fixed };
|
||||
match fixed.parse::<f64>() {
|
||||
Ok(x) => Ok(Param::Real(x)),
|
||||
Err(_) => self.fail(&format!("bad number `{t}`")),
|
||||
}
|
||||
}
|
||||
fn string(&mut self) -> Result<String, String> {
|
||||
// Opening quote consumed by the caller.
|
||||
let mut raw = Vec::new();
|
||||
loop {
|
||||
match self.peek() {
|
||||
None => return self.fail("unterminated string"),
|
||||
Some(b'\'') => {
|
||||
if self.s.get(self.i + 1) == Some(&b'\'') {
|
||||
raw.push(b'\'');
|
||||
self.i += 2;
|
||||
} else {
|
||||
self.i += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(b'\n') | Some(b'\r') => self.i += 1,
|
||||
Some(c) => {
|
||||
raw.push(c);
|
||||
self.i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(decode_string(&raw))
|
||||
}
|
||||
fn param(&mut self, depth: usize) -> Result<Param, String> {
|
||||
if depth > MAX_DEPTH {
|
||||
return self.fail("parameters nested too deeply");
|
||||
}
|
||||
self.ws()?;
|
||||
let Some(c) = self.peek() else { return self.fail("unexpected end of file") };
|
||||
match c {
|
||||
b'$' => {
|
||||
self.i += 1;
|
||||
Ok(Param::Null)
|
||||
}
|
||||
b'*' => {
|
||||
self.i += 1;
|
||||
Ok(Param::Derived)
|
||||
}
|
||||
b'\'' => {
|
||||
self.i += 1;
|
||||
Ok(Param::Str(self.string()?))
|
||||
}
|
||||
b'"' => {
|
||||
self.i += 1;
|
||||
let st = self.i;
|
||||
while self.peek().is_some_and(|c| c != b'"') {
|
||||
self.i += 1;
|
||||
}
|
||||
let v = String::from_utf8_lossy(self.s.get(st..self.i).unwrap_or(&[])).to_string();
|
||||
self.expect(b'"')?;
|
||||
Ok(Param::Binary(v))
|
||||
}
|
||||
b'#' => {
|
||||
self.i += 1;
|
||||
let st = self.i;
|
||||
while self.peek().is_some_and(|c| c.is_ascii_digit()) {
|
||||
self.i += 1;
|
||||
}
|
||||
let t = std::str::from_utf8(self.s.get(st..self.i).unwrap_or(&[])).unwrap_or("");
|
||||
t.parse::<u64>().map(Param::Ref).or_else(|_| self.fail("bad entity reference"))
|
||||
}
|
||||
b'.' if self.s.get(self.i + 1).is_some_and(|c| c.is_ascii_alphabetic() || *c == b'_') => {
|
||||
self.i += 1;
|
||||
let st = self.i;
|
||||
while self.peek().is_some_and(|c| c != b'.') {
|
||||
self.i += 1;
|
||||
}
|
||||
let v = String::from_utf8_lossy(self.s.get(st..self.i).unwrap_or(&[])).trim().to_ascii_uppercase();
|
||||
self.expect(b'.')?;
|
||||
Ok(Param::Enum(v))
|
||||
}
|
||||
b'(' => {
|
||||
self.i += 1;
|
||||
Ok(Param::List(self.list_rest(depth + 1)?))
|
||||
}
|
||||
c if c.is_ascii_digit() || matches!(c, b'+' | b'-' | b'.') => self.number(),
|
||||
c if c.is_ascii_alphabetic() || c == b'!' => {
|
||||
let name = self.keyword()?;
|
||||
self.expect(b'(')?;
|
||||
Ok(Param::Typed(name, self.list_rest(depth + 1)?))
|
||||
}
|
||||
_ => self.fail(&format!("unexpected `{}`", c as char)),
|
||||
}
|
||||
}
|
||||
/// Parameters after an opening `(` up to and including the `)`.
|
||||
fn list_rest(&mut self, depth: usize) -> Result<Vec<Param>, String> {
|
||||
let mut v = Vec::new();
|
||||
if self.eat(b')')? {
|
||||
return Ok(v);
|
||||
}
|
||||
loop {
|
||||
v.push(self.param(depth)?);
|
||||
if self.eat(b',')? {
|
||||
continue;
|
||||
}
|
||||
self.expect(b')')?;
|
||||
return Ok(v);
|
||||
}
|
||||
}
|
||||
fn record(&mut self) -> Result<Record, String> {
|
||||
let name = self.keyword()?;
|
||||
self.expect(b'(')?;
|
||||
Ok(Record { name, params: self.list_rest(1)? })
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode the Part 21 string escapes (`\X\hh`, `\X2\…\X0\`, `\X4\…\X0\`, `\S\c`, `\\`).
|
||||
fn decode_string(raw: &[u8]) -> String {
|
||||
let mut out = String::new();
|
||||
let mut i = 0;
|
||||
let hex = |b: &[u8]| std::str::from_utf8(b).ok().and_then(|t| u32::from_str_radix(t, 16).ok());
|
||||
while let Some(&c) = raw.get(i) {
|
||||
if c == b'\\' {
|
||||
let rest = raw.get(i..).unwrap_or(&[]);
|
||||
if rest.starts_with(b"\\\\") {
|
||||
out.push('\\');
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if rest.starts_with(b"\\X\\")
|
||||
&& let Some(v) = rest.get(3..5).and_then(hex)
|
||||
{
|
||||
out.push(char::from_u32(v).unwrap_or('?'));
|
||||
i += 5;
|
||||
continue;
|
||||
}
|
||||
let mut run = false;
|
||||
for (tag, width) in [(&b"\\X2\\"[..], 4usize), (&b"\\X4\\"[..], 8)] {
|
||||
if rest.starts_with(tag)
|
||||
&& let Some(end) = rest.windows(4).position(|w| w == b"\\X0\\")
|
||||
{
|
||||
let body = rest.get(4..end).unwrap_or(&[]);
|
||||
let units: Vec<u32> = body.chunks(width).filter_map(hex).collect();
|
||||
if width == 4 {
|
||||
let u16s: Vec<u16> = units.iter().map(|u| *u as u16).collect();
|
||||
out.push_str(&String::from_utf16_lossy(&u16s));
|
||||
} else {
|
||||
out.extend(units.iter().map(|u| char::from_u32(*u).unwrap_or('?')));
|
||||
}
|
||||
i += end + 4;
|
||||
run = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// A run may be followed straight away by the backslash of the next escape (#2344).
|
||||
if run {
|
||||
continue;
|
||||
}
|
||||
if rest.starts_with(b"\\S\\")
|
||||
&& let Some(&ch) = rest.get(3)
|
||||
{
|
||||
out.push(char::from_u32(ch as u32 + 128).unwrap_or('?'));
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
out.push('\\');
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
// Plain bytes: keep UTF-8 runs intact.
|
||||
let st = i;
|
||||
while raw.get(i).is_some_and(|c| *c != b'\\') {
|
||||
i += 1;
|
||||
}
|
||||
out.push_str(&String::from_utf8_lossy(raw.get(st..i).unwrap_or(&[])));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Parse a Part 21 file.
|
||||
/// [`solidcraft_step::p21::parse`] under the current [`crate::watch`].
|
||||
pub fn parse(text: &str) -> Result<Exchange, String> {
|
||||
if text.len() > MAX_BYTES {
|
||||
return err(format!("STEP file too large ({} MB, limit {} MB)", text.len() >> 20, MAX_BYTES >> 20));
|
||||
}
|
||||
let mut lx = Lexer { s: text.as_bytes(), i: 0 };
|
||||
let magic = lx.keyword()?;
|
||||
if magic != "ISO-10303-21" {
|
||||
return err("not a STEP file (no ISO-10303-21 header)");
|
||||
}
|
||||
lx.expect(b';')?;
|
||||
let mut ex = Exchange::default();
|
||||
let watch = crate::watch::current();
|
||||
loop {
|
||||
lx.ws()?;
|
||||
if lx.peek().is_none() {
|
||||
return err("STEP file ends without END-ISO-10303-21");
|
||||
}
|
||||
let kw = lx.keyword()?;
|
||||
match kw.as_str() {
|
||||
"HEADER" => {
|
||||
lx.expect(b';')?;
|
||||
loop {
|
||||
let k = lx.keyword()?;
|
||||
if k == "ENDSEC" {
|
||||
lx.expect(b';')?;
|
||||
break;
|
||||
}
|
||||
lx.expect(b'(')?;
|
||||
let params = lx.list_rest(1)?;
|
||||
lx.expect(b';')?;
|
||||
match k.as_str() {
|
||||
"FILE_SCHEMA" => {
|
||||
if let Some(Param::List(v)) = params.first() {
|
||||
ex.schemas = v.iter().filter_map(|p| p.as_str().map(str::to_string)).collect();
|
||||
}
|
||||
}
|
||||
"FILE_DESCRIPTION" => {
|
||||
if let Some(Param::List(v)) = params.first() {
|
||||
ex.description = v.iter().filter_map(|p| p.as_str()).collect::<Vec<_>>().join(" ");
|
||||
}
|
||||
}
|
||||
"FILE_NAME" => {
|
||||
ex.preprocessor = params.get(4).and_then(Param::as_str).unwrap_or("").to_string();
|
||||
ex.originating_system = params.get(5).and_then(Param::as_str).unwrap_or("").to_string();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
"DATA" => {
|
||||
// Optional `DATA('name', ('schema'));`
|
||||
if lx.eat(b'(')? {
|
||||
lx.list_rest(1)?;
|
||||
}
|
||||
lx.expect(b';')?;
|
||||
loop {
|
||||
lx.ws()?;
|
||||
if lx.peek() != Some(b'#') {
|
||||
let k = lx.keyword()?;
|
||||
if k == "ENDSEC" {
|
||||
lx.expect(b';')?;
|
||||
break;
|
||||
}
|
||||
return lx.fail(&format!("unexpected `{k}` in DATA"));
|
||||
}
|
||||
lx.i += 1;
|
||||
let st = lx.i;
|
||||
while lx.peek().is_some_and(|c| c.is_ascii_digit()) {
|
||||
lx.i += 1;
|
||||
}
|
||||
let id: u64 = match std::str::from_utf8(lx.s.get(st..lx.i).unwrap_or(&[])).ok().and_then(|t| t.parse().ok()) {
|
||||
Some(id) => id,
|
||||
None => return lx.fail("bad entity id"),
|
||||
};
|
||||
lx.expect(b'=')?;
|
||||
let records = if lx.eat(b'(')? {
|
||||
let mut rs = Vec::new();
|
||||
while !lx.eat(b')')? {
|
||||
if rs.len() > 64 {
|
||||
return lx.fail("complex entity with too many parts");
|
||||
}
|
||||
rs.push(lx.record()?);
|
||||
}
|
||||
rs
|
||||
} else {
|
||||
vec![lx.record()?]
|
||||
};
|
||||
lx.expect(b';')?;
|
||||
if ex.entities.len() % 4096 == 0
|
||||
&& let Some(w) = &watch
|
||||
{
|
||||
if w.cancelled() {
|
||||
return err("STEP import cancelled");
|
||||
}
|
||||
w.progress("reading", lx.i, lx.s.len());
|
||||
}
|
||||
if ex.entities.len() >= MAX_ENTITIES {
|
||||
return err(format!("STEP file has more than {MAX_ENTITIES} entities"));
|
||||
}
|
||||
// A duplicate id is invalid; some writers emit them for geometry nobody
|
||||
// reads. Keep the first definition and report it.
|
||||
match ex.entities.entry(id) {
|
||||
std::collections::hash_map::Entry::Occupied(_) => ex.duplicates.push(id),
|
||||
std::collections::hash_map::Entry::Vacant(v) => {
|
||||
v.insert(Entity { records });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"END-ISO-10303-21" => {
|
||||
lx.expect(b';')?;
|
||||
return Ok(ex);
|
||||
}
|
||||
// ANCHOR / REFERENCE / SIGNATURE sections (Part 21 edition 3): skip to ENDSEC.
|
||||
"ANCHOR" | "REFERENCE" | "SIGNATURE" => {
|
||||
let rest = lx.s.get(lx.i..).unwrap_or(&[]);
|
||||
match rest.windows(7).position(|w| w == b"ENDSEC;") {
|
||||
Some(p) => lx.i += p + 7,
|
||||
None => return lx.fail("unterminated section"),
|
||||
}
|
||||
}
|
||||
_ => return lx.fail(&format!("unexpected `{kw}`")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_simple_and_complex() {
|
||||
let t = "ISO-10303-21;\nHEADER;FILE_DESCRIPTION((''),'2;1');\nFILE_NAME('a','',(''),(''),'','sys','');\nFILE_SCHEMA(('AUTOMOTIVE_DESIGN'));ENDSEC;\nDATA;\n#1=CARTESIAN_POINT('p',(0.,1.5E1,-2));\n/* c */ #2=(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.));\n#3=X('it''s','\\X2\\00E9\\X0\\',$,#1,LENGTH_MEASURE(1.E-3),\"0F\");\nENDSEC;\nEND-ISO-10303-21;\n";
|
||||
let ex = parse(t).unwrap();
|
||||
assert_eq!(ex.schemas, vec!["AUTOMOTIVE_DESIGN"]);
|
||||
assert_eq!(ex.originating_system, "sys");
|
||||
let p = ex.get(1).unwrap();
|
||||
assert_eq!(p.name(), "CARTESIAN_POINT");
|
||||
let xyz: Vec<f64> = p.params()[1].as_list().unwrap().iter().filter_map(Param::as_f64).collect();
|
||||
assert_eq!(xyz, vec![0.0, 15.0, -2.0]);
|
||||
let u = ex.get(2).unwrap();
|
||||
assert!(u.is_complex() && u.has("SI_UNIT"));
|
||||
assert_eq!(u.record("SI_UNIT").unwrap()[0].as_enum(), Some("MILLI"));
|
||||
let x = ex.get(3).unwrap().params();
|
||||
assert_eq!(x[0].as_str(), Some("it's"));
|
||||
assert_eq!(x[1].as_str(), Some("\u{e9}"));
|
||||
assert_eq!(x[2], Param::Null);
|
||||
assert_eq!(x[3].as_ref_id(), Some(1));
|
||||
assert_eq!(x[4].as_f64(), Some(1e-3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_text_is_an_error() {
|
||||
for t in [
|
||||
"",
|
||||
"garbage",
|
||||
"ISO-10303-21;",
|
||||
"ISO-10303-21;DATA;#1=A(",
|
||||
"ISO-10303-21;DATA;#1=A('x",
|
||||
"ISO-10303-21;DATA;#1=A(/*",
|
||||
"ISO-10303-21;DATA;#=A();ENDSEC;END-ISO-10303-21;",
|
||||
"ISO-10303-21;DATA;#99999999999999999999999=A();ENDSEC;END-ISO-10303-21;",
|
||||
"ISO-10303-21;DATA;#1=A(1e999999.);ENDSEC;END-ISO-10303-21;",
|
||||
] {
|
||||
let _ = parse(t);
|
||||
}
|
||||
let deep = format!("ISO-10303-21;DATA;#1=A({}{});ENDSEC;END-ISO-10303-21;", "(".repeat(10_000), ")".repeat(10_000));
|
||||
assert!(parse(&deep).is_err());
|
||||
let dup = parse("ISO-10303-21;DATA;#1=A(1);#1=B();ENDSEC;END-ISO-10303-21;").unwrap();
|
||||
assert_eq!(dup.duplicates, vec![1]);
|
||||
assert_eq!(dup.get(1).unwrap().name(), "A");
|
||||
assert_eq!(decode_string(b"\\X\\E9"), "\u{e9}");
|
||||
assert_eq!(decode_string(b"a\\X2\\00E900E8\\X0\\b"), "a\u{e9}\u{e8}b");
|
||||
assert_eq!(decode_string(b"\\"), "\\");
|
||||
// One run per character, as `step_out` writes them (#2344).
|
||||
assert_eq!(decode_string(b"\\X2\\5706\\X0\\\\X2\\89D2\\X0\\20"), "\u{5706}\u{89d2}20");
|
||||
assert_eq!(decode_string(b"\\X4\\0001F600\\X0\\\\X2\\00E9\\X0\\\\S\\a"), "\u{1f600}\u{e9}\u{e1}");
|
||||
}
|
||||
let watch = crate::watch::current().map(Watched);
|
||||
solidcraft_step::p21::parse_with(text, watch.as_ref().map(|w| w as &dyn solidcraft_step::p21::Progress))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "solidcraft-step"
|
||||
description = "SolidCraft STEP (ISO 10303) exchange: the Part 21 exchange structure and Part 42 geometry as plain data, with no modelling backend"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,193 @@
|
||||
//! Points on STEP curves and surfaces, in STEP's own parameterisation (ISO 10303-42), so tests
|
||||
//! and the topology reader can check that vertices lie on their curves. `None` for a parameter
|
||||
//! the geometry is not defined at, or for geometry too inconsistent to evaluate.
|
||||
|
||||
use crate::geom::{BSplineCurve, BSplineSurface, Curve, P3, Surface, add, cross, dot, norm, scale, sub};
|
||||
|
||||
/// Every knot repeated by its multiplicity.
|
||||
fn expanded(knots: &[f64], mults: &[usize]) -> Option<Vec<f64>> {
|
||||
if knots.len() != mults.len() {
|
||||
return None;
|
||||
}
|
||||
let total = mults.iter().try_fold(0usize, |a, m| a.checked_add(*m))?;
|
||||
let mut v = Vec::with_capacity(total);
|
||||
for (k, m) in knots.iter().zip(mults) {
|
||||
v.extend(std::iter::repeat_n(*k, *m));
|
||||
}
|
||||
Some(v)
|
||||
}
|
||||
|
||||
/// Index `i` of the knot span `[U[i], U[i+1])` holding `t`, with `degree <= i < n` for `n`
|
||||
/// control points (the last span takes the domain's upper end).
|
||||
fn span(u: &[f64], degree: usize, n: usize, t: f64) -> Option<usize> {
|
||||
let lo = *u.get(degree)?;
|
||||
let hi = *u.get(n)?;
|
||||
if !(t >= lo - 1e-12 * (1.0 + lo.abs()) && t <= hi + 1e-12 * (1.0 + hi.abs())) {
|
||||
return None;
|
||||
}
|
||||
let mut i = degree;
|
||||
while i + 1 < n && *u.get(i + 1)? <= t {
|
||||
i += 1;
|
||||
}
|
||||
Some(i)
|
||||
}
|
||||
|
||||
/// De Boor on homogeneous points `[x·w, y·w, z·w, w]`.
|
||||
fn de_boor(u: &[f64], degree: usize, ctrl: &[[f64; 4]], t: f64) -> Option<[f64; 4]> {
|
||||
let k = span(u, degree, ctrl.len(), t)?;
|
||||
let mut d: Vec<[f64; 4]> = (0..=degree).map(|j| ctrl.get(j + k - degree).copied()).collect::<Option<_>>()?;
|
||||
for r in 1..=degree {
|
||||
for j in (r..=degree).rev() {
|
||||
let i = j + k - degree;
|
||||
let a = *u.get(i)?;
|
||||
let b = *u.get(i + 1 + degree - r)?;
|
||||
let alpha = if b - a > 0.0 { (t - a) / (b - a) } else { 0.0 };
|
||||
let p = *d.get(j - 1)?;
|
||||
let q = *d.get(j)?;
|
||||
*d.get_mut(j)? = [0, 1, 2, 3].map(|c| (1.0 - alpha) * p[c] + alpha * q[c]);
|
||||
}
|
||||
}
|
||||
d.get(degree).copied()
|
||||
}
|
||||
|
||||
fn from_homogeneous(h: [f64; 4]) -> Option<P3> {
|
||||
(h[3].abs() > 1e-300).then(|| [h[0] / h[3], h[1] / h[3], h[2] / h[3]]).filter(|p| p.iter().all(|x| x.is_finite()))
|
||||
}
|
||||
|
||||
fn homogeneous(p: P3, w: f64) -> [f64; 4] {
|
||||
[p[0] * w, p[1] * w, p[2] * w, w]
|
||||
}
|
||||
|
||||
impl BSplineCurve {
|
||||
/// Parameter range: from knot `degree` to knot `n` of the expanded knot vector.
|
||||
pub fn domain(&self) -> Option<(f64, f64)> {
|
||||
let u = expanded(&self.knots, &self.multiplicities)?;
|
||||
Some((*u.get(self.degree)?, *u.get(self.points.len())?))
|
||||
}
|
||||
pub fn point(&self, t: f64) -> Option<P3> {
|
||||
let u = expanded(&self.knots, &self.multiplicities)?;
|
||||
if u.len() != self.points.len() + self.degree + 1 {
|
||||
return None;
|
||||
}
|
||||
let ctrl: Vec<[f64; 4]> = match &self.weights {
|
||||
Some(w) if w.len() == self.points.len() => self.points.iter().zip(w).map(|(p, w)| homogeneous(*p, *w)).collect(),
|
||||
Some(_) => return None,
|
||||
None => self.points.iter().map(|p| homogeneous(*p, 1.0)).collect(),
|
||||
};
|
||||
from_homogeneous(de_boor(&u, self.degree, &ctrl, t)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl BSplineSurface {
|
||||
pub fn point(&self, s: f64, t: f64) -> Option<P3> {
|
||||
let uk = expanded(&self.u_knots, &self.u_multiplicities)?;
|
||||
let vk = expanded(&self.v_knots, &self.v_multiplicities)?;
|
||||
let nu = self.points.len();
|
||||
let nv = self.points.first()?.len();
|
||||
if uk.len() != nu + self.u_degree + 1 || vk.len() != nv + self.v_degree + 1 {
|
||||
return None;
|
||||
}
|
||||
// Each u row evaluated along v, then the column of results along u.
|
||||
let mut column = Vec::with_capacity(nu);
|
||||
for (i, row) in self.points.iter().enumerate() {
|
||||
if row.len() != nv {
|
||||
return None;
|
||||
}
|
||||
let ctrl: Vec<[f64; 4]> = match &self.weights {
|
||||
Some(w) => {
|
||||
let w = w.get(i).filter(|w| w.len() == nv)?;
|
||||
row.iter().zip(w).map(|(p, w)| homogeneous(*p, *w)).collect()
|
||||
}
|
||||
None => row.iter().map(|p| homogeneous(*p, 1.0)).collect(),
|
||||
};
|
||||
column.push(de_boor(&vk, self.v_degree, &ctrl, t)?);
|
||||
}
|
||||
from_homogeneous(de_boor(&uk, self.u_degree, &column, s)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Curve {
|
||||
/// The point at parameter `t`.
|
||||
pub fn point(&self, t: f64) -> Option<P3> {
|
||||
self.point_at(t, 0)
|
||||
}
|
||||
|
||||
fn point_at(&self, t: f64, depth: usize) -> Option<P3> {
|
||||
if depth > 16 || !t.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let p = match self {
|
||||
Curve::Line { origin, direction, magnitude } => add(*origin, scale(*direction, t * magnitude)),
|
||||
Curve::Circle { position, radius } => position.at(radius * t.cos(), radius * t.sin(), 0.0),
|
||||
Curve::Ellipse { position, semi_axis_1, semi_axis_2 } => position.at(semi_axis_1 * t.cos(), semi_axis_2 * t.sin(), 0.0),
|
||||
Curve::Hyperbola { position, semi_axis, semi_imag_axis } => position.at(semi_axis * t.cosh(), semi_imag_axis * t.sinh(), 0.0),
|
||||
Curve::Parabola { position, focal_dist } => position.at(focal_dist * t * t, 2.0 * focal_dist * t, 0.0),
|
||||
Curve::BSpline(b) => b.point(t)?,
|
||||
Curve::Polyline(pts) => {
|
||||
if pts.len() < 2 || t < 0.0 || t > (pts.len() - 1) as f64 {
|
||||
return None;
|
||||
}
|
||||
let i = (t.floor() as usize).min(pts.len() - 2);
|
||||
let (a, b) = (*pts.get(i)?, *pts.get(i + 1)?);
|
||||
add(a, scale(sub(b, a), t - i as f64))
|
||||
}
|
||||
Curve::Trimmed { basis, .. } => basis.point_at(t, depth + 1)?,
|
||||
Curve::SurfaceCurve { curve_3d, .. } => curve_3d.point_at(t, depth + 1)?,
|
||||
// Needs the basis curve's derivative; not evaluated yet.
|
||||
Curve::OffsetCurve3d { .. } => return None,
|
||||
};
|
||||
p.iter().all(|x| x.is_finite()).then_some(p)
|
||||
}
|
||||
}
|
||||
|
||||
impl Surface {
|
||||
/// The point at parameters `(u, v)`.
|
||||
pub fn point(&self, u: f64, v: f64) -> Option<P3> {
|
||||
self.point_at(u, v, 0)
|
||||
}
|
||||
|
||||
/// The unit normal `∂S/∂u × ∂S/∂v` at `(u, v)`, by central differences.
|
||||
pub fn normal(&self, u: f64, v: f64) -> Option<P3> {
|
||||
self.normal_at(u, v, 0)
|
||||
}
|
||||
|
||||
fn normal_at(&self, u: f64, v: f64, depth: usize) -> Option<P3> {
|
||||
let h = 1e-6;
|
||||
let du = sub(self.point_at(u + h, v, depth)?, self.point_at(u - h, v, depth)?);
|
||||
let dv = sub(self.point_at(u, v + h, depth)?, self.point_at(u, v - h, depth)?);
|
||||
let n = cross(du, dv);
|
||||
let m = norm(n);
|
||||
(m > 1e-30 && m.is_finite()).then(|| scale(n, 1.0 / m))
|
||||
}
|
||||
|
||||
fn point_at(&self, u: f64, v: f64, depth: usize) -> Option<P3> {
|
||||
if depth > 16 || !u.is_finite() || !v.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let p = match self {
|
||||
Surface::Plane { position } => position.at(u, v, 0.0),
|
||||
Surface::Cylinder { position, radius } => position.at(radius * u.cos(), radius * u.sin(), v),
|
||||
Surface::Cone { position, radius, semi_angle } => {
|
||||
let r = radius + v * semi_angle.tan();
|
||||
position.at(r * u.cos(), r * u.sin(), v)
|
||||
}
|
||||
Surface::Sphere { position, radius } => position.at(radius * v.cos() * u.cos(), radius * v.cos() * u.sin(), radius * v.sin()),
|
||||
Surface::Torus { position, major_radius, minor_radius } | Surface::DegenerateTorus { position, major_radius, minor_radius, .. } => {
|
||||
let r = major_radius + minor_radius * v.cos();
|
||||
position.at(r * u.cos(), r * u.sin(), minor_radius * v.sin())
|
||||
}
|
||||
Surface::BSpline(b) => b.point(u, v)?,
|
||||
Surface::Revolution { profile, axis } => {
|
||||
// Rodrigues: the profile point turned by u about the axis.
|
||||
let q = sub(profile.point_at(v, depth + 1)?, axis.origin);
|
||||
let k = axis.direction;
|
||||
let (s, c) = u.sin_cos();
|
||||
add(axis.origin, add(add(scale(q, c), scale(cross(k, q), s)), scale(k, dot(k, q) * (1.0 - c))))
|
||||
}
|
||||
Surface::Extrusion { profile, direction, magnitude } => add(profile.point_at(u, depth + 1)?, scale(*direction, v * magnitude)),
|
||||
Surface::Offset { basis, distance, .. } => add(basis.point_at(u, v, depth + 1)?, scale(basis.normal_at(u, v, depth + 1)?, *distance)),
|
||||
Surface::RectangularTrimmed { basis, .. } => basis.point_at(u, v, depth + 1)?,
|
||||
};
|
||||
p.iter().all(|x| x.is_finite()).then_some(p)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
//! STEP geometry (ISO 10303-42) as plain data: the curves and surfaces a file defines, exactly
|
||||
//! as it defines them, in millimetres and radians. Nothing is approximated, bounded or turned
|
||||
//! into another kind on the way in: a circle stays a circle with its placement, an unbounded
|
||||
//! cylinder stays unbounded, and a B-spline keeps its own knots and form. Every curve and surface
|
||||
//! keeps STEP's own parameterisation, so a 2D curve in a surface's parameter space means the same
|
||||
//! thing here as in the file (SMLib's `SmSTEPSurface` evaluates the same parameterisation, with
|
||||
//! angles in degrees where we use radians).
|
||||
//!
|
||||
//! Entities the file shares (one surface under many edges' pcurves) are shared here too, through
|
||||
//! `Arc`, so reading is linear in the file however often an entity is referenced.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A point or vector.
|
||||
pub type P3 = [f64; 3];
|
||||
|
||||
/// AXIS2_PLACEMENT_3D (or _2D, with `axis` = +Z): an origin and two orthogonal unit directions.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Placement {
|
||||
pub origin: P3,
|
||||
/// Local Z.
|
||||
pub axis: P3,
|
||||
/// Local X (`ref_direction`, made orthogonal to `axis`).
|
||||
pub ref_direction: P3,
|
||||
}
|
||||
|
||||
impl Placement {
|
||||
/// Local Y (`axis` × `ref_direction`).
|
||||
pub fn y(&self) -> P3 {
|
||||
cross(self.axis, self.ref_direction)
|
||||
}
|
||||
/// `origin + x·X + y·Y + z·Z`.
|
||||
pub fn at(&self, x: f64, y: f64, z: f64) -> P3 {
|
||||
let yy = self.y();
|
||||
add(self.origin, add(scale(self.ref_direction, x), add(scale(yy, y), scale(self.axis, z))))
|
||||
}
|
||||
}
|
||||
|
||||
/// AXIS1_PLACEMENT: an origin and a unit direction.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Axis1 {
|
||||
pub origin: P3,
|
||||
pub direction: P3,
|
||||
}
|
||||
|
||||
/// A STEP LOGICAL.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Logical {
|
||||
True,
|
||||
False,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// `b_spline_curve_form`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum CurveForm {
|
||||
PolylineForm,
|
||||
CircularArc,
|
||||
EllipticArc,
|
||||
ParabolicArc,
|
||||
HyperbolicArc,
|
||||
Unspecified,
|
||||
}
|
||||
|
||||
/// `b_spline_surface_form`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SurfaceForm {
|
||||
PlaneSurf,
|
||||
CylindricalSurf,
|
||||
ConicalSurf,
|
||||
SphericalSurf,
|
||||
ToroidalSurf,
|
||||
SurfOfRevolution,
|
||||
RuledSurf,
|
||||
GeneralisedCone,
|
||||
QuadricSurf,
|
||||
SurfOfLinearExtrusion,
|
||||
Unspecified,
|
||||
}
|
||||
|
||||
/// `knot_type`. A curve given without knots (BEZIER_CURVE, UNIFORM_CURVE, QUASI_UNIFORM_CURVE)
|
||||
/// has them worked out on reading, and keeps the matching type here.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum KnotSpec {
|
||||
UniformKnots,
|
||||
QuasiUniformKnots,
|
||||
PiecewiseBezierKnots,
|
||||
Unspecified,
|
||||
}
|
||||
|
||||
/// A B-spline curve: distinct knots with their multiplicities, as STEP writes them.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BSplineCurve {
|
||||
pub degree: usize,
|
||||
pub points: Vec<P3>,
|
||||
/// Present for a rational curve, one positive weight per point.
|
||||
pub weights: Option<Vec<f64>>,
|
||||
pub knots: Vec<f64>,
|
||||
pub multiplicities: Vec<usize>,
|
||||
pub form: CurveForm,
|
||||
pub closed: Logical,
|
||||
pub self_intersect: Logical,
|
||||
pub knot_spec: KnotSpec,
|
||||
}
|
||||
|
||||
/// A B-spline surface; `points[i][j]` runs along u with `i`, along v with `j`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BSplineSurface {
|
||||
pub u_degree: usize,
|
||||
pub v_degree: usize,
|
||||
pub points: Vec<Vec<P3>>,
|
||||
pub weights: Option<Vec<Vec<f64>>>,
|
||||
pub u_knots: Vec<f64>,
|
||||
pub u_multiplicities: Vec<usize>,
|
||||
pub v_knots: Vec<f64>,
|
||||
pub v_multiplicities: Vec<usize>,
|
||||
pub form: SurfaceForm,
|
||||
pub u_closed: Logical,
|
||||
pub v_closed: Logical,
|
||||
pub self_intersect: Logical,
|
||||
pub knot_spec: KnotSpec,
|
||||
}
|
||||
|
||||
/// One end of a TRIMMED_CURVE: a point on the basis curve, its parameter, or both.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Trim {
|
||||
Point(P3),
|
||||
Parameter(f64),
|
||||
}
|
||||
|
||||
/// `trimming_preference`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TrimPreference {
|
||||
Cartesian,
|
||||
Parameter,
|
||||
Unspecified,
|
||||
}
|
||||
|
||||
/// `preferred_surface_curve_representation`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PreferredRep {
|
||||
Curve3d,
|
||||
PcurveS1,
|
||||
PcurveS2,
|
||||
}
|
||||
|
||||
/// Which SURFACE_CURVE subtype.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SurfaceCurveKind {
|
||||
Surface,
|
||||
Seam,
|
||||
Intersection,
|
||||
BoundedSurface,
|
||||
}
|
||||
|
||||
/// PCURVE: a 2D curve in the parameter space of `surface`. Its coordinates are the surface's
|
||||
/// own parameters (lengths in mm, angles in radians).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Pcurve {
|
||||
pub surface: Arc<Surface>,
|
||||
/// A 2D curve: every point and placement has z = 0 and every placement axis is +Z.
|
||||
pub curve: Arc<Curve>,
|
||||
}
|
||||
|
||||
/// An item of a SURFACE_CURVE's `associated_geometry`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Associated {
|
||||
Pcurve(Pcurve),
|
||||
Surface(Arc<Surface>),
|
||||
}
|
||||
|
||||
/// A STEP curve.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Curve {
|
||||
/// `origin + t·magnitude·direction`.
|
||||
Line {
|
||||
origin: P3,
|
||||
direction: P3,
|
||||
magnitude: f64,
|
||||
},
|
||||
/// Parameter: the angle from `ref_direction`.
|
||||
Circle {
|
||||
position: Placement,
|
||||
radius: f64,
|
||||
},
|
||||
Ellipse {
|
||||
position: Placement,
|
||||
semi_axis_1: f64,
|
||||
semi_axis_2: f64,
|
||||
},
|
||||
/// `origin + a·cosh t·X + b·sinh t·Y`.
|
||||
Hyperbola {
|
||||
position: Placement,
|
||||
semi_axis: f64,
|
||||
semi_imag_axis: f64,
|
||||
},
|
||||
/// `origin + f·t²·X + 2f·t·Y`.
|
||||
Parabola {
|
||||
position: Placement,
|
||||
focal_dist: f64,
|
||||
},
|
||||
BSpline(Box<BSplineCurve>),
|
||||
/// Segment `i` (from 0) is the parameters `[i, i + 1]`.
|
||||
Polyline(Vec<P3>),
|
||||
Trimmed {
|
||||
basis: Arc<Curve>,
|
||||
trim_1: Vec<Trim>,
|
||||
trim_2: Vec<Trim>,
|
||||
sense: bool,
|
||||
preference: TrimPreference,
|
||||
},
|
||||
SurfaceCurve {
|
||||
kind: SurfaceCurveKind,
|
||||
curve_3d: Arc<Curve>,
|
||||
associated: Vec<Associated>,
|
||||
master: PreferredRep,
|
||||
},
|
||||
OffsetCurve3d {
|
||||
basis: Arc<Curve>,
|
||||
distance: f64,
|
||||
self_intersect: Logical,
|
||||
ref_direction: P3,
|
||||
},
|
||||
}
|
||||
|
||||
/// A STEP surface.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Surface {
|
||||
/// `origin + u·X + v·Y`.
|
||||
Plane {
|
||||
position: Placement,
|
||||
},
|
||||
/// `origin + r·(cos u·X + sin u·Y) + v·Z`.
|
||||
Cylinder {
|
||||
position: Placement,
|
||||
radius: f64,
|
||||
},
|
||||
/// `origin + (r + v·tan a)·(cos u·X + sin u·Y) + v·Z`.
|
||||
Cone {
|
||||
position: Placement,
|
||||
radius: f64,
|
||||
semi_angle: f64,
|
||||
},
|
||||
/// `origin + r·cos v·(cos u·X + sin u·Y) + r·sin v·Z`.
|
||||
Sphere {
|
||||
position: Placement,
|
||||
radius: f64,
|
||||
},
|
||||
/// `origin + (R + r·cos v)·(cos u·X + sin u·Y) + r·sin v·Z`.
|
||||
Torus {
|
||||
position: Placement,
|
||||
major_radius: f64,
|
||||
minor_radius: f64,
|
||||
},
|
||||
/// A torus whose minor radius reaches the axis; `select_outer` keeps the outer part.
|
||||
DegenerateTorus {
|
||||
position: Placement,
|
||||
major_radius: f64,
|
||||
minor_radius: f64,
|
||||
select_outer: bool,
|
||||
},
|
||||
BSpline(Box<BSplineSurface>),
|
||||
/// The profile at `v` turned by the angle `u` about the axis.
|
||||
Revolution {
|
||||
profile: Arc<Curve>,
|
||||
axis: Axis1,
|
||||
},
|
||||
/// `profile(u) + v·magnitude·direction`.
|
||||
Extrusion {
|
||||
profile: Arc<Curve>,
|
||||
direction: P3,
|
||||
magnitude: f64,
|
||||
},
|
||||
Offset {
|
||||
basis: Arc<Surface>,
|
||||
distance: f64,
|
||||
self_intersect: Logical,
|
||||
},
|
||||
RectangularTrimmed {
|
||||
basis: Arc<Surface>,
|
||||
u1: f64,
|
||||
u2: f64,
|
||||
v1: f64,
|
||||
v2: f64,
|
||||
u_sense: bool,
|
||||
v_sense: bool,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) fn add(a: P3, b: P3) -> P3 {
|
||||
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
|
||||
}
|
||||
pub(crate) fn sub(a: P3, b: P3) -> P3 {
|
||||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
||||
}
|
||||
pub(crate) fn scale(a: P3, s: f64) -> P3 {
|
||||
[a[0] * s, a[1] * s, a[2] * s]
|
||||
}
|
||||
pub(crate) fn dot(a: P3, b: P3) -> f64 {
|
||||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
}
|
||||
pub(crate) fn cross(a: P3, b: P3) -> P3 {
|
||||
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
||||
}
|
||||
pub(crate) fn norm(a: P3) -> f64 {
|
||||
dot(a, a).sqrt()
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
//! STEP geometry, one entity at a time: reading (values, units, evaluation in STEP's own
|
||||
//! parameterisation), writing, and read → write → read round trips. Expected values are
|
||||
//! analytic.
|
||||
|
||||
use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, PI};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::geom::{Associated, Curve, CurveForm, KnotSpec, Logical, P3, PreferredRep, Surface, SurfaceCurveKind, Trim};
|
||||
use crate::p21::{self, Exchange};
|
||||
use crate::read::{Reader, Units};
|
||||
use crate::write::{GeomWriter, Writer, real};
|
||||
|
||||
const MM: Units = Units { len: 1.0, ang: 1.0 };
|
||||
|
||||
fn file(data: &str) -> Exchange {
|
||||
let t = format!("ISO-10303-21;\nHEADER;FILE_SCHEMA(('AUTOMOTIVE_DESIGN'));ENDSEC;\nDATA;\n{data}\nENDSEC;\nEND-ISO-10303-21;\n");
|
||||
match p21::parse(&t) {
|
||||
Ok(ex) => ex,
|
||||
Err(e) => panic!("{e}\n{t}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Common entities: #1 origin, #2 +Z, #3 +X, #4 the world placement, #5 +Y.
|
||||
const BASE: &str = "#1=CARTESIAN_POINT('',(0.,0.,0.));#2=DIRECTION('',(0.,0.,1.));#3=DIRECTION('',(1.,0.,0.));\
|
||||
#4=AXIS2_PLACEMENT_3D('',#1,#2,#3);#5=DIRECTION('',(0.,1.,0.));";
|
||||
|
||||
fn curve(data: &str, id: u64, units: Units) -> Arc<Curve> {
|
||||
let ex = file(&format!("{BASE}{data}"));
|
||||
match Reader::new(&ex, units).curve(id) {
|
||||
Ok(c) => c,
|
||||
Err(e) => panic!("{e}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn surface(data: &str, id: u64, units: Units) -> Arc<Surface> {
|
||||
let ex = file(&format!("{BASE}{data}"));
|
||||
match Reader::new(&ex, units).surface(id) {
|
||||
Ok(s) => s,
|
||||
Err(e) => panic!("{e}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn close(a: P3, b: P3) -> bool {
|
||||
a.iter().zip(b).all(|(x, y)| (x - y).abs() <= 1e-9 * (1.0 + y.abs()))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn at_curve(c: &Curve, t: f64, want: P3) {
|
||||
let got = c.point(t);
|
||||
assert!(got.is_some_and(|g| close(g, want)), "at {t}: got {got:?}, want {want:?}");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn at_surface(s: &Surface, u: f64, v: f64, want: P3) {
|
||||
let got = s.point(u, v);
|
||||
assert!(got.is_some_and(|g| close(g, want)), "at ({u}, {v}): got {got:?}, want {want:?}");
|
||||
}
|
||||
|
||||
/// Writes a curve or surface, reads it back, writes it again: the second file must equal the
|
||||
/// first, and the geometry read back must evaluate like the original.
|
||||
fn round_trip_curve(c: &Curve, params: &[f64]) {
|
||||
let mut w = Writer::new();
|
||||
let id = GeomWriter::new(&mut w).curve(c);
|
||||
let text = w.file("t", "AUTOMOTIVE_DESIGN");
|
||||
let ex = p21::parse(&text).unwrap_or_else(|e| panic!("{e}\n{text}"));
|
||||
let back = Reader::new(&ex, MM).curve(id).unwrap_or_else(|e| panic!("{e}\n{text}"));
|
||||
assert_eq!(std::mem::discriminant(c), std::mem::discriminant(&*back), "{text}");
|
||||
for &t in params {
|
||||
let (a, b) = (c.point(t), back.point(t));
|
||||
assert!(matches!((a, b), (Some(a), Some(b)) if close(a, b)) || (a.is_none() && b.is_none()), "at {t}: {a:?} vs {b:?}\n{text}");
|
||||
}
|
||||
let mut w2 = Writer::new();
|
||||
GeomWriter::new(&mut w2).curve(&back);
|
||||
assert_eq!(w2.data(), w.data(), "a second write differs");
|
||||
}
|
||||
|
||||
fn round_trip_surface(s: &Surface, params: &[(f64, f64)]) {
|
||||
let mut w = Writer::new();
|
||||
let id = GeomWriter::new(&mut w).surface(s);
|
||||
let text = w.file("t", "AUTOMOTIVE_DESIGN");
|
||||
let ex = p21::parse(&text).unwrap_or_else(|e| panic!("{e}\n{text}"));
|
||||
let back = Reader::new(&ex, MM).surface(id).unwrap_or_else(|e| panic!("{e}\n{text}"));
|
||||
assert_eq!(std::mem::discriminant(s), std::mem::discriminant(&*back), "{text}");
|
||||
for &(u, v) in params {
|
||||
let (a, b) = (s.point(u, v), back.point(u, v));
|
||||
assert!(matches!((a, b), (Some(a), Some(b)) if close(a, b)), "at ({u}, {v}): {a:?} vs {b:?}\n{text}");
|
||||
}
|
||||
let mut w2 = Writer::new();
|
||||
GeomWriter::new(&mut w2).surface(&back);
|
||||
assert_eq!(w2.data(), w.data(), "a second write differs");
|
||||
}
|
||||
|
||||
const TS: [f64; 5] = [0.0, 0.3, 1.0, FRAC_PI_2, 2.5];
|
||||
const UVS: [(f64, f64); 4] = [(0.0, 0.0), (0.4, 0.2), (FRAC_PI_2, 1.0), (2.0, -0.5)];
|
||||
|
||||
#[test]
|
||||
fn line() {
|
||||
let c = curve("#10=CARTESIAN_POINT('',(1.,2.,3.));#11=VECTOR('',#3,5.);#12=LINE('',#10,#11);", 12, MM);
|
||||
assert!(matches!(*c, Curve::Line { magnitude, .. } if magnitude == 5.0));
|
||||
at_curve(&c, 2.0, [11.0, 2.0, 3.0]);
|
||||
round_trip_curve(&c, &TS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circle_and_ellipse() {
|
||||
let c = curve("#10=CIRCLE('',#4,10.);", 10, MM);
|
||||
at_curve(&c, 0.0, [10.0, 0.0, 0.0]);
|
||||
at_curve(&c, FRAC_PI_2, [0.0, 10.0, 0.0]);
|
||||
round_trip_curve(&c, &TS);
|
||||
let e = curve("#10=ELLIPSE('',#4,10.,4.);", 10, MM);
|
||||
at_curve(&e, FRAC_PI_2, [0.0, 4.0, 0.0]);
|
||||
at_curve(&e, PI, [-10.0, 0.0, 0.0]);
|
||||
round_trip_curve(&e, &TS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyperbola_and_parabola() {
|
||||
let h = curve("#10=HYPERBOLA('',#4,2.,3.);", 10, MM);
|
||||
at_curve(&h, 1.0, [2.0 * 1f64.cosh(), 3.0 * 1f64.sinh(), 0.0]);
|
||||
round_trip_curve(&h, &TS);
|
||||
let p = curve("#10=PARABOLA('',#4,0.5);", 10, MM);
|
||||
at_curve(&p, 2.0, [2.0, 2.0, 0.0]);
|
||||
round_trip_curve(&p, &TS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bspline_curve_with_knots() {
|
||||
// A quadratic Bézier from (0,0) through control (1,2) to (2,0): its middle is (1, 1).
|
||||
let c = curve(
|
||||
"#10=CARTESIAN_POINT('',(0.,0.,0.));#11=CARTESIAN_POINT('',(1.,2.,0.));#12=CARTESIAN_POINT('',(2.,0.,0.));\
|
||||
#13=B_SPLINE_CURVE_WITH_KNOTS('',2,(#10,#11,#12),.UNSPECIFIED.,.F.,.F.,(3,3),(0.,1.),.PIECEWISE_BEZIER_KNOTS.);",
|
||||
13,
|
||||
MM,
|
||||
);
|
||||
let Curve::BSpline(b) = &*c else { panic!("{c:?}") };
|
||||
assert_eq!((b.degree, b.multiplicities.clone(), b.knot_spec), (2, vec![3, 3], KnotSpec::PiecewiseBezierKnots));
|
||||
assert_eq!(b.domain(), Some((0.0, 1.0)));
|
||||
at_curve(&c, 0.5, [1.0, 1.0, 0.0]);
|
||||
round_trip_curve(&c, &[0.0, 0.25, 0.5, 1.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rational_bspline_curve_is_an_exact_quarter_circle() {
|
||||
let w = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let c = curve(
|
||||
&format!(
|
||||
"#10=CARTESIAN_POINT('',(1.,0.,0.));#11=CARTESIAN_POINT('',(1.,1.,0.));#12=CARTESIAN_POINT('',(0.,1.,0.));\
|
||||
#13=(BOUNDED_CURVE()B_SPLINE_CURVE(2,(#10,#11,#12),.CIRCULAR_ARC.,.F.,.F.)B_SPLINE_CURVE_WITH_KNOTS((3,3),(0.,1.),.UNSPECIFIED.)\
|
||||
CURVE()GEOMETRIC_REPRESENTATION_ITEM()RATIONAL_B_SPLINE_CURVE((1.,{},1.))REPRESENTATION_ITEM(''));",
|
||||
real(w)
|
||||
),
|
||||
13,
|
||||
MM,
|
||||
);
|
||||
let Curve::BSpline(b) = &*c else { panic!("{c:?}") };
|
||||
assert_eq!(b.form, CurveForm::CircularArc);
|
||||
assert!(b.weights.is_some());
|
||||
for t in [0.1, 0.5, 0.9] {
|
||||
let p = c.point(t).unwrap_or_default();
|
||||
assert!((p[0].hypot(p[1]) - 1.0).abs() < 1e-12, "{p:?}");
|
||||
}
|
||||
round_trip_curve(&c, &[0.0, 0.3, 0.7, 1.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implied_knots_and_repeated_knots() {
|
||||
let pts = "#10=CARTESIAN_POINT('',(0.,0.,0.));#11=CARTESIAN_POINT('',(1.,1.,0.));#12=CARTESIAN_POINT('',(2.,0.,0.));#13=CARTESIAN_POINT('',(3.,1.,0.));";
|
||||
let q = curve(&format!("{pts}#14=QUASI_UNIFORM_CURVE('',2,(#10,#11,#12,#13),.UNSPECIFIED.,.F.,.F.);"), 14, MM);
|
||||
let Curve::BSpline(b) = &*q else { panic!("{q:?}") };
|
||||
assert_eq!((b.knots.clone(), b.multiplicities.clone()), (vec![0.0, 1.0, 2.0], vec![3, 1, 3]));
|
||||
at_curve(&q, 0.0, [0.0, 0.0, 0.0]);
|
||||
at_curve(&q, 2.0, [3.0, 1.0, 0.0]);
|
||||
let bez = curve(&format!("{pts}#14=BEZIER_CURVE('',3,(#10,#11,#12,#13),.UNSPECIFIED.,.F.,.F.);"), 14, MM);
|
||||
let Curve::BSpline(b) = &*bez else { panic!("{bez:?}") };
|
||||
assert_eq!((b.knots.clone(), b.multiplicities.clone()), (vec![0.0, 1.0], vec![4, 4]));
|
||||
// A knot written twice is one knot.
|
||||
let rep = curve(
|
||||
&format!("{pts}#14=B_SPLINE_CURVE_WITH_KNOTS('',2,(#10,#11,#12,#13),.UNSPECIFIED.,.F.,.F.,(3,1,1,2),(0.,1.,1.,2.),.UNSPECIFIED.);"),
|
||||
14,
|
||||
MM,
|
||||
);
|
||||
let Curve::BSpline(b) = &*rep else { panic!("{rep:?}") };
|
||||
assert_eq!((b.knots.clone(), b.multiplicities.clone()), (vec![0.0, 1.0, 2.0], vec![3, 2, 2]));
|
||||
assert_eq!(b.domain(), Some((0.0, 1.0)));
|
||||
round_trip_curve(&rep, &[0.0, 0.5, 1.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polyline_and_trimmed_curve() {
|
||||
let pl = curve(
|
||||
"#10=CARTESIAN_POINT('',(0.,0.,0.));#11=CARTESIAN_POINT('',(4.,0.,0.));#12=CARTESIAN_POINT('',(4.,2.,0.));#13=POLYLINE('',(#10,#11,#12));",
|
||||
13,
|
||||
MM,
|
||||
);
|
||||
at_curve(&pl, 0.5, [2.0, 0.0, 0.0]);
|
||||
at_curve(&pl, 1.5, [4.0, 1.0, 0.0]);
|
||||
round_trip_curve(&pl, &[0.0, 0.5, 1.0, 1.5, 2.0]);
|
||||
// A circle trimmed in degrees: the parameters become radians.
|
||||
let deg = Units { len: 1.0, ang: PI / 180.0 };
|
||||
let t = curve(
|
||||
"#10=CIRCLE('',#4,10.);#11=CARTESIAN_POINT('',(10.,0.,0.));#12=TRIMMED_CURVE('',#10,(#11,PARAMETER_VALUE(0.)),(PARAMETER_VALUE(90.)),.T.,.PARAMETER.);",
|
||||
12,
|
||||
deg,
|
||||
);
|
||||
let Curve::Trimmed { trim_1, trim_2, sense, .. } = &*t else { panic!("{t:?}") };
|
||||
assert_eq!(trim_1.first(), Some(&Trim::Point([10.0, 0.0, 0.0])));
|
||||
assert!(matches!(trim_2.first(), Some(Trim::Parameter(a)) if (a - FRAC_PI_2).abs() < 1e-15));
|
||||
assert!(*sense);
|
||||
round_trip_curve(&t, &TS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surface_curve_with_pcurves_shares_its_surface() {
|
||||
// A seam on a cylinder of radius 5: the line x = 5 with pcurves u = 0 on both sides.
|
||||
let c = curve(
|
||||
"#10=CYLINDRICAL_SURFACE('',#4,5.);#11=CARTESIAN_POINT('',(5.,0.,0.));#12=VECTOR('',#2,1.);#13=LINE('',#11,#12);\
|
||||
#20=CARTESIAN_POINT('',(0.,0.));#21=DIRECTION('',(0.,1.));#22=VECTOR('',#21,1.);#23=LINE('',#20,#22);\
|
||||
#24=(GEOMETRIC_REPRESENTATION_CONTEXT(2)PARAMETRIC_REPRESENTATION_CONTEXT()REPRESENTATION_CONTEXT('2D SPACE',''));\
|
||||
#25=DEFINITIONAL_REPRESENTATION('',(#23),#24);#26=PCURVE('',#10,#25);\
|
||||
#27=CARTESIAN_POINT('',(6.28318530717959,0.));#28=LINE('',#27,#22);#29=DEFINITIONAL_REPRESENTATION('',(#28),#24);#30=PCURVE('',#10,#29);\
|
||||
#31=SEAM_CURVE('',#13,(#26,#30),.PCURVE_S1.);",
|
||||
31,
|
||||
MM,
|
||||
);
|
||||
let Curve::SurfaceCurve { kind, associated, master, curve_3d } = &*c else { panic!("{c:?}") };
|
||||
assert_eq!((*kind, *master), (SurfaceCurveKind::Seam, PreferredRep::PcurveS1));
|
||||
at_curve(curve_3d, 3.0, [5.0, 0.0, 3.0]);
|
||||
let [Associated::Pcurve(a), Associated::Pcurve(b)] = associated.as_slice() else { panic!("{associated:?}") };
|
||||
assert!(Arc::ptr_eq(&a.surface, &b.surface), "one surface entity, read once");
|
||||
// Each pcurve, lifted onto the surface, is the 3D curve.
|
||||
for t in [0.0, 2.0] {
|
||||
for pc in [a, b] {
|
||||
let uv = pc.curve.point(t).unwrap_or_default();
|
||||
let p = pc.surface.point(uv[0], uv[1]).unwrap_or_default();
|
||||
assert!(close(p, curve_3d.point(t).unwrap_or_default()), "{p:?}");
|
||||
}
|
||||
}
|
||||
round_trip_curve(&c, &[0.0, 1.0, 4.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcurves_scale_with_the_surface_parameters() {
|
||||
// Inches and degrees: a cylinder pcurve's u is an angle, v a length.
|
||||
let units = Units { len: 25.4, ang: PI / 180.0 };
|
||||
let c = curve(
|
||||
"#10=CYLINDRICAL_SURFACE('',#4,1.);#11=CARTESIAN_POINT('',(1.,0.,0.));#12=VECTOR('',#2,1.);#13=LINE('',#11,#12);\
|
||||
#20=CARTESIAN_POINT('',(90.,0.));#21=DIRECTION('',(0.,1.));#22=VECTOR('',#21,1.);#23=LINE('',#20,#22);\
|
||||
#24=(GEOMETRIC_REPRESENTATION_CONTEXT(2)PARAMETRIC_REPRESENTATION_CONTEXT()REPRESENTATION_CONTEXT('2D SPACE',''));\
|
||||
#25=DEFINITIONAL_REPRESENTATION('',(#23),#24);#26=PCURVE('',#10,#25);#27=SURFACE_CURVE('',#13,(#26),.CURVE_3D.);",
|
||||
27,
|
||||
units,
|
||||
);
|
||||
let Curve::SurfaceCurve { associated, .. } = &*c else { panic!("{c:?}") };
|
||||
let Some(Associated::Pcurve(pc)) = associated.first() else { panic!("{associated:?}") };
|
||||
let uv = pc.curve.point(2.0).unwrap_or_default();
|
||||
assert!((uv[0] - FRAC_PI_2).abs() < 1e-12 && (uv[1] - 2.0 * 25.4).abs() < 1e-9, "{uv:?}");
|
||||
at_surface(&pc.surface, uv[0], uv[1], [0.0, 25.4, 50.8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analytic_surfaces() {
|
||||
let p = surface("#10=PLANE('',#4);", 10, MM);
|
||||
at_surface(&p, 2.0, 3.0, [2.0, 3.0, 0.0]);
|
||||
round_trip_surface(&p, &UVS);
|
||||
let c = surface("#10=CYLINDRICAL_SURFACE('',#4,5.);", 10, MM);
|
||||
at_surface(&c, FRAC_PI_2, 7.0, [0.0, 5.0, 7.0]);
|
||||
round_trip_surface(&c, &UVS);
|
||||
let k = surface("#10=CONICAL_SURFACE('',#4,5.,0.785398163397448);", 10, MM);
|
||||
at_surface(&k, 0.0, 2.0, [7.0, 0.0, 2.0]);
|
||||
round_trip_surface(&k, &UVS);
|
||||
let s = surface("#10=SPHERICAL_SURFACE('',#4,3.);", 10, MM);
|
||||
at_surface(&s, 0.0, FRAC_PI_2, [0.0, 0.0, 3.0]);
|
||||
at_surface(&s, PI, 0.0, [-3.0, 0.0, 0.0]);
|
||||
round_trip_surface(&s, &UVS);
|
||||
let t = surface("#10=TOROIDAL_SURFACE('',#4,10.,2.);", 10, MM);
|
||||
at_surface(&t, FRAC_PI_2, PI, [0.0, 8.0, 0.0]);
|
||||
round_trip_surface(&t, &UVS);
|
||||
let d = surface("#10=DEGENERATE_TOROIDAL_SURFACE('',#4,1.,2.,.T.);", 10, MM);
|
||||
assert!(matches!(*d, Surface::DegenerateTorus { select_outer: true, .. }));
|
||||
round_trip_surface(&d, &UVS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn units_scale_lengths_and_angles() {
|
||||
let units = Units { len: 25.4, ang: PI / 180.0 };
|
||||
let k = surface("#10=CONICAL_SURFACE('',#4,1.,45.);", 10, units);
|
||||
let Surface::Cone { radius, semi_angle, .. } = *k else { panic!("{k:?}") };
|
||||
assert!((radius - 25.4).abs() < 1e-12 && (semi_angle - FRAC_PI_4).abs() < 1e-15);
|
||||
let c = curve("#10=CARTESIAN_POINT('',(1.,0.,0.));#11=VECTOR('',#3,2.);#12=LINE('',#10,#11);", 12, units);
|
||||
at_curve(&c, 1.0, [76.2, 0.0, 0.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placement_axes_are_made_orthonormal() {
|
||||
// A reference direction not perpendicular to the axis is projected; the axis is unit.
|
||||
let s = surface("#10=DIRECTION('',(0.,0.,2.));#11=DIRECTION('',(1.,0.,1.));#12=AXIS2_PLACEMENT_3D('',#1,#10,#11);#13=PLANE('',#12);", 13, MM);
|
||||
let Surface::Plane { position } = *s else { panic!("{s:?}") };
|
||||
assert_eq!((position.axis, position.ref_direction), ([0.0, 0.0, 1.0], [1.0, 0.0, 0.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bspline_surfaces() {
|
||||
// A bilinear patch over the unit square, then the same as a rational complex entity.
|
||||
let pts = "#10=CARTESIAN_POINT('',(0.,0.,0.));#11=CARTESIAN_POINT('',(0.,1.,0.));#12=CARTESIAN_POINT('',(1.,0.,0.));#13=CARTESIAN_POINT('',(1.,1.,1.));";
|
||||
let s = surface(
|
||||
&format!(
|
||||
"{pts}#14=B_SPLINE_SURFACE_WITH_KNOTS('',1,1,((#10,#11),(#12,#13)),.UNSPECIFIED.,.F.,.F.,.F.,(2,2),(2,2),(0.,1.),(0.,1.),.UNSPECIFIED.);"
|
||||
),
|
||||
14,
|
||||
MM,
|
||||
);
|
||||
at_surface(&s, 0.5, 0.5, [0.5, 0.5, 0.25]);
|
||||
at_surface(&s, 1.0, 0.0, [1.0, 0.0, 0.0]);
|
||||
round_trip_surface(&s, &[(0.0, 0.0), (0.3, 0.7), (1.0, 1.0)]);
|
||||
let r = surface(
|
||||
&format!(
|
||||
"{pts}#14=(BOUNDED_SURFACE()B_SPLINE_SURFACE(1,1,((#10,#11),(#12,#13)),.UNSPECIFIED.,.F.,.F.,.F.)\
|
||||
B_SPLINE_SURFACE_WITH_KNOTS((2,2),(2,2),(0.,1.),(0.,1.),.UNSPECIFIED.)GEOMETRIC_REPRESENTATION_ITEM()\
|
||||
RATIONAL_B_SPLINE_SURFACE(((1.,1.),(1.,1.)))REPRESENTATION_ITEM('')SURFACE());"
|
||||
),
|
||||
14,
|
||||
MM,
|
||||
);
|
||||
let Surface::BSpline(b) = &*r else { panic!("{r:?}") };
|
||||
assert!(b.weights.is_some());
|
||||
at_surface(&r, 0.5, 0.5, [0.5, 0.5, 0.25]);
|
||||
round_trip_surface(&r, &[(0.0, 0.0), (0.3, 0.7), (1.0, 1.0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swept_surfaces() {
|
||||
// A circle of radius 2 about the Z axis, centred at x = 10: revolved, a torus.
|
||||
let rev = surface(
|
||||
"#10=CARTESIAN_POINT('',(10.,0.,0.));#11=AXIS2_PLACEMENT_3D('',#10,#5,#3);#12=CIRCLE('',#11,2.);\
|
||||
#13=AXIS1_PLACEMENT('',#1,#2);#14=SURFACE_OF_REVOLUTION('',#12,#13);",
|
||||
14,
|
||||
MM,
|
||||
);
|
||||
at_surface(&rev, 0.0, 0.0, [12.0, 0.0, 0.0]);
|
||||
at_surface(&rev, FRAC_PI_2, 0.0, [0.0, 12.0, 0.0]);
|
||||
round_trip_surface(&rev, &UVS);
|
||||
let ext = surface("#10=CIRCLE('',#4,3.);#11=VECTOR('',#2,4.);#12=SURFACE_OF_LINEAR_EXTRUSION('',#10,#11);", 12, MM);
|
||||
at_surface(&ext, FRAC_PI_2, 0.5, [0.0, 3.0, 2.0]);
|
||||
round_trip_surface(&ext, &UVS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_and_trimmed_surfaces() {
|
||||
let o = surface("#10=CYLINDRICAL_SURFACE('',#4,5.);#11=OFFSET_SURFACE('',#10,1.5,.F.);", 11, MM);
|
||||
at_surface(&o, 0.0, 1.0, [6.5, 0.0, 1.0]);
|
||||
round_trip_surface(&o, &UVS);
|
||||
let deg = Units { len: 1.0, ang: PI / 180.0 };
|
||||
let t = surface("#10=CYLINDRICAL_SURFACE('',#4,5.);#11=RECTANGULAR_TRIMMED_SURFACE('',#10,0.,90.,0.,10.,.T.,.T.);", 11, deg);
|
||||
let Surface::RectangularTrimmed { u2, v2, .. } = *t else { panic!("{t:?}") };
|
||||
assert!((u2 - FRAC_PI_2).abs() < 1e-15 && v2 == 10.0, "{u2} {v2}");
|
||||
round_trip_surface(&t, &UVS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_curve_reads_and_round_trips() {
|
||||
let c = curve("#10=CIRCLE('',#4,10.);#11=OFFSET_CURVE_3D('',#10,1.,.F.,#2);", 11, MM);
|
||||
assert!(matches!(*c, Curve::OffsetCurve3d { distance, self_intersect: Logical::False, .. } if distance == 1.0));
|
||||
round_trip_curve(&c, &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_entities_are_written_once() {
|
||||
let c = Arc::new(Curve::Circle {
|
||||
position: crate::geom::Placement { origin: [0.0; 3], axis: [0.0, 0.0, 1.0], ref_direction: [1.0, 0.0, 0.0] },
|
||||
radius: 1.0,
|
||||
});
|
||||
let mut w = Writer::new();
|
||||
let mut g = GeomWriter::new(&mut w);
|
||||
let a = g.curve(&c);
|
||||
let b = g.curve(&c);
|
||||
assert_eq!(a, b);
|
||||
// Point, two directions, placement, circle.
|
||||
assert_eq!(w.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_geometry_is_an_error_never_a_panic() {
|
||||
let cases = [
|
||||
"#10=CIRCLE('',#4,-1.);",
|
||||
"#10=CIRCLE('',#99,1.);",
|
||||
"#10=CIRCLE('',#4,1.E300);",
|
||||
"#10=LINE('',#1,#1);",
|
||||
"#10=DIRECTION('',(0.,0.,0.));#11=VECTOR('',#10,1.);#12=LINE('',#1,#11);",
|
||||
"#10=B_SPLINE_CURVE_WITH_KNOTS('',2,(#1,#1),.UNSPECIFIED.,.F.,.F.,(3,3),(0.,1.),.UNSPECIFIED.);",
|
||||
"#10=B_SPLINE_CURVE_WITH_KNOTS('',99,(#1,#1),.UNSPECIFIED.,.F.,.F.,(1,1),(0.,1.),.UNSPECIFIED.);",
|
||||
"#10=B_SPLINE_CURVE_WITH_KNOTS('',1,(#1,#1),.UNSPECIFIED.,.F.,.F.,(4000000000000,1),(0.,1.),.UNSPECIFIED.);",
|
||||
"#10=B_SPLINE_CURVE_WITH_KNOTS('',1,(#1,#1),.UNSPECIFIED.,.F.,.F.,(2,2),(1.,0.),.UNSPECIFIED.);",
|
||||
"#10=(B_SPLINE_CURVE(1,(#1,#1),.UNSPECIFIED.,.F.,.F.)B_SPLINE_CURVE_WITH_KNOTS((2,2),(0.,1.),.UNSPECIFIED.)RATIONAL_B_SPLINE_CURVE((1.,-1.)));",
|
||||
"#10=TRIMMED_CURVE('',#10,(PARAMETER_VALUE(0.)),(PARAMETER_VALUE(1.)),.T.,.PARAMETER.);",
|
||||
"#10=SURFACE_CURVE('',#10,(),.CURVE_3D.);",
|
||||
"#10=POLYLINE('',(#1));",
|
||||
"#10=UNKNOWN_CURVE('');",
|
||||
];
|
||||
for c in cases {
|
||||
let ex = file(&format!("{BASE}{c}"));
|
||||
assert!(Reader::new(&ex, MM).curve(10).is_err(), "{c}");
|
||||
}
|
||||
let surfaces = [
|
||||
"#10=CYLINDRICAL_SURFACE('',#4,0.);",
|
||||
"#10=CONICAL_SURFACE('',#4,1.,2.);",
|
||||
"#10=TOROIDAL_SURFACE('',#4,1.,-2.);",
|
||||
"#10=OFFSET_SURFACE('',#10,1.,.F.);",
|
||||
"#10=B_SPLINE_SURFACE_WITH_KNOTS('',1,1,((#1,#1),(#1)),.UNSPECIFIED.,.F.,.F.,.F.,(2,2),(2,2),(0.,1.),(0.,1.),.UNSPECIFIED.);",
|
||||
"#10=SURFACE_OF_REVOLUTION('',#10,#4);",
|
||||
"#10=PLANE('',#3);",
|
||||
];
|
||||
for s in surfaces {
|
||||
let ex = file(&format!("{BASE}{s}"));
|
||||
assert!(Reader::new(&ex, MM).surface(10).is_err(), "{s}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Every edge curve and face surface of every STEP file in `SOLIDCRAFT_STEP_CORPUS` (the
|
||||
/// tangency-labs/models files): read, written, read back. Run with `--ignored`.
|
||||
#[test]
|
||||
#[ignore = "needs SOLIDCRAFT_STEP_CORPUS"]
|
||||
fn corpus_geometry_reads_and_round_trips() {
|
||||
let Ok(dir) = std::env::var("SOLIDCRAFT_STEP_CORPUS") else { return };
|
||||
let mut paths: Vec<_> = std::fs::read_dir(&dir).into_iter().flatten().flatten().map(|e| e.path()).collect();
|
||||
paths.sort();
|
||||
let mut failed = 0;
|
||||
for path in paths.iter().filter(|p| p.extension().is_some_and(|e| e.eq_ignore_ascii_case("step") || e.eq_ignore_ascii_case("stp"))) {
|
||||
let text = std::fs::read_to_string(path).unwrap_or_default();
|
||||
let t0 = std::time::Instant::now();
|
||||
let ex = p21::parse(&text).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
|
||||
let mut reader = Reader::new(&ex, MM);
|
||||
let (mut curves, mut surfaces) = (Vec::new(), Vec::new());
|
||||
let mut errors: std::collections::BTreeMap<String, usize> = Default::default();
|
||||
let mut ids: Vec<u64> = ex.entities.keys().copied().collect();
|
||||
ids.sort_unstable();
|
||||
for id in ids {
|
||||
let Some(e) = ex.get(id) else { continue };
|
||||
let (r, is_curve) = match e.name() {
|
||||
"EDGE_CURVE" => (e.params().get(3).and_then(p21::Param::as_ref_id), true),
|
||||
"ADVANCED_FACE" | "FACE_SURFACE" => (e.params().get(2).and_then(p21::Param::as_ref_id), false),
|
||||
_ => continue,
|
||||
};
|
||||
let Some(r) = r else { continue };
|
||||
let got = if is_curve { reader.curve(r).map(|c| curves.push(c)) } else { reader.surface(r).map(|s| surfaces.push(s)) };
|
||||
if let Err(e) = got {
|
||||
*errors.entry(e).or_default() += 1;
|
||||
}
|
||||
}
|
||||
let read = t0.elapsed();
|
||||
let mut w = Writer::new();
|
||||
let mut g = GeomWriter::new(&mut w);
|
||||
let cids: Vec<u64> = curves.iter().map(|c| g.curve(c)).collect();
|
||||
let sids: Vec<u64> = surfaces.iter().map(|s| g.surface(s)).collect();
|
||||
let out = w.file("corpus", "AUTOMOTIVE_DESIGN");
|
||||
let back = p21::parse(&out).unwrap_or_else(|e| panic!("{}: written file: {e}", path.display()));
|
||||
let mut again = Reader::new(&back, MM);
|
||||
let mut mismatched = 0;
|
||||
for (c, id) in curves.iter().zip(&cids) {
|
||||
match again.curve(*id) {
|
||||
Ok(b)
|
||||
if (0..=4).map(|i| i as f64 * 0.25).all(|t| match (c.point(t), b.point(t)) {
|
||||
(Some(x), Some(y)) => close(x, y),
|
||||
(x, y) => x.is_none() && y.is_none(),
|
||||
}) => {}
|
||||
_ => mismatched += 1,
|
||||
}
|
||||
}
|
||||
for (s, id) in surfaces.iter().zip(&sids) {
|
||||
match again.surface(*id) {
|
||||
Ok(b)
|
||||
if [(0.0, 0.0), (0.3, 0.6)].iter().all(|&(u, v)| match (s.point(u, v), b.point(u, v)) {
|
||||
(Some(x), Some(y)) => close(x, y),
|
||||
(x, y) => x.is_none() && y.is_none(),
|
||||
}) => {}
|
||||
_ => mismatched += 1,
|
||||
}
|
||||
}
|
||||
let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
println!(
|
||||
"{name}: {} edge curves, {} face surfaces read in {:.0} ms; {} unread; {mismatched} differ after a round trip",
|
||||
curves.len(),
|
||||
surfaces.len(),
|
||||
read.as_secs_f64() * 1e3,
|
||||
errors.values().sum::<usize>()
|
||||
);
|
||||
for (e, n) in &errors {
|
||||
println!(" {n} × {e}");
|
||||
}
|
||||
failed += mismatched + errors.values().sum::<usize>();
|
||||
}
|
||||
assert_eq!(failed, 0);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//! STEP (ISO 10303) for SolidCraft, independent of any modelling backend. [`p21`] reads the
|
||||
//! exchange structure, [`read`] turns its geometry entities into the plain curves and surfaces
|
||||
//! of [`geom`], and [`write`] writes them back. Written from the public ISO 10303-21 and -42
|
||||
//! specifications. Topology (vertices to solids) builds on the kernel-neutral B-rep and comes
|
||||
//! next.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod eval;
|
||||
pub mod geom;
|
||||
pub mod p21;
|
||||
pub mod read;
|
||||
pub mod write;
|
||||
|
||||
#[cfg(test)]
|
||||
mod geom_tests;
|
||||
@@ -0,0 +1,580 @@
|
||||
//! ISO 10303-21 ("Part 21") exchange structure parser: the HEADER and the DATA section's entity
|
||||
//! instances, simple (`#1=NAME(...)`) and complex (`#2=(A(...) B(...))`). Written from the
|
||||
//! public standard. Input is hostile: sizes, entity counts and nesting depth are capped and every
|
||||
//! error is a message, never a panic.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Largest STEP text we parse.
|
||||
pub const MAX_BYTES: usize = 512 << 20;
|
||||
/// Most entity instances in one file.
|
||||
pub const MAX_ENTITIES: usize = 20_000_000;
|
||||
/// Deepest parameter list nesting.
|
||||
const MAX_DEPTH: usize = 64;
|
||||
|
||||
/// A parameter value.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Param {
|
||||
Int(i64),
|
||||
Real(f64),
|
||||
Str(String),
|
||||
/// `.NAME.` (`.T.`/`.F.` included).
|
||||
Enum(String),
|
||||
Ref(u64),
|
||||
List(Vec<Param>),
|
||||
/// `NAME(params)` (a typed value such as `LENGTH_MEASURE(1.)`).
|
||||
Typed(String, Vec<Param>),
|
||||
Binary(String),
|
||||
/// `$`
|
||||
Null,
|
||||
/// `*`
|
||||
Derived,
|
||||
}
|
||||
|
||||
impl Param {
|
||||
pub fn as_f64(&self) -> Option<f64> {
|
||||
match self {
|
||||
Param::Real(x) => Some(*x).filter(|x| x.is_finite()),
|
||||
Param::Int(i) => Some(*i as f64),
|
||||
Param::Typed(_, v) if v.len() == 1 => v.first().and_then(Param::as_f64),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn as_i64(&self) -> Option<i64> {
|
||||
match self {
|
||||
Param::Int(i) => Some(*i),
|
||||
Param::Real(x) if x.fract() == 0.0 && x.abs() < 9e15 => Some(*x as i64),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn as_ref_id(&self) -> Option<u64> {
|
||||
match self {
|
||||
Param::Ref(r) => Some(*r),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn as_str(&self) -> Option<&str> {
|
||||
match self {
|
||||
Param::Str(s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn as_enum(&self) -> Option<&str> {
|
||||
match self {
|
||||
Param::Enum(s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// `.T.` → true, `.F.` → false (`.U.` and anything else → None).
|
||||
pub fn as_bool(&self) -> Option<bool> {
|
||||
match self.as_enum()? {
|
||||
"T" => Some(true),
|
||||
"F" => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn as_list(&self) -> Option<&[Param]> {
|
||||
match self {
|
||||
Param::List(v) => Some(v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One record of an entity instance: a simple instance has one, a complex one several.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Record {
|
||||
pub name: String,
|
||||
pub params: Vec<Param>,
|
||||
}
|
||||
|
||||
/// An entity instance.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Entity {
|
||||
pub records: Vec<Record>,
|
||||
}
|
||||
|
||||
impl Entity {
|
||||
pub fn is_complex(&self) -> bool {
|
||||
self.records.len() > 1
|
||||
}
|
||||
/// Name of a simple instance (the first record of a complex one).
|
||||
pub fn name(&self) -> &str {
|
||||
self.records.first().map(|r| r.name.as_str()).unwrap_or("")
|
||||
}
|
||||
/// Parameters of the record called `name`.
|
||||
pub fn record(&self, name: &str) -> Option<&[Param]> {
|
||||
self.records.iter().find(|r| r.name == name).map(|r| r.params.as_slice())
|
||||
}
|
||||
pub fn has(&self, name: &str) -> bool {
|
||||
self.records.iter().any(|r| r.name == name)
|
||||
}
|
||||
/// Parameters of a simple instance.
|
||||
pub fn params(&self) -> &[Param] {
|
||||
self.records.first().map(|r| r.params.as_slice()).unwrap_or(&[])
|
||||
}
|
||||
}
|
||||
|
||||
/// A parsed exchange file.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Exchange {
|
||||
/// FILE_SCHEMA names.
|
||||
pub schemas: Vec<String>,
|
||||
/// FILE_NAME originating system.
|
||||
pub originating_system: String,
|
||||
pub entities: HashMap<u64, Entity>,
|
||||
/// Ids defined more than once (the first definition is kept).
|
||||
pub duplicates: Vec<u64>,
|
||||
}
|
||||
|
||||
impl Exchange {
|
||||
pub fn get(&self, id: u64) -> Option<&Entity> {
|
||||
self.entities.get(&id)
|
||||
}
|
||||
}
|
||||
|
||||
struct Lexer<'a> {
|
||||
s: &'a [u8],
|
||||
i: usize,
|
||||
}
|
||||
|
||||
fn err<T>(msg: impl Into<String>) -> Result<T, String> {
|
||||
Err(msg.into())
|
||||
}
|
||||
|
||||
impl<'a> Lexer<'a> {
|
||||
fn line(&self) -> usize {
|
||||
self.s.get(..self.i).map(|b| b.iter().filter(|c| **c == b'\n').count() + 1).unwrap_or(0)
|
||||
}
|
||||
fn fail<T>(&self, msg: &str) -> Result<T, String> {
|
||||
err(format!("STEP syntax error at line {}: {msg}", self.line()))
|
||||
}
|
||||
fn peek(&self) -> Option<u8> {
|
||||
self.s.get(self.i).copied()
|
||||
}
|
||||
/// Skip whitespace and comments.
|
||||
fn ws(&mut self) -> Result<(), String> {
|
||||
loop {
|
||||
match self.peek() {
|
||||
Some(c) if c.is_ascii_whitespace() => self.i += 1,
|
||||
Some(b'/') if self.s.get(self.i + 1) == Some(&b'*') => {
|
||||
let rest = self.s.get(self.i + 2..).unwrap_or(&[]);
|
||||
match rest.windows(2).position(|w| w == b"*/") {
|
||||
Some(p) => self.i += p + 4,
|
||||
None => return self.fail("unterminated comment"),
|
||||
}
|
||||
}
|
||||
_ => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn eat(&mut self, c: u8) -> Result<bool, String> {
|
||||
self.ws()?;
|
||||
if self.peek() == Some(c) {
|
||||
self.i += 1;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
fn expect(&mut self, c: u8) -> Result<(), String> {
|
||||
if self.eat(c)? { Ok(()) } else { self.fail(&format!("expected `{}`", c as char)) }
|
||||
}
|
||||
fn keyword(&mut self) -> Result<String, String> {
|
||||
self.ws()?;
|
||||
let st = self.i;
|
||||
if self.peek() == Some(b'!') {
|
||||
self.i += 1;
|
||||
}
|
||||
while let Some(c) = self.peek() {
|
||||
if c.is_ascii_alphanumeric() || c == b'_' || c == b'-' {
|
||||
self.i += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if self.i == st {
|
||||
return self.fail("expected a keyword");
|
||||
}
|
||||
Ok(String::from_utf8_lossy(self.s.get(st..self.i).unwrap_or(&[])).to_ascii_uppercase())
|
||||
}
|
||||
fn number(&mut self) -> Result<Param, String> {
|
||||
let st = self.i;
|
||||
while let Some(c) = self.peek() {
|
||||
if c.is_ascii_digit() || matches!(c, b'+' | b'-' | b'.' | b'E' | b'e') {
|
||||
self.i += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let t = std::str::from_utf8(self.s.get(st..self.i).unwrap_or(&[])).unwrap_or("");
|
||||
if !t.contains(['.', 'E', 'e'])
|
||||
&& let Ok(i) = t.parse::<i64>()
|
||||
{
|
||||
return Ok(Param::Int(i));
|
||||
}
|
||||
// STEP allows `1.` and `1.E-3`.
|
||||
let fixed = t.replace(".E", ".0E").replace(".e", ".0e");
|
||||
let fixed = if fixed.ends_with('.') { format!("{fixed}0") } else { fixed };
|
||||
match fixed.parse::<f64>() {
|
||||
Ok(x) => Ok(Param::Real(x)),
|
||||
Err(_) => self.fail(&format!("bad number `{t}`")),
|
||||
}
|
||||
}
|
||||
fn string(&mut self) -> Result<String, String> {
|
||||
// Opening quote consumed by the caller.
|
||||
let mut raw = Vec::new();
|
||||
loop {
|
||||
match self.peek() {
|
||||
None => return self.fail("unterminated string"),
|
||||
Some(b'\'') => {
|
||||
if self.s.get(self.i + 1) == Some(&b'\'') {
|
||||
raw.push(b'\'');
|
||||
self.i += 2;
|
||||
} else {
|
||||
self.i += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(b'\n') | Some(b'\r') => self.i += 1,
|
||||
Some(c) => {
|
||||
raw.push(c);
|
||||
self.i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(decode_string(&raw))
|
||||
}
|
||||
fn param(&mut self, depth: usize) -> Result<Param, String> {
|
||||
if depth > MAX_DEPTH {
|
||||
return self.fail("parameters nested too deeply");
|
||||
}
|
||||
self.ws()?;
|
||||
let Some(c) = self.peek() else { return self.fail("unexpected end of file") };
|
||||
match c {
|
||||
b'$' => {
|
||||
self.i += 1;
|
||||
Ok(Param::Null)
|
||||
}
|
||||
b'*' => {
|
||||
self.i += 1;
|
||||
Ok(Param::Derived)
|
||||
}
|
||||
b'\'' => {
|
||||
self.i += 1;
|
||||
Ok(Param::Str(self.string()?))
|
||||
}
|
||||
b'"' => {
|
||||
self.i += 1;
|
||||
let st = self.i;
|
||||
while self.peek().is_some_and(|c| c != b'"') {
|
||||
self.i += 1;
|
||||
}
|
||||
let v = String::from_utf8_lossy(self.s.get(st..self.i).unwrap_or(&[])).to_string();
|
||||
self.expect(b'"')?;
|
||||
Ok(Param::Binary(v))
|
||||
}
|
||||
b'#' => {
|
||||
self.i += 1;
|
||||
let st = self.i;
|
||||
while self.peek().is_some_and(|c| c.is_ascii_digit()) {
|
||||
self.i += 1;
|
||||
}
|
||||
let t = std::str::from_utf8(self.s.get(st..self.i).unwrap_or(&[])).unwrap_or("");
|
||||
t.parse::<u64>().map(Param::Ref).or_else(|_| self.fail("bad entity reference"))
|
||||
}
|
||||
b'.' if self.s.get(self.i + 1).is_some_and(|c| c.is_ascii_alphabetic() || *c == b'_') => {
|
||||
self.i += 1;
|
||||
let st = self.i;
|
||||
while self.peek().is_some_and(|c| c != b'.') {
|
||||
self.i += 1;
|
||||
}
|
||||
let v = String::from_utf8_lossy(self.s.get(st..self.i).unwrap_or(&[])).trim().to_ascii_uppercase();
|
||||
self.expect(b'.')?;
|
||||
Ok(Param::Enum(v))
|
||||
}
|
||||
b'(' => {
|
||||
self.i += 1;
|
||||
Ok(Param::List(self.list_rest(depth + 1)?))
|
||||
}
|
||||
c if c.is_ascii_digit() || matches!(c, b'+' | b'-' | b'.') => self.number(),
|
||||
c if c.is_ascii_alphabetic() || c == b'!' => {
|
||||
let name = self.keyword()?;
|
||||
self.expect(b'(')?;
|
||||
Ok(Param::Typed(name, self.list_rest(depth + 1)?))
|
||||
}
|
||||
_ => self.fail(&format!("unexpected `{}`", c as char)),
|
||||
}
|
||||
}
|
||||
/// Parameters after an opening `(` up to and including the `)`.
|
||||
fn list_rest(&mut self, depth: usize) -> Result<Vec<Param>, String> {
|
||||
let mut v = Vec::new();
|
||||
if self.eat(b')')? {
|
||||
return Ok(v);
|
||||
}
|
||||
loop {
|
||||
v.push(self.param(depth)?);
|
||||
if self.eat(b',')? {
|
||||
continue;
|
||||
}
|
||||
self.expect(b')')?;
|
||||
return Ok(v);
|
||||
}
|
||||
}
|
||||
fn record(&mut self) -> Result<Record, String> {
|
||||
let name = self.keyword()?;
|
||||
self.expect(b'(')?;
|
||||
Ok(Record { name, params: self.list_rest(1)? })
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode the Part 21 string escapes (`\X\hh`, `\X2\…\X0\`, `\X4\…\X0\`, `\S\c`, `\\`).
|
||||
fn decode_string(raw: &[u8]) -> String {
|
||||
let mut out = String::new();
|
||||
let mut i = 0;
|
||||
let hex = |b: &[u8]| std::str::from_utf8(b).ok().and_then(|t| u32::from_str_radix(t, 16).ok());
|
||||
while let Some(&c) = raw.get(i) {
|
||||
if c == b'\\' {
|
||||
let rest = raw.get(i..).unwrap_or(&[]);
|
||||
if rest.starts_with(b"\\\\") {
|
||||
out.push('\\');
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if rest.starts_with(b"\\X\\")
|
||||
&& let Some(v) = rest.get(3..5).and_then(hex)
|
||||
{
|
||||
out.push(char::from_u32(v).unwrap_or('?'));
|
||||
i += 5;
|
||||
continue;
|
||||
}
|
||||
let mut run = false;
|
||||
for (tag, width) in [(&b"\\X2\\"[..], 4usize), (&b"\\X4\\"[..], 8)] {
|
||||
if rest.starts_with(tag)
|
||||
&& let Some(end) = rest.windows(4).position(|w| w == b"\\X0\\")
|
||||
{
|
||||
let body = rest.get(4..end).unwrap_or(&[]);
|
||||
let units: Vec<u32> = body.chunks(width).filter_map(hex).collect();
|
||||
if width == 4 {
|
||||
let u16s: Vec<u16> = units.iter().map(|u| *u as u16).collect();
|
||||
out.push_str(&String::from_utf16_lossy(&u16s));
|
||||
} else {
|
||||
out.extend(units.iter().map(|u| char::from_u32(*u).unwrap_or('?')));
|
||||
}
|
||||
i += end + 4;
|
||||
run = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// A run may be followed straight away by the backslash of the next escape (#2344).
|
||||
if run {
|
||||
continue;
|
||||
}
|
||||
if rest.starts_with(b"\\S\\")
|
||||
&& let Some(&ch) = rest.get(3)
|
||||
{
|
||||
out.push(char::from_u32(ch as u32 + 128).unwrap_or('?'));
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
out.push('\\');
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
// Plain bytes: keep UTF-8 runs intact.
|
||||
let st = i;
|
||||
while raw.get(i).is_some_and(|c| *c != b'\\') {
|
||||
i += 1;
|
||||
}
|
||||
out.push_str(&String::from_utf8_lossy(raw.get(st..i).unwrap_or(&[])));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Parse a Part 21 file.
|
||||
/// Someone waiting for a long parse: told how far it is, and able to stop it.
|
||||
pub trait Progress {
|
||||
/// Stop at the next check.
|
||||
fn cancelled(&self) -> bool;
|
||||
/// `done` of `total` bytes read.
|
||||
fn progress(&self, stage: &'static str, done: usize, total: usize);
|
||||
}
|
||||
|
||||
/// Parse an exchange structure.
|
||||
pub fn parse(text: &str) -> Result<Exchange, String> {
|
||||
parse_with(text, None)
|
||||
}
|
||||
|
||||
/// [`parse`], reporting progress to `watch` and stopping when it cancels.
|
||||
pub fn parse_with(text: &str, watch: Option<&dyn Progress>) -> Result<Exchange, String> {
|
||||
if text.len() > MAX_BYTES {
|
||||
return err(format!("STEP file too large ({} MB, limit {} MB)", text.len() >> 20, MAX_BYTES >> 20));
|
||||
}
|
||||
let mut lx = Lexer { s: text.as_bytes(), i: 0 };
|
||||
let magic = lx.keyword()?;
|
||||
if magic != "ISO-10303-21" {
|
||||
return err("not a STEP file (no ISO-10303-21 header)");
|
||||
}
|
||||
lx.expect(b';')?;
|
||||
let mut ex = Exchange::default();
|
||||
loop {
|
||||
lx.ws()?;
|
||||
if lx.peek().is_none() {
|
||||
return err("STEP file ends without END-ISO-10303-21");
|
||||
}
|
||||
let kw = lx.keyword()?;
|
||||
match kw.as_str() {
|
||||
"HEADER" => {
|
||||
lx.expect(b';')?;
|
||||
loop {
|
||||
let k = lx.keyword()?;
|
||||
if k == "ENDSEC" {
|
||||
lx.expect(b';')?;
|
||||
break;
|
||||
}
|
||||
lx.expect(b'(')?;
|
||||
let params = lx.list_rest(1)?;
|
||||
lx.expect(b';')?;
|
||||
match k.as_str() {
|
||||
"FILE_SCHEMA" => {
|
||||
if let Some(Param::List(v)) = params.first() {
|
||||
ex.schemas = v.iter().filter_map(|p| p.as_str().map(str::to_string)).collect();
|
||||
}
|
||||
}
|
||||
"FILE_NAME" => {
|
||||
ex.originating_system = params.get(5).and_then(Param::as_str).unwrap_or("").to_string();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
"DATA" => {
|
||||
// Optional `DATA('name', ('schema'));`
|
||||
if lx.eat(b'(')? {
|
||||
lx.list_rest(1)?;
|
||||
}
|
||||
lx.expect(b';')?;
|
||||
loop {
|
||||
lx.ws()?;
|
||||
if lx.peek() != Some(b'#') {
|
||||
let k = lx.keyword()?;
|
||||
if k == "ENDSEC" {
|
||||
lx.expect(b';')?;
|
||||
break;
|
||||
}
|
||||
return lx.fail(&format!("unexpected `{k}` in DATA"));
|
||||
}
|
||||
lx.i += 1;
|
||||
let st = lx.i;
|
||||
while lx.peek().is_some_and(|c| c.is_ascii_digit()) {
|
||||
lx.i += 1;
|
||||
}
|
||||
let id: u64 = match std::str::from_utf8(lx.s.get(st..lx.i).unwrap_or(&[])).ok().and_then(|t| t.parse().ok()) {
|
||||
Some(id) => id,
|
||||
None => return lx.fail("bad entity id"),
|
||||
};
|
||||
lx.expect(b'=')?;
|
||||
let records = if lx.eat(b'(')? {
|
||||
let mut rs = Vec::new();
|
||||
while !lx.eat(b')')? {
|
||||
if rs.len() > 64 {
|
||||
return lx.fail("complex entity with too many parts");
|
||||
}
|
||||
rs.push(lx.record()?);
|
||||
}
|
||||
rs
|
||||
} else {
|
||||
vec![lx.record()?]
|
||||
};
|
||||
lx.expect(b';')?;
|
||||
if ex.entities.len() % 4096 == 0
|
||||
&& let Some(w) = watch
|
||||
{
|
||||
if w.cancelled() {
|
||||
return err("STEP import cancelled");
|
||||
}
|
||||
w.progress("reading", lx.i, lx.s.len());
|
||||
}
|
||||
if ex.entities.len() >= MAX_ENTITIES {
|
||||
return err(format!("STEP file has more than {MAX_ENTITIES} entities"));
|
||||
}
|
||||
// A duplicate id is invalid; some writers emit them for geometry nobody
|
||||
// reads. Keep the first definition and report it.
|
||||
match ex.entities.entry(id) {
|
||||
std::collections::hash_map::Entry::Occupied(_) => ex.duplicates.push(id),
|
||||
std::collections::hash_map::Entry::Vacant(v) => {
|
||||
v.insert(Entity { records });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"END-ISO-10303-21" => {
|
||||
lx.expect(b';')?;
|
||||
return Ok(ex);
|
||||
}
|
||||
// ANCHOR / REFERENCE / SIGNATURE sections (Part 21 edition 3): skip to ENDSEC.
|
||||
"ANCHOR" | "REFERENCE" | "SIGNATURE" => {
|
||||
let rest = lx.s.get(lx.i..).unwrap_or(&[]);
|
||||
match rest.windows(7).position(|w| w == b"ENDSEC;") {
|
||||
Some(p) => lx.i += p + 7,
|
||||
None => return lx.fail("unterminated section"),
|
||||
}
|
||||
}
|
||||
_ => return lx.fail(&format!("unexpected `{kw}`")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_simple_and_complex() {
|
||||
let t = "ISO-10303-21;\nHEADER;FILE_DESCRIPTION((''),'2;1');\nFILE_NAME('a','',(''),(''),'','sys','');\nFILE_SCHEMA(('AUTOMOTIVE_DESIGN'));ENDSEC;\nDATA;\n#1=CARTESIAN_POINT('p',(0.,1.5E1,-2));\n/* c */ #2=(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.));\n#3=X('it''s','\\X2\\00E9\\X0\\',$,#1,LENGTH_MEASURE(1.E-3),\"0F\");\nENDSEC;\nEND-ISO-10303-21;\n";
|
||||
let ex = parse(t).unwrap();
|
||||
assert_eq!(ex.schemas, vec!["AUTOMOTIVE_DESIGN"]);
|
||||
assert_eq!(ex.originating_system, "sys");
|
||||
let p = ex.get(1).unwrap();
|
||||
assert_eq!(p.name(), "CARTESIAN_POINT");
|
||||
let xyz: Vec<f64> = p.params()[1].as_list().unwrap().iter().filter_map(Param::as_f64).collect();
|
||||
assert_eq!(xyz, vec![0.0, 15.0, -2.0]);
|
||||
let u = ex.get(2).unwrap();
|
||||
assert!(u.is_complex() && u.has("SI_UNIT"));
|
||||
assert_eq!(u.record("SI_UNIT").unwrap()[0].as_enum(), Some("MILLI"));
|
||||
let x = ex.get(3).unwrap().params();
|
||||
assert_eq!(x[0].as_str(), Some("it's"));
|
||||
assert_eq!(x[1].as_str(), Some("\u{e9}"));
|
||||
assert_eq!(x[2], Param::Null);
|
||||
assert_eq!(x[3].as_ref_id(), Some(1));
|
||||
assert_eq!(x[4].as_f64(), Some(1e-3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_text_is_an_error() {
|
||||
for t in [
|
||||
"",
|
||||
"garbage",
|
||||
"ISO-10303-21;",
|
||||
"ISO-10303-21;DATA;#1=A(",
|
||||
"ISO-10303-21;DATA;#1=A('x",
|
||||
"ISO-10303-21;DATA;#1=A(/*",
|
||||
"ISO-10303-21;DATA;#=A();ENDSEC;END-ISO-10303-21;",
|
||||
"ISO-10303-21;DATA;#99999999999999999999999=A();ENDSEC;END-ISO-10303-21;",
|
||||
"ISO-10303-21;DATA;#1=A(1e999999.);ENDSEC;END-ISO-10303-21;",
|
||||
] {
|
||||
let _ = parse(t);
|
||||
}
|
||||
let deep = format!("ISO-10303-21;DATA;#1=A({}{});ENDSEC;END-ISO-10303-21;", "(".repeat(10_000), ")".repeat(10_000));
|
||||
assert!(parse(&deep).is_err());
|
||||
let dup = parse("ISO-10303-21;DATA;#1=A(1);#1=B();ENDSEC;END-ISO-10303-21;").unwrap();
|
||||
assert_eq!(dup.duplicates, vec![1]);
|
||||
assert_eq!(dup.get(1).unwrap().name(), "A");
|
||||
assert_eq!(decode_string(b"\\X\\E9"), "\u{e9}");
|
||||
assert_eq!(decode_string(b"a\\X2\\00E900E8\\X0\\b"), "a\u{e9}\u{e8}b");
|
||||
assert_eq!(decode_string(b"\\"), "\\");
|
||||
// One run per character, as `step_out` writes them (#2344).
|
||||
assert_eq!(decode_string(b"\\X2\\5706\\X0\\\\X2\\89D2\\X0\\20"), "\u{5706}\u{89d2}20");
|
||||
assert_eq!(decode_string(b"\\X4\\0001F600\\X0\\\\X2\\00E9\\X0\\\\S\\a"), "\u{1f600}\u{e9}\u{e1}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,762 @@
|
||||
//! STEP geometry entities to [`crate::geom`]. Written from ISO 10303-42 (geometry) and
|
||||
//! ISO 10303-21 (the exchange structure). Input is hostile: every count, number and reference is
|
||||
//! checked, nesting is capped, and shared entities are read once.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::geom::{
|
||||
Associated, Axis1, BSplineCurve, BSplineSurface, Curve, CurveForm, KnotSpec, Logical, P3, Pcurve, Placement, PreferredRep, Surface,
|
||||
SurfaceCurveKind, SurfaceForm, Trim, TrimPreference, dot, norm, scale, sub,
|
||||
};
|
||||
use crate::p21::{Entity, Exchange, Param};
|
||||
|
||||
pub type R<T> = Result<T, String>;
|
||||
|
||||
/// Geometry nesting we follow (a trimmed curve of a surface curve of …).
|
||||
pub const MAX_GEOM_DEPTH: usize = 12;
|
||||
/// Most control points in one B-spline (the whole net for a surface).
|
||||
pub const MAX_CTRL: usize = 1_000_000;
|
||||
/// Largest coordinate or length, in file units.
|
||||
pub const MAX_COORD: f64 = 1e9;
|
||||
/// Highest B-spline degree.
|
||||
pub const MAX_DEGREE: usize = 32;
|
||||
|
||||
/// The units of a representation context.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Units {
|
||||
/// Millimetres per file length unit.
|
||||
pub len: f64,
|
||||
/// Radians per file plane angle unit.
|
||||
pub ang: f64,
|
||||
}
|
||||
|
||||
impl Default for Units {
|
||||
fn default() -> Self {
|
||||
Units { len: 1.0, ang: 1.0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// How a parameter of a curve or surface scales from file units: angles by the angle unit,
|
||||
/// lengths by the length unit, anything else (B-spline knots, ratios) not at all.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum ParamKind {
|
||||
Length,
|
||||
Angle,
|
||||
Plain,
|
||||
}
|
||||
|
||||
/// Reads geometry from one exchange structure, in one context's units.
|
||||
pub struct Reader<'a> {
|
||||
ex: &'a Exchange,
|
||||
units: Units,
|
||||
curves: HashMap<u64, Arc<Curve>>,
|
||||
curves_2d: HashMap<(u64, [u64; 2]), Arc<Curve>>,
|
||||
surfaces: HashMap<u64, Arc<Surface>>,
|
||||
}
|
||||
|
||||
fn entity(ex: &Exchange, id: u64) -> R<&Entity> {
|
||||
ex.get(id).ok_or_else(|| format!("missing entity #{id}"))
|
||||
}
|
||||
|
||||
fn reference(p: Option<&Param>, what: &str) -> R<u64> {
|
||||
p.and_then(Param::as_ref_id).ok_or_else(|| format!("{what}: expected a reference"))
|
||||
}
|
||||
|
||||
fn num(p: Option<&Param>, what: &str) -> R<f64> {
|
||||
let x = p.and_then(Param::as_f64).ok_or_else(|| format!("{what}: expected a number"))?;
|
||||
if x.is_finite() && x.abs() < MAX_COORD { Ok(x) } else { Err(format!("{what}: {x} out of range")) }
|
||||
}
|
||||
|
||||
fn logical(p: Option<&Param>, what: &str) -> R<Logical> {
|
||||
match p.and_then(Param::as_enum) {
|
||||
Some("T") => Ok(Logical::True),
|
||||
Some("F") => Ok(Logical::False),
|
||||
Some("U") => Ok(Logical::Unknown),
|
||||
_ => Err(format!("{what}: expected a logical")),
|
||||
}
|
||||
}
|
||||
|
||||
fn boolean(p: Option<&Param>, what: &str) -> R<bool> {
|
||||
p.and_then(Param::as_bool).ok_or_else(|| format!("{what}: expected a boolean"))
|
||||
}
|
||||
|
||||
fn unit(v: P3, what: &str) -> R<P3> {
|
||||
let m = norm(v);
|
||||
if m > 1e-12 && m.is_finite() { Ok(scale(v, 1.0 / m)) } else { Err(format!("{what}: zero direction")) }
|
||||
}
|
||||
|
||||
fn any_perpendicular(z: P3) -> P3 {
|
||||
let a = if z[0].abs() < 0.9 { [1.0, 0.0, 0.0] } else { [0.0, 1.0, 0.0] };
|
||||
let x = sub(a, scale(z, dot(a, z)));
|
||||
scale(x, 1.0 / norm(x))
|
||||
}
|
||||
|
||||
fn curve_form(p: Option<&Param>) -> CurveForm {
|
||||
match p.and_then(Param::as_enum) {
|
||||
Some("POLYLINE_FORM") => CurveForm::PolylineForm,
|
||||
Some("CIRCULAR_ARC") => CurveForm::CircularArc,
|
||||
Some("ELLIPTIC_ARC") => CurveForm::EllipticArc,
|
||||
Some("PARABOLIC_ARC") => CurveForm::ParabolicArc,
|
||||
Some("HYPERBOLIC_ARC") => CurveForm::HyperbolicArc,
|
||||
_ => CurveForm::Unspecified,
|
||||
}
|
||||
}
|
||||
|
||||
fn surface_form(p: Option<&Param>) -> SurfaceForm {
|
||||
match p.and_then(Param::as_enum) {
|
||||
Some("PLANE_SURF") => SurfaceForm::PlaneSurf,
|
||||
Some("CYLINDRICAL_SURF") => SurfaceForm::CylindricalSurf,
|
||||
Some("CONICAL_SURF") => SurfaceForm::ConicalSurf,
|
||||
Some("SPHERICAL_SURF") => SurfaceForm::SphericalSurf,
|
||||
Some("TOROIDAL_SURF") => SurfaceForm::ToroidalSurf,
|
||||
Some("SURF_OF_REVOLUTION") => SurfaceForm::SurfOfRevolution,
|
||||
Some("RULED_SURF") => SurfaceForm::RuledSurf,
|
||||
Some("GENERALISED_CONE") => SurfaceForm::GeneralisedCone,
|
||||
Some("QUADRIC_SURF") => SurfaceForm::QuadricSurf,
|
||||
Some("SURF_OF_LINEAR_EXTRUSION") => SurfaceForm::SurfOfLinearExtrusion,
|
||||
_ => SurfaceForm::Unspecified,
|
||||
}
|
||||
}
|
||||
|
||||
fn knot_spec(p: Option<&Param>) -> KnotSpec {
|
||||
match p.and_then(Param::as_enum) {
|
||||
Some("UNIFORM_KNOTS") => KnotSpec::UniformKnots,
|
||||
Some("QUASI_UNIFORM_KNOTS") => KnotSpec::QuasiUniformKnots,
|
||||
Some("PIECEWISE_BEZIER_KNOTS") => KnotSpec::PiecewiseBezierKnots,
|
||||
_ => KnotSpec::Unspecified,
|
||||
}
|
||||
}
|
||||
|
||||
/// Distinct knots and multiplicities, checked against `n_ctrl` points of `degree` before
|
||||
/// anything is expanded (a file can claim multiplicities that would need gigabytes).
|
||||
fn knots(mults: Option<&Param>, values: Option<&Param>, n_ctrl: usize, degree: usize) -> R<(Vec<f64>, Vec<usize>)> {
|
||||
let m: Vec<usize> = mults
|
||||
.and_then(Param::as_list)
|
||||
.ok_or("B-spline without knot multiplicities")?
|
||||
.iter()
|
||||
.map(|x| x.as_i64().filter(|v| *v >= 1 && *v as usize <= degree + 1).map(|v| v as usize).ok_or_else(|| "bad knot multiplicity".to_string()))
|
||||
.collect::<R<_>>()?;
|
||||
let raw: Vec<f64> = values.and_then(Param::as_list).ok_or("B-spline without knots")?.iter().map(|x| num(Some(x), "knot")).collect::<R<_>>()?;
|
||||
if m.len() != raw.len() || raw.windows(2).any(|w| w.first() > w.get(1)) {
|
||||
return Err("knots must not decrease, one multiplicity each".into());
|
||||
}
|
||||
// Some writers list a knot twice; that is one knot with both multiplicities.
|
||||
let (mut k, mut mm): (Vec<f64>, Vec<usize>) = (Vec::with_capacity(raw.len()), Vec::with_capacity(raw.len()));
|
||||
for (x, n) in raw.into_iter().zip(m) {
|
||||
match (k.last(), mm.last_mut()) {
|
||||
(Some(last), Some(lm)) if *last == x => *lm += n,
|
||||
_ => {
|
||||
k.push(x);
|
||||
mm.push(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
let m = mm;
|
||||
if k.len() < 2 || m.iter().any(|v| *v > degree + 1) {
|
||||
return Err("bad knot vector".into());
|
||||
}
|
||||
if m.iter().try_fold(0usize, |a, v| a.checked_add(*v)) != Some(n_ctrl + degree + 1) {
|
||||
return Err("knot multiplicities do not match the control points".into());
|
||||
}
|
||||
Ok((k, m))
|
||||
}
|
||||
|
||||
/// Knots of a B-spline given by its type alone (BEZIER_, UNIFORM_, QUASI_UNIFORM_CURVE).
|
||||
fn implied_knots(spec: KnotSpec, n_ctrl: usize, degree: usize) -> R<(Vec<f64>, Vec<usize>)> {
|
||||
if n_ctrl <= degree {
|
||||
return Err("too few control points".into());
|
||||
}
|
||||
match spec {
|
||||
KnotSpec::PiecewiseBezierKnots => {
|
||||
// Segments of `degree`, full interior multiplicity.
|
||||
let segs = (n_ctrl - 1) / degree;
|
||||
if segs * degree + 1 != n_ctrl {
|
||||
return Err("Bezier control point count".into());
|
||||
}
|
||||
let k = (0..=segs).map(|s| s as f64).collect();
|
||||
let mut m = vec![degree; segs + 1];
|
||||
if let Some(f) = m.first_mut() {
|
||||
*f = degree + 1;
|
||||
}
|
||||
if let Some(l) = m.last_mut() {
|
||||
*l = degree + 1;
|
||||
}
|
||||
Ok((k, m))
|
||||
}
|
||||
KnotSpec::UniformKnots => {
|
||||
let n = n_ctrl + degree + 1;
|
||||
Ok(((0..n).map(|i| i as f64 - degree as f64).collect(), vec![1; n]))
|
||||
}
|
||||
_ => {
|
||||
// Quasi-uniform: clamped ends, uniform interior.
|
||||
let inner = n_ctrl - degree;
|
||||
let k = (0..=inner).map(|i| i as f64).collect();
|
||||
let mut m = vec![1; inner + 1];
|
||||
if let Some(f) = m.first_mut() {
|
||||
*f = degree + 1;
|
||||
}
|
||||
if let Some(l) = m.last_mut() {
|
||||
*l = degree + 1;
|
||||
}
|
||||
Ok((k, m))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn weights(p: Option<&Param>, n: usize) -> R<Vec<f64>> {
|
||||
let w: Vec<f64> = p
|
||||
.and_then(Param::as_list)
|
||||
.ok_or("rational B-spline without weights")?
|
||||
.iter()
|
||||
.map(|w| num(Some(w), "weight").and_then(|w| if w > 0.0 { Ok(w) } else { Err("non-positive weight".into()) }))
|
||||
.collect::<R<_>>()?;
|
||||
if w.len() != n { Err("weights do not match the control points".into()) } else { Ok(w) }
|
||||
}
|
||||
|
||||
fn degree(p: Option<&Param>) -> R<usize> {
|
||||
p.and_then(Param::as_i64).filter(|d| (1..=MAX_DEGREE as i64).contains(d)).map(|d| d as usize).ok_or_else(|| "bad B-spline degree".into())
|
||||
}
|
||||
|
||||
/// The knot type a B-spline entity's (sub)type implies when it lists no knots.
|
||||
fn spec_of_type(e: &Entity) -> KnotSpec {
|
||||
let has = |n: &str| e.name() == n || e.has(n);
|
||||
if has("BEZIER_CURVE") || has("BEZIER_SURFACE") {
|
||||
KnotSpec::PiecewiseBezierKnots
|
||||
} else if has("UNIFORM_CURVE") || has("UNIFORM_SURFACE") {
|
||||
KnotSpec::UniformKnots
|
||||
} else {
|
||||
KnotSpec::QuasiUniformKnots
|
||||
}
|
||||
}
|
||||
|
||||
/// The parameters of a simple or complex B-spline entity, as the simple form lays them out:
|
||||
/// the name first, then the `B_SPLINE_*` attributes, then the `*_WITH_KNOTS` ones.
|
||||
fn bspline_params(e: &Entity, base: &str, with_knots: &str) -> R<(Vec<Param>, Option<Vec<Param>>)> {
|
||||
if e.is_complex() {
|
||||
let b = e.record(base).ok_or_else(|| format!("complex entity without {base}"))?;
|
||||
let mut p = vec![Param::Str(String::new())];
|
||||
p.extend(b.iter().cloned());
|
||||
Ok((p, e.record(with_knots).map(<[Param]>::to_vec)))
|
||||
} else {
|
||||
let p = e.params();
|
||||
let n = if base == "B_SPLINE_CURVE" { 6 } else { 8 };
|
||||
let k = (e.name() == with_knots).then(|| p.get(n..).map(<[Param]>::to_vec).unwrap_or_default());
|
||||
Ok((p.to_vec(), k))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
pub fn new(ex: &'a Exchange, units: Units) -> Self {
|
||||
Reader { ex, units, curves: HashMap::new(), curves_2d: HashMap::new(), surfaces: HashMap::new() }
|
||||
}
|
||||
|
||||
pub fn units(&self) -> Units {
|
||||
self.units
|
||||
}
|
||||
|
||||
fn length(&self, p: Option<&Param>, what: &str) -> R<f64> {
|
||||
Ok(num(p, what)? * self.units.len)
|
||||
}
|
||||
|
||||
fn angle(&self, p: Option<&Param>, what: &str) -> R<f64> {
|
||||
Ok(num(p, what)? * self.units.ang)
|
||||
}
|
||||
|
||||
/// A CARTESIAN_POINT, scaled per coordinate (`s` are the factors).
|
||||
fn point_scaled(&self, id: u64, s: [f64; 3], dim: usize) -> R<P3> {
|
||||
let e = entity(self.ex, id)?;
|
||||
if e.name() != "CARTESIAN_POINT" {
|
||||
return Err(format!("#{id}: expected CARTESIAN_POINT, found {}", e.name()));
|
||||
}
|
||||
let c = e.params().get(1).and_then(Param::as_list).ok_or("CARTESIAN_POINT without coordinates")?;
|
||||
if c.is_empty() || c.len() > dim {
|
||||
return Err(format!("#{id}: point with {} coordinates", c.len()));
|
||||
}
|
||||
let g = |i: usize| -> R<f64> { if i < c.len() { num(c.get(i), "coordinate") } else { Ok(0.0) } };
|
||||
Ok([g(0)? * s[0], g(1)? * s[1], if dim == 3 { g(2)? * s[2] } else { 0.0 }])
|
||||
}
|
||||
|
||||
/// A CARTESIAN_POINT in millimetres.
|
||||
pub fn point(&self, id: u64) -> R<P3> {
|
||||
let l = self.units.len;
|
||||
self.point_scaled(id, [l, l, l], 3)
|
||||
}
|
||||
|
||||
/// A DIRECTION, as a unit vector (with 2D axes scaled by `s` first).
|
||||
fn direction_scaled(&self, id: u64, s: [f64; 3], dim: usize) -> R<P3> {
|
||||
let e = entity(self.ex, id)?;
|
||||
if e.name() != "DIRECTION" {
|
||||
return Err(format!("#{id}: expected DIRECTION, found {}", e.name()));
|
||||
}
|
||||
let c = e.params().get(1).and_then(Param::as_list).ok_or("DIRECTION without ratios")?;
|
||||
if c.is_empty() || c.len() > dim {
|
||||
return Err(format!("#{id}: direction with {} ratios", c.len()));
|
||||
}
|
||||
let g = |i: usize| -> R<f64> { if i < c.len() { num(c.get(i), "direction ratio") } else { Ok(0.0) } };
|
||||
unit([g(0)? * s[0], g(1)? * s[1], if dim == 3 { g(2)? * s[2] } else { 0.0 }], &format!("#{id}"))
|
||||
}
|
||||
|
||||
pub fn direction(&self, id: u64) -> R<P3> {
|
||||
self.direction_scaled(id, [1.0; 3], 3)
|
||||
}
|
||||
|
||||
/// AXIS2_PLACEMENT_3D, or AXIS2_PLACEMENT_2D as a placement in the XY plane.
|
||||
fn placement_scaled(&self, id: u64, s: [f64; 3], dim: usize) -> R<Placement> {
|
||||
let e = entity(self.ex, id)?;
|
||||
let p = e.params();
|
||||
let origin = self.point_scaled(reference(p.get(1), "placement location")?, s, dim)?;
|
||||
let (axis, rf) = match (e.name(), dim) {
|
||||
("AXIS2_PLACEMENT_3D", 3) => {
|
||||
let z = match p.get(2).and_then(Param::as_ref_id) {
|
||||
Some(d) => self.direction_scaled(d, s, dim)?,
|
||||
None => [0.0, 0.0, 1.0],
|
||||
};
|
||||
(z, p.get(3).and_then(Param::as_ref_id))
|
||||
}
|
||||
("AXIS2_PLACEMENT_2D", 2) => ([0.0, 0.0, 1.0], p.get(2).and_then(Param::as_ref_id)),
|
||||
(n, _) => return Err(format!("#{id}: unexpected placement {n} in {dim}D")),
|
||||
};
|
||||
let x = match rf {
|
||||
Some(d) => {
|
||||
let r = self.direction_scaled(d, s, dim)?;
|
||||
let x = sub(r, scale(axis, dot(r, axis)));
|
||||
if norm(x) > 1e-9 { scale(x, 1.0 / norm(x)) } else { any_perpendicular(axis) }
|
||||
}
|
||||
None => any_perpendicular(axis),
|
||||
};
|
||||
Ok(Placement { origin, axis, ref_direction: x })
|
||||
}
|
||||
|
||||
pub fn placement(&self, id: u64) -> R<Placement> {
|
||||
let l = self.units.len;
|
||||
self.placement_scaled(id, [l, l, l], 3)
|
||||
}
|
||||
|
||||
pub fn axis1(&self, id: u64) -> R<Axis1> {
|
||||
let e = entity(self.ex, id)?;
|
||||
if e.name() != "AXIS1_PLACEMENT" {
|
||||
return Err(format!("#{id}: expected AXIS1_PLACEMENT, found {}", e.name()));
|
||||
}
|
||||
let p = e.params();
|
||||
let origin = self.point(reference(p.get(1), "axis location")?)?;
|
||||
let direction = match p.get(2).and_then(Param::as_ref_id) {
|
||||
Some(d) => self.direction(d)?,
|
||||
None => [0.0, 0.0, 1.0],
|
||||
};
|
||||
Ok(Axis1 { origin, direction })
|
||||
}
|
||||
|
||||
/// A VECTOR: unit direction and magnitude (scaled as a length by `len`).
|
||||
fn vector_scaled(&self, id: u64, s: [f64; 3], dim: usize) -> R<(P3, f64)> {
|
||||
let e = entity(self.ex, id)?;
|
||||
if e.name() != "VECTOR" {
|
||||
return Err(format!("#{id}: expected VECTOR, found {}", e.name()));
|
||||
}
|
||||
let p = e.params();
|
||||
// Scaling a 2D direction unevenly changes the magnitude too.
|
||||
let raw = self.direction_scaled(reference(p.get(1), "vector orientation")?, [1.0; 3], dim)?;
|
||||
let m = num(p.get(2), "vector magnitude")?;
|
||||
if m < 0.0 {
|
||||
return Err("negative vector magnitude".into());
|
||||
}
|
||||
let v = [raw[0] * s[0] * m, raw[1] * s[1] * m, raw[2] * s[2] * m];
|
||||
let len = norm(v);
|
||||
if !(len > 0.0) {
|
||||
return Err("zero vector".into());
|
||||
}
|
||||
Ok((scale(v, 1.0 / len), len))
|
||||
}
|
||||
|
||||
/// A 3D curve entity.
|
||||
pub fn curve(&mut self, id: u64) -> R<Arc<Curve>> {
|
||||
self.curve_in(id, 0)
|
||||
}
|
||||
|
||||
fn curve_in(&mut self, id: u64, depth: usize) -> R<Arc<Curve>> {
|
||||
if let Some(c) = self.curves.get(&id) {
|
||||
return Ok(c.clone());
|
||||
}
|
||||
if depth > MAX_GEOM_DEPTH {
|
||||
return Err("curve definitions nested too deeply".into());
|
||||
}
|
||||
let l = self.units.len;
|
||||
let c = Arc::new(self.read_curve(id, [l, l, l], 3, depth)?);
|
||||
self.curves.insert(id, c.clone());
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
/// A 2D curve in a surface's parameter space, scaled by the surface's parameter units.
|
||||
fn curve_2d(&mut self, id: u64, kinds: (ParamKind, ParamKind), depth: usize) -> R<Arc<Curve>> {
|
||||
let s = [self.factor(kinds.0), self.factor(kinds.1), 1.0];
|
||||
let key = (id, [s[0].to_bits(), s[1].to_bits()]);
|
||||
if let Some(c) = self.curves_2d.get(&key) {
|
||||
return Ok(c.clone());
|
||||
}
|
||||
if depth > MAX_GEOM_DEPTH {
|
||||
return Err("curve definitions nested too deeply".into());
|
||||
}
|
||||
let c = Arc::new(self.read_curve(id, s, 2, depth)?);
|
||||
self.curves_2d.insert(key, c.clone());
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
fn factor(&self, k: ParamKind) -> f64 {
|
||||
match k {
|
||||
ParamKind::Length => self.units.len,
|
||||
ParamKind::Angle => self.units.ang,
|
||||
ParamKind::Plain => 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// A conic radius: a length, scaled evenly (`s` must be even for a conic).
|
||||
fn conic_scale(s: [f64; 3]) -> R<f64> {
|
||||
if (s[0] - s[1]).abs() > 1e-12 * s[0].abs().max(s[1].abs()) {
|
||||
return Err("not supported yet: a conic in a parameter space with mixed units".into());
|
||||
}
|
||||
Ok(s[0])
|
||||
}
|
||||
|
||||
fn read_curve(&mut self, id: u64, s: [f64; 3], dim: usize, depth: usize) -> R<Curve> {
|
||||
let e = entity(self.ex, id)?;
|
||||
if e.is_complex() {
|
||||
return self.bspline_curve(e, s, dim);
|
||||
}
|
||||
let p = e.params();
|
||||
let positive = |x: f64, what: &str| if x > 0.0 { Ok(x) } else { Err(format!("{what} must be positive")) };
|
||||
match e.name() {
|
||||
"LINE" => {
|
||||
let origin = self.point_scaled(reference(p.get(1), "line point")?, s, dim)?;
|
||||
let (direction, magnitude) = self.vector_scaled(reference(p.get(2), "line vector")?, s, dim)?;
|
||||
Ok(Curve::Line { origin, direction, magnitude })
|
||||
}
|
||||
"CIRCLE" => {
|
||||
let k = Self::conic_scale(s)?;
|
||||
let position = self.placement_scaled(reference(p.get(1), "circle placement")?, s, dim)?;
|
||||
Ok(Curve::Circle { position, radius: positive(num(p.get(2), "radius")? * k, "circle radius")? })
|
||||
}
|
||||
"ELLIPSE" => {
|
||||
let k = Self::conic_scale(s)?;
|
||||
let position = self.placement_scaled(reference(p.get(1), "ellipse placement")?, s, dim)?;
|
||||
Ok(Curve::Ellipse {
|
||||
position,
|
||||
semi_axis_1: positive(num(p.get(2), "semi axis")? * k, "ellipse axis")?,
|
||||
semi_axis_2: positive(num(p.get(3), "semi axis")? * k, "ellipse axis")?,
|
||||
})
|
||||
}
|
||||
"HYPERBOLA" => {
|
||||
let k = Self::conic_scale(s)?;
|
||||
let position = self.placement_scaled(reference(p.get(1), "hyperbola placement")?, s, dim)?;
|
||||
Ok(Curve::Hyperbola {
|
||||
position,
|
||||
semi_axis: positive(num(p.get(2), "semi axis")? * k, "hyperbola axis")?,
|
||||
semi_imag_axis: positive(num(p.get(3), "semi imaginary axis")? * k, "hyperbola axis")?,
|
||||
})
|
||||
}
|
||||
"PARABOLA" => {
|
||||
let k = Self::conic_scale(s)?;
|
||||
let position = self.placement_scaled(reference(p.get(1), "parabola placement")?, s, dim)?;
|
||||
let focal_dist = num(p.get(2), "focal distance")? * k;
|
||||
if focal_dist == 0.0 {
|
||||
return Err("parabola focal distance must not be zero".into());
|
||||
}
|
||||
Ok(Curve::Parabola { position, focal_dist })
|
||||
}
|
||||
"B_SPLINE_CURVE_WITH_KNOTS" | "BEZIER_CURVE" | "UNIFORM_CURVE" | "QUASI_UNIFORM_CURVE" => self.bspline_curve(e, s, dim),
|
||||
"POLYLINE" => {
|
||||
let ids = p.get(1).and_then(Param::as_list).ok_or("POLYLINE without points")?;
|
||||
if ids.len() < 2 || ids.len() > MAX_CTRL {
|
||||
return Err("polyline point count".into());
|
||||
}
|
||||
let pts = ids.iter().map(|c| self.point_scaled(reference(Some(c), "polyline point")?, s, dim)).collect::<R<_>>()?;
|
||||
Ok(Curve::Polyline(pts))
|
||||
}
|
||||
"TRIMMED_CURVE" => {
|
||||
let b = reference(p.get(1), "trimmed curve basis")?;
|
||||
let basis = if dim == 3 { self.curve_in(b, depth + 1)? } else { Arc::new(self.read_curve(b, s, dim, depth + 1)?) };
|
||||
// Conic parameters are angles; every other basis curve's are plain numbers.
|
||||
let kind = match &*basis {
|
||||
Curve::Circle { .. } | Curve::Ellipse { .. } => ParamKind::Angle,
|
||||
_ => ParamKind::Plain,
|
||||
};
|
||||
let trim_1 = self.trims(p.get(2), kind, s, dim)?;
|
||||
let trim_2 = self.trims(p.get(3), kind, s, dim)?;
|
||||
let sense = boolean(p.get(4), "trimmed curve sense")?;
|
||||
let preference = match p.get(5).and_then(Param::as_enum) {
|
||||
Some("CARTESIAN") => TrimPreference::Cartesian,
|
||||
Some("PARAMETER") => TrimPreference::Parameter,
|
||||
_ => TrimPreference::Unspecified,
|
||||
};
|
||||
Ok(Curve::Trimmed { basis, trim_1, trim_2, sense, preference })
|
||||
}
|
||||
"SURFACE_CURVE" | "SEAM_CURVE" | "INTERSECTION_CURVE" | "BOUNDED_SURFACE_CURVE" if dim == 3 => {
|
||||
let kind = match e.name() {
|
||||
"SEAM_CURVE" => SurfaceCurveKind::Seam,
|
||||
"INTERSECTION_CURVE" => SurfaceCurveKind::Intersection,
|
||||
"BOUNDED_SURFACE_CURVE" => SurfaceCurveKind::BoundedSurface,
|
||||
_ => SurfaceCurveKind::Surface,
|
||||
};
|
||||
let curve_3d = self.curve_in(reference(p.get(1), "surface curve 3D curve")?, depth + 1)?;
|
||||
let items = p.get(2).and_then(Param::as_list).ok_or("surface curve without associated geometry")?;
|
||||
if items.len() > 2 {
|
||||
return Err("surface curve with more than two associated items".into());
|
||||
}
|
||||
let mut associated = Vec::with_capacity(items.len());
|
||||
for item in items {
|
||||
associated.push(self.associated(reference(Some(item), "associated geometry")?, depth + 1)?);
|
||||
}
|
||||
let master = match p.get(3).and_then(Param::as_enum) {
|
||||
Some("PCURVE_S1") => PreferredRep::PcurveS1,
|
||||
Some("PCURVE_S2") => PreferredRep::PcurveS2,
|
||||
_ => PreferredRep::Curve3d,
|
||||
};
|
||||
Ok(Curve::SurfaceCurve { kind, curve_3d, associated, master })
|
||||
}
|
||||
"OFFSET_CURVE_3D" if dim == 3 => {
|
||||
let basis = self.curve_in(reference(p.get(1), "offset curve basis")?, depth + 1)?;
|
||||
let distance = self.length(p.get(2), "offset distance")?;
|
||||
let self_intersect = logical(p.get(3), "offset curve self_intersect")?;
|
||||
let ref_direction = self.direction(reference(p.get(4), "offset curve direction")?)?;
|
||||
Ok(Curve::OffsetCurve3d { basis, distance, self_intersect, ref_direction })
|
||||
}
|
||||
n => Err(format!("not supported yet: curve {n}{}", if dim == 2 { " in parameter space" } else { "" })),
|
||||
}
|
||||
}
|
||||
|
||||
fn trims(&self, p: Option<&Param>, kind: ParamKind, s: [f64; 3], dim: usize) -> R<Vec<Trim>> {
|
||||
let items = p.and_then(Param::as_list).ok_or("trimmed curve without trims")?;
|
||||
if items.is_empty() || items.len() > 2 {
|
||||
return Err("trimmed curve trim count".into());
|
||||
}
|
||||
items
|
||||
.iter()
|
||||
.map(|t| match t {
|
||||
Param::Ref(r) => Ok(Trim::Point(self.point_scaled(*r, s, dim)?)),
|
||||
Param::Typed(name, v) if name == "PARAMETER_VALUE" => Ok(Trim::Parameter(num(v.first(), "trim parameter")? * self.factor(kind))),
|
||||
Param::Real(_) | Param::Int(_) => Ok(Trim::Parameter(num(Some(t), "trim parameter")? * self.factor(kind))),
|
||||
_ => Err("bad trim".into()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn associated(&mut self, id: u64, depth: usize) -> R<Associated> {
|
||||
let e = entity(self.ex, id)?;
|
||||
if e.name() != "PCURVE" {
|
||||
return Ok(Associated::Surface(self.surface_in(id, depth)?));
|
||||
}
|
||||
let p = e.params();
|
||||
let surface = self.surface_in(reference(p.get(1), "pcurve surface")?, depth)?;
|
||||
let rep = entity(self.ex, reference(p.get(2), "pcurve representation")?)?;
|
||||
if rep.name() != "DEFINITIONAL_REPRESENTATION" {
|
||||
return Err(format!("pcurve representation is {}", rep.name()));
|
||||
}
|
||||
let items = rep.params().get(1).and_then(Param::as_list).ok_or("pcurve representation without items")?;
|
||||
let [item] = items else { return Err("pcurve representation must hold one curve".into()) };
|
||||
let kinds = Self::param_kinds(&surface);
|
||||
let curve = self.curve_2d(reference(Some(item), "pcurve curve")?, kinds, depth)?;
|
||||
Ok(Associated::Pcurve(Pcurve { surface, curve }))
|
||||
}
|
||||
|
||||
/// How a surface's (u, v) parameters scale from file units.
|
||||
fn param_kinds(s: &Surface) -> (ParamKind, ParamKind) {
|
||||
use ParamKind::*;
|
||||
match s {
|
||||
Surface::Plane { .. } => (Length, Length),
|
||||
Surface::Cylinder { .. } | Surface::Cone { .. } => (Angle, Length),
|
||||
Surface::Sphere { .. } | Surface::Torus { .. } | Surface::DegenerateTorus { .. } => (Angle, Angle),
|
||||
Surface::Revolution { profile, .. } => (Angle, Self::curve_param_kind(profile)),
|
||||
Surface::Extrusion { profile, .. } => (Self::curve_param_kind(profile), Plain),
|
||||
Surface::BSpline(_) => (Plain, Plain),
|
||||
Surface::Offset { basis, .. } | Surface::RectangularTrimmed { basis, .. } => Self::param_kinds(basis),
|
||||
}
|
||||
}
|
||||
|
||||
fn curve_param_kind(c: &Curve) -> ParamKind {
|
||||
match c {
|
||||
Curve::Circle { .. } | Curve::Ellipse { .. } => ParamKind::Angle,
|
||||
Curve::Trimmed { basis, .. } | Curve::OffsetCurve3d { basis, .. } => Self::curve_param_kind(basis),
|
||||
Curve::SurfaceCurve { curve_3d, .. } => Self::curve_param_kind(curve_3d),
|
||||
_ => ParamKind::Plain,
|
||||
}
|
||||
}
|
||||
|
||||
fn bspline_curve(&mut self, e: &Entity, s: [f64; 3], dim: usize) -> R<Curve> {
|
||||
let (base, with_knots) = bspline_params(e, "B_SPLINE_CURVE", "B_SPLINE_CURVE_WITH_KNOTS")?;
|
||||
let degree = degree(base.get(1))?;
|
||||
let ids = base.get(2).and_then(Param::as_list).ok_or("B-spline without control points")?;
|
||||
if ids.len() > MAX_CTRL {
|
||||
return Err("B-spline with too many control points".into());
|
||||
}
|
||||
let points: Vec<P3> = ids.iter().map(|c| self.point_scaled(reference(Some(c), "control point")?, s, dim)).collect::<R<_>>()?;
|
||||
let (knots, multiplicities, knot_spec) = match &with_knots {
|
||||
Some(k) => {
|
||||
let (kv, m) = knots(k.first(), k.get(1), points.len(), degree)?;
|
||||
(kv, m, knot_spec(k.get(2)))
|
||||
}
|
||||
None => {
|
||||
let spec = spec_of_type(e);
|
||||
let (kv, m) = implied_knots(spec, points.len(), degree)?;
|
||||
(kv, m, spec)
|
||||
}
|
||||
};
|
||||
let weights = match e.record("RATIONAL_B_SPLINE_CURVE") {
|
||||
Some(r) => Some(weights(r.first(), points.len())?),
|
||||
None => None,
|
||||
};
|
||||
Ok(Curve::BSpline(Box::new(BSplineCurve {
|
||||
degree,
|
||||
points,
|
||||
weights,
|
||||
knots,
|
||||
multiplicities,
|
||||
form: curve_form(base.get(3)),
|
||||
closed: logical(base.get(4), "B-spline closed_curve").unwrap_or(Logical::Unknown),
|
||||
self_intersect: logical(base.get(5), "B-spline self_intersect").unwrap_or(Logical::Unknown),
|
||||
knot_spec,
|
||||
})))
|
||||
}
|
||||
|
||||
/// A surface entity.
|
||||
pub fn surface(&mut self, id: u64) -> R<Arc<Surface>> {
|
||||
self.surface_in(id, 0)
|
||||
}
|
||||
|
||||
fn surface_in(&mut self, id: u64, depth: usize) -> R<Arc<Surface>> {
|
||||
if let Some(s) = self.surfaces.get(&id) {
|
||||
return Ok(s.clone());
|
||||
}
|
||||
if depth > MAX_GEOM_DEPTH {
|
||||
return Err("surface definitions nested too deeply".into());
|
||||
}
|
||||
let s = Arc::new(self.read_surface(id, depth)?);
|
||||
self.surfaces.insert(id, s.clone());
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn read_surface(&mut self, id: u64, depth: usize) -> R<Surface> {
|
||||
let e = entity(self.ex, id)?;
|
||||
if e.is_complex() {
|
||||
return self.bspline_surface(e);
|
||||
}
|
||||
let p = e.params();
|
||||
let position = |r: &Self| r.placement(reference(p.get(1), "surface placement")?);
|
||||
let positive = |x: f64, what: &str| if x > 0.0 { Ok(x) } else { Err(format!("{what} must be positive")) };
|
||||
match e.name() {
|
||||
"PLANE" => Ok(Surface::Plane { position: position(self)? }),
|
||||
"CYLINDRICAL_SURFACE" => {
|
||||
Ok(Surface::Cylinder { position: position(self)?, radius: positive(self.length(p.get(2), "radius")?, "cylinder radius")? })
|
||||
}
|
||||
"CONICAL_SURFACE" => {
|
||||
let radius = self.length(p.get(2), "radius")?;
|
||||
let semi_angle = self.angle(p.get(3), "semi angle")?;
|
||||
if radius < 0.0 || !(semi_angle.abs() > 1e-12 && semi_angle.abs() < std::f64::consts::FRAC_PI_2 - 1e-12) {
|
||||
return Err("cone with a negative radius or a semi angle outside (0, 90°)".into());
|
||||
}
|
||||
Ok(Surface::Cone { position: position(self)?, radius, semi_angle })
|
||||
}
|
||||
"SPHERICAL_SURFACE" => {
|
||||
Ok(Surface::Sphere { position: position(self)?, radius: positive(self.length(p.get(2), "radius")?, "sphere radius")? })
|
||||
}
|
||||
"TOROIDAL_SURFACE" | "DEGENERATE_TOROIDAL_SURFACE" => {
|
||||
let major_radius = self.length(p.get(2), "major radius")?;
|
||||
let minor_radius = positive(self.length(p.get(3), "minor radius")?, "torus minor radius")?;
|
||||
if major_radius < 0.0 {
|
||||
return Err("torus major radius must not be negative".into());
|
||||
}
|
||||
let position = position(self)?;
|
||||
if e.name() == "DEGENERATE_TOROIDAL_SURFACE" {
|
||||
let select_outer = boolean(p.get(4), "degenerate torus select_outer")?;
|
||||
Ok(Surface::DegenerateTorus { position, major_radius, minor_radius, select_outer })
|
||||
} else {
|
||||
Ok(Surface::Torus { position, major_radius, minor_radius })
|
||||
}
|
||||
}
|
||||
"B_SPLINE_SURFACE_WITH_KNOTS" | "BEZIER_SURFACE" | "UNIFORM_SURFACE" | "QUASI_UNIFORM_SURFACE" => self.bspline_surface(e),
|
||||
"SURFACE_OF_REVOLUTION" => {
|
||||
let profile = self.curve_in(reference(p.get(1), "revolution profile")?, depth + 1)?;
|
||||
let axis = self.axis1(reference(p.get(2), "revolution axis")?)?;
|
||||
Ok(Surface::Revolution { profile, axis })
|
||||
}
|
||||
"SURFACE_OF_LINEAR_EXTRUSION" => {
|
||||
let profile = self.curve_in(reference(p.get(1), "extrusion profile")?, depth + 1)?;
|
||||
let l = self.units.len;
|
||||
let (direction, magnitude) = self.vector_scaled(reference(p.get(2), "extrusion vector")?, [l, l, l], 3)?;
|
||||
Ok(Surface::Extrusion { profile, direction, magnitude })
|
||||
}
|
||||
"OFFSET_SURFACE" => {
|
||||
let basis = self.surface_in(reference(p.get(1), "offset basis")?, depth + 1)?;
|
||||
let distance = self.length(p.get(2), "offset distance")?;
|
||||
let self_intersect = logical(p.get(3), "offset self_intersect")?;
|
||||
Ok(Surface::Offset { basis, distance, self_intersect })
|
||||
}
|
||||
"RECTANGULAR_TRIMMED_SURFACE" => {
|
||||
let basis = self.surface_in(reference(p.get(1), "trimmed surface basis")?, depth + 1)?;
|
||||
let (ku, kv) = Self::param_kinds(&basis);
|
||||
let (fu, fv) = (self.factor(ku), self.factor(kv));
|
||||
Ok(Surface::RectangularTrimmed {
|
||||
u1: num(p.get(2), "u1")? * fu,
|
||||
u2: num(p.get(3), "u2")? * fu,
|
||||
v1: num(p.get(4), "v1")? * fv,
|
||||
v2: num(p.get(5), "v2")? * fv,
|
||||
u_sense: boolean(p.get(6), "usense")?,
|
||||
v_sense: boolean(p.get(7), "vsense")?,
|
||||
basis,
|
||||
})
|
||||
}
|
||||
n => Err(format!("not supported yet: surface {n}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn bspline_surface(&mut self, e: &Entity) -> R<Surface> {
|
||||
let (base, with_knots) = bspline_params(e, "B_SPLINE_SURFACE", "B_SPLINE_SURFACE_WITH_KNOTS")?;
|
||||
let (u_degree, v_degree) = (degree(base.get(1))?, degree(base.get(2))?);
|
||||
let rows = base.get(3).and_then(Param::as_list).ok_or("B-spline surface without control points")?;
|
||||
let mut points: Vec<Vec<P3>> = Vec::with_capacity(rows.len().min(MAX_CTRL));
|
||||
let mut total = 0usize;
|
||||
for r in rows {
|
||||
let r = r.as_list().ok_or("bad control point row")?;
|
||||
total = total.saturating_add(r.len());
|
||||
if total > MAX_CTRL {
|
||||
return Err("B-spline surface with too many control points".into());
|
||||
}
|
||||
points.push(r.iter().map(|c| self.point(reference(Some(c), "control point")?)).collect::<R<_>>()?);
|
||||
}
|
||||
let nu = points.len();
|
||||
let nv = points.first().map(Vec::len).unwrap_or(0);
|
||||
if nu == 0 || nv == 0 || points.iter().any(|r| r.len() != nv) {
|
||||
return Err("ragged B-spline control net".into());
|
||||
}
|
||||
let ((u_knots, u_multiplicities), (v_knots, v_multiplicities), knot_spec) = match &with_knots {
|
||||
Some(k) => (knots(k.first(), k.get(2), nu, u_degree)?, knots(k.get(1), k.get(3), nv, v_degree)?, knot_spec(k.get(4))),
|
||||
None => {
|
||||
let spec = spec_of_type(e);
|
||||
(implied_knots(spec, nu, u_degree)?, implied_knots(spec, nv, v_degree)?, spec)
|
||||
}
|
||||
};
|
||||
let weights = match e.record("RATIONAL_B_SPLINE_SURFACE") {
|
||||
Some(r) => {
|
||||
let w = r.first().and_then(Param::as_list).ok_or("rational surface without weights")?;
|
||||
if w.len() != nu {
|
||||
return Err("weights do not match the control net".into());
|
||||
}
|
||||
Some(w.iter().map(|row| weights(Some(row), nv)).collect::<R<_>>()?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
Ok(Surface::BSpline(Box::new(BSplineSurface {
|
||||
u_degree,
|
||||
v_degree,
|
||||
points,
|
||||
weights,
|
||||
u_knots,
|
||||
u_multiplicities,
|
||||
v_knots,
|
||||
v_multiplicities,
|
||||
form: surface_form(base.get(4)),
|
||||
u_closed: logical(base.get(5), "u_closed").unwrap_or(Logical::Unknown),
|
||||
v_closed: logical(base.get(6), "v_closed").unwrap_or(Logical::Unknown),
|
||||
self_intersect: logical(base.get(7), "self_intersect").unwrap_or(Logical::Unknown),
|
||||
knot_spec,
|
||||
})))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
//! Writing ISO 10303-21: a DATA section built entity by entity, and [`crate::geom`] curves and
|
||||
//! surfaces as ISO 10303-42 entities. Identical entities are written once and shared, as the
|
||||
//! reader shares them.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use crate::geom::{
|
||||
Associated, Axis1, BSplineCurve, BSplineSurface, Curve, CurveForm, KnotSpec, Logical, P3, Placement, PreferredRep, Surface, SurfaceCurveKind,
|
||||
SurfaceForm, Trim, TrimPreference,
|
||||
};
|
||||
use crate::p21::Param;
|
||||
|
||||
/// A real as Part 21 writes it: always with a decimal point, exponent as `E`.
|
||||
pub fn real(x: f64) -> String {
|
||||
if !x.is_finite() {
|
||||
return "0.".into();
|
||||
}
|
||||
let s = format!("{x:?}");
|
||||
match s.split_once('e') {
|
||||
Some((m, e)) => {
|
||||
let m = if m.contains('.') { m.to_string() } else { format!("{m}.") };
|
||||
format!("{m}E{e}")
|
||||
}
|
||||
None if s.contains('.') => s,
|
||||
None => format!("{s}."),
|
||||
}
|
||||
}
|
||||
|
||||
/// A string literal: quotes doubled, non-ASCII as `\X2\` UTF-16 runs.
|
||||
pub fn string(s: &str) -> String {
|
||||
let mut o = String::from("'");
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'\'' => o.push_str("''"),
|
||||
'\\' => o.push_str("\\\\"),
|
||||
c if c.is_ascii() && !c.is_ascii_control() => o.push(c),
|
||||
c if c.is_ascii_control() => {}
|
||||
c => {
|
||||
o.push_str("\\X2\\");
|
||||
let mut buf = [0u16; 2];
|
||||
for u in c.encode_utf16(&mut buf) {
|
||||
let _ = write!(o, "{u:04X}");
|
||||
}
|
||||
o.push_str("\\X0\\");
|
||||
}
|
||||
}
|
||||
}
|
||||
o.push('\'');
|
||||
o
|
||||
}
|
||||
|
||||
fn param(p: &Param, o: &mut String) {
|
||||
match p {
|
||||
Param::Int(i) => {
|
||||
let _ = write!(o, "{i}");
|
||||
}
|
||||
Param::Real(x) => o.push_str(&real(*x)),
|
||||
Param::Str(s) => o.push_str(&string(s)),
|
||||
Param::Enum(e) => {
|
||||
let _ = write!(o, ".{e}.");
|
||||
}
|
||||
Param::Ref(r) => {
|
||||
let _ = write!(o, "#{r}");
|
||||
}
|
||||
Param::List(v) => {
|
||||
o.push('(');
|
||||
params(v, o);
|
||||
o.push(')');
|
||||
}
|
||||
Param::Typed(name, v) => {
|
||||
o.push_str(name);
|
||||
o.push('(');
|
||||
params(v, o);
|
||||
o.push(')');
|
||||
}
|
||||
Param::Binary(b) => {
|
||||
let _ = write!(o, "\"{b}\"");
|
||||
}
|
||||
Param::Null => o.push('$'),
|
||||
Param::Derived => o.push('*'),
|
||||
}
|
||||
}
|
||||
|
||||
fn params(v: &[Param], o: &mut String) {
|
||||
for (i, p) in v.iter().enumerate() {
|
||||
if i > 0 {
|
||||
o.push(',');
|
||||
}
|
||||
param(p, o);
|
||||
}
|
||||
}
|
||||
|
||||
/// A DATA section under construction. Entities are numbered from 1 in the order they are added;
|
||||
/// adding an entity identical to an earlier one returns the earlier one's id.
|
||||
#[derive(Default)]
|
||||
pub struct Writer {
|
||||
lines: Vec<String>,
|
||||
shared: HashMap<String, u64>,
|
||||
}
|
||||
|
||||
impl Writer {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn push(&mut self, body: String) -> u64 {
|
||||
if let Some(id) = self.shared.get(&body) {
|
||||
return *id;
|
||||
}
|
||||
let id = self.lines.len() as u64 + 1;
|
||||
self.lines.push(format!("#{id}={body};"));
|
||||
self.shared.insert(body, id);
|
||||
id
|
||||
}
|
||||
|
||||
/// A simple entity instance.
|
||||
pub fn add(&mut self, name: &str, p: &[Param]) -> u64 {
|
||||
let mut body = format!("{name}(");
|
||||
params(p, &mut body);
|
||||
body.push(')');
|
||||
self.push(body)
|
||||
}
|
||||
|
||||
/// A complex entity instance; its records are written in alphabetical order, as Part 21 asks.
|
||||
pub fn add_complex(&mut self, records: &[(&str, Vec<Param>)]) -> u64 {
|
||||
let mut sorted: Vec<&(&str, Vec<Param>)> = records.iter().collect();
|
||||
sorted.sort_by(|a, b| a.0.cmp(b.0));
|
||||
let mut body = String::from("(");
|
||||
for (name, p) in sorted {
|
||||
body.push_str(name);
|
||||
body.push('(');
|
||||
params(p, &mut body);
|
||||
body.push(')');
|
||||
}
|
||||
body.push(')');
|
||||
self.push(body)
|
||||
}
|
||||
|
||||
/// Entities added so far.
|
||||
pub fn len(&self) -> usize {
|
||||
self.lines.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.lines.is_empty()
|
||||
}
|
||||
|
||||
/// The DATA section's entity lines.
|
||||
pub fn data(&self) -> String {
|
||||
let mut o = String::new();
|
||||
for l in &self.lines {
|
||||
o.push_str(l);
|
||||
o.push('\n');
|
||||
}
|
||||
o
|
||||
}
|
||||
|
||||
/// A whole exchange file around the entities written so far.
|
||||
pub fn file(&self, name: &str, schema: &str) -> String {
|
||||
format!(
|
||||
"ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION((''),'2;1');\nFILE_NAME({},'',(''),(''),'SolidCraft','SolidCraft','');\nFILE_SCHEMA(({}));\nENDSEC;\nDATA;\n{}ENDSEC;\nEND-ISO-10303-21;\n",
|
||||
string(name),
|
||||
string(schema),
|
||||
self.data()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn s() -> Param {
|
||||
Param::Str(String::new())
|
||||
}
|
||||
fn r(id: u64) -> Param {
|
||||
Param::Ref(id)
|
||||
}
|
||||
fn f(x: f64) -> Param {
|
||||
Param::Real(x)
|
||||
}
|
||||
fn e(name: &str) -> Param {
|
||||
Param::Enum(name.into())
|
||||
}
|
||||
fn list(v: Vec<Param>) -> Param {
|
||||
Param::List(v)
|
||||
}
|
||||
fn reals(v: &[f64]) -> Param {
|
||||
list(v.iter().map(|x| f(*x)).collect())
|
||||
}
|
||||
fn ints(v: &[usize]) -> Param {
|
||||
list(v.iter().map(|x| Param::Int(*x as i64)).collect())
|
||||
}
|
||||
fn logical(l: Logical) -> Param {
|
||||
e(match l {
|
||||
Logical::True => "T",
|
||||
Logical::False => "F",
|
||||
Logical::Unknown => "U",
|
||||
})
|
||||
}
|
||||
fn boolean(b: bool) -> Param {
|
||||
e(if b { "T" } else { "F" })
|
||||
}
|
||||
|
||||
fn curve_form(f: CurveForm) -> Param {
|
||||
e(match f {
|
||||
CurveForm::PolylineForm => "POLYLINE_FORM",
|
||||
CurveForm::CircularArc => "CIRCULAR_ARC",
|
||||
CurveForm::EllipticArc => "ELLIPTIC_ARC",
|
||||
CurveForm::ParabolicArc => "PARABOLIC_ARC",
|
||||
CurveForm::HyperbolicArc => "HYPERBOLIC_ARC",
|
||||
CurveForm::Unspecified => "UNSPECIFIED",
|
||||
})
|
||||
}
|
||||
|
||||
fn surface_form(f: SurfaceForm) -> Param {
|
||||
e(match f {
|
||||
SurfaceForm::PlaneSurf => "PLANE_SURF",
|
||||
SurfaceForm::CylindricalSurf => "CYLINDRICAL_SURF",
|
||||
SurfaceForm::ConicalSurf => "CONICAL_SURF",
|
||||
SurfaceForm::SphericalSurf => "SPHERICAL_SURF",
|
||||
SurfaceForm::ToroidalSurf => "TOROIDAL_SURF",
|
||||
SurfaceForm::SurfOfRevolution => "SURF_OF_REVOLUTION",
|
||||
SurfaceForm::RuledSurf => "RULED_SURF",
|
||||
SurfaceForm::GeneralisedCone => "GENERALISED_CONE",
|
||||
SurfaceForm::QuadricSurf => "QUADRIC_SURF",
|
||||
SurfaceForm::SurfOfLinearExtrusion => "SURF_OF_LINEAR_EXTRUSION",
|
||||
SurfaceForm::Unspecified => "UNSPECIFIED",
|
||||
})
|
||||
}
|
||||
|
||||
fn knot_spec(k: KnotSpec) -> Param {
|
||||
e(match k {
|
||||
KnotSpec::UniformKnots => "UNIFORM_KNOTS",
|
||||
KnotSpec::QuasiUniformKnots => "QUASI_UNIFORM_KNOTS",
|
||||
KnotSpec::PiecewiseBezierKnots => "PIECEWISE_BEZIER_KNOTS",
|
||||
KnotSpec::Unspecified => "UNSPECIFIED",
|
||||
})
|
||||
}
|
||||
|
||||
/// Writes geometry into a [`Writer`]. Lengths are written in millimetres and angles in radians:
|
||||
/// the representation context the caller writes must say so.
|
||||
pub struct GeomWriter<'w> {
|
||||
pub w: &'w mut Writer,
|
||||
/// The 2D parameter-space context pcurves refer to, once written.
|
||||
pcurve_context: Option<u64>,
|
||||
}
|
||||
|
||||
impl<'w> GeomWriter<'w> {
|
||||
pub fn new(w: &'w mut Writer) -> Self {
|
||||
GeomWriter { w, pcurve_context: None }
|
||||
}
|
||||
|
||||
fn coords(p: P3, dim: usize) -> Param {
|
||||
reals(p.get(..dim).unwrap_or(&p))
|
||||
}
|
||||
|
||||
pub fn point(&mut self, p: P3) -> u64 {
|
||||
self.point_in(p, 3)
|
||||
}
|
||||
|
||||
fn point_in(&mut self, p: P3, dim: usize) -> u64 {
|
||||
self.w.add("CARTESIAN_POINT", &[s(), Self::coords(p, dim)])
|
||||
}
|
||||
|
||||
fn direction_in(&mut self, d: P3, dim: usize) -> u64 {
|
||||
self.w.add("DIRECTION", &[s(), Self::coords(d, dim)])
|
||||
}
|
||||
|
||||
pub fn direction(&mut self, d: P3) -> u64 {
|
||||
self.direction_in(d, 3)
|
||||
}
|
||||
|
||||
fn vector_in(&mut self, d: P3, magnitude: f64, dim: usize) -> u64 {
|
||||
let d = self.direction_in(d, dim);
|
||||
self.w.add("VECTOR", &[s(), r(d), f(magnitude)])
|
||||
}
|
||||
|
||||
fn placement_in(&mut self, p: &Placement, dim: usize) -> u64 {
|
||||
let o = self.point_in(p.origin, dim);
|
||||
let x = self.direction_in(p.ref_direction, dim);
|
||||
if dim == 2 {
|
||||
self.w.add("AXIS2_PLACEMENT_2D", &[s(), r(o), r(x)])
|
||||
} else {
|
||||
let z = self.direction_in(p.axis, dim);
|
||||
self.w.add("AXIS2_PLACEMENT_3D", &[s(), r(o), r(z), r(x)])
|
||||
}
|
||||
}
|
||||
|
||||
pub fn placement(&mut self, p: &Placement) -> u64 {
|
||||
self.placement_in(p, 3)
|
||||
}
|
||||
|
||||
pub fn axis1(&mut self, a: &Axis1) -> u64 {
|
||||
let o = self.point(a.origin);
|
||||
let d = self.direction(a.direction);
|
||||
self.w.add("AXIS1_PLACEMENT", &[s(), r(o), r(d)])
|
||||
}
|
||||
|
||||
/// A 3D curve.
|
||||
pub fn curve(&mut self, c: &Curve) -> u64 {
|
||||
self.curve_in(c, 3)
|
||||
}
|
||||
|
||||
fn curve_in(&mut self, c: &Curve, dim: usize) -> u64 {
|
||||
match c {
|
||||
Curve::Line { origin, direction, magnitude } => {
|
||||
let p = self.point_in(*origin, dim);
|
||||
let v = self.vector_in(*direction, *magnitude, dim);
|
||||
self.w.add("LINE", &[s(), r(p), r(v)])
|
||||
}
|
||||
Curve::Circle { position, radius } => {
|
||||
let p = self.placement_in(position, dim);
|
||||
self.w.add("CIRCLE", &[s(), r(p), f(*radius)])
|
||||
}
|
||||
Curve::Ellipse { position, semi_axis_1, semi_axis_2 } => {
|
||||
let p = self.placement_in(position, dim);
|
||||
self.w.add("ELLIPSE", &[s(), r(p), f(*semi_axis_1), f(*semi_axis_2)])
|
||||
}
|
||||
Curve::Hyperbola { position, semi_axis, semi_imag_axis } => {
|
||||
let p = self.placement_in(position, dim);
|
||||
self.w.add("HYPERBOLA", &[s(), r(p), f(*semi_axis), f(*semi_imag_axis)])
|
||||
}
|
||||
Curve::Parabola { position, focal_dist } => {
|
||||
let p = self.placement_in(position, dim);
|
||||
self.w.add("PARABOLA", &[s(), r(p), f(*focal_dist)])
|
||||
}
|
||||
Curve::BSpline(b) => self.bspline_curve(b, dim),
|
||||
Curve::Polyline(pts) => {
|
||||
let ids = pts.iter().map(|p| r(self.point_in(*p, dim))).collect();
|
||||
self.w.add("POLYLINE", &[s(), list(ids)])
|
||||
}
|
||||
Curve::Trimmed { basis, trim_1, trim_2, sense, preference } => {
|
||||
let b = self.curve_in(basis, dim);
|
||||
let t1 = self.trims(trim_1, dim);
|
||||
let t2 = self.trims(trim_2, dim);
|
||||
let pref = e(match preference {
|
||||
TrimPreference::Cartesian => "CARTESIAN",
|
||||
TrimPreference::Parameter => "PARAMETER",
|
||||
TrimPreference::Unspecified => "UNSPECIFIED",
|
||||
});
|
||||
self.w.add("TRIMMED_CURVE", &[s(), r(b), t1, t2, boolean(*sense), pref])
|
||||
}
|
||||
Curve::SurfaceCurve { kind, curve_3d, associated, master } => {
|
||||
let c3 = self.curve_in(curve_3d, 3);
|
||||
let items = associated
|
||||
.iter()
|
||||
.map(|a| match a {
|
||||
Associated::Pcurve(pc) => {
|
||||
let surf = self.surface(&pc.surface);
|
||||
let c2 = self.curve_in(&pc.curve, 2);
|
||||
let ctx = self.pcurve_context();
|
||||
let rep = self.w.add("DEFINITIONAL_REPRESENTATION", &[s(), list(vec![r(c2)]), r(ctx)]);
|
||||
r(self.w.add("PCURVE", &[s(), r(surf), r(rep)]))
|
||||
}
|
||||
Associated::Surface(sf) => r(self.surface(sf)),
|
||||
})
|
||||
.collect();
|
||||
let name = match kind {
|
||||
SurfaceCurveKind::Surface => "SURFACE_CURVE",
|
||||
SurfaceCurveKind::Seam => "SEAM_CURVE",
|
||||
SurfaceCurveKind::Intersection => "INTERSECTION_CURVE",
|
||||
SurfaceCurveKind::BoundedSurface => "BOUNDED_SURFACE_CURVE",
|
||||
};
|
||||
let m = e(match master {
|
||||
PreferredRep::Curve3d => "CURVE_3D",
|
||||
PreferredRep::PcurveS1 => "PCURVE_S1",
|
||||
PreferredRep::PcurveS2 => "PCURVE_S2",
|
||||
});
|
||||
self.w.add(name, &[s(), r(c3), list(items), m])
|
||||
}
|
||||
Curve::OffsetCurve3d { basis, distance, self_intersect, ref_direction } => {
|
||||
let b = self.curve_in(basis, 3);
|
||||
let d = self.direction(*ref_direction);
|
||||
self.w.add("OFFSET_CURVE_3D", &[s(), r(b), f(*distance), logical(*self_intersect), r(d)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pcurve_context(&mut self) -> u64 {
|
||||
if let Some(c) = self.pcurve_context {
|
||||
return c;
|
||||
}
|
||||
let c = self.w.add_complex(&[
|
||||
("GEOMETRIC_REPRESENTATION_CONTEXT", vec![Param::Int(2)]),
|
||||
("PARAMETRIC_REPRESENTATION_CONTEXT", vec![]),
|
||||
("REPRESENTATION_CONTEXT", vec![Param::Str("2D SPACE".into()), s()]),
|
||||
]);
|
||||
self.pcurve_context = Some(c);
|
||||
c
|
||||
}
|
||||
|
||||
fn trims(&mut self, t: &[Trim], dim: usize) -> Param {
|
||||
list(
|
||||
t.iter()
|
||||
.map(|t| match t {
|
||||
Trim::Point(p) => r(self.point_in(*p, dim)),
|
||||
Trim::Parameter(x) => Param::Typed("PARAMETER_VALUE".into(), vec![f(*x)]),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn bspline_curve(&mut self, b: &BSplineCurve, dim: usize) -> u64 {
|
||||
let ctrl = list(b.points.iter().map(|p| r(self.point_in(*p, dim))).collect());
|
||||
let common = vec![Param::Int(b.degree as i64), ctrl, curve_form(b.form), logical(b.closed), logical(b.self_intersect)];
|
||||
let knots = vec![ints(&b.multiplicities), reals(&b.knots), knot_spec(b.knot_spec)];
|
||||
match &b.weights {
|
||||
None => {
|
||||
let mut p = vec![s()];
|
||||
p.extend(common);
|
||||
p.extend(knots);
|
||||
self.w.add("B_SPLINE_CURVE_WITH_KNOTS", &p)
|
||||
}
|
||||
Some(w) => self.w.add_complex(&[
|
||||
("BOUNDED_CURVE", vec![]),
|
||||
("B_SPLINE_CURVE", common),
|
||||
("B_SPLINE_CURVE_WITH_KNOTS", knots),
|
||||
("CURVE", vec![]),
|
||||
("GEOMETRIC_REPRESENTATION_ITEM", vec![]),
|
||||
("RATIONAL_B_SPLINE_CURVE", vec![reals(w)]),
|
||||
("REPRESENTATION_ITEM", vec![s()]),
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
/// A surface.
|
||||
pub fn surface(&mut self, sf: &Surface) -> u64 {
|
||||
match sf {
|
||||
Surface::Plane { position } => {
|
||||
let p = self.placement(position);
|
||||
self.w.add("PLANE", &[s(), r(p)])
|
||||
}
|
||||
Surface::Cylinder { position, radius } => {
|
||||
let p = self.placement(position);
|
||||
self.w.add("CYLINDRICAL_SURFACE", &[s(), r(p), f(*radius)])
|
||||
}
|
||||
Surface::Cone { position, radius, semi_angle } => {
|
||||
let p = self.placement(position);
|
||||
self.w.add("CONICAL_SURFACE", &[s(), r(p), f(*radius), f(*semi_angle)])
|
||||
}
|
||||
Surface::Sphere { position, radius } => {
|
||||
let p = self.placement(position);
|
||||
self.w.add("SPHERICAL_SURFACE", &[s(), r(p), f(*radius)])
|
||||
}
|
||||
Surface::Torus { position, major_radius, minor_radius } => {
|
||||
let p = self.placement(position);
|
||||
self.w.add("TOROIDAL_SURFACE", &[s(), r(p), f(*major_radius), f(*minor_radius)])
|
||||
}
|
||||
Surface::DegenerateTorus { position, major_radius, minor_radius, select_outer } => {
|
||||
let p = self.placement(position);
|
||||
self.w.add("DEGENERATE_TOROIDAL_SURFACE", &[s(), r(p), f(*major_radius), f(*minor_radius), boolean(*select_outer)])
|
||||
}
|
||||
Surface::BSpline(b) => self.bspline_surface(b),
|
||||
Surface::Revolution { profile, axis } => {
|
||||
let c = self.curve(profile);
|
||||
let a = self.axis1(axis);
|
||||
self.w.add("SURFACE_OF_REVOLUTION", &[s(), r(c), r(a)])
|
||||
}
|
||||
Surface::Extrusion { profile, direction, magnitude } => {
|
||||
let c = self.curve(profile);
|
||||
let v = self.vector_in(*direction, *magnitude, 3);
|
||||
self.w.add("SURFACE_OF_LINEAR_EXTRUSION", &[s(), r(c), r(v)])
|
||||
}
|
||||
Surface::Offset { basis, distance, self_intersect } => {
|
||||
let b = self.surface(basis);
|
||||
self.w.add("OFFSET_SURFACE", &[s(), r(b), f(*distance), logical(*self_intersect)])
|
||||
}
|
||||
Surface::RectangularTrimmed { basis, u1, u2, v1, v2, u_sense, v_sense } => {
|
||||
let b = self.surface(basis);
|
||||
self.w.add("RECTANGULAR_TRIMMED_SURFACE", &[s(), r(b), f(*u1), f(*u2), f(*v1), f(*v2), boolean(*u_sense), boolean(*v_sense)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn bspline_surface(&mut self, b: &BSplineSurface) -> u64 {
|
||||
let ctrl = list(b.points.iter().map(|row| list(row.iter().map(|p| r(self.point(*p))).collect())).collect());
|
||||
let common = vec![
|
||||
Param::Int(b.u_degree as i64),
|
||||
Param::Int(b.v_degree as i64),
|
||||
ctrl,
|
||||
surface_form(b.form),
|
||||
logical(b.u_closed),
|
||||
logical(b.v_closed),
|
||||
logical(b.self_intersect),
|
||||
];
|
||||
let knots = vec![ints(&b.u_multiplicities), ints(&b.v_multiplicities), reals(&b.u_knots), reals(&b.v_knots), knot_spec(b.knot_spec)];
|
||||
match &b.weights {
|
||||
None => {
|
||||
let mut p = vec![s()];
|
||||
p.extend(common);
|
||||
p.extend(knots);
|
||||
self.w.add("B_SPLINE_SURFACE_WITH_KNOTS", &p)
|
||||
}
|
||||
Some(w) => self.w.add_complex(&[
|
||||
("BOUNDED_SURFACE", vec![]),
|
||||
("B_SPLINE_SURFACE", common),
|
||||
("B_SPLINE_SURFACE_WITH_KNOTS", knots),
|
||||
("GEOMETRIC_REPRESENTATION_ITEM", vec![]),
|
||||
("RATIONAL_B_SPLINE_SURFACE", vec![list(w.iter().map(|row| reals(row)).collect())]),
|
||||
("REPRESENTATION_ITEM", vec![s()]),
|
||||
("SURFACE", vec![]),
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -45,6 +45,8 @@ pub const TABLE: &[(&str, Class)] = &[
|
||||
("sketch", Class::Layer(1)),
|
||||
// NVIDIA's SMLib behind its C API (kernel-smlib branch, ADR 0002); only the kernel uses it
|
||||
("smlib", Class::Layer(1)),
|
||||
// STEP (ISO 10303) exchange structure and geometry, no modelling backend
|
||||
("step", Class::Layer(1)),
|
||||
("kernel", Class::Layer(1)),
|
||||
("render", Class::Layer(1)),
|
||||
("doc", Class::Layer(2)),
|
||||
@@ -63,8 +65,8 @@ pub const TABLE: &[(&str, Class)] = &[
|
||||
|
||||
/// Explicit orderings *within* a layer (earlier may be used by later).
|
||||
/// The viewport draws display plans the GPU crate evaluates and draws with its pipelines; the MCP
|
||||
/// server's tests draw screenshots with them.
|
||||
pub const INTRA_LAYER_ORDER: &[&[&str]] = &[&["gpu", "ui-egui"], &["gpu", "mcp"], &["smlib", "kernel"]];
|
||||
/// server's tests draw screenshots with them. The kernel reads and writes STEP through `step`.
|
||||
pub const INTRA_LAYER_ORDER: &[&[&str]] = &[&["gpu", "ui-egui"], &["gpu", "mcp"], &["smlib", "kernel"], &["step", "kernel"]];
|
||||
|
||||
fn intra_layer_allowed(from: &str, to: &str) -> bool {
|
||||
let (from, to) = (short_name(from), short_name(to));
|
||||
|
||||
Reference in New Issue
Block a user