IGES 5.3 import and export; OBJ import; STL reading hardened
- IGES import (kernel iges_in, from the published specification). The file
is restated as STEP and read by the STEP reader, so an IGES file becomes
the same Import base feature.
- Manifold solid B-reps (186/514/510/508/504/502).
- Trimmed (144), bounded (143) and bare B-spline surfaces, sewn into a
solid by shared edges, with face orientation made consistent.
- Curves: lines, arcs, ellipses, composite and rational B-spline curves.
- Surfaces: planes, analytic surfaces 190-198, B-spline surfaces,
surfaces of revolution and tabulated cylinders.
- Transformations, units, colours, names (406 form 15 or labels).
Open, insert and drop .igs/.iges like STEP.
- IGES export (kernel iges_out): each body as a manifold solid B-rep on the
surfaces a STEP export uses, with name properties and colours.
ExportCommand to .igs.
- OBJ import as mesh bodies: objects/groups, polygons fanned, negative
indices, bad lines reported.
- STL: binary files whose header starts with "solid" and those with a zero
triangle count read as binary; non-finite vertices dropped; facet
normals ignored; ASCII with non-UTF-8 names. Mesh bodies get consistent
winding (stray inverted facets turned over).
- Tests: primitives and features round-trip through IGES (volume, area,
name, colour). Six trimmed patches sew into a closed box. Hostile IGES,
OBJ and STL inputs never panic. STL quirks. The engine exports a design
to IGES and opens it again. All 76 oracle parts round-trip through IGES
with matching volume (local check).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
7a2eb059c4
commit
51ffaf9945
13 files changed
+2615
-31
No files matched your search
+135
-23
@@ -15,6 +15,8 @@ pub use dxf::{read_dxf, write_dxf};
|
||||
pub use safe_write::{backup_path, write_atomic};
|
||||
pub use sketch2d::{Geom2, MAX_DRAWING_BYTES};
|
||||
pub use svg::read_svg;
|
||||
mod obj;
|
||||
pub use obj::read_obj;
|
||||
pub use threemf::{MAX_3MF_BYTES, MeshObject, model_xml, read_3mf, weld, write_3mf};
|
||||
|
||||
use solvecraft_doc::{Document, ModelState};
|
||||
@@ -44,9 +46,18 @@ pub type Result<T> = std::result::Result<T, IoError>;
|
||||
/// Largest STEP file we read.
|
||||
pub const MAX_STEP_BYTES: usize = 512 << 20;
|
||||
|
||||
/// Is this a STEP file name (`.step` / `.stp`, any case)?
|
||||
/// Is this a CAD exchange file read as an Import base feature: STEP (`.step` / `.stp`) or IGES
|
||||
/// (`.igs` / `.iges`), any case?
|
||||
pub fn is_step_path(path: &str) -> bool {
|
||||
std::path::Path::new(path).extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("step") || e.eq_ignore_ascii_case("stp"))
|
||||
std::path::Path::new(path)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.is_some_and(|e| ["step", "stp", "igs", "iges"].iter().any(|x| e.eq_ignore_ascii_case(x)))
|
||||
}
|
||||
|
||||
/// Is this an IGES file name (`.igs` / `.iges`, any case)?
|
||||
pub fn is_iges_path(path: &str) -> bool {
|
||||
std::path::Path::new(path).extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("igs") || e.eq_ignore_ascii_case("iges"))
|
||||
}
|
||||
|
||||
/// A STEP file read as an import feature.
|
||||
@@ -65,8 +76,11 @@ pub fn step_import_feature(bytes: &[u8], file: &str) -> Result<StepFeature> {
|
||||
if bytes.len() > MAX_STEP_BYTES {
|
||||
return Err(IoError::Invalid(format!("STEP file too large ({} MB, limit {} MB)", bytes.len() >> 20, MAX_STEP_BYTES >> 20)));
|
||||
}
|
||||
// STEP is 7-bit text with escapes; tolerate stray 8-bit bytes.
|
||||
// STEP is 7-bit text with escapes; tolerate stray 8-bit bytes. IGES is restated as STEP
|
||||
// (the feature keeps that STEP text).
|
||||
let text = String::from_utf8_lossy(bytes).into_owned();
|
||||
let (text, iges_warnings) =
|
||||
if is_iges_path(file) { solvecraft_kernel::iges_to_step(&text).map_err(IoError::Invalid)? } else { (text, Vec::new()) };
|
||||
let imp = solvecraft_kernel::step_import_shared(&text)?;
|
||||
let stem = std::path::Path::new(file)
|
||||
.file_stem()
|
||||
@@ -85,16 +99,16 @@ pub fn step_import_feature(bytes: &[u8], file: &str) -> Result<StepFeature> {
|
||||
name,
|
||||
body_names: imp.bodies.iter().map(|b| b.name.clone()).collect(),
|
||||
kind: solvecraft_doc::FeatureKind::Import { file: file_name, step: text, components: imp.tree.clone() },
|
||||
warnings: imp.warnings.clone(),
|
||||
warnings: iges_warnings.into_iter().chain(imp.warnings.iter().cloned()).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Is this a mesh file we import (`.3mf`, `.stl`, any case)?
|
||||
/// Is this a mesh file we import (`.3mf`, `.stl`, `.obj`, any case)?
|
||||
pub fn is_mesh_path(path: &str) -> bool {
|
||||
std::path::Path::new(path).extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("3mf") || e.eq_ignore_ascii_case("stl"))
|
||||
std::path::Path::new(path).extension().and_then(|e| e.to_str()).is_some_and(|e| ["3mf", "stl", "obj"].iter().any(|x| e.eq_ignore_ascii_case(x)))
|
||||
}
|
||||
|
||||
/// A 3MF or STL file read as a mesh import feature.
|
||||
/// A 3MF, STL or OBJ file read as a mesh import feature.
|
||||
pub struct MeshFeature {
|
||||
/// Timeline name: the only mesh's name, else the file name.
|
||||
pub name: String,
|
||||
@@ -103,15 +117,17 @@ pub struct MeshFeature {
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// Read 3MF or STL bytes (the format from `file`'s extension) into a MeshImport feature.
|
||||
/// Read 3MF, STL or OBJ bytes (the format from `file`'s extension) into a MeshImport feature.
|
||||
pub fn mesh_import_feature(bytes: &[u8], file: &str) -> Result<MeshFeature> {
|
||||
if bytes.len() > MAX_3MF_BYTES {
|
||||
return Err(IoError::Invalid(format!("mesh file too large ({} MB)", bytes.len() >> 20)));
|
||||
}
|
||||
let path = std::path::Path::new(file);
|
||||
let stem = path.file_stem().map(|s| s.to_string_lossy().to_string()).filter(|s| !s.trim().is_empty()).unwrap_or_else(|| "Mesh".into());
|
||||
let is_stl = path.extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("stl"));
|
||||
let (objects, warnings) = if is_stl {
|
||||
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("").to_ascii_lowercase();
|
||||
let (objects, warnings) = if ext == "obj" {
|
||||
obj::read_obj(bytes, &stem)?
|
||||
} else if ext == "stl" {
|
||||
let tris = read_stl(bytes)?;
|
||||
if tris.is_empty() {
|
||||
return Err(IoError::Invalid("STL has no triangles".into()));
|
||||
@@ -154,6 +170,7 @@ pub enum Format {
|
||||
StlAscii,
|
||||
Obj,
|
||||
Step,
|
||||
Iges,
|
||||
ThreeMf,
|
||||
Design,
|
||||
}
|
||||
@@ -167,6 +184,7 @@ impl Format {
|
||||
"stla" | "stl-ascii" => Format::StlAscii,
|
||||
"obj" => Format::Obj,
|
||||
"step" | "stp" => Format::Step,
|
||||
"igs" | "iges" => Format::Iges,
|
||||
"3mf" => Format::ThreeMf,
|
||||
"solvecraft" | "json" => Format::Design,
|
||||
_ => return Err(IoError::Format(ext)),
|
||||
@@ -263,6 +281,12 @@ pub fn export(state: &ModelState, bodies: &[String], format: Format, name: &str)
|
||||
let header = solvecraft_kernel::StepHeader { file_name: format!("{name}.step"), ..Default::default() };
|
||||
solvecraft_kernel::step_export_bodies(&bs, &header)?.into_bytes()
|
||||
}
|
||||
Format::Iges => {
|
||||
let bs: Vec<solvecraft_kernel::ExportBody> =
|
||||
sel.iter().map(|b| solvecraft_kernel::ExportBody { name: b.name.clone(), body: &b.body, color: b.body.color() }).collect();
|
||||
let header = solvecraft_kernel::StepHeader { file_name: format!("{name}.igs"), ..Default::default() };
|
||||
solvecraft_kernel::iges_export_bodies(&bs, &header)?.into_bytes()
|
||||
}
|
||||
Format::ThreeMf => {
|
||||
let objs: Vec<MeshObject> = sel
|
||||
.iter()
|
||||
@@ -333,39 +357,70 @@ pub fn write_design(doc: &Document) -> Vec<u8> {
|
||||
|
||||
/// Parse a binary or ASCII STL (used by tests and round-trip checks): triangles.
|
||||
pub fn read_stl(bytes: &[u8]) -> Result<Vec<[[f32; 3]; 3]>> {
|
||||
let ascii = bytes.starts_with(b"solid") && std::str::from_utf8(bytes).is_ok_and(|s| s.contains("facet"));
|
||||
// Binary when the size matches the triangle count in the header (some binary files start
|
||||
// with "solid" too); ASCII when the text has facets.
|
||||
let count = bytes.get(80..84).map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as usize);
|
||||
let binary_size = count.is_some_and(|n| bytes.len() == 84 + n.saturating_mul(50));
|
||||
let head = String::from_utf8_lossy(bytes.get(..bytes.len().min(512)).unwrap_or_default()).to_string();
|
||||
let ascii = !binary_size && head.trim_start().starts_with("solid") && String::from_utf8_lossy(bytes).contains("facet");
|
||||
let mut tris = Vec::new();
|
||||
let ok = |v: &[f32; 3]| v.iter().all(|x| x.is_finite() && x.abs() < 1e9);
|
||||
if ascii {
|
||||
let s = std::str::from_utf8(bytes).map_err(|_| IoError::Invalid("bad STL".into()))?;
|
||||
let s = String::from_utf8_lossy(bytes);
|
||||
let mut cur: Vec<[f32; 3]> = Vec::new();
|
||||
for l in s.lines() {
|
||||
let mut it = l.split_whitespace();
|
||||
if it.next() == Some("vertex") {
|
||||
let v: Vec<f32> = it.filter_map(|x| x.parse().ok()).collect();
|
||||
if let [x, y, z] = v[..] {
|
||||
cur.push([x, y, z]);
|
||||
match it.next() {
|
||||
Some("vertex") => {
|
||||
let v: Vec<f32> = it.filter_map(|x| x.parse().ok()).collect();
|
||||
if let [x, y, z] = v[..] {
|
||||
cur.push([x, y, z]);
|
||||
}
|
||||
}
|
||||
if let [a, b, c] = cur[..] {
|
||||
tris.push([a, b, c]);
|
||||
// Each facet takes exactly its three vertices (a bad vertex drops the facet).
|
||||
Some("endfacet") => {
|
||||
if let [a, b, c] = cur[..]
|
||||
&& [a, b, c].iter().all(ok)
|
||||
{
|
||||
tris.push([a, b, c]);
|
||||
}
|
||||
cur.clear();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if tris.len() > MAX_STL_TRIANGLES {
|
||||
return Err(IoError::Invalid(format!("STL with more than {MAX_STL_TRIANGLES} triangles")));
|
||||
}
|
||||
}
|
||||
return Ok(tris);
|
||||
}
|
||||
let n = bytes.get(80..84).map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as usize).ok_or_else(|| IoError::Invalid("short STL".into()))?;
|
||||
if bytes.len() < 84 + n.saturating_mul(50) {
|
||||
return Err(IoError::Invalid("truncated STL".into()));
|
||||
let n = count.ok_or_else(|| IoError::Invalid("short STL".into()))?;
|
||||
// A wrong count in the header (some writers leave 0): use the triangles that are there.
|
||||
let room = bytes.len().saturating_sub(84) / 50;
|
||||
let n = if n == 0 || n > room { room } else { n };
|
||||
if n == 0 {
|
||||
return Err(IoError::Invalid("STL has no triangles".into()));
|
||||
}
|
||||
let f = |o: usize| bytes.get(o..o + 4).map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).unwrap_or(0.0);
|
||||
if n > MAX_STL_TRIANGLES {
|
||||
return Err(IoError::Invalid(format!("STL with {n} triangles (limit {MAX_STL_TRIANGLES})")));
|
||||
}
|
||||
let f = |o: usize| bytes.get(o..o + 4).map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).unwrap_or(f32::NAN);
|
||||
tris.reserve(n);
|
||||
for i in 0..n {
|
||||
// The facet normal (first 12 bytes) is ignored: winding gives the orientation.
|
||||
let o = 84 + i * 50 + 12;
|
||||
let v = |k: usize| [f(o + k * 12), f(o + k * 12 + 4), f(o + k * 12 + 8)];
|
||||
tris.push([v(0), v(1), v(2)]);
|
||||
let t = [v(0), v(1), v(2)];
|
||||
if t.iter().all(ok) {
|
||||
tris.push(t);
|
||||
}
|
||||
}
|
||||
Ok(tris)
|
||||
}
|
||||
|
||||
/// Most triangles we read from one STL file.
|
||||
pub const MAX_STL_TRIANGLES: usize = 20_000_000;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -384,6 +439,63 @@ mod tests {
|
||||
(doc, m)
|
||||
}
|
||||
|
||||
/// A binary STL of a cube's triangles.
|
||||
fn cube_stl(flip: &[usize], header: &[u8], count: Option<u32>, normals: [f32; 3]) -> Vec<u8> {
|
||||
let c = |i: usize| [(i & 1) as f32 * 10.0, ((i >> 1) & 1) as f32 * 10.0, ((i >> 2) & 1) as f32 * 10.0];
|
||||
let quads = [[0, 2, 3, 1], [4, 5, 7, 6], [0, 1, 5, 4], [2, 6, 7, 3], [0, 4, 6, 2], [1, 3, 7, 5]];
|
||||
let mut tris = Vec::new();
|
||||
for q in quads {
|
||||
tris.push([c(q[0]), c(q[1]), c(q[2])]);
|
||||
tris.push([c(q[0]), c(q[2]), c(q[3])]);
|
||||
}
|
||||
for &k in flip {
|
||||
tris[k].swap(1, 2);
|
||||
}
|
||||
let mut b = header.to_vec();
|
||||
b.resize(80, b' ');
|
||||
b.extend(count.unwrap_or(tris.len() as u32).to_le_bytes());
|
||||
for t in &tris {
|
||||
for x in normals.iter().chain(t.iter().flatten()) {
|
||||
b.extend(x.to_le_bytes());
|
||||
}
|
||||
b.extend([0, 0]);
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
fn mesh_volume(bytes: &[u8]) -> f64 {
|
||||
let f = mesh_import_feature(bytes, "x.stl").unwrap();
|
||||
let solvecraft_doc::FeatureKind::MeshImport { meshes, .. } = f.kind else { panic!() };
|
||||
let m = &meshes[0];
|
||||
let pos: Vec<Vec3> = m.positions.chunks(3).map(|c| Vec3::new(c[0], c[1], c[2])).collect();
|
||||
let tris: Vec<[u32; 3]> = m.triangles.chunks(3).map(|c| [c[0], c[1], c[2]]).collect();
|
||||
solvecraft_kernel::measure(&solvecraft_kernel::mesh_body(&pos, &tris).unwrap()).unwrap().volume
|
||||
}
|
||||
|
||||
/// STL quirks: a binary header starting with "solid", a zero triangle count, garbage
|
||||
/// normals, stray inverted facets and non-finite vertices.
|
||||
#[test]
|
||||
fn stl_quirks_read_cleanly() {
|
||||
let v = mesh_volume(&cube_stl(&[], b"solid exported by something", None, [0.0; 3]));
|
||||
assert!((v - 1000.0).abs() < 1e-6, "{v}");
|
||||
let v = mesh_volume(&cube_stl(&[], b"x", Some(0), [f32::NAN, 9.0, -7.0]));
|
||||
assert!((v - 1000.0).abs() < 1e-6, "{v}");
|
||||
let v = mesh_volume(&cube_stl(&[1, 4, 7], b"x", None, [1.0, 0.0, 0.0]));
|
||||
assert!((v - 1000.0).abs() < 1e-6, "flipped facets: {v}");
|
||||
let mut bad = cube_stl(&[], b"x", None, [0.0; 3]);
|
||||
bad[84 + 12..84 + 16].copy_from_slice(&f32::INFINITY.to_le_bytes());
|
||||
assert_eq!(read_stl(&bad).unwrap().len(), 11);
|
||||
// ASCII with a non-UTF-8 name and a facet with a bad vertex.
|
||||
let mut ascii = b"solid caf\xe9\n".to_vec();
|
||||
ascii.extend(b"facet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\n");
|
||||
ascii.extend(b"facet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex nan 0 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid\n");
|
||||
assert_eq!(read_stl(&ascii).unwrap().len(), 1);
|
||||
for b in [&b""[..], b"solid", &[0u8; 83], &[0xff; 200], &cube_stl(&[], b"x", Some(u32::MAX), [0.0; 3])[..]] {
|
||||
let r = std::panic::catch_unwind(|| read_stl(b).map(|t| t.len()));
|
||||
assert!(r.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stl_round_trip_volume() {
|
||||
let (_, m) = model();
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
//! Wavefront OBJ import: polygon meshes (`v`, `f` with any of `v`, `v/vt`, `v//vn`, `v/vt/vn`,
|
||||
//! negative indices counting back), one mesh body per object (`o`, else group `g`). Polygons
|
||||
//! are split into fans. Texture coordinates, normals and materials are ignored. The file is
|
||||
//! untrusted: counts are capped and bad lines become warnings.
|
||||
|
||||
use solvecraft_geom::Vec3;
|
||||
|
||||
use crate::threemf::MeshObject;
|
||||
use crate::{IoError, Result};
|
||||
|
||||
/// Most vertices and triangles we read from one file.
|
||||
const MAX_VERTICES: usize = 20_000_000;
|
||||
const MAX_TRIANGLES: usize = 20_000_000;
|
||||
|
||||
/// Meshes of an OBJ file and what could not be read.
|
||||
pub fn read_obj(bytes: &[u8], default_name: &str) -> Result<(Vec<MeshObject>, Vec<String>)> {
|
||||
let text = String::from_utf8_lossy(bytes);
|
||||
let mut verts: Vec<Vec3> = Vec::new();
|
||||
// Objects: name and polygons as vertex indices into `verts`.
|
||||
let mut objects: Vec<(String, Vec<[u32; 3]>)> = vec![(default_name.to_string(), Vec::new())];
|
||||
let mut warnings: Vec<String> = Vec::new();
|
||||
let mut bad_lines = 0usize;
|
||||
let mut triangles = 0usize;
|
||||
let mut named_by_o = false;
|
||||
for line in text.lines() {
|
||||
let line = line.split('#').next().unwrap_or("").trim();
|
||||
let mut it = line.split_whitespace();
|
||||
match it.next() {
|
||||
Some("v") => {
|
||||
let c: Vec<f64> = it.take(3).filter_map(|x| x.parse::<f64>().ok()).collect();
|
||||
match c[..] {
|
||||
[x, y, z] if x.is_finite() && y.is_finite() && z.is_finite() => verts.push(Vec3::new(x, y, z)),
|
||||
_ => {
|
||||
// Keep the numbering: later faces count vertices by position.
|
||||
verts.push(Vec3::new(f64::NAN, f64::NAN, f64::NAN));
|
||||
bad_lines += 1;
|
||||
}
|
||||
}
|
||||
if verts.len() > MAX_VERTICES {
|
||||
return Err(IoError::Invalid(format!("OBJ with more than {MAX_VERTICES} vertices")));
|
||||
}
|
||||
}
|
||||
Some("o") | Some("g") => {
|
||||
let is_o = line.starts_with('o');
|
||||
let name = it.collect::<Vec<_>>().join(" ");
|
||||
if name.is_empty() || (!is_o && named_by_o) {
|
||||
continue;
|
||||
}
|
||||
named_by_o |= is_o;
|
||||
match objects.last_mut() {
|
||||
// An empty object takes the name instead of starting another.
|
||||
Some((n, tris)) if tris.is_empty() => *n = name,
|
||||
_ => objects.push((name, Vec::new())),
|
||||
}
|
||||
}
|
||||
Some("f") => {
|
||||
let n = verts.len() as i64;
|
||||
let idx: Option<Vec<u32>> = it
|
||||
.map(|tok| {
|
||||
let i: i64 = tok.split('/').next()?.parse().ok()?;
|
||||
let i = if i < 0 { n + i } else { i - 1 };
|
||||
(0..n).contains(&i).then(|| u32::try_from(i).ok()).flatten()
|
||||
})
|
||||
.collect();
|
||||
match idx {
|
||||
Some(p) if p.len() >= 3 && p.iter().all(|i| verts.get(*i as usize).is_some_and(|v| v.is_finite())) => {
|
||||
if let Some((_, tris)) = objects.last_mut() {
|
||||
for k in 1..p.len() - 1 {
|
||||
tris.push([p[0], p[k], p[k + 1]]);
|
||||
}
|
||||
triangles += p.len() - 2;
|
||||
}
|
||||
}
|
||||
_ => bad_lines += 1,
|
||||
}
|
||||
if triangles > MAX_TRIANGLES {
|
||||
return Err(IoError::Invalid(format!("OBJ with more than {MAX_TRIANGLES} triangles")));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if bad_lines > 0 {
|
||||
warnings.push(format!("{bad_lines} vertex or face line(s) could not be read and were left out"));
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for (name, tris) in objects {
|
||||
if tris.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Each object keeps only its own vertices.
|
||||
let mut map = std::collections::HashMap::new();
|
||||
let mut positions = Vec::new();
|
||||
let mut triangles = Vec::with_capacity(tris.len());
|
||||
for t in tris {
|
||||
let mut nt = [0u32; 3];
|
||||
for (k, i) in t.iter().enumerate() {
|
||||
let next = u32::try_from(positions.len()).map_err(|_| IoError::Invalid("OBJ too large".into()))?;
|
||||
nt[k] = *map.entry(*i).or_insert_with(|| {
|
||||
positions.push(verts.get(*i as usize).copied().unwrap_or(Vec3::ZERO));
|
||||
next
|
||||
});
|
||||
}
|
||||
triangles.push(nt);
|
||||
}
|
||||
out.push(MeshObject { name, positions, triangles, ..Default::default() });
|
||||
}
|
||||
if out.is_empty() {
|
||||
return Err(IoError::Invalid("OBJ has no faces".into()));
|
||||
}
|
||||
Ok((out, warnings))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn objects_quads_and_negative_indices() {
|
||||
let src = "# cube\no Cube\nv 0 0 0\nv 1 0 0\nv 1 1 0\nv 0 1 0\nv 0 0 1\nv 1 0 1\nv 1 1 1\nv 0 1 1\n\
|
||||
f 1 4 3 2\nf 5 6 7 8\nf 1 2 6 5\nf 2/1 3/2 7/3 6/4\nf 3//1 4//1 8//1 7//1\nf -4 -8 -5 -1\n\
|
||||
o Tri\nv 5 5 5\nv 6 5 5\nv 5 6 5\nf -3 -2 -1\n";
|
||||
let (objs, warnings) = read_obj(src.as_bytes(), "x").unwrap();
|
||||
assert!(warnings.is_empty(), "{warnings:?}");
|
||||
assert_eq!(objs.len(), 2);
|
||||
assert_eq!(objs[0].name, "Cube");
|
||||
assert_eq!(objs[0].triangles.len(), 12);
|
||||
assert_eq!(objs[0].positions.len(), 8);
|
||||
assert_eq!(objs[1].name, "Tri");
|
||||
assert_eq!(objs[1].triangles.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_obj_never_panics() {
|
||||
for src in [
|
||||
"",
|
||||
"f 1 2 3",
|
||||
"v 1 2\nf 1 1 1",
|
||||
"v nan 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3",
|
||||
"v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 99999999999999999999",
|
||||
"v 0 0 0\nv 1 0 0\nv 0 1 0\nf -1 -2 -9",
|
||||
"o\ng\nf",
|
||||
"\u{0}\u{ff}v 1 1 1",
|
||||
] {
|
||||
let r = std::panic::catch_unwind(|| read_obj(src.as_bytes(), "x").map(|(o, _)| o.len()));
|
||||
assert!(r.is_ok(), "{src:?}");
|
||||
}
|
||||
assert!(read_obj(b"v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\nf 1 2 x", "x").unwrap().1.len() == 1);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user