From 08cf223ced197fcabb1d79703d2932858b7a1962 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Wed, 7 Oct 2026 23:39:57 +0000 Subject: [PATCH] STEP export: unique ids, plain curves, right revolution senses; seams on split faces Vendored truck-stepio (writer only): an intersection curve's second surface was written at the first one's id (duplicate ids for every boolean edge), and revolved surfaces lost their orientation (the modeling-surface sense was always true; inverted or mirrored revolutions came out inside out). Export now writes intersection edges as C1 cubic B-splines through exact points, so files carry plain 3D curves and no redundant surfaces. Vendored truck-meshalgo: hinted parameter searches that run off a B-spline's domain are rejected or clamped (meshing their grid never finished). STEP import: straight revolution profiles are bounded in the meridian plane and stop at the axis; loops on revolutions start away from the axis; face extents include edge end points. Edges between pieces of one split surface draw as seams. kernel::step_validate checks a STEP file (duplicate ids, dangling references, dense numbering); the oracle replay now exports every rebuilt part, validates it and re-imports it with no warnings and the same volume (all 32 rebuildable cases pass). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL --- ATTRIBUTION.md | 2 + Cargo.lock | 4 - Cargo.toml | 2 + apps/solvecraft-cli/src/oracle.rs | 28 + crates/kernel/src/lib.rs | 2 +- crates/kernel/src/step.rs | 61 +- crates/kernel/src/step_in/brep.rs | 23 +- crates/kernel/src/step_in/geom.rs | 55 +- crates/kernel/src/step_in/mod.rs | 32 + crates/kernel/src/step_in/tests.rs | 62 +- crates/kernel/src/topo.rs | 6 +- docs/step-import.md | 93 +- vendor/README.md | 23 + vendor/truck-meshalgo/Cargo.toml | 113 + vendor/truck-meshalgo/LICENSE | 177 ++ vendor/truck-meshalgo/README.md | 68 + .../truck-meshalgo/src/analyzers/collision.rs | 275 ++ .../src/analyzers/in_out_judge.rs | 140 + vendor/truck-meshalgo/src/analyzers/mod.rs | 17 + .../point_cloud/hashed_point_cloud.rs | 283 ++ .../src/analyzers/point_cloud/mod.rs | 84 + .../analyzers/point_cloud/sort_end_points.rs | 121 + .../truck-meshalgo/src/analyzers/splitting.rs | 343 ++ .../truck-meshalgo/src/analyzers/topology.rs | 117 + vendor/truck-meshalgo/src/analyzers/volume.rs | 175 ++ vendor/truck-meshalgo/src/common/adjacency.rs | 77 + .../truck-meshalgo/src/common/face_normal.rs | 26 + vendor/truck-meshalgo/src/common/mod.rs | 6 + .../truck-meshalgo/src/common/triangulate.rs | 105 + vendor/truck-meshalgo/src/filters/mod.rs | 12 + .../src/filters/normal_filters.rs | 396 +++ .../truck-meshalgo/src/filters/optimizing.rs | 441 +++ .../truck-meshalgo/src/filters/structuring.rs | 242 ++ .../truck-meshalgo/src/filters/subdivision.rs | 199 ++ vendor/truck-meshalgo/src/lib.rs | 56 + vendor/truck-meshalgo/src/tessellation/mod.rs | 360 +++ .../src/tessellation/triangulation.rs | 705 +++++ vendor/truck-meshalgo/src/vtk.rs | 525 ++++ vendor/truck-stepio/Cargo.toml | 109 + vendor/truck-stepio/LICENSE | 177 ++ vendor/truck-stepio/README.md | 21 + vendor/truck-stepio/src/in/alias.rs | 397 +++ vendor/truck-stepio/src/in/mod.rs | 2784 +++++++++++++++++ vendor/truck-stepio/src/lib.rs | 78 + vendor/truck-stepio/src/out/geometry.rs | 813 +++++ vendor/truck-stepio/src/out/mod.rs | 388 +++ vendor/truck-stepio/src/out/topology.rs | 493 +++ 47 files changed, 10677 insertions(+), 39 deletions(-) create mode 100644 vendor/truck-meshalgo/Cargo.toml create mode 100644 vendor/truck-meshalgo/LICENSE create mode 100644 vendor/truck-meshalgo/README.md create mode 100644 vendor/truck-meshalgo/src/analyzers/collision.rs create mode 100644 vendor/truck-meshalgo/src/analyzers/in_out_judge.rs create mode 100644 vendor/truck-meshalgo/src/analyzers/mod.rs create mode 100644 vendor/truck-meshalgo/src/analyzers/point_cloud/hashed_point_cloud.rs create mode 100644 vendor/truck-meshalgo/src/analyzers/point_cloud/mod.rs create mode 100644 vendor/truck-meshalgo/src/analyzers/point_cloud/sort_end_points.rs create mode 100644 vendor/truck-meshalgo/src/analyzers/splitting.rs create mode 100644 vendor/truck-meshalgo/src/analyzers/topology.rs create mode 100644 vendor/truck-meshalgo/src/analyzers/volume.rs create mode 100644 vendor/truck-meshalgo/src/common/adjacency.rs create mode 100644 vendor/truck-meshalgo/src/common/face_normal.rs create mode 100644 vendor/truck-meshalgo/src/common/mod.rs create mode 100644 vendor/truck-meshalgo/src/common/triangulate.rs create mode 100644 vendor/truck-meshalgo/src/filters/mod.rs create mode 100644 vendor/truck-meshalgo/src/filters/normal_filters.rs create mode 100644 vendor/truck-meshalgo/src/filters/optimizing.rs create mode 100644 vendor/truck-meshalgo/src/filters/structuring.rs create mode 100644 vendor/truck-meshalgo/src/filters/subdivision.rs create mode 100644 vendor/truck-meshalgo/src/lib.rs create mode 100644 vendor/truck-meshalgo/src/tessellation/mod.rs create mode 100644 vendor/truck-meshalgo/src/tessellation/triangulation.rs create mode 100644 vendor/truck-meshalgo/src/vtk.rs create mode 100644 vendor/truck-stepio/Cargo.toml create mode 100644 vendor/truck-stepio/LICENSE create mode 100644 vendor/truck-stepio/README.md create mode 100644 vendor/truck-stepio/src/in/alias.rs create mode 100644 vendor/truck-stepio/src/in/mod.rs create mode 100644 vendor/truck-stepio/src/lib.rs create mode 100644 vendor/truck-stepio/src/out/geometry.rs create mode 100644 vendor/truck-stepio/src/out/mod.rs create mode 100644 vendor/truck-stepio/src/out/topology.rs diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 67702f62..0e3b5044 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -38,4 +38,6 @@ Generated-in-code assets are original and have no file to list: | Directory | Source | Licence | Changes | |---|---|---|---| +| vendor/truck-meshalgo | truck-meshalgo 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | meshing: parameter searches kept inside the surface domain (vendor/README.md) | +| vendor/truck-stepio | truck-stepio 0.3.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | STEP writer: intersection-curve surfaces written at unique ids (vendor/README.md) | | vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; trace macro (vendor/README.md) | diff --git a/Cargo.lock b/Cargo.lock index 1d81a203..e311fe56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3525,8 +3525,6 @@ dependencies = [ [[package]] name = "truck-meshalgo" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e18f325738e822c79bb1a48897ec789cbf50cbeeb0841105295f9da85bdf3613" dependencies = [ "array-macro", "derive_more", @@ -3591,8 +3589,6 @@ dependencies = [ [[package]] name = "truck-stepio" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d29cf5b0dec4b927e80b5dca26ce3ca69601c77ea5b9c953e30bfc3e123d189" dependencies = [ "chrono", "truck-geometry", diff --git a/Cargo.toml b/Cargo.toml index 55ee44eb..57ac3cf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,3 +81,5 @@ debug = 0 # truck-shapeops with SolveCraft fixes (Apache-2.0, see vendor/truck-shapeops and ATTRIBUTION.md). [patch.crates-io] truck-shapeops = { path = "vendor/truck-shapeops" } +truck-stepio = { path = "vendor/truck-stepio" } +truck-meshalgo = { path = "vendor/truck-meshalgo" } diff --git a/apps/solvecraft-cli/src/oracle.rs b/apps/solvecraft-cli/src/oracle.rs index ceb9089f..85264b7e 100644 --- a/apps/solvecraft-cli/src/oracle.rs +++ b/apps/solvecraft-cli/src/oracle.rs @@ -44,9 +44,37 @@ fn check_case(dir: &str) -> Value { num("faces", got["total"]["faces"].as_f64(), want["total"]["faces"].as_f64(), 0.0); num("edges", got["total"]["edges"].as_f64(), want["total"]["edges"].as_f64(), 0.0); num("vertices", got["total"]["vertices"].as_f64(), want["total"]["vertices"].as_f64(), 0.0); + // Our STEP export of the rebuilt part: well formed, and read back with no warnings and the + // same volume. + let (ok, note) = step_export_check(&s, got["total"]["volume_mm3"].as_f64().unwrap_or(0.0)); + pass &= ok; + checks.push(json!({"check": "step_export", "got": note, "want": "valid, re-imports cleanly", "ok": ok})); json!({"case": name, "pass": pass, "checks": checks}) } +/// Export the session's bodies as STEP, validate the file and import it again. +fn step_export_check(s: &solvecraft_engine::Session, volume: f64) -> (bool, String) { + let st = s.model.state(); + let bytes = match solvecraft_engine::io::export(&st, &[], solvecraft_engine::io::Format::Step, "oracle") { + Ok(b) => b, + Err(e) => return (false, format!("export: {e}")), + }; + let text = String::from_utf8_lossy(&bytes); + if let Err(e) = solvecraft_engine::kernel::step_validate(&text) { + return (false, format!("invalid: {e}")); + } + match solvecraft_engine::kernel::step_import(&text) { + Ok(imp) if !imp.warnings.is_empty() => (false, format!("warnings: {}", imp.warnings.join("; "))), + Ok(imp) if imp.bodies.len() != st.bodies.len() => (false, format!("{} bodies back of {}", imp.bodies.len(), st.bodies.len())), + Ok(imp) => { + let v: f64 = imp.bodies.iter().filter_map(|b| solvecraft_engine::kernel::measure(&b.body).ok()).map(|m| m.volume).sum(); + let ok = (v - volume).abs() <= REL_TOL * volume.abs().max(1.0); + (ok, format!("re-imported volume {v:.3}")) + } + Err(e) => (false, format!("import: {e}")), + } +} + /// Import a case's `part.step` (as File → Open does) and compare with Fusion's measurements: /// body count, volume and area (relative 1e-3) and the faces read from the file. fn check_step_case(dir: &str) -> Value { diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 284aa78b..80967edf 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -36,7 +36,7 @@ pub use ops::{BoolOp, boolean, split_by_plane, transform, transform_matrix}; pub use polybool::planar_boolean; pub use polyhedron::{HalfSpace, convex_polyhedron, draft, offset_faces, shell}; pub use step::step_export; -pub use step_in::{ImportedBody, StepImport, StepNode, step_import, step_import_shared}; +pub use step_in::{ImportedBody, StepImport, StepNode, step_import, step_import_shared, step_validate}; pub use topo::{CylinderFace, cylinder_face_at}; pub use topo::{TopoCounts, merged_topology, seam_flags}; diff --git a/crates/kernel/src/step.rs b/crates/kernel/src/step.rs index 0be173c4..d6d65d2c 100644 --- a/crates/kernel/src/step.rs +++ b/crates/kernel/src/step.rs @@ -1,8 +1,67 @@ +use truck_modeling as mt; use truck_stepio::out; use crate::body::Body; use crate::{KernelError, Result, guard}; +/// Intersection-curve edges (from booleans) as cubic B-splines through the exact curve: STEP +/// readers get plain 3D geometry that lies on both faces, and the file does not repeat the two +/// surfaces for every such edge. +fn plain_curves(b: &Body) -> mt::Solid { + let s = b.deep_copy(); + let size = b.size(); + let mut seen = std::collections::HashSet::new(); + for e in s.edge_iter() { + if !seen.insert(e.id()) { + continue; + } + if let mt::Curve::IntersectionCurve(ic) = e.curve() + && let Some(c) = hermite_through(&ic, size * 1e-4) + { + e.set_curve(mt::Curve::BSplineCurve(c)); + } + } + s +} + +/// A C¹ cubic B-spline through exact points of `c` spaced so that chords deviate at most `tol` +/// (Catmull–Rom tangents over chord length; the cubic is far closer than the chords). +fn hermite_through(c: &C, tol: f64) -> Option> +where + C: mt::ParametricCurve + mt::BoundedCurve + mt::ParameterDivision1D, +{ + use mt::{InnerSpace, MetricSpace}; + let (_, pts) = c.parameter_division(c.range_tuple(), tol); + let pts: Vec = pts.into_iter().fold(Vec::new(), |mut v: Vec, p| { + if v.last().is_none_or(|q| q.distance(p) > tol * 1e-3) { + v.push(p); + } + v + }); + let n = pts.len().checked_sub(1).filter(|n| *n >= 1 && *n < 1_000_000)?; + let mut u = vec![0.0; n + 1]; + for i in 1..=n { + u[i] = u[i - 1] + pts[i].distance(pts[i - 1]); + } + let tangent = |i: usize| { + let (a, b) = (i.saturating_sub(1), (i + 1).min(n)); + (pts[b] - pts[a]) / (u[b] - u[a]) + }; + let mut ctrl = vec![pts[0]]; + let mut knots = vec![u[0]; 4]; + for i in 0..n { + let h = u[i + 1] - u[i]; + ctrl.push(pts[i] + tangent(i) * (h / 3.0)); + ctrl.push(pts[i + 1] - tangent(i + 1) * (h / 3.0)); + ctrl.push(pts[i + 1]); + knots.extend(std::iter::repeat_n(u[i + 1], if i + 1 == n { 4 } else { 3 })); + } + if ctrl.iter().any(|p| !(p.x.is_finite() && p.y.is_finite() && p.z.is_finite())) || tangent(0).magnitude() == 0.0 { + return None; + } + mt::BSplineCurve::try_new(mt::KnotVec::from(knots), ctrl).ok() +} + /// STEP (ISO 10303-21) text for the given bodies. pub fn step_export(bodies: &[&Body], system: &str) -> Result { if bodies.is_empty() { @@ -12,7 +71,7 @@ pub fn step_export(bodies: &[&Body], system: &str) -> Result { b.require_brep("STEP export")?; } guard("step export", || { - let cs: Vec<_> = bodies.iter().map(|b| b.solid.compress()).collect(); + let cs: Vec<_> = bodies.iter().map(|b| plain_curves(b).compress()).collect(); let models: out::StepModels<_, _, _> = cs.iter().collect(); let header = out::StepHeaderDescriptor { organization_system: system.to_owned(), ..Default::default() }; Ok(out::CompleteStepDisplay::new(models, header).to_string()) diff --git a/crates/kernel/src/step_in/brep.rs b/crates/kernel/src/step_in/brep.rs index dae0cd79..334a734c 100644 --- a/crates/kernel/src/step_in/brep.rs +++ b/crates/kernel/src/step_in/brep.rs @@ -209,7 +209,7 @@ impl Builder { .flat_map(|e| { let c = e.curve(); let (t0, t1) = c.range_tuple(); - (0..8).map(move |i| c.subs(t0 + (t1 - t0) * i as f64 / 8.0)).collect::>() + (0..=8).map(move |i| c.subs(t0 + (t1 - t0) * i as f64 / 8.0)).collect::>() }) .collect(); // Seam edges: used twice by the face's loops. @@ -236,6 +236,27 @@ impl Builder { guard("step surface", || geom::surface(cx, sid, &extent, &seam, sense, 0).map_err(crate::KernelError::Failed)) .map_err(|e| e.to_string())? }; + // Loops on a surface of revolution start away from the axis: a pole (cone apex, sphere + // pole) has no definite rotation parameter, and the mesher takes the first point's + // parameter as the anchor for the rest of the loop. + let wires: Vec = match &surface { + mt::Surface::RevolutedCurve(rc) => { + let (o, a) = (rc.entity().origin(), rc.entity().axis()); + let dist = |e: &mt::Edge| { + let v = e.front().point() - o; + (v - a * v.dot(a)).magnitude() + }; + wires + .into_iter() + .map(|w| { + let edges: Vec = w.edge_iter().cloned().collect(); + let start = edges.iter().enumerate().max_by(|x, y| dist(x.1).total_cmp(&dist(y.1))).map(|(i, _)| i).unwrap_or(0); + edges.iter().skip(start).chain(edges.iter().take(start)).cloned().collect::>().into() + }) + .collect() + } + _ => wires, + }; match mt::Face::try_new(wires.clone(), surface.clone()) { Ok(f) => Ok(f), // Faces on periodic surfaces run along a seam edge twice: valid in STEP. diff --git a/crates/kernel/src/step_in/geom.rs b/crates/kernel/src/step_in/geom.rs index c128baf8..b4e1f08f 100644 --- a/crates/kernel/src/step_in/geom.rs +++ b/crates/kernel/src/step_in/geom.rs @@ -611,6 +611,55 @@ fn tube_angle(f: &Frame, big: f64, p: mt::Point3) -> f64 { z.atan2(rho - big) } +/// A straight profile of a surface of revolution, bounded by the face: boundary points are +/// turned into the profile's meridian plane and projected onto the line, and the line stops at +/// the axis (beyond it the surface would fold over itself). +fn revolved_line(p: mt::Point3, d: mt::Vector3, o: mt::Point3, axis: mt::Vector3, extent: &[mt::Point3]) -> R { + let radial = |q: mt::Point3| { + let v = q - o; + v - axis * v.dot(axis) + }; + // The meridian half-plane of the profile. + let e = { + let r = radial(p); + let r2 = radial(p + d); + let r = if r.magnitude() >= r2.magnitude() { r } else { r2 }; + if r.magnitude() < 1e-12 { + return Err("profile line on the axis".into()); + } + r / r.magnitude() + }; + let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY); + for q in extent { + let v = q - o; + let h = v.dot(axis); + let r = radial(*q).magnitude(); + let t = (o + axis * h + e * r - p).dot(d); + lo = lo.min(t); + hi = hi.max(t); + } + if !(lo.is_finite() && hi.is_finite()) { + return Err("surface of revolution without extent".into()); + } + let m = (hi - lo) * 0.02 + 1e-6; + let (mut lo, mut hi) = (lo - m, hi + m); + // Where the line meets the axis (radial component along e vanishes). + let de = d.dot(e); + if de.abs() > 1e-12 { + let t_axis = -(p - o).dot(e) / de; + // The meridian half-plane has a non-negative radius along `e`. + if de > 0.0 { + lo = lo.max(t_axis); + } else { + hi = hi.min(t_axis); + } + } + if hi - lo <= 1e-12 { + return Err("degenerate surface of revolution".into()); + } + Ok(mt::Curve::Line(mt::Line(p + d * lo, p + d * hi))) +} + fn revolved(curve: mt::Curve, o: mt::Point3, axis: mt::Vector3) -> mt::Surface { mt::Surface::RevolutedCurve(mt::Processor::new(mt::RevolutedCurve::by_revolution(curve, o, axis))) } @@ -802,8 +851,12 @@ pub(crate) fn surface(cx: &Ctx, id: u64, extent: &[mt::Point3], seam: &[mt::Poin "SURFACE_OF_REVOLUTION" => { let g = curve(cx, p.get(1).and_then(Param::as_ref_id).ok_or("revolution without profile")?, depth + 1)?; let (o, axis) = axis1(cx, p.get(2).and_then(Param::as_ref_id).ok_or("revolution without axis")?)?; + let profile = match &g { + CurveGeo::Line { p, d } => revolved_line(*p, *d, o, axis, extent)?, + _ => bounded_curve(&g, extent)?, + }; // Reversed sense: turn the other way round the axis. - Ok(revolved(bounded_curve(&g, extent)?, o, axis * sign)) + Ok(revolved(profile, o, axis * sign)) } "SURFACE_OF_LINEAR_EXTRUSION" => { let g = curve(cx, p.get(1).and_then(Param::as_ref_id).ok_or("extrusion without profile")?, depth + 1)?; diff --git a/crates/kernel/src/step_in/mod.rs b/crates/kernel/src/step_in/mod.rs index 14cd425c..b5a01b92 100644 --- a/crates/kernel/src/step_in/mod.rs +++ b/crates/kernel/src/step_in/mod.rs @@ -576,6 +576,38 @@ impl<'a> Reader<'a> { } } +/// Check a STEP file's structure: it parses, no entity id is defined twice, every reference +/// names a defined entity, and ids are dense (`#1`…`#n`). Returns the entity count. +pub fn step_validate(text: &str) -> std::result::Result { + let ex = p21::parse(text)?; + if let Some(d) = ex.duplicates.first() { + return Err(format!("{} duplicate entity ids (first #{d})", ex.duplicates.len())); + } + fn refs(p: &Param, out: &mut Vec) { + match p { + Param::Ref(r) => out.push(*r), + Param::List(v) | Param::Typed(_, v) => v.iter().for_each(|x| refs(x, out)), + _ => {} + } + } + let mut ids: Vec<&u64> = ex.entities.keys().collect(); + ids.sort(); + for id in &ids { + let mut out = Vec::new(); + if let Some(e) = ex.get(**id) { + e.records.iter().flat_map(|r| &r.params).for_each(|p| refs(p, &mut out)); + } + if let Some(r) = out.iter().find(|r| ex.get(**r).is_none()) { + return Err(format!("#{id} refers to undefined #{r}")); + } + } + let n = ex.entities.len(); + if ids.last().is_some_and(|m| **m != n as u64) || ids.first().is_some_and(|m| **m != 1) { + return Err(format!("entity ids are not dense: {n} entities, #{}…#{}", ids.first().map_or(0, |x| **x), ids.last().map_or(0, |x| **x))); + } + Ok(n) +} + /// Recent imports, so a file read for a command and then evaluated in the timeline is only /// read once. static RECENT: std::sync::Mutex)>> = std::sync::Mutex::new(Vec::new()); diff --git a/crates/kernel/src/step_in/tests.rs b/crates/kernel/src/step_in/tests.rs index fdf4b739..a45c042d 100644 --- a/crates/kernel/src/step_in/tests.rs +++ b/crates/kernel/src/step_in/tests.rs @@ -10,10 +10,10 @@ fn rel(a: f64, b: f64) -> f64 { /// Export → import → same volume, area and face count. fn round_trip(b: &Body) -> StepImport { let text = step_export(&[b], "test").unwrap(); + step_validate(&text).unwrap(); let imp = step_import(&text).unwrap(); assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings); - // truck's writer repeats entity ids for the surfaces of intersection curves (unused here). - assert!(imp.warnings.iter().all(|w| w.contains("defined twice")), "{:?}", imp.warnings); + assert!(imp.warnings.is_empty(), "{:?}", imp.warnings); let (m0, m1) = (measure(b).unwrap(), measure(&imp.bodies[0].body).unwrap()); assert!(rel(m1.volume, m0.volume) < 1e-3, "volume {} vs {}", m1.volume, m0.volume); assert!(rel(m1.area, m0.area) < 1e-3, "area {} vs {}", m1.area, m0.area); @@ -306,3 +306,61 @@ fn exponential_assemblies_are_bounded() { assert!(!imp.bodies.is_empty() && imp.bodies.len() <= MAX_BODIES); assert!(imp.warnings.iter().any(|w| w.contains("left out")), "{:?}", imp.warnings); } + +#[test] +fn validator_catches_broken_files() { + assert!(step_validate(&format!("{HEAD}#1=A(#2);\n#2=B();\n{TAIL}")).is_ok()); + assert!(step_validate(&format!("{HEAD}#1=A(#2);\n#2=B();\n#2=C();\n{TAIL}")).unwrap_err().contains("duplicate")); + assert!(step_validate(&format!("{HEAD}#1=A((#7));\n{TAIL}")).unwrap_err().contains("undefined")); + assert!(step_validate(&format!("{HEAD}#1=A();\n#5=B();\n{TAIL}")).unwrap_err().contains("dense")); +} + +#[test] +fn split_band_edges_are_seams() { + // A quarter pipe (torus R 10, r 3) whose face wraps around the tube with a seam edge: it is + // split for the kernel, and the split edge is drawn as a seam. + let mut t = String::from(HEAD); + t += "#1=CARTESIAN_POINT('',(0.,0.,0.));\n#2=DIRECTION('',(0.,0.,1.));\n#3=DIRECTION('',(1.,0.,0.));\n#4=AXIS2_PLACEMENT_3D('',#1,#2,#3);\n"; + t += "#5=TOROIDAL_SURFACE('',#4,10.,3.);\n"; + t += "#10=CARTESIAN_POINT('',(13.,0.,0.));\n#11=CARTESIAN_POINT('',(0.,13.,0.));\n#12=VERTEX_POINT('',#10);\n#13=VERTEX_POINT('',#11);\n"; + // Tube circles at azimuth 0 (plane XZ) and 90 degrees (plane YZ), the seam around Z. + t += "#20=CARTESIAN_POINT('',(10.,0.,0.));\n#21=DIRECTION('',(0.,-1.,0.));\n#22=AXIS2_PLACEMENT_3D('',#20,#21,#3);\n#23=CIRCLE('',#22,3.);\n"; + t += "#24=CARTESIAN_POINT('',(0.,10.,0.));\n#25=DIRECTION('',(1.,0.,0.));\n#26=DIRECTION('',(0.,1.,0.));\n#27=AXIS2_PLACEMENT_3D('',#24,#25,#26);\n#28=CIRCLE('',#27,3.);\n"; + t += "#29=CIRCLE('',#4,13.);\n"; + t += "#30=EDGE_CURVE('',#12,#12,#23,.T.);\n#31=EDGE_CURVE('',#13,#13,#28,.T.);\n#32=EDGE_CURVE('',#12,#13,#29,.T.);\n"; + t += "#40=ORIENTED_EDGE('',*,*,#30,.T.);\n#41=ORIENTED_EDGE('',*,*,#32,.T.);\n#42=ORIENTED_EDGE('',*,*,#31,.F.);\n#43=ORIENTED_EDGE('',*,*,#32,.F.);\n"; + t += "#44=EDGE_LOOP('',(#40,#41,#42,#43));\n#45=FACE_OUTER_BOUND('',#44,.T.);\n#46=ADVANCED_FACE('',(#45),#5,.F.);\n"; + t += "#47=PLANE('',#22);\n#48=ORIENTED_EDGE('',*,*,#30,.F.);\n#49=EDGE_LOOP('',(#48));\n#50=FACE_OUTER_BOUND('',#49,.T.);\n#51=ADVANCED_FACE('',(#50),#47,.T.);\n"; + t += "#52=PLANE('',#27);\n#53=ORIENTED_EDGE('',*,*,#31,.T.);\n#54=EDGE_LOOP('',(#53));\n#55=FACE_OUTER_BOUND('',#54,.T.);\n#56=ADVANCED_FACE('',(#55),#52,.T.);\n"; + t += "#60=CLOSED_SHELL('',(#46,#51,#56));\n#61=MANIFOLD_SOLID_BREP('Pipe',#60);\n#62=SHAPE_REPRESENTATION('',(#61),$);\n"; + t += TAIL; + let imp = step_import(&t).unwrap(); + let ib = &imp.bodies[0]; + assert_eq!(ib.file_faces, 3); + assert_eq!(ib.body.face_count(), 4, "{:?}", imp.warnings); + // The two halves of the tube face make a quarter torus: area π²·R·r. + let curved: f64 = ib.body.faces(1e-3).unwrap().iter().filter(|f| f.plane_normal.is_none()).map(|f| f.area).sum(); + let want = std::f64::consts::PI.powi(2) * 30.0; + assert!(rel(curved, want) < 2e-3, "{curved} vs {want}"); + let m = ib.body.display_mesh(ib.body.size() * 1e-3).unwrap(); + // Seams: the file's own seam (inside the tube face) and the split edge. + assert!(m.seams.iter().filter(|s| **s).count() >= 2, "{:?}", m.seams); +} + +/// Revolved faces of a mirrored tool keep their orientation through STEP (the writer used to +/// drop an inverted or mirrored revolution's sense). +#[test] +fn mirrored_revolved_faces_round_trip() { + for mirror in [false, true] { + let pl = Plane { origin: Vec3::new(20.0, 15.0, 0.0), ..Plane::XZ }; + let prof = + Region2 { outer: Loop2::polygon(&[Vec2::new(0.0, 4.0), Vec2::new(3.0, 5.0), Vec2::new(3.0, 12.0), Vec2::new(0.0, 12.0)]), holes: vec![] }; + let mut tool = revolve(&pl, &[prof], Vec2::ZERO, Vec2::Y, std::f64::consts::TAU).unwrap().pop().unwrap(); + if mirror { + tool = + crate::transform_matrix(&tool, [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, -1.0, 0.0], [0.0, 0.0, 16.0, 1.0]]).unwrap(); + } + let plate = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 10.0)).unwrap(); + round_trip(&boolean(&plate, &tool, BoolOp::Cut).unwrap().unwrap()); + } +} diff --git a/crates/kernel/src/topo.rs b/crates/kernel/src/topo.rs index fba748fa..6033fea3 100644 --- a/crates/kernel/src/topo.rs +++ b/crates/kernel/src/topo.rs @@ -239,6 +239,9 @@ pub fn seam_flags(b: &Body, mesh: &Mesh) -> Vec { let tol = (b.size() * 1e-4).max(1e-6); let surfs = classify_faces(mesh, nf, tol); let verts = face_vertices(mesh, nf); + // Faces cut from one surface (an imported face split where it wraps around a closed + // surface) carry the very same surface: the edge between them is a seam. + let surf_keys: Vec = solid.face_iter().map(|f| format!("{:?}", f.surface())).collect(); let mut faces_of: HashMap> = HashMap::new(); let mut order: Vec = Vec::new(); for (fi, f) in solid.face_iter().enumerate() { @@ -258,7 +261,8 @@ pub fn seam_flags(b: &Body, mesh: &Mesh) -> Vec { .enumerate() .map(|(ei, k)| match faces_of.get(k).map(Vec::as_slice) { Some([a, b2]) => { - !matches!(surfs.get(*a), Some(Surf::Plane { .. })) && same_surface(&surfs, &verts, mesh.edges.get(ei), *a, *b2, tol * 10.0) + (!matches!(surfs.get(*a), Some(Surf::Plane { .. })) && same_surface(&surfs, &verts, mesh.edges.get(ei), *a, *b2, tol * 10.0)) + || (surf_keys.get(*a).is_some_and(|k| Some(k) == surf_keys.get(*b2)) && !matches!(surfs.get(*a), Some(Surf::Plane { .. }))) } _ => false, }) diff --git a/docs/step-import.md b/docs/step-import.md index 43f1134d..54eb5831 100644 --- a/docs/step-import.md +++ b/docs/step-import.md @@ -12,36 +12,73 @@ Read: manifold solid breps (with voids), faceted breps, shell-based surface mode Generated by `cargo xtask step-corpus`. Each case is a part exported from Fusion as STEP (AP214; kept locally in `plan/fusion/oracle/`, never committed). SolveCraft opens `part.step` exactly as File → Open does (an Import base feature) and compares with Fusion's own measurements: body count, volume and area (relative tolerance 1e-3) and the number of faces read from the file. -**29 / 29 files pass.** +**60 / 66 files pass.** | Case | Result | Bodies | Volume mm³ (got / Fusion) | Area mm² (got / Fusion) | Faces (got / Fusion) | ms | Notes | |---|---|---|---|---|---|---|---| -| 01-box | pass | Box | 24000.000 / 24000.000 | 5200.000 / 5200.000 | 6 / 6 | 2 | | -| 02-cylinder | pass | Cylinder | 35339.346 / 35342.917 | 6125.844 / 6126.106 | 3 / 3 | 7 | | -| 03-box-through-hole | pass | Box | 45738.489 / 45738.053 | 9327.795 / 9327.788 | 7 / 7 | 4 | | -| 04-revolve-vase | pass | Vase | 112487.028 / 112499.479 | 13156.053 / 13156.615 | 5 / 5 | 189 | | -| 05-taper-extrude | pass | Frustum | 61814.826 / 61814.826 | 9676.915 / 9676.915 | 6 / 6 | 1 | | +| 01-box | pass | Box | 24000.000 / 24000.000 | 5200.000 / 5200.000 | 6 / 6 | 16 | | +| 02-cylinder | pass | Cylinder | 35339.346 / 35342.917 | 6125.844 / 6126.106 | 3 / 3 | 37 | | +| 03-box-through-hole | pass | Box | 45738.489 / 45738.053 | 9327.795 / 9327.788 | 7 / 7 | 28 | | +| 04-revolve-vase | pass | Vase | 112487.028 / 112499.479 | 13156.053 / 13156.615 | 5 / 5 | 371 | | +| 05-taper-extrude | pass | Frustum | 61814.826 / 61814.826 | 9676.915 / 9676.915 | 6 / 6 | 19 | | | 06-symmetric-extrude | pass | Block | 30000.000 / 30000.000 | 6200.000 / 6200.000 | 6 / 6 | 1 | | -| 07-fillet-all-edges | pass | Box | 23340.395 / 23340.849 | 4726.169 / 4726.265 | 26 / 26 | 18 | | -| 08-chamfer | pass | Box | 23730.667 / 23730.667 | 5029.352 / 5029.352 | 10 / 10 | 1 | | -| 09-shell | pass | Box | 13632.000 / 13632.000 | 13992.000 / 13992.000 | 11 / 11 | 1 | | -| 10-rect-pattern-holes | pass | Plate | 29803.869 / 29803.540 | 14652.467 / 14652.389 | 18 / 18 | 76 | | -| 11-circular-pattern-flange | pass | Flange | 66758.484 / 66758.844 | 19320.589 / 19320.795 | 10 / 10 | 50 | | -| 12-mirror-feature | pass | Base | 21392.261 / 21392.920 | 6530.917 / 6530.973 | 10 / 10 | 7 | | -| 13-combine-join | pass | A | 45547.074 / 45571.680 | 8633.463 / 8634.336 | 9 / 9 | 5 | | -| 14-combine-cut | pass | A | 42934.678 / 42931.417 | 9706.604 / 9706.858 | 7 / 7 | 61 | | -| 15-combine-intersect | pass | A | 54442.787 / 54454.273 | 7381.985 / 7382.743 | 7 / 7 | 134 | | -| 16-sweep | pass | Tube | 2584.167 / 2584.724 | 1779.580 / 1779.698 | 5 / 5 | 304 | | -| 17-loft | pass | Loft | 35124.785 / 35125.498 | 6496.433 / 6495.000 | 6 / 6 | 148 | | -| 18-hole-features | pass | Plate | 33890.986 / 33890.710 | 8568.685 / 8568.696 | 13 / 13 | 63 | | -| 19-l-bracket-constrained | pass | LBracket | 14720.000 / 14720.000 | 5472.000 / 5472.000 | 8 / 8 | 1 | | -| 20-slot-constrained | pass | Slot | 2965.374 / 2965.487 | 1774.635 / 1774.690 | 6 / 6 | 17 | | +| 07-fillet-all-edges | pass | Box | 23340.395 / 23340.849 | 4726.168 / 4726.265 | 26 / 26 | 63 | | +| 08-chamfer | pass | Box | 23730.667 / 23730.667 | 5029.352 / 5029.352 | 10 / 10 | 12 | | +| 09-shell | pass | Box | 13632.000 / 13632.000 | 13992.000 / 13992.000 | 11 / 11 | 8 | | +| 10-rect-pattern-holes | pass | Plate | 29803.869 / 29803.540 | 14652.467 / 14652.389 | 18 / 18 | 270 | | +| 11-circular-pattern-flange | pass | Flange | 66758.484 / 66758.844 | 19320.589 / 19320.795 | 10 / 10 | 90 | | +| 12-mirror-feature | pass | Base | 21392.261 / 21392.920 | 6530.917 / 6530.973 | 10 / 10 | 16 | | +| 13-combine-join | pass | A | 45547.074 / 45571.680 | 8633.463 / 8634.336 | 9 / 9 | 19 | | +| 14-combine-cut | pass | A | 42934.679 / 42931.417 | 9706.604 / 9706.858 | 7 / 7 | 95 | | +| 15-combine-intersect | pass | A | 54442.787 / 54454.273 | 7381.985 / 7382.743 | 7 / 7 | 223 | | +| 16-sweep | pass | Tube | 2584.167 / 2584.724 | 1779.580 / 1779.698 | 5 / 5 | 430 | | +| 17-loft | pass | Loft | 35124.785 / 35125.498 | 6496.433 / 6495.000 | 6 / 6 | 91 | | +| 18-hole-features | pass | Plate | 33890.986 / 33890.710 | 8568.685 / 8568.696 | 13 / 13 | 26 | | +| 19-l-bracket-constrained | pass | LBracket | 14720.000 / 14720.000 | 5472.000 / 5472.000 | 8 / 8 | 9 | | +| 20-slot-constrained | pass | Slot | 2965.374 / 2965.487 | 1774.635 / 1774.690 | 6 / 6 | 4 | | | 21-draft | pass | Box | 54574.737 / 54574.737 | 8781.998 / 8781.998 | 6 / 6 | 1 | | -| 22-split-body | pass | Cyl, Cyl (1) | 75380.935 / 75398.224 | 12954.433 / 12955.175 | 6 / 6 | 31 | | -| 23-bracket-multifeature | pass | Bracket | 59331.560 / 59331.129 | 18290.761 / 18290.737 | 19 / 19 | 74 | | -| 24-user-parameters | pass | Block | 34822.124 / 34821.903 | 8114.167 / 8114.159 | 7 / 7 | 23 | | -| 25-sketch-triangle | pass | Triangle | 4032.155 / 4032.155 | 2274.099 / 2274.099 | 5 / 5 | 1 | | -| 26-sketch-belt | pass | Belt | 11326.051 / 11326.170 | 5637.346 / 5637.404 | 7 / 7 | 27 | | -| 27-sketch-trapezoid | pass | Trapezoid | 5342.311 / 5342.257 | 3066.959 / 3066.946 | 8 / 8 | 23 | | -| 28-blind-pocket-fillets | pass | Block | 84299.400 / 84299.249 | 16279.542 / 16279.572 | 23 / 23 | 72 | | -| 29-two-sided-and-partial-revolve | pass | Block | 25731.359 / 25731.307 | 6312.589 / 6312.602 | 12 / 12 | 14 | | +| 22-split-body | pass | Cyl, Cyl (1) | 75380.935 / 75398.224 | 12954.433 / 12955.175 | 6 / 6 | 19 | | +| 23-bracket-multifeature | pass | Bracket | 59331.560 / 59331.129 | 18290.761 / 18290.737 | 19 / 19 | 37 | | +| 24-user-parameters | pass | Block | 34822.124 / 34821.903 | 8114.167 / 8114.159 | 7 / 7 | 4 | | +| 25-sketch-triangle | pass | Triangle | 4032.155 / 4032.155 | 2274.099 / 2274.099 | 5 / 5 | 18 | | +| 26-sketch-belt | pass | Belt | 11326.051 / 11326.170 | 5637.346 / 5637.404 | 7 / 7 | 24 | | +| 27-sketch-trapezoid | pass | Trapezoid | 5342.311 / 5342.257 | 3066.959 / 3066.946 | 8 / 8 | 9 | | +| 28-blind-pocket-fillets | pass | Block | 84299.400 / 84299.249 | 16279.542 / 16279.572 | 23 / 23 | 56 | | +| 29-two-sided-and-partial-revolve | pass | Block | 25731.359 / 25731.307 | 6312.589 / 6312.602 | 12 / 12 | 9 | | +| 30-fillet-after-shell | pass | Box | 21745.919 / 21745.876 | 14968.279 / 14968.332 | 27 / 27 | 82 | | +| 31-fillet-l-all-edges | pass | L | 14321.347 / 14321.626 | 5023.960 / 5024.045 | 38 / 38 | 68 | | +| 32-boss-plate-fillets | pass | Part | 33590.191 / 33590.350 | 9929.841 / 9929.864 | 10 / 10 | 620 | | +| 33-fillet-tangent-chain | pass | Slot | 8236.938 / 8237.122 | 2793.516 / 2793.569 | 14 / 14 | 202 | | +| 34-chamfer-then-fillet | pass | Box | 22892.369 / 22892.421 | 4753.674 / 4753.688 | 18 / 18 | 31 | | +| 35-fillet-curved-curved | FAIL | Tee | 63090.315 / 63171.443 | 10267.244 / 10269.348 | 6 / 6 | 1602 | | +| 36-corner-blend-two-radii | pass | Box | 57799.345 / 57800.240 | 8585.404 / 8585.524 | 18 / 18 | 88 | | +| 37-pyramid-apex-blend | FAIL | Pyramid | 15483.389 / 15465.575 | 3972.101 / 3914.581 | 18 / 18 | 786 | | +| 38-coincident-cylinders | pass | A | 15706.106 / 15707.963 | 3769.742 / 3769.911 | 3 / 3 | 27 | | +| 39-tee-pipe | FAIL | Tee | 24069.177 / 24096.237 | 16706.241 / 16710.048 | 7 / 7 | 64 | | +| 40-box-sphere-torus | pass | A | 36287.351 / 36287.433 | 10695.939 / 10696.165 | 10 / 10 | 764 | | +| 41-tube-radial-holes | FAIL | Tube | 20070.139 / 20243.981 | 14849.742 / 14873.773 | 22 / 22 | 428 | | +| 42-overlapping-patterns | pass | Plate | 38489.045 / 38487.064 | 21687.203 / 21686.603 | 43 / 43 | 469 | solid 'Plate': imported as an open body (the shell is not closed) | +| 43-cut-splits-body | pass | Bar, Bar (1) | 28000.000 / 28000.000 | 7200.000 / 7200.000 | 12 / 12 | 19 | | +| 44-shell-l-bracket | pass | L | 8384.000 / 8384.000 | 8784.000 / 8784.000 | 15 / 15 | 17 | | +| 45-shell-boss-plate | pass | Part | 13676.925 / 13676.602 | 14156.500 / 14156.602 | 15 / 15 | 11 | | +| 46-shell-two-open-faces | pass | Box | 16044.000 / 16044.000 | 11398.000 / 11398.000 | 10 / 10 | 2 | | +| 47-draft-curved-pocket | pass | Block | 111388.144 / 111387.712 | 17700.329 / 17700.361 | 11 / 11 | 11 | | +| 48-pulley | pass | Pulley | 168507.117 / 168510.841 | 23718.181 / 23718.644 | 12 / 12 | 83 | | +| 49-helical-sweep | pass | Spring | 4736.230 / 4738.099 | 4766.821 / 4767.652 | 3 / 3 | 390482 | solid 'Spring': face wrapping around a closed surface kept whole: split line does not reach the rings | +| 50-sweep-tight-bend | FAIL | Bend | 3822.321 / 3758.443 | 1708.995 / 2153.937 | 5 / 5 | 1797 | | +| 51-loft-rect-circle-offset | pass | Loft | 61143.729 / 61133.116 | 9415.804 / 9411.655 | 6 / 6 | 142 | | +| 52-circular-pattern-fillet | pass | Hub | 47206.223 / 47206.850 | 14142.022 / 14142.171 | 21 / 21 | 625 | | +| 53-path-pattern | pass | Plate | 35010.589 / 35010.398 | 16463.939 / 16463.894 | 13 / 13 | 87 | | +| 54-mirror-body-fillets | pass | Bracket | 22780.205 / 22780.089 | 6972.823 / 6972.832 | 12 / 12 | 38 | | +| 55-tapped-hole-cosmetic | pass | Plate | 17542.404 / 17542.314 | 4761.584 / 4761.596 | 8 / 8 | 13 | | +| 56-holes-on-curved-face | FAIL | Cyl | 77733.175 / 77942.388 | 10590.123 / 10593.139 | 7 / 7 | 93 | | +| 57-hole-pattern-sketch-points | pass | Plate | 36767.098 / 36766.712 | 11818.159 / 11818.153 | 24 / 24 | 165 | | +| 58-phone-stand | pass | Stand | 74559.445 / 74559.177 | 29148.741 / 29148.785 | 19 / 19 | 25 | | +| 59-enclosure-base | pass | Enclosure | 34309.026 / 34309.486 | 33795.404 / 33795.610 | 39 / 39 | 74 | | +| 60-pipe-flange | pass | Flange | 323660.116 / 323660.979 | 56984.492 / 56984.564 | 16 / 16 | 495 | | +| 61-shaft-keyway | pass | Shaft | 47493.351 / 47525.946 | 9675.779 / 9677.096 | 14 / 14 | 77 | | +| 62-heat-sink | pass | HeatSink | 43125.000 / 43125.000 | 45725.000 / 45725.000 | 66 / 66 | 41 | | +| 63-hinge-knuckle | pass | Hinge | 4781.312 / 4783.117 | 4109.841 / 4110.287 | 14 / 14 | 47 | | +| 64-sketch-tangent-arc-chain | pass | Wave | 4800.000 / 4800.000 | 3843.701 / 3843.712 | 9 / 9 | 47 | | +| 65-sketch-gear-tooth | pass | Tooth | 3987.829 / 3988.084 | 1945.416 / 1945.485 | 9 / 9 | 20 | | +| 66-sketch-linkage | pass | Link | 8045.431 / 8045.386 | 4308.168 / 4308.175 | 10 / 10 | 94 | | diff --git a/vendor/README.md b/vendor/README.md index ff095c57..e3c35c16 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -13,3 +13,26 @@ Changes: the nearest parameter when the exact search fails (intersection curves are approximations). - `lib.rs`: `tr!` reports where an operation gives up when `SHAPEOPS_TRACE` is set; compiler warnings no longer fail the build. + +## truck-stepio 0.3.0 (Apache-2.0, © RICOS Co. Ltd., https://github.com/ricosjp/truck) + +Only the writer (`out`) is used; SolveCraft reads STEP with its own reader. + +Changes: +- `out/geometry.rs`: an `INTERSECTION_CURVE`'s second surface was written at the first + surface's entity index, so every boolean-made edge produced duplicate entity ids. +- `out/geometry.rs`: surfaces of revolution carry the right `same_sense`: truck's revolution + has the same parameterisation and normal as ISO 10303-42 (the writer flipped it), and a + mirroring transform reverses the normal. +- `lib.rs`: compiler warnings no longer fail the build. Example and test targets are dropped + (their sources are not vendored). + +## truck-meshalgo 0.4.0 (Apache-2.0, © RICOS Co. Ltd., https://github.com/ricosjp/truck) + +Changes: +- `tessellation/triangulation.rs`: parameter searches that land outside a surface's bounded, + non-periodic domain are rejected (a hinted B-spline search can run off and extrapolate to + thousands of parameter units, and meshing that grid never finishes); the last-resort nearest + search is clamped into the domain. +- `lib.rs`: compiler warnings no longer fail the build. Example, test and bench targets are + dropped (their sources are not vendored). diff --git a/vendor/truck-meshalgo/Cargo.toml b/vendor/truck-meshalgo/Cargo.toml new file mode 100644 index 00000000..081896c2 --- /dev/null +++ b/vendor/truck-meshalgo/Cargo.toml @@ -0,0 +1,113 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "truck-meshalgo" +version = "0.4.0" +build = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Mesh algorigthms, include tessellations of the shape." +homepage = "https://github.com/ricosjp/truck" +readme = "README.md" +keywords = [ + "truck", + "graphics", + "mesh", +] +categories = ["graphics"] +license = "Apache-2.0" +repository = "https://github.com/ricosjp/truck" + +[lib] +name = "truck_meshalgo" +path = "src/lib.rs" + + + + + + + + + + + + + +[dependencies.array-macro] +version = "2.1.8" + +[dependencies.derive_more] +version = "0.99.18" + +[dependencies.itertools] +version = "0.13.0" + +[dependencies.rustc-hash] +version = "2.0.0" + +[dependencies.spade] +version = "2.12.1" +optional = true + +[dependencies.truck-base] +version = "0.5.0" + +[dependencies.truck-geometry] +version = "0.5.0" + +[dependencies.truck-polymesh] +version = "0.6.0" + +[dependencies.truck-topology] +version = "0.6.0" + +[dev-dependencies.proptest] +version = "1.5.0" + +[dev-dependencies.rand] +version = "0.8.5" + +[dev-dependencies.serde_json] +version = "1.0.128" + +[dev-dependencies.truck-modeling] +version = "0.6.0" + +[features] +analyzers = [] +default = [ + "analyzers", + "filters", + "tessellation", + "vtk", +] +filters = [] +tessellation = [ + "spade", + "filters", +] +vtk = ["vtkio"] + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.rayon] +version = "1.10.0" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.vtkio] +version = "0.6.3" +optional = true + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies.getrandom] +version = "0.2.15" +features = ["js"] diff --git a/vendor/truck-meshalgo/LICENSE b/vendor/truck-meshalgo/LICENSE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/vendor/truck-meshalgo/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/vendor/truck-meshalgo/README.md b/vendor/truck-meshalgo/README.md new file mode 100644 index 00000000..5779ec5b --- /dev/null +++ b/vendor/truck-meshalgo/README.md @@ -0,0 +1,68 @@ +# truck-meshalgo + +[![Crates.io](https://img.shields.io/crates/v/truck-meshalgo.svg)](https://crates.io/crates/truck-meshalgo) [![Docs.rs](https://docs.rs/truck-meshalgo/badge.svg)](https://docs.rs/truck-meshalgo) + +Mesh algorigthms, include tessellations of the shape. + +## Sample Codes + +### filleted-cube + +An experiment to decompose a mesh into elements for future NURBS-shape approximation of the mesh. +Contains `doc(hidden)` methods. + +- Input: filleted_cube.obj +- Output: planes.obj, lower.obj, upper.obj + +### irregular-sphere + +Add the normal to the sphere containing +the irregular normal generated from the NURBS containing the critical point. + +- Input: irregular_sphere.obj +- Output: regular_sphere.obj + +### octahedron-subdivision + +Apply loop subdivision to regular octahedron. + +- Input: hardcoded octahedron +- Output: octahedron.obj, subdivision-octahedron.obj + +### requadrangulate-buddha + +A benchmark that reads in heavy mesh data, applies triangulation and quadrangulation, and writes it out. + +- Input: happy-buddha.obj +- Output: requadrangulated-buddha.obj + +### smoothing-bunny + +Adds smooth normals to the stanford bunny. + +- Input: bunny.obj +- Output: smooth_bunny.obj + +### splitting-sample + +An experiment to decompose a mesh into elements for future NURBS-shape approximation of the mesh. + +- Input: sample.obj +- Output: planes_parts_#.obj, others_parts_#.obj + +### teapot + +Adds smooth normals to and quadrangulate the famous teapot. + +- Input: teapot.obj +- Output: quaded_pot.obj + +### tessellate-shape + +Tessellate a shape and output an obj file. + +```bash +usage: tessellate-shape +``` + +The default `` is output.obj. diff --git a/vendor/truck-meshalgo/src/analyzers/collision.rs b/vendor/truck-meshalgo/src/analyzers/collision.rs new file mode 100644 index 00000000..e435ce9d --- /dev/null +++ b/vendor/truck-meshalgo/src/analyzers/collision.rs @@ -0,0 +1,275 @@ +use super::*; +use array_macro::array; + +/// Find collisions between two polygon meshes and extract interference lines. +/// +/// # Details +/// +/// The algorithm for mesh extraction is as follows. +/// All the triangles are projected toward the axis to form intervals. +/// By looking at the overlap of these intervals, we can narrow down the interfering triangles. +/// The overlap of the intervals is obtained by sorting the endpoints. +/// +/// # Remarks +/// +/// We see that the surfaces that are in contact are not interfering with each other. +/// Therefore, polygon meshes included in one plane are not interfering. +pub trait Collision { + /// If `self` and `other` collide, then returns only one interference line. + /// Otherwise, returns `None`. + fn collide_with(&self, other: &PolygonMesh) -> Option<(Point3, Point3)>; + /// Extract all interference lines between `self` and `other`. + /// # Remarks + /// The results is not arranged so that included lines make continuous maximal polyline curve. + fn extract_interference(&self, other: &PolygonMesh) -> Vec<(Point3, Point3)>; +} + +impl Collision for PolygonMesh { + #[inline(always)] + fn collide_with(&self, other: &PolygonMesh) -> Option<(Point3, Point3)> { + are_colliding(self, other) + } + #[inline(always)] + fn extract_interference(&self, other: &PolygonMesh) -> Vec<(Point3, Point3)> { + collision(self, other) + } +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +enum EndPointType { + Front, + Back, +} + +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] +struct EndPoint { + entity: f64, + r#type: EndPointType, + segnum: usize, + index: usize, +} + +impl EndPoint { + #[inline(always)] + fn new(entity: f64, r#type: EndPointType, segnum: usize, index: usize) -> EndPoint { + EndPoint { + entity, + r#type, + segnum, + index, + } + } + #[inline(always)] + fn from_seg(seg: (f64, f64), segnum: usize, index: usize) -> Vec { + vec![ + EndPoint::new(seg.0, EndPointType::Front, segnum, index), + EndPoint::new(seg.1, EndPointType::Back, segnum, index), + ] + } +} + +fn tri_to_seg(tri: [Point3; 3], unit: Vector3) -> (f64, f64) { + let a = tri[0].to_vec().dot(unit); + let b = tri[1].to_vec().dot(unit); + let c = tri[2].to_vec().dot(unit); + (f64::min(f64::min(a, b), c), f64::max(f64::max(a, b), c)) +} + +fn sorted_endpoints(iter0: I, iter1: J, unit: Vector3) -> Vec +where + I: IntoIterator, + J: IntoIterator, { + let mut res: Vec = iter0 + .into_iter() + .enumerate() + .filter(|(_, tri)| !(tri[1] - tri[0]).cross(tri[2] - tri[0]).so_small()) + .flat_map(|(i, tri)| EndPoint::from_seg(tri_to_seg(tri, unit), 0, i)) + .chain( + iter1 + .into_iter() + .enumerate() + .filter(|(_, tri)| !(tri[1] - tri[0]).cross(tri[2] - tri[0]).so_small()) + .flat_map(|(i, tri)| EndPoint::from_seg(tri_to_seg(tri, unit), 1, i)), + ) + .collect(); + res.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Greater)); + res +} + +fn colliding_segment_pairs(sort_endpoints: Vec) -> impl Iterator { + let mut current = [Vec::::new(), Vec::::new()]; + sort_endpoints + .into_iter() + .filter_map( + move |EndPoint { + r#type, + segnum, + index, + .. + }| match r#type { + EndPointType::Front => { + current[segnum].push(index); + Some(current[1 - segnum].clone().into_iter().map(move |i| { + if segnum == 0 { + (index, i) + } else { + (i, index) + } + })) + } + EndPointType::Back => { + let i = current[segnum] + .iter() + .enumerate() + .find(|(_, idx)| **idx == index) + .unwrap() + .0; + current[segnum].swap_remove(i); + None + } + }, + ) + .flatten() +} + +fn disjoint_bdbs(tri0: [Point3; 3], tri1: [Point3; 3]) -> bool { + let bdb0: BoundingBox = tri0.iter().collect(); + let bdb1: BoundingBox = tri1.iter().collect(); + bdb0.max()[0] < bdb1.min()[0] + || bdb1.max()[0] < bdb0.min()[0] + || bdb0.max()[1] < bdb1.min()[1] + || bdb1.max()[1] < bdb0.min()[1] + || bdb0.max()[2] < bdb1.min()[2] + || bdb1.max()[2] < bdb0.min()[2] +} + +fn collide_seg_triangle(seg: [Point3; 2], tri: [Point3; 3]) -> Option { + let ab = tri[1] - tri[0]; + let bc = tri[2] - tri[1]; + let ca = tri[0] - tri[2]; + let nor = ab.cross(ca); + if nor.so_small() { + return None; + } + let ap = seg[0] - tri[0]; + let aq = seg[1] - tri[0]; + let dotapnor = ap.dot(nor); + let dotaqnor = aq.dot(nor); + if dotapnor * dotaqnor > 0.0 { + return None; + } + let h = seg[0] + dotapnor / (dotapnor - dotaqnor) * (seg[1] - seg[0]); + if f64::signum(ab.cross(nor).dot(h - tri[0]) + TOLERANCE2) + + f64::signum(bc.cross(nor).dot(h - tri[1]) + TOLERANCE2) + + f64::signum(ca.cross(nor).dot(h - tri[2]) + TOLERANCE2) + >= 2.0 + { + Some(h) + } else { + None + } +} + +fn collide_triangles(tri0: [Point3; 3], tri1: [Point3; 3]) -> Option<(Point3, Point3)> { + let mut tuple = (None, None); + [ + collide_seg_triangle([tri0[0], tri0[1]], tri1), + collide_seg_triangle([tri0[1], tri0[2]], tri1), + collide_seg_triangle([tri0[2], tri0[0]], tri1), + collide_seg_triangle([tri1[0], tri1[1]], tri0), + collide_seg_triangle([tri1[1], tri1[2]], tri0), + collide_seg_triangle([tri1[2], tri1[0]], tri0), + ] + .into_iter() + .for_each(|pt| match tuple { + (None, _) => tuple.0 = pt, + (Some(_), None) => tuple.1 = pt, + (Some(ref mut p), Some(ref mut q)) => { + if let Some(pt) = pt { + let dist0 = pt.distance2(*p); + let dist1 = pt.distance2(*q); + let dist2 = p.distance2(*q); + if dist2 < dist0 { + *q = pt; + } else if dist2 < dist1 { + *p = pt; + } + } + } + }); + match tuple { + (Some(a), Some(b)) => Some((a, b)), + _ => None, + } +} + +fn make_pos_tri(poly: &PolygonMesh, face: [StandardVertex; 3]) -> [Point3; 3] { + array![i => poly.positions()[face[i].pos]; 3] +} + +fn collision(poly0: &PolygonMesh, poly1: &PolygonMesh) -> Vec<(Point3, Point3)> { + let unit = match poly0.positions().is_empty() { + true => return Vec::new(), + false => hash::take_one_unit(poly0.positions()[0]), + }; + let tris0 = poly0.faces().triangle_iter().collect::>(); + let tris1 = poly1.faces().triangle_iter().collect::>(); + let iter0 = tris0.iter().map(|face| make_pos_tri(poly0, *face)); + let iter1 = tris1.iter().map(|face| make_pos_tri(poly1, *face)); + colliding_segment_pairs(sorted_endpoints(iter0, iter1, unit)) + .filter_map(|(idx0, idx1)| { + let tri0 = make_pos_tri(poly0, tris0[idx0]); + let tri1 = make_pos_tri(poly1, tris1[idx1]); + match disjoint_bdbs(tri0, tri1) { + true => None, + false => collide_triangles(tri0, tri1), + } + }) + .collect() +} + +fn are_colliding(poly0: &PolygonMesh, poly1: &PolygonMesh) -> Option<(Point3, Point3)> { + let unit = match poly0.positions().is_empty() { + true => return None, + false => hash::take_one_unit(poly0.positions()[0]), + }; + let tris0 = poly0.faces().triangle_iter().collect::>(); + let tris1 = poly1.faces().triangle_iter().collect::>(); + let iter0 = tris0.iter().map(|face| make_pos_tri(poly0, *face)); + let iter1 = tris1.iter().map(|face| make_pos_tri(poly1, *face)); + colliding_segment_pairs(sorted_endpoints(iter0, iter1, unit)).find_map(|(idx0, idx1)| { + let tri0 = make_pos_tri(poly0, tris0[idx0]); + let tri1 = make_pos_tri(poly1, tris1[idx1]); + match disjoint_bdbs(tri0, tri1) { + true => None, + false => collide_triangles(tri0, tri1), + } + }) +} + +#[test] +fn collide_triangles_test() { + let tri0 = [ + Point3::origin(), + Point3::new(1.0, 0.0, 0.0), + Point3::new(0.0, 1.0, 0.0), + ]; + let tri1 = [ + Point3::new(0.0, 0.0, -1.0), + Point3::new(-1.0, -1.0, 1.0), + Point3::new(1.0, 1.0, 1.0), + ]; + assert!(collide_triangles(tri0, tri1).is_some()); + let tri0 = [ + Point3::new(0.0, 0.0, 0.0), + Point3::new(1.0, 0.0, 0.0), + Point3::new(0.0, 1.0, 0.0), + ]; + let tri1 = [ + Point3::new(0.0, 0.0, 0.5), + Point3::new(1.0, 0.0, 1.0), + Point3::new(1.0, 1.0, 1.0), + ]; + assert!(collide_triangles(tri0, tri1).is_none()); +} diff --git a/vendor/truck-meshalgo/src/analyzers/in_out_judge.rs b/vendor/truck-meshalgo/src/analyzers/in_out_judge.rs new file mode 100644 index 00000000..63447332 --- /dev/null +++ b/vendor/truck-meshalgo/src/analyzers/in_out_judge.rs @@ -0,0 +1,140 @@ +use super::*; +use derive_more::{Deref, DerefMut}; + +#[derive(Clone, Copy, Debug)] +struct Ray { + origin: Point3, + direction: Vector3, +} + +#[derive(Clone, Copy, Debug, Deref, DerefMut)] +struct Triangle([Point3; 3]); + +impl Triangle { + #[inline(always)] + fn normal(self) -> Vector3 { (self[1] - self[0]).cross(self[2] - self[0]).normalize() } + + fn is_crossing(self, ray: Ray) -> bool { + let a = self[0] - self[1]; + let b = self[0] - self[2]; + let mat = Matrix3::from_cols(a, b, ray.direction); + if mat.determinant().so_small() { + false + } else { + let inv = mat.invert().unwrap(); + let uvt = inv * (self[0] - ray.origin); + uvt[0] > 0.0 && uvt[1] > 0.0 && uvt[0] + uvt[1] < 1.0 && uvt[2] > 0.0 + } + } +} + +/// whether a point is in a domain rounded by a closed polygon. +pub trait IncludingPointInDomain { + /// Count signed number of faces crossing ray with origin `point` and direction `ray_direction`. + /// Counter increase if the dot product of the ray and the normal of a face is positive, + /// and decrease if it is negative. + /// + /// # Examples + /// ``` + /// use truck_meshalgo::prelude::*; + /// let simplex = PolygonMesh::new( + /// StandardAttributes { + /// positions: vec![ + /// Point3::new(0.0, 0.0, 0.0), + /// Point3::new(1.0, 0.0, 0.0), + /// Point3::new(0.0, 1.0, 0.0), + /// Point3::new(0.0, 0.0, 1.0), + /// ], + /// ..Default::default() + /// }, + /// Faces::from_iter(vec![ + /// [0, 2, 1], + /// [0, 1, 3], + /// [0, 3, 2], + /// [1, 2, 3], + /// ]), + /// ); + /// + /// assert_eq!(simplex.signed_crossing_faces(Point3::new(0.1, 0.1, 0.1), Vector3::unit_x()), 1); + /// assert_eq!(simplex.signed_crossing_faces(Point3::new(-0.1, 0.1, 0.1), Vector3::unit_x()), 0); + /// ``` + fn signed_crossing_faces(&self, point: Point3, ray_direction: Vector3) -> isize; + /// whether `point` is in a domain rounded by a closed polygon. + /// + /// # Examples + /// ``` + /// use truck_meshalgo::prelude::*; + /// let simplex = PolygonMesh::new( + /// StandardAttributes { + /// positions: vec![ + /// Point3::new(0.0, 0.0, 0.0), + /// Point3::new(1.0, 0.0, 0.0), + /// Point3::new(0.0, 1.0, 0.0), + /// Point3::new(0.0, 0.0, 1.0), + /// ], + /// ..Default::default() + /// }, + /// Faces::from_iter(vec![ + /// [0, 2, 1], + /// [0, 1, 3], + /// [0, 3, 2], + /// [1, 2, 3], + /// ]), + /// ); + /// + /// assert!(simplex.inside(Point3::new(0.1, 0.1, 0.1))); + /// assert!(!simplex.inside(Point3::new(-0.1, 0.1, 0.1))); + /// ``` + fn inside(&self, point: Point3) -> bool; +} + +impl IncludingPointInDomain for PolygonMesh { + fn signed_crossing_faces(&self, point: Point3, ray_direction: Vector3) -> isize { + let ray = Ray { + origin: point, + direction: ray_direction, + }; + self.face_iter().fold(0, |mut counter, face| { + for i in 2..face.len() { + let tri = Triangle([ + self.positions()[face[0].pos], + self.positions()[face[i - 1].pos], + self.positions()[face[i].pos], + ]); + if tri.is_crossing(ray) { + counter += f64::signum(tri.normal().dot(ray.direction)) as isize; + } + } + counter + }) + } + fn inside(&self, point: Point3) -> bool { + let dir = hash::take_one_unit(point); + self.signed_crossing_faces(point, dir) >= 1 + } +} + +#[test] +fn inside100() { + let positions = vec![ + Point3::new(0.0, 0.0, 0.0), + Point3::new(1.0, 0.0, 0.0), + Point3::new(0.0, 1.0, 0.0), + Point3::new(0.0, 0.0, 1.0), + ]; + let faces: Faces = vec![[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]] + .into_iter() + .collect(); + let simplex = PolygonMesh::new( + StandardAttributes { + positions, + ..Default::default() + }, + faces, + ); + + for _ in 0..100 { + assert!(simplex.inside(Point3::new(0.1, 0.1, 0.1))); + assert!(!simplex.inside(Point3::new(-0.1, 0.1, 0.1))); + } +} diff --git a/vendor/truck-meshalgo/src/analyzers/mod.rs b/vendor/truck-meshalgo/src/analyzers/mod.rs new file mode 100644 index 00000000..993bc706 --- /dev/null +++ b/vendor/truck-meshalgo/src/analyzers/mod.rs @@ -0,0 +1,17 @@ +use crate::*; + +mod collision; +mod in_out_judge; +mod point_cloud; +mod splitting; +mod topology; +mod volume; + +pub use collision::Collision; +pub use in_out_judge::IncludingPointInDomain; +pub use point_cloud::WithPointCloud; +pub use splitting::ExperimentalSplitters; +pub use splitting::Splitting; +pub use topology::Topology; +pub use truck_topology::shell::ShellCondition; +pub use volume::CalcVolume; diff --git a/vendor/truck-meshalgo/src/analyzers/point_cloud/hashed_point_cloud.rs b/vendor/truck-meshalgo/src/analyzers/point_cloud/hashed_point_cloud.rs new file mode 100644 index 00000000..4e537f93 --- /dev/null +++ b/vendor/truck-meshalgo/src/analyzers/point_cloud/hashed_point_cloud.rs @@ -0,0 +1,283 @@ +use super::*; +use array_macro::array; + +#[derive(Clone, Debug)] +pub struct HashedPointCloud { + space: Vec>, + size: [usize; 3], + range: [[f64; 2]; 3], + num_points: usize, +} + +#[allow(dead_code)] +impl HashedPointCloud { + #[inline(always)] + pub fn new(size: [usize; 3], range: [[f64; 2]; 3]) -> Self { + HashedPointCloud { + space: vec![Vec::new(); size[0] * size[1] * size[2]], + size, + range, + num_points: 0, + } + } + #[inline(always)] + pub fn from_points<'a, I>(points: I, inf: f64) -> Self + where + I: IntoIterator + Clone, + I::IntoIter: Iterator, { + let mut bdb = BoundingBox::::new(); + let len = points.clone().into_iter().copied().fold(0, |counter, pt| { + bdb.push(pt); + counter + 1 + }); + let size = 1.0 + f64::powf(len as f64, 1.0 / 3.0); + let size = bdb.diagonal().map(|a| { + if a / size < inf { + 1 + (a / inf) as usize + } else { + size as usize + } + }); + let mut res = + HashedPointCloud::new(size.into(), array![i => [bdb.min()[i], bdb.max()[i]]; 3]); + points.into_iter().for_each(|pt| res.push(*pt)); + res + } + #[inline(always)] + pub fn size(&self) -> [usize; 3] { self.size } + #[inline(always)] + pub fn push(&mut self, point: Point3) { + self.num_points += 1; + let idx = point.hash(self); + self[idx].push(point); + } + #[inline(always)] + pub fn distance(&self, t: impl DistanceWithPointCloud) -> f64 { t.distance(self) } + #[inline(always)] + pub fn distance2(&self, t: impl DistanceWithPointCloud) -> f64 { t.distance2(self) } + #[inline(always)] + pub fn is_colliding(&self, t: impl DistanceWithPointCloud, tol: f64) -> bool { + t.is_colliding(self, tol) + } +} + +impl std::ops::Index<[usize; 3]> for HashedPointCloud { + type Output = Vec; + #[inline(always)] + fn index(&self, idcs: [usize; 3]) -> &Vec { + &self.space[(idcs[0] * self.size[1] + idcs[1]) * self.size[2] + idcs[2]] + } +} + +impl std::ops::IndexMut<[usize; 3]> for HashedPointCloud { + #[inline(always)] + fn index_mut(&mut self, idcs: [usize; 3]) -> &mut Vec { + &mut self.space[(idcs[0] * self.size[1] + idcs[1]) * self.size[2] + idcs[2]] + } +} + +pub trait SpaceHash { + fn hash(self, space: &HashedPointCloud) -> [usize; 3]; +} + +impl SpaceHash for Point3 { + #[inline(always)] + fn hash(self, space: &HashedPointCloud) -> [usize; 3] { + let x = (self[0] - space.range[0][0]) / (space.range[0][1] - space.range[0][0]); + let ix = f64::clamp(x * space.size[0] as f64, 0.0, space.size[0] as f64 - 1.0) as usize; + let y = (self[1] - space.range[1][0]) / (space.range[1][1] - space.range[1][0]); + let iy = f64::clamp(y * space.size[1] as f64, 0.0, space.size[1] as f64 - 1.0) as usize; + let z = (self[2] - space.range[2][0]) / (space.range[2][1] - space.range[2][0]); + let iz = f64::clamp(z * space.size[2] as f64, 0.0, space.size[2] as f64 - 1.0) as usize; + [ix, iy, iz] + } +} + +impl SpaceHash for usize { + #[inline(always)] + fn hash(self, space: &HashedPointCloud) -> [usize; 3] { + [ + self / (space.size[1] * space.size[2]), + self % (space.size[1] * space.size[2]) / space.size[2], + self % space.size[2], + ] + } +} + +pub trait DistanceWithPointCloud: Sized { + fn distance2(&self, space: &HashedPointCloud) -> f64; + fn distance(&self, space: &HashedPointCloud) -> f64 { f64::sqrt(self.distance2(space)) } + fn is_colliding(&self, space: &HashedPointCloud, tol: f64) -> bool { + nonpositive_tolerance!(tol, 0.0); + self.distance2(space) < tol * tol + } +} + +impl DistanceWithPointCloud for Point3 { + fn distance2(&self, space: &HashedPointCloud) -> f64 { + let idcs = self.hash(space); + let closure = |dist2: f64, pt: &Point3| f64::min(dist2, MetricSpace::distance2(*self, *pt)); + let mut dist2 = space[idcs].iter().fold(f64::INFINITY, closure); + if idcs[0] > 0 { + dist2 = space[[idcs[0] - 1, idcs[1], idcs[2]]] + .iter() + .fold(dist2, closure); + } + if idcs[0] + 1 < space.size[0] { + dist2 = space[[idcs[0] + 1, idcs[1], idcs[2]]] + .iter() + .fold(dist2, closure); + } + if idcs[1] > 0 { + dist2 = space[[idcs[0], idcs[1] - 1, idcs[2]]] + .iter() + .fold(dist2, closure); + } + if idcs[1] + 1 < space.size[1] { + dist2 = space[[idcs[0], idcs[1] + 1, idcs[2]]] + .iter() + .fold(dist2, closure); + } + if idcs[2] > 0 { + dist2 = space[[idcs[0], idcs[1], idcs[2] - 1]] + .iter() + .fold(dist2, closure); + } + if idcs[2] + 1 < space.size[2] { + dist2 = space[[idcs[0], idcs[1], idcs[2] + 1]] + .iter() + .fold(dist2, closure); + } + dist2 + } +} + +impl DistanceWithPointCloud for [Point3; 3] { + fn distance2(&self, space: &HashedPointCloud) -> f64 { + let bdd: BoundingBox = self.iter().collect(); + let mut range: [[usize; 3]; 2] = [bdd.min().hash(space), bdd.max().hash(space)]; + range[0][0] = usize::max(range[0][0], 1) - 1; + range[0][1] = usize::max(range[0][1], 1) - 1; + range[0][2] = usize::max(range[0][2], 1) - 1; + range[1][0] = usize::min(range[1][0] + 1, space.size[0] - 1); + range[1][1] = usize::min(range[1][1] + 1, space.size[1] - 1); + range[1][2] = usize::min(range[1][2] + 1, space.size[2] - 1); + let mut dist2 = f64::INFINITY; + (range[0][0]..=range[1][0]).for_each(|ix| { + (range[0][1]..=range[1][1]).for_each(|iy| { + (range[0][2]..=range[1][2]).for_each(|iz| { + dist2 = space[[ix, iy, iz]].iter().fold(dist2, |dist2, pt| { + f64::min(dist2, distance2_point_triangle(*pt, *self)) + }); + }) + }) + }); + dist2 + } +} + +impl<'a> DistanceWithPointCloud for &'a PolygonMesh { + fn distance2(&self, space: &HashedPointCloud) -> f64 { + let dist2 = self.faces().triangle_iter().fold(-1.0, |dist2, tri| { + let tri = array![i => self.positions()[tri[i].pos]; 3]; + f64::max(dist2, tri.distance2(space)) + }); + match dist2 < 0.0 { + true => f64::INFINITY, + false => dist2, + } + } + fn is_colliding(&self, space: &HashedPointCloud, tol: f64) -> bool { + nonpositive_tolerance!(tol, 0.0); + self.faces().triangle_iter().any(|tri| { + let tri = array![i => self.positions()[tri[i].pos]; 3]; + tri.distance2(space) < tol * tol + }) + } +} + +#[test] +fn distance_point_triangle_test() { + let triangle = [ + Point3::new(0.0, 0.0, 0.0), + Point3::new(1.0, 0.0, 0.0), + Point3::new(0.0, 1.0, 0.0), + ]; + assert_eq!( + 12.45 * 12.45, + distance2_point_triangle(Point3::new(0.25, 0.25, 12.45), triangle) + ); + assert_eq!( + 12.45 * 12.45, + distance2_point_triangle(Point3::new(0.25, 0.25, -12.45), triangle) + ); + assert_eq!( + 2.0, + distance2_point_triangle(Point3::new(-1.0, 0.5, -1.0), triangle) + ); + assert_eq!( + 2.0, + distance2_point_triangle(Point3::new(0.5, -1.0, 1.0), triangle) + ); + assert_eq!( + 1.5, + distance2_point_triangle(Point3::new(1.0, 1.0, 1.0), triangle) + ); + assert_eq!( + 3.0, + distance2_point_triangle(Point3::new(-1.0, -1.0, -1.0), triangle) + ); +} + +#[cfg(test)] +fn exec_space_division_distance() { + const SPACE_SIZE: f64 = 100.0; + const NUM_POINTS: usize = 1000; + const NUM_TRIANGLES: usize = 10; + const TRIANGLE_DISPLACEMENT: f64 = 1.0; + + let points = (0..NUM_POINTS) + .map(|_| { + Point3::new( + SPACE_SIZE * rand::random::() - SPACE_SIZE / 2.0, + SPACE_SIZE * rand::random::() - SPACE_SIZE / 2.0, + SPACE_SIZE * rand::random::() - SPACE_SIZE / 2.0, + ) + }) + .collect::>(); + let triangles = (0..NUM_TRIANGLES) + .map(|_| { + let pt = Point3::new( + SPACE_SIZE * rand::random::() - SPACE_SIZE / 2.0, + SPACE_SIZE * rand::random::() - SPACE_SIZE / 2.0, + SPACE_SIZE * rand::random::() - SPACE_SIZE / 2.0, + ); + [ + pt, + pt + Vector3::new( + TRIANGLE_DISPLACEMENT * 2.0 * rand::random::() - TRIANGLE_DISPLACEMENT, + TRIANGLE_DISPLACEMENT * 2.0 * rand::random::() - TRIANGLE_DISPLACEMENT, + TRIANGLE_DISPLACEMENT * 2.0 * rand::random::() - TRIANGLE_DISPLACEMENT, + ), + pt + Vector3::new( + TRIANGLE_DISPLACEMENT * 2.0 * rand::random::() - TRIANGLE_DISPLACEMENT, + TRIANGLE_DISPLACEMENT * 2.0 * rand::random::() - TRIANGLE_DISPLACEMENT, + TRIANGLE_DISPLACEMENT * 2.0 * rand::random::() - TRIANGLE_DISPLACEMENT, + ), + ] + }) + .collect::>(); + let hashed = HashedPointCloud::from_points(&points, 1.0); + let dist_0 = triangles.iter().fold(std::f64::INFINITY, |dist, triangle| { + f64::min(dist, hashed.distance2(*triangle)) + }); + let dist_1 = points.iter().fold(std::f64::INFINITY, |dist2, pt| { + triangles.iter().fold(dist2, |dist2, triangle| { + f64::min(dist2, distance2_point_triangle(*pt, *triangle)) + }) + }); + assert_eq!(dist_0, dist_1); +} + +#[test] +fn space_division_distance() { (0..10).for_each(|_| exec_space_division_distance()) } diff --git a/vendor/truck-meshalgo/src/analyzers/point_cloud/mod.rs b/vendor/truck-meshalgo/src/analyzers/point_cloud/mod.rs new file mode 100644 index 00000000..da6d88c9 --- /dev/null +++ b/vendor/truck-meshalgo/src/analyzers/point_cloud/mod.rs @@ -0,0 +1,84 @@ +use super::*; +mod hashed_point_cloud; +use hashed_point_cloud::HashedPointCloud; +mod sort_end_points; + +/// Investigates positional relation between polygon mesh and point cloud. +pub trait WithPointCloud { + /// Whether all faces of the polygon mesh `self` has intersection with the neighborhood of `point_cloud`. + /// # Arguments + /// - `tol`: the radius of the neighborhoods of points in point cloud. + /// # Panics + /// `tol` must be more than `TOLERANCE`. + /// # Examples + /// ``` + /// use truck_meshalgo::prelude::*; + /// let mesh = PolygonMesh::new( + /// StandardAttributes { + /// positions: vec![ + /// Point3::new(0.0, 0.0, 0.0), + /// Point3::new(1.0, 0.0, 0.0), + /// Point3::new(0.0, 1.0, 0.0), + /// Point3::new(0.0, 0.0, 2.0), + /// Point3::new(1.0, 0.0, 2.0), + /// Point3::new(0.0, 1.0, 2.0), + /// ], + /// ..Default::default() + /// }, + /// Faces::from_iter(vec![[0, 1, 2], [3, 4, 5]]), + /// ); + /// + /// let mut point_cloud = vec![Point3::new(0.25, 0.25, 0.0)]; + /// assert!(!mesh.is_clung_to_by(&point_cloud, 0.001)); + /// point_cloud.push(Point3::new(0.25, 0.25, 2.0)); + /// assert!(mesh.is_clung_to_by(&point_cloud, 0.001)); + /// ``` + fn is_clung_to_by(&self, point_cloud: &[Point3], tol: f64) -> bool; + /// Whether the neighborhood of the polygon mesh `self` includes `point_cloud`. + /// # Panics + /// `tol` must be more than `TOLERANCE`. + fn neighborhood_include(&self, point_cloud: &[Point3], tol: f64) -> bool; + /// Whether the polygon mesh `self` and `point_cloud` collides. + /// # Panics + /// `tol` must be more than `TOLERANCE`. + fn collide_with_neiborhood_of(&self, point_cloud: &[Point3], tol: f64) -> bool; +} + +impl WithPointCloud for PolygonMesh { + #[inline(always)] + fn is_clung_to_by(&self, point_cloud: &[Point3], tol: f64) -> bool { + nonpositive_tolerance!(tol); + HashedPointCloud::from_points(point_cloud, tol * 2.0).distance2(self) < tol * tol + } + #[inline(always)] + fn collide_with_neiborhood_of(&self, point_cloud: &[Point3], tol: f64) -> bool { + HashedPointCloud::from_points(point_cloud, tol * 2.0).is_colliding(self, tol) + } + #[inline(always)] + fn neighborhood_include(&self, point_cloud: &[Point3], tol: f64) -> bool { + sort_end_points::pointcloud_in_polygon_neighborhood(self, point_cloud, tol) + } +} + +// https://iquilezles.org/www/articles/distfunctions/distfunctions.htm +fn distance2_point_triangle(point: Point3, triangle: [Point3; 3]) -> f64 { + let ab = triangle[1] - triangle[0]; + let ap = point - triangle[0]; + let bc = triangle[2] - triangle[1]; + let bp = point - triangle[1]; + let ca = triangle[0] - triangle[2]; + let cp = point - triangle[2]; + let nor = ab.cross(ca); + + let coef = f64::signum(ab.cross(nor).dot(ap)) + + f64::signum(bc.cross(nor).dot(bp)) + + f64::signum(ca.cross(nor).dot(cp)); + if coef < 2.0 || nor.magnitude().so_small() { + let a = (ap - ab * f64::clamp(ab.dot(ap) / ab.dot(ab), 0.0, 1.0)).magnitude2(); + let b = (bp - bc * f64::clamp(bc.dot(bp) / bc.dot(bc), 0.0, 1.0)).magnitude2(); + let c = (cp - ca * f64::clamp(ca.dot(cp) / ca.dot(ca), 0.0, 1.0)).magnitude2(); + f64::min(f64::min(a, b), c) + } else { + nor.dot(ap) * nor.dot(ap) / nor.magnitude2() + } +} diff --git a/vendor/truck-meshalgo/src/analyzers/point_cloud/sort_end_points.rs b/vendor/truck-meshalgo/src/analyzers/point_cloud/sort_end_points.rs new file mode 100644 index 00000000..6e4156f5 --- /dev/null +++ b/vendor/truck-meshalgo/src/analyzers/point_cloud/sort_end_points.rs @@ -0,0 +1,121 @@ +use super::*; +use array_macro::array; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +enum EndPointType { + Front, + Middle, + Back, +} + +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] +struct EndPoint { + entity: f64, + r#type: EndPointType, + index: usize, +} + +impl EndPoint { + #[inline(always)] + fn new(entity: f64, r#type: EndPointType, index: usize) -> EndPoint { + EndPoint { + entity, + r#type, + index, + } + } + #[inline(always)] + fn from_seg(seg: (f64, f64), index: usize) -> Vec { + vec![ + EndPoint::new(seg.0, EndPointType::Front, index), + EndPoint::new(seg.1, EndPointType::Back, index), + ] + } +} + +fn tri_to_seg(tri: [Point3; 3], unit: Vector3, tol: f64) -> (f64, f64) { + let a = tri[0].to_vec().dot(unit); + let b = tri[1].to_vec().dot(unit); + let c = tri[2].to_vec().dot(unit); + ( + f64::min(f64::min(a, b), c) - tol, + f64::max(f64::max(a, b), c) + tol, + ) +} + +fn sorted_endpoints<'a, I, J>(iter0: I, iter1: J, unit: Vector3, tol: f64) -> Vec +where + I: IntoIterator, + J: IntoIterator, { + let mut res: Vec = iter0 + .into_iter() + .enumerate() + .filter(|(_, tri)| !(tri[1] - tri[0]).cross(tri[2] - tri[0]).so_small()) + .flat_map(|(i, tri)| EndPoint::from_seg(tri_to_seg(tri, unit, tol), i)) + .chain( + iter1 + .into_iter() + .enumerate() + .map(|(i, point)| EndPoint::new(point.to_vec().dot(unit), EndPointType::Middle, i)), + ) + .collect(); + res.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Greater)); + res +} + +fn sorted_endpoints_by_polymesh_points( + polygon: &PolygonMesh, + points: &[Point3], + tol: f64, +) -> Vec { + let unit = if !polygon.positions().is_empty() { + hash::take_one_unit(polygon.positions()[0]) + } else if !points.is_empty() { + hash::take_one_unit(points[0]) + } else { + return Vec::new(); + }; + sorted_endpoints( + polygon + .faces() + .triangle_iter() + .map(|tri| array![i => polygon.positions()[tri[i].pos]; 3]), + points.iter(), + unit, + tol, + ) +} + +pub fn pointcloud_in_polygon_neighborhood( + polygon: &PolygonMesh, + points: &[Point3], + tol: f64, +) -> bool { + nonpositive_tolerance!(tol, 0.0); + let mut current = Vec::new(); + let triangles = polygon.faces().triangle_iter().collect::>(); + sorted_endpoints_by_polymesh_points(polygon, points, tol) + .into_iter() + .all(move |EndPoint { r#type, index, .. }| match r#type { + EndPointType::Front => { + current.push(index); + true + } + EndPointType::Back => { + let i = current + .iter() + .enumerate() + .find(|(_, idx)| **idx == index) + .unwrap() + .0; + current.swap_remove(i); + true + } + EndPointType::Middle => current.iter().any(|i| { + let tri = triangles[*i]; + let tri = array![i => polygon.positions()[tri[i].pos]; 3]; + distance2_point_triangle(points[index], tri) < tol * tol + }), + }) +} diff --git a/vendor/truck-meshalgo/src/analyzers/splitting.rs b/vendor/truck-meshalgo/src/analyzers/splitting.rs new file mode 100644 index 00000000..9d5c27c9 --- /dev/null +++ b/vendor/truck-meshalgo/src/analyzers/splitting.rs @@ -0,0 +1,343 @@ +use super::*; +use std::f64::consts::PI; + +/// Splitting the faces into several clusters. +pub trait Splitting { + /// Creates a sub mesh by the face indices. + /// # Examples + /// ``` + /// use truck_polymesh::*; + /// use truck_meshalgo::analyzers::*; + /// + /// // cube + /// let mesh = PolygonMesh::new( + /// StandardAttributes { + /// positions: vec![ + /// Point3::new(0.0, 0.0, 0.0), + /// Point3::new(1.0, 0.0, 0.0), + /// Point3::new(1.0, 1.0, 0.0), + /// Point3::new(0.0, 1.0, 0.0), + /// Point3::new(0.0, 0.0, 1.0), + /// Point3::new(1.0, 0.0, 1.0), + /// Point3::new(1.0, 1.0, 1.0), + /// Point3::new(0.0, 1.0, 1.0), + /// ], + /// ..Default::default() + /// }, + /// Faces::from_iter(&[ + /// &[3, 2, 1, 0], &[0, 1, 5, 4], &[1, 2, 6, 5], + /// &[2, 3, 7, 6], &[3, 0, 4, 7], &[4, 5, 6, 7], + /// ]), + /// ); + /// + /// let submesh = mesh.create_mesh_by_face_indices(&[0, 3, 5]); + /// + /// // the same attributes vector + /// assert_eq!(mesh.positions(), submesh.positions()); + /// + /// let faces0 = Faces::from_iter(&[ + /// &[3, 2, 1, 0], &[2, 3, 7, 6], &[4, 5, 6, 7], + /// ]); + /// assert_eq!(submesh.faces().len(), faces0.len()); + /// assert_eq!(submesh.faces()[0], faces0[0]); + /// assert_eq!(submesh.faces()[1], faces0[1]); + /// assert_eq!(submesh.faces()[2], faces0[2]); + /// ``` + fn create_mesh_by_face_indices(&self, indices: &[usize]) -> PolygonMesh; + /// Extracts polygons such that there exists its normal is the same as its face normal. + /// # Arguments + /// - `tol`: tolerance to be regarded as the same normal as the face normal + /// # Returns + /// - The first polygon consists the faces included in planes. + /// - The second polygon is the extracted remainder. + /// # Panics + /// `tol` must be more than `TOLERANCE`. + /// # Examples + /// ``` + /// use truck_polymesh::*; + /// use truck_meshalgo::analyzers::*; + /// let mesh = PolygonMesh::new( + /// StandardAttributes { + /// positions: vec![ + /// Point3::new(0.0, 0.5, 0.0), + /// Point3::new(0.0, 0.5, 1.0), + /// Point3::new(1.0, 0.5, 1.0), + /// Point3::new(1.0, 0.5, 0.0), + /// Point3::new(0.0, 0.0, 2.0), + /// Point3::new(1.0, 0.0, 2.0), + /// ], + /// normals: vec![ + /// Vector3::new(0.0, 1.0, 0.0), + /// // displaced normals for smooth rendering + /// Vector3::new(0.0, 1.0, 1.0).normalize(), + /// ], + /// ..Default::default() + /// }, + /// Faces::from_iter(&[ + /// &[(0, None, Some(0)), (1, None, Some(0)), (2, None, Some(0)), (3, None, Some(0))], + /// &[(2, None, Some(0)), (1, None, Some(0)), (4, None, Some(1)), (5, None, Some(1))], + /// ]), + /// ); + /// let (plane, remained) = mesh.extract_planes(TOLERANCE); // TOLERANCE == 1.0e-7 + /// assert_eq!(plane.len(), 1); assert_eq!(plane[0], 0); + /// assert_eq!(remained.len(), 1); assert_eq!(remained[0], 1); + /// ``` + fn extract_planes(&self, tol: f64) -> (Vec, Vec); + /// Splits into the components. + /// # Details + /// Two polygons are considered to be in the same component if they share an edge + /// whose vertices has the same positions (and normals if `use_normal == true`). + /// # Examples + /// ``` + /// use truck_polymesh::*; + /// use truck_meshalgo::{analyzers::*, filters::*}; + /// + /// // cube consisting tri_faces + /// let mut mesh = PolygonMesh::new( + /// StandardAttributes { + /// positions: vec![ + /// Point3::new(0.0, 0.0, 0.0), + /// Point3::new(1.0, 0.0, 0.0), + /// Point3::new(1.0, 1.0, 0.0), + /// Point3::new(0.0, 1.0, 0.0), + /// Point3::new(0.0, 0.0, 1.0), + /// Point3::new(1.0, 0.0, 1.0), + /// Point3::new(1.0, 1.0, 1.0), + /// Point3::new(0.0, 1.0, 1.0), + /// ], + /// ..Default::default() + /// }, + /// Faces::from_iter(&[ + /// &[3, 2, 0], &[1, 0, 2], &[0, 1, 4], &[5, 4, 1], + /// &[1, 2, 5], &[6, 5, 2], &[2, 3, 6], &[7, 6, 3], + /// &[3, 0, 7], &[4, 7, 0], &[4, 5, 7], &[6, 7, 5], + /// ]), + /// ); + /// + /// // sign up normals + /// mesh.add_naive_normals(true).put_together_same_attrs(TOLERANCE); + /// + /// // into components with normals + /// let components = mesh.components(true); + /// // The number of components is six because the mesh is a cube. + /// assert_eq!(components.len(), 6); + /// + /// // into components without normals + /// let components = mesh.components(false); + /// // The number of components is one because the mesh is a connected cube. + /// assert_eq!(components.len(), 1); + /// ``` + fn components(&self, use_normal: bool) -> Vec>; +} + +impl Splitting for PolygonMesh { + fn create_mesh_by_face_indices(&self, indices: &[usize]) -> PolygonMesh { + let positions = self.positions().clone(); + let uv_coords = self.uv_coords().clone(); + let normals = self.normals().clone(); + let faces: Faces = indices.iter().map(|i| &self.faces()[*i]).collect(); + PolygonMesh::new( + StandardAttributes { + positions, + uv_coords, + normals, + }, + faces, + ) + } + + fn extract_planes(&self, tol: f64) -> (Vec, Vec) { + nonpositive_tolerance!(tol, 0.0); + self.faces_into_two_clusters(|face: &[Vertex]| { + is_in_the_plane(self.positions(), self.normals(), face, tol * tol) + }) + } + + fn components(&self, use_normal: bool) -> Vec> { + let face_adjacency = self.faces().face_adjacency(use_normal); + get_components(&face_adjacency) + } +} + +#[doc(hidden)] +pub trait ExperimentalSplitters { + fn faces_into_two_clusters bool>( + &self, + func: F, + ) -> (Vec, Vec); + fn clustering_faces_by_gcurvature( + &self, + threshold: f64, + preferred_upper: bool, + ) -> (Vec, Vec); + fn get_gcurve(&self) -> Vec; +} + +/// splitting the global faces +impl ExperimentalSplitters for PolygonMesh { + /// separate all faces into two clusters, one with `func` returning true and the other with false. + /// # Returns + /// (the vector of all faces `f` which `func(f) == true`, the one of the other faces) + fn faces_into_two_clusters bool>( + &self, + func: F, + ) -> (Vec, Vec) { + let mut true_faces = Vec::new(); + let mut false_faces = Vec::new(); + for (i, face) in self.face_iter().enumerate() { + match func(face) { + true => true_faces.push(i), + false => false_faces.push(i), + } + } + (true_faces, false_faces) + } + + fn clustering_faces_by_gcurvature( + &self, + threshold: f64, + preferred_upper: bool, + ) -> (Vec, Vec) { + let gcurve = self.get_gcurve(); + self.faces_into_two_clusters(|face: &[Vertex]| { + is_signed_up_upper(face, &gcurve, preferred_upper, threshold) + }) + } + + fn get_gcurve(&self) -> Vec { + let positions = self.positions(); + let mut angles = vec![0.0; positions.len()]; + let mut weights = vec![0.0; positions.len()]; + for face in self.tri_faces() { + angles[face[0].pos] += get_angle(positions, face, 0, 1, 2); + angles[face[1].pos] += get_angle(positions, face, 1, 2, 0); + angles[face[2].pos] += get_angle(positions, face, 2, 0, 1); + add_weights(&mut weights, positions, face); + } + for face in self.quad_faces() { + angles[face[0].pos] += get_angle(positions, face, 0, 1, 3); + angles[face[1].pos] += get_angle(positions, face, 1, 2, 0); + angles[face[2].pos] += get_angle(positions, face, 2, 3, 1); + angles[face[3].pos] += get_angle(positions, face, 3, 0, 1); + add_weights(&mut weights, positions, face); + } + for face in self.other_faces() { + let n = face.len() - 1; + angles[face[0].pos] += get_angle(positions, face, 0, 1, n); + for i in 1..n { + angles[face[i].pos] += get_angle(positions, face, i, i + 1, i - 1); + } + angles[face[n].pos] += get_angle(positions, face, n, 0, n - 1); + add_weights(&mut weights, positions, face); + } + + angles + .into_iter() + .zip(weights) + .map(|(ang, weight)| (PI * 2.0 - ang) / weight) + .collect() + } +} + +/// divide the graph to the connected components. +/// # Arguments +/// * adjacency - the adjacency matrix +/// # Return +/// * the list of the indices of faces contained in each components +fn get_components(adjacency: &[Vec]) -> Vec> { + let mut unchecked = vec![true; adjacency.len()]; + let mut components = Vec::new(); + loop { + let first = match unchecked.iter().position(|x| *x) { + Some(idx) => idx, + None => return components, + }; + let mut stack = vec![first]; + let mut component = vec![first]; + unchecked[first] = false; + while let Some(cursor) = stack.pop() { + for i in &adjacency[cursor] { + if unchecked[*i] { + unchecked[*i] = false; + component.push(*i); + stack.push(*i); + } + } + } + components.push(component); + } +} + +fn is_in_the_plane(positions: &[Point3], normals: &[Vector3], face: &[Vertex], tol2: f64) -> bool { + let n = FaceNormal::new(positions, face, 0).normal; + for v in face { + if let Some(nor) = v.nor { + if n.distance2(normals[nor]) < tol2 { + return true; + } + } + } + false +} + +fn is_signed_up_upper( + face: &[Vertex], + gcurve: &[f64], + preferred_upper: bool, + threshold: f64, +) -> bool { + if preferred_upper { + face.as_ref().iter().any(|v| gcurve[v.pos] > threshold) + } else { + face.as_ref().iter().all(|v| gcurve[v.pos] > threshold) + } +} + +fn get_angle(positions: &[Point3], face: &[Vertex], idx0: usize, idx1: usize, idx2: usize) -> f64 { + let vec0 = positions[face[idx1].pos] - positions[face[idx0].pos]; + let vec1 = positions[face[idx2].pos] - positions[face[idx0].pos]; + vec0.angle(vec1).0 +} + +fn add_weights(weights: &mut [f64], positions: &[Point3], face: &[Vertex]) { + let area = (2..face.len()).fold(0.0, |sum, i| { + let vec0 = positions[face[i - 1].pos] - positions[face[0].pos]; + let vec1 = positions[face[i].pos] - positions[face[0].pos]; + sum + (vec0.cross(vec1)).magnitude() / 2.0 + }) / (face.len() as f64); + for v in face { + weights[v.pos] += area; + } +} + +#[test] +fn into_components_test() { + let faces = vec![ + [(0, None, Some(0)), (1, None, Some(1)), (2, None, Some(2))].as_ref(), + &[(0, None, Some(0)), (5, None, Some(5)), (1, None, Some(1))], + &[(0, None, Some(7)), (5, None, Some(5)), (6, None, Some(6))], + &[(5, None, Some(5)), (6, None, Some(6)), (7, None, Some(7))], + &[ + (1, None, Some(1)), + (4, None, Some(4)), + (3, None, Some(3)), + (2, None, Some(2)), + ], + ] + .into_iter() + .collect(); + let positions = vec![Point3::origin(); 8]; + let normals = vec![Vector3::unit_x(); 8]; + let mesh = PolygonMesh::new( + StandardAttributes { + positions, + normals, + ..Default::default() + }, + faces, + ); + let comp = mesh.components(true); + assert_eq!(comp.len(), 2); + assert_eq!(comp[0], vec![0, 1, 4]); + assert_eq!(comp[1], vec![2, 3]); +} diff --git a/vendor/truck-meshalgo/src/analyzers/topology.rs b/vendor/truck-meshalgo/src/analyzers/topology.rs new file mode 100644 index 00000000..e993c3e5 --- /dev/null +++ b/vendor/truck-meshalgo/src/analyzers/topology.rs @@ -0,0 +1,117 @@ +use super::*; +use itertools::Itertools; +use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; +use truck_topology::shell::ShellCondition; + +/// Extracts boundaries, and check shell condition: closed or orientation. +pub trait Topology { + /// Returns a vector of all boundaries as line strip. + fn extract_boundaries(&self) -> Vec>; + /// Determines the shell conditions: non-regular, regular, oriented, or closed. + /// The complexity increases in proportion to the number of edges. + /// + /// Examples for each condition can be found on the page of + /// [`ShellCondition`](https://docs.rs/truck-topology/0.2.0/truck_topology/shell/enum.ShellCondition.html). + fn shell_condition(&self) -> ShellCondition; +} + +#[derive(Clone, Debug)] +struct Boundaries { + checked: HashSet<[usize; 2]>, + boundary: HashMap<[usize; 2], bool>, + condition: ShellCondition, +} + +impl Boundaries { + #[inline(always)] + fn new() -> Self { + Boundaries { + checked: Default::default(), + boundary: Default::default(), + condition: ShellCondition::Oriented, + } + } + #[inline(always)] + fn insert(&mut self, edge: [Vertex; 2]) { + let ori = edge[0].pos < edge[1].pos; + let edge = match ori { + true => [edge[0].pos, edge[1].pos], + false => [edge[1].pos, edge[0].pos], + }; + self.condition = self.condition + & match (self.checked.insert(edge), self.boundary.insert(edge, ori)) { + (true, None) => ShellCondition::Oriented, + (false, None) => ShellCondition::Irregular, + (true, Some(_)) => panic!("unexpected case!"), + (false, Some(ori0)) => { + self.boundary.remove(&edge); + match ori == ori0 { + true => ShellCondition::Regular, + false => ShellCondition::Oriented, + } + } + }; + } + + #[inline(always)] + fn condition(&self) -> ShellCondition { + if self.condition == ShellCondition::Oriented && self.boundary.is_empty() { + ShellCondition::Closed + } else { + self.condition + } + } +} + +impl FromIterator<[Vertex; 2]> for Boundaries { + fn from_iter>(iter: I) -> Boundaries { + let mut boundaries = Boundaries::new(); + iter.into_iter().for_each(|edge| boundaries.insert(edge)); + boundaries + } +} + +fn face_edge_iter(face: &[T]) -> impl Iterator + '_ { + face.iter().circular_tuple_windows().map(|(v, w)| [*v, *w]) +} + +impl Topology for Faces { + fn extract_boundaries(&self) -> Vec> { + let boundaries: Boundaries = self.face_iter().flat_map(face_edge_iter).collect(); + let mut vemap: HashMap = boundaries + .boundary + .into_iter() + .map(|(edge, ori)| match ori { + true => (edge[0], edge[1]), + false => (edge[1], edge[0]), + }) + .collect(); + + let mut res = Vec::new(); + while !vemap.is_empty() { + let mut wire = Vec::new(); + let front = vemap.iter().next().unwrap(); + let front = (*front.0, *front.1); + vemap.remove(&front.0); + wire.push(front.0); + let mut cursor = front.1; + while cursor != front.0 { + wire.push(cursor); + cursor = vemap.remove(&cursor).unwrap_or(front.0); + } + res.push(wire); + } + res + } + fn shell_condition(&self) -> ShellCondition { + self.face_iter() + .flat_map(face_edge_iter) + .collect::() + .condition() + } +} + +impl Topology for PolygonMesh { + fn extract_boundaries(&self) -> Vec> { self.faces().extract_boundaries() } + fn shell_condition(&self) -> ShellCondition { self.faces().shell_condition() } +} diff --git a/vendor/truck-meshalgo/src/analyzers/volume.rs b/vendor/truck-meshalgo/src/analyzers/volume.rs new file mode 100644 index 00000000..52129b3e --- /dev/null +++ b/vendor/truck-meshalgo/src/analyzers/volume.rs @@ -0,0 +1,175 @@ +use super::*; +use array_macro::array; + +/// Calculate the volume and the center of gravity of mesh. +pub trait CalcVolume { + /// Returns the volume of the mesh if the mesh is closed. + /// + /// # Details + /// More strictly, it returns the integral value of `xdydz`. Because of the linearity of this operation, + /// if multiple meshes represent a single closed geometry, the overall volume can be calculated by computing + /// this value for each and adding them together. + /// + /// # Examples + /// ``` + /// // The regular trihedron in the unit sphere. + /// use truck_meshalgo::prelude::*; + /// let a = 1.0 / f64::sqrt(3.0); + /// let positions = vec![ + /// Point3::new(-a, -a, -a), + /// Point3::new(a, a, -a), + /// Point3::new(a, -a, a), + /// Point3::new(-a, a, a), + /// ]; + /// let attrs = StandardAttributes { + /// positions, + /// ..Default::default() + /// }; + /// let faces = Faces::from_iter(&[ + /// [0, 1, 2], + /// [1, 3, 2], + /// [1, 0, 3], + /// [0, 2, 3], + /// ]); + /// let polygon = PolygonMesh::new(attrs, faces); + /// assert_near!(polygon.volume(), 8.0 * f64::sqrt(3.0) / 27.0); + /// ``` + /// ``` + /// // Cube with a hollow inside, defined by two boundaries. + /// use truck_meshalgo::prelude::*; + /// let positions0 = vec![ + /// Point3::new(0.0, 0.0, 0.0), + /// Point3::new(3.0, 0.0, 0.0), + /// Point3::new(0.0, 3.0, 0.0), + /// Point3::new(0.0, 0.0, 3.0), + /// Point3::new(0.0, 3.0, 3.0), + /// Point3::new(3.0, 0.0, 3.0), + /// Point3::new(3.0, 3.0, 0.0), + /// Point3::new(3.0, 3.0, 3.0), + /// ]; + /// let attrs0 = StandardAttributes { + /// positions: positions0, + /// ..Default::default() + /// }; + /// let faces0 = Faces::from_iter(&[ + /// [0, 2, 6, 1], + /// [0, 1, 5, 3], + /// [1, 6, 7, 5], + /// [6, 2, 4, 7], + /// [2, 0, 3, 4], + /// [3, 5, 7, 4], + /// ]); + /// let polygon0 = PolygonMesh::new(attrs0, faces0); + /// let positions1 = vec![ + /// Point3::new(1.0, 1.0, 1.0), + /// Point3::new(2.0, 1.0, 1.0), + /// Point3::new(1.0, 2.0, 1.0), + /// Point3::new(1.0, 1.0, 2.0), + /// Point3::new(1.0, 2.0, 2.0), + /// Point3::new(2.0, 1.0, 2.0), + /// Point3::new(2.0, 2.0, 1.0), + /// Point3::new(2.0, 2.0, 2.0), + /// ]; + /// let attrs1 = StandardAttributes { + /// positions: positions1, + /// ..Default::default() + /// }; + /// let faces1 = Faces::from_iter(&[ + /// [1, 6, 2, 0], + /// [3, 5, 1, 0], + /// [5, 7, 6, 1], + /// [7, 4, 2, 6], + /// [4, 3, 0, 2], + /// [4, 7, 5, 3], + /// ]); + /// let polygon1 = PolygonMesh::new(attrs1, faces1); + /// // We consider the solid defined by two boundaries(polygon0 and polygon1). + /// // The volume will be calculated by `position0.volume() + position1.volume()`. + /// assert_near!(polygon0.volume() + polygon1.volume(), 26.0); + /// ``` + fn volume(&self) -> f64; + /// Returns the center of gravity of the mesh by homogeneous coordinate if the mesh is closed. + /// + /// # Details + /// More strictly, it returns the integral value of `(1/2 x^2 dydz, 1/2 y^2dzdx, 1/2 z^2 dxdy, xdydz)`. + /// Because of the linearity of this operation, if multiple meshes represent a single closed geometry, + /// the overall volume can be calculated by computing this value for each and adding them together. + /// + /// # Examples + /// ``` + /// // The triangular prism + /// use truck_meshalgo::prelude::*; + /// let positions = vec![ + /// Point3::new(0.0, 0.0, 0.0), + /// Point3::new(2.0, 5.0, 0.0), + /// Point3::new(-5.0, 1.0, 0.0), + /// Point3::new(0.0, 0.0, 2.0), + /// Point3::new(2.0, 5.0, 2.0), + /// Point3::new(-5.0, 1.0, 2.0), + /// ]; + /// let attrs = StandardAttributes { + /// positions, + /// ..Default::default() + /// }; + /// let faces = Faces::from_iter(&[ + /// [2, 1, 0].as_slice(), + /// &[0, 1, 4, 3], + /// &[1, 2, 5, 4], + /// &[2, 0, 3, 5], + /// &[3, 4, 5], + /// ]); + /// let polygon = PolygonMesh::new(attrs, faces); + /// let homog = polygon.center_of_gravity(); + /// assert_near!(homog.to_point(), Point3::new(-1.0, 2.0, 1.0)); + /// ``` + fn center_of_gravity(&self) -> Vector4; +} + +impl CalcVolume for PolygonMesh { + fn volume(&self) -> f64 { + point_triangles(self).fold(0.0, |sum, [p, q, r]| { + sum + (p.x + q.x + r.x) * ((q.y - p.y) * (r.z - p.z) - (r.y - p.y) * (q.z - p.z)) + }) / 6.0 + } + fn center_of_gravity(&self) -> Vector4 { + let arr = point_triangles(self).fold([0.0; 4], |sum, [p, q, r]| { + let det = array![i => { + let (j, k) = ((i + 1) % 3, (i + 2) % 3); + (q[j] - p[j]) * (r[k] - p[k]) - (r[j] - p[j]) * (q[k] - p[k]) + }; 3]; + let vals = array![i => { + let s = p[i] + q[i] + r[i]; + s * s - p[i] * q[i] - q[i] * r[i] - r[i] * p[i] + }; 3]; + let res = array![i => sum[i] + vals[i] * det[i]; 3]; + let res3 = sum[3] + (p.x + q.x + r.x) * det[0]; + [res[0], res[1], res[2], res3] + }); + Vector4::new(arr[0] / 24.0, arr[1] / 24.0, arr[2] / 24.0, arr[3] / 6.0) + } +} + +fn point_triangles(poly: &PolygonMesh) -> impl Iterator + '_ { + poly.faces() + .triangle_iter() + .map(|faces| array![i => poly.positions()[faces[i].pos]; 3]) +} + +impl CalcVolume for truck_topology::Solid, PolygonMesh> { + fn volume(&self) -> f64 { + self.face_iter() + .map(|face| match face.orientation() { + true => face.surface().volume(), + false => -face.surface().volume(), + }) + .sum::() + } + fn center_of_gravity(&self) -> Vector4 { + self.face_iter() + .map(|face| match face.orientation() { + true => face.surface().center_of_gravity(), + false => -face.surface().center_of_gravity(), + }) + .sum::() + } +} diff --git a/vendor/truck-meshalgo/src/common/adjacency.rs b/vendor/truck-meshalgo/src/common/adjacency.rs new file mode 100644 index 00000000..79ffc36c --- /dev/null +++ b/vendor/truck-meshalgo/src/common/adjacency.rs @@ -0,0 +1,77 @@ +use super::*; +use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; + +pub trait Adjacency { + #[allow(dead_code)] + fn vertex_adjacency(&self, num_of_vertices: usize) -> Vec>; + /// create the adjacency list of the faces + fn face_adjacency(&self, use_normal: bool) -> Vec>; +} + +impl Adjacency for Faces { + fn vertex_adjacency(&self, num_of_vertices: usize) -> Vec> { + let mut already = HashSet::default(); + let mut res = vec![Vec::new(); num_of_vertices]; + for face in self.face_iter() { + face.windows(2) + .chain(std::iter::once([face[face.len() - 1], face[0]].as_ref())) + .map(|edge| [edge[0].pos, edge[1].pos]) + .for_each(|edge| { + let first = match edge[0] < edge[1] { + true => already.insert((edge[0], edge[1])), + false => already.insert((edge[1], edge[0])), + }; + if first { + res[edge[0]].push(edge[1]); + res[edge[1]].push(edge[0]); + } + }) + } + res + } + fn face_adjacency(&self, use_normal: bool) -> Vec> { + let len = self.len(); + let mut face_adjacency = vec![Vec::::new(); len]; + let mut edge_face_map = HashMap::default(); + for (i, face) in self.face_iter().enumerate() { + face.windows(2) + .chain(std::iter::once([face[face.len() - 1], face[0]].as_ref())) + .for_each(|v| { + signup_adjacency( + i, + v[0], + v[1], + &mut face_adjacency, + &mut edge_face_map, + use_normal, + ) + }) + } + face_adjacency + } +} + +fn signup_adjacency( + i: usize, + v0: Vertex, + v1: Vertex, + face_adjacency: &mut [Vec], + edge_face_map: &mut HashMap<[(usize, Option); 2], usize>, + use_normal: bool, +) { + let edge = match (v0.pos < v1.pos, use_normal) { + (true, true) => [(v0.pos, v0.nor), (v1.pos, v1.nor)], + (false, true) => [(v1.pos, v1.nor), (v0.pos, v0.nor)], + (true, false) => [(v0.pos, None), (v1.pos, None)], + (false, false) => [(v1.pos, None), (v0.pos, None)], + }; + match edge_face_map.get(&edge) { + Some(j) => { + face_adjacency[i].push(*j); + face_adjacency[*j].push(i); + } + None => { + edge_face_map.insert(edge, i); + } + } +} diff --git a/vendor/truck-meshalgo/src/common/face_normal.rs b/vendor/truck-meshalgo/src/common/face_normal.rs new file mode 100644 index 00000000..4a9e5d6f --- /dev/null +++ b/vendor/truck-meshalgo/src/common/face_normal.rs @@ -0,0 +1,26 @@ +use super::*; + +#[derive(Clone, Copy, Debug)] +pub struct FaceNormal { + pub face_id: usize, + pub normal: Vector3, +} + +impl FaceNormal { + pub fn new(positions: &[Point3], face: &[Vertex], face_id: usize) -> FaceNormal { + let center = face + .iter() + .fold(Vector3::zero(), |sum, v| sum + positions[v.pos].to_vec()) + / face.len() as f64; + let normal = face + .windows(2) + .chain(std::iter::once([face[face.len() - 1], face[0]].as_ref())) + .fold(Vector3::zero(), |sum, v| { + let vec0 = positions[v[0].pos].to_vec() - center; + let vec1 = positions[v[1].pos].to_vec() - center; + sum + vec0.cross(vec1) + }) + .normalize(); + FaceNormal { face_id, normal } + } +} diff --git a/vendor/truck-meshalgo/src/common/mod.rs b/vendor/truck-meshalgo/src/common/mod.rs new file mode 100644 index 00000000..322e0459 --- /dev/null +++ b/vendor/truck-meshalgo/src/common/mod.rs @@ -0,0 +1,6 @@ +use crate::*; + +mod adjacency; +mod face_normal; +pub(super) use adjacency::Adjacency; +pub(super) use face_normal::FaceNormal; diff --git a/vendor/truck-meshalgo/src/common/triangulate.rs b/vendor/truck-meshalgo/src/common/triangulate.rs new file mode 100644 index 00000000..f60a39fa --- /dev/null +++ b/vendor/truck-meshalgo/src/common/triangulate.rs @@ -0,0 +1,105 @@ +use super::*; + +/// STL face generate from `PolygonMesh` +#[derive(Debug)] +pub struct TriangleIter<'a> { + tri_faces: std::slice::Iter<'a, [Vertex; 3]>, + quad_faces: std::slice::Iter<'a, [Vertex; 4]>, + other_faces: std::slice::Iter<'a, [Vertex; 3]>, + current_face: Option<&'a [Vertex]>, + current_vertex: usize, + len: usize, +} + +impl<'a> Iterator for TriangleIter<'a> { + type Item = [Vertex; 3]; + fn next(&mut self) -> Option<[Vertex; 3]> { + if self.current_face.is_none() { + self.current_face = if let Some(face) = self.tri_faces.next() { + Some(face) + } else if let Some(face) = self.quad_faces.next() { + Some(face) + } else if let Some(face) = self.other_faces.next() { + Some(face) + } else { + return None; + } + } + let face = self.current_face.unwrap(); + let res = [ + face[0], + face[self.current_vertex + 1], + face[self.current_vertex + 2], + ]; + if self.current_vertex + 3 == face.len() { + self.current_face = None; + self.current_vertex = 0; + } else { + self.current_vertex += 1; + } + self.len -= 1; + Some(res) + } + #[inline(always)] + fn size_hint(&self) -> (usize, Option) { (self.len, Some(self.len)) } +} + +impl<'a> ExactSizeIterator for TriangleIter<'a> {} + +#[derive(Clone, Debug)] +pub struct Triangulate<'a> { + entity: &'a PolygonMesh, + other_faces: Vec<[Vertex; 3]>, +} + +impl<'a> Triangulate<'a> { + #[inline(always)] + pub fn new(entity: &'a PolygonMesh) -> Self { + fn divide_mesh(face: &Vec) -> impl Iterator + '_ { + face.windows(2).skip(1).map(move |a| [face[0], a[0], a[1]]) + } + Triangulate { + other_faces: entity.other_faces().iter().flat_map(divide_mesh).collect(), + entity, + } + } + #[inline(always)] + pub fn get(&self, idx: usize) -> [Vertex; 3] { + let tri_and_quad = self.entity.tri_faces().len() + 2 * self.entity.quad_faces().len(); + if idx < self.entity.tri_faces().len() { + self.entity.tri_faces()[idx] + } else if idx < tri_and_quad { + let idx = idx - self.entity.tri_faces().len(); + let face = self.entity.quad_faces()[idx / 2]; + if idx % 2 == 0 { + [face[0], face[1], face[2]] + } else { + [face[0], face[2], face[3]] + } + } else { + self.other_faces[idx - tri_and_quad] + } + } + #[inline(always)] + pub fn entity(&self) -> &PolygonMesh { self.entity } +} + +impl<'a> IntoIterator for &'a Triangulate<'a> { + type Item = [Vertex; 3]; + type IntoIter = TriangleIter<'a>; + #[inline(always)] + fn into_iter(self) -> TriangleIter<'a> { + let len = self + .entity + .face_iter() + .fold(0, |len, face| len + face.len()); + TriangleIter { + tri_faces: self.entity.tri_faces().iter(), + quad_faces: self.entity.quad_faces().iter(), + other_faces: self.other_faces.iter(), + current_face: None, + current_vertex: 0, + len, + } + } +} diff --git a/vendor/truck-meshalgo/src/filters/mod.rs b/vendor/truck-meshalgo/src/filters/mod.rs new file mode 100644 index 00000000..991106da --- /dev/null +++ b/vendor/truck-meshalgo/src/filters/mod.rs @@ -0,0 +1,12 @@ +use crate::*; +use polygon_mesh::PolygonMeshEditor; + +mod normal_filters; +mod optimizing; +mod structuring; +mod subdivision; + +pub use normal_filters::NormalFilters; +pub use optimizing::OptimizingFilter; +pub use structuring::StructuringFilter; +pub use subdivision::Subdivision; diff --git a/vendor/truck-meshalgo/src/filters/normal_filters.rs b/vendor/truck-meshalgo/src/filters/normal_filters.rs new file mode 100644 index 00000000..fea680c6 --- /dev/null +++ b/vendor/truck-meshalgo/src/filters/normal_filters.rs @@ -0,0 +1,396 @@ +use super::*; +use rustc_hash::FxHashMap as HashMap; + +/// Filters for adding normals +pub trait NormalFilters { + /// Normalize all normals and assign `None` to the `nor` index of the vertices + /// that has irregular normals. + /// # Examples + /// ``` + /// use truck_polymesh::*; + /// use truck_meshalgo::filters::*; + /// + /// // Morbid data only for testing + /// let mut mesh = PolygonMesh::new( + /// StandardAttributes { + /// positions: vec![Point3::new(0.0, 0.0, 0.0)], + /// normals: vec![ + /// Vector3::new(100.0, 20.0, 56.0), + /// Vector3::new(1.0e-12, 3.536e10, std::f64::NAN), + /// Vector3::new(0.0, 1.0, 0.0), + /// ], + /// ..Default::default() + /// }, + /// Faces::from_iter(&[&[ + /// (0, None, Some(0)), + /// (0, None, Some(1)), + /// (0, None, Some(2)) + /// ]]), + /// ); + /// + /// mesh.normalize_normals(); + /// assert!(mesh.normals()[0].magnitude().near(&1.0)); + /// assert_eq!(mesh.faces()[0][1].nor, None); + /// ``` + fn normalize_normals(&mut self) -> &mut Self; + /// Adds face normals to each vertices. + /// # Arguments + /// - If `overwrite == true`, clear all normals and update all normals in vertices. + /// - If `overwrite == false`, add normals only for `nor` is `None`. + /// # Examples + /// Compare with the examples of [`add_smooth_normals`](./trait.NormalFilters.html#tymethod.add_smooth_normals). + /// ``` + /// use truck_meshalgo::prelude::*; + /// let mut mesh = PolygonMesh::new( + /// StandardAttributes { + /// positions: vec![ + /// Point3::new(-5.0, 0.0, 0.0), + /// Point3::new(0.0, 2.0, -2.0), + /// Point3::new(0.0, 2.0, 0.0), + /// Point3::new(0.0, 2.0, 2.0), + /// Point3::new(5.0, 0.0, 0.0), + /// ], + /// ..Default::default() + /// }, + /// Faces::from_iter(&[ + /// &[0, 2, 1], + /// &[0, 3, 2], + /// &[1, 2, 4], + /// &[2, 3, 4], + /// ]), + /// ); + /// + /// mesh.add_naive_normals(true); + /// let v0: StandardVertex = mesh.faces()[0][1]; + /// let v1: StandardVertex = mesh.faces()[3][0]; + /// + /// // those vertices are at position with the index 2. + /// assert_eq!(v0.pos, 2); assert_eq!(v1.pos, 2); + /// + /// // Each normal is each face normal. + /// assert!(mesh.normals()[v0.nor.unwrap()].near(&Vector3::new(-2.0, 5.0, 0.0).normalize())); + /// assert!(mesh.normals()[v1.nor.unwrap()].near(&Vector3::new(2.0, 5.0, 0.0).normalize())); + /// ``` + fn add_naive_normals(&mut self, overwrite: bool) -> &mut Self; + /// add the smooth normal vectors to the mesh. + /// # Details + /// For each vertex, apply the following algorithm: + /// 1. prepare vectors that enumerate the normals of the faces containing the target vertices in order. + /// 1. cluster each normal `n` in turn in the following manner. + /// * If there is an existing cluster `A` in which the angle between the weighted + /// average of `A` and `n` is less than or equal to `tol_ang`, add `n` to `A`. + /// * If cluster `A` as described above does not exist, create a new cluster that contains only `n`. + /// # Arguments + /// - If `overwrite == true`, clear all normals and update all normals in vertices. + /// - If `overwrite == false`, add normals only for `nor` is `None`. + /// # Examples + /// Compare with the examples of [`add_smooth_normals`](./trait.NormalFilters.html#tymethod.add_smooth_normals). + /// ``` + /// use truck_meshalgo::prelude::*; + /// let mut mesh = PolygonMesh::new( + /// StandardAttributes { + /// positions: vec![ + /// Point3::new(-5.0, 0.0, 0.0), + /// Point3::new(0.0, 2.0, -2.0), + /// Point3::new(0.0, 2.0, 0.0), + /// Point3::new(0.0, 2.0, 2.0), + /// Point3::new(5.0, 0.0, 0.0), + /// ], + /// ..Default::default() + /// }, + /// Faces::from_iter(&[ + /// &[0, 2, 1], &[0, 3, 2], &[1, 2, 4], &[2, 3, 4], + /// ]), + /// ); + /// + /// mesh.add_smooth_normals(0.8, true); + /// let v0: StandardVertex = mesh.faces()[0][1]; + /// let v1: StandardVertex = mesh.faces()[3][0]; + /// + /// // those vertices are at position with the index 2. + /// assert_eq!(v0.pos, 2); assert_eq!(v1.pos, 2); + /// + /// // Normals are avaraged! + /// assert!(mesh.normals()[v0.nor.unwrap()].near(&Vector3::new(0.0, 1.0, 0.0))); + /// assert!(mesh.normals()[v1.nor.unwrap()].near(&Vector3::new(0.0, 1.0, 0.0))); + /// assert_eq!(v0.nor, v1.nor); + /// + /// // If the tolerance is enough little, the faces are recognized as edges. + /// mesh.add_smooth_normals(0.6, true); // Normals are overwritten! + /// let v0: StandardVertex = mesh.faces()[0][1]; + /// let v1: StandardVertex = mesh.faces()[3][0]; + /// assert!(mesh.normals()[v0.nor.unwrap()].near(&Vector3::new(-2.0, 5.0, 0.0).normalize())); + /// assert!(mesh.normals()[v1.nor.unwrap()].near(&Vector3::new(2.0, 5.0, 0.0).normalize())); + /// ``` + fn add_smooth_normals(&mut self, tol_ang: f64, overwrite: bool) -> &mut Self; + /// Makes the orientation of faces compatible to the normal vectors. + /// # Examples + /// ``` + /// use truck_polymesh::*; + /// use truck_meshalgo::filters::*; + /// let mut polymesh = PolygonMesh::new( + /// StandardAttributes { + /// positions: vec![ + /// Point3::new(0.0, 0.0, 0.0), + /// Point3::new(1.0, 0.0, 0.0), + /// Point3::new(0.0, 1.0, 0.0), + /// ], + /// normals: vec![Vector3::new(0.0, 0.0, -1.0)], + /// ..Default::default() + /// }, + /// Faces::from_tri_and_quad_faces( + /// vec![[ + /// (0, None, Some(0)).into(), + /// (1, None, Some(0)).into(), + /// (2, None, Some(0)).into(), + /// ]], + /// Vec::new(), + /// ), + /// ); + /// polymesh.make_face_compatible_to_normal(); + /// assert_eq!(polymesh.faces()[0][0].pos, 2); + /// assert_eq!(polymesh.faces()[0][1].pos, 1); + /// assert_eq!(polymesh.faces()[0][2].pos, 0); + /// ``` + fn make_face_compatible_to_normal(&mut self) -> &mut Self; + /// Makes the orientation of faces compatible to the normal vectors. + /// # Examples + /// ``` + /// use truck_polymesh::*; + /// use truck_meshalgo::filters::*; + /// let mut polymesh = PolygonMesh::new( + /// StandardAttributes { + /// positions: vec![ + /// Point3::new(0.0, 0.0, 0.0), + /// Point3::new(1.0, 0.0, 0.0), + /// Point3::new(0.0, 1.0, 0.0), + /// ], + /// normals: vec![Vector3::new(0.0, 0.0, -1.0)], + /// ..Default::default() + /// }, + /// Faces::from_tri_and_quad_faces( + /// vec![[ + /// (0, None, Some(0)).into(), + /// (1, None, Some(0)).into(), + /// (2, None, Some(0)).into(), + /// ]], + /// Vec::new(), + /// ), + /// ); + /// polymesh.make_normal_compatible_to_face(); + /// let nor = polymesh.faces()[0][0].nor.unwrap(); + /// assert_eq!(polymesh.normals()[nor], Vector3::new(0.0, 0.0, 1.0)); + /// ``` + fn make_normal_compatible_to_face(&mut self) -> &mut Self; +} + +impl NormalFilters for PolygonMesh { + fn normalize_normals(&mut self) -> &mut Self { + let mut mesh = self.debug_editor(); + let PolygonMeshEditor { + attributes: StandardAttributes { normals, .. }, + faces, + .. + } = &mut mesh; + normals + .iter_mut() + .for_each(move |normal| *normal = normal.normalize()); + faces.face_iter_mut().flatten().for_each(|v| { + if let Some(idx) = v.nor { + if !normals[idx].magnitude2().near(&1.0) { + v.nor = None; + } + } + }); + drop(mesh); + self + } + fn make_face_compatible_to_normal(&mut self) -> &mut Self { + let mut mesh = self.debug_editor(); + let PolygonMeshEditor { + attributes: StandardAttributes { + positions, normals, .. + }, + faces, + .. + } = &mut mesh; + for face in faces.face_iter_mut() { + let normal = face.iter().fold(Vector3::zero(), |normal, v| { + normal + v.nor.map(|i| normals[i]).unwrap_or_else(Vector3::zero) + }); + let face_normal = FaceNormal::new(positions, face, 0).normal; + if normal.dot(face_normal) < 0.0 { + face.reverse(); + } + } + drop(mesh); + self + } + fn make_normal_compatible_to_face(&mut self) -> &mut Self { + let mut mesh = self.debug_editor(); + let PolygonMeshEditor { + attributes: StandardAttributes { + positions, normals, .. + }, + faces, + .. + } = &mut mesh; + for face in faces.face_iter_mut() { + let face_normal = FaceNormal::new(positions, face, 0).normal; + face.iter_mut().for_each(|v| { + v.nor = v.nor.map(|idx| { + if normals[idx].dot(face_normal) < 0.0 { + normals.push(-normals[idx]); + normals.len() - 1 + } else { + idx + } + }); + }) + } + drop(mesh); + self + } + fn add_naive_normals(&mut self, overwrite: bool) -> &mut Self { + let mut mesh = self.debug_editor(); + let PolygonMeshEditor { + attributes: StandardAttributes { + positions, normals, .. + }, + faces, + .. + } = &mut mesh; + if overwrite { + normals.clear() + } + faces.face_iter_mut().for_each(move |face| { + let normal = FaceNormal::new(positions, face, 0).normal; + let mut added = false; + face.iter_mut().for_each(|v| { + if v.nor.is_none() || overwrite { + if !added { + normals.push(normal); + added = true; + } + v.nor = Some(normals.len() - 1); + } + }); + }); + drop(mesh); + self + } + fn add_smooth_normals(&mut self, tol_ang: f64, overwrite: bool) -> &mut Self { + let vnmap = self.clustering_noraml_faces(tol_ang.cos()); + self.reflect_normal_clusters(vnmap, overwrite); + self + } +} + +trait SubNormalFilter { + fn clustering_noraml_faces(&self, inf: f64) -> HashMap>>; + fn reflect_normal_clusters( + &mut self, + vnmap: HashMap>>, + overwrite: bool, + ); +} + +impl SubNormalFilter for PolygonMesh { + fn clustering_noraml_faces(&self, inf: f64) -> HashMap>> { + let positions = self.positions(); + let mut vnmap = HashMap::default(); + self.face_iter() + .enumerate() + .for_each(|(i, face)| add_face_normal(positions, i, face, &mut vnmap, inf)); + vnmap + } + + fn reflect_normal_clusters( + &mut self, + vnmap: HashMap>>, + overwrite: bool, + ) { + let mut mesh = self.debug_editor(); + let PolygonMeshEditor { + attributes: StandardAttributes { normals, .. }, + faces, + .. + } = &mut mesh; + if overwrite { + normals.clear(); + } + for (pos_id, vecs) in vnmap.into_iter() { + for vec in vecs { + let normal = vec + .iter() + .fold(Vector3::zero(), |sum, x| sum + x.normal) + .normalize(); + for FaceNormal { face_id, .. } in vec { + signup_vertex_normal(pos_id, face_id, normals, normal, faces, overwrite); + } + } + } + } +} + +fn add_face_normal( + positions: &[Point3], + face_id: usize, + face: &[Vertex], + vnmap: &mut HashMap>>, + inf: f64, +) { + let face_normal = FaceNormal::new(positions, face, face_id); + face.iter().for_each(|v| { + add_to_vnmap(v.pos, face_normal, vnmap, inf); + }) +} + +fn add_to_vnmap( + pos_id: usize, + face_normal: FaceNormal, + vnmap: &mut HashMap>>, + inf: f64, +) { + match vnmap.get_mut(&pos_id) { + Some(vecs) => { + for vec in vecs.iter_mut() { + let normal = vec + .iter() + .fold(Vector3::zero(), |sum, x| sum + x.normal) + .normalize(); + if face_normal.normal.dot(normal) > inf { + vec.push(face_normal); + return; + } + } + vecs.push(vec![face_normal]); + } + None => { + let vecs = vec![vec![face_normal]]; + vnmap.insert(pos_id, vecs); + } + } +} + +fn signup_vertex_normal( + pos_id: usize, + face_id: usize, + normals: &mut Vec, + normal: Vector3, + faces: &mut Faces, + overwrite: bool, +) { + let face = faces[face_id].as_mut(); + let j = (0..face.len()).find(|j| face[*j].pos == pos_id).unwrap(); + if face[j].nor.is_none() || overwrite { + if let Some(n) = normals.last() { + if n != &normal { + normals.push(normal); + } + } else { + normals.push(normal); + } + face[j].nor = Some(normals.len() - 1); + } +} diff --git a/vendor/truck-meshalgo/src/filters/optimizing.rs b/vendor/truck-meshalgo/src/filters/optimizing.rs new file mode 100644 index 00000000..48a583b1 --- /dev/null +++ b/vendor/truck-meshalgo/src/filters/optimizing.rs @@ -0,0 +1,441 @@ +use super::*; +use rustc_hash::FxHashMap as HashMap; +use std::iter::Iterator; +use std::ops::{Div, Mul}; + +/// Filters for optimizing data +pub trait OptimizingFilter { + /// remove all unused position, texture coordinates, and normal vectors. + /// # Examples + /// ``` + /// use truck_polymesh::*; + /// use truck_meshalgo::filters::*; + /// let mut mesh = PolygonMesh::new( + /// StandardAttributes { + /// positions: vec![ + /// Point3::new(0.0, 0.0, 0.0), + /// Point3::new(1.0, 0.0, 0.0), + /// Point3::new(0.0, 1.0, 0.0), + /// Point3::new(0.0, 0.0, 1.0), + /// ], + /// ..Default::default() + /// }, + /// // 0 is not used! + /// Faces::from_iter(&[&[1, 2, 3]]), + /// ); + /// + /// assert_eq!(mesh.positions().len(), 4); + /// mesh.remove_unused_attrs(); + /// assert_eq!(mesh.positions().len(), 3); + /// ``` + fn remove_unused_attrs(&mut self) -> &mut Self; + /// Removes degenerate polygons. + /// # Examples + /// ``` + /// use truck_polymesh::*; + /// use truck_meshalgo::filters::*; + /// let mut mesh = PolygonMesh::new( + /// StandardAttributes { + /// positions: vec![ + /// Point3::new(0.0, 0.0, 0.0), + /// Point3::new(1.0, 0.0, 0.0), + /// Point3::new(0.0, 1.0, 0.0), + /// Point3::new(0.0, 0.0, 1.0), + /// ], + /// ..Default::default() + /// }, + /// Faces::from_iter(&[ + /// &[0, 1, 2], + /// &[2, 1, 2], // degenerate face! + /// &[2, 1, 3], + /// ]), + /// ); + /// + /// assert_eq!(mesh.faces().len(), 3); + /// mesh.remove_degenerate_faces(); + /// assert_eq!(mesh.faces().len(), 2); + /// ``` + fn remove_degenerate_faces(&mut self) -> &mut Self; + /// Gives the same indices to the same positions, texture coordinate, and normal vectors, respectively. + /// # Remarks + /// No longer needed attributes are NOT autoremoved. + /// One can remove such attributes by running [`remove_unused_attrs`] manually. + /// + /// [`remove_unused_attrs`]: ./trait.WasteEliminatingFilter.html#tymethod.remove_unused_attrs + /// + /// # Examples + /// ``` + /// use truck_meshalgo::prelude::*; + /// let mut mesh = PolygonMesh::new( + /// StandardAttributes { + /// positions: vec![ + /// Point3::new(0.0, 0.0, 0.0), + /// Point3::new(1.0, 0.0, 0.0), + /// Point3::new(0.0, 1.0, 0.0), + /// Point3::new(1.0, 1.0, 0.0), + /// Point3::new(0.0, 1.0, 0.0), + /// Point3::new(1.0, 0.0, 0.0), + /// ], + /// ..Default::default() + /// }, + /// Faces::from_iter(&[ + /// &[0, 1, 2], + /// &[3, 4, 5], + /// ]), + /// ); + /// + /// assert_eq!(mesh.faces()[1][1], StandardVertex { pos: 4, uv: None, nor: None }); + /// mesh.put_together_same_attrs(TOLERANCE); + /// assert_eq!(mesh.faces()[1][1], StandardVertex { pos: 2, uv: None, nor: None }); + /// + /// // Remarks: No longer needed attributes are NOT autoremoved! + /// assert_eq!(mesh.positions().len(), 6); + /// mesh.remove_unused_attrs(); + /// assert_eq!(mesh.positions().len(), 4); + /// ``` + fn put_together_same_attrs(&mut self, tol: f64) -> &mut Self; +} + +fn all_pos_mut(faces: &mut Faces) -> impl Iterator { + faces.face_iter_mut().flatten().map(move |v| &mut v.pos) +} + +fn all_uv_mut(faces: &mut Faces) -> impl Iterator { + faces + .face_iter_mut() + .flatten() + .filter_map(move |v| v.uv.as_mut()) +} + +fn all_nor_mut(faces: &mut Faces) -> impl Iterator { + faces + .face_iter_mut() + .flatten() + .filter_map(move |v| v.nor.as_mut()) +} + +impl OptimizingFilter for PolygonMesh { + fn remove_unused_attrs(&mut self) -> &mut Self { + let mut mesh = self.debug_editor(); + let PolygonMeshEditor { + attributes: + StandardAttributes { + positions, + uv_coords, + normals, + }, + faces, + .. + } = &mut mesh; + let pos_iter = all_pos_mut(faces); + let idcs = sub_remove_unused_attrs(pos_iter, positions.len()); + *positions = idcs.iter().map(|i| positions[*i]).collect(); + let uv_iter = all_uv_mut(faces); + let idcs = sub_remove_unused_attrs(uv_iter, uv_coords.len()); + *uv_coords = idcs.iter().map(|i| uv_coords[*i]).collect(); + let nor_iter = all_nor_mut(faces); + let idcs = sub_remove_unused_attrs(nor_iter, normals.len()); + *normals = idcs.iter().map(|i| normals[*i]).collect(); + drop(mesh); + self + } + + fn remove_degenerate_faces(&mut self) -> &mut Self { + let mesh = self.debug_editor(); + let mut faces = Faces::default(); + for tri in mesh.faces.tri_faces() { + if !degenerate_triangle(*tri) { + faces.push(tri); + } + } + for quad in mesh.faces.quad_faces() { + match degenerate_quadrangle(*quad) { + QuadrangleType::TotallyDegenerate => {} + QuadrangleType::Triangle(tri) => faces.push(tri), + QuadrangleType::NonDegenerate => faces.push(quad), + } + } + for face in mesh.faces.other_faces() { + faces.extend(split_into_nondegenerate(face.clone())); + } + *mesh.faces = faces; + drop(mesh); + self + } + + fn put_together_same_attrs(&mut self, tol: f64) -> &mut Self { + let mut mesh = self.debug_editor(); + let PolygonMeshEditor { + attributes: + StandardAttributes { + positions, + uv_coords, + normals, + }, + faces, + .. + } = &mut mesh; + let bnd_box: BoundingBox<_> = positions.iter().collect(); + let center = bnd_box.center(); + let diag = bnd_box.diagonal().map(|a| f64::max(a.abs(), 1.0)); + let normalized_positions = positions + .iter() + .map(move |position| 2.0 * (position - center).zip(diag, |a, b| a / b)) + .collect::>(); + let pos_map = sub_put_together_same_attrs(&normalized_positions, tol); + all_pos_mut(faces).for_each(|idx| *idx = pos_map[*idx]); + let uv_map = sub_put_together_same_attrs(uv_coords, tol); + all_uv_mut(faces).for_each(|idx| *idx = uv_map[*idx]); + let nor_map = sub_put_together_same_attrs(normals, tol); + all_nor_mut(faces).for_each(|idx| *idx = nor_map[*idx]); + drop(mesh); + self + } +} + +fn sub_remove_unused_attrs<'a, I: Iterator>( + iter: I, + old_len: usize, +) -> Vec { + let mut new2old = Vec::new(); + let mut old2new = vec![None; old_len]; + for idx in iter { + *idx = match old2new[*idx] { + Some(k) => k, + None => { + let k = new2old.len(); + new2old.push(*idx); + old2new[*idx] = Some(k); + k + } + }; + } + new2old +} + +trait SameAttr: Copy + CastIntVector + MetricSpace {} +impl SameAttr for T where T: Copy + CastIntVector + MetricSpace {} + +fn sub_put_together_same_attrs(attrs: &[T], tol: f64) -> Vec { + let map = create_blocks(attrs, tol); + let adj = create_adjacency(map, attrs, tol); + let components = create_components(adj, attrs); + centralize(components, attrs) +} + +type BlockMap<'a, T> = HashMap<::IntVector, Vec>; +fn create_blocks(attrs: &[T], tol: f64) -> BlockMap<'_, T> { + let mut map = HashMap::default(); + attrs.iter().enumerate().for_each(|(i, attr)| { + attr.round(tol).neighborhood().into_iter().for_each(|idx| { + map.entry(idx).or_insert_with(Vec::new).push(i); + }); + }); + map +} + +fn create_adjacency(map: BlockMap<'_, T>, attrs: &[T], tol: f64) -> Vec> { + let mut adj = vec![Vec::new(); attrs.len()]; + map.into_iter().for_each(|(_, vec)| { + if vec.len() > 1 { + vec.iter().copied().enumerate().for_each(|(k, i)| { + vec[(k + 1)..].iter().copied().for_each(|j| { + if attrs[i].distance2(attrs[j]) < tol * tol { + adj[i].push(j); + adj[j].push(i); + } + }); + }); + } + }); + adj +} + +fn create_components( + mut adj: Vec>, + attrs: &[T], +) -> impl Iterator> { + let mut already = vec![false; attrs.len()]; + (0..adj.len()).filter_map(move |i| { + if already[i] { + return None; + } + let mut stack = vec![i]; + let mut component = Vec::new(); + while let Some(i) = stack.pop() { + if !already[i] { + component.push(i); + already[i] = true; + stack.append(&mut adj[i]); + } + } + Some(component) + }) +} + +fn centralize( + components: impl Iterator>, + attrs: &[T], +) -> Vec { + let mut res = vec![0; attrs.len()]; + components.for_each(|component| { + let center = component + .iter() + .fold(T::zero(), |sum, j| sum.add_element_wise(attrs[*j])) + / component.len() as f64; + let (mut mindist, mut mindx) = (f64::INFINITY, 0); + component.iter().copied().for_each(|i| { + let dist = attrs[i].distance2(center); + if dist < mindist { + mindist = dist; + mindx = i; + } + }); + component.into_iter().for_each(|i| { + res[i] = mindx; + }); + }); + res +} + +fn degenerate_triangle(tri: [Vertex; 3]) -> bool { + tri[0].pos == tri[1].pos || tri[1].pos == tri[2].pos || tri[2].pos == tri[0].pos +} + +enum QuadrangleType { + NonDegenerate, + Triangle([Vertex; 3]), + TotallyDegenerate, +} + +fn degenerate_quadrangle(quad: [Vertex; 4]) -> QuadrangleType { + if (quad[0].pos == quad[2].pos || quad[1].pos == quad[3].pos) + || (quad[0].pos == quad[1].pos && quad[2].pos == quad[3].pos) + || quad[1].pos == quad[2].pos && quad[3].pos == quad[0].pos + { + QuadrangleType::TotallyDegenerate + } else if quad[0].pos == quad[1].pos || quad[1].pos == quad[2].pos { + QuadrangleType::Triangle([quad[0], quad[2], quad[3]]) + } else if quad[2].pos == quad[3].pos || quad[3].pos == quad[0].pos { + QuadrangleType::Triangle([quad[0], quad[1], quad[2]]) + } else { + QuadrangleType::NonDegenerate + } +} + +fn split_into_nondegenerate(poly: Vec) -> Vec> { + for i in 0..poly.len() { + for j in (i + 1)..poly.len() { + if poly[i].pos == poly[j].pos { + let polygon0: Vec<_> = (0..(j - i)).map(|k| poly[k + i]).collect(); + let polygon1: Vec<_> = ((j - i)..poly.len()) + .map(|k| poly[(k + i) % poly.len()]) + .collect(); + let mut result = split_into_nondegenerate(polygon0); + result.extend(split_into_nondegenerate(polygon1)); + return result; + } + } + } + vec![poly] +} + +trait CastIntVector: + Sized + ElementWise + Mul + Div { + type IntVector: IntVector; + fn cast_int(self) -> Self::IntVector; + fn round(self, tol: f64) -> Self::IntVector; + fn zero() -> Self; +} + +macro_rules! impl_cast_int { + ($typename: ident, $n: expr) => { + impl CastIntVector for $typename { + type IntVector = [i64; $n]; + fn cast_int(self) -> [i64; $n] { + self.cast::() + .unwrap_or_else(|| panic!("failed to cast: {self:?}")) + .into() + } + fn round(self, tol: f64) -> Self::IntVector { (self / tol).cast_int() } + fn zero() -> Self { Self::from([0.0; $n]) } + } + }; +} +impl_cast_int!(Vector2, 2); +impl_cast_int!(Vector3, 3); +impl_cast_int!(Point3, 3); + +trait IntVector: Copy + std::hash::Hash + Eq { + type Neighborhood: IntoIterator; + fn neighborhood(self) -> Self::Neighborhood; +} + +impl IntVector for [i64; 2] { + type Neighborhood = [[i64; 2]; 9]; + fn neighborhood(self) -> Self::Neighborhood { + let [a, b] = self; + [ + [a - 1, b - 1], + [a, b - 1], + [a + 1, b - 1], + [a - 1, b], + [a, b], + [a + 1, b], + [a - 1, b + 1], + [a, b + 1], + [a + 1, b + 1], + ] + } +} + +impl IntVector for [i64; 3] { + type Neighborhood = [[i64; 3]; 27]; + fn neighborhood(self) -> Self::Neighborhood { + let [a, b, c] = self; + [ + [a - 1, b - 1, c - 1], + [a, b - 1, c - 1], + [a + 1, b - 1, c - 1], + [a - 1, b, c - 1], + [a, b, c - 1], + [a + 1, b, c - 1], + [a - 1, b + 1, c - 1], + [a, b + 1, c - 1], + [a + 1, b + 1, c - 1], + [a - 1, b - 1, c], + [a, b - 1, c], + [a + 1, b - 1, c], + [a - 1, b, c], + [a, b, c], + [a + 1, b, c], + [a - 1, b + 1, c], + [a, b + 1, c], + [a + 1, b + 1, c], + [a - 1, b - 1, c + 1], + [a, b - 1, c + 1], + [a + 1, b - 1, c + 1], + [a - 1, b, c + 1], + [a, b, c + 1], + [a + 1, b, c + 1], + [a - 1, b + 1, c + 1], + [a, b + 1, c + 1], + [a + 1, b + 1, c + 1], + ] + } +} +#[cfg(test)] +mod tests { + use super::*; + + fn into_vertices(iter: &[usize]) -> Vec { iter.iter().map(|i| i.into()).collect() } + + #[test] + fn degenerate_polygon_test() { + let poly = into_vertices(&[0, 1, 2, 0, 3, 4, 5, 6, 3, 7, 8, 9]); + let polys = split_into_nondegenerate(poly); + assert_eq!(polys[0], into_vertices(&[0, 1, 2])); + assert_eq!(polys[1], into_vertices(&[3, 4, 5, 6])); + assert_eq!(polys[2], into_vertices(&[3, 7, 8, 9, 0])); + } +} diff --git a/vendor/truck-meshalgo/src/filters/structuring.rs b/vendor/truck-meshalgo/src/filters/structuring.rs new file mode 100644 index 00000000..941bf053 --- /dev/null +++ b/vendor/truck-meshalgo/src/filters/structuring.rs @@ -0,0 +1,242 @@ +use super::*; + +/// triangulation, quadrangulation, give a structure +pub trait StructuringFilter { + /// triangulate all n-gons + /// # Examples + /// ``` + /// use truck_polymesh::*; + /// use truck_meshalgo::filters::*; + /// + /// // cube consisting quad faces + /// let mut mesh = PolygonMesh::new( + /// StandardAttributes { + /// positions: vec![ + /// Point3::new(0.0, 0.0, 0.0), + /// Point3::new(1.0, 0.0, 0.0), + /// Point3::new(1.0, 1.0, 0.0), + /// Point3::new(0.0, 1.0, 0.0), + /// Point3::new(0.0, 0.0, 1.0), + /// Point3::new(1.0, 0.0, 1.0), + /// Point3::new(1.0, 1.0, 1.0), + /// Point3::new(0.0, 1.0, 1.0), + /// ], + /// ..Default::default() + /// }, + /// Faces::from_iter(&[ + /// &[3, 2, 1, 0], &[0, 1, 5, 4], &[1, 2, 6, 5], + /// &[2, 3, 7, 6], &[3, 0, 4, 7], &[4, 5, 6, 7], + /// ]), + /// ); + /// + /// // the number of face becomes twice since each quadrangle decompose into two triangles. + /// assert_eq!(mesh.faces().len(), 6); + /// mesh.triangulate(); + /// assert_eq!(mesh.faces().len(), 12); + /// ``` + fn triangulate(&mut self) -> &mut Self; + /// join two triangles into one quadrangle. + /// # Arguments + /// * `plane_tol` - the tolerance for determining that four points are in the same plane + /// * `score_tol` - The upper limit of the score to determine if the four points form an uncrushed rectangle + /// # Details + /// The overview of the algorithm is the following: + /// 1. make the list of pairs of triangles satisfying the following conditions: + /// * two faces are adjacent by one edge, + /// * the pair of faces consists four vertices in the same plane + /// 1. for each joined quadrangle, calculate the score by the sum of the absolute value of + /// the cosine for each angles, + /// 1. sort the list of the pairs of triangles by the score + /// 1. take a pair of triangles in order from the top of the list and register a new one + /// if it doesn't conflict with the one has been already registered. + /// # Panics + /// `plane_tol` and `score_tol` must be more than `TOLERANCE`. + /// # Examples + /// ``` + /// use truck_polymesh::*; + /// use truck_meshalgo::filters::*; + /// + /// // cube consisting tri_faces + /// let mut mesh = PolygonMesh::new( + /// StandardAttributes { + /// positions: vec![ + /// Point3::new(0.0, 0.0, 0.0), + /// Point3::new(1.0, 0.0, 0.0), + /// Point3::new(1.0, 1.0, 0.0), + /// Point3::new(0.0, 1.0, 0.0), + /// Point3::new(0.0, 0.0, 1.0), + /// Point3::new(1.0, 0.0, 1.0), + /// Point3::new(1.0, 1.0, 1.0), + /// Point3::new(0.0, 1.0, 1.0), + /// ], + /// ..Default::default() + /// }, + /// Faces::from_iter(&[ + /// &[3, 2, 0], &[1, 0, 2], &[0, 1, 4], &[5, 4, 1], + /// &[1, 2, 5], &[6, 5, 2], &[2, 3, 6], &[7, 6, 3], + /// &[3, 0, 7], &[4, 7, 0], &[4, 5, 7], &[6, 7, 5], + /// ]), + /// ); + /// + /// // The number of faces becomes a half since each pair of triangles is combined. + /// assert_eq!(mesh.faces().len(), 12); + /// mesh.quadrangulate(0.01, 0.1); + /// assert_eq!(mesh.faces().len(), 6); + /// ``` + fn quadrangulate(&mut self, plane_tol: f64, score_tol: f64) -> &mut Self; +} + +impl StructuringFilter for PolygonMesh { + fn triangulate(&mut self) -> &mut Self { + let tri_faces = self.faces().triangle_iter().collect::>(); + *self.debug_editor().faces = Faces::from_tri_and_quad_faces(tri_faces, Vec::new()); + self + } + fn quadrangulate(&mut self, plane_tol: f64, score_tol: f64) -> &mut Self { + nonpositive_tolerance!(plane_tol, 0.0); + nonpositive_tolerance!(score_tol, 0.0); + let list = self.create_face_edge_list(plane_tol, score_tol); + self.reflect_face_edge_list(list); + self + } +} + +trait SubStructureFilter { + fn create_face_edge_list(&self, plane_tol: f64, score_tol: f64) -> Vec; + fn reflect_face_edge_list(&mut self, list: Vec); + fn get_face_edge( + &self, + face0_id: usize, + face1_id: usize, + plane_tol: f64, + score_tol: f64, + ) -> Option; +} + +impl SubStructureFilter for PolygonMesh { + fn create_face_edge_list(&self, plane_tol: f64, score_tol: f64) -> Vec { + let face_adjacency = self.faces().face_adjacency(true); + let mut passed = Vec::new(); + let tri_len = self.faces().tri_faces().len(); + for (i, face) in face_adjacency.iter().enumerate().take(tri_len) { + for j in face { + if i > *j { + continue; + } else if let Some(face_edge) = self.get_face_edge(i, *j, plane_tol, score_tol) { + passed.push(face_edge); + } + } + } + passed.sort_by(|x, y| x.score.partial_cmp(&y.score).unwrap()); + passed + } + + fn reflect_face_edge_list(&mut self, list: Vec) { + let mut used = vec![false; self.faces().tri_faces().len()]; + let mut quad_faces = self.faces().quad_faces().clone(); + quad_faces.extend(list.into_iter().filter_map(|face_edge| { + let (i, j) = face_edge.faces; + if used[i] || used[j] { + None + } else { + used[i] = true; + used[j] = true; + Some(face_edge.positions) + } + })); + let tri_faces = self.faces().tri_faces(); + let tri_faces = used + .into_iter() + .enumerate() + .filter_map(move |(i, flag)| match flag { + true => None, + false => Some(tri_faces[i]), + }) + .collect::>(); + *self.debug_editor().faces = Faces::from_tri_and_quad_faces(tri_faces, quad_faces); + } + fn get_face_edge( + &self, + face0_id: usize, + face1_id: usize, + plane_tol: f64, + score_tol: f64, + ) -> Option { + let face0 = self.faces().tri_faces()[face0_id]; + let face1 = self.faces().tri_faces()[face1_id]; + + let k = (0..3) + .find(|k| face0.iter().all(|x| x.pos != face1[*k].pos)) + .unwrap(); + let vec0 = self.positions()[face0[1].pos] - self.positions()[face0[0].pos]; + let vec1 = self.positions()[face0[2].pos] - self.positions()[face0[0].pos]; + let mut n = vec0.cross(vec1); + n /= n.magnitude(); + let vec2 = self.positions()[face1[k].pos] - self.positions()[face0[0].pos]; + let mat = Matrix3::from_cols(vec0, vec1, n); + let coef = mat.invert().unwrap() * vec2; + + if coef[2] > plane_tol { + None + } else if coef[0] > 0.0 && coef[1] > 0.0 { + let score = calc_score(vec0, vec2 - vec0, vec1 - vec2, vec1); + if score < score_tol { + Some(FaceEdge { + faces: (face0_id, face1_id), + positions: [face0[0], face0[1], face1[k], face0[2]], + score, + }) + } else { + None + } + } else if coef[0] < 0.0 && coef[1] > 0.0 && coef[0] + coef[1] < 1.0 { + let score = calc_score(vec0, vec1 - vec0, vec2 - vec1, vec2); + if score < score_tol { + Some(FaceEdge { + faces: (face0_id, face1_id), + positions: [face0[0], face0[1], face0[2], face1[k]], + score, + }) + } else { + None + } + } else if coef[0] > 0.0 && coef[1] < 0.0 && coef[0] + coef[1] < 1.0 { + let score = calc_score(vec2, vec0 - vec2, vec1 - vec0, vec1); + if score < score_tol { + Some(FaceEdge { + faces: (face0_id, face1_id), + positions: [face0[0], face1[k], face0[1], face0[2]], + score, + }) + } else { + None + } + } else { + None + } + } +} + +struct FaceEdge { + faces: (usize, usize), + positions: [Vertex; 4], + score: f64, +} + +#[inline(always)] +fn calc_score(edge0: Vector3, edge1: Vector3, edge2: Vector3, edge3: Vector3) -> f64 { + edge0.cos_angle(edge1).abs() + + edge1.cos_angle(edge2).abs() + + edge2.cos_angle(edge3).abs() + + edge3.cos_angle(edge0).abs() +} + +trait CosAngle { + fn cos_angle(self, other: Self) -> f64; +} + +impl CosAngle for Vector3 { + fn cos_angle(self, other: Self) -> f64 { + self.dot(other) / (self.magnitude() * other.magnitude()) + } +} diff --git a/vendor/truck-meshalgo/src/filters/subdivision.rs b/vendor/truck-meshalgo/src/filters/subdivision.rs new file mode 100644 index 00000000..64ee8513 --- /dev/null +++ b/vendor/truck-meshalgo/src/filters/subdivision.rs @@ -0,0 +1,199 @@ +use super::*; +use rustc_hash::FxHashMap as HashMap; +use std::f64::consts::PI; + +/// subdivision surface algorithms +pub trait Subdivision { + /// Extended Loop method + /// + /// # Remarks + /// Confirm: + /// - All faces are triangles. + /// - `self.shell_condition()` is `Oriented` or `Closed` before use. + /// + /// This method does NOT check these conditions. + fn loop_subdivision(&mut self) -> &mut Self; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct Edge(usize, usize); +#[derive(Clone, Copy, Debug)] +struct EdgeInfo { + idx: usize, + first_wing: usize, + second_wing: Option, +} + +impl Edge { + #[inline] + fn new(v0: usize, v1: usize) -> Self { Edge(usize::min(v0, v1), usize::max(v0, v1)) } +} + +impl EdgeInfo { + #[inline] + fn new(idx: usize, first_wing: usize) -> Self { + Self { + idx, + first_wing, + second_wing: None, + } + } + #[inline] + fn add_second_wing(&mut self, second_wing: usize) { self.second_wing = Some(second_wing); } +} + +impl Subdivision for PolygonMesh { + fn loop_subdivision(&mut self) -> &mut Self { + let mut edges = HashMap::default(); + let mut vertex_adjacency = vec![Vec::new(); self.positions().len()]; + self.tri_faces().iter().for_each(|face| { + let face = [face[0].pos, face[1].pos, face[2].pos]; + add_vertex_edge(&mut edges, &mut vertex_adjacency, face[1], face[2], face[0]); + add_vertex_edge(&mut edges, &mut vertex_adjacency, face[2], face[0], face[1]); + add_vertex_edge(&mut edges, &mut vertex_adjacency, face[0], face[1], face[2]); + }); + let positions = vertex_adjacency + .iter() + .enumerate() + .map(|t| calc_new_position(t.0, t.1, &edges, self.positions())) + .chain(edge_positions(&edges, &vertex_adjacency, self.positions())) + .collect::>(); + let tri_faces = self + .tri_faces() + .iter() + .flat_map(|v| { + let len = self.positions().len(); + let e: [StandardVertex; 3] = [ + (edges.get(&Edge::new(v[1].pos, v[2].pos)).unwrap().idx + len).into(), + (edges.get(&Edge::new(v[2].pos, v[0].pos)).unwrap().idx + len).into(), + (edges.get(&Edge::new(v[0].pos, v[1].pos)).unwrap().idx + len).into(), + ]; + vec![ + [v[0], e[2], e[1]], + [e[0], v[2], e[1]], + [e[0], e[2], v[1]], + [e[0], e[1], e[2]], + ] + }) + .collect::>(); + { + let editor = self.debug_editor(); + editor.attributes.positions = positions; + *editor.faces = Faces::from_tri_and_quad_faces(tri_faces, Vec::new()); + } + self + } +} + +fn add_vertex_edge( + edges: &mut HashMap, + vertex_adjacency: &mut [Vec], + v0: usize, + v1: usize, + v2: usize, +) { + let idx = edges.len(); + edges + .entry(Edge::new(v0, v1)) + .and_modify(|tuple| tuple.add_second_wing(v2)) + .or_insert_with(|| { + vertex_adjacency[v0].push(v1); + vertex_adjacency[v1].push(v0); + EdgeInfo::new(idx, v2) + }); +} + +#[derive(Clone, Copy, Debug)] +enum VertexBoundaryCondition { + #[allow(dead_code)] + Corner(usize, usize), + Boundary(usize, usize), + Inner, +} + +impl VertexBoundaryCondition { + fn new(v: usize, adjacency: &[usize], edges: &HashMap) -> Self { + let binfo = adjacency.iter().copied().fold((None, None), |binfo, w| { + let edge = edges.get(&Edge::new(v, w)).unwrap(); + match (edge.second_wing, binfo) { + (None, (None, _)) => (Some(w), None), + (None, (Some(x), None)) => (Some(x), Some(w)), + _ => binfo, + } + }); + if let (Some(x), Some(y)) = binfo { + if adjacency.len() == 2 { + Self::Corner(x, y) + } else { + Self::Boundary(x, y) + } + } else { + Self::Inner + } + } +} + +fn calc_new_position( + v: usize, + adjacency: &[usize], + edges: &HashMap, + positions: &[Point3], +) -> Point3 { + use VertexBoundaryCondition::*; + let point = positions[v]; + match VertexBoundaryCondition::new(v, adjacency, edges) { + Corner(_, _) => point, + Boundary(w0, w1) => point + ((positions[w0] - point) + (positions[w1] - point)) / 8.0, + Inner => { + let alpha = 3.0 / 8.0 + f64::cos(2.0 * PI / adjacency.len() as f64) / 4.0; + let alpha = (5.0 / 8.0 - alpha * alpha) / adjacency.len() as f64; + point + + adjacency + .iter() + .copied() + .map(|w| positions[w] - point) + .sum::() + * alpha + } + } +} + +fn edge_positions( + edges: &HashMap, + vertex_adjacency: &[Vec], + positions: &[Point3], +) -> Vec { + use VertexBoundaryCondition::*; + let mut res = vec![Point3::origin(); edges.len()]; + edges.iter().for_each(|tuple| { + let Edge(v0, v1) = *tuple.0; + let EdgeInfo { + idx, + first_wing, + second_wing, + } = *tuple.1; + res[idx] = match second_wing { + Some(second_wing) => { + let gamma = match VertexBoundaryCondition::new(v0, &vertex_adjacency[v0], edges) { + Boundary(_, _) => { + 5.0 / 8.0 - f64::cos(PI / vertex_adjacency[v0].len() as f64) / 4.0 + } + Inner => match VertexBoundaryCondition::new(v1, &vertex_adjacency[v1], edges) { + Boundary(_, _) => { + 3.0 / 8.0 + f64::cos(PI / vertex_adjacency[v1].len() as f64) / 4.0 + } + Inner => 0.5, + Corner(_, _) => unreachable!(), + }, + Corner(_, _) => unreachable!(), + }; + positions[v0] + + (positions[v1] - positions[v0]) * gamma + + (positions[first_wing] - positions[v0]) / 8.0 + + (positions[second_wing] - positions[v1]) / 8.0 + } + None => positions[v0].midpoint(positions[v1]), + }; + }); + res +} diff --git a/vendor/truck-meshalgo/src/lib.rs b/vendor/truck-meshalgo/src/lib.rs new file mode 100644 index 00000000..79725617 --- /dev/null +++ b/vendor/truck-meshalgo/src/lib.rs @@ -0,0 +1,56 @@ +//! Mesh algorigthms, include tessellations of the shape. + +// SolveCraft: compiler warnings no longer fail the build. +#![deny(clippy::all, rust_2018_idioms)] +#![warn( + missing_docs, + missing_debug_implementations, + trivial_casts, + trivial_numeric_casts, + unsafe_code, + unstable_features, + unused_import_braces, + unused_qualifications +)] + +use common::*; + +/// re-export polymesh +pub mod rexport_polymesh { + pub use truck_polymesh::*; +} +use truck_polymesh::{StandardVertex as Vertex, *}; + +/// polygon mesh analizers, including +/// +/// - determines topological properties: connectivity, boundary extraction, or shell conditions (closed or oriented) +/// - detects collisions between two meshes and extracts interference lines +/// - investigates positional relations between mesh and point clouds. +#[cfg(feature = "analyzers")] +pub mod analyzers; +mod common; +/// Edits meshes. Add normals, optimizing data, and so on. +#[cfg(feature = "filters")] +pub mod filters; +/// Tessellates shapes. +#[cfg(feature = "tessellation")] +pub mod tessellation; + +/// VTK Output +#[cfg(feature = "vtk")] +#[cfg(not(target_arch = "wasm32"))] +pub mod vtk; + +/// This module contains all traits and re-exports `truck_polymesh`. +pub mod prelude { + #[cfg(feature = "analyzers")] + pub use crate::analyzers::*; + #[cfg(feature = "filters")] + pub use crate::filters::*; + pub use crate::rexport_polymesh::*; + #[cfg(feature = "tessellation")] + pub use crate::tessellation::*; + #[cfg(feature = "vtk")] + #[cfg(not(target_arch = "wasm32"))] + pub use crate::vtk::*; +} diff --git a/vendor/truck-meshalgo/src/tessellation/mod.rs b/vendor/truck-meshalgo/src/tessellation/mod.rs new file mode 100644 index 00000000..2e70d1a0 --- /dev/null +++ b/vendor/truck-meshalgo/src/tessellation/mod.rs @@ -0,0 +1,360 @@ +use crate::*; +use spade::{iterators::*, *}; +use truck_topology::{compress::*, *}; + +#[cfg(not(target_arch = "wasm32"))] +mod parallelizable { + /// Parallelizable by `rayon`. + pub trait Parallelizable: Send + Sync {} + impl Parallelizable for T {} +} + +#[cfg(target_arch = "wasm32")] +mod parallelizable { + /// No parallelization in the case of wasm. + pub trait Parallelizable {} + impl Parallelizable for T {} +} + +pub use parallelizable::*; + +/// Gathered the traits used in tessellation. +pub trait PolylineableCurve: + ParametricCurve3D + BoundedCurve + ParameterDivision1D + Parallelizable { +} +impl< + C: ParametricCurve3D + BoundedCurve + ParameterDivision1D + Parallelizable, + > PolylineableCurve for C +{ +} + +/// It can be meshed, but not necessarily trimmed. +pub trait PreMeshableSurface: ParametricSurface3D + ParameterDivision2D + Parallelizable {} +impl PreMeshableSurface for S {} + +/// The generated mesh can be trimmed only if the boundary curves ride strictly on a surface. +pub trait MeshableSurface: PreMeshableSurface + SearchParameter {} +impl> MeshableSurface for S {} + +/// The generated mesh can be trimmed if the boundary curves does not ride strictly on a surface. +pub trait RobustMeshableSurface: + MeshableSurface + SearchNearestParameter { +} +impl> RobustMeshableSurface for S {} + +type PolylineCurve = truck_polymesh::PolylineCurve; + +/// Trait for converting tessellated shape into polygon. +pub trait MeshedShape { + /// Converts tessellated shape into polygon. + fn to_polygon(&self) -> PolygonMesh; +} + +impl MeshedShape for Shell { + fn to_polygon(&self) -> PolygonMesh { + let mut polygon = PolygonMesh::default(); + self.face_iter().for_each(|face| { + polygon.merge(face.oriented_surface()); + }); + polygon + } +} + +impl MeshedShape for Shell> { + fn to_polygon(&self) -> PolygonMesh { + let mut polygon = PolygonMesh::default(); + self.face_iter().for_each(|face| { + if let Some(mut poly) = face.surface() { + if !face.orientation() { + poly.invert(); + } + polygon.merge(poly); + } + }); + polygon + } +} + +impl MeshedShape for Solid +where Shell: MeshedShape +{ + fn to_polygon(&self) -> PolygonMesh { + let mut polygon = PolygonMesh::default(); + self.boundaries().iter().for_each(|shell| { + polygon.merge(shell.to_polygon()); + }); + polygon + } +} + +impl MeshedShape for CompressedShell { + fn to_polygon(&self) -> PolygonMesh { + let mut polygon = PolygonMesh::default(); + self.faces.iter().for_each(|face| match face.orientation { + true => polygon.merge(face.surface.clone()), + false => polygon.merge(face.surface.inverse()), + }); + polygon + } +} + +impl MeshedShape for CompressedShell> { + fn to_polygon(&self) -> PolygonMesh { + let mut polygon = PolygonMesh::default(); + self.faces.iter().for_each(|face| { + if let Some(surface) = &face.surface { + match face.orientation { + true => polygon.merge(surface.clone()), + false => polygon.merge(surface.inverse()), + } + } + }); + polygon + } +} + +impl MeshedShape for CompressedSolid +where CompressedShell: MeshedShape +{ + fn to_polygon(&self) -> PolygonMesh { + let mut polygon = PolygonMesh::default(); + self.boundaries.iter().for_each(|shell| { + polygon.merge(shell.to_polygon()); + }); + polygon + } +} + +/// Trait for tessellating `Shell` and `Solid`. +pub trait MeshableShape { + /// Shape whose edges are made polylines and faces polygon surface. + type MeshedShape: MeshedShape; + /// Tessellates shapes. The division of curves and surfaces are by `ParameterDivision1D` and `ParameterDivision2D`, + /// and the constrained Delauney triangulation is based on the crate [`spade`](https://crates.io/crates/spade). + /// + /// # Panics + /// + /// `tol` must be more than `TOLERANCE`. + /// + /// # Remarks + /// + /// - The tessellated mesh is not necessarily closed even if `self` is `Solid`. + /// If you want to get closed mesh, use [`OptimizingFilter::put_together_same_attrs`]. + /// - This method requires that the curve ride strictly on a surface. If not, try [`RobustMeshableShape`]. + /// + /// [`OptimizingFilter::put_together_same_attrs`]: crate::filters::OptimizingFilter::put_together_same_attrs + /// + /// # Examples + /// ``` + /// use truck_meshalgo::prelude::*; + /// use truck_modeling::builder; + /// use truck_topology::shell::ShellCondition; + /// + /// // modeling a unit cube + /// let v = builder::vertex(Point3::origin()); + /// let e = builder::tsweep(&v, Vector3::unit_x()); + /// let f = builder::tsweep(&e, Vector3::unit_y()); + /// let cube = builder::tsweep(&f, Vector3::unit_z()); + /// + /// // cube is Solid, however, the tessellated mesh is not closed. + /// let mut mesh = cube.triangulation(0.01).to_polygon(); + /// assert_ne!(mesh.shell_condition(), ShellCondition::Closed); + /// + /// // use optimization filters! + /// mesh.put_together_same_attrs(TOLERANCE); + /// assert_eq!(mesh.shell_condition(), ShellCondition::Closed); + /// ``` + fn triangulation(&self, tol: f64) -> Self::MeshedShape; +} + +/// Trait for tessellating `Shell` and `Solid` in `truck-modeling`. +pub trait RobustMeshableShape { + /// Shape whose edges are made polylines and faces polygon surface. + type MeshedShape: MeshedShape; + /// Tessellates shapes. The division of curves and surfaces are by `ParameterDivision1D` and `ParameterDivision2D`, + /// and the constrained Delauney triangulation is based on the crate [`spade`](https://crates.io/crates/spade). + /// + /// # Panics + /// + /// `tol` must be more than `TOLERANCE`. + /// + /// # Remarks + /// + /// Since polyline vertices are projected onto the surface, processing speed is often slower than with [`MeshableShape::triangulation`]. + /// It also does not close the mesh of a solid even if one uses [`OptimizingFilter::put_together_same_attrs`]. + /// + /// [`OptimizingFilter::put_together_same_attrs`]: crate::filters::OptimizingFilter::put_together_same_attrs + /// + /// # Examples + /// ``` + /// use truck_meshalgo::prelude::*; + /// use truck_modeling::*; + /// use truck_topology::shell::ShellCondition; + /// + /// // manual modeling an open half cylinder + /// + /// // points + /// let p = [ + /// Point3::new(1.0, 0.0, 0.0), + /// Point3::new(-1.0, 0.0, 0.0), + /// Point3::new(1.0, 0.0, 1.0), + /// Point3::new(-1.0, 0.0, 1.0), + /// ]; + /// // vertices + /// let v = Vertex::news(&p); + /// // Curves that do not ride on a cylinder + /// let bsp0: Curve = BSplineCurve::new( + /// KnotVec::bezier_knot(3), + /// vec![ + /// p[0], + /// Point3::new(1.0, 4.0 / 3.0, 0.0), + /// Point3::new(-1.0, 4.0 / 3.0, 0.0), + /// p[1], + /// ], + /// ) + /// .into(); + /// let bsp1: Curve = BSplineCurve::new( + /// KnotVec::bezier_knot(3), + /// vec![ + /// p[3], + /// Point3::new(-1.0, 4.0 / 3.0, 1.0), + /// Point3::new(1.0, 4.0 / 3.0, 1.0), + /// p[2], + /// ], + /// ) + /// .into(); + /// // wire + /// let w: Wire = vec![ + /// builder::line(&v[2], &v[0]), + /// Edge::new(&v[0], &v[1], bsp0), + /// builder::line(&v[1], &v[3]), + /// Edge::new(&v[3], &v[2], bsp1), + /// ] + /// .into(); + /// // revoluted curve + /// let surface_raw = RevolutedCurve::by_revolution( + /// Curve::Line(Line(p[2], p[0])), + /// Point3::origin(), + /// Vector3::unit_z(), + /// ); + /// let surface: Surface = Processor::new(surface_raw).into(); + /// // shell + /// let shell: Shell = vec![Face::new(vec![w], surface)].into(); + /// + /// // Simple triangulation fails since some edges do not ride on a cylinder + /// let poly_shell = shell.triangulation(0.01); + /// assert!(poly_shell[0].surface().is_none()); + /// + /// // Robust triangulation! + /// let poly_shell = shell.robust_triangulation(0.01); + /// let poly = poly_shell[0].surface().unwrap(); + /// assert!(!poly.positions().is_empty()); + /// ``` + fn robust_triangulation(&self, tol: f64) -> Self::MeshedShape; +} + +impl MeshableShape for Shell { + type MeshedShape = Shell>; + fn triangulation(&self, tol: f64) -> Self::MeshedShape { + nonpositive_tolerance!(tol); + #[cfg(not(target_arch = "wasm32"))] + let res = triangulation::shell_tessellation(self, tol, triangulation::by_search_parameter); + #[cfg(target_arch = "wasm32")] + let res = triangulation::shell_tessellation_single_thread( + self, + tol, + triangulation::by_search_parameter, + ); + res + } +} + +impl RobustMeshableShape for Shell { + type MeshedShape = Shell>; + fn robust_triangulation(&self, tol: f64) -> Self::MeshedShape { + nonpositive_tolerance!(tol); + #[cfg(not(target_arch = "wasm32"))] + let res = triangulation::shell_tessellation( + self, + tol, + triangulation::by_search_nearest_parameter, + ); + #[cfg(target_arch = "wasm32")] + let res = triangulation::shell_tessellation_single_thread( + self, + tol, + triangulation::by_search_nearest_parameter, + ); + res + } +} + +impl MeshableShape for Solid { + type MeshedShape = Solid>; + fn triangulation(&self, tol: f64) -> Self::MeshedShape { + let boundaries = self + .boundaries() + .iter() + .map(|shell| shell.triangulation(tol)) + .collect::>(); + Solid::new(boundaries) + } +} + +impl RobustMeshableShape for Solid { + type MeshedShape = Solid>; + fn robust_triangulation(&self, tol: f64) -> Self::MeshedShape { + let boundaries = self + .boundaries() + .iter() + .map(|shell| shell.robust_triangulation(tol)) + .collect::>(); + Solid::new(boundaries) + } +} + +impl MeshableShape for CompressedShell { + type MeshedShape = CompressedShell>; + fn triangulation(&self, tol: f64) -> Self::MeshedShape { + nonpositive_tolerance!(tol); + triangulation::cshell_tessellation(self, tol, triangulation::by_search_parameter) + } +} + +impl RobustMeshableShape + for CompressedShell +{ + type MeshedShape = CompressedShell>; + fn robust_triangulation(&self, tol: f64) -> Self::MeshedShape { + nonpositive_tolerance!(tol); + triangulation::cshell_tessellation(self, tol, triangulation::by_search_nearest_parameter) + } +} + +impl MeshableShape for CompressedSolid { + type MeshedShape = CompressedSolid>; + fn triangulation(&self, tol: f64) -> Self::MeshedShape { + let boundaries = self + .boundaries + .iter() + .map(|shell| shell.triangulation(tol)) + .collect::>(); + CompressedSolid { boundaries } + } +} + +impl RobustMeshableShape + for CompressedSolid +{ + type MeshedShape = CompressedSolid>; + fn robust_triangulation(&self, tol: f64) -> Self::MeshedShape { + let boundaries = self + .boundaries + .iter() + .map(|shell| shell.robust_triangulation(tol)) + .collect::>(); + CompressedSolid { boundaries } + } +} + +mod triangulation; diff --git a/vendor/truck-meshalgo/src/tessellation/triangulation.rs b/vendor/truck-meshalgo/src/tessellation/triangulation.rs new file mode 100644 index 00000000..d483200f --- /dev/null +++ b/vendor/truck-meshalgo/src/tessellation/triangulation.rs @@ -0,0 +1,705 @@ +#![allow(clippy::many_single_char_names)] + +use super::*; +use crate::filters::NormalFilters; +use crate::Point2; +use array_macro::array; +use itertools::Itertools; +use rustc_hash::FxHashMap as HashMap; + +#[cfg(not(target_arch = "wasm32"))] +use rayon::prelude::*; + +type SPoint2 = spade::Point2; +type Cdt = ConstrainedDelaunayTriangulation; +type MeshedShell = Shell>; +type MeshedCShell = CompressedShell>; + +pub(super) trait SP: + Fn(&S, Point3, Option<(f64, f64)>) -> Option<(f64, f64)> + Parallelizable { +} +impl SP for F where F: Fn(&S, Point3, Option<(f64, f64)>) -> Option<(f64, f64)> + Parallelizable {} + +/// SolveCraft: a parameter inside the surface's bounded, non-periodic ranges. +fn in_domain(surface: &S, (u, v): (f64, f64)) -> bool { + let ok = |x: f64, range: Option<(f64, f64)>, period: Option| { + x.is_finite() + && match (range, period) { + (_, Some(_)) | (None, None) => true, + (Some((a, b)), None) => { + let m = (b - a).abs() * 1e-6 + 1e-9; + x >= a.min(b) - m && x <= a.max(b) + m + } + } + }; + let (ur, vr) = surface.try_range_tuple(); + ok(u, ur, surface.u_period()) && ok(v, vr, surface.v_period()) +} + +/// SolveCraft: the parameter clamped into the surface's bounded, non-periodic ranges. +fn clamped(surface: &S, (u, v): (f64, f64)) -> (f64, f64) { + let c = |x: f64, range: Option<(f64, f64)>, period: Option| match (range, period) { + (Some((a, b)), None) => x.clamp(a.min(b), a.max(b)), + _ => x, + }; + let (ur, vr) = surface.try_range_tuple(); + (c(u, ur, surface.u_period()), c(v, vr, surface.v_period())) +} + +/// SolveCraft: a nearest-parameter search can run off a B-spline's domain and extrapolate to +/// thousands of parameter units (meshing that grid never finishes). Outside the nominal domain +/// keep the result only if it is really closer to the point than the clamped one (planes report +/// a nominal unit range but extend for ever). +fn sane(surface: &S, point: Point3, uv: (f64, f64)) -> (f64, f64) { + if in_domain(surface, uv) { + return uv; + } + let c = clamped(surface, uv); + let d = |(u, v): (f64, f64)| surface.subs(u, v).distance2(point); + if d(uv) <= d(c) { uv } else { c } +} + +pub(super) fn by_search_parameter( + surface: &S, + point: Point3, + hint: Option<(f64, f64)>, +) -> Option<(f64, f64)> +where + S: MeshableSurface, +{ + surface + .search_parameter(point, hint, 100) + .or_else(|| surface.search_parameter(point, None, 100)) +} + +pub(super) fn by_search_nearest_parameter( + surface: &S, + point: Point3, + hint: Option<(f64, f64)>, +) -> Option<(f64, f64)> +where + S: RobustMeshableSurface, +{ + surface + .search_parameter(point, hint, 100) + .or_else(|| surface.search_parameter(point, None, 100)) + .or_else(|| surface.search_nearest_parameter(point, hint, 100).map(|uv| sane(surface, point, uv))) + .or_else(|| surface.search_nearest_parameter(point, None, 100).map(|uv| sane(surface, point, uv))) +} + +/// Tessellates faces +#[cfg(not(target_arch = "wasm32"))] +pub(super) fn shell_tessellation<'a, C, S>( + shell: &Shell, + tol: f64, + sp: impl SP, +) -> MeshedShell +where + C: PolylineableCurve + 'a, + S: PreMeshableSurface + 'a, +{ + let vmap: HashMap<_, _> = shell + .vertex_par_iter() + .map(|v| (v.id(), v.mapped(Point3::clone))) + .collect(); + let eset: HashMap<_, _> = shell.edge_par_iter().map(move |e| (e.id(), e)).collect(); + let edge_map: HashMap<_, _> = eset + .into_par_iter() + .map(move |(id, edge)| { + let v0 = vmap.get(&edge.absolute_front().id()).unwrap(); + let v1 = vmap.get(&edge.absolute_back().id()).unwrap(); + let curve = edge.curve(); + let poly = PolylineCurve::from_curve(&curve, curve.range_tuple(), tol); + (id, Edge::debug_new(v0, v1, poly)) + }) + .collect(); + let create_edge = |edge: &Edge| -> Edge<_, _> { + let new_edge = edge_map.get(&edge.id()).unwrap(); + match edge.orientation() { + true => new_edge.clone(), + false => new_edge.inverse(), + } + }; + let create_boundary = + |wire: &Wire| -> Wire<_, _> { wire.edge_iter().map(create_edge).collect() }; + let create_face = move |face: &Face| -> Face<_, _, _> { + let wires: Vec<_> = face + .absolute_boundaries() + .iter() + .map(create_boundary) + .collect(); + shell_create_polygon(&face.surface(), wires, face.orientation(), tol, &sp) + }; + shell.face_par_iter().map(create_face).collect() +} + +/// Tessellates faces +#[cfg(any(target_arch = "wasm32", test))] +pub(super) fn shell_tessellation_single_thread<'a, C, S>( + shell: &'a Shell, + tol: f64, + sp: impl SP, +) -> MeshedShell +where + C: PolylineableCurve + 'a, + S: PreMeshableSurface + 'a, +{ + use truck_base::entry_map::FxEntryMap as EntryMap; + use truck_topology::Vertex as TVertex; + let mut vmap = EntryMap::new( + move |v: &TVertex| v.id(), + move |v| v.mapped(Point3::clone), + ); + let mut edge_map = EntryMap::new( + move |edge: &'a Edge| edge.id(), + move |edge| { + let vf = edge.absolute_front(); + let v0 = vmap.entry_or_insert(vf).clone(); + let vb = edge.absolute_back(); + let v1 = vmap.entry_or_insert(vb).clone(); + let curve = edge.curve(); + let poly = PolylineCurve::from_curve(&curve, curve.range_tuple(), tol); + Edge::debug_new(&v0, &v1, poly) + }, + ); + let mut create_edge = move |edge: &'a Edge| -> Edge<_, _> { + let new_edge = edge_map.entry_or_insert(edge); + match edge.orientation() { + true => new_edge.clone(), + false => new_edge.inverse(), + } + }; + let mut create_boundary = move |wire: &'a Wire| -> Wire<_, _> { + wire.edge_iter().map(&mut create_edge).collect() + }; + let create_face = move |face: &'a Face| -> Face<_, _, _> { + let wires: Vec<_> = face + .absolute_boundaries() + .iter() + .map(&mut create_boundary) + .collect(); + shell_create_polygon(&face.surface(), wires, face.orientation(), tol, &sp) + }; + shell.face_iter().map(create_face).collect() +} + +/// Tessellates faces +pub(super) fn cshell_tessellation<'a, C, S>( + shell: &CompressedShell, + tol: f64, + sp: impl SP, +) -> MeshedCShell +where + C: PolylineableCurve + 'a, + S: PreMeshableSurface + 'a, +{ + let vertices = shell.vertices.clone(); + let tessellate_edge = |edge: &CompressedEdge| { + let curve = &edge.curve; + CompressedEdge { + vertices: edge.vertices, + curve: PolylineCurve::from_curve(curve, curve.range_tuple(), tol), + } + }; + #[cfg(not(target_arch = "wasm32"))] + let edges: Vec<_> = shell.edges.par_iter().map(tessellate_edge).collect(); + #[cfg(target_arch = "wasm32")] + let edges: Vec<_> = shell.edges.iter().map(tessellate_edge).collect(); + let tessellate_face = |face: &CompressedFace| { + let boundaries = face.boundaries.clone(); + let surface = &face.surface; + let create_edge = |edge_idx: &CompressedEdgeIndex| match edge_idx.orientation { + true => Some(edges.get(edge_idx.index)?.curve.clone()), + false => Some(edges.get(edge_idx.index)?.curve.inverse()), + }; + let create_boundary = |wire: &Vec| { + let wire_iter = wire.iter().filter_map(create_edge); + PolyBoundaryPiece::try_new(surface, wire_iter, &sp) + }; + let preboundary: Option> = boundaries.iter().map(create_boundary).collect(); + let polygon: Option = (|| { + let boundary = PolyBoundary::new(preboundary?, &surface, tol); + Some(trimming_tessellation(&surface, &boundary, tol)) + })(); + CompressedFace { + boundaries, + orientation: face.orientation, + surface: polygon, + } + }; + #[cfg(not(target_arch = "wasm32"))] + let faces = shell.faces.par_iter().map(tessellate_face).collect(); + #[cfg(target_arch = "wasm32")] + let faces = shell.faces.iter().map(tessellate_face).collect(); + MeshedCShell { + vertices, + edges, + faces, + } +} + +fn shell_create_polygon( + surface: &S, + wires: Vec>, + orientation: bool, + tol: f64, + sp: impl SP, +) -> Face> { + let preboundary = wires + .iter() + .map(|wire: &Wire<_, _>| { + let wire_iter = wire.iter().map(Edge::oriented_curve); + PolyBoundaryPiece::try_new(surface, wire_iter, &sp) + }) + .collect::>>(); + let polygon: Option = (|| { + let boundary = PolyBoundary::new(preboundary?, &surface, tol); + Some(trimming_tessellation(surface, &boundary, tol)) + })(); + let mut new_face = Face::debug_new(wires, polygon); + if !orientation { + new_face.invert(); + } + new_face +} + +#[derive(Debug, Default, Clone)] +struct PolyBoundaryPiece(Vec); + +impl PolyBoundaryPiece { + fn try_new( + surface: &S, + wire: impl Iterator, + sp: impl SP, + ) -> Option { + let (up, vp) = (surface.u_period(), surface.v_period()); + let (urange, vrange) = surface.try_range_tuple(); + let mut bdry3d: Vec = wire + .flat_map(|poly_edge| { + let n = poly_edge.len() - 1; + poly_edge.into_iter().take(n) + }) + .collect(); + bdry3d.push(bdry3d[0]); + let mut previous = None; + let mut vec = bdry3d + .into_iter() + .flat_map(|pt| { + let (mut u, mut v) = match sp(surface, pt, previous) { + Some(hint) => hint, + None => return vec![None], + }; + if let (Some(up), Some((u0, _))) = (up, previous) { + u = get_mindiff(u, u0, up); + } + if let (Some(vp), Some((_, v0))) = (vp, previous) { + v = get_mindiff(v, v0, vp); + } + let res = (|| { + if let Some((u0, v0)) = previous { + if !u0.near(&u) && surface.uder(u0, v0).so_small() { + return vec![Some(Point2::new(u, v0)), Some(Point2::new(u, v))]; + } else if !v0.near(&v) && surface.vder(u0, v0).so_small() { + return vec![Some(Point2::new(u0, v)), Some(Point2::new(u, v))]; + } + } + vec![Some(Point2::new(u, v))] + })(); + previous = Some((u, v)); + res + }) + .collect::>>()?; + let grav = vec.iter().fold(Point2::origin(), |g, p| g + p.to_vec()) / vec.len() as f64; + if let (Some(up), Some((u0, _))) = (up, urange) { + let quot = f64::floor((grav.x - u0) / up); + vec.iter_mut().for_each(|p| p.x -= quot * up); + } + if let (Some(vp), Some((v0, _))) = (vp, vrange) { + let quot = f64::floor((grav.y - v0) / vp); + vec.iter_mut().for_each(|p| p.y -= quot * vp); + } + let last = *vec.last().unwrap(); + if !vec[0].near(&last) { + let Point2 { x: u0, y: v0 } = last; + if surface.uder(u0, v0).so_small() || surface.vder(u0, v0).so_small() { + vec.push(vec[0]); + } + } + Some(Self(vec)) + } +} + +fn abs_diff(previous: f64) -> impl Fn(&f64, &f64) -> std::cmp::Ordering { + let f = move |x: &f64| f64::abs(x - previous); + move |x: &f64, y: &f64| f(x).partial_cmp(&f(y)).unwrap() +} +fn get_mindiff(u: f64, u0: f64, up: f64) -> f64 { + let closure = |i| u + i as f64 * up; + (-2..=2).map(closure).min_by(abs_diff(u0)).unwrap() +} + +#[derive(Debug, Default, Clone)] +struct PolyBoundary(Vec>); + +fn normalize_range(curve: &mut Vec, compidx: usize, (u0, u1): (f64, f64)) { + let p = curve[0]; + let q = curve[curve.len() - 1]; + let tmp = f64::min(p[compidx], q[compidx]) + TOLERANCE; + let del = f64::floor((tmp - u0) / (u1 - u0)) * (u1 - u0); + curve.iter_mut().for_each(|p| p[compidx] -= del); + let Some(i) = curve + .iter() + .position(|p| (curve[0][compidx] - u1) * (p[compidx] - u1) < 0.0) + else { + return; + }; + let mut curve1 = curve.split_off(i + 1); + curve1.pop(); + curve1.insert(0, curve[i]); + match curve[0][compidx] < curve[curve.len() - 1][compidx] { + true => curve1.iter_mut(), + false => curve.iter_mut(), + } + .for_each(|p| p[compidx] -= u1 - u0); + curve1.append(curve); + *curve = curve1; +} + +fn loop_orientation(curve: &[Point2]) -> bool { + curve + .iter() + .circular_tuple_windows() + .fold(0.0, |sum, (p, q)| sum + (q.x + p.x) * (q.y - p.y)) + > 0.0 +} + +impl PolyBoundary { + fn new(pieces: Vec, surface: &impl PreMeshableSurface, tol: f64) -> Self { + let (mut closed, mut open) = (Vec::new(), Vec::new()); + pieces.into_iter().for_each(|PolyBoundaryPiece(mut vec)| { + match vec[0].distance(vec[vec.len() - 1]) < 1.0e-3 { + true => { + vec.pop(); + closed.push(vec) + } + false => open.push(vec), + } + }); + fn connect_edges(vecs: impl IntoIterator>) -> Vec { + let closure = |vec: Vec| { + let len = vec.len(); + vec.into_iter().take(len - 1) + }; + vecs.into_iter().flat_map(closure).collect() + } + match open.len() { + 1 => { + let mut curve = open.pop().unwrap(); + let p = curve[0]; + let q = curve[curve.len() - 1]; + if let (Some((u0, u1)), Some((v0, v1))) = surface.try_range_tuple() { + if p.x < q.x - TOLERANCE { + normalize_range(&mut curve, 0, (u0, u1)); + let p = curve[0]; + let q = curve[curve.len() - 1]; + let x = Point2::new(u0, v1); + let y = Point2::new(u1, v1); + let vec0 = polyline_on_surface(surface, q, y, tol); + let vec1 = polyline_on_surface(surface, y, x, tol); + let vec2 = polyline_on_surface(surface, x, p, tol); + closed.push(connect_edges([vec0, vec1, vec2, curve])); + } else if q.x < p.x - TOLERANCE { + normalize_range(&mut curve, 0, (u0, u1)); + let p = curve[0]; + let q = curve[curve.len() - 1]; + let x = Point2::new(u1, v0); + let y = Point2::new(u0, v0); + let vec0 = polyline_on_surface(surface, q, y, tol); + let vec1 = polyline_on_surface(surface, y, x, tol); + let vec2 = polyline_on_surface(surface, x, p, tol); + closed.push(connect_edges([vec0, vec1, vec2, curve])); + } else if p.y < q.y - TOLERANCE { + normalize_range(&mut curve, 1, (v0, v1)); + let p = curve[0]; + let q = curve[curve.len() - 1]; + let x = Point2::new(u0, v0); + let y = Point2::new(u0, v1); + let vec0 = polyline_on_surface(surface, q, y, tol); + let vec1 = polyline_on_surface(surface, y, x, tol); + let vec2 = polyline_on_surface(surface, x, p, tol); + closed.push(connect_edges([vec0, vec1, vec2, curve])); + } else if q.y < p.y - TOLERANCE { + normalize_range(&mut curve, 1, (v0, v1)); + let p = curve[0]; + let q = curve[curve.len() - 1]; + let x = Point2::new(u1, v1); + let y = Point2::new(u1, v0); + let vec0 = polyline_on_surface(surface, q, y, tol); + let vec1 = polyline_on_surface(surface, y, x, tol); + let vec2 = polyline_on_surface(surface, x, p, tol); + closed.push(connect_edges([vec0, vec1, vec2, curve])); + } + } + } + 2 => { + let mut curve1 = open.pop().unwrap(); + let mut curve0 = open.pop().unwrap(); + fn end_pts(vec: &[T]) -> (T, T) { (vec[0], vec[vec.len() - 1]) } + let ((p0, p1), (q0, q1)) = (end_pts(&curve0), end_pts(&curve1)); + if !p0.x.near(&p1.x) && !q0.x.near(&q1.x) { + if let (Some(urange), _) = surface.try_range_tuple() { + normalize_range(&mut curve0, 0, urange); + normalize_range(&mut curve1, 0, urange); + } + } else if !p0.y.near(&p1.y) && !q0.y.near(&q1.y) { + if let (_, Some(vrange)) = surface.try_range_tuple() { + normalize_range(&mut curve0, 1, vrange); + normalize_range(&mut curve1, 1, vrange); + } + } + let ((p0, p1), (q0, q1)) = (end_pts(&curve0), end_pts(&curve1)); + let vec0 = polyline_on_surface(surface, p1, q0, tol); + let vec1 = polyline_on_surface(surface, q1, p0, tol); + closed.push(connect_edges([curve0, vec0, curve1, vec1])); + } + _ => {} + } + if !closed.iter().any(|curve| loop_orientation(curve)) { + if let (Some((u0, u1)), Some((v0, v1))) = surface.try_range_tuple() { + let p = [ + Point2::new(u0, v0), + Point2::new(u1, v0), + Point2::new(u1, v1), + Point2::new(u0, v1), + ]; + let vec0 = polyline_on_surface(surface, p[0], p[1], tol); + let vec1 = polyline_on_surface(surface, p[1], p[2], tol); + let vec2 = polyline_on_surface(surface, p[2], p[3], tol); + let vec3 = polyline_on_surface(surface, p[3], p[0], tol); + closed.push(connect_edges([vec0, vec1, vec2, vec3])); + } + } + Self(closed) + } + + /// whether `c` is included in the domain with boundary = `self`. + fn include(&self, c: Point2) -> bool { + let t = 2.0 * std::f64::consts::PI * HashGen::hash1(c); + let r = Vector2::new(f64::cos(t), f64::sin(t)); + self.0 + .iter() + .flat_map(|vec| vec.iter().circular_tuple_windows()) + .try_fold(0_i32, move |counter, (p0, p1)| { + let a = p0 - c; + let b = p1 - c; + let s0 = r.x * a.y - r.y * a.x; // v times a + let s1 = r.x * b.y - r.y * b.x; // v times b + let s2 = a.x * b.y - a.y * b.x; // a times b + let x = s2 / (s1 - s0); + if x.so_small() && s0 * s1 < 0.0 { + None + } else if x > 0.0 && s0 <= 0.0 && s1 > 0.0 { + Some(counter + 1) + } else if x > 0.0 && s0 >= 0.0 && s1 < 0.0 { + Some(counter - 1) + } else { + Some(counter) + } + }) + .map(|counter| counter > 0) + .unwrap_or(false) + } + + /// Inserts points and adds constraint into triangulation. + fn insert_to(&self, triangulation: &mut Cdt) { + let poly2tri: Vec<_> = self + .0 + .iter() + .flatten() + .map(|pt| { + let p = [spade_round(pt.x), spade_round(pt.y)]; + triangulation.insert(SPoint2::from(p)).ok() + }) + .collect(); + let mut prev: Option = None; + let mut counter = 0; + self.0 + .iter() + .map(Vec::len) + .flat_map(|len| { + let range = counter..counter + len; + counter += len; + range.circular_tuple_windows() + }) + .for_each(|(i, j)| { + let Some(vj) = poly2tri[j] else { return }; + if let Some(p) = prev { + let Some(v) = poly2tri[p] else { return }; + if triangulation.can_add_constraint(v, vj) { + triangulation.add_constraint(v, vj); + prev = None; + } + } else { + let Some(vi) = poly2tri[i] else { return }; + if triangulation.can_add_constraint(vi, vj) { + triangulation.add_constraint(vi, vj); + } else { + prev = Some(i); + } + } + }); + } +} + +fn spade_round(x: f64) -> f64 { + match f64::abs(x) < MIN_ALLOWED_VALUE { + true => 0.0, + false => x, + } +} + +/// Tessellates one surface trimmed by polyline. +fn trimming_tessellation(surface: &S, polyboundary: &PolyBoundary, tol: f64) -> PolygonMesh +where S: PreMeshableSurface { + let mut triangulation = Cdt::new(); + polyboundary.insert_to(&mut triangulation); + insert_surface(&mut triangulation, surface, polyboundary, tol); + let mut mesh = triangulation_into_polymesh( + triangulation.vertices(), + triangulation.inner_faces(), + surface, + polyboundary, + ); + mesh.make_face_compatible_to_normal(); + mesh +} + +/// Inserts parameter divisions into triangulation. +fn insert_surface( + triangulation: &mut Cdt, + surface: impl PreMeshableSurface, + polyline: &PolyBoundary, + tol: f64, +) { + let bdb: BoundingBox = polyline.0.iter().flatten().collect(); + let range = ((bdb.min()[0], bdb.max()[0]), (bdb.min()[1], bdb.max()[1])); + let (udiv, vdiv) = surface.parameter_division(range, tol); + let insert_res: Vec>> = udiv + .into_iter() + .map(|u| { + vdiv.iter() + .map(|v| match polyline.include(Point2::new(u, *v)) { + true => triangulation.insert(SPoint2::new(u, *v)).ok(), + false => None, + }) + .collect() + }) + .collect(); + insert_res.windows(2).for_each(|vec| { + vec[0].windows(2).zip(&vec[1]).for_each(|(a, z)| { + if let Some(x) = a[0] { + if let Some(y) = a[1] { + if triangulation.can_add_constraint(x, y) { + triangulation.add_constraint(x, y); + } + } + if let Some(z) = z { + if triangulation.can_add_constraint(x, *z) { + triangulation.add_constraint(x, *z); + } + } + } + }); + let idx = vec[0].len() - 1; + if let (Some(x), Some(y)) = (vec[0][idx], vec[1][idx]) { + if triangulation.can_add_constraint(x, y) { + triangulation.add_constraint(x, y); + } + } + }); +} + +/// Converts triangulation into `PolygonMesh`. +fn triangulation_into_polymesh<'a>( + vertices: VertexIterator<'a, SPoint2, (), CdtEdge<()>, ()>, + triangles: InnerFaceIterator<'a, SPoint2, (), CdtEdge<()>, ()>, + surface: &impl ParametricSurface3D, + polyline: &PolyBoundary, +) -> PolygonMesh { + let mut positions = Vec::::new(); + let mut uv_coords = Vec::::new(); + let mut normals = Vec::::new(); + let vmap: HashMap<_, _> = vertices + .enumerate() + .map(|(i, v)| { + let p = *v.as_ref(); + let uv = Vector2::new(p.x, p.y); + positions.push(surface.subs(uv[0], uv[1])); + uv_coords.push(uv); + normals.push(surface.normal(uv[0], uv[1])); + (v.fix(), i) + }) + .collect(); + let tri_faces: Vec<[StandardVertex; 3]> = triangles + .map(|tri| tri.vertices()) + .filter(|tri| { + fn sp2cg(p: SPoint2) -> Point2 { Point2::new(p.x, p.y) } + let tri = array![i => sp2cg(*tri[i].as_ref()); 3]; + let (a, b) = (tri[1] - tri[0], tri[2] - tri[0]); + let c = tri[0] + (a + b) / 3.0; + let area = a.x * b.y - a.y * b.x; + polyline.include(c) && !area.so_small2() + }) + .map(|tri| { + let idcs = array![i => vmap[&tri[i].fix()]; 3]; + array![i => [idcs[i], idcs[i], idcs[i]].into(); 3] + }) + .collect(); + PolygonMesh::debug_new( + StandardAttributes { + positions, + uv_coords, + normals, + }, + Faces::from_tri_and_quad_faces(tri_faces, Vec::new()), + ) +} + +fn polyline_on_surface( + surface: impl PreMeshableSurface, + p: Point2, + q: Point2, + tol: f64, +) -> Vec { + use truck_geometry::prelude::*; + let line = Line(p, q); + let pcurve = PCurve::new(line, surface); + let (vec, _) = pcurve.parameter_division(pcurve.range_tuple(), tol); + vec.into_iter().map(|t| line.subs(t)).collect() +} + +#[test] +#[ignore] +#[cfg(not(target_arch = "wasm32"))] +fn par_bench() { + use std::time::Instant; + use truck_modeling::*; + const JSON: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../resources/shape/bottle.json" + )); + let solid: Solid = serde_json::from_str(JSON).unwrap(); + let shell = solid.into_boundaries().pop().unwrap(); + + let instant = Instant::now(); + (0..100).for_each(|_| { + let _shell = shell_tessellation(&shell, 0.01, by_search_parameter); + }); + println!("{}ms", instant.elapsed().as_millis()); + + let instant = Instant::now(); + (0..100).for_each(|_| { + let _shell = shell_tessellation_single_thread(&shell, 0.01, by_search_parameter); + }); + println!("{}ms", instant.elapsed().as_millis()); +} diff --git a/vendor/truck-meshalgo/src/vtk.rs b/vendor/truck-meshalgo/src/vtk.rs new file mode 100644 index 00000000..7c829be1 --- /dev/null +++ b/vendor/truck-meshalgo/src/vtk.rs @@ -0,0 +1,525 @@ +use crate::*; +use rustc_hash::FxHashMap as HashMap; +use std::convert::identity; +use truck_base::tolerance::TOLERANCE; +use truck_topology::{compress::*, Vertex, *}; +pub use vtkio; +use vtkio::model::{Attributes, *}; + +/// Trait for convert to [`DataSet`] +pub trait ToDataSet { + /// Converts to [`DataSet`] + /// # Remarks + /// The `DataSet` is generated assuming output in XML format. + fn to_data_set(&self) -> DataSet; +} + +fn to_vertex_numbers(faces: &Faces) -> VertexNumbers { + let connectivity = faces + .face_iter() + .flat_map(|face| face.iter().map(|idx| *idx as u64)) + .collect::>(); + let mut offset: u64 = 0; + let offsets = faces + .face_iter() + .map(|face| { + offset += face.len() as u64; + offset + }) + .collect::>(); + VertexNumbers::XML { + connectivity, + offsets, + } +} + +impl ToDataSet for PolygonMesh> { + fn to_data_set(&self) -> DataSet { + let flatten_points = self + .attributes() + .iter() + .flat_map(|p| Into::<[f64; 3]>::into(*p)) + .collect::>(); + DataSet::PolyData { + meta: None, + pieces: vec![Piece::Inline(Box::new(PolyDataPiece { + points: IOBuffer::F64(flatten_points), + polys: Some(to_vertex_numbers(self.faces())), + ..Default::default() + }))], + } + } +} + +impl ToDataSet for PolygonMesh> { + fn to_data_set(&self) -> DataSet { + let flatten_points = self + .attributes() + .iter() + .flat_map(|(p, _)| Into::<[f64; 3]>::into(*p)) + .collect::>(); + let flatten_uvs = self + .attributes() + .iter() + .flat_map(|(_, uv)| Into::<[f64; 2]>::into(*uv)) + .collect::>(); + let uvs = Attribute::DataArray(DataArray { + name: "TCoords".to_owned(), + elem: ElementType::TCoords(2), + data: IOBuffer::F64(flatten_uvs), + }); + DataSet::PolyData { + meta: None, + pieces: vec![Piece::Inline(Box::new(PolyDataPiece { + points: IOBuffer::F64(flatten_points), + polys: Some(to_vertex_numbers(self.faces())), + data: Attributes { + point: vec![uvs], + ..Default::default() + }, + ..Default::default() + }))], + } + } +} + +impl ToDataSet for PolygonMesh> { + fn to_data_set(&self) -> DataSet { + let flatten_points = self + .attributes() + .iter() + .flat_map(|(p, _)| Into::<[f64; 3]>::into(*p)) + .collect::>(); + let flatten_normals = self + .attributes() + .iter() + .flat_map(|(_, n)| Into::<[f64; 3]>::into(*n)) + .collect::>(); + let normals = Attribute::DataArray(DataArray { + name: "Normals".to_owned(), + elem: ElementType::Normals, + data: IOBuffer::F64(flatten_normals), + }); + DataSet::PolyData { + meta: None, + pieces: vec![Piece::Inline(Box::new(PolyDataPiece { + points: IOBuffer::F64(flatten_points), + polys: Some(to_vertex_numbers(self.faces())), + data: Attributes { + point: vec![normals], + ..Default::default() + }, + ..Default::default() + }))], + } + } +} + +impl ToDataSet for PolygonMesh> { + fn to_data_set(&self) -> DataSet { + let flatten_points = self + .attributes() + .iter() + .flat_map(|attr| Into::<[f64; 3]>::into(attr.position)) + .collect::>(); + let flatten_uvs = self + .attributes() + .iter() + .flat_map(|attr| match attr.uv_coord { + Some(uv) => Into::<[f64; 2]>::into(uv), + None => [f64::NAN; 2], + }) + .collect::>(); + let uvs = Attribute::DataArray(DataArray { + name: "TCoords".to_owned(), + elem: ElementType::TCoords(2), + data: IOBuffer::F64(flatten_uvs), + }); + let flatten_normals = self + .attributes() + .iter() + .flat_map(|attr| match attr.normal { + Some(normal) => Into::<[f64; 3]>::into(normal), + None => [f64::NAN; 3], + }) + .collect::>(); + let normals = Attribute::DataArray(DataArray { + name: "Normals".to_owned(), + elem: ElementType::Normals, + data: IOBuffer::F64(flatten_normals), + }); + DataSet::PolyData { + meta: None, + pieces: vec![Piece::Inline(Box::new(PolyDataPiece { + points: IOBuffer::F64(flatten_points), + polys: Some(to_vertex_numbers(self.faces())), + data: Attributes { + point: vec![uvs, normals], + ..Default::default() + }, + ..Default::default() + }))], + } + } +} + +impl ToDataSet for PolygonMesh { + fn to_data_set(&self) -> DataSet { + let attrs = self.attributes(); + match (attrs.uv_coords().is_empty(), attrs.normals().is_empty()) { + (true, true) => self.to_positions_mesh().to_data_set(), + (false, true) => self + .expands(|attr| { + let uv = match attr.uv_coord { + Some(uv) => uv, + None => Vector2::new(f64::NAN, f64::NAN), + }; + (attr.position, uv) + }) + .to_data_set(), + (true, false) => self + .expands(|attr| { + let normal = match attr.normal { + Some(normal) => normal, + None => Vector3::new(f64::NAN, f64::NAN, f64::NAN), + }; + (attr.position, normal) + }) + .to_data_set(), + (false, false) => self.expands(identity).to_data_set(), + } + } +} + +fn hash_point(p: Point3) -> Option<[i64; 3]> { + (p / (TOLERANCE * 50.0) - Vector3::new(0.5, 0.5, 0.5)) + .cast() + .map(Into::into) +} + +impl ToDataSet for Vertex { + fn to_data_set(&self) -> DataSet { + DataSet::UnstructuredGrid { + meta: None, + pieces: vec![Piece::Inline(Box::new(UnstructuredGridPiece { + points: IOBuffer::F64(Into::<[f64; 3]>::into(self.point()).to_vec()), + cells: Cells { + cell_verts: VertexNumbers::XML { + connectivity: vec![0], + offsets: vec![1], + }, + types: vec![CellType::Vertex], + }, + data: Default::default(), + }))], + } + } +} + +impl ToDataSet for Edge> { + fn to_data_set(&self) -> DataSet { + let curve = self.oriented_curve(); + let points = curve + .0 + .iter() + .copied() + .flat_map(Into::<[f64; 3]>::into) + .collect::>(); + let len = curve.0.len() as u64; + let connectivity = (0..len).chain(vec![0, len - 1]).collect(); + DataSet::UnstructuredGrid { + meta: None, + pieces: vec![Piece::Inline(Box::new(UnstructuredGridPiece { + points: IOBuffer::F64(points), + cells: Cells { + cell_verts: VertexNumbers::XML { + connectivity, + offsets: vec![len, len + 1, len + 2], + }, + types: vec![CellType::PolyLine, CellType::Vertex, CellType::Vertex], + }, + data: Default::default(), + }))], + } + } +} + +impl ToDataSet for Wire> { + fn to_data_set(&self) -> DataSet { + let mut points = Vec::<[f64; 3]>::new(); + let mut connectivity = Vec::::new(); + let mut offsets = Vec::::new(); + let mut types = Vec::::new(); + let mut vmap = truck_base::entry_map::FxEntryMap::new( + |v: &Vertex| v.id(), + |v: &Vertex| { + points.push(v.point().into()); + points.len() as u64 - 1 + }, + ); + let tmp: Vec<_> = self.vertex_iter().collect(); + tmp.iter().for_each(|v| { + vmap.entry_or_insert(v); + }); + let vmap: HashMap<_, _> = vmap.into(); + self.edge_iter().for_each(|edge| { + let pts = edge.oriented_curve().0; + connectivity.push(*vmap.get(&edge.front().id()).unwrap()); + connectivity.extend((1..pts.len() - 1).map(|i| (points.len() + i - 1) as u64)); + connectivity.push(*vmap.get(&edge.back().id()).unwrap()); + offsets.push(connectivity.len() as u64); + points.extend( + pts[1..pts.len() - 1] + .iter() + .copied() + .map(Into::<[f64; 3]>::into), + ); + types.push(CellType::PolyLine); + }); + self.vertex_iter().enumerate().for_each(|(idx, _)| { + connectivity.push(idx as u64); + offsets.push(connectivity.len() as u64); + types.push(CellType::Vertex); + }); + DataSet::UnstructuredGrid { + meta: None, + pieces: vec![Piece::Inline(Box::new(UnstructuredGridPiece { + points: IOBuffer::F64(points.concat().to_vec()), + cells: Cells { + cell_verts: VertexNumbers::XML { + connectivity, + offsets, + }, + types, + }, + data: Default::default(), + }))], + } + } +} + +fn face_piece( + face: &Face, PolygonMesh>, +) -> Piece { + let polygon = face.oriented_surface().expands(identity); + let mut length = 0; + let map: HashMap, usize> = polygon + .attributes() + .iter() + .map(move |attr| { + length += 1; + (hash_point(attr.position), length - 1) + }) + .collect(); + let flatten_points = polygon + .attributes() + .iter() + .flat_map(|attr| Into::<[f64; 3]>::into(attr.position)) + .collect::>(); + let flatten_uvs = polygon + .attributes() + .iter() + .flat_map(|attr| match attr.uv_coord { + Some(uv) => Into::<[f64; 2]>::into(uv), + None => [f64::NAN; 2], + }) + .collect::>(); + let uvs = Attribute::DataArray(DataArray { + name: "TCoords".to_owned(), + elem: ElementType::TCoords(2), + data: IOBuffer::F64(flatten_uvs), + }); + let flatten_normals = polygon + .attributes() + .iter() + .flat_map(|attr| match attr.normal { + Some(normal) => Into::<[f64; 3]>::into(normal), + None => [f64::NAN; 3], + }) + .collect::>(); + let normals = Attribute::DataArray(DataArray { + name: "Normals".to_owned(), + elem: ElementType::Normals, + data: IOBuffer::F64(flatten_normals), + }); + let mut cell_verts = to_vertex_numbers(polygon.faces()); + let mut types = vec![CellType::Polygon; cell_verts.num_cells()]; + face.boundaries().into_iter().flatten().for_each(|edge| { + types.push(CellType::PolyLine); + let curve = edge.oriented_curve().0; + let polyline = curve + .into_iter() + .filter_map(|p| map.get(&hash_point(p)).map(|i| *i as u64)); + if let VertexNumbers::XML { + connectivity, + offsets, + } = &mut cell_verts + { + connectivity.extend(polyline); + offsets.push(connectivity.len() as u64); + } + }); + face.vertex_iter().try_for_each(|v| { + types.push(CellType::Vertex); + let idx = map.get(&hash_point(v.point())).map(|i| *i as u64)?; + if let VertexNumbers::XML { + connectivity, + offsets, + } = &mut cell_verts + { + connectivity.push(idx); + offsets.push(connectivity.len() as u64) + } + Some(()) + }); + Piece::Inline(Box::new(UnstructuredGridPiece { + points: IOBuffer::F64(flatten_points), + cells: Cells { cell_verts, types }, + data: Attributes { + point: vec![uvs, normals], + ..Default::default() + }, + })) +} + +impl ToDataSet for Face, PolygonMesh> { + fn to_data_set(&self) -> DataSet { + DataSet::UnstructuredGrid { + meta: None, + pieces: vec![face_piece(self)], + } + } +} + +impl ToDataSet for Shell, PolygonMesh> { + fn to_data_set(&self) -> DataSet { + DataSet::UnstructuredGrid { + meta: None, + pieces: self.face_iter().map(face_piece).collect(), + } + } +} + +fn cface_piece( + vertices: &[Point3], + edges: &[CompressedEdge>], + face: &CompressedFace, +) -> Option> { + let polygon = match face.orientation { + true => face.surface.expands(identity), + false => face.surface.inverse().expands(identity), + }; + let mut length = 0; + let map: HashMap, usize> = polygon + .attributes() + .iter() + .map(move |attr| { + length += 1; + (hash_point(attr.position), length - 1) + }) + .collect(); + let flatten_points = polygon + .attributes() + .iter() + .flat_map::<[f64; 3], _>(|attr| attr.position.into()) + .collect::>(); + let flatten_uvs = polygon + .attributes() + .iter() + .flat_map(|attr| match attr.uv_coord { + Some(uv) => uv.into(), + None => [f64::NAN; 2], + }) + .collect::>(); + let uvs = Attribute::DataArray(DataArray { + name: "TCoords".to_owned(), + elem: ElementType::TCoords(2), + data: IOBuffer::F64(flatten_uvs), + }); + let flatten_normals = polygon + .attributes() + .iter() + .flat_map(|attr| match attr.normal { + Some(normal) => normal.into(), + None => [f64::NAN; 3], + }) + .collect::>(); + let normals = Attribute::DataArray(DataArray { + name: "Normals".to_owned(), + elem: ElementType::Normals, + data: IOBuffer::F64(flatten_normals), + }); + let mut cell_verts = to_vertex_numbers(polygon.faces()); + let mut types = vec![CellType::Polygon; cell_verts.num_cells()]; + let edge_indices: Vec<_> = face + .boundaries + .iter() + .flat_map::>, _>(|boundary| { + match face.orientation { + true => Box::new(boundary.iter()), + false => Box::new(boundary.iter().rev()), + } + }) + .collect(); + edge_indices.iter().try_for_each(|edge| { + let curve = match face.orientation == edge.orientation { + true => edges[edge.index].curve.clone(), + false => edges[edge.index].curve.inverse(), + }; + let polyline = curve + .iter() + .filter_map(|p| map.get(&hash_point(*p)).map(|i| *i as u64)) + .collect::>(); + if polyline.is_empty() { + return None; + } + types.push(CellType::PolyLine); + if let VertexNumbers::XML { + connectivity, + offsets, + } = &mut cell_verts + { + connectivity.extend(polyline); + offsets.push(connectivity.len() as u64); + } + Some(()) + })?; + edge_indices.into_iter().try_for_each(|edge| { + let v = match face.orientation == edge.orientation { + true => edges[edge.index].vertices.0, + false => edges[edge.index].vertices.1, + }; + let idx = map.get(&hash_point(vertices[v])).map(|i| *i as u64)?; + types.push(CellType::Vertex); + if let VertexNumbers::XML { + connectivity, + offsets, + } = &mut cell_verts + { + connectivity.push(idx); + offsets.push(connectivity.len() as u64) + } + Some(()) + })?; + Some(Piece::Inline(Box::new(UnstructuredGridPiece { + points: IOBuffer::F64(flatten_points), + cells: Cells { cell_verts, types }, + data: Attributes { + point: vec![uvs, normals], + ..Default::default() + }, + }))) +} + +impl ToDataSet for CompressedShell, PolygonMesh> { + fn to_data_set(&self) -> DataSet { + DataSet::UnstructuredGrid { + meta: None, + pieces: self + .faces + .iter() + .filter_map(|face| cface_piece(&self.vertices, &self.edges, face)) + .collect(), + } + } +} diff --git a/vendor/truck-stepio/Cargo.toml b/vendor/truck-stepio/Cargo.toml new file mode 100644 index 00000000..31b7433e --- /dev/null +++ b/vendor/truck-stepio/Cargo.toml @@ -0,0 +1,109 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "truck-stepio" +version = "0.3.0" +authors = ["Yoshinori Tanimura "] +build = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Reads/writes STEP files from/to truck" +homepage = "https://github.com/ricosjp/truck" +readme = "README.md" +keywords = [ + "truck", + "graphics", +] +categories = ["graphics"] +license = "Apache-2.0" +repository = "https://github.com/ricosjp/truck" + +[lib] +name = "truck_stepio" +path = "src/lib.rs" + + + + + + +[dependencies.chrono] +version = "0.4.38" +features = ["wasmbind"] + +[dependencies.derive_more] +version = "0.99.18" +optional = true + +[dependencies.ruststep] +version = "0.4.0" +optional = true + +[dependencies.serde] +version = "1.0.210" +features = ["derive"] +optional = true + +[dependencies.truck-derivers] +version = "0.1.0" +optional = true + +[dependencies.truck-geometry] +version = "0.5.0" + +[dependencies.truck-geotrait] +version = "0.4.0" +optional = true + +[dependencies.truck-modeling] +version = "0.6.0" + +[dependencies.truck-polymesh] +version = "0.6.0" + +[dependencies.truck-topology] +version = "0.6.0" + +[dev-dependencies.clap] +version = "4.5.17" +features = ["derive"] + +[dev-dependencies.espr-derive] +version = "0.4.0" + +[dev-dependencies.itertools] +version = "0.13.0" + +[dev-dependencies.proptest] +version = "1.5.0" + +[dev-dependencies.serde_json] +version = "1.0.128" + +[dev-dependencies.truck-meshalgo] +version = "0.4.0" + +[features] +default = [ + "in", + "derive", +] +derive = ["truck-derivers"] +in = [ + "derive_more", + "ruststep", + "serde", + "truck-geotrait", +] diff --git a/vendor/truck-stepio/LICENSE b/vendor/truck-stepio/LICENSE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/vendor/truck-stepio/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/vendor/truck-stepio/README.md b/vendor/truck-stepio/README.md new file mode 100644 index 00000000..2e42212e --- /dev/null +++ b/vendor/truck-stepio/README.md @@ -0,0 +1,21 @@ +# truck-stepio + +[![Crates.io](https://img.shields.io/crates/v/truck-stepio.svg)](https://crates.io/crates/truck-stepio) [![Docs.rs](https://docs.rs/truck-stepio/badge.svg)](https://docs.rs/truck-stepio) + +Reads/writes STEP files from/to truck. + +## Sample Codes + +### shape-to-step + +convert from truck shape json to step file. + +#### usage + +```bash +shape-to-step [output shape file] +``` + +### step-to-mesh + +Parse STEP data, extract shape, and meshing. diff --git a/vendor/truck-stepio/src/in/alias.rs b/vendor/truck-stepio/src/in/alias.rs new file mode 100644 index 00000000..6555fece --- /dev/null +++ b/vendor/truck-stepio/src/in/alias.rs @@ -0,0 +1,397 @@ +use crate::{self as truck_stepio}; +use derive_more::*; +use serde::{Deserialize, Serialize}; +use truck_derivers::{DisplayByStep, StepCurve, StepLength, StepSurface}; + +/// re-export structs in `truck-geometry` and `truck-polymesh`. +pub mod re_exports { + pub use truck_geometry::prelude::*; + pub use truck_polymesh::*; +} +pub use re_exports::*; + +/// Errors that occur when converting STEP format +pub type StepConvertingError = Box; + +/// `ellipse`, realized in `truck` +pub type Ellipse = Processor>, M>; +/// `hyperbola`, realized in `truck` +pub type Hyperbola = Processor>, M>; +/// `parabola`, realized in `truck` +pub type Parabola = Processor>, M>; +/// `spherical_surface`, realized in `truck` +pub type SphericalSurface = Processor; +/// `cylindrical_surface`, realized in `truck` +pub type CylindricalSurface = Processor>, Matrix4>; +/// `toroidal_surface`, realized in `truck` +pub type ToroidalSurface = Processor; +/// `conical_surface`, realized in `truck` +pub type ConicalSurface = Processor>, Matrix4>; +/// `surface_of_linear_extrusion`, realized in `truck` +pub type StepExtrudedCurve = ExtrudedCurve; +/// `surface_of_revolution`, realized in `truck` +pub type StepRevolutedCurve = Processor, Matrix4>; +/// `pcurve`, realized in `truck` +pub type PCurve = truck_geometry::prelude::PCurve, Box>; + +/// `conic` in 2D, realized in `truck` +#[derive( + Clone, + Copy, + Debug, + PartialEq, + From, + Serialize, + Deserialize, + ParametricCurve, + BoundedCurve, + Cut, + Invertible, + ParameterDivision1D, + SearchParameterD1, + SearchNearestParameterD1, + TransformedM3, + StepLength, + DisplayByStep, + StepCurve, +)] +pub enum Conic2D { + Ellipse(Ellipse), + Hyperbola(Hyperbola), + Parabola(Parabola), +} + +/// `curve` in 2D, realized in `truck` +#[derive( + Clone, + Debug, + PartialEq, + From, + Serialize, + Deserialize, + ParametricCurve, + BoundedCurve, + Cut, + Invertible, + ParameterDivision1D, + SearchParameterD1, + SearchNearestParameterD1, + TransformedM3, + StepLength, + DisplayByStep, + StepCurve, +)] + +pub enum Curve2D { + Line(Line), + Polyline(PolylineCurve), + Conic(Conic2D), + BSplineCurve(BSplineCurve), + NurbsCurve(NurbsCurve), +} + +/// `conic` in 3D, realized in `truck` +#[derive( + Clone, + Copy, + Debug, + PartialEq, + From, + Serialize, + Deserialize, + ParametricCurve, + BoundedCurve, + Cut, + Invertible, + ParameterDivision1D, + SearchParameterD1, + SearchNearestParameterD1, + TransformedM4, + StepLength, + DisplayByStep, + StepCurve, +)] +pub enum Conic3D { + Ellipse(Ellipse), + Hyperbola(Hyperbola), + Parabola(Parabola), +} + +/// `curve` in 3D, realized in `truck` +#[derive( + Clone, + Debug, + PartialEq, + From, + Serialize, + Deserialize, + ParametricCurve, + BoundedCurve, + Cut, + Invertible, + ParameterDivision1D, + SearchParameterD1, + SearchNearestParameterD1, + TransformedM4, + StepLength, + DisplayByStep, + StepCurve, +)] +pub enum Curve3D { + Line(Line), + Polyline(PolylineCurve), + Conic(Conic3D), + BSplineCurve(BSplineCurve), + PCurve(PCurve), + NurbsCurve(NurbsCurve), +} + +/// `elementary_surface`, realized in `truck` +#[derive( + Clone, + Copy, + Debug, + PartialEq, + Serialize, + Deserialize, + ParametricSurface3D, + ParameterDivision2D, + SearchParameterD2, + SearchNearestParameterD2, + Invertible, + TransformedM4, + StepLength, + DisplayByStep, + StepSurface, +)] +pub enum ElementarySurface { + Plane(Plane), + Sphere(SphericalSurface), + CylindricalSurface(CylindricalSurface), + ToroidalSurface(ToroidalSurface), + ConicalSurface(ConicalSurface), +} + +/// `swept_surface`, realized in `truck` +#[derive( + Clone, + Debug, + PartialEq, + Serialize, + Deserialize, + ParametricSurface3D, + ParameterDivision2D, + SearchParameterD2, + SearchNearestParameterD2, + Invertible, + TransformedM4, + StepLength, + DisplayByStep, + StepSurface, +)] +pub enum SweptCurve { + ExtrudedCurve(StepExtrudedCurve), + RevolutedCurve(StepRevolutedCurve), +} + +/// `surface`, realized in `truck` +#[derive( + Clone, + Debug, + PartialEq, + Serialize, + Deserialize, + ParametricSurface3D, + ParameterDivision2D, + SearchParameterD2, + SearchNearestParameterD2, + Invertible, + TransformedM4, + StepLength, + StepSurface, +)] +pub enum Surface { + ElementarySurface(Box), + SweptCurve(Box), + BSplineSurface(Box>), + NurbsSurface(Box>), +} + +impl truck_stepio::out::DisplayByStep for Surface { + fn fmt(&self, idx: usize, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use Surface::*; + match self { + ElementarySurface(x) => x.fmt(idx, f), + SweptCurve(x) => x.fmt(idx, f), + BSplineSurface(x) => x.fmt(idx, f), + NurbsSurface(x) => x.fmt(idx, f), + } + } +} + +/// `spherical_surface`, realized in `truck` +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, StepSurface)] +pub struct Sphere(pub truck_geometry::prelude::Sphere); + +impl truck_stepio::out::StepSurface for Processor { + #[inline(always)] + fn same_sense(&self) -> bool { self.orientation() } +} + +mod sphere { + use super::*; + use std::f64::consts::PI; + use std::ops::Bound; + impl ParametricSurface for Sphere { + type Point = Point3; + type Vector = Vector3; + #[inline] + fn subs(&self, u: f64, v: f64) -> Point3 { self.0.subs(PI / 2.0 - v, u) } + #[inline] + fn uder(&self, u: f64, v: f64) -> Vector3 { self.0.vder(PI / 2.0 - v, u) } + #[inline] + fn vder(&self, u: f64, v: f64) -> Vector3 { -self.0.uder(PI / 2.0 - v, u) } + #[inline] + fn uuder(&self, u: f64, v: f64) -> Vector3 { self.0.vvder(PI / 2.0 - v, u) } + #[inline] + fn uvder(&self, u: f64, v: f64) -> Vector3 { -self.0.uvder(PI / 2.0 - v, u) } + #[inline] + fn vvder(&self, u: f64, v: f64) -> Vector3 { self.0.uuder(PI / 2.0 - v, u) } + #[inline] + fn parameter_range(&self) -> (ParameterRange, ParameterRange) { + ( + (Bound::Included(0.0), Bound::Excluded(2.0 * PI)), + (Bound::Included(-PI / 2.0), Bound::Excluded(PI / 2.0)), + ) + } + #[inline] + fn u_period(&self) -> Option { Some(2.0 * PI) } + } + impl ParametricSurface3D for Sphere { + #[inline] + fn normal(&self, u: f64, v: f64) -> Vector3 { self.0.normal(PI / 2.0 - v, u) } + } + impl SearchNearestParameter for Sphere { + type Point = Point3; + #[inline] + fn search_nearest_parameter>( + &self, + point: Self::Point, + hint: H, + trials: usize, + ) -> Option<(f64, f64)> { + self.0 + .search_nearest_parameter(point, hint, trials) + .map(|(u, v)| (v, PI / 2.0 - u)) + } + } + impl SearchParameter for Sphere { + type Point = Point3; + #[inline] + fn search_parameter>( + &self, + point: Self::Point, + hint: H, + trials: usize, + ) -> Option<(f64, f64)> { + self.0 + .search_parameter(point, hint, trials) + .map(|(u, v)| (v, PI / 2.0 - u)) + } + } + impl ParameterDivision2D for Sphere { + #[inline] + fn parameter_division( + &self, + ((u0, u1), (v0, v1)): ((f64, f64), (f64, f64)), + tol: f64, + ) -> (Vec, Vec) { + let range = ((PI / 2.0 - v1, PI / 2.0 - v0), (u0, u1)); + let (udiv0, vdiv0) = self.0.parameter_division(range, tol); + let vdiv = udiv0.into_iter().map(|u| PI / 2.0 - u).collect(); + (vdiv0, vdiv) + } + } + + #[cfg(test)] + proptest::proptest! { + #[test] + fn surface( + center in proptest::array::uniform3(-100.0f64..100.0f64), + radius in 0.1f64..100.0f64, + (u, v) in (0.0..=2.0 * PI, -PI / 2.0..=PI / 2.0), + ) { + const EPS: f64 = 1.0e-3; + let sphere = Sphere(truck_geometry::prelude::Sphere::new(center.into(), radius)); + + let uder0 = sphere.uder(u, v); + let uder1 = (sphere.subs(u + EPS, v) - sphere.subs(u - EPS, v)) / (2.0 * EPS); + assert!( + (uder0 - uder1).magnitude2() < EPS, + "uder failed: {uder0:?}, {uder1:?}" + ); + + let vder0 = sphere.vder(u, v); + let vder1 = (sphere.subs(u, v + EPS) - sphere.subs(u, v - EPS)) / (2.0 * EPS); + assert!( + (vder0 - vder1).magnitude2() < EPS, + "vder failed: {vder0:?}, {vder1:?}" + ); + + let uuder0 = sphere.uuder(u, v); + let uuder1 = (sphere.uder(u + EPS, v) - sphere.uder(u - EPS, v)) / (2.0 * EPS); + assert!( + (uuder0 - uuder1).magnitude2() < EPS, + "uuder failed: {uuder0:?}, {uuder1:?}" + ); + + let uvder0 = sphere.uvder(u, v); + let uvder1 = (sphere.uder(u, v + EPS) - sphere.uder(u, v - EPS)) / (2.0 * EPS); + assert!( + (uvder0 - uvder1).magnitude2() < EPS, + "uvder failed: {uvder0:?}, {uvder1:?}" + ); + + let vvder0 = sphere.vvder(u, v); + let vvder1 = (sphere.vder(u, v + EPS) - sphere.vder(u, v - EPS)) / (2.0 * EPS); + assert!( + (vvder0 - vvder1).magnitude2() < EPS, + "vvder failed: {vvder0:?}, {vvder1:?}" + ); + + let n0 = sphere.normal(u, v); + let n1 = sphere.uder(u, v).cross(sphere.vder(u, v)).normalize(); + assert!( + (n0 - n1).magnitude2() < EPS, + "normal failed: {n0:?}, {n1:?}" + ); + } + } +} + +impl truck_stepio::out::ConstStepLength for Processor { + const LENGTH: usize = Processor::::LENGTH; +} +impl truck_stepio::out::StepLength for Processor { + fn step_length(&self) -> usize { ::LENGTH } +} +impl truck_stepio::out::DisplayByStep for Processor { + fn fmt(&self, idx: usize, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Processor::new(self.entity().0) + .transformed(*self.transform()) + .fmt(idx, f) + } +} + +/// Implementation required to apply a closed surface division to a shape parsed from a STEP file. +mod from_pcurve { + use super::{Curve2D, Curve3D, Surface}; + use truck_geometry::prelude::*; + + impl From, Surface>> for Curve3D { + fn from(value: PCurve, Surface>) -> Self { + let (line, surface) = value.decompose(); + Curve3D::PCurve(PCurve::new(Curve2D::Line(line).into(), surface.into())) + } + } +} diff --git a/vendor/truck-stepio/src/in/mod.rs b/vendor/truck-stepio/src/in/mod.rs new file mode 100644 index 00000000..a34a995c --- /dev/null +++ b/vendor/truck-stepio/src/in/mod.rs @@ -0,0 +1,2784 @@ +#![allow(missing_docs, unused_qualifications)] + +/// re-export [`ruststep`](https://docs.rs/ruststep/latest/ruststep/) +pub use ruststep; + +use ruststep::{ + ast::{DataSection, EntityInstance, Name, Parameter, SubSuperRecord}, + primitive::Logical, + tables::{EntityTable, IntoOwned, PlaceHolder}, + Holder, +}; +use serde::{Deserialize, Serialize}; +use std::result::Result; +use std::{collections::HashMap, f64::consts::PI}; +use truck_geometry::prelude as truck; +use truck_topology::compress::*; + +/// type alias +pub mod alias; +use alias::*; + +/// the exchange structure corresponds to a graph in STEP file +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Table { + // primitives + pub cartesian_point: HashMap, + pub direction: HashMap, + pub vector: HashMap, + pub placement: HashMap, + pub axis1_placement: HashMap, + pub axis2_placement_2d: HashMap, + pub axis2_placement_3d: HashMap, + + // curve + pub line: HashMap, + pub polyline: HashMap, + pub b_spline_curve_with_knots: HashMap, + pub bezier_curve: HashMap, + pub quasi_uniform_curve: HashMap, + pub uniform_curve: HashMap, + pub rational_b_spline_curve: HashMap, + pub circle: HashMap, + pub ellipse: HashMap, + pub hyperbola: HashMap, + pub parabola: HashMap, + pub pcurve: HashMap, + pub surface_curve: HashMap, + + // surface + pub plane: HashMap, + pub spherical_surface: HashMap, + pub cylindrical_surface: HashMap, + pub toroidal_surface: HashMap, + pub conical_surface: HashMap, + pub b_spline_surface_with_knots: HashMap, + pub uniform_surface: HashMap, + pub quasi_uniform_surface: HashMap, + pub bezier_surface: HashMap, + pub rational_b_spline_surface: HashMap, + pub surface_of_linear_extrusion: HashMap, + pub surface_of_revolution: HashMap, + + // topology + pub vertex_point: HashMap, + pub edge_curve: HashMap, + pub oriented_edge: HashMap, + pub edge_loop: HashMap, + pub face_bound: HashMap, + pub face_surface: HashMap, + pub oriented_face: HashMap, + pub shell: HashMap, + pub oriented_shell: HashMap, + + // others + pub definitional_representation: HashMap, + + // dummy + pub dummy: HashMap, +} + +impl Table { + pub fn push_instance(&mut self, instance: &EntityInstance) -> ruststep::error::Result<()> { + match instance { + EntityInstance::Simple { id, record } => match record.name.as_str() { + "CARTESIAN_POINT" => { + self.cartesian_point + .insert(*id, Deserialize::deserialize(record)?); + } + "DIRECTION" => { + self.direction + .insert(*id, Deserialize::deserialize(record)?); + } + "VECTOR" => { + self.vector.insert(*id, Deserialize::deserialize(record)?); + } + "PLACEMENT" => { + self.placement + .insert(*id, Deserialize::deserialize(record)?); + } + "AXIS1_PLACEMENT" => { + self.axis1_placement + .insert(*id, Deserialize::deserialize(&record.parameter)?); + } + "AXIS2_PLACEMENT_2D" => { + self.axis2_placement_2d + .insert(*id, Deserialize::deserialize(&record.parameter)?); + } + "AXIS2_PLACEMENT_3D" => { + self.axis2_placement_3d + .insert(*id, Deserialize::deserialize(&record.parameter)?); + } + "LINE" => { + self.line + .insert(*id, Deserialize::deserialize(&record.parameter)?); + } + "POLYLINE" => { + self.polyline.insert(*id, Deserialize::deserialize(record)?); + } + "B_SPLINE_CURVE_WITH_KNOTS" => { + self.b_spline_curve_with_knots + .insert(*id, Deserialize::deserialize(record)?); + } + "BEZIER_CURVE" => { + self.bezier_curve + .insert(*id, Deserialize::deserialize(record)?); + } + "QUASI_UNIFORM_CURVE" => { + self.quasi_uniform_curve + .insert(*id, Deserialize::deserialize(record)?); + } + "UNIFORM_CURVE" => { + self.uniform_curve + .insert(*id, Deserialize::deserialize(record)?); + } + "CIRCLE" => { + self.circle.insert(*id, Deserialize::deserialize(record)?); + } + "ELLIPSE" => { + self.ellipse.insert(*id, Deserialize::deserialize(record)?); + } + "HYPERBOLA" => { + self.hyperbola + .insert(*id, Deserialize::deserialize(record)?); + } + "PARABOLA" => { + self.parabola.insert(*id, Deserialize::deserialize(record)?); + } + "PCURVE" => { + self.pcurve.insert(*id, Deserialize::deserialize(record)?); + } + "SURFACE_CURVE" => { + self.surface_curve + .insert(*id, Deserialize::deserialize(record)?); + } + "SEAM_CURVE" => { + self.surface_curve + .insert(*id, Deserialize::deserialize(&record.parameter)?); + } + "PLANE" => { + self.plane.insert(*id, Deserialize::deserialize(record)?); + } + "SPHERICAL_SURFACE" => { + self.spherical_surface + .insert(*id, Deserialize::deserialize(record)?); + } + "CYLINDRICAL_SURFACE" => { + self.cylindrical_surface + .insert(*id, Deserialize::deserialize(record)?); + } + "TOROIDAL_SURFACE" => { + self.toroidal_surface + .insert(*id, Deserialize::deserialize(record)?); + } + "CONICAL_SURFACE" => { + self.conical_surface + .insert(*id, Deserialize::deserialize(record)?); + } + "B_SPLINE_SURFACE_WITH_KNOTS" => { + self.b_spline_surface_with_knots + .insert(*id, Deserialize::deserialize(record)?); + } + "UNIFORM_SURFACE" => { + self.uniform_surface + .insert(*id, Deserialize::deserialize(record)?); + } + "QUASI_UNIFORM_SURFACE" => { + self.quasi_uniform_surface + .insert(*id, Deserialize::deserialize(record)?); + } + "BEZIER_SURFACE" => { + self.bezier_surface + .insert(*id, Deserialize::deserialize(record)?); + } + "SURFACE_OF_LINEAR_EXTRUSION" => { + self.surface_of_linear_extrusion + .insert(*id, Deserialize::deserialize(record)?); + } + "SURFACE_OF_REVOLUTION" => { + self.surface_of_revolution + .insert(*id, Deserialize::deserialize(record)?); + } + + "VERTEX_POINT" => { + self.vertex_point + .insert(*id, Deserialize::deserialize(record)?); + } + "EDGE_CURVE" => { + self.edge_curve + .insert(*id, Deserialize::deserialize(record)?); + } + "ORIENTED_EDGE" => { + if let Parameter::List(params) = &record.parameter { + if params.len() == 5 { + self.oriented_edge.insert( + *id, + OrientedEdgeHolder { + label: Deserialize::deserialize(¶ms[0])?, + edge_element: Deserialize::deserialize(¶ms[3])?, + orientation: Deserialize::deserialize(¶ms[4])?, + }, + ); + } + } + } + "EDGE_LOOP" => { + self.edge_loop + .insert(*id, Deserialize::deserialize(record)?); + } + "FACE_BOUND" => { + self.face_bound + .insert(*id, Deserialize::deserialize(record)?); + } + "FACE_OUTER_BOUND" => { + self.face_bound + .insert(*id, Deserialize::deserialize(&record.parameter)?); + } + "FACE_SURFACE" => { + self.face_surface + .insert(*id, Deserialize::deserialize(record)?); + } + "ADVANCED_FACE" => { + self.face_surface + .insert(*id, Deserialize::deserialize(&record.parameter)?); + } + "ORIENTED_FACE" => { + if let Parameter::List(params) = &record.parameter { + if params.len() == 4 { + self.oriented_face.insert( + *id, + OrientedFaceHolder { + label: Deserialize::deserialize(¶ms[0])?, + face_element: Deserialize::deserialize(¶ms[2])?, + orientation: Deserialize::deserialize(¶ms[3])?, + }, + ); + } + } + } + "OPEN_SHELL" => { + self.shell + .insert(*id, Deserialize::deserialize(&record.parameter)?); + } + "CLOSED_SHELL" => { + self.shell + .insert(*id, Deserialize::deserialize(&record.parameter)?); + } + "ORIENTED_OPEN_SHELL" => { + if let Parameter::List(params) = &record.parameter { + if params.len() == 4 { + self.oriented_shell.insert( + *id, + OrientedShellHolder { + label: Deserialize::deserialize(¶ms[0])?, + shell_element: Deserialize::deserialize(¶ms[2])?, + orientation: Deserialize::deserialize(¶ms[3])?, + }, + ); + } + } + } + "ORIENTED_CLOSED_SHELL" => { + if let Parameter::List(params) = &record.parameter { + if params.len() == 4 { + self.oriented_shell.insert( + *id, + OrientedShellHolder { + label: Deserialize::deserialize(¶ms[0])?, + shell_element: Deserialize::deserialize(¶ms[2])?, + orientation: Deserialize::deserialize(¶ms[3])?, + }, + ); + } + } + } + "DEFINITIONAL_REPRESENTATION" => { + if let Parameter::List(params) = &record.parameter { + if params.len() == 3 { + self.definitional_representation.insert( + *id, + DefinitionalRepresentationHolder { + label: Deserialize::deserialize(¶ms[0])?, + representation_item: Deserialize::deserialize(¶ms[1])?, + contex_of_items: match ¶ms[2] { + Parameter::Ref(x) => PlaceHolder::Ref(x.clone()), + _ => PlaceHolder::Owned(DummyHolder { + record: format!("{:?}", params[2]), + is_simple: true, + }), + }, + }, + ); + } + } + } + _ => { + self.dummy.insert( + *id, + DummyHolder { + record: format!("{record:?}"), + is_simple: true, + }, + ); + } + }, + EntityInstance::Complex { + id, + subsuper: SubSuperRecord(records), + } => { + use NonRationalBSplineCurveHolder as NRBC; + use NonRationalBSplineSurfaceHolder as NRBS; + if records.len() == 7 { + match ( + records[0].name.as_str(), + &records[0].parameter, + records[1].name.as_str(), + &records[1].parameter, + records[2].name.as_str(), + &records[2].parameter, + records[3].name.as_str(), + &records[3].parameter, + records[4].name.as_str(), + &records[4].parameter, + records[5].name.as_str(), + &records[5].parameter, + records[6].name.as_str(), + &records[6].parameter, + ) { + ( + "BOUNDED_CURVE", + _, + "B_SPLINE_CURVE", + Parameter::List(bsp_params), + "B_SPLINE_CURVE_WITH_KNOTS", + Parameter::List(knots_params), + "CURVE", + _, + "GEOMETRIC_REPRESENTATION_ITEM", + _, + "RATIONAL_B_SPLINE_CURVE", + Parameter::List(weights), + "REPRESENTATION_ITEM", + Parameter::List(label), + ) => { + let mut params = label.clone(); + params.extend(bsp_params.clone()); + params.extend(knots_params.clone()); + self.rational_b_spline_curve.insert( + *id, + RationalBSplineCurveHolder { + non_rational_b_spline_curve: PlaceHolder::Owned( + NRBC::BSplineCurveWithKnots(Deserialize::deserialize( + &Parameter::List(params), + )?), + ), + weights_data: Deserialize::deserialize(&weights[0])?, + }, + ); + } + ( + "BEZIER_CURVE", + _, + "BOUNDED_CURVE", + _, + "B_SPLINE_CURVE", + Parameter::List(bsp_params), + "CURVE", + _, + "GEOMETRIC_REPRESENTATION_ITEM", + _, + "RATIONAL_B_SPLINE_CURVE", + Parameter::List(weights), + "REPRESENTATION_ITEM", + Parameter::List(label), + ) => { + let mut params = label.clone(); + params.extend(bsp_params.clone()); + self.rational_b_spline_curve.insert( + *id, + RationalBSplineCurveHolder { + non_rational_b_spline_curve: PlaceHolder::Owned( + NRBC::BezierCurve(Deserialize::deserialize( + &Parameter::List(params), + )?), + ), + weights_data: Deserialize::deserialize(&weights[0])?, + }, + ); + } + ( + "BOUNDED_CURVE", + _, + "B_SPLINE_CURVE", + Parameter::List(bsp_params), + "CURVE", + _, + "GEOMETRIC_REPRESENTATION_ITEM", + _, + "QUASI_UNIFORM_CURVE", + _, + "RATIONAL_B_SPLINE_CURVE", + Parameter::List(weights), + "REPRESENTATION_ITEM", + Parameter::List(label), + ) => { + let mut params = vec![label[0].clone()]; + params.extend(bsp_params.iter().cloned()); + self.rational_b_spline_curve.insert( + *id, + RationalBSplineCurveHolder { + non_rational_b_spline_curve: PlaceHolder::Owned( + NRBC::QuasiUniformCurve(Deserialize::deserialize( + &Parameter::List(params), + )?), + ), + weights_data: Deserialize::deserialize(&weights[0])?, + }, + ); + } + ( + "BOUNDED_CURVE", + _, + "B_SPLINE_CURVE", + Parameter::List(bsp_params), + "CURVE", + _, + "GEOMETRIC_REPRESENTATION_ITEM", + _, + "RATIONAL_B_SPLINE_CURVE", + Parameter::List(weights), + "REPRESENTATION_ITEM", + Parameter::List(label), + "UNIFORM_CURVE", + _, + ) => { + let mut params = vec![label[0].clone()]; + params.extend(bsp_params.iter().cloned()); + self.rational_b_spline_curve.insert( + *id, + RationalBSplineCurveHolder { + non_rational_b_spline_curve: PlaceHolder::Owned( + NRBC::UniformCurve(Deserialize::deserialize( + &Parameter::List(params), + )?), + ), + weights_data: Deserialize::deserialize(&weights[0])?, + }, + ); + } + ( + "BOUNDED_SURFACE", + _, + "B_SPLINE_SURFACE", + Parameter::List(bsp_params), + "B_SPLINE_SURFACE_WITH_KNOTS", + Parameter::List(knots_params), + "GEOMETRIC_REPRESENTATION_ITEM", + _, + "RATIONAL_B_SPLINE_SURFACE", + Parameter::List(weights), + "REPRESENTATION_ITEM", + Parameter::List(label), + "SURFACE", + _, + ) => { + let mut params = label.clone(); + params.extend(bsp_params.clone()); + params.extend(knots_params.clone()); + self.rational_b_spline_surface.insert( + *id, + RationalBSplineSurfaceHolder { + non_rational_b_spline_surface: PlaceHolder::Owned( + NRBS::BSplineSurfaceWithKnots(Deserialize::deserialize( + &Parameter::List(params), + )?), + ), + weights_data: Deserialize::deserialize(&weights[0])?, + }, + ); + } + ( + "BEZIER_SURFACE", + _, + "BOUNDED_SURFACE", + _, + "B_SPLINE_SURFACE", + Parameter::List(bsp_params), + "GEOMETRIC_REPRESENTATION_ITEM", + _, + "RATIONAL_B_SPLINE_SURFACE", + Parameter::List(weights), + "REPRESENTATION_ITEM", + Parameter::List(label), + "SURFACE", + _, + ) => { + let mut params = label.clone(); + params.extend(bsp_params.clone()); + self.rational_b_spline_surface.insert( + *id, + RationalBSplineSurfaceHolder { + non_rational_b_spline_surface: PlaceHolder::Owned( + NRBS::BezierSurface(Deserialize::deserialize( + &Parameter::List(params), + )?), + ), + weights_data: Deserialize::deserialize(&weights[0])?, + }, + ); + } + ( + "BOUNDED_SURFACE", + _, + "B_SPLINE_SURFACE", + Parameter::List(bsp_params), + "GEOMETRIC_REPRESENTATION_ITEM", + _, + "QUASI_UNIFORM_SURFACE", + _, + "RATIONAL_B_SPLINE_SURFACE", + Parameter::List(weights), + "REPRESENTATION_ITEM", + Parameter::List(label), + "SURFACE", + _, + ) => { + let mut params = label.clone(); + params.extend(bsp_params.clone()); + self.rational_b_spline_surface.insert( + *id, + RationalBSplineSurfaceHolder { + non_rational_b_spline_surface: PlaceHolder::Owned( + NRBS::QuasiUniformSurface(Deserialize::deserialize( + &Parameter::List(params), + )?), + ), + weights_data: Deserialize::deserialize(&weights[0])?, + }, + ); + } + ( + "BOUNDED_SURFACE", + _, + "B_SPLINE_SURFACE", + Parameter::List(bsp_params), + "GEOMETRIC_REPRESENTATION_ITEM", + _, + "RATIONAL_B_SPLINE_SURFACE", + Parameter::List(weights), + "REPRESENTATION_ITEM", + Parameter::List(label), + "SURFACE", + _, + "UNIFORM_SURFACE", + _, + ) => { + let mut params = label.clone(); + params.extend(bsp_params.clone()); + self.rational_b_spline_surface.insert( + *id, + RationalBSplineSurfaceHolder { + non_rational_b_spline_surface: PlaceHolder::Owned( + NRBS::UniformSurface(Deserialize::deserialize( + &Parameter::List(params), + )?), + ), + weights_data: Deserialize::deserialize(&weights[0])?, + }, + ); + } + _ => { + self.dummy.insert( + *id, + DummyHolder { + record: format!("{records:?}"), + is_simple: false, + }, + ); + } + } + } else { + self.dummy.insert( + *id, + DummyHolder { + record: format!("{records:?}"), + is_simple: false, + }, + ); + } + } + } + Ok(()) + } + #[inline(always)] + pub fn from_data_section(data_section: &DataSection) -> Table { + Table::from_iter(&data_section.entities) + } + #[inline(always)] + pub fn from_step(step_str: &str) -> Option { + let exchange = ruststep::parser::parse(step_str).ok()?; + Some(Table::from_data_section(&exchange.data[0])) + } +} + +impl<'a> FromIterator<&'a EntityInstance> for Table { + fn from_iter>(iter: I) -> Table { + let mut res = Table::default(); + iter.into_iter().for_each(|instance| { + res.push_instance(instance) + .unwrap_or_else(|e| eprintln!("{e}")) + }); + res + } +} + +/// Undefined structures are parsed into this. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = dummy)] +#[holder(generate_deserialize)] +pub struct Dummy { + pub record: String, + pub is_simple: bool, +} + +/// `cartesian_point` +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = cartesian_point)] +#[holder(generate_deserialize)] +pub struct CartesianPoint { + pub label: String, + pub coordinates: Vec, +} +impl From<&CartesianPoint> for Point2 { + #[inline(always)] + fn from(pt: &CartesianPoint) -> Self { + let pt = &pt.coordinates; + match pt.len() { + 0 => Point2::origin(), + 1 => Point2::new(pt[0], 0.0), + _ => Point2::new(pt[0], pt[1]), + } + } +} +impl From<&CartesianPoint> for Point3 { + #[inline(always)] + fn from(pt: &CartesianPoint) -> Self { + let pt = &pt.coordinates; + match pt.len() { + 0 => Point3::origin(), + 1 => Point3::new(pt[0], 0.0, 0.0), + 2 => Point3::new(pt[0], pt[1], 0.0), + _ => Point3::new(pt[0], pt[1], pt[2]), + } + } +} + +/// `direction` +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = direction)] +#[holder(generate_deserialize)] +pub struct Direction { + pub label: String, + pub direction_ratios: Vec, +} +impl From<&Direction> for Vector2 { + #[inline(always)] + fn from(dir: &Direction) -> Self { + let dir = &dir.direction_ratios; + match dir.len() { + 0 => Vector2::zero(), + 1 => Vector2::new(dir[0], 0.0), + _ => Vector2::new(dir[0], dir[1]), + } + } +} +impl From<&Direction> for Vector3 { + #[inline(always)] + fn from(dir: &Direction) -> Self { + let dir = &dir.direction_ratios; + match dir.len() { + 0 => Vector3::zero(), + 1 => Vector3::new(dir[0], 0.0, 0.0), + 2 => Vector3::new(dir[0], dir[1], 0.0), + _ => Vector3::new(dir[0], dir[1], dir[2]), + } + } +} + +/// `vector` +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = vector)] +#[holder(generate_deserialize)] +pub struct Vector { + pub label: String, + #[holder(use_place_holder)] + pub orientation: Direction, + pub magnitude: f64, +} +impl From<&Vector> for Vector2 { + #[inline(always)] + fn from(vec: &Vector) -> Self { Self::from(&vec.orientation) * vec.magnitude } +} +impl From<&Vector> for Vector3 { + #[inline(always)] + fn from(vec: &Vector) -> Self { Self::from(&vec.orientation) * vec.magnitude } +} + +/// `placement` +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = placement)] +#[holder(generate_deserialize)] +pub struct Placement { + pub label: String, + #[holder(use_place_holder)] + pub location: CartesianPoint, +} +impl From<&Placement> for Point2 { + #[inline(always)] + fn from(p: &Placement) -> Self { Self::from(&p.location) } +} +impl From<&Placement> for Point3 { + #[inline(always)] + fn from(p: &Placement) -> Self { Self::from(&p.location) } +} + +/// `axis1_placement` +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = axis1_placement)] +#[holder(generate_deserialize)] +pub struct Axis1Placement { + pub label: String, + #[holder(use_place_holder)] + pub location: CartesianPoint, + #[holder(use_place_holder)] + pub direction: Option, +} + +impl Axis1Placement { + pub fn direction(&self) -> Vector3 { + self.direction + .as_ref() + .map(Vector3::from) + .unwrap_or_else(Vector3::unit_z) + } +} + +/// `axis2_placement` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(generate_deserialize)] +pub enum Axis2Placement { + #[holder(use_place_holder)] + Axis2Placement2d(Axis2Placement2d), + #[holder(use_place_holder)] + Axis2Placement3d(Axis2Placement3d), +} + +impl TryFrom<&Axis2Placement> for Matrix3 { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(axis: &Axis2Placement) -> Result { + use Axis2Placement::*; + match axis { + Axis2Placement2d(axis) => Ok(Matrix3::from(axis)), + Axis2Placement3d(_) => Err("This is not a 2D axis placement.".into()), + } + } +} +impl TryFrom<&Axis2Placement> for Matrix4 { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(axis: &Axis2Placement) -> Result { + use Axis2Placement::*; + match axis { + Axis2Placement2d(_) => Err("This is not a 3D axis placement.".into()), + Axis2Placement3d(axis) => Ok(Matrix4::from(axis)), + } + } +} + +/// `axis2_placement_2d` +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = axis2_placement_2d)] +#[holder(generate_deserialize)] +pub struct Axis2Placement2d { + pub label: String, + #[holder(use_place_holder)] + pub location: CartesianPoint, + #[holder(use_place_holder)] + pub ref_direction: Option, +} + +impl From<&Axis2Placement2d> for Matrix3 { + #[inline(always)] + fn from(axis: &Axis2Placement2d) -> Self { + let z = Point2::from(&axis.location); + let x = match &axis.ref_direction { + Some(axis) => Vector2::from(axis), + None => Vector2::unit_x(), + }; + let y = Vector2::new(-x.y, x.x); + Matrix3::from_cols(x.extend(0.0), y.extend(0.0), z.to_vec().extend(1.0)) + } +} + +/// `axis2_placement_3d` +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = axis2_placement_3d)] +#[holder(generate_deserialize)] +pub struct Axis2Placement3d { + pub label: String, + #[holder(use_place_holder)] + pub location: CartesianPoint, + #[holder(use_place_holder)] + pub axis: Option, + #[holder(use_place_holder)] + pub ref_direction: Option, +} + +impl From<&Axis2Placement3d> for Matrix4 { + #[inline(always)] + fn from(axis: &Axis2Placement3d) -> Matrix4 { + let w = Point3::from(&axis.location); + let z = match &axis.axis { + Some(axis) => Vector3::from(axis), + None => Vector3::unit_z(), + }; + let x = match &axis.ref_direction { + Some(axis) => Vector3::from(axis), + None => Vector3::unit_x(), + }; + let x = (x - x.dot(z) * z).normalize(); + let y = z.cross(x); + Matrix4::from_cols( + x.extend(0.0), + y.extend(0.0), + z.extend(0.0), + w.to_vec().extend(1.0), + ) + } +} + +/// `curve` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(generate_deserialize)] +pub enum CurveAny { + #[holder(use_place_holder)] + Line(Box), + #[holder(use_place_holder)] + BoundedCurve(Box), + #[holder(use_place_holder)] + Conic(Box), + #[holder(use_place_holder)] + Pcurve(Box), + #[holder(use_place_holder)] + SurfaceCurve(Box), +} + +impl TryFrom<&CurveAny> for Curve2D { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(curve: &CurveAny) -> Result { + use CurveAny::*; + Ok(match curve { + Line(line) => Self::Line(line.as_ref().into()), + BoundedCurve(b) => b.as_ref().try_into()?, + Conic(curve) => Self::Conic(curve.as_ref().try_into()?), + Pcurve(_) => return Err("Pcurves cannot be parsed to 2D curves.".into()), + SurfaceCurve(_) => return Err("Surface curves cannot be parsed to 2D curves.".into()), + }) + } +} + +impl TryFrom<&CurveAny> for Curve3D { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(curve: &CurveAny) -> Result { + use CurveAny::*; + Ok(match curve { + Line(line) => Self::Line(line.as_ref().into()), + BoundedCurve(b) => b.as_ref().try_into()?, + Conic(curve) => Self::Conic(curve.as_ref().try_into()?), + Pcurve(c) => Self::PCurve(c.as_ref().try_into()?), + SurfaceCurve(c) => c.as_ref().try_into()?, + }) + } +} + +/// `line` +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = line)] +#[holder(generate_deserialize)] +pub struct Line { + pub label: String, + #[holder(use_place_holder)] + pub pnt: CartesianPoint, + #[holder(use_place_holder)] + pub dir: Vector, +} +impl<'a, P> From<&'a Line> for truck::Line

+where + P: EuclideanSpace + From<&'a CartesianPoint>, + P::Diff: From<&'a Vector>, +{ + #[inline(always)] + fn from(line: &'a Line) -> Self { + let p = P::from(&line.pnt); + let q = p + P::Diff::from(&line.dir); + Self(p, q) + } +} + +/// `bounded_curve` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(generate_deserialize)] +pub enum BoundedCurveAny { + #[holder(use_place_holder)] + Polyline(Box), + #[holder(use_place_holder)] + BSplineCurve(Box), +} + +impl TryFrom<&BoundedCurveAny> for Curve2D { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(value: &BoundedCurveAny) -> Result { + use BoundedCurveAny::*; + Ok(match value { + Polyline(x) => Self::Polyline(x.as_ref().into()), + BSplineCurve(x) => x.as_ref().try_into()?, + }) + } +} + +impl TryFrom<&BoundedCurveAny> for Curve3D { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(value: &BoundedCurveAny) -> Result { + use BoundedCurveAny::*; + Ok(match value { + Polyline(x) => Self::Polyline(x.as_ref().into()), + BSplineCurve(x) => x.as_ref().try_into()?, + }) + } +} + +/// `polyline` +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = polyline)] +#[holder(generate_deserialize)] +pub struct Polyline { + pub label: String, + #[holder(use_place_holder)] + pub points: Vec, +} +impl<'a, P: From<&'a CartesianPoint>> From<&'a Polyline> for PolylineCurve

{ + #[inline(always)] + fn from(poly: &'a Polyline) -> Self { Self(poly.points.iter().map(|pt| P::from(pt)).collect()) } +} + +/// `b_spline_curve_form` +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum BSplineCurveForm { + PolylineForm, + CircularArc, + EllipticArc, + ParabolicArc, + HyperbolicArc, + Unspecified, +} + +/// `knot_type` +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum KnotType { + UniformKnots, + Unspecified, + QuasiUniformKnots, + PiecewiseBezierKnots, +} + +/// `b_spline_curve_with_knots` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = b_spline_curve_with_knots)] +#[holder(generate_deserialize)] +pub struct BSplineCurveWithKnots { + pub label: String, + pub degree: i64, + #[holder(use_place_holder)] + pub control_points_list: Vec, + pub curve_form: BSplineCurveForm, + pub closed_curve: Logical, + pub self_intersect: Logical, + pub knot_multiplicities: Vec, + pub knots: Vec, + pub knot_spec: KnotType, +} +impl From<&'a CartesianPoint>> TryFrom<&BSplineCurveWithKnots> for BSplineCurve

{ + type Error = StepConvertingError; + #[inline(always)] + fn try_from(curve: &BSplineCurveWithKnots) -> Result { + let knots = curve.knots.clone(); + let multi = curve + .knot_multiplicities + .iter() + .map(|n| *n as usize) + .collect(); + let knots = KnotVec::from_single_multi(knots, multi).unwrap(); + let ctrpts = curve.control_points_list.iter().map(Into::into).collect(); + Ok(Self::try_new(knots, ctrpts)?) + } +} + +/// `bezier_curve` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = bezier_curve)] +#[holder(generate_deserialize)] +pub struct BezierCurve { + pub label: String, + pub degree: i64, + #[holder(use_place_holder)] + pub control_points_list: Vec, + pub curve_form: BSplineCurveForm, + pub closed_curve: Logical, + pub self_intersect: Logical, +} +impl From<&'a CartesianPoint>> TryFrom<&BezierCurve> for BSplineCurve

{ + type Error = StepConvertingError; + #[inline(always)] + fn try_from(curve: &BezierCurve) -> Result { + let degree = curve.degree as usize; + let knots = KnotVec::bezier_knot(degree); + let ctrpts = curve.control_points_list.iter().map(Into::into).collect(); + Ok(Self::try_new(knots, ctrpts)?) + } +} + +/// `quasi_uniform_curve` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = quasi_uniform_curve)] +#[holder(generate_deserialize)] +pub struct QuasiUniformCurve { + pub label: String, + pub degree: i64, + #[holder(use_place_holder)] + pub control_points_list: Vec, + pub curve_form: BSplineCurveForm, + pub closed_curve: Logical, + pub self_intersect: Logical, +} +impl From<&'a CartesianPoint>> TryFrom<&QuasiUniformCurve> for BSplineCurve

{ + type Error = StepConvertingError; + #[inline(always)] + fn try_from(curve: &QuasiUniformCurve) -> Result { + let knots = quasi_uniform_knots(curve.control_points_list.len(), curve.degree as usize); + let ctrpts = curve.control_points_list.iter().map(Into::into).collect(); + Ok(Self::try_new(knots, ctrpts)?) + } +} + +fn quasi_uniform_knots(num_ctrl: usize, degree: usize) -> KnotVec { + let division = num_ctrl - degree; + let mut knots = KnotVec::uniform_knot(degree, division); + knots.transform(division as f64, 0.0); + knots +} + +/// `uniform_curve` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = uniform_curve)] +#[holder(generate_deserialize)] +pub struct UniformCurve { + pub label: String, + pub degree: i64, + #[holder(use_place_holder)] + pub control_points_list: Vec, + pub curve_form: BSplineCurveForm, + pub closed_curve: Logical, + pub self_intersect: Logical, +} +impl From<&'a CartesianPoint>> TryFrom<&UniformCurve> for BSplineCurve

{ + type Error = StepConvertingError; + #[inline(always)] + fn try_from(curve: &UniformCurve) -> Result { + let knots = uniform_knots(curve.control_points_list.len(), curve.degree as usize)?; + let ctrpts = curve.control_points_list.iter().map(Into::into).collect(); + Ok(Self::try_new(knots, ctrpts)?) + } +} + +fn uniform_knots(num_ctrl: usize, degree: usize) -> truck::Result { + KnotVec::try_from( + (0..degree + num_ctrl + 1) + .map(|i| i as f64 - degree as f64) + .collect::>(), + ) +} + +/// Entity that does not exist in AP042. +/// Curve before rationalization of [`RationalBSplineCurve`] defined by a complex entity +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(generate_deserialize)] +pub enum NonRationalBSplineCurve { + #[holder(use_place_holder)] + BSplineCurveWithKnots(BSplineCurveWithKnots), + #[holder(use_place_holder)] + BezierCurve(BezierCurve), + #[holder(use_place_holder)] + QuasiUniformCurve(QuasiUniformCurve), + #[holder(use_place_holder)] + UniformCurve(UniformCurve), +} + +impl From<&'a CartesianPoint>> TryFrom<&NonRationalBSplineCurve> for BSplineCurve

{ + type Error = StepConvertingError; + #[inline(always)] + fn try_from(curve: &NonRationalBSplineCurve) -> Result { + use NonRationalBSplineCurve::*; + match curve { + BSplineCurveWithKnots(x) => x.try_into(), + BezierCurve(x) => x.try_into(), + QuasiUniformCurve(x) => x.try_into(), + UniformCurve(x) => x.try_into(), + } + } +} + +/// `rational_b_spline_curve` as complex entity +/// +/// This struct is an ad hoc implementation that differs from the definition by EXPRESS: +/// in AP042, rationalized curves are defined as complex entities, +/// but here the curves before rationalization are held as internal variables. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = rational_b_spline_curve)] +#[holder(generate_deserialize)] +pub struct RationalBSplineCurve { + #[holder(use_place_holder)] + pub non_rational_b_spline_curve: NonRationalBSplineCurve, + pub weights_data: Vec, +} +impl TryFrom<&RationalBSplineCurve> for NurbsCurve +where + V: Homogeneous, + V::Point: for<'a> From<&'a CartesianPoint>, +{ + type Error = StepConvertingError; + #[inline(always)] + fn try_from(curve: &RationalBSplineCurve) -> Result { + Ok(Self::try_from_bspline_and_weights( + BSplineCurve::try_from(&curve.non_rational_b_spline_curve)?, + curve.weights_data.clone(), + )?) + } +} + +/// b_spline_curve +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(generate_deserialize)] +pub enum BSplineCurveAny { + #[holder(use_place_holder)] + NonRationalBSplineCurve(Box), + #[holder(use_place_holder)] + RationalBSplineCurve(Box), +} + +impl TryFrom<&BSplineCurveAny> for Curve2D { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(value: &BSplineCurveAny) -> Result { + use BSplineCurveAny::*; + Ok(match value { + NonRationalBSplineCurve(bsp) => Self::BSplineCurve(bsp.as_ref().try_into()?), + RationalBSplineCurve(bsp) => Self::NurbsCurve(bsp.as_ref().try_into()?), + }) + } +} + +impl TryFrom<&BSplineCurveAny> for Curve3D { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(value: &BSplineCurveAny) -> Result { + use BSplineCurveAny::*; + Ok(match value { + NonRationalBSplineCurve(bsp) => Self::BSplineCurve(bsp.as_ref().try_into()?), + RationalBSplineCurve(bsp) => Self::NurbsCurve(bsp.as_ref().try_into()?), + }) + } +} + +/// `conic` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(generate_deserialize)] +pub enum Conic { + #[holder(use_place_holder)] + Circle(Circle), + #[holder(use_place_holder)] + Ellipse(Ellipse), + #[holder(use_place_holder)] + Hyperbola(Hyperbola), + #[holder(use_place_holder)] + Parabola(Parabola), +} + +impl TryFrom<&Conic> for Conic2D { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(value: &Conic) -> Result { + Ok(match value { + Conic::Circle(value) => Conic2D::Ellipse(value.try_into()?), + Conic::Ellipse(value) => Conic2D::Ellipse(value.try_into()?), + Conic::Hyperbola(value) => Conic2D::Hyperbola(value.try_into()?), + Conic::Parabola(value) => Conic2D::Parabola(value.try_into()?), + }) + } +} + +impl TryFrom<&Conic> for Conic3D { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(value: &Conic) -> Result { + Ok(match value { + Conic::Circle(value) => Conic3D::Ellipse(value.try_into()?), + Conic::Ellipse(value) => Conic3D::Ellipse(value.try_into()?), + Conic::Hyperbola(value) => Conic3D::Hyperbola(value.try_into()?), + Conic::Parabola(value) => Conic3D::Parabola(value.try_into()?), + }) + } +} + +/// `circle` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = circle)] +#[holder(generate_deserialize)] +pub struct Circle { + pub label: String, + #[holder(use_place_holder)] + pub position: Axis2Placement, + pub radius: f64, +} + +impl TryFrom<&Circle> for alias::Ellipse { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(circle: &Circle) -> Result { + let transform = Matrix3::try_from(&circle.position)? * Matrix3::from_scale(circle.radius); + Ok( + Processor::new(truck::TrimmedCurve::new(UnitCircle::new(), (0.0, 2.0 * PI))) + .transformed(transform), + ) + } +} + +impl TryFrom<&Circle> for alias::Ellipse { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(circle: &Circle) -> Result { + let transform = Matrix4::try_from(&circle.position)? * Matrix4::from_scale(circle.radius); + Ok( + Processor::new(truck::TrimmedCurve::new(UnitCircle::new(), (0.0, 2.0 * PI))) + .transformed(transform), + ) + } +} + +/// `ellipse` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = ellipse)] +#[holder(generate_deserialize)] +pub struct Ellipse { + pub label: String, + #[holder(use_place_holder)] + pub position: Axis2Placement, + pub semi_axis_1: f64, + pub semi_axis_2: f64, +} + +impl TryFrom<&Ellipse> for alias::Ellipse { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(ellipse: &Ellipse) -> Result { + let (r0, r1) = (ellipse.semi_axis_1, ellipse.semi_axis_2); + let transform = + Matrix3::try_from(&ellipse.position)? * Matrix3::from_nonuniform_scale(r0, r1); + Ok( + Processor::new(truck::TrimmedCurve::new(UnitCircle::new(), (0.0, 2.0 * PI))) + .transformed(transform), + ) + } +} + +impl TryFrom<&Ellipse> for alias::Ellipse { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(ellipse: &Ellipse) -> Result { + let (r0, r1) = (ellipse.semi_axis_1, ellipse.semi_axis_2); + let transform = Matrix4::try_from(&ellipse.position)? + * Matrix4::from_nonuniform_scale(r0, r1, f64::min(r0, r1)); + Ok( + Processor::new(truck::TrimmedCurve::new(UnitCircle::new(), (0.0, 2.0 * PI))) + .transformed(transform), + ) + } +} + +/// `hyperbola` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = hyperbola)] +#[holder(generate_deserialize)] +pub struct Hyperbola { + pub label: String, + #[holder(use_place_holder)] + pub position: Axis2Placement, + pub semi_axis: f64, + pub semi_imag_axis: f64, +} + +impl TryFrom<&Hyperbola> for alias::Hyperbola { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(hyperbola: &Hyperbola) -> Result { + let (r0, r1) = (hyperbola.semi_axis, hyperbola.semi_imag_axis); + let transform = + Matrix3::try_from(&hyperbola.position)? * Matrix3::from_nonuniform_scale(r0, r1); + Ok( + Processor::new(truck::TrimmedCurve::new(UnitHyperbola::new(), (-1.0, 1.0))) + .transformed(transform), + ) + } +} + +impl TryFrom<&Hyperbola> for alias::Hyperbola { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(hyperbola: &Hyperbola) -> Result { + let (r0, r1) = (hyperbola.semi_axis, hyperbola.semi_imag_axis); + let transform = Matrix4::try_from(&hyperbola.position)? + * Matrix4::from_nonuniform_scale(r0, r1, f64::min(r0, r1)); + Ok( + Processor::new(truck::TrimmedCurve::new(UnitHyperbola::new(), (-1.0, 1.0))) + .transformed(transform), + ) + } +} + +/// `parabola` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = parabola)] +#[holder(generate_deserialize)] +pub struct Parabola { + pub label: String, + #[holder(use_place_holder)] + pub position: Axis2Placement, + pub focal_dist: f64, +} + +impl TryFrom<&Parabola> for alias::Parabola { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(parabola: &Parabola) -> Result { + let transform = + Matrix3::try_from(¶bola.position)? * Matrix3::from_scale(parabola.focal_dist); + Ok( + Processor::new(truck::TrimmedCurve::new(UnitParabola::new(), (-1.0, 1.0))) + .transformed(transform), + ) + } +} + +impl TryFrom<&Parabola> for alias::Parabola { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(parabola: &Parabola) -> Result { + let transform = + Matrix4::try_from(¶bola.position)? * Matrix4::from_scale(parabola.focal_dist); + Ok( + Processor::new(truck::TrimmedCurve::new(UnitParabola::new(), (-1.0, 1.0))) + .transformed(transform), + ) + } +} + +/// `definitional_representation` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = definitional_representation)] +#[holder(generate_deserialize)] +pub struct DefinitionalRepresentation { + label: String, + #[holder(use_place_holder)] + representation_item: Vec, + #[holder(use_place_holder)] + contex_of_items: Dummy, +} + +/// `pcurve` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = pcurve)] +#[holder(generate_deserialize)] +pub struct Pcurve { + label: String, + #[holder(use_place_holder)] + basis_surface: SurfaceAny, + #[holder(use_place_holder)] + reference_to_curve: DefinitionalRepresentation, +} + +impl TryFrom<&Pcurve> for PCurve { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(value: &Pcurve) -> Result { + let surface: Surface = (&value.basis_surface).try_into()?; + let curve: Curve2D = value + .reference_to_curve + .representation_item + .first() + .ok_or("no representation item")? + .try_into()?; + Ok(alias::PCurve::new(Box::new(curve), Box::new(surface))) + } +} + +/// `pcurve_or_surface` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(generate_deserialize)] +pub enum PcurveOrSurface { + #[holder(use_place_holder)] + Pcurve(Box), + #[holder(use_place_holder)] + Surface(Box), +} + +/// `preferred_surface_representation` +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +pub enum PreferredSurfaceCurveRepresentation { + Curve3D, + PcurveS1, + PcurveS2, +} + +#[test] +fn deserialize_pscr() { + let (_, p) = ruststep::parser::exchange::parameter(".PCURVE_S1.").unwrap(); + let x = PreferredSurfaceCurveRepresentation::deserialize(&p).unwrap(); + assert!(matches!(x, PreferredSurfaceCurveRepresentation::PcurveS1)); + let (_, p) = ruststep::parser::exchange::parameter(".PCURVE_S2.").unwrap(); + let x = PreferredSurfaceCurveRepresentation::deserialize(&p).unwrap(); + assert!(matches!(x, PreferredSurfaceCurveRepresentation::PcurveS2)); +} + +/// `surface_curve` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = surface_curve)] +#[holder(generate_deserialize)] +pub struct SurfaceCurve { + label: String, + #[holder(use_place_holder)] + curve_3d: CurveAny, + #[holder(use_place_holder)] + associated_geometry: Vec, + master_representation: PreferredSurfaceCurveRepresentation, +} + +impl TryFrom<&SurfaceCurve> for Curve3D { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(value: &SurfaceCurve) -> Result { + use PreferredSurfaceCurveRepresentation as PSCR; + match &value.master_representation { + PSCR::Curve3D => Ok((&value.curve_3d).try_into()?), + PSCR::PcurveS1 => { + if let Some(PcurveOrSurface::Pcurve(x)) = value.associated_geometry.first() { + Ok(Self::PCurve(x.as_ref().try_into()?)) + } else { + Err("The 0-indexed associated geometry is nothing or not PCURVE.".into()) + } + } + PSCR::PcurveS2 => { + if let Some(PcurveOrSurface::Pcurve(x)) = value.associated_geometry.get(1) { + Ok(Self::PCurve(x.as_ref().try_into()?)) + } else { + Err("The 1-indexed associated geometry is nothing or not PCURVE.".into()) + } + } + } + } +} + +/// `surface` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(generate_deserialize)] +pub enum SurfaceAny { + #[holder(use_place_holder)] + ElementarySurface(Box), + #[holder(use_place_holder)] + BSplineSurface(Box), + #[holder(use_place_holder)] + SweptSurface(Box), +} + +impl TryFrom<&SurfaceAny> for Surface { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(x: &SurfaceAny) -> Result { + use SurfaceAny::*; + Ok(match x { + ElementarySurface(x) => Self::ElementarySurface(Box::new(x.as_ref().into())), + BSplineSurface(x) => x.as_ref().try_into()?, + SweptSurface(x) => Self::SweptCurve(Box::new(x.as_ref().try_into()?)), + }) + } +} + +/// `elementary_surface` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(generate_deserialize)] +pub enum ElementarySurfaceAny { + #[holder(use_place_holder)] + Plane(Box), + #[holder(use_place_holder)] + SphericalSurface(Box), + #[holder(use_place_holder)] + CylindricalSurface(Box), + #[holder(use_place_holder)] + ToroidalSurface(Box), + #[holder(use_place_holder)] + ConicalSurface(Box), +} + +impl From<&ElementarySurfaceAny> for ElementarySurface { + #[inline(always)] + fn from(value: &ElementarySurfaceAny) -> Self { + use ElementarySurfaceAny::*; + match value { + Plane(x) => Self::Plane(x.as_ref().into()), + SphericalSurface(x) => Self::Sphere(x.as_ref().into()), + CylindricalSurface(x) => Self::CylindricalSurface(x.as_ref().into()), + ToroidalSurface(x) => Self::ToroidalSurface(x.as_ref().into()), + ConicalSurface(x) => Self::ConicalSurface(x.as_ref().into()), + } + } +} + +/// `plane` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = plane)] +#[holder(generate_deserialize)] +pub struct Plane { + label: String, + #[holder(use_place_holder)] + position: Axis2Placement3d, +} + +impl From<&Plane> for truck::Plane { + #[inline(always)] + fn from(plane: &Plane) -> Self { + let mat = Matrix4::from(&plane.position); + let o = Point3::from_homogeneous(mat[3]); + let p = o + mat[0].truncate(); + let q = o + mat[1].truncate(); + Self::new(o, p, q) + } +} + +/// `spherical_surface` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = spherical_surface)] +#[holder(generate_deserialize)] +pub struct SphericalSurface { + label: String, + #[holder(use_place_holder)] + position: Axis2Placement3d, + radius: f64, +} + +impl From<&SphericalSurface> for alias::SphericalSurface { + #[inline(always)] + fn from(ss: &SphericalSurface) -> Self { + let mat = Matrix4::from(&ss.position); + let sphere = Sphere(truck::Sphere::new(Point3::origin(), ss.radius)); + Processor::new(sphere).transformed(mat) + } +} + +/// `cylindrical_surface` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = cylindrical_surface)] +#[holder(generate_deserialize)] +pub struct CylindricalSurface { + label: String, + #[holder(use_place_holder)] + position: Axis2Placement3d, + radius: f64, +} + +impl From<&CylindricalSurface> for alias::CylindricalSurface { + #[inline(always)] + fn from(cs: &CylindricalSurface) -> Self { + let mat = Matrix4::from(&cs.position); + let x = mat[0].truncate(); + let z = mat[2].truncate(); + let center = Point3::from_homogeneous(mat[3]); + let radius = cs.radius; + let p = center + x * radius; + let mut res = Processor::new(RevolutedCurve::by_revolution(Line(p, p + z), center, z)); + res.invert(); + res + } +} + +/// `toroidal_surface` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = toroidal_surface)] +#[holder(generate_deserialize)] +pub struct ToroidalSurface { + label: String, + #[holder(use_place_holder)] + position: Axis2Placement3d, + major_radius: f64, + minor_radius: f64, +} + +impl From<&ToroidalSurface> for alias::ToroidalSurface { + #[inline(always)] + fn from( + ToroidalSurface { + position, + major_radius, + minor_radius, + .. + }: &ToroidalSurface, + ) -> Self { + let mat = Matrix4::from(position); + let torus = Torus::new(Point3::origin(), *major_radius, *minor_radius); + Processor::new(torus).transformed(mat) + } +} + +/// `conical_surface` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = conical_surface)] +#[holder(generate_deserialize)] +pub struct ConicalSurface { + label: String, + #[holder(use_place_holder)] + position: Axis2Placement3d, + radius: f64, + semi_angle: f64, +} + +impl From<&ConicalSurface> for alias::ConicalSurface { + fn from( + ConicalSurface { + position, + radius, + semi_angle, + .. + }: &ConicalSurface, + ) -> Self { + let mat = Matrix4::from(position); + let p = Point3::new(*radius, 0.0, 0.0); + let v = Vector3::new(f64::tan(*semi_angle), 0.0, 1.0); + let rev = + RevolutedCurve::by_revolution(Line(p, p + v), Point3::origin(), Vector3::unit_z()); + let mut processor = Processor::new(rev); + processor.transform_by(mat); + processor.invert(); + processor + } +} + +/// `b_spline_surface` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(generate_deserialize)] +pub enum BSplineSurfaceAny { + #[holder(use_place_holder)] + NonRationalBSplineSurface(NonRationalBSplineSurface), + #[holder(use_place_holder)] + RationalBSplineSurface(RationalBSplineSurface), +} + +impl TryFrom<&BSplineSurfaceAny> for Surface { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(value: &BSplineSurfaceAny) -> Result { + use BSplineSurfaceAny::*; + Ok(match value { + NonRationalBSplineSurface(bsp) => Surface::BSplineSurface(Box::new(bsp.try_into()?)), + RationalBSplineSurface(bsp) => Surface::NurbsSurface(Box::new(bsp.try_into()?)), + }) + } +} + +/// `b_spline_surface_form` +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum BSplineSurfaceForm { + PlaneSurf, + CylindricalSurf, + ConicalSurf, + SphericalSurf, + ToroidalSurf, + SurfOfRevolution, + RuledSurf, + GeneralisedCone, + QuadricSurf, + SurfOfLinearExtrusion, + Unspecified, +} + +/// `b_spline_surface_with_knots` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = b_spline_surface_with_knots)] +#[holder(generate_deserialize)] +pub struct BSplineSurfaceWithKnots { + label: String, + u_degree: i64, + v_degree: i64, + #[holder(use_place_holder)] + control_points_list: Vec>, + surface_form: BSplineSurfaceForm, + u_closed: Logical, + v_closed: Logical, + self_intersect: Logical, + u_multiplicities: Vec, + v_multiplicities: Vec, + u_knots: Vec, + v_knots: Vec, + knot_spec: KnotType, +} + +impl TryFrom<&BSplineSurfaceWithKnots> for BSplineSurface { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(surface: &BSplineSurfaceWithKnots) -> Result { + let uknots = surface.u_knots.to_vec(); + let umulti = surface + .u_multiplicities + .iter() + .map(|n| *n as usize) + .collect(); + let uknots = KnotVec::from_single_multi(uknots, umulti).unwrap(); + let vknots = surface.v_knots.to_vec(); + let vmulti = surface + .v_multiplicities + .iter() + .map(|n| *n as usize) + .collect(); + let vknots = KnotVec::from_single_multi(vknots, vmulti).unwrap(); + let ctrls = surface + .control_points_list + .iter() + .map(|vec| vec.iter().map(Point3::from).collect()) + .collect(); + Ok(Self::try_new((uknots, vknots), ctrls)?) + } +} + +/// `uniform_surface` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = uniform_surface)] +#[holder(generate_deserialize)] +pub struct UniformSurface { + label: String, + u_degree: i64, + v_degree: i64, + #[holder(use_place_holder)] + control_points_list: Vec>, + surface_form: BSplineSurfaceForm, + u_closed: Logical, + v_closed: Logical, + self_intersect: Logical, +} + +impl TryFrom<&UniformSurface> for BSplineSurface { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(surface: &UniformSurface) -> Result { + let uknots = uniform_knots(surface.control_points_list.len(), surface.u_degree as usize)?; + let first = surface + .control_points_list + .first() + .ok_or("control points list is empty.")?; + let vknots = uniform_knots(first.len(), surface.v_degree as usize)?; + let ctrls = surface + .control_points_list + .iter() + .map(|vec| vec.iter().map(Point3::from).collect()) + .collect(); + Ok(Self::try_new((uknots, vknots), ctrls)?) + } +} + +/// `quasi_uniform_surface` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = quasi_uniform_surface)] +#[holder(generate_deserialize)] +pub struct QuasiUniformSurface { + label: String, + u_degree: i64, + v_degree: i64, + #[holder(use_place_holder)] + control_points_list: Vec>, + surface_form: BSplineSurfaceForm, + u_closed: Logical, + v_closed: Logical, + self_intersect: Logical, +} + +impl TryFrom<&QuasiUniformSurface> for BSplineSurface { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(surface: &QuasiUniformSurface) -> Result { + let uknots = + quasi_uniform_knots(surface.control_points_list.len(), surface.u_degree as usize); + let first = surface + .control_points_list + .first() + .ok_or("control points list is empty.")?; + let vknots = quasi_uniform_knots(first.len(), surface.v_degree as usize); + let ctrls = surface + .control_points_list + .iter() + .map(|vec| vec.iter().map(Point3::from).collect()) + .collect(); + Ok(Self::try_new((uknots, vknots), ctrls)?) + } +} + +/// `bezier_surface` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = bezier_surface)] +#[holder(generate_deserialize)] +pub struct BezierSurface { + label: String, + u_degree: i64, + v_degree: i64, + #[holder(use_place_holder)] + control_points_list: Vec>, + surface_form: BSplineSurfaceForm, + u_closed: Logical, + v_closed: Logical, + self_intersect: Logical, +} + +impl From<&BezierSurface> for BSplineSurface { + #[inline(always)] + fn from(value: &BezierSurface) -> Self { + let uknots = KnotVec::bezier_knot(value.u_degree as usize); + let vknots = KnotVec::bezier_knot(value.v_degree as usize); + let ctrls = value + .control_points_list + .iter() + .map(|vec| vec.iter().map(Point3::from).collect()) + .collect(); + Self::new((uknots, vknots), ctrls) + } +} + +/// Entity that does not exist in AP042. +/// Surface before rationalization of [`RationalBSplineSurface`] defined by a complex entity +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(generate_deserialize)] +pub enum NonRationalBSplineSurface { + #[holder(use_place_holder)] + BSplineSurfaceWithKnots(Box), + #[holder(use_place_holder)] + UniformSurface(Box), + #[holder(use_place_holder)] + QuasiUniformSurface(Box), + #[holder(use_place_holder)] + BezierSurface(Box), +} + +impl TryFrom<&NonRationalBSplineSurface> for BSplineSurface { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(value: &NonRationalBSplineSurface) -> Result { + use NonRationalBSplineSurface::*; + match value { + BSplineSurfaceWithKnots(x) => x.as_ref().try_into(), + UniformSurface(x) => x.as_ref().try_into(), + QuasiUniformSurface(x) => x.as_ref().try_into(), + BezierSurface(x) => Ok(x.as_ref().into()), + } + } +} + +/// `rational_b_spline_surface` as complex entity +/// +/// This struct is an ad hoc implementation that differs from the definition by EXPRESS: +/// in AP042, rationalized curves are defined as complex entities, +/// but here the surfaces before rationalization are held as internal variables. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = rational_b_spline_surface)] +#[holder(generate_deserialize)] +pub struct RationalBSplineSurface { + #[holder(use_place_holder)] + non_rational_b_spline_surface: NonRationalBSplineSurface, + weights_data: Vec>, +} + +impl TryFrom<&RationalBSplineSurface> for NurbsSurface { + type Error = StepConvertingError; + #[inline(always)] + fn try_from( + RationalBSplineSurface { + non_rational_b_spline_surface, + weights_data, + }: &RationalBSplineSurface, + ) -> Result { + let surface: BSplineSurface = non_rational_b_spline_surface.try_into()?; + Ok(Self::try_from_bspline_and_weights( + surface, + weights_data.clone(), + )?) + } +} + +/// `swept_surface` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(generate_deserialize)] +pub enum SweptSurfaceAny { + #[holder(use_place_holder)] + SurfaceOfLinearExtrusion(Box), + #[holder(use_place_holder)] + SurfaceOfRevolution(Box), +} + +impl TryFrom<&SweptSurfaceAny> for SweptCurve { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(value: &SweptSurfaceAny) -> Result { + use SweptSurfaceAny::*; + Ok(match value { + SurfaceOfLinearExtrusion(x) => SweptCurve::ExtrudedCurve(x.as_ref().try_into()?), + SurfaceOfRevolution(x) => SweptCurve::RevolutedCurve(x.as_ref().try_into()?), + }) + } +} + +/// `surface_of_linear_extrusion` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = surface_of_linear_extrusion)] +#[holder(generate_deserialize)] +pub struct SurfaceOfLinearExtrusion { + label: String, + #[holder(use_place_holder)] + swept_curve: CurveAny, + #[holder(use_place_holder)] + extrusion_axis: Vector, +} + +impl TryFrom<&SurfaceOfLinearExtrusion> for StepExtrudedCurve { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(sr: &SurfaceOfLinearExtrusion) -> Result { + let curve = Curve3D::try_from(&sr.swept_curve)?; + let vector = Vector3::from(&sr.extrusion_axis); + Ok(ExtrudedCurve::by_extrusion(curve, vector)) + } +} + +/// `surface_of_revolution` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = surface_of_revolution)] +#[holder(generate_deserialize)] +pub struct SurfaceOfRevolution { + label: String, + #[holder(use_place_holder)] + swept_curve: CurveAny, + #[holder(use_place_holder)] + axis_position: Axis1Placement, +} + +impl TryFrom<&SurfaceOfRevolution> for StepRevolutedCurve { + type Error = StepConvertingError; + #[inline(always)] + fn try_from(sr: &SurfaceOfRevolution) -> Result { + let curve = Curve3D::try_from(&sr.swept_curve)?; + let origin = Point3::from(&sr.axis_position.location); + let axis = sr.axis_position.direction().normalize(); + let mut rev = Processor::new(RevolutedCurve::by_revolution(curve, origin, axis)); + rev.invert(); + Ok(rev) + } +} + +/// `vertex_point` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = vertex_point)] +#[holder(generate_deserialize)] +pub struct VertexPoint { + pub label: String, + #[holder(use_place_holder)] + pub vertex_geometry: CartesianPoint, +} + +/// `edge` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(generate_deserialize)] +pub enum EdgeAny { + #[holder(use_place_holder)] + EdgeCurve(EdgeCurve), + #[holder(use_place_holder)] + OrientedEdge(OrientedEdge), +} + +/// `edge_curve` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = edge_curve)] +#[holder(generate_deserialize)] +pub struct EdgeCurve { + pub label: String, + #[holder(use_place_holder)] + pub edge_start: VertexPoint, + #[holder(use_place_holder)] + pub edge_end: VertexPoint, + #[holder(use_place_holder)] + pub edge_geometry: CurveAny, + pub same_sense: bool, +} + +impl EdgeCurve { + pub fn parse_curve2d(&self) -> Result { + let p = Point2::from(&self.edge_start.vertex_geometry); + let q = Point2::from(&self.edge_end.vertex_geometry); + let (p, q) = match self.same_sense { + true => (p, q), + false => (q, p), + }; + Self::sub_parse_2d(&self.edge_geometry, p, q, self.same_sense) + } + fn sub_parse_2d( + curve: &CurveAny, + p: Point2, + q: Point2, + same_sense: bool, + ) -> Result { + let mut curve = match curve { + CurveAny::Line(line) => { + let line = truck::Line::::from(line.as_ref()); + let p = line.projection(p); + let q = line.projection(q); + Curve2D::Line(Line(p, q)) + } + CurveAny::BoundedCurve(b) => b.as_ref().try_into()?, + CurveAny::Conic(curve) => match curve.as_ref() { + Conic::Circle(circle) => { + let mat = + Matrix3::try_from(&circle.position)? * Matrix3::from_scale(circle.radius); + let inv_mat = mat + .invert() + .ok_or_else(|| "Failed to convert Circle".to_string())?; + let (p, q) = (inv_mat.transform_point(p), inv_mat.transform_point(q)); + let (u, mut v) = ( + UnitCircle::::new() + .search_nearest_parameter(p, None, 0) + .ok_or_else(|| "the point is not on circle".to_string())?, + UnitCircle::::new() + .search_nearest_parameter(q, None, 0) + .ok_or_else(|| "the point is not on circle".to_string())?, + ); + if v <= u + TOLERANCE { + v += 2.0 * PI; + } + let circle = TrimmedCurve::new(UnitCircle::::new(), (u, v)); + let mut ellipse = Processor::new(circle); + ellipse.transform_by(mat); + Curve2D::Conic(Conic2D::Ellipse(ellipse)) + } + Conic::Ellipse(ellipse) => { + let mat = Matrix3::try_from(&ellipse.position)? + * Matrix3::from_nonuniform_scale(ellipse.semi_axis_1, ellipse.semi_axis_2); + let inv_mat = mat + .invert() + .ok_or_else(|| "Failed to convert Circle".to_string())?; + let (p, q) = (inv_mat.transform_point(p), inv_mat.transform_point(q)); + let (u, mut v) = ( + UnitCircle::::new() + .search_nearest_parameter(p, None, 0) + .ok_or_else(|| "the point is not on circle".to_string())?, + UnitCircle::::new() + .search_nearest_parameter(q, None, 0) + .ok_or_else(|| "the point is not on circle".to_string())?, + ); + if v <= u + TOLERANCE { + v += 2.0 * PI; + } + let circle = TrimmedCurve::new(UnitCircle::::new(), (u, v)); + let mut ellipse = Processor::new(circle); + ellipse.transform_by(mat); + Curve2D::Conic(Conic2D::Ellipse(ellipse)) + } + Conic::Hyperbola(hyperbola) => { + let mat = Matrix3::try_from(&hyperbola.position)? + * Matrix3::from_nonuniform_scale( + hyperbola.semi_axis, + hyperbola.semi_imag_axis, + ); + let inv_mat = mat + .invert() + .ok_or_else(|| "Failed to convert Hyperbola".to_string())?; + let (p, q) = (inv_mat.transform_point(p), inv_mat.transform_point(q)); + let (u, v) = ( + UnitHyperbola::::new() + .search_nearest_parameter(p, None, 0) + .ok_or_else(|| "the point is not on hyperbola".to_string())?, + UnitHyperbola::::new() + .search_nearest_parameter(q, None, 0) + .ok_or_else(|| "the point is not on hyparbola".to_string())?, + ); + let unit = TrimmedCurve::new(UnitHyperbola::::new(), (u, v)); + let mut hyperbola = Processor::new(unit); + hyperbola.transform_by(mat); + Curve2D::Conic(Conic2D::Hyperbola(hyperbola)) + } + Conic::Parabola(parabola) => { + let mat = Matrix3::try_from(¶bola.position)? + * Matrix3::from_scale(parabola.focal_dist); + let inv_mat = mat + .invert() + .ok_or_else(|| "Failed to convert Parabola".to_string())?; + let (p, q) = (inv_mat.transform_point(p), inv_mat.transform_point(q)); + let (u, v) = ( + UnitHyperbola::::new() + .search_nearest_parameter(p, None, 0) + .ok_or_else(|| "the point is not on parabola".to_string())?, + UnitHyperbola::::new() + .search_nearest_parameter(q, None, 0) + .ok_or_else(|| "the point is not on parabola".to_string())?, + ); + let unit = TrimmedCurve::new(UnitHyperbola::::new(), (u, v)); + let mut parabola = Processor::new(unit); + parabola.transform_by(mat); + Curve2D::Conic(Conic2D::Hyperbola(parabola)) + } + }, + CurveAny::Pcurve(_) => return Err("Pcurves cannot be parsed to 2D curves.".into()), + CurveAny::SurfaceCurve(_) => { + return Err("Surface curves cannot be parsed to 2D curves.".into()) + } + }; + if !same_sense { + curve.invert(); + } + Ok(curve) + } + pub fn parse_curve3d(&self) -> Result { + let p = Point3::from(&self.edge_start.vertex_geometry); + let q = Point3::from(&self.edge_end.vertex_geometry); + let (p, q) = match self.same_sense { + true => (p, q), + false => (q, p), + }; + Self::sub_parse_curve3d(&self.edge_geometry, p, q, self.same_sense) + } + fn sub_parse_curve3d( + curve: &CurveAny, + p: Point3, + q: Point3, + same_sense: bool, + ) -> Result { + let mut curve = match curve { + CurveAny::Line(_) => Curve3D::Line(Line(p, q)), + CurveAny::BoundedCurve(b) => b.as_ref().try_into()?, + CurveAny::Conic(curve) => match curve.as_ref() { + Conic::Circle(circle) => { + let mat = + Matrix4::try_from(&circle.position)? * Matrix4::from_scale(circle.radius); + let inv_mat = mat + .invert() + .ok_or_else(|| "Failed to convert Circle".to_string())?; + let (p, q) = (inv_mat.transform_point(p), inv_mat.transform_point(q)); + let (u, mut v) = ( + UnitCircle::::new() + .search_nearest_parameter(p, None, 0) + .ok_or_else(|| format!("the point is not on circle: {p:?}"))?, + UnitCircle::::new() + .search_nearest_parameter(q, None, 0) + .ok_or_else(|| format!("the point is not on circle: {q:?}"))?, + ); + if v <= u + TOLERANCE { + v += 2.0 * PI; + } + let circle = TrimmedCurve::new(UnitCircle::::new(), (u, v)); + let mut ellipse = Processor::new(circle); + ellipse.transform_by(mat); + Curve3D::Conic(Conic3D::Ellipse(ellipse)) + } + Conic::Ellipse(ellipse) => { + let mat = Matrix4::try_from(&ellipse.position)? + * Matrix4::from_nonuniform_scale( + ellipse.semi_axis_1, + ellipse.semi_axis_2, + f64::min(ellipse.semi_axis_1, ellipse.semi_axis_2), + ); + let inv_mat = mat + .invert() + .ok_or_else(|| "Failed to convert Circle".to_string())?; + let (p, q) = (inv_mat.transform_point(p), inv_mat.transform_point(q)); + let (u, mut v) = ( + UnitCircle::::new() + .search_nearest_parameter(p, None, 0) + .ok_or_else(|| format!("the point is not on circle: {p:?}"))?, + UnitCircle::::new() + .search_nearest_parameter(q, None, 0) + .ok_or_else(|| format!("the point is not on circle: {q:?}"))?, + ); + if v <= u + TOLERANCE { + v += 2.0 * PI; + } + let circle = TrimmedCurve::new(UnitCircle::::new(), (u, v)); + let mut ellipse = Processor::new(circle); + ellipse.transform_by(mat); + Curve3D::Conic(Conic3D::Ellipse(ellipse)) + } + Conic::Hyperbola(hyperbola) => { + let mat = Matrix4::try_from(&hyperbola.position)? + * Matrix4::from_nonuniform_scale( + hyperbola.semi_axis, + hyperbola.semi_imag_axis, + f64::min(hyperbola.semi_axis, hyperbola.semi_imag_axis), + ); + let inv_mat = mat + .invert() + .ok_or_else(|| "Failed to convert Circle".to_string())?; + let (p, q) = (inv_mat.transform_point(p), inv_mat.transform_point(q)); + let (u, mut v) = ( + UnitHyperbola::::new() + .search_nearest_parameter(p, None, 0) + .ok_or_else(|| "the point is not on circle".to_string())?, + UnitHyperbola::::new() + .search_nearest_parameter(q, None, 0) + .ok_or_else(|| "the point is not on circle".to_string())?, + ); + if v <= u + TOLERANCE { + v += 2.0 * PI; + } + let unit = TrimmedCurve::new(UnitHyperbola::::new(), (u, v)); + let mut hyperbola = Processor::new(unit); + hyperbola.transform_by(mat); + Curve3D::Conic(Conic3D::Hyperbola(hyperbola)) + } + Conic::Parabola(parabola) => { + let mat = Matrix4::try_from(¶bola.position)? + * Matrix4::from_scale(parabola.focal_dist); + let inv_mat = mat + .invert() + .ok_or_else(|| "Failed to convert Parabola".to_string())?; + let (p, q) = (inv_mat.transform_point(p), inv_mat.transform_point(q)); + let (u, v) = ( + UnitHyperbola::::new() + .search_nearest_parameter(p, None, 0) + .ok_or_else(|| "the point is not on parabola".to_string())?, + UnitHyperbola::::new() + .search_nearest_parameter(q, None, 0) + .ok_or_else(|| "the point is not on parabola".to_string())?, + ); + let unit = TrimmedCurve::new(UnitHyperbola::::new(), (u, v)); + let mut parabola = Processor::new(unit); + parabola.transform_by(mat); + Curve3D::Conic(Conic3D::Hyperbola(parabola)) + } + }, + CurveAny::Pcurve(c) => { + let surface: Surface = (&c.basis_surface).try_into()?; + let u = surface + .search_nearest_parameter(p, None, 100) + .ok_or_else(|| "the point is not on surface".to_string())?; + let v = surface + .search_nearest_parameter(q, None, 100) + .ok_or_else(|| "the point is not on surface".to_string())?; + let curve2d = c + .reference_to_curve + .representation_item + .first() + .ok_or("no representation item")?; + let curve2d = Self::sub_parse_2d( + curve2d, + Point2::new(u.0, u.1), + Point2::new(v.0, v.1), + true, + )?; + Curve3D::PCurve(truck::PCurve::new(Box::new(curve2d), Box::new(surface))) + } + CurveAny::SurfaceCurve(c) => { + if p.near(&q) { + return Self::sub_parse_curve3d(&c.curve_3d, p, q, same_sense); + } + use PreferredSurfaceCurveRepresentation::*; + match c.master_representation { + Curve3D => Self::sub_parse_curve3d(&c.curve_3d, p, q, same_sense)?, + PcurveS1 => { + if let Some(PcurveOrSurface::Pcurve(c)) = c.associated_geometry.first() { + Self::sub_parse_curve3d(&CurveAny::Pcurve(c.clone()), p, q, true)? + } else { + return Err( + "The 0-indexed associated geometry is nothing or not PCURVE." + .into(), + ); + } + } + PcurveS2 => { + if let Some(PcurveOrSurface::Pcurve(c)) = c.associated_geometry.get(1) { + Self::sub_parse_curve3d(&CurveAny::Pcurve(c.clone()), p, q, true)? + } else { + return Err( + "The 1-indexed associated geometry is nothing or not PCURVE." + .into(), + ); + } + } + } + } + }; + if !same_sense { + curve.invert(); + } + Ok(curve) + } +} + +/// `oriented_edge` +/// +/// `oriented_edge` has duplicated information. +/// These are not included here because they are essentially omitted. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = oriented_edge)] +#[holder(generate_deserialize)] +pub struct OrientedEdge { + pub label: String, + #[holder(use_place_holder)] + pub edge_element: EdgeCurve, + pub orientation: bool, +} + +impl OrientedEdgeHolder { + fn edge_element_holder(&self, table: &Table) -> Option { + match &self.edge_element { + PlaceHolder::Owned(holder) => Some(holder.clone()), + PlaceHolder::Ref(Name::Entity(idx)) => table.edge_curve.get(idx).cloned(), + _ => None, + } + } + fn edge_element_idx(&self) -> Option { + if let PlaceHolder::Ref(Name::Entity(idx)) = self.edge_element { + Some(idx) + } else { + None + } + } +} + +/// `edge_loop` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = edge_loop)] +#[holder(generate_deserialize)] +pub struct EdgeLoop { + pub label: String, + #[holder(use_place_holder)] + pub edge_list: Vec, +} + +/// `face_bound` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = face_bound)] +#[holder(generate_deserialize)] +/// `FACE_OUTER_BOUNDS` is also parsed to this struct. +pub struct FaceBound { + pub label: String, + // For now, we are going with the policy of accepting nothing but edgeloop. + #[holder(use_place_holder)] + pub bound: EdgeLoop, + pub orientation: bool, +} + +impl FaceBoundHolder { + fn bound_holder(&self, table: &Table) -> Option { + match &self.bound { + PlaceHolder::Owned(holder) => Some(holder.clone()), + PlaceHolder::Ref(Name::Entity(ref idx)) => table.edge_loop.get(idx).cloned(), + _ => None, + } + } +} + +/// `face` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(generate_deserialize)] +pub enum FaceAny { + #[holder(use_place_holder)] + FaceSurface(FaceSurface), + #[holder(use_place_holder)] + OrientedFace(OrientedFace), +} + +/// `face_surface` +/// +/// `advanced_face` is also parsed to this struct. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = face_surface)] +#[holder(generate_deserialize)] +pub struct FaceSurface { + pub label: String, + #[holder(use_place_holder)] + pub bounds: Vec, + #[holder(use_place_holder)] + pub face_geometry: SurfaceAny, + pub same_sense: bool, +} + +impl FaceSurfaceHolder { + fn bounds_holder<'a>(&'a self, table: &'a Table) -> Vec> { + self.bounds + .iter() + .map(|bound| match bound { + PlaceHolder::Owned(bound) => Some(bound.clone()), + PlaceHolder::Ref(Name::Entity(ref idx)) => table.face_bound.get(idx).cloned(), + _ => None, + }) + .collect() + } +} + +/// `oriented_face` +/// +/// `oriented_face` has duplicated information. +/// These are not included here because they are essentially omitted. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = oriented_face)] +#[holder(generate_deserialize)] +pub struct OrientedFace { + pub label: String, + #[holder(use_place_holder)] + pub face_element: FaceSurface, + pub orientation: bool, +} + +impl OrientedFaceHolder { + fn face_element_holder(&self, table: &Table) -> Option { + match &self.face_element { + PlaceHolder::Ref(Name::Entity(ref idx)) => table.face_surface.get(idx).cloned(), + PlaceHolder::Owned(x) => Some(x.clone()), + _ => None, + } + } +} + +/// `shell` +/// +/// Includes `open_shell` and `closed_shell`. +/// Since these differences are only informal propositions, the data structure does not distinguish between the two. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = shell)] +#[holder(generate_deserialize)] +pub struct Shell { + pub label: String, + #[holder(use_place_holder)] + pub cfs_faces: Vec, +} + +impl ShellHolder { + fn cfs_faces_holder<'a>( + &'a self, + table: &'a Table, + ) -> impl Iterator> + 'a { + self.cfs_faces.iter().map(|face| match face { + PlaceHolder::Owned(holder) => Some(holder.clone()), + PlaceHolder::Ref(Name::Entity(ref idx)) => table + .oriented_face + .get(idx) + .cloned() + .map(FaceAnyHolder::OrientedFace) + .or_else(|| { + table + .face_surface + .get(idx) + .cloned() + .map(FaceAnyHolder::FaceSurface) + }), + _ => None, + }) + } +} + +/// `oriented_shell` +/// +/// Includes `oriented_open_shell` and `oriented_closed_shell`. +/// Since these differences are only informal propositions, the data structure does not distinguish between the two. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Holder)] +#[holder(table = Table)] +#[holder(field = oriented_shell)] +#[holder(generate_deserialize)] +pub struct OrientedShell { + pub label: String, + #[holder(use_place_holder)] + pub shell_element: Shell, + pub orientation: bool, +} + +impl Table { + fn place_holder_edge_any_to_index_and_edge_curve( + &self, + edge: &PlaceHolder, + ) -> Option<(u64, EdgeCurveHolder)> { + use PlaceHolder::Ref; + let Ref(Name::Entity(ref idx)) = edge else { + return None; + }; + self.oriented_edge + .get(idx) + .and_then(|oriented_edge| { + Some(( + oriented_edge.edge_element_idx()?, + oriented_edge.edge_element_holder(self)?, + )) + }) + .or_else(|| { + self.edge_curve + .get(idx) + .map(|edge_curve| (*idx, edge_curve.clone())) + }) + } + fn face_any_to_orientation_and_face( + &self, + face: Option, + ) -> Option<(bool, FaceSurfaceHolder)> { + match face? { + FaceAnyHolder::FaceSurface(face) => Some((true, face)), + FaceAnyHolder::OrientedFace(oriented_face) => { + let face_element = oriented_face.face_element_holder(self)?; + Some((oriented_face.orientation, face_element)) + } + } + } + + fn shell_vertices(&self, shell: &ShellHolder) -> (Vec, HashMap) { + use PlaceHolder::Ref; + let mut vidx_map = HashMap::::new(); + let vertex_to_point = |v: PlaceHolder| { + if let Ref(Name::Entity(ref idx)) = v { + if !vidx_map.contains_key(idx) { + let len = vidx_map.len(); + vidx_map.insert(*idx, len); + let p = EntityTable::::get_owned(self, *idx) + .map_err(|e| eprintln!("{e}")) + .ok()?; + return Some(Point3::from(&p.vertex_geometry)); + } + } + None + }; + let vertices: Vec = shell + .cfs_faces_holder(self) + .filter_map(move |face| self.face_any_to_orientation_and_face(face)) + .flat_map(move |(_, face)| face.bounds_holder(self)) + .filter_map(move |bound| bound?.bound_holder(self)) + .flat_map(move |bound| bound.edge_list) + .filter_map(move |edge| self.place_holder_edge_any_to_index_and_edge_curve(&edge)) + .flat_map(move |(_, edge)| [edge.edge_start, edge.edge_end]) + .filter_map(vertex_to_point) + .collect(); + (vertices, vidx_map) + } + + fn shell_edges( + &self, + shell: &ShellHolder, + vidx_map: &HashMap, + ) -> (Vec>, HashMap) { + use PlaceHolder::Ref; + let mut eidx_map = HashMap::::new(); + let edge_curve_to_compressed_edge = |(idx, edge): (u64, EdgeCurveHolder)| { + if eidx_map.contains_key(&idx) { + return None; + } + let len = eidx_map.len(); + eidx_map.insert(idx, len); + let edge_curve = edge + .clone() + .into_owned(self) + .map_err(|e| eprintln!("{e}")) + .ok()?; + let curve = edge_curve + .parse_curve3d() + .map_err(|e| eprintln!("{e}")) + .ok()?; + let Ref(Name::Entity(front_idx)) = edge.edge_start else { + return None; + }; + let Ref(Name::Entity(back_idx)) = edge.edge_end else { + return None; + }; + Some(CompressedEdge { + vertices: (*vidx_map.get(&front_idx)?, *vidx_map.get(&back_idx)?), + curve, + }) + }; + let edges: Vec> = shell + .cfs_faces_holder(self) + .filter_map(move |face| self.face_any_to_orientation_and_face(face)) + .flat_map(move |(_, face)| face.bounds_holder(self)) + .filter_map(move |bound| bound?.bound_holder(self)) + .flat_map(move |bound| bound.edge_list) + .filter_map(move |edge| self.place_holder_edge_any_to_index_and_edge_curve(&edge)) + .filter_map(edge_curve_to_compressed_edge) + .collect(); + (edges, eidx_map) + } + fn face_bound_to_edges( + &self, + bound: FaceBoundHolder, + eidx_map: &HashMap, + ) -> Option> { + use PlaceHolder::Ref; + let ori = bound.orientation; + let bound = bound.bound_holder(self)?; + let mut edges: Vec = bound + .edge_list + .into_iter() + .filter_map(|edge| { + let Ref(Name::Entity(ref idx)) = edge else { + return None; + }; + let edge_idx = if let Some(oriented_edge) = self.oriented_edge.get(idx) { + CompressedEdgeIndex { + index: *eidx_map.get(&oriented_edge.edge_element_idx()?)?, + orientation: oriented_edge.orientation == ori, + } + } else { + CompressedEdgeIndex { + index: *eidx_map.get(idx)?, + orientation: ori, + } + }; + Some(edge_idx) + }) + .collect(); + if !ori { + edges.reverse(); + } + Some(edges) + } + + fn shell_faces( + &self, + shell: &ShellHolder, + eidx_map: &HashMap, + ) -> Vec> { + shell + .cfs_faces_holder(self) + .filter_map(|face| self.face_any_to_orientation_and_face(face)) + .filter_map(|(orientation, face)| { + let step_surface: SurfaceAny = face + .face_geometry + .clone() + .into_owned(self) + .map_err(|e| eprintln!("{e}")) + .ok()?; + let mut surface = Surface::try_from(&step_surface) + .map_err(|e| eprintln!("{e}")) + .ok()?; + if !face.same_sense { + surface.invert() + } + let boundaries: Vec<_> = face + .bounds_holder(self) + .into_iter() + .filter_map(|bound| self.face_bound_to_edges(bound?, eidx_map)) + .collect(); + Some(CompressedFace { + surface, + boundaries, + orientation, + }) + }) + .collect() + } + + /// construct `CompressedShell` of `truck` from `Shell` in STEP file + /// # Example + /// ``` + /// use truck_stepio::r#in::{*, alias::*}; + /// use ruststep::tables::EntityTable; + /// // read file + /// let step_string = include_str!(concat!( + /// env!("CARGO_MANIFEST_DIR"), + /// "/../resources/step/occt-cube.step", + /// )); + /// // parse step file + /// let exchange = ruststep::parser::parse(&step_string).unwrap(); + /// // convert the parsing results to a Rust struct + /// let table = Table::from_data_section(&exchange.data[0]); + /// // take one shell (this is only one shell) + /// let step_shell = &table.shell.values().next().unwrap(); + /// // convert STEP shell to `CompressedShell` + /// let cshell = table.to_compressed_shell(step_shell).unwrap(); + /// // The cube has 6 faces! + /// assert_eq!(cshell.faces.len(), 6); + /// ``` + pub fn to_compressed_shell( + &self, + shell: &ShellHolder, + ) -> Result, StepConvertingError> { + let (vertices, vidx_map) = self.shell_vertices(shell); + let (edges, eidx_map) = self.shell_edges(shell, &vidx_map); + Ok(CompressedShell { + vertices, + edges, + faces: self.shell_faces(shell, &eidx_map), + }) + } +} diff --git a/vendor/truck-stepio/src/lib.rs b/vendor/truck-stepio/src/lib.rs new file mode 100644 index 00000000..2bde80e5 --- /dev/null +++ b/vendor/truck-stepio/src/lib.rs @@ -0,0 +1,78 @@ +//! Reads/writes STEP files from/to truck. +//! +//! # Current Status +//! +//! It is possible to output data modeled by truck-modeling. +//! Shapes created by set operations cannot be output yet. +//! Input will come further down the road. + +// SolveCraft: compiler warnings no longer fail the build. +#![deny(clippy::all, rust_2018_idioms)] +#![warn( + missing_docs, + missing_debug_implementations, + trivial_casts, + trivial_numeric_casts, + unsafe_code, + unstable_features, + unused_import_braces, + unused_qualifications +)] + +/// STEP input module +/// # Example +/// ``` +/// use truck_stepio::r#in::{*, alias::*}; +/// use ruststep::tables::EntityTable; +/// // read file +/// let step_string = include_str!(concat!( +/// env!("CARGO_MANIFEST_DIR"), +/// "/../resources/step/occt-cube.step", +/// )); +/// // parse step file +/// let exchange = ruststep::parser::parse(&step_string).unwrap(); +/// // convert the parsing results to a Rust struct +/// let table = Table::from_data_section(&exchange.data[0]); +/// // get `CartesianPoint` registered in #102 +/// let step_point = EntityTable::::get_owned(&table, 102).unwrap(); +/// // convert `CartesianPoint` in STEP to `Point3` in cgmath +/// let cgmath_point = Point3::from(&step_point); +/// // check parse result +/// assert_eq!(cgmath_point, Point3::new(0.0, 10.0, 0.0)); +/// ``` +#[cfg(feature = "in")] +pub mod r#in; +/// STEP output module +pub mod out; + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_from { + ($(impl From<&$refed: ty> for $converted: ty { + $from_func: item + })*) => { + $(impl From<&$refed> for $converted { + $from_func + } + impl From<$refed> for $converted { + fn from(x: $refed) -> Self { Self::from(&x) } + })* + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_try_from { + ($(impl TryFrom<&$refed: ty> for $converted: ty { + $try_from_func: item + })*) => { + $(impl TryFrom<&$refed> for $converted { + type Error = ExpressParseError; + $try_from_func + } + impl TryFrom<$refed> for $converted { + type Error = ExpressParseError; + fn try_from(x: $refed) -> Result { Self::try_from(&x) } + })* + }; +} diff --git a/vendor/truck-stepio/src/out/geometry.rs b/vendor/truck-stepio/src/out/geometry.rs new file mode 100644 index 00000000..ce89255b --- /dev/null +++ b/vendor/truck-stepio/src/out/geometry.rs @@ -0,0 +1,813 @@ +use super::{Result, *}; +use truck_geometry::prelude::*; +use truck_modeling::{Curve as ModelingCurve, Leader, Surface as ModelingSurface}; +use truck_polymesh::PolylineCurve; + +impl DisplayByStep for Point2 { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + f.write_fmt(format_args!( + "#{idx} = CARTESIAN_POINT('', {coordinates});\n", + coordinates = SliceDisplay(AsRef::<[f64; 2]>::as_ref(self)), + )) + } +} +impl_const_step_length!(Point2, 1); + +impl DisplayByStep for Point3 { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + f.write_fmt(format_args!( + "#{idx} = CARTESIAN_POINT('', {coordinates});\n", + coordinates = SliceDisplay(AsRef::<[f64; 3]>::as_ref(self)), + )) + } +} +impl_const_step_length!(Point3, 1); + +/// class for display `DIRECTION`. +#[derive(Clone, Debug, Copy)] +pub struct VectorAsDirection(pub V); + +impl DisplayByStep for VectorAsDirection { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + f.write_fmt(format_args!( + "#{idx} = DIRECTION('', {direction_ratios});\n", + direction_ratios = SliceDisplay(AsRef::<[f64; 2]>::as_ref(&self.0)), + )) + } +} + +impl DisplayByStep for VectorAsDirection { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + f.write_fmt(format_args!( + "#{idx} = DIRECTION('', {direction_ratios});\n", + direction_ratios = SliceDisplay(AsRef::<[f64; 3]>::as_ref(&self.0)), + )) + } +} +impl_const_step_length!(VectorAsDirection, 1, ); + +impl DisplayByStep for V +where + V: InnerSpace, + VectorAsDirection: DisplayByStep, +{ + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let magnitude = self.magnitude(); + let direction_idx = idx + 1; + f.write_fmt(format_args!( + "#{idx} = VECTOR('', #{direction_idx}, {magnitude});\n{direction}", + direction = StepDisplay::new(VectorAsDirection(*self / magnitude), direction_idx), + magnitude = FloatDisplay(magnitude), + )) + } +} +impl_const_step_length!(Vector2, 2); +impl_const_step_length!(Vector3, 2); + +impl

DisplayByStep for Line

+where + P: EuclideanSpace + ConstStepLength + DisplayByStep, + P::Diff: DisplayByStep, +{ + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let pnt_idx = idx + 1; + let dir_idx = idx + 1 + P::LENGTH; + f.write_fmt(format_args!( + "#{idx} = LINE('', #{pnt_idx}, #{dir_idx});\n{pnt}{dir}", + pnt = StepDisplay::new(self.0, pnt_idx), + dir = StepDisplay::new(self.1 - self.0, dir_idx), + )) + } +} + +impl

StepLength for Line

+where + P: EuclideanSpace + ConstStepLength, + P::Diff: ConstStepLength, +{ + #[inline(always)] + fn step_length(&self) -> usize { ::LENGTH } +} + +impl

ConstStepLength for Line

+where + P: EuclideanSpace + ConstStepLength, + P::Diff: ConstStepLength, +{ + const LENGTH: usize = 1 + P::LENGTH + P::Diff::LENGTH; +} + +impl

StepCurve for Line

{} + +impl

DisplayByStep for PolylineCurve

+where P: Copy + ConstStepLength + DisplayByStep +{ + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + f.write_fmt(format_args!( + "#{idx} = POLYLINE('', {range});\n", + range = IndexSliceDisplay(idx + 1..=idx + self.0.len()) + ))?; + let closure = |(i, p): (usize, &P)| p.fmt(idx + 1 + i * P::LENGTH, f); + self.0.iter().enumerate().try_for_each(closure) + } +} + +impl StepLength for PolylineCurve

{ + #[inline(always)] + fn step_length(&self) -> usize { 1 + self.0.len() * P::LENGTH } +} + +impl

StepCurve for PolylineCurve

{} + +impl

DisplayByStep for BSplineCurve

+where P: Copy + ConstStepLength + DisplayByStep +{ + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let (knots, multi) = self.knot_vec().to_single_multi(); + let control_points_instances = self + .control_points() + .iter() + .enumerate() + .map(|(i, p)| StepDisplay::new(*p, idx + 1 + i * P::LENGTH)) + .collect::>(); + f.write_fmt(format_args!( + "#{idx} = B_SPLINE_CURVE_WITH_KNOTS('', {degree}, {control_points_list}, .UNSPECIFIED., .U., .U., {knot_multiplicities}, {knots}, .UNSPECIFIED.);\n{control_points_instances}", + degree = self.degree(), + control_points_list = IndexSliceDisplay((idx + 1..=idx + self.control_points().len() * P::LENGTH).step_by(P::LENGTH)), + knot_multiplicities = SliceDisplay(&multi), + knots = SliceDisplay(&knots), + control_points_instances = SliceDisplay(&control_points_instances), + )) + } +} + +impl

StepLength for BSplineCurve

{ + #[inline(always)] + fn step_length(&self) -> usize { self.control_points().len() + 1 } +} + +impl

StepCurve for BSplineCurve

{} + +impl DisplayByStep for NurbsCurve +where + V: Homogeneous, + V::Point: ConstStepLength + DisplayByStep, +{ + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let (knots, multi) = self.knot_vec().to_single_multi(); + let control_points_instances = self + .control_points() + .iter() + .enumerate() + .map(|(i, v)| StepDisplay::new(v.to_point(), idx + 1 + i * V::Point::LENGTH)) + .collect::>(); + let weights = self + .control_points() + .iter() + .map(|v| v.weight()) + .collect::>(); + f.write_fmt(format_args!( + "#{idx} = ( + BOUNDED_CURVE() + B_SPLINE_CURVE({degree}, {control_points_list}, .UNSPECIFIED., .U., .U.) + B_SPLINE_CURVE_WITH_KNOTS({knot_multiplicities}, {knots}, .UNSPECIFIED.) + CURVE() + GEOMETRIC_REPRESENTATION_ITEM() + RATIONAL_B_SPLINE_CURVE({weights}) + REPRESENTATION_ITEM('') +);\n{control_points_instances}", + degree = self.degree(), + control_points_list = IndexSliceDisplay( + (idx + 1..=idx + self.control_points().len() * V::Point::LENGTH) + .step_by(V::Point::LENGTH) + ), + knot_multiplicities = SliceDisplay(&multi), + knots = SliceDisplay(&knots), + weights = SliceDisplay(&weights), + control_points_instances = SliceDisplay(&control_points_instances), + )) + } +} + +impl StepLength for NurbsCurve { + #[inline(always)] + fn step_length(&self) -> usize { self.control_points().len() + 1 } +} + +impl StepCurve for NurbsCurve {} + +impl DisplayByStep for Processor>, Matrix3> { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let transform = *self.transform(); + let position_idx = idx + 1; + let location_idx = idx + 2; + let ref_direction_idx = idx + 3; + let r0 = transform[0].magnitude(); + let r1 = transform[1].magnitude(); + let ref_direction = VectorAsDirection(transform[0].truncate() / r0); + let location = transform[2].to_point(); + if r0.near(&r1) { + let r = FloatDisplay(r0); + f.write_fmt(format_args!("#{idx} = CIRCLE('', #{position_idx}, {r});\n"))?; + } else { + let (r0, r1) = (FloatDisplay(r0), FloatDisplay(r1)); + f.write_fmt(format_args!( + "#{idx} = ELLIPSE('', #{position_idx}, {r0}, {r1});\n" + ))?; + } + f.write_fmt(format_args!( + "#{position_idx} = AXIS2_PLACEMENT_2D('', #{location_idx}, #{ref_direction_idx});\n", + ))?; + DisplayByStep::fmt(&location, location_idx, f)?; + DisplayByStep::fmt(&ref_direction, ref_direction_idx, f) + } +} +impl_const_step_length!(Processor>, Matrix3>, 4); + +impl DisplayByStep for Processor>, Matrix4> { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let transform = self.transform(); + let position_idx = idx + 1; + let location_idx = idx + 2; + let axis_idx = idx + 3; + let ref_direction_idx = idx + 4; + let location = transform[3].to_point(); + let axis = VectorAsDirection(transform[2].truncate().normalize()); + let r0 = transform[0].magnitude(); + let r1 = transform[1].magnitude(); + let ref_direction = VectorAsDirection(transform[0].truncate() / r0); + if r0.near(&r1) { + let r = FloatDisplay(r0); + f.write_fmt(format_args!("#{idx} = CIRCLE('', #{position_idx}, {r});\n"))?; + } else { + let (r0, r1) = (FloatDisplay(r0), FloatDisplay(r1)); + f.write_fmt(format_args!( + "#{idx} = ELLIPSE('', #{position_idx}, {r0}, {r1});\n" + ))?; + } + f.write_fmt(format_args!( + "#{position_idx} = AXIS2_PLACEMENT_3D('', #{location_idx}, #{axis_idx}, #{ref_direction_idx});\n", + ))?; + DisplayByStep::fmt(&location, location_idx, f)?; + DisplayByStep::fmt(&axis, axis_idx, f)?; + DisplayByStep::fmt(&ref_direction, ref_direction_idx, f) + } +} +impl_const_step_length!(Processor>, Matrix4>, 5); + +impl DisplayByStep for Processor>, Matrix3> { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let transform = *self.transform(); + let position_idx = idx + 1; + let location_idx = idx + 2; + let ref_direction_idx = idx + 3; + let r0 = transform[0].magnitude(); + let r1 = transform[1].magnitude(); + let ref_direction_raw = VectorAsDirection(transform[0].truncate() / r0); + let ref_direction = StepDisplay::new(ref_direction_raw, ref_direction_idx); + let location = StepDisplay::new(transform[2].to_point(), location_idx); + let (r0, r1) = (FloatDisplay(r0), FloatDisplay(r1)); + f.write_fmt(format_args!( + "#{idx} = HYPERBOLA('', #{position_idx}, {r0}, {r1}); +#{position_idx} = AXIS2_PLACEMENT_2D('', #{location_idx}, #{ref_direction_idx}); +{location}{ref_direction}" + )) + } +} +impl_const_step_length!(Processor>, Matrix3>, 4); + +impl DisplayByStep for Processor>, Matrix4> { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let transform = self.transform(); + let position_idx = idx + 1; + let location_idx = idx + 2; + let axis_idx = idx + 3; + let ref_direction_idx = idx + 4; + let location = StepDisplay::new(transform[3].to_point(), location_idx); + let axis_raw = VectorAsDirection(transform[2].truncate().normalize()); + let axis = StepDisplay::new(axis_raw, axis_idx); + let r0 = transform[0].magnitude(); + let r1 = transform[1].magnitude(); + let ref_direction_raw = VectorAsDirection(transform[0].truncate() / r0); + let ref_direction = StepDisplay::new(ref_direction_raw, ref_direction_idx); + let (r0, r1) = (FloatDisplay(r0), FloatDisplay(r1)); + f.write_fmt(format_args!( + "#{idx} = HYPERBOLA('', #{position_idx}, {r0}, {r1}); +#{position_idx} = AXIS2_PLACEMENT_3D('', #{location_idx}, #{axis_idx}, #{ref_direction_idx}); +{location}{axis}{ref_direction}" + )) + } +} +impl_const_step_length!(Processor>, Matrix4>, 5); + +impl DisplayByStep for Processor>, Matrix3> { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let transform = *self.transform(); + let position_idx = idx + 1; + let location_idx = idx + 2; + let ref_direction_idx = idx + 3; + let r0 = transform[0].magnitude(); + let r1 = transform[1].magnitude(); + let focal_dist = FloatDisplay(r1 * r1 / r0); + let ref_direction_raw = VectorAsDirection(transform[0].truncate() / r0); + let ref_direction = StepDisplay::new(ref_direction_raw, ref_direction_idx); + let location = StepDisplay::new(transform[2].to_point(), location_idx); + f.write_fmt(format_args!( + "#{idx} = PARABOLA('', #{position_idx}, {focal_dist}); +#{position_idx} = AXIS2_PLACEMENT_2D('', #{location_idx}, #{ref_direction_idx}); +{location}{ref_direction}" + )) + } +} +impl_const_step_length!(Processor>, Matrix3>, 4); + +impl DisplayByStep for Processor>, Matrix4> { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let transform = self.transform(); + let position_idx = idx + 1; + let location_idx = idx + 2; + let axis_idx = idx + 3; + let ref_direction_idx = idx + 4; + let location = StepDisplay::new(transform[3].to_point(), location_idx); + let axis_raw = VectorAsDirection(transform[2].truncate().normalize()); + let axis = StepDisplay::new(axis_raw, axis_idx); + let r0 = transform[0].magnitude(); + let r1 = transform[1].magnitude(); + let focal_dist = FloatDisplay(r1 * r1 / r0); + let ref_direction_raw = VectorAsDirection(transform[0].truncate() / r0); + let ref_direction = StepDisplay::new(ref_direction_raw, ref_direction_idx); + f.write_fmt(format_args!( + "#{idx} = PARABOLA('', #{position_idx}, {focal_dist}); +#{position_idx} = AXIS2_PLACEMENT_3D('', #{location_idx}, #{axis_idx}, #{ref_direction_idx}); +{location}{axis}{ref_direction}" + )) + } +} +impl_const_step_length!(Processor>, Matrix4>, 5); + +impl StepCurve for Processor { + #[inline(always)] + fn same_sense(&self) -> bool { self.orientation() } +} + +impl DisplayByStep for IntersectionCurve +where + C: StepLength + DisplayByStep, + S: StepLength + DisplayByStep, +{ + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let curve_idx = idx + 1; + let surface0_idx = curve_idx + self.leader().step_length(); + let surface1_idx = surface0_idx + self.surface0().step_length(); + f.write_fmt(format_args!( + "#{idx} = INTERSECTION_CURVE('', #{curve_idx}, (#{surface0_idx}, #{surface1_idx}), .CURVE_3D.);\n" + ))?; + self.leader().fmt(curve_idx, f)?; + self.surface0().fmt(surface0_idx, f)?; + // SolveCraft: the second surface was written at the first one's index (duplicate ids). + self.surface1().fmt(surface1_idx, f) + } +} + +impl StepLength for IntersectionCurve { + #[inline(always)] + fn step_length(&self) -> usize { + 1 + self.leader().step_length() + + self.surface0().step_length() + + self.surface1().step_length() + } +} + +impl ConstStepLength for IntersectionCurve +where + C: ConstStepLength, + S: ConstStepLength, +{ + const LENGTH: usize = 1 + C::LENGTH + S::LENGTH + S::LENGTH; +} + +impl StepCurve for IntersectionCurve { + #[inline(always)] + fn same_sense(&self) -> bool { self.leader().same_sense() } +} + +impl DisplayByStep for PCurve +where + C: DisplayByStep, + S: DisplayByStep + StepLength, +{ + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let surface_idx = idx + 1; + let repr_idx = surface_idx + self.surface().step_length(); + let context_idx = repr_idx + 1; + let curve_idx = repr_idx + 2; + let curve = StepDisplay::new(self.curve(), curve_idx); + let surface = StepDisplay::new(self.surface(), surface_idx); + f.write_fmt(format_args!( + "#{idx} = PCURVE('', #{surface_idx}, #{repr_idx}); +{surface}#{repr_idx} = DEFINITIONAL_REPRESENTATION('', (#{curve_idx}), #{context_idx}); +#{context_idx} = ( + GEOMETRIC_REPRESENTATION_CONTEXT(2) + PARAMETRIC_REPRESENTATION_CONTEXT() + REPRESENTATION_CONTEXT('2D SPACE', '') +); +{curve}" + )) + } +} + +impl StepLength for PCurve { + fn step_length(&self) -> usize { 3 + self.curve().step_length() + self.surface().step_length() } +} + +impl ConstStepLength for PCurve +where + C: ConstStepLength, + S: ConstStepLength, +{ + const LENGTH: usize = 3 + C::LENGTH + S::LENGTH; +} + +impl StepCurve for PCurve { + #[inline(always)] + fn same_sense(&self) -> bool { self.curve().same_sense() } +} + +impl DisplayByStep for Leader { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + match self { + Leader::Polyline(x) => DisplayByStep::fmt(x, idx, f), + Leader::BSpline(x) => DisplayByStep::fmt(x, idx, f), + } + } +} + +impl StepLength for Leader { + fn step_length(&self) -> usize { + match self { + Leader::Polyline(x) => x.step_length(), + Leader::BSpline(x) => x.step_length(), + } + } +} + +impl StepCurve for Leader {} + +impl DisplayByStep for ModelingCurve { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + match self { + ModelingCurve::Line(x) => DisplayByStep::fmt(x, idx, f), + ModelingCurve::BSplineCurve(x) => DisplayByStep::fmt(x, idx, f), + ModelingCurve::NurbsCurve(x) => DisplayByStep::fmt(x, idx, f), + ModelingCurve::IntersectionCurve(x) => DisplayByStep::fmt(x, idx, f), + } + } +} + +impl StepLength for ModelingCurve { + fn step_length(&self) -> usize { + match self { + ModelingCurve::Line(_) => Line::::LENGTH, + ModelingCurve::BSplineCurve(x) => x.step_length(), + ModelingCurve::NurbsCurve(x) => x.step_length(), + ModelingCurve::IntersectionCurve(x) => x.step_length(), + } + } +} + +impl StepCurve for ModelingCurve {} + +impl DisplayByStep for Plane { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let axis2_placement_idx = idx + 1; + let location_idx = idx + 2; + let z_axis_idx = idx + 3; + let x_axis_idx = idx + 4; + f.write_fmt(format_args!( + "#{idx} = PLANE('', #{axis2_placement_idx}); +#{axis2_placement_idx} = AXIS2_PLACEMENT_3D('', #{location_idx}, #{z_axis_idx}, #{x_axis_idx}); +{location}{z_axis}{x_axis}", + location = StepDisplay::new(self.origin(), location_idx), + z_axis = StepDisplay::new(VectorAsDirection(self.normal()), z_axis_idx), + x_axis = StepDisplay::new(VectorAsDirection(self.u_axis().normalize()), x_axis_idx) + )) + } +} +impl_const_step_length!(Plane, 5); + +impl StepSurface for Plane {} + +impl DisplayByStep for Processor { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let sphere = *self.entity(); + let transform = self.transform(); + let position_idx = idx + 1; + let location_idx = idx + 2; + let axis_idx = idx + 3; + let ref_direction_idx = idx + 4; + let location = transform[3].to_point() + sphere.center().to_vec(); + let axis = VectorAsDirection(transform[2].truncate().normalize()); + let r0 = transform[0].magnitude(); + let r1 = transform[1].magnitude(); + if !r0.near(&r1) { + f.write_str("The transform of sphere includes non-uniform scale.")?; + return ERR; + } + let ref_direction = VectorAsDirection(transform[0].truncate() / r0); + let r = FloatDisplay(r0 * sphere.radius()); + f.write_fmt(format_args!( + "#{idx} = SPHERICAL_SURFACE('', #{position_idx}, {r}); +#{position_idx} = AXIS2_PLACEMENT_3D('', #{location_idx}, #{axis_idx}, #{ref_direction_idx});\n" + ))?; + DisplayByStep::fmt(&location, location_idx, f)?; + DisplayByStep::fmt(&axis, axis_idx, f)?; + DisplayByStep::fmt(&ref_direction, ref_direction_idx, f) + } +} +impl_const_step_length!(Processor, 5); + +impl DisplayByStep for Sphere { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + DisplayByStep::fmt(&Processor::new(*self), idx, f) + } +} +impl_const_step_length!(Sphere, 5); +impl StepSurface for Sphere {} + +impl DisplayByStep for Processor { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let torus = *self.entity(); + let transform = self.transform(); + let position_idx = idx + 1; + let location_idx = idx + 2; + let axis_idx = idx + 3; + let ref_direction_idx = idx + 4; + let location = transform[3].to_point() + torus.center().to_vec(); + let axis = VectorAsDirection(transform[2].truncate().normalize()); + let r0 = transform[0].magnitude(); + let r1 = transform[1].magnitude(); + if !r0.near(&r1) { + f.write_str("The transform of sphere includes non-uniform scale.")?; + return ERR; + } + let ref_direction = VectorAsDirection(transform[0].truncate() / r0); + let greater = FloatDisplay(r0 * torus.large_radius()); + let lesser = FloatDisplay(r0 * torus.small_radius()); + f.write_fmt(format_args!( + "#{idx} = TOROIDAL_SURFACE('', #{position_idx}, {greater}, {lesser}); +#{position_idx} = AXIS2_PLACEMENT_3D('', #{location_idx}, #{axis_idx}, #{ref_direction_idx});\n", + ))?; + DisplayByStep::fmt(&location, location_idx, f)?; + DisplayByStep::fmt(&axis, axis_idx, f)?; + DisplayByStep::fmt(&ref_direction, ref_direction_idx, f) + } +} +impl_const_step_length!(Processor, 5); + +impl StepSurface for Processor { + #[inline(always)] + fn same_sense(&self) -> bool { self.orientation() } +} + +impl DisplayByStep for Torus { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + DisplayByStep::fmt(&Processor::new(*self), idx, f) + } +} +impl_const_step_length!(Torus, 5); +impl StepSurface for Torus {} + +impl

DisplayByStep for BSplineSurface

+where P: Copy + DisplayByStep +{ + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let control_points = self.control_points(); + let control_points_instances = self + .control_points() + .iter() + .flatten() + .enumerate() + .map(|(i, p)| StepDisplay::new(*p, idx + i + 1)) + .collect::>(); + let mut counter = 0; + let control_points_list = control_points + .iter() + .map(|slice| { + counter += slice.len(); + IndexSliceDisplay(idx + counter - slice.len() + 1..=idx + counter) + }) + .collect::>(); + let (uknots, umulti) = self.uknot_vec().to_single_multi(); + let (vknots, vmulti) = self.vknot_vec().to_single_multi(); + f.write_fmt(format_args!( + "#{idx} = B_SPLINE_SURFACE_WITH_KNOTS('', {u_degree}, {v_degree}, {control_points_list}, .UNSPECIFIED., .U., .U., .U., \ +{u_multiplicities}, {v_multiplicities}, {u_knots}, {v_knots}, .UNSPECIFIED.);\n{control_points_instances}", + u_degree = self.udegree(), + v_degree = self.vdegree(), + control_points_list = SliceDisplay(&control_points_list), + u_multiplicities = SliceDisplay(&umulti), + v_multiplicities = SliceDisplay(&vmulti), + u_knots = SliceDisplay(&uknots), + v_knots = SliceDisplay(&vknots), + control_points_instances = SliceDisplay(&control_points_instances), + )) + } +} + +impl

StepLength for BSplineSurface

{ + #[inline(always)] + fn step_length(&self) -> usize { 1 + self.control_points().iter().map(Vec::len).sum::() } +} +impl

StepSurface for BSplineSurface

{} + +impl DisplayByStep for NurbsSurface +where + V: Homogeneous, + V::Point: Copy + DisplayByStep, +{ + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let control_points_instances = self + .control_points() + .iter() + .flatten() + .enumerate() + .map(|(i, v)| StepDisplay::new(v.to_point(), idx + i + 1)) + .collect::>(); + let mut counter = 0; + let control_points_list = self + .control_points() + .iter() + .map(|slice| { + counter += slice.len(); + IndexSliceDisplay(idx + counter - slice.len() + 1..=idx + counter) + }) + .collect::>(); + let weights = self + .control_points() + .iter() + .map(|slice| slice.iter().map(|v| v.weight()).collect::>()) + .collect::>(); + let weights = weights + .iter() + .map(|slice| SliceDisplay(slice)) + .collect::>(); + let (uknots, umulti) = self.uknot_vec().to_single_multi(); + let (vknots, vmulti) = self.vknot_vec().to_single_multi(); + f.write_fmt(format_args!( + "#{idx} = ( + BOUNDED_SURFACE() + B_SPLINE_SURFACE({u_degree}, {v_degree}, {control_points_list}, .UNSPECIFIED., .U., .U., .U.) + B_SPLINE_SURFACE_WITH_KNOTS({u_multiplicities}, {v_multiplicities}, {u_knots}, {v_knots}, .UNSPECIFIED.) + GEOMETRIC_REPRESENTATION_ITEM() + RATIONAL_B_SPLINE_SURFACE({weights}) + REPRESENTATION_ITEM('') + SURFACE() +);\n{control_points_instances}", + u_degree = self.udegree(), + v_degree = self.vdegree(), + control_points_list = SliceDisplay(&control_points_list), + u_multiplicities = SliceDisplay(&umulti), + v_multiplicities = SliceDisplay(&vmulti), + u_knots = SliceDisplay(&uknots), + v_knots = SliceDisplay(&vknots), + control_points_instances = SliceDisplay(&control_points_instances), + weights = SliceDisplay(&weights), + )) + } +} + +impl StepLength for NurbsSurface { + #[inline(always)] + fn step_length(&self) -> usize { 1 + self.control_points().iter().map(Vec::len).sum::() } +} +impl StepSurface for NurbsSurface {} + +impl DisplayByStep for ExtrudedCurve +where C: StepLength + DisplayByStep +{ + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let curve = self.entity_curve(); + let curve_idx = idx + 1; + let vector_idx = idx + 1 + curve.step_length(); + let vector = self.extruding_vector(); + f.write_fmt(format_args!( + "#{idx} = SURFACE_OF_LINEAR_EXTRUSION('', #{curve_idx}, #{vector_idx});{}{}", + StepDisplay::new(curve, curve_idx), + StepDisplay::new(vector, vector_idx), + )) + } +} +impl StepLength for ExtrudedCurve { + fn step_length(&self) -> usize { 1 + self.entity_curve().step_length() + Vector3::LENGTH } +} +impl ConstStepLength for ExtrudedCurve { + const LENGTH: usize = 1 + C::LENGTH + Vector3::LENGTH; +} +impl StepSurface for ExtrudedCurve {} + +impl StepSurface for Processor, T> { + #[inline(always)] + fn same_sense(&self) -> bool { self.orientation() } +} + +impl DisplayByStep for RevolutedCurve +where C: StepLength + DisplayByStep +{ + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let curve = self.entity_curve(); + let curve_idx = idx + 1; + let axis_idx = curve_idx + curve.step_length(); + let location_idx = axis_idx + 1; + let dir_idx = location_idx + 1; + f.write_fmt(format_args!( + "#{idx} = SURFACE_OF_REVOLUTION('', #{curve_idx}, #{axis_idx}); +{curve}#{axis_idx} = AXIS1_PLACEMENT('', #{location_idx}, #{dir_idx});\n{location}{dir}", + curve = StepDisplay::new(curve, curve_idx), + location = StepDisplay::new(self.origin(), location_idx), + dir = StepDisplay::new(VectorAsDirection(self.axis()), dir_idx), + )) + } +} + +impl StepLength for RevolutedCurve { + #[inline(always)] + fn step_length(&self) -> usize { 4 + self.entity_curve().step_length() } +} + +impl ConstStepLength for RevolutedCurve { + const LENGTH: usize = 4 + C::LENGTH; +} + +// SolveCraft: truck revolves right-handed with u along the profile and v around the axis, the +// parameterisation of ISO 10303-42 surface_of_revolution, so the normals agree (was `false`). +impl StepSurface for RevolutedCurve { + #[inline(always)] + fn same_sense(&self) -> bool { true } +} + +impl DisplayByStep for Processor, Matrix4> +where C: StepLength + Transformed + DisplayByStep +{ + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + let surface = self.entity(); + let transform = self.transform(); + let (k, a, _) = match transform.iwasawa_decomposition() { + Some(x) => x, + None => { + f.write_str("Transform is not regular")?; + return ERR; + } + }; + if !a[0][0].near(&a[1][1]) || !a[1][1].near(&a[2][2]) { + f.write_str("Transform contains non-uniform scale.")?; + return ERR; + } + let curve = surface.entity_curve().transformed(*transform); + let axis = k.transform_vector(surface.axis()); + let origin = transform.transform_point(surface.origin()); + let surface = RevolutedCurve::by_revolution(curve, origin, axis); + DisplayByStep::fmt(&surface, idx, f) + } +} +impl StepLength for Processor, Matrix4> { + fn step_length(&self) -> usize { self.entity().step_length() } +} + +// SolveCraft: the written surface revolves the transformed profile about the transformed +// axis; a mirroring transform reverses the turn, and with it the normal (was `!orientation`). +impl StepSurface for Processor, Matrix4> { + #[inline(always)] + fn same_sense(&self) -> bool { self.orientation() == (self.transform().determinant() >= 0.0) } +} + +impl DisplayByStep for ModelingSurface { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + match self { + ModelingSurface::Plane(x) => DisplayByStep::fmt(x, idx, f), + ModelingSurface::BSplineSurface(x) => DisplayByStep::fmt(x, idx, f), + ModelingSurface::NurbsSurface(x) => DisplayByStep::fmt(x, idx, f), + ModelingSurface::RevolutedCurve(x) => DisplayByStep::fmt(x, idx, f), + } + } +} + +impl StepLength for ModelingSurface { + fn step_length(&self) -> usize { + match self { + ModelingSurface::Plane(_) => Plane::LENGTH, + ModelingSurface::BSplineSurface(x) => x.step_length(), + ModelingSurface::NurbsSurface(x) => x.step_length(), + ModelingSurface::RevolutedCurve(x) => x.entity().step_length(), + } + } +} + +// SolveCraft: was the default (`true`), which dropped an inverted revolution's orientation. +impl StepSurface for ModelingSurface { + fn same_sense(&self) -> bool { + match self { + ModelingSurface::RevolutedCurve(x) => StepSurface::same_sense(x), + _ => true, + } + } +} diff --git a/vendor/truck-stepio/src/out/mod.rs b/vendor/truck-stepio/src/out/mod.rs new file mode 100644 index 00000000..9d1e767c --- /dev/null +++ b/vendor/truck-stepio/src/out/mod.rs @@ -0,0 +1,388 @@ +use std::fmt::{Debug, Display, Formatter, Result}; + +use truck_topology::compress::*; + +use self::topology::PreStepModel; + +const ERR: Result = Err(std::fmt::Error); + +#[cfg(feature = "derive")] +pub use truck_derivers::{DisplayByStep, StepLength}; + +/// display boolean number to step file +#[derive(Clone, Copy, Debug)] +pub struct BooleanDisplay(pub bool); + +impl Display for BooleanDisplay { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match self.0 { + true => f.write_str(".T."), + false => f.write_str(".F."), + } + } +} + +/// display float number to step file +#[derive(Clone, Copy, Debug)] +pub struct FloatDisplay(pub f64); + +impl Display for FloatDisplay { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let FloatDisplay(x) = *self; + if f64::abs(x) < 1.0e-2 && x != 0.0 { + f.write_fmt(format_args!("{x:.10E}")) + } else { + f.write_fmt(format_args!("{x:?}")) + } + } +} + +/// display step slice +/// # Examples +/// ``` +/// use truck_stepio::out::SliceDisplay; +/// let slice = &[1.0, 2.0, 3.0, 4.0]; +/// let display = SliceDisplay(slice); +/// let step_string = display.to_string(); +/// assert_eq!(step_string, "(1.0, 2.0, 3.0, 4.0)"); +/// ``` +#[derive(Clone, Debug)] +pub struct SliceDisplay<'a, T>(pub &'a [T]); + +impl<'a> Display for SliceDisplay<'a, f64> { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.write_str("(")?; + self.0.iter().enumerate().try_for_each(|(i, x)| { + if i != 0 { + f.write_str(", ")?; + } + Display::fmt(&FloatDisplay(*x), f) + })?; + f.write_str(")") + } +} + +impl<'a> Display for SliceDisplay<'a, usize> { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.write_str("(")?; + self.0.iter().enumerate().try_for_each(|(i, x)| { + if i != 0 { + f.write_str(", ")?; + } + Display::fmt(x, f) + })?; + f.write_str(")") + } +} + +impl<'a> Display for SliceDisplay<'a, String> { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.write_str("(")?; + self.0.iter().enumerate().try_for_each(|(i, x)| { + if i != 0 { + f.write_str(", ")?; + } + f.write_fmt(format_args!("'{x}'")) + })?; + f.write_str(")") + } +} + +impl<'a> Display for SliceDisplay<'a, SliceDisplay<'a, f64>> { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.write_str("(")?; + self.0.iter().enumerate().try_for_each(|(i, x)| { + if i != 0 { + f.write_str(", ")?; + } + Display::fmt(x, f) + })?; + f.write_str(")") + } +} + +/// display index slice +/// # Examples +/// ``` +/// use truck_stepio::out::*; +/// let indices = [1, 10, 100, 1000, 10000]; +/// let display = IndexSliceDisplay(indices.into_iter()); +/// let step_string = display.to_string(); +/// assert_eq!(step_string, "(#1, #10, #100, #1000, #10000)"); +/// ``` +#[derive(Clone, Debug)] +pub struct IndexSliceDisplay(pub I); + +impl> Display for IndexSliceDisplay { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.write_str("(")?; + self.0 + .clone() + .into_iter() + .enumerate() + .try_for_each(|(i, idx)| { + if i != 0 { + f.write_fmt(format_args!(", #{idx}")) + } else { + f.write_fmt(format_args!("#{idx}")) + } + })?; + f.write_str(")") + } +} + +impl<'a, I: Clone + IntoIterator> Display for SliceDisplay<'a, IndexSliceDisplay> { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.write_str("(")?; + self.0.iter().enumerate().try_for_each(|(i, x)| { + if i != 0 { + f.write_str(", ")?; + } + Display::fmt(x, f) + })?; + f.write_str(")") + } +} + +/// trait for outputting by STEP file format. +pub trait DisplayByStep { + /// formatter + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result; +} + +impl DisplayByStep for &T { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { DisplayByStep::fmt(*self, idx, f) } +} + +impl DisplayByStep for Box { + fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { + DisplayByStep::fmt(self.as_ref(), idx, f) + } +} + +/// Display struct for outputting some objects to STEP file format. +#[derive(Clone, Debug)] +pub struct StepDisplay { + entity: T, + idx: usize, +} + +impl<'a, T> Display for SliceDisplay<'a, StepDisplay> +where StepDisplay: Display +{ + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + self.0.iter().try_for_each(|x| Display::fmt(x, f)) + } +} + +impl StepDisplay { + /// constructor + #[inline] + pub const fn new(entity: T, idx: usize) -> Self { Self { entity, idx } } + /// return entity + #[inline] + pub const fn entity(&self) -> &T { &self.entity } + /// return index + #[inline] + pub const fn index(&self) -> usize { self.idx } +} + +impl Display for StepDisplay { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { DisplayByStep::fmt(&self.entity, self.idx, f) } +} + +/// Calculate how many lines are used in outputting an object to a STEP file +pub trait StepLength { + /// Calculate how many lines are used in outputting an object to a STEP file + fn step_length(&self) -> usize; +} + +impl StepLength for &T { + #[inline(always)] + fn step_length(&self) -> usize { StepLength::step_length(*self) } +} + +impl StepLength for Box { + #[inline(always)] + fn step_length(&self) -> usize { self.as_ref().step_length() } +} + +/// Constant numbers of lines for outputting an object to a STEP file. +/// `x.step_length() == X::LENGTH` must always hold. +pub trait ConstStepLength: StepLength { + /// the number of line + const LENGTH: usize; +} + +impl ConstStepLength for &T { + const LENGTH: usize = T::LENGTH; +} + +impl ConstStepLength for Box { + const LENGTH: usize = T::LENGTH; +} + +macro_rules! impl_const_step_length { + ($type: ty, $len: expr $(,<$($gen: ident),*>)?) => { + impl$(<$($gen),*>)? ConstStepLength for $type { + const LENGTH: usize = $len; + } + impl$(<$($gen),*>)? StepLength for $type { + #[inline(always)] + fn step_length(&self) -> usize { ::LENGTH } + } + }; +} + +/// Additional information for output to `edge_curve`. +pub trait StepCurve { + /// the parameter `same_sense`. + #[inline(always)] + fn same_sense(&self) -> bool { true } +} + +impl StepCurve for &T { + #[inline(always)] + fn same_sense(&self) -> bool { (*self).same_sense() } +} + +impl StepCurve for Box { + #[inline(always)] + fn same_sense(&self) -> bool { self.as_ref().same_sense() } +} + +/// Additional information for output to `face_surface`. +pub trait StepSurface { + /// the parameter `same_sense`. + #[inline(always)] + fn same_sense(&self) -> bool { true } +} + +impl StepSurface for &T { + #[inline(always)] + fn same_sense(&self) -> bool { (*self).same_sense() } +} + +impl StepSurface for Box { + #[inline(always)] + fn same_sense(&self) -> bool { self.as_ref().same_sense() } +} + +/// Describe STEP file header +#[derive(Clone, Debug)] +pub struct StepHeaderDescriptor { + /// file name + pub file_name: String, + /// time stamp + pub time_stamp: String, + /// authors + pub authors: Vec, + /// organization + pub organization: Vec, + /// organization system + pub organization_system: String, + /// authorization + pub authorization: String, +} + +#[derive(Clone, Debug)] +struct StepHeader { + file_name: String, + time_stamp: String, + authors: Vec, + organization: Vec, + origination_system: String, + authorization: String, + schema: String, +} + +impl Default for StepHeaderDescriptor { + fn default() -> Self { + Self { + file_name: Default::default(), + time_stamp: chrono::Utc::now().naive_local().to_string(), + authors: Default::default(), + organization: Default::default(), + organization_system: Default::default(), + authorization: Default::default(), + } + } +} + +impl Display for StepHeader { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let empty_string = [String::new()]; + f.write_fmt(format_args!( + "HEADER; +FILE_DESCRIPTION(('Shape Data from Truck'), '2;1'); +FILE_NAME('{}', '{}', ({}), ({}), 'truck', '{}', '{}'); +FILE_SCHEMA(('{}')); +ENDSEC;\n", + self.file_name, + self.time_stamp, + if self.authors.is_empty() { + SliceDisplay(&empty_string) + } else { + SliceDisplay(&self.authors) + }, + if self.organization.is_empty() { + SliceDisplay(&empty_string) + } else { + SliceDisplay(&self.organization) + }, + self.origination_system, + self.authorization, + self.schema, + )) + } +} + +/// Display model with configuations +#[derive(Clone, Debug)] +pub struct StepModel<'a, P, C, S>(PreStepModel<'a, P, C, S>); + +/// Display models with configuations +#[derive(Clone, Debug)] +pub struct StepModels<'a, P, C, S> { + models: Vec>, + next_idx: usize, +} + +/// Display struct for outputting STEP file format with header. +#[derive(Clone, Debug)] +pub struct CompleteStepDisplay { + display: T, + header: StepHeader, +} + +impl Display for CompleteStepDisplay { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.write_fmt(format_args!( + "ISO-10303-21;\n{}DATA;\n{}ENDSEC;\nEND-ISO-10303-21;\n", + self.header, self.display, + )) + } +} + +impl CompleteStepDisplay { + /// constructor + #[inline] + pub fn new(display: T, header: StepHeaderDescriptor) -> Self { + CompleteStepDisplay { + display, + header: StepHeader { + file_name: header.file_name, + time_stamp: header.time_stamp, + authors: header.authors, + organization: header.organization, + origination_system: header.organization_system, + authorization: header.authorization, + schema: "ISO-10303-042".to_string(), + }, + } + } +} + +mod geometry; +mod topology; +pub use geometry::VectorAsDirection; diff --git a/vendor/truck-stepio/src/out/topology.rs b/vendor/truck-stepio/src/out/topology.rs new file mode 100644 index 00000000..a1152287 --- /dev/null +++ b/vendor/truck-stepio/src/out/topology.rs @@ -0,0 +1,493 @@ +use super::{Result, *}; + +#[derive(Clone, Debug)] +pub(super) struct StepShell<'a, P, C, S> { + entity: &'a CompressedShell, + idx: usize, + face_indices: Vec, + ep_edges: usize, + ep_vertices: usize, + surface_indices: Vec, + curve_indices: Vec, + ep_points: usize, + is_open: bool, +} + +impl<'a, P, C, S> StepShell<'a, P, C, S> +where + P: Copy, + C: StepLength, + S: StepLength, +{ + fn new(shell: &'a CompressedShell, idx: usize, is_open: bool) -> Self { + let faces = &shell.faces; + let edges = &shell.edges; + let vertices = &shell.vertices; + let mut cursor = idx + 1; + let face_indices = faces + .iter() + .map(|f| { + let res = cursor; + cursor += match f.boundaries.is_empty() { + true => 5, + false => 1 + f.boundaries.iter().map(|b| 2 + b.len()).sum::(), + }; + res + }) + .collect::>(); + let ep_edges = cursor; + let ep_vertices = ep_edges + edges.len(); + cursor = ep_vertices + vertices.len(); + let surface_indices = faces + .iter() + .map(|f| { + let res = cursor; + cursor += f.surface.step_length(); + res + }) + .collect::>(); + let curve_indices = edges + .iter() + .map(|e| { + let res = cursor; + cursor += e.curve.step_length(); + res + }) + .collect::>(); + let ep_points = cursor; + StepShell { + entity: shell, + idx, + face_indices, + ep_edges, + ep_vertices, + surface_indices, + curve_indices, + ep_points, + is_open, + } + } +} + +impl<'a, P, C, S> Display for StepShell<'a, P, C, S> +where + P: DisplayByStep + Copy, + C: DisplayByStep + StepCurve, + S: DisplayByStep + StepSurface, +{ + fn fmt(&self, formatter: &mut Formatter<'_>) -> Result { + let StepShell { + entity, + idx, + face_indices, + ep_edges, + ep_vertices, + surface_indices, + curve_indices, + ep_points, + is_open, + } = self; + let faces = &entity.faces; + let edges = &entity.edges; + let vertices = &entity.vertices; + let shell_kind = match is_open { + true => "OPEN_SHELL", + false => "CLOSED_SHELL", + }; + formatter.write_fmt(format_args!( + "#{idx} = {shell_kind}('', {face_indices});\n", + face_indices = IndexSliceDisplay(self.face_indices.clone()), + ))?; + faces.iter().enumerate().try_for_each(|(i, f)| { + let idx = face_indices[i]; + let mut cursor = idx + 1; + let face_geometry = surface_indices[i]; + let face_bounds = match f.boundaries.is_empty() { + true => vec![cursor], + false => { + let closure = |b: &Vec| { + let res = cursor; + cursor += 2 + b.len(); + res + }; + f.boundaries.iter().map(closure).collect() + } + }; + formatter.write_fmt(format_args!( + "#{idx} = FACE_SURFACE('', {face_bound}, #{face_geometry}, {same_sense});\n", + same_sense = BooleanDisplay(f.orientation == f.surface.same_sense()), + face_bound = IndexSliceDisplay(face_bounds.clone()), + ))?; + cursor = idx + 1; + if f.boundaries.is_empty() { + let face_bound_idx = cursor; + let vertex_loop_idx = cursor + 1; + let vertex_idx = cursor + 2; + let vertex_geometry = cursor + 3; + formatter.write_fmt(format_args!( + "#{face_bound_idx} = FACE_BOUND('', #{vertex_loop_idx}, .T.); +#{vertex_loop_idx} = VERTEX_LOOP('', #{vertex_idx}); +#{vertex_idx} = VERTEX_POINT('', #{vertex_geometry}); +#{vertex_geometry} = POINT_ON_SURFACE('', #{face_geometry}, 0.0, 0.0);\n" + ))?; + } + f.boundaries.iter().try_for_each(|b| { + let face_bound_idx = cursor; + let edge_loop_idx = cursor + 1; + let ep_oriented_edges = cursor + 2; + cursor += 2 + b.len(); + formatter.write_fmt(format_args!( + "#{face_bound_idx} = FACE_BOUND('', #{edge_loop_idx}, {orientation}); +#{edge_loop_idx} = EDGE_LOOP('', {oriented_edge_indices});\n", + orientation = BooleanDisplay(f.orientation), + oriented_edge_indices = + IndexSliceDisplay(ep_oriented_edges..ep_oriented_edges + b.len()), + ))?; + b.iter().enumerate().try_for_each(|(j, ce)| { + formatter.write_fmt(format_args!( + "#{idx} = ORIENTED_EDGE('', *, *, #{edge_element}, {orientation});\n", + idx = ep_oriented_edges + j, + edge_element = ep_edges + ce.index, + orientation = if ce.orientation { ".T." } else { ".F." }, + )) + }) + }) + })?; + edges.iter().enumerate().try_for_each(|(i, e)| { + let same_sense = if e.curve.same_sense() { ".T." } else { ".F." }; + formatter.write_fmt(format_args!( + "#{idx} = EDGE_CURVE('', #{edge_start}, #{edge_end}, #{edge_geometry}, {same_sense});\n", + idx = ep_edges + i, + edge_start = ep_vertices + e.vertices.0, + edge_end = ep_vertices + e.vertices.1, + edge_geometry = curve_indices[i], + )) + })?; + (0..vertices.len()).try_for_each(|i| { + formatter.write_fmt(format_args!( + "#{idx} = VERTEX_POINT('', #{vertex_geometry});\n", + idx = ep_vertices + i, + vertex_geometry = ep_points + i, + )) + })?; + faces.iter().zip(surface_indices).try_for_each(|(f, idx)| { + Display::fmt(&StepDisplay::new(&f.surface, *idx), formatter) + })?; + edges + .iter() + .zip(curve_indices) + .try_for_each(|(e, idx)| Display::fmt(&StepDisplay::new(&e.curve, *idx), formatter))?; + vertices + .iter() + .enumerate() + .try_for_each(|(i, v)| Display::fmt(&StepDisplay::new(*v, ep_points + i), formatter)) + } +} + +impl<'a, P, C, S> StepLength for StepShell<'a, P, C, S> { + fn step_length(&self) -> usize { + 1 + self.ep_points + self.entity.vertices.len() - self.face_indices[0] + } +} + +#[derive(Clone, Debug)] +pub(super) struct StepSolid<'a, P, C, S> { + idx: usize, + boundaries: Vec>, +} + +impl<'a, P, C, S> StepSolid<'a, P, C, S> +where + P: Copy, + C: StepLength, + S: StepLength, +{ + fn new(solid: &'a CompressedSolid, idx: usize) -> Self { + let mut cursor = idx + 1; + let boundaries = solid + .boundaries + .iter() + .map(|shell| { + let res = StepShell::new(shell, cursor, false); + cursor += 1 + res.step_length(); + res + }) + .collect::>(); + StepSolid { idx, boundaries } + } +} + +impl<'a, P, C, S> Display for StepSolid<'a, P, C, S> +where + P: DisplayByStep + Copy, + C: DisplayByStep + StepLength + StepCurve, + S: DisplayByStep + StepLength + StepSurface, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let StepSolid { idx, boundaries } = self; + match boundaries.len() { + 0 => { + f.pad("empty solid!")?; + Err(std::fmt::Error) + } + 1 => { + let shell_idx = idx + 1; + let step_shell = &boundaries[0]; + f.write_fmt(format_args!( + "#{idx} = MANIFOLD_SOLID_BREP('', #{shell_idx});\n" + ))?; + Display::fmt(step_shell, f) + } + _ => { + let first_shell_idx = boundaries[0].face_indices[0] - 1; + f.write_fmt(format_args!( + "#{idx} = BREP_WITH_VOIDS('', #{first_shell_idx}, {other_shells});\n", + other_shells = IndexSliceDisplay( + boundaries[1..] + .iter() + .map(|step_shell| step_shell.face_indices[0] - 2) + ), + ))?; + Display::fmt(&boundaries[0], f)?; + boundaries[1..].iter().try_for_each(|step_shell| { + let oriented_shell_idx = step_shell.face_indices[0] - 2; + let shell_idx = step_shell.face_indices[0] - 1; + f.write_fmt(format_args!( + "#{oriented_shell_idx} = ORIENTED_CLOSED_SHELL('', *, #{shell_idx}, .T.);\n", + ))?; + Display::fmt(step_shell, f) + }) + } + } + } +} + +impl<'a, P, C, S> StepLength for StepSolid<'a, P, C, S> { + fn step_length(&self) -> usize { + let b = &self.boundaries; + match b.len() { + 0 => 0, + 1 => 1 + b[0].step_length(), + _ => b.len() + b.iter().map(StepLength::step_length).sum::(), + } + } +} + +#[derive(Clone, Debug)] +pub(super) enum PreStepModel<'a, P, C, S> { + /// shell based surface model + Shell(StepShell<'a, P, C, S>), + /// solid model + Solid(StepSolid<'a, P, C, S>), +} + +impl<'a, P, C, S> From<&'a CompressedShell> for PreStepModel<'a, P, C, S> +where + P: Copy, + C: StepLength, + S: StepLength, +{ + fn from(shell: &'a CompressedShell) -> Self { + Self::Shell(StepShell::new(shell, 17, true)) + } +} + +impl<'a, P, C, S> From<&'a CompressedSolid> for PreStepModel<'a, P, C, S> +where + P: Copy, + C: StepLength, + S: StepLength, +{ + fn from(solid: &'a CompressedSolid) -> Self { Self::Solid(StepSolid::new(solid, 16)) } +} + +impl<'a, P, C, S> Display for PreStepModel<'a, P, C, S> +where + P: DisplayByStep + Copy, + C: DisplayByStep + StepLength + StepCurve, + S: DisplayByStep + StepLength + StepSurface, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match self { + Self::Shell(x) => { + f.write_fmt(format_args!( + "#{idx} = SHELL_BASED_SURFACE_MODEL('', (#{shell_idx}));\n", + idx = x.idx - 1, + shell_idx = x.idx + ))?; + Display::fmt(&x, f) + } + Self::Solid(x) => Display::fmt(x, f), + } + } +} + +impl<'a, P, C, S> StepLength for PreStepModel<'a, P, C, S> { + fn step_length(&self) -> usize { + match self { + Self::Shell(x) => 1 + x.step_length(), + Self::Solid(x) => x.step_length(), + } + } +} + +impl<'a, P, C, S> From<&'a CompressedShell> for StepModel<'a, P, C, S> +where + P: Copy, + C: StepLength, + S: StepLength, +{ + fn from(shell: &'a CompressedShell) -> Self { Self(shell.into()) } +} + +impl<'a, P, C, S> From<&'a CompressedSolid> for StepModel<'a, P, C, S> +where + P: Copy, + C: StepLength, + S: StepLength, +{ + fn from(solid: &'a CompressedSolid) -> Self { Self(solid.into()) } +} + +impl<'a, P, C, S> Display for StepModel<'a, P, C, S> +where + P: DisplayByStep + Copy, + C: DisplayByStep + StepLength + StepCurve, + S: DisplayByStep + StepLength + StepSurface, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.pad( +"#1 = APPLICATION_PROTOCOL_DEFINITION('international standard', 'automotive_design', 2000, #2); +#2 = APPLICATION_CONTEXT('core data for automotive mechanical design processes'); +#3 = SHAPE_DEFINITION_REPRESENTATION(#4, #10); +#4 = PRODUCT_DEFINITION_SHAPE('','', #5); +#5 = PRODUCT_DEFINITION('design','', #6, #9); +#6 = PRODUCT_DEFINITION_FORMATION('','', #7); +#7 = PRODUCT('','','', (#8)); +#8 = PRODUCT_CONTEXT('', #2, 'mechanical'); +#9 = PRODUCT_DEFINITION_CONTEXT('part definition', #2, 'design'); +#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('', (#16), #11); +#11 = ( + GEOMETRIC_REPRESENTATION_CONTEXT(3) + GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#15)) + GLOBAL_UNIT_ASSIGNED_CONTEXT((#12, #13, #14)) + REPRESENTATION_CONTEXT('Context #1', '3D Context with UNIT and UNCERTAINTY') +); +#12 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#13 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#14 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#15 = UNCERTAINTY_MEASURE_WITH_UNIT(1.0E-6, #12, 'distance_accuracy_value','confusion accuracy');\n" + )?; + Display::fmt(&self.0, f) + } +} + +impl<'a, P, C, S> Default for StepModels<'a, P, C, S> { + fn default() -> Self { + Self { + models: Vec::new(), + next_idx: 16, + } + } +} + +impl<'a, P, C, S> StepModels<'a, P, C, S> +where + P: Copy, + C: StepLength, + S: StepLength, +{ + /// push a shell to step models + pub fn push_shell(&mut self, shell: &'a CompressedShell) { + let model = PreStepModel::Shell(StepShell::new(shell, self.next_idx + 1, true)); + self.next_idx += model.step_length(); + self.models.push(model) + } + /// push a solid to step models + pub fn push_solid(&mut self, solid: &'a CompressedSolid) { + let model = PreStepModel::Solid(StepSolid::new(solid, self.next_idx)); + self.next_idx += model.step_length(); + self.models.push(model) + } +} + +impl<'a, P, C, S> FromIterator<&'a CompressedShell> for StepModels<'a, P, C, S> +where + P: Copy, + C: StepLength, + S: StepLength, +{ + fn from_iter>>(iter: T) -> Self { + let mut next_idx = 16; + let models = iter + .into_iter() + .map(|shell| { + let model = PreStepModel::Shell(StepShell::new(shell, next_idx + 1, true)); + next_idx += model.step_length(); + model + }) + .collect(); + Self { models, next_idx } + } +} + +impl<'a, P, C, S> FromIterator<&'a CompressedSolid> for StepModels<'a, P, C, S> +where + P: Copy, + C: StepLength, + S: StepLength, +{ + fn from_iter>>(iter: T) -> Self { + let mut next_idx = 16; + let models = iter + .into_iter() + .map(|solid| { + let model = PreStepModel::Solid(StepSolid::new(solid, next_idx)); + next_idx += model.step_length(); + model + }) + .collect(); + Self { models, next_idx } + } +} + +impl<'a, P, C, S> Display for StepModels<'a, P, C, S> +where + P: DisplayByStep + Copy, + C: DisplayByStep + StepLength + StepCurve, + S: DisplayByStep + StepLength + StepSurface, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.pad( +"#1 = APPLICATION_PROTOCOL_DEFINITION('international standard', 'automotive_design', 2000, #2); +#2 = APPLICATION_CONTEXT('core data for automotive mechanical design processes'); +#3 = SHAPE_DEFINITION_REPRESENTATION(#4, #10); +#4 = PRODUCT_DEFINITION_SHAPE('','', #5); +#5 = PRODUCT_DEFINITION('design','', #6, #9); +#6 = PRODUCT_DEFINITION_FORMATION('','', #7); +#7 = PRODUCT('','','', (#8)); +#8 = PRODUCT_CONTEXT('', #2, 'mechanical'); +#9 = PRODUCT_DEFINITION_CONTEXT('part definition', #2, 'design');\n")?; + let models_slice = IndexSliceDisplay(self.models.iter().map(|model| match model { + PreStepModel::Shell(x) => x.idx - 1, + PreStepModel::Solid(x) => x.idx, + })); + f.write_fmt(format_args!( + "#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('', {models_slice}, #11);\n" + ))?; + f.pad("#11 = ( + GEOMETRIC_REPRESENTATION_CONTEXT(3) + GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#15)) + GLOBAL_UNIT_ASSIGNED_CONTEXT((#12, #13, #14)) + REPRESENTATION_CONTEXT('Context #1', '3D Context with UNIT and UNCERTAINTY') +); +#12 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#13 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#14 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#15 = UNCERTAINTY_MEASURE_WITH_UNIT(1.0E-6, #12, 'distance_accuracy_value','confusion accuracy');\n" + )?; + self.models + .iter() + .try_for_each(|model| Display::fmt(model, f)) + } +}