mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 13:01:52 +00:00
topology: B-rep writer, part-file reader and the corpus parity harness (S1)
- topology::brep_data_write / geometry_write: write B-reps and geometry to the SMLib database format; topology::part_file: read part files. - difftest units topology.part_file and corpus.parity (whole upstream TestFiles corpus, Rust vs C++), with their shims and FFI, plus the corpus example. - Fixes found by the corpus in line, ellipse, plane, offset-surface, B-spline and surface code. - WR-449: one sphere_diff fillet face integrates 0.6 too much area upstream; its area and volume are exempted in corpus.parity, cause under investigation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJzQKZ9sjU1DhgS253e8J5
This commit is contained in:
@@ -149,6 +149,17 @@ fn main() -> ExitCode {
|
||||
if raw.first().map(String::as_str) == Some("--step-case") {
|
||||
return difftest::step::run_child(raw.get(1..).unwrap_or(&[]));
|
||||
}
|
||||
// Child of unit `corpus.parity`: one side on one model file
|
||||
// (see `difftest::units::corpus_parity::run_child`).
|
||||
if raw.first().map(String::as_str) == Some("--corpus-parity-child") {
|
||||
#[cfg(feature = "cpp")]
|
||||
return difftest::units::corpus_parity::run_child(raw.get(1..).unwrap_or(&[]));
|
||||
#[cfg(not(feature = "cpp"))]
|
||||
{
|
||||
eprintln!("difftest: --corpus-parity-child needs feature `cpp`");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
}
|
||||
// Child of `--fuzz`: one batch of fuzz cases (see `difftest::fuzz::run_child`).
|
||||
if raw.first().map(String::as_str) == Some("--fuzz-exec") {
|
||||
return fuzz::run_child(raw.get(1..).unwrap_or(&[]));
|
||||
|
||||
@@ -0,0 +1,745 @@
|
||||
//! Runner for unit `corpus.parity`: the Rust port is at least as good as upstream on
|
||||
//! every real model file under `upstream/TestFiles/pt_TestFiles` (188 `.smb` B-rep
|
||||
//! files and 74 `.smp` part files). C++: `solidcore_ffi::units::corpus_parity`. Rust:
|
||||
//! the readers (`SmBrep::ReadFromFile`, `SmBrepData::ReadPartFromFile`) and the
|
||||
//! measures of `crates/solidcore/examples/corpus`.
|
||||
//!
|
||||
//! One case per file. Each side reads the file and measures every B-rep: the kind
|
||||
//! (`IsManifoldSolid`, else `IsSheet`, else other; empty without faces, edges and
|
||||
//! vertices), the face, edge, vertex and loop counts, the area (`SmApiFaceComputeArea`'s
|
||||
//! per-face integration without its accuracy clamp) and, for a solid, the volume
|
||||
//! (`SmApiBrepComputeVolume`), at integration accuracy [`ACCURACY`]. Both sides run in
|
||||
//! child processes (`difftest --corpus-parity-child <cpp|rust> <path> <accuracy>`), so a
|
||||
//! crash or hang on one file fails only that file's side (timeout [`TIMEOUT`]).
|
||||
//!
|
||||
//! The file's class, decided on the Rust side (`run_cpp` only records the expected
|
||||
//! verdict):
|
||||
//! * `pass`: both succeed, with equal B-rep counts, kinds and counts, and areas and
|
||||
//! volumes within `10 * ACCURACY` relative (the integration accuracy, not the 1e-9
|
||||
//! of the ported units: the two integrations are separate); also when both fail.
|
||||
//! * `rust-better`: upstream fails (error status, crash or timeout) and the port
|
||||
//! succeeds. Not a failure: recorded as the diagnostic `rust_better` and printed as
|
||||
//! `corpus.parity: rust-better <file>`. So is a disagreement on a file listed in
|
||||
//! [`KNOWN_CPP_DEFECTS`] (upstream's value there is a recorded C++ defect) when every
|
||||
//! quantity outside the defect agrees; it also gets the diagnostic `cpp_defect`.
|
||||
//! * `REGRESSION`: upstream succeeds and the port fails or disagrees. The only
|
||||
//! failing class: the Rust output is the error `REGRESSION: <why>`.
|
||||
//! * deferred: upstream succeeds and the port stops at an `Unported` callee (the unit
|
||||
//! is `partial` until the callee is ported).
|
||||
//!
|
||||
//! A file whose read passes is then round-tripped on both sides: every B-rep written as
|
||||
//! an ASCII `.smb` (`SmBrep::WriteToFile`: `SmBrepData::FromBrep`, `WriteToFile`), read
|
||||
//! back into a new B-rep and measured again, and the read-back measures are compared
|
||||
//! the same way (children `cpp-roundtrip` / `rust-roundtrip`). Upstream's writer prints
|
||||
//! fixed-format doubles, so a read-back volume can drift from the read one by more than
|
||||
//! the integration accuracy; the two sides write the same bytes and must drift alike.
|
||||
|
||||
use std::io::Read as _;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, ExitCode, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
|
||||
use solidcore::geom::object::ObjectRef;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::mass::integrator::properties::{brep_volume, face_properties, PropertyAttributes};
|
||||
use solidcore::nurbs::b_spline_surface_n_lib::p2::OrientType;
|
||||
use solidcore::topology::brep::p1 as bl;
|
||||
use solidcore::topology::brep::p10::TopologyEnv;
|
||||
use solidcore::topology::brep::p2::{is_manifold_solid, is_sheet};
|
||||
use solidcore::topology::brep::p7::read_from_file;
|
||||
use solidcore::topology::brep_data::BrepData;
|
||||
use solidcore::topology::brep_data_write::FromBrepEnv;
|
||||
use solidcore::topology::database_io::FileType;
|
||||
use solidcore::topology::face::p2::{self as fl, PPF_AREA};
|
||||
use solidcore::topology::part_file::{read_part_from_file, PartEnv};
|
||||
use solidcore::topology::topology::Topology;
|
||||
use solidcore::topology::{Brep, EdgeId, EdgeuseId, FaceId};
|
||||
use solidcore::{EntityRef, SolidError};
|
||||
use solidcore_ffi::units::corpus_parity as ffi;
|
||||
|
||||
use crate::case::{CaseContext, CaseSource};
|
||||
use crate::{Case, HarnessError, Output, UnitDiff};
|
||||
|
||||
/// Integration accuracy of the areas and volumes (relative).
|
||||
pub const ACCURACY: f64 = 1e-6;
|
||||
/// Longest a side may take on one file.
|
||||
pub const TIMEOUT: Duration = Duration::from_secs(300);
|
||||
/// The child-process flag of `difftest`.
|
||||
pub const CHILD_FLAG: &str = "--corpus-parity-child";
|
||||
/// Deepest directory level searched below the test files.
|
||||
const MAX_DEPTH: usize = 32;
|
||||
/// Values per B-rep in a measurement (as [`ffi::PER_BREP`]).
|
||||
const PER_BREP: usize = ffi::PER_BREP;
|
||||
/// First identity of the geometry the readers make (topology identities stay below).
|
||||
const GEOMETRY_KEYS: u64 = 1 << 62;
|
||||
|
||||
/// `corpus.parity` runner.
|
||||
pub struct Runner;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Inputs {
|
||||
/// The model file, relative to `upstream/TestFiles/pt_TestFiles`.
|
||||
file: String,
|
||||
/// Its absolute path.
|
||||
path: String,
|
||||
}
|
||||
|
||||
// ---- the measurements -----------------------------------------------------------------
|
||||
|
||||
/// What one side made of a file.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum Measured {
|
||||
/// `read_status n` then [`PER_BREP`] values per B-rep (`kind faces edges vertices
|
||||
/// loops status area volume`; volume NaN unless a solid).
|
||||
Values(Vec<f64>),
|
||||
/// The port stopped at an unported callee.
|
||||
Unported { upstream: String, unit: String },
|
||||
/// The side failed: an error, a crash or a timeout (the reason).
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
impl Measured {
|
||||
/// The B-reps' records when the side read the file and measured every B-rep.
|
||||
fn breps(&self) -> Option<Vec<&[f64]>> {
|
||||
let Self::Values(v) = self else { return None };
|
||||
let (&status, rest) = v.split_first()?;
|
||||
let (&n, rest) = rest.split_first()?;
|
||||
if status != 0.0 || rest.len() != n as usize * PER_BREP {
|
||||
return None;
|
||||
}
|
||||
let breps: Vec<&[f64]> = rest.chunks(PER_BREP).collect();
|
||||
breps
|
||||
.iter()
|
||||
.all(|b| b.get(5) == Some(&0.0))
|
||||
.then_some(breps)
|
||||
}
|
||||
|
||||
/// Why the side did not succeed (for the report).
|
||||
fn why(&self) -> String {
|
||||
match self {
|
||||
Self::Values(v) => match v.split_first() {
|
||||
Some((&s, _)) if s != 0.0 => format!("read status {s}"),
|
||||
_ => "a B-rep's measurement failed".to_string(),
|
||||
},
|
||||
Self::Unported { upstream, unit } => format!("unported {upstream} ({unit})"),
|
||||
Self::Failed(why) => why.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `|a - b| <= 10 * ACCURACY * max(|a|, |b|)` (both NaN: no volume on either side).
|
||||
fn close(a: f64, b: f64) -> bool {
|
||||
if a.is_nan() && b.is_nan() {
|
||||
return true;
|
||||
}
|
||||
(a - b).abs() <= 10.0 * ACCURACY * a.abs().max(b.abs())
|
||||
}
|
||||
|
||||
/// The first disagreement between two successful measurements, if any.
|
||||
fn disagreement(cpp: &[&[f64]], rust: &[&[f64]]) -> Option<String> {
|
||||
disagreement_except(cpp, rust, &[])
|
||||
}
|
||||
|
||||
/// [`disagreement`] ignoring the quantities named in `skip`.
|
||||
fn disagreement_except(cpp: &[&[f64]], rust: &[&[f64]], skip: &[&str]) -> Option<String> {
|
||||
const NAMES: [&str; 5] = ["kind", "faces", "edges", "vertices", "loops"];
|
||||
if cpp.len() != rust.len() {
|
||||
return Some(format!("{} B-reps, upstream {}", rust.len(), cpp.len()));
|
||||
}
|
||||
for (i, (c, r)) in cpp.iter().zip(rust).enumerate() {
|
||||
for (j, name) in NAMES.iter().enumerate() {
|
||||
if !skip.contains(name) && c.get(j) != r.get(j) {
|
||||
return Some(format!(
|
||||
"B-rep {i}: {name} {:?}, upstream {:?}",
|
||||
r.get(j),
|
||||
c.get(j)
|
||||
));
|
||||
}
|
||||
}
|
||||
for (j, name) in [(6, "area"), (7, "volume")] {
|
||||
if skip.contains(&name) {
|
||||
continue;
|
||||
}
|
||||
let (a, b) = (c.get(j).copied(), r.get(j).copied());
|
||||
match (a, b) {
|
||||
(Some(a), Some(b)) if close(a, b) => {}
|
||||
_ => return Some(format!("B-rep {i}: {name} {b:?}, upstream {a:?}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Files where upstream's result is a known C++ defect and the port's is right: the file,
|
||||
/// the quantities the defect affects (empty: the whole result) and the evidence.
|
||||
const KNOWN_CPP_DEFECTS: [(&str, &[&str], &str); 2] = [
|
||||
(
|
||||
"Fillet/sphere_diff_3cylinders.smb",
|
||||
&["area", "volume"],
|
||||
"plan/upstream-issues/WR-449--sphere-diff-3cylinders-one-fillet-face-integrates-0-6-too-large.md: \
|
||||
upstream's fillet face 7 integrates 0.60 more area and 2.14 more volume than its five \
|
||||
symmetric siblings, which the port matches; the trims differ on edge 30, made on demand \
|
||||
(CreateUVTrimCurve / DropCurve / sm_FixUVTrimCurves), the diverging step not yet pinned",
|
||||
),
|
||||
(
|
||||
"Solids/fillet_ts.smb",
|
||||
&[],
|
||||
"plan/upstream-issues.md ND-257 (UB-254): upstream's B-rep depends on the allocator; \
|
||||
without address reuse it is the port's",
|
||||
),
|
||||
];
|
||||
|
||||
/// The evidence when `file`'s disagreement is a known C++ defect: everything outside the
|
||||
/// defect's quantities agrees.
|
||||
fn known_cpp_defect(file: &str, cpp: &[&[f64]], rust: &[&[f64]]) -> Option<&'static str> {
|
||||
KNOWN_CPP_DEFECTS
|
||||
.iter()
|
||||
.find(|(f, _, _)| *f == file)
|
||||
.and_then(|(_, quantities, why)| {
|
||||
(quantities.is_empty() || disagreement_except(cpp, rust, quantities).is_none())
|
||||
.then_some(*why)
|
||||
})
|
||||
}
|
||||
|
||||
// ---- the Rust side --------------------------------------------------------------------
|
||||
|
||||
/// A distinct identity for every topology object of B-rep `brep` the readers build
|
||||
/// (as the corpus example gives them).
|
||||
fn key_of(brep: usize, e: EntityRef) -> ObjectRef {
|
||||
let (type_code, kind, i) = match e {
|
||||
EntityRef::Brep(x) => (16011, 0, x.index()),
|
||||
EntityRef::Region(x) => (16012, 1, x.index()),
|
||||
EntityRef::Shell(x) => (16013, 2, x.index()),
|
||||
EntityRef::Faceuse(x) => (16022, 3, x.index()),
|
||||
EntityRef::Face(x) => (16002, 4, x.index()),
|
||||
EntityRef::Loopuse(x) => (16004, 5, x.index()),
|
||||
EntityRef::Loop(x) => (16003, 6, x.index()),
|
||||
EntityRef::Edgeuse(x) => (16005, 7, x.index()),
|
||||
EntityRef::Edge(x) => (16016, 8, x.index()),
|
||||
EntityRef::Vertexuse(x) => (16006, 9, x.index()),
|
||||
EntityRef::Vertex(x) => (16021, 10, x.index()),
|
||||
};
|
||||
let brep = (brep as u64 + 1) << 36;
|
||||
ObjectRef {
|
||||
type_code,
|
||||
key: ObjectKey(brep | ((kind + 1) << 32) | u64::from(i)),
|
||||
}
|
||||
}
|
||||
|
||||
/// A model file as read: its B-reps and the records and attributes the readers made.
|
||||
struct Read {
|
||||
breps: Vec<Brep>,
|
||||
attributes: AttributeStore,
|
||||
records: std::collections::BTreeMap<ObjectKey, Topology>,
|
||||
}
|
||||
|
||||
/// The B-reps of a model file: one from a B-rep file, any number from a part file.
|
||||
fn read_breps(k: &mut Kernel, bytes: Vec<u8>) -> solidcore::Result<Read> {
|
||||
let mut records = std::collections::BTreeMap::new();
|
||||
let mut attributes = AttributeStore::default();
|
||||
if bytes.starts_with(b"//[Output Summary]") {
|
||||
let mut env = PartEnv {
|
||||
attributes: &mut attributes,
|
||||
key_of: &key_of,
|
||||
records: &mut records,
|
||||
next_key: GEOMETRY_KEYS,
|
||||
};
|
||||
let breps = read_part_from_file(k, bytes, FileType::Ascii, &mut env)?.breps;
|
||||
return Ok(Read {
|
||||
breps,
|
||||
attributes,
|
||||
records,
|
||||
});
|
||||
}
|
||||
let mut b = Brep::new_with_infinite_region()?;
|
||||
let one = |e| key_of(0, e);
|
||||
let mut env = TopologyEnv {
|
||||
all_attributes: &[],
|
||||
attributes: &mut attributes,
|
||||
key_of: &one,
|
||||
records: &mut records,
|
||||
next_key: GEOMETRY_KEYS,
|
||||
};
|
||||
read_from_file(k, &mut b, bytes, FileType::Ascii, false, &mut env)?;
|
||||
Ok(Read {
|
||||
breps: vec![b],
|
||||
attributes,
|
||||
records,
|
||||
})
|
||||
}
|
||||
|
||||
/// The area at `accuracy`: `SmApiFaceComputeArea`'s per-face integration without its
|
||||
/// accuracy clamp.
|
||||
fn integrated_area(
|
||||
k: &Kernel,
|
||||
b: &mut Brep,
|
||||
faces: &[FaceId],
|
||||
accuracy: f64,
|
||||
) -> solidcore::Result<f64> {
|
||||
let mut area = 0.0;
|
||||
for f in faces {
|
||||
let bbox = solidcore::query::face::p1::calculate_bounding_box(k, &*b, *f, false)?;
|
||||
let s = bbox.size();
|
||||
let d = s.x.max(s.y).max(s.z);
|
||||
let p = face_properties(
|
||||
k,
|
||||
b,
|
||||
*f,
|
||||
OrientType::Same,
|
||||
accuracy,
|
||||
bbox.mid(),
|
||||
d * d / 100.0,
|
||||
0.0,
|
||||
PPF_AREA,
|
||||
)?;
|
||||
area += p.area.abs();
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
|
||||
/// One B-rep's record (as the shim's `measure`).
|
||||
fn measure(
|
||||
k: &mut Kernel,
|
||||
b: &mut Brep,
|
||||
accuracy: f64,
|
||||
out: &mut Vec<f64>,
|
||||
) -> solidcore::Result<()> {
|
||||
let faces = bl::faces(b, None)?;
|
||||
let edges = bl::edges(b, None)?;
|
||||
let vertices = bl::vertices(b, None)?;
|
||||
let mut loops = 0;
|
||||
for f in &faces {
|
||||
loops += fl::loops(b, *f, false, None)?.len();
|
||||
}
|
||||
let kind = if faces.is_empty() && edges.is_empty() && vertices.is_empty() {
|
||||
0.0
|
||||
} else if is_manifold_solid(b)? {
|
||||
1.0
|
||||
} else if is_sheet(k, b)? {
|
||||
2.0
|
||||
} else {
|
||||
3.0
|
||||
};
|
||||
let area = integrated_area(k, b, &faces, accuracy)?;
|
||||
let mut volume = f64::NAN;
|
||||
if kind == 1.0 {
|
||||
let store = AttributeStore::new();
|
||||
let key = |_| ObjectKey(0);
|
||||
let attributes = PropertyAttributes {
|
||||
store: &store,
|
||||
key_of: &key,
|
||||
};
|
||||
volume = brep_volume(k, b, accuracy, &attributes)?;
|
||||
}
|
||||
out.extend([
|
||||
kind,
|
||||
faces.len() as f64,
|
||||
edges.len() as f64,
|
||||
vertices.len() as f64,
|
||||
loops as f64,
|
||||
0.0,
|
||||
area,
|
||||
volume,
|
||||
]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The Rust side on one file.
|
||||
fn measure_rust(path: &Path, accuracy: f64) -> Measured {
|
||||
let bytes = match std::fs::read(path) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return Measured::Failed(format!("read: {e}")),
|
||||
};
|
||||
let fail = |stage: &str, e: SolidError| match e {
|
||||
SolidError::Unported { upstream, unit } => Measured::Unported {
|
||||
upstream: upstream.to_string(),
|
||||
unit: unit.to_string(),
|
||||
},
|
||||
e => Measured::Failed(format!("{stage}: {e}")),
|
||||
};
|
||||
let mut k = Kernel::default();
|
||||
let mut read = match read_breps(&mut k, bytes) {
|
||||
Ok(r) => r,
|
||||
Err(e) => return fail("read", e),
|
||||
};
|
||||
let mut out = vec![0.0, read.breps.len() as f64];
|
||||
for b in &mut read.breps {
|
||||
if let Err(e) = measure(&mut k, b, accuracy, &mut out) {
|
||||
return fail("measure", e);
|
||||
}
|
||||
}
|
||||
Measured::Values(out)
|
||||
}
|
||||
|
||||
/// The Rust side's round trip of one file, as the shim's `sc_corpus_parity_roundtrip`:
|
||||
/// every B-rep flattened (`SmBrepData::FromBrep`), written as ASCII (`WriteToFile`, which
|
||||
/// makes a lazy surface's NURB as upstream's `GetOrCreateGwNurbPointer` does), read back
|
||||
/// into a new B-rep and measured.
|
||||
fn measure_rust_roundtrip(path: &Path, accuracy: f64) -> Measured {
|
||||
let bytes = match std::fs::read(path) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return Measured::Failed(format!("read: {e}")),
|
||||
};
|
||||
let fail = |stage: &str, e: SolidError| match e {
|
||||
SolidError::Unported { upstream, unit } => Measured::Unported {
|
||||
upstream: upstream.to_string(),
|
||||
unit: unit.to_string(),
|
||||
},
|
||||
e => Measured::Failed(format!("{stage}: {e}")),
|
||||
};
|
||||
let mut k = Kernel::default();
|
||||
let read = match read_breps(&mut k, bytes) {
|
||||
Ok(r) => r,
|
||||
Err(e) => return fail("read", e),
|
||||
};
|
||||
let mut out = vec![0.0, read.breps.len() as f64];
|
||||
for (i, b) in read.breps.iter().enumerate() {
|
||||
let key = |e| key_of(i, e);
|
||||
let env = FromBrepEnv {
|
||||
attributes: &read.attributes,
|
||||
key_of: &key,
|
||||
records: &read.records,
|
||||
curve_key: &|_: EdgeId| ObjectKey(0),
|
||||
uv_curve_key: &|_: EdgeuseId| ObjectKey(0),
|
||||
surface_key: &|_: FaceId| ObjectKey(0),
|
||||
};
|
||||
let mut all = Vec::new();
|
||||
let written = BrepData::from_brep(b, &env, &mut all)
|
||||
.and_then(|mut data| data.write_to_file_with_nurbs(&k, FileType::Ascii));
|
||||
let bytes = match written {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => return fail("write", e),
|
||||
};
|
||||
let mut back = match read_breps(&mut k, bytes) {
|
||||
Ok(r) => r,
|
||||
Err(e) => return fail("read back", e),
|
||||
};
|
||||
let Some(b2) = back.breps.first_mut() else {
|
||||
return Measured::Failed("read back: no B-rep".to_string());
|
||||
};
|
||||
if let Err(e) = measure(&mut k, b2, accuracy, &mut out) {
|
||||
return fail("measure read back", e);
|
||||
}
|
||||
}
|
||||
Measured::Values(out)
|
||||
}
|
||||
|
||||
// ---- the child processes --------------------------------------------------------------
|
||||
|
||||
/// `difftest --corpus-parity-child <cpp|rust|cpp-roundtrip|rust-roundtrip> <path>
|
||||
/// <accuracy>`: measures one file (or its ASCII round trip) on one side and prints one line (`CP <values..>`, `CP-UNPORTED <upstream> <unit>` or
|
||||
/// `CP-ERROR <why>`, tab separated).
|
||||
pub fn run_child(args: &[String]) -> ExitCode {
|
||||
let (Some(side), Some(path), Some(accuracy)) = (args.first(), args.get(1), args.get(2)) else {
|
||||
eprintln!(
|
||||
"usage: difftest {CHILD_FLAG} <cpp|rust|cpp-roundtrip|rust-roundtrip> <path> <accuracy>"
|
||||
);
|
||||
return ExitCode::from(2);
|
||||
};
|
||||
let Ok(accuracy) = accuracy.parse::<f64>() else {
|
||||
eprintln!("{CHILD_FLAG}: bad accuracy {accuracy:?}");
|
||||
return ExitCode::from(2);
|
||||
};
|
||||
let m = match side.as_str() {
|
||||
"cpp" => match ffi::measure(path, accuracy) {
|
||||
Ok(v) => Measured::Values(v),
|
||||
Err(e) => Measured::Failed(format!("shim: {e}")),
|
||||
},
|
||||
"rust" => measure_rust(Path::new(path), accuracy),
|
||||
"cpp-roundtrip" => match PrivateDir::new() {
|
||||
// (the shim writes and reads the file by name: only inside a directory of
|
||||
// our own, never at a guessable path another user could have planted)
|
||||
Ok(dir) => {
|
||||
let tmp = dir.0.join("roundtrip.smb");
|
||||
match ffi::roundtrip(path, &tmp.to_string_lossy(), accuracy) {
|
||||
Ok(v) => Measured::Values(v),
|
||||
Err(e) => Measured::Failed(format!("shim: {e}")),
|
||||
}
|
||||
}
|
||||
Err(e) => Measured::Failed(format!("private temp dir: {e}")),
|
||||
},
|
||||
"rust-roundtrip" => measure_rust_roundtrip(Path::new(path), accuracy),
|
||||
_ => {
|
||||
eprintln!("{CHILD_FLAG}: side must be cpp, rust, cpp-roundtrip or rust-roundtrip");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
match m {
|
||||
Measured::Values(v) => {
|
||||
let s: Vec<String> = v.iter().map(|x| format!("{x:e}")).collect();
|
||||
println!("CP\t{}", s.join("\t"));
|
||||
}
|
||||
Measured::Unported { upstream, unit } => println!("CP-UNPORTED\t{upstream}\t{unit}"),
|
||||
Measured::Failed(why) => println!("CP-ERROR\t{}", why.replace(['\n', '\t'], " ")),
|
||||
}
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
/// A directory only this user can reach (mode 0700 on Unix), made fresh under the
|
||||
/// system temp directory for the C++ round trip's file and removed with everything in it
|
||||
/// when dropped (on every return path). `create_dir` fails on any existing entry,
|
||||
/// a planted symlink included, and the next name is tried.
|
||||
struct PrivateDir(PathBuf);
|
||||
|
||||
impl PrivateDir {
|
||||
fn new() -> std::io::Result<Self> {
|
||||
let base = std::env::temp_dir();
|
||||
let nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_or(0, |d| d.subsec_nanos());
|
||||
let mut last = None;
|
||||
for attempt in 0..64_u32 {
|
||||
let dir = base.join(format!(
|
||||
"corpus-parity-{}-{nanos:09}-{attempt}",
|
||||
std::process::id()
|
||||
));
|
||||
let mut builder = std::fs::DirBuilder::new();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::DirBuilderExt;
|
||||
builder.mode(0o700);
|
||||
}
|
||||
match builder.create(&dir) {
|
||||
Ok(()) => return Ok(Self(dir)),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => last = Some(e),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Err(last.unwrap_or_else(|| std::io::Error::other("no free name")))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PrivateDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs one side on `path` in a child process, with [`TIMEOUT`].
|
||||
fn measure_isolated(side: &str, path: &str) -> Measured {
|
||||
let exe = match std::env::current_exe() {
|
||||
Ok(e) => e,
|
||||
Err(e) => return Measured::Failed(format!("current_exe: {e}")),
|
||||
};
|
||||
let child = Command::new(exe)
|
||||
.arg(CHILD_FLAG)
|
||||
.arg(side)
|
||||
.arg(path)
|
||||
.arg(ACCURACY.to_string())
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn();
|
||||
let mut child = match child {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Measured::Failed(format!("spawn: {e}")),
|
||||
};
|
||||
let reader = child.stdout.take().map(|mut out| {
|
||||
std::thread::spawn(move || {
|
||||
let mut s = String::new();
|
||||
let _ = out.read_to_string(&mut s);
|
||||
s
|
||||
})
|
||||
});
|
||||
let start = Instant::now();
|
||||
let status = loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(s)) => break Some(s),
|
||||
Ok(None) if start.elapsed() > TIMEOUT => {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
break None;
|
||||
}
|
||||
Ok(None) => std::thread::sleep(Duration::from_millis(20)),
|
||||
Err(e) => return Measured::Failed(format!("wait: {e}")),
|
||||
}
|
||||
};
|
||||
let stdout = reader.and_then(|h| h.join().ok()).unwrap_or_default();
|
||||
let Some(status) = status else {
|
||||
return Measured::Failed(format!("timeout after {}s", TIMEOUT.as_secs()));
|
||||
};
|
||||
if !status.success() {
|
||||
return Measured::Failed(format!("crash ({status})"));
|
||||
}
|
||||
parse_child(&stdout)
|
||||
}
|
||||
|
||||
/// The measurement a child printed.
|
||||
fn parse_child(stdout: &str) -> Measured {
|
||||
for line in stdout.lines() {
|
||||
let mut fields = line.split('\t');
|
||||
match fields.next() {
|
||||
Some("CP") => {
|
||||
let v: Result<Vec<f64>, _> = fields.map(str::parse::<f64>).collect();
|
||||
return match v {
|
||||
Ok(v) => Measured::Values(v),
|
||||
Err(e) => Measured::Failed(format!("bad child output: {e}")),
|
||||
};
|
||||
}
|
||||
Some("CP-UNPORTED") => {
|
||||
return Measured::Unported {
|
||||
upstream: fields.next().unwrap_or_default().to_string(),
|
||||
unit: fields.next().unwrap_or_default().to_string(),
|
||||
};
|
||||
}
|
||||
Some("CP-ERROR") => {
|
||||
return Measured::Failed(fields.next().unwrap_or_default().to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Measured::Failed("no result from the child".to_string())
|
||||
}
|
||||
|
||||
// ---- the cases ------------------------------------------------------------------------
|
||||
|
||||
/// The `.smb` and `.smp` files below `dir` (relative paths, sorted).
|
||||
fn model_files(dir: &Path) -> Vec<(PathBuf, String)> {
|
||||
let mut out = Vec::new();
|
||||
let mut stack = vec![(dir.to_path_buf(), 0usize)];
|
||||
while let Some((d, depth)) = stack.pop() {
|
||||
let Ok(rd) = std::fs::read_dir(&d) else {
|
||||
continue;
|
||||
};
|
||||
for e in rd.flatten() {
|
||||
let p = e.path();
|
||||
if p.is_dir() {
|
||||
if depth < MAX_DEPTH {
|
||||
stack.push((p, depth + 1));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let ext = p
|
||||
.extension()
|
||||
.and_then(|x| x.to_str())
|
||||
.map(str::to_ascii_lowercase);
|
||||
if matches!(ext.as_deref(), Some("smb" | "smp")) {
|
||||
if let Ok(rel) = p.strip_prefix(dir) {
|
||||
out.push((p.clone(), rel.to_string_lossy().replace('\\', "/")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
out
|
||||
}
|
||||
|
||||
/// The round trip's verdict on a file whose read passed (`read`, kept when the round trip
|
||||
/// passes too): each side writes every B-rep as ASCII and reads it back
|
||||
/// (`cpp-roundtrip` / `rust-roundtrip` children), and the read-back measures are compared
|
||||
/// as the read ones are. A disagreement is a `REGRESSION` (upstream's ASCII writer drops
|
||||
/// digits, so the read-back volume can move from the read one; both sides must move
|
||||
/// alike), unless it is a known C++ defect of the file.
|
||||
fn roundtrip_verdict(i: &Inputs, read: Output) -> Output {
|
||||
let cpp = measure_isolated("cpp-roundtrip", &i.path);
|
||||
let rust = measure_isolated("rust-roundtrip", &i.path);
|
||||
match (cpp.breps(), rust.breps()) {
|
||||
(Some(c), Some(r)) => match disagreement(&c, &r) {
|
||||
None => read.diagnostic("roundtrip_pass", 1),
|
||||
Some(why) => match known_cpp_defect(&i.file, &c, &r) {
|
||||
Some(_) => read.diagnostic("roundtrip_cpp_defect", 1),
|
||||
None => Output::err(format!("REGRESSION: round trip: {why}")),
|
||||
},
|
||||
},
|
||||
(Some(_), None) => match rust {
|
||||
Measured::Unported { upstream, unit } => Output::deferred(unit, upstream),
|
||||
_ => Output::err(format!("REGRESSION: round trip: {}", rust.why())),
|
||||
},
|
||||
(None, Some(_)) => {
|
||||
eprintln!(
|
||||
"corpus.parity: rust-better {} (upstream round trip: {})",
|
||||
i.file,
|
||||
cpp.why()
|
||||
);
|
||||
read.diagnostic("roundtrip_rust_better", 1)
|
||||
}
|
||||
(None, None) => read.diagnostic("roundtrip_both_fail", 1),
|
||||
}
|
||||
}
|
||||
|
||||
impl UnitDiff for Runner {
|
||||
fn id(&self) -> &'static str {
|
||||
"corpus.parity"
|
||||
}
|
||||
|
||||
fn cases(&self, ctx: &CaseContext) -> Result<Vec<Case>, HarnessError> {
|
||||
let dir = ctx.root.join(crate::fixtures::UPSTREAM_TEST_FILES);
|
||||
let files = model_files(&dir);
|
||||
if files.is_empty() {
|
||||
return Err(HarnessError::new(format!(
|
||||
"no .smb/.smp files under {}",
|
||||
dir.display()
|
||||
)));
|
||||
}
|
||||
files
|
||||
.into_iter()
|
||||
.map(|(path, file)| {
|
||||
let name = file.replace('/', "__");
|
||||
let inputs = Inputs {
|
||||
file,
|
||||
path: path.to_string_lossy().into_owned(),
|
||||
};
|
||||
Ok(Case {
|
||||
unit: self.id().to_string(),
|
||||
name,
|
||||
source: CaseSource::Inline,
|
||||
inputs: serde_json::to_value(inputs)
|
||||
.map_err(|e| HarnessError::new(e.to_string()))?,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The verdict expected of every file: no regression (the measurements run on the
|
||||
/// Rust side, which classifies the file).
|
||||
fn run_cpp(&self, _case: &Case) -> Result<Output, HarnessError> {
|
||||
Ok(Output::new().int("regression", 0))
|
||||
}
|
||||
|
||||
fn run_rust(&self, case: &Case) -> Result<Output, HarnessError> {
|
||||
let i: Inputs = case.parse()?;
|
||||
let cpp = measure_isolated("cpp", &i.path);
|
||||
let rust = measure_isolated("rust", &i.path);
|
||||
let ok = Output::new().int("regression", 0);
|
||||
let read = match (cpp.breps(), rust.breps()) {
|
||||
(Some(c), Some(r)) => match disagreement(&c, &r) {
|
||||
None => ok.diagnostic("pass", 1),
|
||||
Some(why) => match known_cpp_defect(&i.file, &c, &r) {
|
||||
Some(defect) => {
|
||||
eprintln!(
|
||||
"corpus.parity: rust-better {} (known C++ defect: {why}; {defect})",
|
||||
i.file
|
||||
);
|
||||
ok.diagnostic("rust_better", 1).diagnostic("cpp_defect", 1)
|
||||
}
|
||||
None => Output::err(format!("REGRESSION: {why}")),
|
||||
},
|
||||
},
|
||||
(Some(_), None) => match rust {
|
||||
Measured::Unported { upstream, unit } => Output::deferred(unit, upstream),
|
||||
_ => Output::err(format!("REGRESSION: {}", rust.why())),
|
||||
},
|
||||
(None, Some(_)) => {
|
||||
eprintln!(
|
||||
"corpus.parity: rust-better {} (upstream: {})",
|
||||
i.file,
|
||||
cpp.why()
|
||||
);
|
||||
ok.diagnostic("rust_better", 1)
|
||||
}
|
||||
(None, None) => ok.diagnostic("both_fail", 1),
|
||||
};
|
||||
if read.error.is_some() || read.deferred.is_some() {
|
||||
return Ok(read);
|
||||
}
|
||||
Ok(roundtrip_verdict(&i, read))
|
||||
}
|
||||
|
||||
fn generate(&self, _rng: &mut crate::Rng) -> Option<Value> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
//! Difftest runner for the part reader (`topology::part_file`, outside the plan:
|
||||
//! `SmBrepData::ReadPartFromFile` is not a plan unit, see `plan/findings.md`). Run it
|
||||
//! with `--unit topology.part_file`; `--green` does not include it.
|
||||
//!
|
||||
//! One case per upstream part file under `upstream/TestFiles/pt_TestFiles` (the `.smp`
|
||||
//! files and the `.smb` files that start with `//[Output Summary]`). Both sides read the
|
||||
//! file and dump, as `out`: the stand-alone curves (type, dimension, natural interval),
|
||||
//! the surfaces (type, natural UV domain), the Boolean tree nodes and per B-rep its
|
||||
//! tolerance, manifold-solid flag and region, shell, face, loop, edge and vertex counts
|
||||
//! (layout in `solidcore_ffi::units::topology_part_file`). A read failure is compared
|
||||
//! as a status only. The B-reps go through `MakeTopologyFromData`, so a case defers
|
||||
//! wherever that path still reaches an unported call.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
|
||||
use solidcore::geom::object::ObjectRef;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::topology::brep::p1 as bl;
|
||||
use solidcore::topology::brep::p2::is_manifold_solid;
|
||||
use solidcore::topology::database_io::FileType;
|
||||
use solidcore::topology::face::p2 as fl;
|
||||
use solidcore::topology::part_file::{read_part_from_file, Part, PartEnv};
|
||||
use solidcore::{EntityRef, SolidError};
|
||||
use solidcore_ffi::units::topology_part_file as ffi;
|
||||
|
||||
use crate::case::{repo_root, CaseContext};
|
||||
use crate::fixtures::UPSTREAM_TEST_FILES;
|
||||
use crate::{Case, CaseSource, HarnessError, Output, UnitDiff};
|
||||
|
||||
/// Status output of a failed read (compared on both sides).
|
||||
const READ_FAILED: &str = "read failed";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct Inputs {
|
||||
/// The part file, relative to `upstream/TestFiles/pt_TestFiles`.
|
||||
file: String,
|
||||
}
|
||||
|
||||
/// Differential runner for `SmBrepData::ReadPartFromFile`.
|
||||
pub struct Runner;
|
||||
|
||||
fn path_of(rel: &str) -> String {
|
||||
repo_root()
|
||||
.join(UPSTREAM_TEST_FILES)
|
||||
.join(rel)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Every part file under the upstream test files, sorted.
|
||||
fn part_files() -> Vec<String> {
|
||||
let root = repo_root().join(UPSTREAM_TEST_FILES);
|
||||
let mut out = Vec::new();
|
||||
let mut stack = vec![root.clone()];
|
||||
while let Some(dir) = stack.pop() {
|
||||
let Ok(rd) = std::fs::read_dir(&dir) else {
|
||||
continue;
|
||||
};
|
||||
for entry in rd.flatten() {
|
||||
let p = entry.path();
|
||||
if p.is_dir() {
|
||||
stack.push(p);
|
||||
} else if is_part_file(&p) {
|
||||
if let Ok(rel) = p.strip_prefix(&root) {
|
||||
out.push(rel.to_string_lossy().replace('\\', "/"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
fn is_part_file(p: &Path) -> bool {
|
||||
let ext = p.extension().and_then(|x| x.to_str()).unwrap_or_default();
|
||||
(ext == "smp" || ext == "smb")
|
||||
&& std::fs::read(p).is_ok_and(|b| b.starts_with(b"//[Output Summary]"))
|
||||
}
|
||||
|
||||
/// The identity of topology entity `e` of B-rep `brep` (geometry keys start at `1 << 62`).
|
||||
fn key_of(brep: usize, e: EntityRef) -> ObjectRef {
|
||||
let (type_code, kind, i) = match e {
|
||||
EntityRef::Brep(x) => (16011, 0, x.index()),
|
||||
EntityRef::Region(x) => (16012, 1, x.index()),
|
||||
EntityRef::Shell(x) => (16013, 2, x.index()),
|
||||
EntityRef::Faceuse(x) => (16022, 3, x.index()),
|
||||
EntityRef::Face(x) => (16002, 4, x.index()),
|
||||
EntityRef::Loopuse(x) => (16004, 5, x.index()),
|
||||
EntityRef::Loop(x) => (16003, 6, x.index()),
|
||||
EntityRef::Edgeuse(x) => (16005, 7, x.index()),
|
||||
EntityRef::Edge(x) => (16016, 8, x.index()),
|
||||
EntityRef::Vertexuse(x) => (16006, 9, x.index()),
|
||||
EntityRef::Vertex(x) => (16021, 10, x.index()),
|
||||
};
|
||||
ObjectRef {
|
||||
type_code,
|
||||
key: ObjectKey(((brep as u64 + 1) << 36) | ((kind + 1) << 32) | u64::from(i)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The dump of a part read on the Rust side (the shim's layout).
|
||||
fn dump(k: &mut Kernel, part: &Part) -> Result<Vec<f64>, SolidError> {
|
||||
let mut o = vec![0.0, part.curves.len() as f64];
|
||||
for c in &part.curves {
|
||||
let ivl = c.object.natural_interval();
|
||||
let (ty, dim) = (c.object.type_code() as f64, c.object.dim() as f64);
|
||||
o.extend([ty, dim, ivl.min(), ivl.max()]);
|
||||
}
|
||||
o.push(part.surfaces.len() as f64);
|
||||
for s in &part.surfaces {
|
||||
let d = s.object.natural_uv_domain();
|
||||
let (lo, hi) = (d.min(), d.max());
|
||||
o.extend([s.object.type_code() as f64, lo.x, lo.y, hi.x, hi.y]);
|
||||
}
|
||||
o.push(part.boolean_tree_nodes.len() as f64);
|
||||
o.extend(part.boolean_tree_nodes.iter().map(|&n| n as f64));
|
||||
o.push(part.breps.len() as f64);
|
||||
for b in &part.breps {
|
||||
let faces = bl::faces(b, None)?;
|
||||
let mut loops = 0usize;
|
||||
for &f in &faces {
|
||||
loops += fl::loops(b, f, false, None)?.len();
|
||||
}
|
||||
o.push(b.tolerance());
|
||||
o.push(f64::from(u8::from(is_manifold_solid(b)?)));
|
||||
o.push(bl::regions(b, None)?.len() as f64);
|
||||
o.push(bl::shells(k, b, None)?.len() as f64);
|
||||
o.push(faces.len() as f64);
|
||||
o.push(loops as f64);
|
||||
o.push(bl::edges(b, None)?.len() as f64);
|
||||
o.push(bl::vertices(b, None)?.len() as f64);
|
||||
}
|
||||
Ok(o)
|
||||
}
|
||||
|
||||
fn rust(rel: &str) -> Result<Output, HarnessError> {
|
||||
let bytes =
|
||||
std::fs::read(path_of(rel)).map_err(|e| HarnessError::new(format!("{rel}: {e}")))?;
|
||||
let mut k = Kernel::default();
|
||||
let mut store = AttributeStore::new();
|
||||
let mut records = BTreeMap::new();
|
||||
let mut env = PartEnv {
|
||||
attributes: &mut store,
|
||||
key_of: &key_of,
|
||||
records: &mut records,
|
||||
next_key: 1 << 62,
|
||||
};
|
||||
let result = read_part_from_file(&mut k, bytes, FileType::Ascii, &mut env)
|
||||
.and_then(|part| dump(&mut k, &part));
|
||||
Ok(match result {
|
||||
Ok(v) => Output::new().values("out", v),
|
||||
Err(e) => Output::unported(&e).unwrap_or_else(|| Output::err(READ_FAILED)),
|
||||
})
|
||||
}
|
||||
|
||||
fn cpp(rel: &str) -> Result<Output, HarnessError> {
|
||||
let v = ffi::run(&path_of(rel)).map_err(|e| HarnessError::new(format!("c++: {e}")))?;
|
||||
Ok(match v.first() {
|
||||
Some(&0.0) => Output::new().values("out", v),
|
||||
st => {
|
||||
// (the status only goes to stderr: both sides' errors compare as READ_FAILED)
|
||||
eprintln!(
|
||||
"topology.part_file: c++ {rel}: status {st:?}, {} values",
|
||||
v.len()
|
||||
);
|
||||
Output::err(READ_FAILED)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl UnitDiff for Runner {
|
||||
fn id(&self) -> &'static str {
|
||||
"topology.part_file"
|
||||
}
|
||||
|
||||
fn cases(&self, _ctx: &CaseContext) -> Result<Vec<Case>, HarnessError> {
|
||||
Ok(part_files()
|
||||
.into_iter()
|
||||
.map(|rel| Case {
|
||||
unit: self.id().to_string(),
|
||||
name: rel.clone(),
|
||||
source: CaseSource::Inline,
|
||||
inputs: serde_json::to_value(Inputs { file: rel }).unwrap_or(Value::Null),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn run_cpp(&self, case: &Case) -> Result<Output, HarnessError> {
|
||||
let i: Inputs = case.parse()?;
|
||||
cpp(&i.file)
|
||||
}
|
||||
|
||||
fn run_rust(&self, case: &Case) -> Result<Output, HarnessError> {
|
||||
let i: Inputs = case.parse()?;
|
||||
rust(&i.file)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//! FFI wrappers for unit `corpus.parity`.
|
||||
//!
|
||||
//! Shim: `harness/cpp/shim/corpus_parity.cpp` (`sc_corpus_parity_measure`): upstream's
|
||||
//! `SmBrep::ReadFromFile` / `SmBrepData::ReadPartFromFile` on one model file, then per
|
||||
//! B-rep `IsManifoldSolid` / `IsSheet`, the counts, the per-face area integration of
|
||||
//! `SmApiFaceComputeArea` (without its accuracy clamp) and `SmApiBrepComputeVolume`.
|
||||
//!
|
||||
//! | Function | Output |
|
||||
//! |-------------|--------|
|
||||
//! | [`measure`] | `read_status n`, then per B-rep `kind faces edges vertices loops status area volume` |
|
||||
//! | [`roundtrip`] | as [`measure`], each B-rep measured after `SmBrep::WriteToFile` (ASCII) and `ReadFromFile` |
|
||||
//!
|
||||
//! `kind` is 0 empty, 1 solid, 2 sheet, 3 other; `status` is 0 or the first failing
|
||||
//! measurement's `SmStatus`; `volume` is NaN unless the B-rep is a solid. The objects
|
||||
//! read are leaked: call it from a short-lived process.
|
||||
|
||||
use crate::{call_with_buffer, FfiError};
|
||||
|
||||
mod sys {
|
||||
extern "C" {
|
||||
pub fn sc_corpus_parity_measure(
|
||||
path: *const std::ffi::c_char,
|
||||
accuracy: f64,
|
||||
out: *mut f64,
|
||||
cap: i64,
|
||||
out_len: *mut i64,
|
||||
) -> i32;
|
||||
pub fn sc_corpus_parity_roundtrip(
|
||||
path: *const std::ffi::c_char,
|
||||
tmp: *const std::ffi::c_char,
|
||||
accuracy: f64,
|
||||
out: *mut f64,
|
||||
cap: i64,
|
||||
out_len: *mut i64,
|
||||
) -> i32;
|
||||
}
|
||||
}
|
||||
|
||||
/// Values per B-rep in [`measure`]'s output.
|
||||
pub const PER_BREP: usize = 8;
|
||||
|
||||
/// Reads model file `path` with upstream's reader and measures every B-rep at
|
||||
/// integration accuracy `accuracy` (see the module docs for the layout).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`FfiError::InvalidArgument`] for a path with a NUL byte or a non-positive or
|
||||
/// non-finite accuracy; [`FfiError::Status`] for a shim failure.
|
||||
pub fn measure(path: &str, accuracy: f64) -> Result<Vec<f64>, FfiError> {
|
||||
if !(accuracy.is_finite() && accuracy > 0.0) {
|
||||
return Err(FfiError::InvalidArgument("accuracy".into()));
|
||||
}
|
||||
let cpath = std::ffi::CString::new(path)
|
||||
.map_err(|_| FfiError::InvalidArgument("path has a NUL byte".into()))?;
|
||||
call_with_buffer(64, |buf, len| {
|
||||
// SAFETY: cpath is a valid NUL-terminated string; buf is valid for buf.len()
|
||||
// writes and the shim honours the capacity; len is valid.
|
||||
unsafe {
|
||||
sys::sc_corpus_parity_measure(
|
||||
cpath.as_ptr(),
|
||||
accuracy,
|
||||
buf.as_mut_ptr(),
|
||||
buf.len() as i64,
|
||||
&raw mut *len,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads model file `path` with upstream's reader, then writes every B-rep to the ASCII
|
||||
/// file `tmp` (`SmBrep::WriteToFile`: `SmBrepData::FromBrep`, `WriteToFile`), reads it
|
||||
/// back into a new B-rep (`SmBrep::ReadFromFile`) and measures that at integration
|
||||
/// accuracy `accuracy`; `tmp` is removed afterwards. The layout is [`measure`]'s, a
|
||||
/// failed write or read back giving its `SmStatus` as the B-rep's `status`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`FfiError::InvalidArgument`] for a path with a NUL byte or a non-positive or
|
||||
/// non-finite accuracy; [`FfiError::Status`] for a shim failure.
|
||||
pub fn roundtrip(path: &str, tmp: &str, accuracy: f64) -> Result<Vec<f64>, FfiError> {
|
||||
if !(accuracy.is_finite() && accuracy > 0.0) {
|
||||
return Err(FfiError::InvalidArgument("accuracy".into()));
|
||||
}
|
||||
let cpath = std::ffi::CString::new(path)
|
||||
.map_err(|_| FfiError::InvalidArgument("path has a NUL byte".into()))?;
|
||||
let ctmp = std::ffi::CString::new(tmp)
|
||||
.map_err(|_| FfiError::InvalidArgument("tmp has a NUL byte".into()))?;
|
||||
call_with_buffer(64, |buf, len| {
|
||||
// SAFETY: cpath and ctmp are valid NUL-terminated strings; buf is valid for
|
||||
// buf.len() writes and the shim honours the capacity; len is valid.
|
||||
unsafe {
|
||||
sys::sc_corpus_parity_roundtrip(
|
||||
cpath.as_ptr(),
|
||||
ctmp.as_ptr(),
|
||||
accuracy,
|
||||
buf.as_mut_ptr(),
|
||||
buf.len() as i64,
|
||||
&raw mut *len,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
//! FFI wrapper for the part reader (`topology::part_file`, outside the plan).
|
||||
//!
|
||||
//! Shim: `harness/cpp/shim/topology_part_file.cpp`.
|
||||
//! Upstream: `source/SMLib/src/SmBrepData.cpp` (`SmBrepData::ReadPartFromFile`).
|
||||
//!
|
||||
//! | Rust | shim | upstream |
|
||||
//! |---------|------------------------------|---------------------------------------------|
|
||||
//! | [`run`] | `sc_topology_part_file_run` | `SmBrepData::ReadPartFromFile` and a dump |
|
||||
//!
|
||||
//! # Output
|
||||
//!
|
||||
//! The read status (`0` = `SM_SUCCESS`), then on success: the curve count and per curve
|
||||
//! its type, dimension and natural interval (2); the surface count and per surface its
|
||||
//! type and natural UV domain (`umin vmin umax vmax`); the Boolean tree node count and
|
||||
//! the nodes; the B-rep count and per B-rep its tolerance, `IsManifoldSolid` flag and
|
||||
//! region, shell, face, loop (over the faces' `GetLoops`), edge and vertex counts.
|
||||
//!
|
||||
//! # Guard
|
||||
//!
|
||||
//! Upstream's part reader trusts every count and extraction of the file, so [`run`]
|
||||
//! accepts only upstream's own test files: a path under
|
||||
//! `upstream/TestFiles/pt_TestFiles` (after resolving it) whose contents start with
|
||||
//! `//[Output Summary]`; anything else is [`FfiError::InvalidArgument`].
|
||||
|
||||
use std::path::{Component, Path};
|
||||
|
||||
use crate::{call_with_buffer, FfiError};
|
||||
|
||||
mod sys {
|
||||
extern "C" {
|
||||
pub fn sc_topology_part_file_run(
|
||||
path: *const std::ffi::c_char,
|
||||
file_type: i32,
|
||||
out: *mut f64,
|
||||
cap: i64,
|
||||
out_len: *mut i64,
|
||||
) -> i32;
|
||||
}
|
||||
}
|
||||
|
||||
fn bad(msg: &str) -> FfiError {
|
||||
FfiError::InvalidArgument(msg.to_string())
|
||||
}
|
||||
|
||||
/// The guard of [`run`]: `path` names an upstream part test file.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`FfiError::InvalidArgument`] for any other file.
|
||||
pub fn validate(path: &str) -> Result<(), FfiError> {
|
||||
let p = Path::new(path);
|
||||
if p.components().any(|c| c == Component::ParentDir) {
|
||||
return Err(bad("path with `..`"));
|
||||
}
|
||||
let canon = p.canonicalize().map_err(|e| bad(&format!("{path}: {e}")))?;
|
||||
let under = canon
|
||||
.ancestors()
|
||||
.any(|a| a.ends_with(Path::new("upstream/TestFiles/pt_TestFiles")));
|
||||
if !under {
|
||||
return Err(bad("not an upstream test file"));
|
||||
}
|
||||
let bytes = std::fs::read(&canon).map_err(|e| bad(&format!("{path}: {e}")))?;
|
||||
if !bytes.starts_with(b"//[Output Summary]") {
|
||||
return Err(bad("not a part file"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `SmBrepData::ReadPartFromFile(ctx, path, ..., SM_ASCII)` and the dump described in
|
||||
/// the module docs.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of [`validate`]; [`FfiError::Status`] for a shim failure.
|
||||
pub fn run(path: &str) -> Result<Vec<f64>, FfiError> {
|
||||
validate(path)?;
|
||||
let cpath = std::ffi::CString::new(path).map_err(|_| bad("path has a NUL byte"))?;
|
||||
call_with_buffer(1024, |buf, len| {
|
||||
// SAFETY: cpath is a valid NUL-terminated string naming a validated upstream
|
||||
// part file; buf is valid for buf.len() writes and the shim honours the
|
||||
// capacity; len is valid.
|
||||
unsafe {
|
||||
sys::sc_topology_part_file_run(
|
||||
cpath.as_ptr(),
|
||||
0,
|
||||
buf.as_mut_ptr(),
|
||||
buf.len() as i64,
|
||||
&raw mut *len,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@
|
||||
//! | [`Ellipse::create_simple_offset`] | `SmCircle::CreateSimpleOffset` |
|
||||
//! | [`Ellipse::assert_valid`] | `SmEllipse::AssertValid`, `SmCircle::AssertValid` |
|
||||
//! | [`Ellipse::read_from_db`] | `SmEllipse::ReadFromDB`, `SmCircle::ReadFromDB` |
|
||||
//! | [`Ellipse::write_to_db`] | `SmEllipse::WriteToDB`, `SmCircle::WriteToDB` |
|
||||
//! | [`Ellipse::convert_to_2d`] | `SmEllipse::ConvertTo2D` |
|
||||
//! | [`Ellipse::convert_t_from_nurbs_to_step`] | `SmEllipse::ConvertTFromNURBSToSTEP` |
|
||||
//! | [`Ellipse::adjust_step_interval`] | `SmEllipse::AdjustSTEPInterval` |
|
||||
@@ -40,6 +41,7 @@
|
||||
//! | [`PolarConversion::is_current`] | `SmPolarConversion::IsCurrent` |
|
||||
//! | [`PolarConversion::assert_valid`] | `SmPolarConversion::AssertValid` |
|
||||
//! | [`PolarConversion::read_from_db`], [`PolarConversion::read_from_db_with_curve`] | `SmPolarConversion::ReadFromDB` (ellipse back pointer; null back pointer, from `SmSurfOfRevolution::ReadFromDB`) |
|
||||
//! | [`PolarConversion::write_to_db`] | `SmPolarConversion::WriteToDB` |
|
||||
//!
|
||||
//! Deviations:
|
||||
//! - **Circle class.** `SmCircle` derives from `SmEllipse` without data of its own;
|
||||
@@ -116,7 +118,9 @@ use crate::nurbs::crv_basic::ConicType;
|
||||
use crate::nurbs::crv_common::{elliptical_arc_tol, EllipseArc};
|
||||
use crate::nurbs::index::at;
|
||||
use crate::nurbs::nurbs_crv::to_euclid;
|
||||
use crate::topology::database_io::{DatabaseReader, FileType, InputStream};
|
||||
use crate::topology::database_io::{
|
||||
fixed16, DatabaseReader, DatabaseWriter, FileType, InputStream,
|
||||
};
|
||||
|
||||
use super::p2::{Ellipse, EllipseOptions, PolarConversion};
|
||||
|
||||
@@ -565,6 +569,101 @@ impl PolarConversion {
|
||||
Ok((pc, curve))
|
||||
}
|
||||
|
||||
/// `WriteToDB`: writes the set-up flag, the knot, angle and exactness counts and
|
||||
/// whether the table owns a copy of its curve (`owned_curve`; the ellipse's own
|
||||
/// table borrows it and passes `None`), then the three arrays (ASCII: a line
|
||||
/// break after the elements with index 5, 10, ... and after a partial last line),
|
||||
/// then the owned curve (`m_cpCurve`, any curve kind) with its type header, as
|
||||
/// a curve nested `depth` levels deep.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of [`crate::topology::geometry_write::write_curve_at`] for the owned curve.
|
||||
pub fn write_to_db(
|
||||
&self,
|
||||
db: &mut DatabaseWriter,
|
||||
version: u64,
|
||||
owned_curve: Option<&Curve>,
|
||||
depth: usize,
|
||||
) -> Result<()> {
|
||||
let ascii = db.file_type() == FileType::Ascii;
|
||||
let n_knots = self.natural_knots.len() as u64;
|
||||
let n_angles = self.angles.len() as u64;
|
||||
let n_exact = self.exact_conversion.len() as u64;
|
||||
let has_curve = i32::from(owned_curve.is_some());
|
||||
let possible = i32::from(self.possible);
|
||||
|
||||
if ascii {
|
||||
db.out_i64(possible.into());
|
||||
db.out_str(
|
||||
" SmPolarConversion PolarConversionPossible - TRUE=tables are initialized \n",
|
||||
);
|
||||
db.out_u64(n_knots);
|
||||
db.out_str(" SmPolarConversion Number of Natural Knots \n");
|
||||
db.out_u64(n_angles);
|
||||
db.out_str(" SmPolarConversion Number of Angles \n");
|
||||
db.out_u64(n_exact);
|
||||
db.out_str(" SmPolarConversion Number of ExactConversion Flags \n");
|
||||
db.out_i64(has_curve.into());
|
||||
db.out_str(
|
||||
" SmPolarConversion TRUE = Has Curve for stored conversions, \
|
||||
FALSE = back pointer \n",
|
||||
);
|
||||
} else {
|
||||
db.write_boolean(possible);
|
||||
db.write_long(n_knots as i64);
|
||||
db.write_long(n_angles as i64);
|
||||
db.write_long(n_exact as i64);
|
||||
db.write_boolean(has_curve);
|
||||
}
|
||||
|
||||
// Write Arrays
|
||||
if ascii {
|
||||
let rows = |db: &mut DatabaseWriter, values: &mut dyn Iterator<Item = String>| {
|
||||
let mut n = 0usize;
|
||||
for (ii, v) in values.enumerate() {
|
||||
db.out_str(&v).out_str(" ");
|
||||
if ii > 0 && ii % 5 == 0 {
|
||||
db.out_str("\n");
|
||||
}
|
||||
n += 1;
|
||||
}
|
||||
if n > 0 && n % 5 != 0 {
|
||||
db.out_str("\n");
|
||||
}
|
||||
};
|
||||
let exact = &self.exact_conversion;
|
||||
let mut flags = exact.iter().map(|&b| i32::from(b).to_string());
|
||||
rows(db, &mut self.natural_knots.iter().map(|&x| fixed16(x)));
|
||||
rows(db, &mut self.angles.iter().map(|&x| fixed16(x)));
|
||||
rows(db, &mut flags);
|
||||
} else {
|
||||
if n_knots > 0 {
|
||||
db.write_doubles(&self.natural_knots);
|
||||
}
|
||||
if n_angles > 0 {
|
||||
db.write_doubles(&self.angles);
|
||||
}
|
||||
if n_exact > 0 {
|
||||
let exact = &self.exact_conversion;
|
||||
let flags: Vec<i32> = exact.iter().map(|&b| i32::from(b)).collect();
|
||||
db.write_booleans(&flags);
|
||||
}
|
||||
}
|
||||
|
||||
// write out the curve the table owns (the ellipse's own table borrows it)
|
||||
if let Some(c) = owned_curve {
|
||||
if ascii {
|
||||
db.out_str(" SmPolarConversion->CurveBeingConverted \n");
|
||||
}
|
||||
db.write_type(c.type_code(), Some(c.dim()));
|
||||
crate::topology::geometry_write::write_curve_at(c, db, version, depth)?;
|
||||
} else if ascii {
|
||||
db.out_str("Has No Copy of SmPolarConversion->CurveBeingConverted \n");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The end of a read whose table has no stored curve: the back pointer
|
||||
/// (`has_back_pointer`: the ellipse being read; else null), then the ASCII line
|
||||
/// break.
|
||||
@@ -1242,6 +1341,53 @@ impl Ellipse {
|
||||
Ok(e)
|
||||
}
|
||||
|
||||
/// `WriteToDB` (`SmCircle::WriteToDB` is the same): writes the placement origin
|
||||
/// and axes, the angular domain (degrees), the two radii and the inside-out flag,
|
||||
/// then the polar converter (which borrows this ellipse) and the B-spline curve
|
||||
/// part.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of [`PolarConversion::write_to_db`]; `SM_ERR` when the B-spline part has
|
||||
/// no NURB (upstream ignores the part's status; its virtual
|
||||
/// `GetOrCreateGwNurbPointer` always has one to write).
|
||||
pub fn write_to_db(&self, db: &mut DatabaseWriter, version: u64) -> Result<()> {
|
||||
let o = self.position.origin();
|
||||
let x = self.position.x_axis();
|
||||
let y = self.position.y_axis();
|
||||
let (lo, hi) = (self.anal_domain.min(), self.anal_domain.max());
|
||||
let inside_out = i32::from(self.inside_out);
|
||||
let ascii = db.file_type() == FileType::Ascii;
|
||||
if ascii {
|
||||
db.out_reals(&[o.x, o.y, o.z]);
|
||||
db.out_str(" SmEllipse Axis Origin \n");
|
||||
db.out_reals(&[x.x, x.y, x.z]);
|
||||
db.out_str(" SmEllipse X Axis\n");
|
||||
db.out_reals(&[y.x, y.y, y.z]);
|
||||
db.out_str(" SmEllipse Y Axis\n");
|
||||
db.out_reals(&[lo, hi]);
|
||||
db.out_str(" SmEllipse angular arc domain in degrees \n");
|
||||
db.out_f64(self.radius_x);
|
||||
db.out_str(" SmEllipse X_Axis radius \n");
|
||||
db.out_f64(self.radius_y);
|
||||
db.out_str(" SmEllipse Y_Axis radius \n");
|
||||
db.out_i64(inside_out.into());
|
||||
db.out_str(" SmEllipse InsideOut Flag \n");
|
||||
} else {
|
||||
db.write_doubles(&[o.x, o.y, o.z, x.x, x.y, x.z, y.x, y.y, y.z]);
|
||||
db.write_doubles(&[lo, hi, self.radius_x, self.radius_y]);
|
||||
db.write_boolean(inside_out);
|
||||
}
|
||||
|
||||
// PolarConverter
|
||||
if ascii {
|
||||
db.out_str(" SmEllipse->PolarConverter \n");
|
||||
}
|
||||
self.polar.write_to_db(db, version, None, 0)?;
|
||||
|
||||
self.base.write_to_db(db, version)
|
||||
}
|
||||
|
||||
// ---- conversions ------------------------------------------------------------------
|
||||
|
||||
/// Projects the ellipse onto the x-y plane (`ConvertTo2D`): the NURB is made 2D
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
//! | [`Line::trim`] | `Trim` |
|
||||
//! | [`Line::assert_valid`] | `AssertValid` |
|
||||
//! | [`Line::read_from_db`] | `ReadFromDB` |
|
||||
//! | [`Line::write_to_db`] | `WriteToDB` |
|
||||
//! | [`Line::is_kind_of`], [`Line::TYPE`], [`Line::type_string`], [`Line::class_string`] | `IsKindOf`, `GetType`, `GetClassType`, `GetTypeString`, `GetClassString` (`SM_COMMON`) |
|
||||
//! | [`Line::step_interval`], [`Line::max_analytic_domain`], [`Line::degree`], [`Line::number_natural_knots`] | `GetSTEPInterval`, `GetMaxAnalyticDomain`, `GetDegree`, `GetNumberNaturalKnots` (inline) |
|
||||
//! | [`Line::line_point`], [`Line::line_vector`], [`Line::line_scale`], [`Line::inside_out`] | `GetLinePoint`, `GetLineVector`, `GetLineScale`, `GetInsideOut` (inline) |
|
||||
@@ -139,7 +140,7 @@ use crate::nurbs::b_spline_curve_n_lib::{InValueType, TrimMode};
|
||||
use crate::nurbs::b_spline_surface::p1::SurfParamType;
|
||||
use crate::nurbs::math::NOW;
|
||||
use crate::nurbs::nurbs_crv::allocate_nurb_curve;
|
||||
use crate::topology::database_io::{DatabaseReader, FileType, InputStream};
|
||||
use crate::topology::database_io::{DatabaseReader, DatabaseWriter, FileType, InputStream};
|
||||
|
||||
const OP_SEGMENT: &str = "curve::line::Line::create_line_segment";
|
||||
const OP_OFFSET: &str = "curve::line::Line::create_simple_offset";
|
||||
@@ -2167,6 +2168,33 @@ impl Line {
|
||||
}
|
||||
Ok(line)
|
||||
}
|
||||
|
||||
/// `WriteToDB`: writes the line point, vector, analytic domain, scale and
|
||||
/// inside-out flag, then the B-spline curve part.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `SM_ERR` when the B-spline part has no NURB (upstream ignores the part's
|
||||
/// status; its virtual `GetOrCreateGwNurbPointer` always has one to write).
|
||||
pub fn write_to_db(&self, db: &mut DatabaseWriter, version: u64) -> Result<()> {
|
||||
let (p, v) = (self.point, self.vector);
|
||||
let (lo, hi) = (self.anal_domain.min(), self.anal_domain.max());
|
||||
let inside_out = i32::from(self.inside_out);
|
||||
if db.file_type() == FileType::Ascii {
|
||||
db.out_reals(&[p.x, p.y, p.z]);
|
||||
db.out_str(" SmLine Line Point \n");
|
||||
db.out_reals(&[v.x, v.y, v.z]);
|
||||
db.out_str(" SmLine Line Vector \n");
|
||||
db.out_reals(&[lo, hi]).out_str(" SmLine Line Domain \n");
|
||||
db.out_f64(self.scale).out_str(" SmLine Line Scale \n");
|
||||
db.out_i64(inside_out.into());
|
||||
db.out_str(" SmLine InsideOut Flag \n");
|
||||
} else {
|
||||
db.write_doubles(&[p.x, p.y, p.z, v.x, v.y, v.z, lo, hi, self.scale]);
|
||||
db.write_boolean(inside_out);
|
||||
}
|
||||
self.base.write_to_db(db, version)
|
||||
}
|
||||
}
|
||||
|
||||
/// The `SM_ASSERT_VALUE_REPORT` macro of `AssertValid` (an `SM_LEVEL_0` test, so the
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
//! | [`BSplineCurve::set_control_point`] | `SetControlPoint` |
|
||||
//! | [`BSplineCurve::adjust_step_interval`] | `AdjustSTEPInterval` |
|
||||
//! | [`BSplineCurve::read_from_db`] | `ReadFromDB` |
|
||||
//! | [`BSplineCurve::write_to_db`] | `WriteToDB` |
|
||||
//!
|
||||
//! The other members of the file belong to `nurbs.b_spline_curve_p1`, `_p3`, ... and
|
||||
//! `nurbs.b_spline_curve_n_lib`.
|
||||
@@ -110,7 +111,7 @@ use crate::nurbs::geom::dist_cpt_cpt;
|
||||
use crate::nurbs::knots::Side;
|
||||
use crate::nurbs::math::{CPoint, NOW, NOZ};
|
||||
use crate::nurbs::nurbs_crv::{allocate_nurb_curve, is_ncrv_degenerate, to_euclid};
|
||||
use crate::topology::database_io::{DatabaseReader, FileType, InputStream};
|
||||
use crate::topology::database_io::{DatabaseReader, DatabaseWriter, FileType, InputStream};
|
||||
|
||||
/// `SmCrvOnSurf_TYPE` (`CURV_BASE_TYPE + 48`).
|
||||
const CRV_ON_SURF_TYPE: i64 = BSplineCurve::TYPE - 33 + 48;
|
||||
@@ -1933,6 +1934,91 @@ impl BSplineCurve {
|
||||
}
|
||||
Ok(new_curve)
|
||||
}
|
||||
|
||||
/// `WriteToDB`: writes the NURB's header (last control point index, degree,
|
||||
/// rational flag and dimension, then for `version > 32` the knot type and form),
|
||||
/// its control points (`x y`, `x y z`, `wx wy w` or `wx wy wz w`: Euclidean for a
|
||||
/// non-rational NURB, weighted for a rational one) and its knots.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `SM_ERR` without NURB (`GetOrCreateGwNurbPointer` cannot make one for a
|
||||
/// B-spline; the analytic kinds always hold theirs).
|
||||
pub fn write_to_db(&self, db: &mut DatabaseWriter, version: u64) -> Result<()> {
|
||||
let cur = self.nurb_ref()?;
|
||||
let n = cur.n() as i64;
|
||||
let p = cur.degree() as i16;
|
||||
let knot_type = self.knot_type as u64;
|
||||
let form = self.form as u64;
|
||||
|
||||
// set type flag for different types of output
|
||||
let rat: i16 = i16::from(cur.is_rational());
|
||||
let dim: i16 = if cur.is_3d() { 3 } else { 2 };
|
||||
|
||||
// output header, n, p, rat, and dim
|
||||
let ascii = db.file_type() == FileType::Ascii;
|
||||
if ascii {
|
||||
db.out_i64(n).out_str(" Number of Control Points \n");
|
||||
db.out_i64(p.into()).out_str(" Degree \n");
|
||||
db.out_i64(rat.into()).out_str(" RationalFlag \n");
|
||||
db.out_i64(dim.into()).out_str(" Dimension \n");
|
||||
if version > 32 {
|
||||
db.out_u64(knot_type);
|
||||
db.out_str(
|
||||
" Knot Type Enum: 1=2d NonRational, 2=3d NonRational, 3=2d Rational, \
|
||||
4=3d Rational\n",
|
||||
);
|
||||
db.out_u64(form).out_str(" BSplineCurveForm Enum\n");
|
||||
}
|
||||
} else {
|
||||
db.write_long(n);
|
||||
db.write_short(p);
|
||||
db.write_short(rat);
|
||||
db.write_short(dim);
|
||||
if version > 32 {
|
||||
db.write_long(knot_type as i64);
|
||||
db.write_long(form as i64);
|
||||
}
|
||||
}
|
||||
|
||||
// for every control point, by curve type
|
||||
for pw in cur.cpts() {
|
||||
let [x, y, z] = cpt_to_xyz(*pw);
|
||||
let values = match (rat == 1, dim == 3) {
|
||||
// 2-D non-rational
|
||||
(false, false) => [x, y, 0.0, 0.0],
|
||||
// 3-D non-rational
|
||||
(false, true) => [x, y, z, 0.0],
|
||||
// 2-D rational
|
||||
(true, false) => [pw.x, pw.y, pw.w, 0.0],
|
||||
// 3-D rational
|
||||
(true, true) => [pw.x, pw.y, pw.z, pw.w],
|
||||
};
|
||||
let count = 2 + usize::from(dim == 3) + usize::from(rat == 1);
|
||||
db.reals(values.get(..count).unwrap_or_default());
|
||||
}
|
||||
|
||||
// for every knot
|
||||
for &u in cur.knots() {
|
||||
db.reals(&[u]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// `N_CPtToXYZ`: the Euclidean coordinates of a control point (`z` of a 2D point is
|
||||
/// `0`; a weighted point is divided by its weight).
|
||||
fn cpt_to_xyz(pw: CPoint) -> [f64; 3] {
|
||||
let mut x = pw.x;
|
||||
let mut y = pw.y;
|
||||
let mut z = if pw.z == NOZ { 0.0 } else { pw.z };
|
||||
if pw.w != NOW {
|
||||
let w = pw.w;
|
||||
x /= w;
|
||||
y /= w;
|
||||
z /= w;
|
||||
}
|
||||
[x, y, z]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
//! | [`BSplineSurface::transform`] | `Transform` |
|
||||
//! | [`BSplineSurface::is_kind_of`] | `IsKindOf` |
|
||||
//! | [`BSplineSurface::read_from_db`] | `ReadFromDB` |
|
||||
//! | [`BSplineSurface::write_to_db`] | `WriteToDB` |
|
||||
//! | [`BSplineSurface::TYPE`], [`BSplineSurface::type_code`], [`BSplineSurface::type_string`], [`BSplineSurface::class_string`] | `GetType`, `GetTypeString`, `GetClassString` (`SM_COMMON`) |
|
||||
//! | [`BSplineSurface::out_of_bounds_enabled_mut`] | `GetOutOfBoundsEnabled` |
|
||||
//! | [`BSplineSurface::is_out_of_bounds_enabled`] | `IsOutOfBoundsEnabled` |
|
||||
@@ -155,7 +156,7 @@ use crate::nurbs::nurbs_srf::{allocate_nurb_surface, copy_nurb_surface};
|
||||
use crate::nurbs::srf_basic::{Direction, Surface};
|
||||
use crate::nurbs::srf_common::{corner_pts, cyl_cone, extrude_crv, CylCone};
|
||||
use crate::surface::surface::p1::SurfaceSolveContext;
|
||||
use crate::topology::database_io::{DatabaseReader, FileType};
|
||||
use crate::topology::database_io::{DatabaseReader, DatabaseWriter, FileType};
|
||||
|
||||
/// `SURF_BASE_TYPE` (`SmTypes.h`).
|
||||
const SURF_BASE_TYPE: i64 = 15000;
|
||||
@@ -182,6 +183,7 @@ const OP_KNOTS: &str = "nurbs::b_spline_surface::p1::BSplineSurface::knots";
|
||||
const OP_STEP_INV: &str = "nurbs::b_spline_surface::p1::BSplineSurface::step_inversion";
|
||||
const OP_XFORM: &str = "nurbs::b_spline_surface::p1::BSplineSurface::transform";
|
||||
const OP_READ: &str = "nurbs::b_spline_surface::p1::BSplineSurface::read_from_db";
|
||||
const OP_WRITE: &str = "nurbs::b_spline_surface::p1::BSplineSurface::write_to_db";
|
||||
|
||||
/// `SmBSplineSurfaceForm`: the STEP form of a B-spline surface; discriminants are the
|
||||
/// upstream values.
|
||||
@@ -2245,6 +2247,67 @@ impl BSplineSurface {
|
||||
form,
|
||||
})
|
||||
}
|
||||
|
||||
/// `WriteToDB`: writes the NURB's last control point indices `n m`, degrees `p q`,
|
||||
/// rational flag and, for `version > 32`, knot type and form; then the control
|
||||
/// net row by row (`wx wy wz`, plus `w` when rational: the stored, weighted
|
||||
/// values) and the u and v knots.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `SM_ERR` without NURB (upstream dereferences `NULL`).
|
||||
pub fn write_to_db(&self, db: &mut DatabaseWriter, version: u64) -> Result<()> {
|
||||
let sur = self.nurb_or_err(OP_WRITE)?;
|
||||
let (n, m) = (sur.n() as i64, sur.m() as i64);
|
||||
let (p, q) = (sur.p() as i16, sur.q() as i16);
|
||||
let knot_type = self.knot_type as u64;
|
||||
let form = self.form as u64;
|
||||
|
||||
// rational flag
|
||||
let rat: i16 = i16::from(sur.is_rational());
|
||||
|
||||
if db.file_type() == FileType::Ascii {
|
||||
db.out_i64(n).out_str(" ").out_i64(m).out_str("\n");
|
||||
db.out_i64(p.into()).out_str(" ").out_i64(q.into());
|
||||
db.out_str("\n");
|
||||
db.out_i64(rat.into()).out_str("\n");
|
||||
if version > 32 {
|
||||
db.out_u64(knot_type);
|
||||
db.out_str(" BSplineSurface Knot Type Flag \n");
|
||||
db.out_u64(form).out_str(" BSplineSurfaceForm Flag \n");
|
||||
}
|
||||
} else {
|
||||
db.write_long(n);
|
||||
db.write_long(m);
|
||||
db.write_short(p);
|
||||
db.write_short(q);
|
||||
db.write_short(rat);
|
||||
if version > 32 {
|
||||
db.write_long(knot_type as i64);
|
||||
db.write_long(form as i64);
|
||||
}
|
||||
}
|
||||
|
||||
// control points, by rational / non-rational
|
||||
for row in sur.rows() {
|
||||
for pw in row {
|
||||
if rat == 1 {
|
||||
db.reals(&[pw.x, pw.y, pw.z, pw.w]);
|
||||
} else {
|
||||
db.reals(&[pw.x, pw.y, pw.z]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// output u knots, then v knots
|
||||
for &u in sur.u_knots() {
|
||||
db.reals(&[u]);
|
||||
}
|
||||
for &v in sur.v_knots() {
|
||||
db.reals(&[v]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// `SmKnotType` with upstream value `code`, if any.
|
||||
|
||||
@@ -164,8 +164,9 @@ pub fn calculate_bounding_box_in(
|
||||
|
||||
let surface = fp2::surface(b, f)?
|
||||
.ok_or_else(|| SolidError::invalid_input(OP_FACE_BOX, "face without surface"))?;
|
||||
let u_deg = surface.degree(SurfParamType::U);
|
||||
let v_deg = surface.degree(SurfParamType::V);
|
||||
// (GetDegree makes a missing NURB: a lazy surface of extrusion is still linear in V)
|
||||
let u_deg = surface.degree_with_nurb(k, SurfParamType::U)?;
|
||||
let v_deg = surface.degree_with_nurb(k, SurfParamType::V)?;
|
||||
|
||||
// If one of the directions of the surface is linear we can compute the bounding box
|
||||
// using the edges and vertices.
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
//! | [`OffsetSurface::reverse`], [`OffsetSurface::split_at`], [`OffsetSurface::step_inversion`] (and `_extending`), [`OffsetSurface::swap_uv`], [`OffsetSurface::transform`], [`OffsetSurface::trim_with_domain`] | `Reverse`, `SplitAt`, `STEPInversion`, `SwapUV`, `Transform`, `TrimWithDomain` |
|
||||
//! | [`OffsetSurface::create_offset_surface`] | `CreateOffsetSurface` |
|
||||
//! | [`OffsetSurface::read_from_db`] | `ReadFromDB` |
|
||||
//! | [`OffsetSurface::write_to_db`] | `WriteToDB` |
|
||||
//! | [`OffsetSurface::is_kind_of`], [`OffsetSurface::type_code`], [`OffsetSurface::type_string`], [`OffsetSurface::class_string`] | `IsKindOf`, `GetType`, `GetTypeString`, `GetClassString` (`SM_COMMON`) |
|
||||
//! | [`OffsetSurface::offset_distance`], [`OffsetSurface::base_surface`], [`OffsetSurface::extended_base_surface`], [`OffsetSurface::max_analytic_domain`], [`OffsetSurface::degree`] | `GetOffsetDistance`, `GetBaseSurface`, `GetExtendedBaseSurface`, `GetMaxAnalyticDomain`, `GetDegree` |
|
||||
//! | [`OffsetSurface::set_offset_distance`], [`OffsetSurface::set_allow_extension`], [`OffsetSurface::set_context`] | `SetOffsetDistance`, `SetAllowExtension`, `SetContext` |
|
||||
@@ -131,7 +132,8 @@ use crate::surface::surface::p2::{
|
||||
};
|
||||
use crate::surface::surface::p7::base_evaluate_normal;
|
||||
use crate::surface::surface::{Surface, SurfaceKind};
|
||||
use crate::topology::database_io::{DatabaseReader, FileType, InputStream};
|
||||
use crate::topology::database_io::{DatabaseReader, DatabaseWriter, FileType, InputStream};
|
||||
use crate::topology::geometry_write::write_surface_at;
|
||||
|
||||
const OP: &str = "surface::offset_surface::OffsetSurface";
|
||||
const OP_NEW: &str = "surface::offset_surface::OffsetSurface::new";
|
||||
@@ -1865,6 +1867,57 @@ impl OffsetSurface {
|
||||
db.leave_nested();
|
||||
result
|
||||
}
|
||||
|
||||
/// `WriteToDB`: writes the offset distance, the extension and periodicity flags
|
||||
/// and whether there is an extended surface, then the base surface and the
|
||||
/// extended surface (if any), each with its type header. `depth` is this
|
||||
/// surface's nesting depth (see [`write_surface_at`]).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of [`write_surface_at`] for the base and extended surfaces.
|
||||
pub fn write_to_db(&self, db: &mut DatabaseWriter, version: u64, depth: usize) -> Result<()> {
|
||||
let ascii = db.file_type() == FileType::Ascii;
|
||||
let distance = self.offset_distance.get();
|
||||
let allow = i32::from(self.allow_extension);
|
||||
self.with_current_extension(|extended| {
|
||||
let has_extended = i32::from(extended.is_some());
|
||||
if ascii {
|
||||
db.out_f64(distance);
|
||||
db.out_str(" SmOffsetSurface Offset Distance \n");
|
||||
db.out_i64(allow.into());
|
||||
db.out_str(" SmOffsetSurface Allow Extension \n");
|
||||
db.out_i64(self.is_periodic_on_u.into());
|
||||
db.out_str(" SmOffsetSurface PeriodicOnU \n");
|
||||
db.out_i64(self.is_periodic_on_v.into());
|
||||
db.out_str(" SmOffsetSurface PeriodicOnV \n");
|
||||
db.out_i64(has_extended.into());
|
||||
db.out_str(" SmOffsetSurface HasExtendedSurface \n");
|
||||
} else {
|
||||
db.write_double(distance);
|
||||
db.write_boolean(allow);
|
||||
db.write_boolean(self.is_periodic_on_u);
|
||||
db.write_boolean(self.is_periodic_on_v);
|
||||
db.write_boolean(has_extended);
|
||||
}
|
||||
|
||||
if ascii {
|
||||
db.out_str(" OffsetSurface->BaseSurface \n");
|
||||
}
|
||||
db.write_type(self.surface.type_code(), None);
|
||||
write_surface_at(&self.surface, db, version, depth + 1)?;
|
||||
if let Some(e) = extended {
|
||||
if ascii {
|
||||
db.out_str(" OffsetSurface->ExtendedSurface \n");
|
||||
}
|
||||
db.write_type(e.type_code(), None);
|
||||
write_surface_at(e, db, version, depth + 1)?;
|
||||
} else if ascii {
|
||||
db.out_str("Has No OffsetSurface->ExtendedSurface \n");
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum wrapper depth accepted by offset construction and restoration.
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
//! | [`Plane::transform`] | `Transform` |
|
||||
//! | [`Plane::trim_with_domain`] | `TrimWithDomain` |
|
||||
//! | [`Plane::read_from_db`] | `ReadFromDB` (static) |
|
||||
//! | [`Plane::write_to_db`] | `WriteToDB` |
|
||||
//! | [`Plane::make_new_plane_line`] | `MakeNewPlaneLine` |
|
||||
//! | [`Plane::make_new_plane_ellipse`] | `MakeNewPlaneEllipse` |
|
||||
//!
|
||||
@@ -87,7 +88,7 @@ use crate::surface::surface::p2::{
|
||||
};
|
||||
use crate::surface::surface::p7::base_evaluate_normal;
|
||||
use crate::surface::surface::{Surface, SurfaceKind};
|
||||
use crate::topology::database_io::{DatabaseReader, FileType, InputStream};
|
||||
use crate::topology::database_io::{DatabaseReader, DatabaseWriter, FileType, InputStream};
|
||||
|
||||
use super::p1::{is_grazing_intersection, Plane};
|
||||
|
||||
@@ -374,6 +375,38 @@ impl Plane {
|
||||
})
|
||||
}
|
||||
|
||||
/// `WriteToDB`: writes the placement origin and axes, the analytic UV domain
|
||||
/// corners and the UV scales, then the B-spline surface part with its NURB (made
|
||||
/// on a copy when missing, as the virtual `GetOrCreateGwNurbPointer` makes it).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of [`Plane::base_with_nurb`] (upstream ignores the B-spline part's
|
||||
/// status, which cannot fail once the NURB exists).
|
||||
pub fn write_to_db(&self, db: &mut DatabaseWriter, version: u64) -> Result<()> {
|
||||
let o = self.position.origin();
|
||||
let x = self.position.x_axis();
|
||||
let y = self.position.y_axis();
|
||||
let (lo, hi) = (self.anal_uv_domain.min(), self.anal_uv_domain.max());
|
||||
let scale = self.uv_scale;
|
||||
if db.file_type() == FileType::Ascii {
|
||||
db.out_reals(&[o.x, o.y, o.z]);
|
||||
db.out_str(" SmPlane Placement Origin \n");
|
||||
db.out_reals(&[x.x, x.y, x.z]);
|
||||
db.out_str(" SmPlane Placement X Axis \n");
|
||||
db.out_reals(&[y.x, y.y, y.z]);
|
||||
db.out_str(" SmPlane Placement Y Axis \n");
|
||||
db.out_reals(&[lo.x, lo.y, hi.x, hi.y]);
|
||||
db.out_str(" SmPlane AnalUVDomain \n");
|
||||
db.out_reals(&[scale.x, scale.y]);
|
||||
db.out_str(" SmPlane UV Scale \n");
|
||||
} else {
|
||||
db.write_doubles(&[o.x, o.y, o.z, x.x, x.y, x.z, y.x, y.y, y.z]);
|
||||
db.write_doubles(&[lo.x, lo.y, hi.x, hi.y, scale.x, scale.y]);
|
||||
}
|
||||
self.base_with_nurb()?.write_to_db(db, version)
|
||||
}
|
||||
|
||||
// ---- recognition ------------------------------------------------------------------
|
||||
|
||||
/// The plane with the parameterisation of `test` when `test` is one
|
||||
|
||||
@@ -159,7 +159,7 @@ use crate::kernel::Kernel;
|
||||
use crate::math::EFF_ZERO_DEG;
|
||||
use crate::math::{max2, min2, BIG_DOUBLE, EFF_ZERO, EFF_ZERO_SQ, EFF_ZERO_SQRT, PI};
|
||||
use crate::nurbs::b_spline_curve_n_lib::TrimMode;
|
||||
use crate::nurbs::b_spline_surface::SurfParamType;
|
||||
use crate::nurbs::b_spline_surface::{BSplineSurface, SurfParamType};
|
||||
use crate::nurbs::b_spline_surface_n_lib::p2::EvaluateFlags;
|
||||
use crate::nurbs::knots::Side;
|
||||
use crate::surface::offset_surface::{OffsetSurface, SS_UNKNOWN};
|
||||
@@ -1547,40 +1547,13 @@ impl Surface {
|
||||
flags.v_side == Side::Left,
|
||||
flags.only_upper_half,
|
||||
),
|
||||
// inherited from SmBSplineSurface
|
||||
SurfaceKind::Cone(s) => s.rev().base().evaluate_simple(
|
||||
uv,
|
||||
highest_u_deriv,
|
||||
highest_v_deriv,
|
||||
flags.u_side == Side::Left,
|
||||
flags.v_side == Side::Left,
|
||||
flags.only_upper_half,
|
||||
),
|
||||
SurfaceKind::SurfOfRevolution(s) => s.base().evaluate_simple(
|
||||
uv,
|
||||
highest_u_deriv,
|
||||
highest_v_deriv,
|
||||
flags.u_side == Side::Left,
|
||||
flags.v_side == Side::Left,
|
||||
flags.only_upper_half,
|
||||
),
|
||||
SurfaceKind::SurfOfExtrusion(s) => s.base().evaluate_simple(
|
||||
uv,
|
||||
highest_u_deriv,
|
||||
highest_v_deriv,
|
||||
flags.u_side == Side::Left,
|
||||
flags.v_side == Side::Left,
|
||||
flags.only_upper_half,
|
||||
),
|
||||
SurfaceKind::Sphere(s) => s.base().base().evaluate_simple(
|
||||
uv,
|
||||
highest_u_deriv,
|
||||
highest_v_deriv,
|
||||
flags.u_side == Side::Left,
|
||||
flags.v_side == Side::Left,
|
||||
flags.only_upper_half,
|
||||
),
|
||||
SurfaceKind::Torus(s) => s.base().base().evaluate_simple(
|
||||
// inherited from SmBSplineSurface, which reads m_pNurb: upstream's analytic
|
||||
// surfaces always hold one (the port makes it on demand, kind_with_nurb)
|
||||
SurfaceKind::Cone(_)
|
||||
| SurfaceKind::SurfOfRevolution(_)
|
||||
| SurfaceKind::SurfOfExtrusion(_)
|
||||
| SurfaceKind::Sphere(_)
|
||||
| SurfaceKind::Torus(_) => self.analytic_base_with_nurb(k)?.evaluate_simple(
|
||||
uv,
|
||||
highest_u_deriv,
|
||||
highest_v_deriv,
|
||||
@@ -1641,11 +1614,13 @@ impl Surface {
|
||||
) -> Result<()> {
|
||||
let base = match self.kind() {
|
||||
SurfaceKind::BSpline(s) => Some(s),
|
||||
SurfaceKind::Cone(s) => Some(s.rev().base()),
|
||||
SurfaceKind::SurfOfRevolution(s) => Some(s.base()),
|
||||
SurfaceKind::SurfOfExtrusion(s) => Some(s.base()),
|
||||
SurfaceKind::Sphere(s) => Some(s.base().base()),
|
||||
SurfaceKind::Torus(s) => Some(s.base().base()),
|
||||
// inherited from SmBSplineSurface, which reads m_pNurb: upstream's analytic
|
||||
// surfaces always hold one (the port makes it on demand, kind_with_nurb)
|
||||
SurfaceKind::Cone(_)
|
||||
| SurfaceKind::SurfOfRevolution(_)
|
||||
| SurfaceKind::SurfOfExtrusion(_)
|
||||
| SurfaceKind::Sphere(_)
|
||||
| SurfaceKind::Torus(_) => Some(self.analytic_base_with_nurb(k)?),
|
||||
SurfaceKind::Plane(_)
|
||||
| SurfaceKind::CurveBounded(_)
|
||||
| SurfaceKind::OffsetSurface(_)
|
||||
@@ -2007,31 +1982,11 @@ impl Surface {
|
||||
};
|
||||
let r = match self.kind() {
|
||||
SurfaceKind::BSpline(s) => s.calculate_bounding_box(k, uv_domain, &req)?,
|
||||
SurfaceKind::Cone(s) => {
|
||||
s.rev()
|
||||
.base()
|
||||
.calculate_bounding_box_of(k, uv_domain, &req, Some(self))?
|
||||
}
|
||||
SurfaceKind::Sphere(s) => {
|
||||
s.base()
|
||||
.base()
|
||||
.calculate_bounding_box_of(k, uv_domain, &req, Some(self))?
|
||||
}
|
||||
SurfaceKind::Torus(s) => {
|
||||
s.base()
|
||||
.base()
|
||||
.calculate_bounding_box_of(k, uv_domain, &req, Some(self))?
|
||||
}
|
||||
SurfaceKind::SurfOfRevolution(s) => {
|
||||
s.base()
|
||||
.calculate_bounding_box_of(k, uv_domain, &req, Some(self))?
|
||||
}
|
||||
SurfaceKind::SurfOfExtrusion(s) => {
|
||||
s.base()
|
||||
.calculate_bounding_box_of(k, uv_domain, &req, Some(self))?
|
||||
}
|
||||
SurfaceKind::Plane(p) => plane_bounding_box(k, p, uv_domain, &req)?,
|
||||
_ => unreachable_kind()?,
|
||||
_ => {
|
||||
let base = self.analytic_base_with_nurb(k)?;
|
||||
base.calculate_bounding_box_of(k, uv_domain, &req, Some(self))?
|
||||
}
|
||||
};
|
||||
Ok(SurfaceBoxes {
|
||||
normal_box: r.normal,
|
||||
@@ -2048,6 +2003,20 @@ impl Surface {
|
||||
}
|
||||
}
|
||||
|
||||
/// The B-spline part of an analytic kind with its NURB (`GetOrCreateGwNurbPointer`
|
||||
/// from a `const` method: [`Surface::kind_with_nurb`], which makes a missing NURB
|
||||
/// once and keeps it on the surface).
|
||||
pub(crate) fn analytic_base_with_nurb(&self, k: &Kernel) -> Result<&BSplineSurface> {
|
||||
Ok(match self.kind_with_nurb(k)? {
|
||||
SurfaceKind::Cone(s) => s.rev().base(),
|
||||
SurfaceKind::Sphere(s) => s.base().base(),
|
||||
SurfaceKind::Torus(s) => s.base().base(),
|
||||
SurfaceKind::SurfOfRevolution(s) => s.base(),
|
||||
SurfaceKind::SurfOfExtrusion(s) => s.base(),
|
||||
_ => return unreachable_kind(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The base body of [`Surface::calculate_bounding_box`].
|
||||
///
|
||||
/// # Errors
|
||||
@@ -4106,7 +4075,6 @@ impl<'a> SssIntersectEnfo<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nurbs::b_spline_surface::BSplineSurface;
|
||||
|
||||
/// A bilinear patch over `[0, 1]^2` spanning `(0,0,0)..(2,1,0)` with a twist.
|
||||
fn bilinear() -> Surface {
|
||||
|
||||
@@ -353,6 +353,9 @@ pub struct Surface {
|
||||
/// The identity a context issued this surface object for its notifications
|
||||
/// (upstream passes the object itself, known by address); a copy has none.
|
||||
notify_identity: IdentitySlot,
|
||||
/// The NURB an analytic kind made on first use from a `const` method
|
||||
/// ([`Surface::kind_with_nurb`], [`NurbMemo`]).
|
||||
made_nurb: NurbMemo,
|
||||
/// Not upstream: the memo of the natural boundary curves `CoincidenceCheck` makes
|
||||
/// (`BoundaryMemo`); a copy starts an empty one, an edit replaces it.
|
||||
boundary_memo: BoundaryMemo,
|
||||
@@ -447,6 +450,24 @@ impl CacheSlot {
|
||||
}
|
||||
}
|
||||
|
||||
/// `GetOrCreateGwNurbPointer` called from a `const` method of an analytic kind that has
|
||||
/// no NURB yet (cone, sphere, torus, surface of revolution or extrusion): upstream runs
|
||||
/// the virtual `MakeNurb` and keeps the NURB on the surface, so every later call sees
|
||||
/// it. The port's `&self` methods cannot fill the kind in place; the kind with its NURB
|
||||
/// made is kept here instead, once, and dropped whenever the kind is changed
|
||||
/// ([`Surface::kind_mut`] and the kind's in-place edits). A copy starts empty.
|
||||
#[derive(Debug, Default)]
|
||||
struct NurbMemo(std::sync::OnceLock<Box<SurfaceKind>>);
|
||||
|
||||
const OP_KIND_WITH_NURB: &str = "surface::surface::p2::Surface::kind_with_nurb";
|
||||
|
||||
impl NurbMemo {
|
||||
/// Forgets the made NURB (the kind changed).
|
||||
fn reset(&mut self) {
|
||||
self.0 = std::sync::OnceLock::new();
|
||||
}
|
||||
}
|
||||
|
||||
/// The copy constructor: copies the geometry, resets the owner (an owning offset in it
|
||||
/// then re-owns its base, whose chain no longer reaches a face).
|
||||
impl Clone for Surface {
|
||||
@@ -458,6 +479,7 @@ impl Clone for Surface {
|
||||
owner_zone_tol_3d: None,
|
||||
cache_slot: CacheSlot::default(),
|
||||
notify_identity: IdentitySlot::new(),
|
||||
made_nurb: NurbMemo::default(),
|
||||
boundary_memo: BoundaryMemo::new(),
|
||||
};
|
||||
s.update_owned_bases();
|
||||
@@ -699,6 +721,7 @@ impl Surface {
|
||||
owner_zone_tol_3d: self.owner_zone_tol_3d,
|
||||
cache_slot: self.cache_slot.share(),
|
||||
notify_identity: IdentitySlot::new(),
|
||||
made_nurb: NurbMemo::default(),
|
||||
boundary_memo: BoundaryMemo::new(),
|
||||
})
|
||||
}
|
||||
@@ -746,6 +769,7 @@ impl Surface {
|
||||
owner_zone_tol_3d: self.owner_zone_tol_3d,
|
||||
cache_slot: self.cache_slot.share(),
|
||||
notify_identity: IdentitySlot::new(),
|
||||
made_nurb: NurbMemo::default(),
|
||||
boundary_memo: BoundaryMemo::new(),
|
||||
}
|
||||
}
|
||||
@@ -897,6 +921,7 @@ impl Surface {
|
||||
owner_zone_tol_3d: None,
|
||||
cache_slot: CacheSlot::default(),
|
||||
notify_identity: IdentitySlot::new(),
|
||||
made_nurb: NurbMemo::default(),
|
||||
boundary_memo: BoundaryMemo::new(),
|
||||
}
|
||||
}
|
||||
@@ -1106,13 +1131,57 @@ impl Surface {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
/// The concrete kind, mutable.
|
||||
/// The concrete kind, mutable (forgets a NURB made by [`Surface::kind_with_nurb`]).
|
||||
pub fn kind_mut(&mut self) -> &mut SurfaceKind {
|
||||
self.made_nurb.reset();
|
||||
// an edit makes another surface value: start an empty boundary memo
|
||||
self.boundary_memo = BoundaryMemo::new();
|
||||
&mut self.kind
|
||||
}
|
||||
|
||||
/// The concrete kind with its NURB (`GetOrCreateGwNurbPointer` from a `const`
|
||||
/// method): this surface's kind when it is not analytic or has its NURB, else the
|
||||
/// kind with the NURB its virtual `MakeNurb` makes (status ignored, as upstream: a
|
||||
/// NURB that cannot be made stays missing), made once and kept on this surface as
|
||||
/// upstream keeps it ([`NurbMemo`]).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::Unported`] from `MakeNurb`.
|
||||
pub fn kind_with_nurb(&self, k: &Kernel) -> Result<&SurfaceKind> {
|
||||
let missing = match &self.kind {
|
||||
SurfaceKind::Cone(s) => s.rev().base().gw_nurb().is_none(),
|
||||
SurfaceKind::Sphere(s) => s.base().base().gw_nurb().is_none(),
|
||||
SurfaceKind::Torus(s) => s.base().base().gw_nurb().is_none(),
|
||||
SurfaceKind::SurfOfRevolution(s) => s.base().gw_nurb().is_none(),
|
||||
SurfaceKind::SurfOfExtrusion(s) => s.base().gw_nurb().is_none(),
|
||||
_ => false,
|
||||
};
|
||||
if !missing {
|
||||
return Ok(&self.kind);
|
||||
}
|
||||
if let Some(made) = self.made_nurb.0.get() {
|
||||
return Ok(made);
|
||||
}
|
||||
let mut kind = match &self.kind {
|
||||
// an exact copy, as SurfOfExtrusion::base_with_nurb makes for a query (no
|
||||
// copy-constructor knot fix-up: upstream makes the NURB on the object itself)
|
||||
SurfaceKind::SurfOfExtrusion(s) => {
|
||||
SurfaceKind::SurfOfExtrusion(Box::new(s.clone_for_evaluation(0)?))
|
||||
}
|
||||
_ => self.kind.clone(),
|
||||
};
|
||||
match &mut kind {
|
||||
SurfaceKind::Cone(s) => ignore_status(s.make_nurb(k))?,
|
||||
SurfaceKind::Sphere(s) => ignore_status(s.make_nurb(k))?,
|
||||
SurfaceKind::Torus(s) => ignore_status(s.make_nurb(k))?,
|
||||
SurfaceKind::SurfOfRevolution(s) => ignore_status(s.make_nurb(k))?,
|
||||
SurfaceKind::SurfOfExtrusion(s) => ignore_status(s.make_nurb(k))?,
|
||||
_ => {}
|
||||
}
|
||||
Ok(self.made_nurb.0.get_or_init(|| Box::new(kind)))
|
||||
}
|
||||
|
||||
/// The concrete kind, by value.
|
||||
#[must_use]
|
||||
pub fn into_kind(self) -> SurfaceKind {
|
||||
@@ -1464,10 +1533,15 @@ impl Surface {
|
||||
SurfaceKind::BSpline(s) => {
|
||||
s.evaluate_ignoring_status(uv, highest_u_deriv, highest_v_deriv, flags)
|
||||
}
|
||||
// (not overridden: the SmBSplineSurface evaluation)
|
||||
SurfaceKind::SurfOfRevolution(s) => {
|
||||
s.base()
|
||||
.evaluate_ignoring_status(uv, highest_u_deriv, highest_v_deriv, flags)
|
||||
// (not overridden: the SmBSplineSurface evaluation, on the m_pNurb upstream's
|
||||
// analytic surfaces always hold)
|
||||
SurfaceKind::SurfOfRevolution(_) | SurfaceKind::Sphere(_) | SurfaceKind::Torus(_) => {
|
||||
self.analytic_base_with_nurb(k)?.evaluate_ignoring_status(
|
||||
uv,
|
||||
highest_u_deriv,
|
||||
highest_v_deriv,
|
||||
flags,
|
||||
)
|
||||
}
|
||||
SurfaceKind::SurfOfExtrusion(s) => s.base_with_nurb(k)?.evaluate_ignoring_status(
|
||||
uv,
|
||||
@@ -1475,18 +1549,6 @@ impl Surface {
|
||||
highest_v_deriv,
|
||||
flags,
|
||||
),
|
||||
SurfaceKind::Sphere(s) => s.base().base().evaluate_ignoring_status(
|
||||
uv,
|
||||
highest_u_deriv,
|
||||
highest_v_deriv,
|
||||
flags,
|
||||
),
|
||||
SurfaceKind::Torus(s) => s.base().base().evaluate_ignoring_status(
|
||||
uv,
|
||||
highest_u_deriv,
|
||||
highest_v_deriv,
|
||||
flags,
|
||||
),
|
||||
// pass the call along
|
||||
SurfaceKind::CurveBounded(s) => {
|
||||
s.basis()
|
||||
@@ -1535,10 +1597,12 @@ impl Surface {
|
||||
SurfaceKind::SrfInVolume(s) => s.evaluate_point(k, uv),
|
||||
SurfaceKind::Plane(s) => Ok(s.evaluate_point(uv)),
|
||||
SurfaceKind::Cone(s) => s.evaluate_point(k, uv),
|
||||
SurfaceKind::SurfOfRevolution(s) => s.base().evaluate_point(uv),
|
||||
// (the inherited SmBSplineSurface evaluation, on the m_pNurb upstream's analytic
|
||||
// surfaces always hold)
|
||||
SurfaceKind::SurfOfRevolution(_) | SurfaceKind::Sphere(_) | SurfaceKind::Torus(_) => {
|
||||
self.analytic_base_with_nurb(k)?.evaluate_point(uv)
|
||||
}
|
||||
SurfaceKind::SurfOfExtrusion(s) => s.base_with_nurb(k)?.evaluate_point(uv),
|
||||
SurfaceKind::Sphere(s) => s.base().base().evaluate_point(uv),
|
||||
SurfaceKind::Torus(s) => s.base().base().evaluate_point(uv),
|
||||
SurfaceKind::OffsetSurface(s) => s.evaluate_point(k, uv),
|
||||
}
|
||||
}
|
||||
@@ -1600,6 +1664,38 @@ impl Surface {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Surface::degree`] as upstream's `GetDegree`, which makes a missing NURB
|
||||
/// (`GetOrCreateGwNurbPointer`): a surface of extrusion (the recogniser's lazy
|
||||
/// generator) or a cone without one reads the degree of the NURB `MakeNurb` gives
|
||||
/// it, on a temporary copy, where [`Surface::degree`] would answer `0`. A failed
|
||||
/// make leaves no NURB, so `0`, as upstream.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::Unported`] from the make.
|
||||
pub fn degree_with_nurb(&self, k: &Kernel, param: SurfParamType) -> Result<usize> {
|
||||
match &self.kind {
|
||||
SurfaceKind::SurfOfExtrusion(s) if s.base().gw_nurb().is_none() => {
|
||||
match s.base_with_nurb(k) {
|
||||
Ok(base) => Ok(base.degree(param)),
|
||||
Err(e) if e.is_unported() => Err(e),
|
||||
Err(_) => Ok(0), // FS
|
||||
}
|
||||
}
|
||||
// (a sphere's or a torus's NURB is quadratic both ways, so its 0 never
|
||||
// reads as linear either)
|
||||
SurfaceKind::Cone(s) if s.rev().base().gw_nurb().is_none() => {
|
||||
let mut copy = s.clone();
|
||||
match copy.make_nurb(k) {
|
||||
Ok(()) => Ok(copy.rev().base().degree(param)),
|
||||
Err(e) if e.is_unported() => Err(e),
|
||||
Err(_) => Ok(0), // FS
|
||||
}
|
||||
}
|
||||
_ => Ok(self.degree(param)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of control points in `param`'s direction
|
||||
/// (`GetNumberControlPoints`).
|
||||
#[must_use]
|
||||
@@ -1717,7 +1813,11 @@ impl Surface {
|
||||
SurfaceKind::Cone(s) => s.convert_uv_from_step_to_nurbs(k, step_uv),
|
||||
SurfaceKind::Sphere(s) => s.convert_uv_from_step_to_nurbs(k, step_uv),
|
||||
SurfaceKind::SurfOfRevolution(s) => s.convert_uv_from_step_to_nurbs(k, step_uv),
|
||||
SurfaceKind::SurfOfExtrusion(s) => s.convert_uv_from_step_to_nurbs(k, step_uv),
|
||||
// (GetNaturalUVDomain makes a missing NURB)
|
||||
SurfaceKind::SurfOfExtrusion(_) => match self.kind_with_nurb(k)? {
|
||||
SurfaceKind::SurfOfExtrusion(s) => s.convert_uv_from_step_to_nurbs(k, step_uv),
|
||||
_ => Err(SolidError::sm_err(OP_KIND_WITH_NURB)),
|
||||
},
|
||||
_ => Ok(base_convert_uv(step_uv)),
|
||||
}
|
||||
}
|
||||
@@ -1734,7 +1834,11 @@ impl Surface {
|
||||
SurfaceKind::Cone(s) => s.convert_uv_from_nurbs_to_step(k, nurbs_uv),
|
||||
SurfaceKind::Sphere(s) => s.convert_uv_from_nurbs_to_step(k, nurbs_uv),
|
||||
SurfaceKind::SurfOfRevolution(s) => s.convert_uv_from_nurbs_to_step(k, nurbs_uv),
|
||||
SurfaceKind::SurfOfExtrusion(s) => s.convert_uv_from_nurbs_to_step(k, nurbs_uv),
|
||||
// (GetNaturalUVDomain makes a missing NURB)
|
||||
SurfaceKind::SurfOfExtrusion(_) => match self.kind_with_nurb(k)? {
|
||||
SurfaceKind::SurfOfExtrusion(s) => s.convert_uv_from_nurbs_to_step(k, nurbs_uv),
|
||||
_ => Err(SolidError::sm_err(OP_KIND_WITH_NURB)),
|
||||
},
|
||||
_ => Ok(base_convert_uv(nurbs_uv)),
|
||||
}
|
||||
}
|
||||
@@ -1876,7 +1980,9 @@ impl Surface {
|
||||
target_distance: Option<f64>,
|
||||
requested: SolutionRequested,
|
||||
) -> Result<SolutionArray<O>> {
|
||||
match &self.kind {
|
||||
// (the analytic kinds' DropPointFast reads the NURB through
|
||||
// GetNaturalUVDomain / GetOrCreateGwNurbPointer, which makes it when missing)
|
||||
match self.kind_with_nurb(k)? {
|
||||
SurfaceKind::Plane(s) => s.drop_point_fast(
|
||||
uv_domain,
|
||||
operation,
|
||||
@@ -2074,6 +2180,7 @@ impl Surface {
|
||||
k: &Kernel,
|
||||
new_step_uv_domain: &Extent2d,
|
||||
) -> Result<()> {
|
||||
self.made_nurb.reset();
|
||||
self.boundary_memo = BoundaryMemo::new();
|
||||
match &mut self.kind {
|
||||
SurfaceKind::BSpline(s) => s.adjust_step_uv_domain(new_step_uv_domain),
|
||||
@@ -2098,6 +2205,7 @@ impl Surface {
|
||||
///
|
||||
/// Those of the kind's override (the base default is `SM_ERR`).
|
||||
pub fn reverse(&mut self, k: &Kernel, param: SurfParamType) -> Result<()> {
|
||||
self.made_nurb.reset();
|
||||
self.boundary_memo = BoundaryMemo::new();
|
||||
match &mut self.kind {
|
||||
SurfaceKind::BSpline(s) => s.reverse(param),
|
||||
@@ -2161,6 +2269,7 @@ impl Surface {
|
||||
///
|
||||
/// Those of the kind's override (the base default is `SM_ERR`).
|
||||
pub fn reparameterize(&mut self, k: &Kernel, new_domain: &Extent2d) -> Result<()> {
|
||||
self.made_nurb.reset();
|
||||
self.boundary_memo = BoundaryMemo::new();
|
||||
match &mut self.kind {
|
||||
SurfaceKind::BSpline(s) => s.reparameterize(new_domain),
|
||||
@@ -2186,6 +2295,7 @@ impl Surface {
|
||||
///
|
||||
/// Those of the kind's override (the base default is `SM_ERR`).
|
||||
pub fn swap_uv(&mut self, k: &Kernel) -> Result<()> {
|
||||
self.made_nurb.reset();
|
||||
self.boundary_memo = BoundaryMemo::new();
|
||||
match &mut self.kind {
|
||||
SurfaceKind::BSpline(s) => s.swap_uv(),
|
||||
@@ -2262,6 +2372,7 @@ impl Surface {
|
||||
scale: Option<Vec3>,
|
||||
depth: usize,
|
||||
) -> Result<()> {
|
||||
self.made_nurb.reset();
|
||||
self.boundary_memo = BoundaryMemo::new();
|
||||
match &mut self.kind {
|
||||
SurfaceKind::Cone(s) => s.transform(k, rotate_n_move, scale),
|
||||
@@ -2302,6 +2413,7 @@ impl Surface {
|
||||
trim_domain: &mut Extent2d,
|
||||
options: TrimOptions,
|
||||
) -> Result<()> {
|
||||
self.made_nurb.reset();
|
||||
self.boundary_memo = BoundaryMemo::new();
|
||||
match &mut self.kind {
|
||||
SurfaceKind::BSpline(s) => s.trim_with_domain_with(trim_domain, options),
|
||||
@@ -2320,7 +2432,9 @@ impl Surface {
|
||||
// ---- new surfaces ---------------------------------------------------------------
|
||||
|
||||
/// The two halves of the surface split at `param` in `surf_param`'s direction
|
||||
/// (`SplitAt`; the kernel is the context the halves are constructed in).
|
||||
/// (`SplitAt`; the kernel is the context the halves are constructed in). An analytic
|
||||
/// kind without its NURB yet makes it first and keeps it on the surface
|
||||
/// ([`Surface::kind_with_nurb`], upstream's `GetOrCreateGwNurbPointer`).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -2345,33 +2459,21 @@ impl Surface {
|
||||
let (l, r) = s.base_for_query().split_at(param, surf_param)?;
|
||||
Ok((Self::from(l), Self::from(r)))
|
||||
}
|
||||
SurfaceKind::Cone(s) => {
|
||||
let (l, r) = s.rev().base().split_at(param, surf_param)?;
|
||||
Ok((Self::from(l), Self::from(r)))
|
||||
}
|
||||
SurfaceKind::Sphere(s) => {
|
||||
let (l, r) = s.base().base().split_at(param, surf_param)?;
|
||||
Ok((Self::from(l), Self::from(r)))
|
||||
}
|
||||
SurfaceKind::Torus(s) => {
|
||||
let (l, r) = s.base().base().split_at(param, surf_param)?;
|
||||
Ok((Self::from(l), Self::from(r)))
|
||||
}
|
||||
SurfaceKind::SurfOfRevolution(s) => {
|
||||
let (l, r) = s.base().split_at(param, surf_param)?;
|
||||
Ok((Self::from(l), Self::from(r)))
|
||||
}
|
||||
SurfaceKind::SurfOfExtrusion(s) => {
|
||||
// GetOrCreateGwNurbPointer: the virtual MakeNurb when the NURB is missing
|
||||
// (as the extrusion recogniser leaves it), here on a temporary copy;
|
||||
// its status is not checked upstream, so a failure splits the NURB-less
|
||||
// base (SM_ERR)
|
||||
let base = match s.base_with_nurb(k) {
|
||||
Ok(b) => b,
|
||||
Err(e) if e.is_unported() => return Err(e),
|
||||
Err(_) => std::borrow::Cow::Borrowed(s.base()),
|
||||
SurfaceKind::Cone(_)
|
||||
| SurfaceKind::Sphere(_)
|
||||
| SurfaceKind::Torus(_)
|
||||
| SurfaceKind::SurfOfRevolution(_)
|
||||
| SurfaceKind::SurfOfExtrusion(_) => {
|
||||
// SmBSplineSurface::SplitAt: GetOrCreateGwNurbPointer (the analytic's
|
||||
// MakeNurb when missing, status ignored; the NURB stays on the surface)
|
||||
let (l, r) = match self.kind_with_nurb(k)? {
|
||||
SurfaceKind::Cone(s) => s.rev().base().split_at(param, surf_param)?,
|
||||
SurfaceKind::Sphere(s) => s.base().base().split_at(param, surf_param)?,
|
||||
SurfaceKind::Torus(s) => s.base().base().split_at(param, surf_param)?,
|
||||
SurfaceKind::SurfOfRevolution(s) => s.base().split_at(param, surf_param)?,
|
||||
SurfaceKind::SurfOfExtrusion(s) => s.base().split_at(param, surf_param)?,
|
||||
_ => return base_split_at(),
|
||||
};
|
||||
let (l, r) = base.split_at(param, surf_param)?;
|
||||
Ok((Self::from(l), Self::from(r)))
|
||||
}
|
||||
SurfaceKind::OffsetSurface(s) => s.split_at(k, param, surf_param),
|
||||
|
||||
@@ -365,9 +365,10 @@ struct BoundaryMemoEntry {
|
||||
/// function of the surface value and the key, so a hit gives what a fresh build gives,
|
||||
/// and the curves carry their own curve cache memos, so their caches are built once
|
||||
/// too. Lent by reference: the check only reads the curves. One entry per live surface,
|
||||
/// freed with it; no `unsafe`, and the surface stays `Send`.
|
||||
/// freed with it; no `unsafe`, and the surface stays `Send`. The entry is boxed so the
|
||||
/// memo adds only a small cell, not the whole entry, to every `Surface` value.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct BoundaryMemo(OnceLock<BoundaryMemoEntry>);
|
||||
pub(crate) struct BoundaryMemo(OnceLock<Box<BoundaryMemoEntry>>);
|
||||
|
||||
impl core::fmt::Debug for BoundaryMemo {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
@@ -402,7 +403,7 @@ fn boundary_curves<'s>(
|
||||
return trim_curves(k, surface, domain).map(Cow::Owned);
|
||||
}
|
||||
let curves = trim_curves(k, surface, domain)?;
|
||||
if let Err(entry) = memo.set(BoundaryMemoEntry { key, curves }) {
|
||||
if let Err(entry) = memo.set(Box::new(BoundaryMemoEntry { key, curves })) {
|
||||
return Ok(Cow::Owned(entry.curves));
|
||||
}
|
||||
match memo.get() {
|
||||
|
||||
@@ -41,11 +41,14 @@
|
||||
pub mod brep;
|
||||
pub mod brep_cache;
|
||||
pub mod brep_data;
|
||||
pub mod brep_data_write;
|
||||
pub mod database_io;
|
||||
pub mod edge;
|
||||
pub mod edgeuse;
|
||||
pub mod face;
|
||||
pub mod geometry_write;
|
||||
pub mod loopuse;
|
||||
pub mod part_file;
|
||||
pub mod proto_topology;
|
||||
pub mod shell;
|
||||
#[allow(clippy::module_inception)]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,14 +24,25 @@
|
||||
//! | [`DatabaseReader::read_booleans`] | `SmDatabaseIO::ReadBooleans` |
|
||||
//! | [`DatabaseReader::read_short`] | `SmDatabaseIO::ReadShort` |
|
||||
//! | [`DatabaseReader::read_char`] | `SmDatabaseIO::ReadChar` |
|
||||
//! | [`DatabaseReader::read_characters`] | `SmDatabaseIO::ReadCharacters` |
|
||||
//! | [`DatabaseReader::read_type`] | `SmDatabaseIO::ReadType` |
|
||||
//! | [`DatabaseReader::go_to_next_line`] | `SmDatabaseIO::GoToNextLine` |
|
||||
//! | [`DatabaseWriter`] | `SmDatabaseIO` / `SmDatabaseIOFile` (write side) |
|
||||
//! | [`DatabaseWriter::open_for_write`] | `SmDatabaseIOFile::OpenFileForWrite` (new file) |
|
||||
//! | [`DatabaseWriter::write_long`] / [`DatabaseWriter::write_longs`] | `WriteLong` / `WriteLongs` |
|
||||
//! | [`DatabaseWriter::write_double`] / [`DatabaseWriter::write_doubles`] | `WriteDouble` / `WriteDoubles` |
|
||||
//! | [`DatabaseWriter::write_boolean`] / [`DatabaseWriter::write_booleans`] | `WriteBoolean` / `WriteBooleans` |
|
||||
//! | [`DatabaseWriter::write_short`] / [`DatabaseWriter::write_char`] | `WriteShort` / `WriteChar` |
|
||||
//! | [`DatabaseWriter::write_characters`] | `SmDatabaseIO::WriteCharacters` |
|
||||
//! | [`DatabaseWriter::write_type`] | `SmDatabaseIO::WriteType` |
|
||||
//! | [`DatabaseWriter::out_str`] ... [`fixed16`] | `*GetOutStreamPtr() << value` (`fixed`, precision 16) |
|
||||
//! | [`is_big_endian`] | `SmDatabaseIO::IsBigEndian` |
|
||||
//! | [`byte_swap_f64`] | `ByteSwap64(double)`, `ByteSwap128(double)` |
|
||||
//! | [`byte_swap_u64`] | `ByteSwap64(unsigned long)`, `ByteSwap64(unsigned long long)` |
|
||||
//! | [`byte_swap_u32`] | `ByteSwap32(unsigned long)`, `ByteSwap32(uint32_t)` |
|
||||
//! | [`byte_swap_u16`] | `ByteSwap16(unsigned short)` |
|
||||
//! | [`InputStream`] | the `std::istream` behind `m_pStreamIn` (libstdc++ semantics) |
|
||||
//! | [`InputStream::getline`] | `std::istream::getline(char*, std::streamsize)` |
|
||||
//! | [`FileType`] | `SmFileType` |
|
||||
//!
|
||||
//! Deviations:
|
||||
@@ -58,8 +69,13 @@
|
||||
//! `__builtin_object_size` when the caller is optimised, as the upstream build is.
|
||||
//! - `ByteSwap32(unsigned short)` is not ported: it is only reached when
|
||||
//! `sizeof(short) != 2` and always returns 0 (its shifts are truncated to 16 bits).
|
||||
//! - `ReadCharacters` and the write side (`Write*`, `OpenFileForWrite`) belong to
|
||||
//! other units and are not here.
|
||||
//! - **Write side.** [`DatabaseWriter`] keeps the stream in memory (no filesystem on
|
||||
//! wasm32): only a new file (`bNewFile = TRUE`) is supported and appending is the
|
||||
//! caller's. Upstream's byte-swapped `WriteLong`/`WriteLongs` swap the variable's
|
||||
//! address instead of its value (`ByteSwap64((unsigned long long) &llLong)`) and so
|
||||
//! write garbage on big-endian machines or with `SM_BYTESWAP`; the port swaps the
|
||||
//! value. On the little-endian reference build with `SM_ASCII` or `SM_BINARY` no
|
||||
//! swap happens and the output agrees.
|
||||
//! - **Nesting bound.** Upstream recurses through nested polymorphic reads (a curve
|
||||
//! holding a curve holding ...) without limit. The port stops at
|
||||
//! [`MAX_READ_NESTING`] = 32 levels ([`DatabaseReader::enter_nested`]); upstream
|
||||
@@ -342,6 +358,42 @@ impl InputStream {
|
||||
}
|
||||
}
|
||||
|
||||
/// `istream::getline(buf, n)` (delimiter `'\n'`): the bytes stored in the
|
||||
/// `n`-byte buffer (without the terminating NUL). Stores at most `n - 1` bytes,
|
||||
/// stopping before the end of the data (`eofbit`) or at a newline (consumed, not
|
||||
/// stored); when the buffer fills before a newline, `failbit` is set and the next
|
||||
/// byte is left in the stream. Nothing extracted sets `failbit`, as does a stream
|
||||
/// that was not good (nothing is read; the buffer is empty).
|
||||
pub fn getline(&mut self, n: usize) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
if !self.sentry(false) {
|
||||
return out;
|
||||
}
|
||||
let mut gcount = 0usize;
|
||||
while gcount + 1 < n {
|
||||
match self.peek_byte() {
|
||||
Some(c) if c != b'\n' => {
|
||||
out.push(c);
|
||||
self.pos += 1;
|
||||
gcount += 1;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
match self.peek_byte() {
|
||||
None => self.set_state(IoState::EOF),
|
||||
Some(b'\n') => {
|
||||
gcount += 1;
|
||||
self.pos += 1;
|
||||
}
|
||||
Some(_) => self.set_state(IoState::FAIL),
|
||||
}
|
||||
if gcount == 0 {
|
||||
self.set_state(IoState::FAIL);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `istream::get()`: the next byte, or `None` (`eofbit|failbit`) at the end
|
||||
/// (`failbit` if the stream was not good).
|
||||
pub fn get(&mut self) -> Option<u8> {
|
||||
@@ -967,6 +1019,13 @@ impl DatabaseReader {
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
/// Reads `n` bytes (`ReadCharacters`; never swapped: `char` is one byte).
|
||||
pub fn read_characters(&mut self, n: usize) -> Result<Vec<u8>> {
|
||||
let items =
|
||||
self.read_raw_items::<1>(n, "topology::database_io::DatabaseReader::read_characters")?;
|
||||
Ok(items.into_iter().map(|[c]| c).collect())
|
||||
}
|
||||
|
||||
/// Reads an object type header (`ReadType`): in ASCII the line
|
||||
/// `Object Type <code>` and, when `with_dim`, `Curve Dim <dim>`; in binary a
|
||||
/// long and optionally an unsigned long.
|
||||
@@ -1005,6 +1064,218 @@ impl DatabaseReader {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- write side -----------------------------------------------------------------------
|
||||
|
||||
/// `std::ostream << double` with `fixed` and `precision(16)` (glibc `%.16f`): `inf`,
|
||||
/// `-inf`, `nan` and `-nan` for the special values.
|
||||
#[must_use]
|
||||
pub fn fixed16(x: f64) -> String {
|
||||
if x.is_nan() {
|
||||
if x.is_sign_negative() {
|
||||
"-nan".to_string()
|
||||
} else {
|
||||
"nan".to_string()
|
||||
}
|
||||
} else if x.is_infinite() {
|
||||
if x < 0.0 {
|
||||
"-inf".to_string()
|
||||
} else {
|
||||
"inf".to_string()
|
||||
}
|
||||
} else {
|
||||
format!("{x:.16}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes values to an SMLib database stream (upstream `SmDatabaseIO` write side over
|
||||
/// the stream `SmDatabaseIOFile::OpenFileForWrite` opens), kept in memory.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DatabaseWriter {
|
||||
file_type: FileType,
|
||||
byte_swap: bool,
|
||||
out: Vec<u8>,
|
||||
}
|
||||
|
||||
impl DatabaseWriter {
|
||||
/// `SmDatabaseIOFile::OpenFileForWrite(name, eType, TRUE)`: an empty stream of
|
||||
/// `file_type`, byte-swapped for [`FileType::ByteSwap`] or on a big-endian machine.
|
||||
/// An ASCII stream writes doubles `fixed` with precision 16 ([`fixed16`]).
|
||||
#[must_use]
|
||||
pub fn open_for_write(file_type: FileType) -> Self {
|
||||
Self {
|
||||
file_type,
|
||||
byte_swap: file_type.default_byte_swap(),
|
||||
out: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The file type (`GetFileType`).
|
||||
#[must_use]
|
||||
pub fn file_type(&self) -> FileType {
|
||||
self.file_type
|
||||
}
|
||||
|
||||
/// Whether binary values are byte-swapped (`m_bByteSwap`).
|
||||
#[must_use]
|
||||
pub fn byte_swap(&self) -> bool {
|
||||
self.byte_swap
|
||||
}
|
||||
|
||||
/// The bytes written so far (the file's contents).
|
||||
#[must_use]
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
&self.out
|
||||
}
|
||||
|
||||
/// The bytes written (the file's contents).
|
||||
#[must_use]
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
self.out
|
||||
}
|
||||
|
||||
/// Hook before writing (`BeginWriting`); does nothing in the base writer.
|
||||
pub fn begin_writing(&mut self) {}
|
||||
|
||||
/// Hook after writing (`EndWriting`); does nothing in the base writer.
|
||||
pub fn end_writing(&mut self) {}
|
||||
|
||||
/// `*GetOutStreamPtr() << "text"`.
|
||||
pub fn out_str(&mut self, s: &str) -> &mut Self {
|
||||
self.out.extend_from_slice(s.as_bytes());
|
||||
self
|
||||
}
|
||||
|
||||
/// `*GetOutStreamPtr() << value` for a signed integer (`long`, `int`, `short`).
|
||||
pub fn out_i64(&mut self, v: i64) -> &mut Self {
|
||||
self.out.extend_from_slice(v.to_string().as_bytes());
|
||||
self
|
||||
}
|
||||
|
||||
/// `*GetOutStreamPtr() << value` for an unsigned integer (`ULONG`).
|
||||
pub fn out_u64(&mut self, v: u64) -> &mut Self {
|
||||
self.out.extend_from_slice(v.to_string().as_bytes());
|
||||
self
|
||||
}
|
||||
|
||||
/// `*GetOutStreamPtr() << value` for a double ([`fixed16`]).
|
||||
pub fn out_f64(&mut self, v: f64) -> &mut Self {
|
||||
self.out.extend_from_slice(fixed16(v).as_bytes());
|
||||
self
|
||||
}
|
||||
|
||||
/// `v0 << " " << v1 << ...`: doubles separated by single spaces (not an upstream
|
||||
/// function).
|
||||
pub fn out_reals(&mut self, values: &[f64]) -> &mut Self {
|
||||
for (i, &v) in values.iter().enumerate() {
|
||||
if i > 0 {
|
||||
self.out_str(" ");
|
||||
}
|
||||
self.out_f64(v);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// One line of doubles, the common shape of the geometry writers (not an upstream
|
||||
/// function): `v0 << " " << v1 ... << "\n"` in ASCII, `WriteDouble` of each in
|
||||
/// binary.
|
||||
pub fn reals(&mut self, values: &[f64]) {
|
||||
if self.file_type == FileType::Ascii {
|
||||
self.out_reals(values).out_str("\n");
|
||||
} else {
|
||||
self.write_doubles(values);
|
||||
}
|
||||
}
|
||||
|
||||
fn put(&mut self, bytes: &[u8]) {
|
||||
self.out.extend_from_slice(bytes);
|
||||
}
|
||||
|
||||
/// Writes a signed 8-byte integer (`WriteLong`).
|
||||
pub fn write_long(&mut self, v: i64) {
|
||||
let v = if self.byte_swap {
|
||||
byte_swap_u64(v as u64) as i64
|
||||
} else {
|
||||
v
|
||||
};
|
||||
self.put(&v.to_ne_bytes());
|
||||
}
|
||||
|
||||
/// Writes unsigned 8-byte integers (`WriteLongs`).
|
||||
pub fn write_longs(&mut self, values: &[u64]) {
|
||||
for &v in values {
|
||||
let v = if self.byte_swap { byte_swap_u64(v) } else { v };
|
||||
self.put(&v.to_ne_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a double (`WriteDouble`).
|
||||
pub fn write_double(&mut self, v: f64) {
|
||||
let v = if self.byte_swap { byte_swap_f64(v) } else { v };
|
||||
self.put(&v.to_ne_bytes());
|
||||
}
|
||||
|
||||
/// Writes doubles (`WriteDoubles`).
|
||||
pub fn write_doubles(&mut self, values: &[f64]) {
|
||||
for &v in values {
|
||||
self.write_double(v);
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes bytes (`WriteCharacters`; never swapped: `char` is one byte).
|
||||
pub fn write_characters(&mut self, chars: &[u8]) {
|
||||
self.put(chars);
|
||||
}
|
||||
|
||||
/// Writes a 4-byte `SmBoolean` (`WriteBoolean`; never swapped, as upstream).
|
||||
pub fn write_boolean(&mut self, v: i32) {
|
||||
self.put(&v.to_ne_bytes());
|
||||
}
|
||||
|
||||
/// Writes 4-byte `SmBoolean`s (`WriteBooleans`), byte-swapped when enabled.
|
||||
pub fn write_booleans(&mut self, values: &[i32]) {
|
||||
for &v in values {
|
||||
if self.byte_swap {
|
||||
self.put(&byte_swap_u32(v as u32).to_ne_bytes());
|
||||
} else {
|
||||
self.put(&v.to_ne_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a 2-byte `short` (`WriteShort`).
|
||||
pub fn write_short(&mut self, v: i16) {
|
||||
let v = if self.byte_swap {
|
||||
byte_swap_u16(v as u16) as i16
|
||||
} else {
|
||||
v
|
||||
};
|
||||
self.put(&v.to_ne_bytes());
|
||||
}
|
||||
|
||||
/// Writes one byte (`WriteChar`; never swapped).
|
||||
pub fn write_char(&mut self, c: u8) {
|
||||
self.put(&[c]);
|
||||
}
|
||||
|
||||
/// Writes an object type header (`WriteType`): in ASCII the line
|
||||
/// `Object Type <code>` and, with `dim`, `Curve Dim <dim>`; in binary a long and
|
||||
/// optionally a second long.
|
||||
pub fn write_type(&mut self, type_code: i64, dim: Option<u64>) {
|
||||
if self.file_type == FileType::Ascii {
|
||||
self.out_str("Object Type ").out_i64(type_code);
|
||||
self.out_str("\n");
|
||||
if let Some(d) = dim {
|
||||
self.out_str("Curve Dim ").out_u64(d).out_str("\n");
|
||||
}
|
||||
} else {
|
||||
self.write_long(type_code);
|
||||
if let Some(d) = dim {
|
||||
self.write_long(d as i64);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
//! Writing curves and surfaces to an SMLib database stream: the virtual `WriteToDB` of
|
||||
//! `SmCurve` and `SmSurface` dispatched over the concrete kinds, and the writers of the
|
||||
//! surfaces of revolution and extrusion and their analytic kinds.
|
||||
//!
|
||||
//! Upstream: `source/SMLib/src/SmSurfOfRevolution.cpp`, `SmSurfOfExtrusion.cpp`,
|
||||
//! `SmCone.cpp`, `SmCylinder.cpp`, `SmSphere.cpp`, `SmTorus.cpp` (and the `WriteToDB`
|
||||
//! overrides of the other curve and surface classes, dispatched to).
|
||||
//!
|
||||
//! | Rust | Upstream |
|
||||
//! |------------------------------|--------------------------------------------------|
|
||||
//! | [`write_curve`] | `SmCurve::WriteToDB` (virtual) |
|
||||
//! | [`write_surface`] | `SmSurface::WriteToDB` (virtual) |
|
||||
//! | `write_surf_of_revolution` (private) | `SmSurfOfRevolution::WriteToDB` |
|
||||
//! | `write_surf_of_extrusion` (private) | `SmSurfOfExtrusion::WriteToDB` |
|
||||
//! | `write_cone` (private) | `SmCone::WriteToDB`, `SmCylinder::WriteToDB` |
|
||||
//! | `write_sphere` (private) | `SmSphere::WriteToDB` |
|
||||
//! | `write_torus` (private) | `SmTorus::WriteToDB` |
|
||||
//!
|
||||
//! Deviations:
|
||||
//! - **Kinds without a writer.** The write side is not in the plan (see
|
||||
//! `plan/findings.md`); only the kinds B-rep files hold are ported: B-spline curves,
|
||||
//! lines, ellipses and circles; B-spline surfaces, planes, cones and cylinders,
|
||||
//! spheres, tori, surfaces of revolution and extrusion, offset surfaces. Another
|
||||
//! kind is [`SolidError::Unported`] with its `WriteToDB` symbol and unit
|
||||
//! `(not in plan)`.
|
||||
//! - **Missing NURB.** Upstream's analytic writers ignore the status of the B-spline
|
||||
//! part they end with; it cannot fail there, as the virtual
|
||||
//! `GetOrCreateGwNurbPointer` makes the NURB. The port cannot make a surface of
|
||||
//! revolution's NURB without the kernel, so a missing NURB is an error (`SM_ERR`)
|
||||
//! instead of a silently short record.
|
||||
//! - **Recursion.** Writers nest through generator curves and offset bases. The depth
|
||||
//! is counted and bounded by [`MAX_GEOMETRY_NESTING`] (the bound every geometry
|
||||
//! constructor and the reader keep), past which the write is
|
||||
//! [`SolidError::InvalidInput`].
|
||||
|
||||
use crate::curve::curve::{Curve, CurveKind};
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::surface::cone::Cone;
|
||||
use crate::surface::sphere::Sphere;
|
||||
use crate::surface::surf_of_extrusion::SurfOfExtrusion;
|
||||
use crate::surface::surf_of_revolution::SurfOfRevolution;
|
||||
use crate::surface::surface::p2::MAX_GEOMETRY_NESTING;
|
||||
use crate::surface::surface::{Surface, SurfaceKind};
|
||||
use crate::surface::torus::Torus;
|
||||
use crate::topology::database_io::{DatabaseWriter, FileType};
|
||||
|
||||
const OP_WRITE: &str = "topology::geometry_write::write_surface";
|
||||
const NOT_IN_PLAN: &str = "(not in plan)";
|
||||
|
||||
/// `pCrv->WriteToDB(rDB, lDBVersionNumber)`: writes `c` (without its type header) in
|
||||
/// its own format.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::Unported`] for a kind without a ported writer; those of the kind's
|
||||
/// writer.
|
||||
pub fn write_curve(c: &Curve, db: &mut DatabaseWriter, version: u64) -> Result<()> {
|
||||
write_curve_at(c, db, version, 0)
|
||||
}
|
||||
|
||||
/// [`write_curve`] for a curve nested `depth` levels deep.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of [`write_curve`]; [`SolidError::InvalidInput`] past
|
||||
/// [`MAX_GEOMETRY_NESTING`].
|
||||
pub fn write_curve_at(
|
||||
c: &Curve,
|
||||
db: &mut DatabaseWriter,
|
||||
version: u64,
|
||||
depth: usize,
|
||||
) -> Result<()> {
|
||||
check_depth(depth)?;
|
||||
match c.kind() {
|
||||
CurveKind::BSpline(b) => b.write_to_db(db, version),
|
||||
CurveKind::Line(l) => l.write_to_db(db, version),
|
||||
CurveKind::Ellipse(e) => e.write_to_db(db, version),
|
||||
CurveKind::Hermite(_) => Err(unported("SmHermiteCurve::WriteToDB")),
|
||||
CurveKind::Hyperbola(_) => Err(unported("SmHyperbola::WriteToDB")),
|
||||
CurveKind::Projected(_) => Err(unported("SmProjectedCurve::WriteToDB")),
|
||||
CurveKind::Iso(_) => Err(unported("SmIsoCurve::WriteToDB")),
|
||||
CurveKind::Offset(_) => Err(unported("SmOffsetCurve::WriteToDB")),
|
||||
CurveKind::Conic(_) => Err(unported("SmConic::WriteToDB")),
|
||||
CurveKind::CrvInVolume(_) => Err(unported("SmCrvInVolume::WriteToDB")),
|
||||
CurveKind::CrvOnSurf(_) => Err(unported("SmCrvOnSurf::WriteToDB")),
|
||||
CurveKind::Parabola(_) => Err(unported("SmParabola::WriteToDB")),
|
||||
CurveKind::Composite(_) => Err(unported("SmCompositeCurve::WriteToDB")),
|
||||
}
|
||||
}
|
||||
|
||||
/// `pSrf->WriteToDB(rDB, lDBVersionNumber)`: writes `s` (without its type header) in
|
||||
/// its own format.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::Unported`] for a kind without a ported writer; those of the kind's
|
||||
/// writer.
|
||||
pub fn write_surface(s: &Surface, db: &mut DatabaseWriter, version: u64) -> Result<()> {
|
||||
write_surface_at(s, db, version, 0)
|
||||
}
|
||||
|
||||
/// [`write_surface`] for a surface nested `depth` levels deep.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of [`write_surface`]; [`SolidError::InvalidInput`] past
|
||||
/// [`MAX_GEOMETRY_NESTING`].
|
||||
pub fn write_surface_at(
|
||||
s: &Surface,
|
||||
db: &mut DatabaseWriter,
|
||||
version: u64,
|
||||
depth: usize,
|
||||
) -> Result<()> {
|
||||
check_depth(depth)?;
|
||||
match s.kind() {
|
||||
SurfaceKind::BSpline(b) => b.write_to_db(db, version),
|
||||
SurfaceKind::Plane(p) => p.write_to_db(db, version),
|
||||
SurfaceKind::Cone(c) => write_cone(c, db, version, depth),
|
||||
SurfaceKind::Sphere(sp) => write_sphere(sp, db, version, depth),
|
||||
SurfaceKind::Torus(t) => write_torus(t, db, version, depth),
|
||||
SurfaceKind::SurfOfRevolution(r) => write_surf_of_revolution(r, db, version, depth),
|
||||
SurfaceKind::SurfOfExtrusion(e) => write_surf_of_extrusion(e, db, version, depth),
|
||||
SurfaceKind::OffsetSurface(o) => o.write_to_db(db, version, depth),
|
||||
SurfaceKind::CurveBounded(_) => Err(unported("SmCurveBoundedSurface::WriteToDB")),
|
||||
SurfaceKind::Step(_) => Err(unported("SmSTEPSurface::WriteToDB")),
|
||||
SurfaceKind::SrfInVolume(_) => Err(unported("SmSrfInVolume::WriteToDB")),
|
||||
}
|
||||
}
|
||||
|
||||
fn unported(symbol: &'static str) -> SolidError {
|
||||
SolidError::unported(symbol, NOT_IN_PLAN)
|
||||
}
|
||||
|
||||
fn check_depth(depth: usize) -> Result<()> {
|
||||
if depth > MAX_GEOMETRY_NESTING {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP_WRITE,
|
||||
"geometry nested too deeply",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_ascii(db: &DatabaseWriter) -> bool {
|
||||
db.file_type() == FileType::Ascii
|
||||
}
|
||||
|
||||
/// `SmCone::WriteToDB` (`SmCylinder::WriteToDB` passes the call on): the base radius,
|
||||
/// the semi-angle (degrees) and the inside-out flag, then the surface of revolution.
|
||||
fn write_cone(c: &Cone, db: &mut DatabaseWriter, version: u64, depth: usize) -> Result<()> {
|
||||
let inside_out = i32::from(c.inside_out);
|
||||
if is_ascii(db) {
|
||||
db.out_f64(c.base_radius);
|
||||
db.out_str(" SmCone Radius of the cone in the plane passing through origin \n");
|
||||
db.out_f64(c.semi_angle_deg);
|
||||
db.out_str(" SmCone SemiAngleDeg between -90 and 90 \n");
|
||||
db.out_i64(inside_out.into());
|
||||
db.out_str(
|
||||
" SmCone InsideOutFlag: FALSE= m_vPosition.ZAxis points from bot to top of Nurb, \
|
||||
TRUE=Inverted \n",
|
||||
);
|
||||
} else {
|
||||
db.write_double(c.base_radius);
|
||||
db.write_double(c.semi_angle_deg);
|
||||
db.write_boolean(inside_out);
|
||||
}
|
||||
write_surf_of_revolution(&c.rev, db, version, depth)
|
||||
}
|
||||
|
||||
/// `SmSphere::WriteToDB`: the radius and the inside-out flag, then the surface of
|
||||
/// revolution.
|
||||
fn write_sphere(s: &Sphere, db: &mut DatabaseWriter, version: u64, depth: usize) -> Result<()> {
|
||||
let inside_out = i32::from(s.inside_out);
|
||||
if is_ascii(db) {
|
||||
db.out_f64(s.radius).out_str(" SmSphere Sphere Radius \n");
|
||||
db.out_i64(inside_out.into());
|
||||
db.out_str(" SmSphere Sphere InsideOut: TRUE=outward pointing normal, FALSE=inverted \n");
|
||||
} else {
|
||||
db.write_double(s.radius);
|
||||
db.write_boolean(inside_out);
|
||||
}
|
||||
write_surf_of_revolution(&s.base, db, version, depth)
|
||||
}
|
||||
|
||||
/// `SmTorus::WriteToDB`: the major and minor radii and the inside-out flag, then the
|
||||
/// surface of revolution.
|
||||
fn write_torus(t: &Torus, db: &mut DatabaseWriter, version: u64, depth: usize) -> Result<()> {
|
||||
let inside_out = i32::from(t.inside_out);
|
||||
if is_ascii(db) {
|
||||
db.out_f64(t.major_radius);
|
||||
db.out_str(" SmTorus Torus Major Radius \n");
|
||||
db.out_f64(t.minor_radius);
|
||||
db.out_str(" SmTorus Torus Minor Radius \n");
|
||||
db.out_i64(inside_out.into());
|
||||
db.out_str(" SmTorus Torus InsideOut: TRUE=outward pointing normal, FALSE=inverted \n");
|
||||
} else {
|
||||
db.write_double(t.major_radius);
|
||||
db.write_double(t.minor_radius);
|
||||
db.write_boolean(inside_out);
|
||||
}
|
||||
write_surf_of_revolution(&t.base, db, version, depth)
|
||||
}
|
||||
|
||||
/// `SmSurfOfRevolution::WriteToDB`: the flags, the placement, the analytic UV domain
|
||||
/// and the swap flag; then the generator curve with its type header, the polar
|
||||
/// converter and the B-spline surface part.
|
||||
fn write_surf_of_revolution(
|
||||
r: &SurfOfRevolution,
|
||||
db: &mut DatabaseWriter,
|
||||
version: u64,
|
||||
depth: usize,
|
||||
) -> Result<()> {
|
||||
let ascii = is_ascii(db);
|
||||
let o = r.position.origin();
|
||||
let x = r.position.x_axis();
|
||||
let y = r.position.y_axis();
|
||||
let (lo, hi) = (r.anal_uv_domain.min(), r.anal_uv_domain.max());
|
||||
let make_nurb_gen_curve = i32::from(r.make_nurb_gen_curve);
|
||||
let planar_generator = i32::from(r.planar_generator);
|
||||
let swap_uv = i32::from(r.swap_uv);
|
||||
if ascii {
|
||||
db.out_i64(make_nurb_gen_curve.into());
|
||||
db.out_str(
|
||||
" SmSurfOfRevolution Make Nurb GenCurve Flag, FALSE=Unbounded analytic, \
|
||||
TRUE=bounded \n",
|
||||
);
|
||||
db.out_i64(planar_generator.into());
|
||||
db.out_str(" SmSurfOfRevolution Planar Generator Curve \n");
|
||||
db.out_reals(&[o.x, o.y, o.z]);
|
||||
db.out_str(" SmSurfOfRevolution Placement Origin \n");
|
||||
db.out_reals(&[x.x, x.y, x.z]);
|
||||
db.out_str(" SmSurfOfRevolution Placement X Axis \n");
|
||||
db.out_reals(&[y.x, y.y, y.z]);
|
||||
db.out_str(" SmSurfOfRevolution Placement Y Axis \n");
|
||||
db.out_reals(&[lo.x, lo.y, hi.x, hi.y]);
|
||||
db.out_str(" SmSurfOfRevolution AnalUVDomain \n");
|
||||
db.out_i64(swap_uv.into());
|
||||
db.out_str(" SmSurfOfRevolution SwapUV Boolean \n");
|
||||
} else {
|
||||
db.write_boolean(make_nurb_gen_curve);
|
||||
db.write_boolean(planar_generator);
|
||||
db.write_doubles(&[o.x, o.y, o.z, x.x, x.y, x.z, y.x, y.y, y.z]);
|
||||
db.write_doubles(&[lo.x, lo.y, hi.x, hi.y]);
|
||||
db.write_boolean(swap_uv);
|
||||
}
|
||||
|
||||
// the generator curve (upstream dereferences a null one)
|
||||
let gen = r
|
||||
.gen_curve
|
||||
.as_ref()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_WRITE))?;
|
||||
if ascii {
|
||||
db.out_str(" SmSurfOfRevolution->GenCurve \n");
|
||||
}
|
||||
db.write_type(gen.type_code(), Some(gen.dim()));
|
||||
write_curve_at(gen, db, version, depth + 1)?;
|
||||
|
||||
// the polar converter (with its own copy of the curve, if it has one)
|
||||
if ascii {
|
||||
db.out_str(" SmSurfOfRevolution->PolarConverter \n");
|
||||
}
|
||||
r.polar
|
||||
.write_to_db(db, version, r.polar_curve.as_ref(), depth + 1)?;
|
||||
|
||||
r.base.write_to_db(db, version)
|
||||
}
|
||||
|
||||
/// `SmSurfOfExtrusion::WriteToDB`: the origin, the extrusion vector, the analytic UV
|
||||
/// domain, the generator's orientation type, the swap flag and the generator's point
|
||||
/// and vector; then the generator curve with its type header and the B-spline surface
|
||||
/// part.
|
||||
fn write_surf_of_extrusion(
|
||||
e: &SurfOfExtrusion,
|
||||
db: &mut DatabaseWriter,
|
||||
version: u64,
|
||||
depth: usize,
|
||||
) -> Result<()> {
|
||||
let ascii = is_ascii(db);
|
||||
let (o, v) = (e.origin, e.extrusion_vec);
|
||||
let (lo, hi) = (e.anal_uv_domain.min(), e.anal_uv_domain.max());
|
||||
let orientation = e.orientation as i64;
|
||||
let swap_uv = i32::from(e.swap_uv);
|
||||
let (cp, cv) = (e.curve_point, e.curve_vec);
|
||||
if ascii {
|
||||
db.out_reals(&[o.x, o.y, o.z]);
|
||||
db.out_str(" SmSurfOfExtrusion Origin \n");
|
||||
db.out_reals(&[v.x, v.y, v.z]);
|
||||
db.out_str(" SmSurfOfExtrusion Extrusion Vector \n");
|
||||
db.out_reals(&[lo.x, lo.y, hi.x, hi.y]);
|
||||
db.out_str(" SmSurfOfExtrusion AnalUVDomain \n");
|
||||
db.out_i64(orientation);
|
||||
db.out_str(" SmSurfOfExtrusion Curve Orientation Type \n");
|
||||
db.out_i64(swap_uv.into());
|
||||
db.out_str(" SmSurfOfExtrusion SwapUV Boolean \n");
|
||||
db.out_reals(&[cp.x, cp.y, cp.z]);
|
||||
db.out_str(
|
||||
" SmSurfOfExtrusion When GenCurve is planar or linear - point on plane or line\n",
|
||||
);
|
||||
db.out_reals(&[cv.x, cv.y, cv.z]);
|
||||
db.out_str(
|
||||
" SmSurfOfExtrusion When GenCurve is planar or linear - plane normal or line vec \n",
|
||||
);
|
||||
} else {
|
||||
db.write_doubles(&[o.x, o.y, o.z, v.x, v.y, v.z]);
|
||||
db.write_doubles(&[lo.x, lo.y, hi.x, hi.y]);
|
||||
db.write_long(orientation);
|
||||
db.write_boolean(swap_uv);
|
||||
db.write_doubles(&[cp.x, cp.y, cp.z, cv.x, cv.y, cv.z]);
|
||||
}
|
||||
|
||||
// the generator curve (upstream dereferences a null one)
|
||||
let gen = e
|
||||
.gen_curve
|
||||
.as_ref()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_WRITE))?;
|
||||
if ascii {
|
||||
db.out_str(" SmSurfOfExtrusion->GenCurve \n");
|
||||
}
|
||||
db.write_type(gen.type_code(), Some(gen.dim()));
|
||||
write_curve_at(gen, db, version, depth + 1)?;
|
||||
|
||||
e.base.write_to_db(db, version)
|
||||
}
|
||||
@@ -0,0 +1,928 @@
|
||||
//! Reading an SMLib part file (`.smp`, ASCII or binary): the stand-alone curves and
|
||||
//! surfaces, the Boolean tree nodes and every B-rep of a part, with the attributes
|
||||
//! they share, as `SmBrepData::ReadPartFromFile` does.
|
||||
//!
|
||||
//! Upstream: `source/SMLib/src/SmBrepData.cpp`, `source/SMLib/inc/SmBrepData.h`,
|
||||
//! `source/SMLib/src/SmAttribute.cpp`.
|
||||
//!
|
||||
//! | Rust | Upstream |
|
||||
//! |------------------------------|--------------------------------------------------|
|
||||
//! | [`read_part_from_file`] | `SmBrepData::ReadPartFromFile` |
|
||||
//! | [`read_part_from_db`] | `SmBrepData::ReadPartFromDB` |
|
||||
//! | [`read_attribute_from_db`] | `SmBrepData::ReadAttributeFromDB` |
|
||||
//! | [`create_attribute`] | `SmAttribute::CreateAttribute` |
|
||||
//! | [`Part`] | the `r3DCurves`, `rSurfaces`, `rBooleanTreeNodes` and `rBreps` out-params |
|
||||
//! | [`PartEnv`] | the attribute heap and the new objects' addresses |
|
||||
//!
|
||||
//! Deviations:
|
||||
//! - **Out-params -> return value.** The part is returned as a [`Part`]. On an error
|
||||
//! nothing is returned; upstream leaves what it read so far in the caller's arrays.
|
||||
//! - **Files.** [`read_part_from_file`] takes the file's bytes (no filesystem on
|
||||
//! wasm32); a file that cannot be opened is the caller's error.
|
||||
//! - **Identities.** Upstream objects are known by address. [`PartEnv::key_of`] names
|
||||
//! the topology objects of each B-rep (by B-rep index) and [`PartEnv::next_key`]
|
||||
//! hands out the keys of the geometry read and of the B-reps' geometry copies, which
|
||||
//! share one [`AttributeStore`] as upstream objects share the heap; the caller keeps
|
||||
//! the two key ranges apart.
|
||||
//! - **ASCII lines.** `fin.getline(sBuff, MAXSIZE)` is [`InputStream::getline`] with
|
||||
//! `MAXSIZE` 256. `SSCANF(sBuff, "<text>%ld<text>", &x)` is `scan_long`, glibc's
|
||||
//! rules for the part before the conversion (white space in the format matches any
|
||||
//! white space, other characters must match) and for `%ld` (skips white space, an
|
||||
//! optional sign, decimal digits, clamped to the `long` range on overflow); the text
|
||||
//! after the conversion cannot change the value. A failed conversion stores nothing:
|
||||
//! the counts keep their initial `0`. The `%ld` read into a `ULONG` count wraps a
|
||||
//! negative value, as upstream. The `[Curve #%ld]`, `[Surface #%ld]` and
|
||||
//! `[Brep #%ld]` indices are never used and are not scanned. The uninitialised
|
||||
//! locals `lNodeValue` and `lTotalAttributes` are `0` when nothing is stored.
|
||||
//! - **Counts (not upstream; upstream trusts them).** A header-declared count is
|
||||
//! [`SolidError::InvalidInput`] when it is above its sanity cap
|
||||
//! ([`MAX_PART_ITEMS`] curves, surfaces, tree nodes or attributes,
|
||||
//! [`MAX_PART_BREPS`] B-reps, [`MAX_ATTRIBUTE_VALUES`] values of one attribute) or
|
||||
//! when the remaining input cannot hold the smallest record of that kind
|
||||
//! ([`DatabaseReader::ensure_available`] with the per-kind minimum sizes below).
|
||||
//! Items are appended as they are read, never preallocated. Every loop over
|
||||
//! such a count fails closed: before each item after the first the stream must be
|
||||
//! good, and each item must consume input, else [`SolidError::InvalidInput`]
|
||||
//! (upstream would go on reading nothing for the rest of the count).
|
||||
//! - **Attribute table.** The placeholder table `sAttributes` is
|
||||
//! [`AttributeSink::all`]; an index first seen past its end leaves `NULL` slots
|
||||
//! below it. `MakeTopologyFromData` gets the table with each `NULL` slot standing
|
||||
//! for an unused placeholder (no record refers to it: every index read made its
|
||||
//! own); the placeholder is deleted at the end. The table is kept in step with the
|
||||
//! slots as B-reps are read (new slots appended; a `NULL` slot a later B-rep fills is
|
||||
//! updated from that B-rep's records), not rebuilt per B-rep. The attribute section reaching a
|
||||
//! `NULL` slot (upstream dereferences it) or an index past the table (upstream reads
|
||||
//! past the array) is [`SolidError::InvalidInput`].
|
||||
//! - **`CreateAttribute`.** The kernel registers no attribute callbacks, so none is
|
||||
//! tried. A colour attribute with fewer than 3 doubles or a tag attribute with fewer
|
||||
//! than 8 longs (upstream reads its uninitialised stack buffer) and a behaviour
|
||||
//! outside `SmAttributeBehaviorType` (upstream casts it) are
|
||||
//! [`SolidError::InvalidInput`].
|
||||
//! - **Recursion.** None here; nested curves are bounded by [`read_curve_from_db`].
|
||||
|
||||
use crate::curve::curve::Curve;
|
||||
use crate::error::{EntityRef, Result, SolidError};
|
||||
use crate::geom::attribute::{
|
||||
ids, Attribute, AttributeHandle, AttributeStore, Behavior, ObjectKey, TagData,
|
||||
};
|
||||
use crate::geom::object::ObjectRef;
|
||||
use crate::kernel::Kernel;
|
||||
use crate::math::Vec3;
|
||||
use crate::surface::surface::Surface;
|
||||
use crate::topology::brep::p10::{make_topology_from_data, MakeTopologyOptions, TopologyEnv};
|
||||
use crate::topology::brep_data::{
|
||||
read_curve_from_db, read_surface_from_db, AttributeSink, BrepData, Keyed, TopologyData,
|
||||
};
|
||||
use crate::topology::database_io::{DatabaseReader, FileType, InputStream};
|
||||
use crate::topology::topology::Topology;
|
||||
use crate::topology::Brep;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const OP_PART: &str = "topology::part_file::read_part_from_db";
|
||||
const OP_ATTRIBUTE: &str = "topology::part_file::read_attribute_from_db";
|
||||
const OP_CREATE: &str = "topology::part_file::create_attribute";
|
||||
|
||||
/// Sanity cap on the stand-alone curves, surfaces, Boolean tree nodes and attributes a
|
||||
/// part may declare (not upstream; upstream fixtures hold a few dozen).
|
||||
pub const MAX_PART_ITEMS: u64 = 1_000_000;
|
||||
/// Sanity cap on the B-reps a part may declare (not upstream; fixtures hold at most 51).
|
||||
pub const MAX_PART_BREPS: u64 = 100_000;
|
||||
/// Sanity cap on the longs, doubles or characters of one attribute (not upstream).
|
||||
pub const MAX_ATTRIBUTE_VALUES: u64 = 1_000_000;
|
||||
|
||||
/// Smallest stand-alone curve or surface record: ASCII, the two separator lines and the
|
||||
/// `Object Type <n>` header (at least 16 bytes); binary, the type and dimension longs
|
||||
/// (a surface: the type long and the B-spline indices).
|
||||
const MIN_GEOMETRY_BYTES: usize = 16;
|
||||
/// Smallest B-rep record: ASCII, the separator lines, `//Brep Starts` and the section
|
||||
/// count lines; binary, the colour (or attribute count), tolerance, flags and the
|
||||
/// twelve section counts (at least 128 bytes either way).
|
||||
const MIN_BREP_BYTES: usize = 128;
|
||||
/// Smallest attribute record: ASCII, two separator lines and the five-number header
|
||||
/// line (12 bytes); binary, five longs (40).
|
||||
const MIN_ATTRIBUTE_BYTES_ASCII: usize = 12;
|
||||
const MIN_ATTRIBUTE_BYTES_BINARY: usize = 40;
|
||||
|
||||
/// `MAXSIZE` of `SmBrepData.cpp`: the `getline` buffer size.
|
||||
const MAXSIZE: usize = 256;
|
||||
|
||||
/// What a part file holds (upstream's out-params).
|
||||
#[derive(Debug)]
|
||||
pub struct Part {
|
||||
/// `r3DCurves`: the stand-alone curves, with their attribute keys.
|
||||
pub curves: Vec<Keyed<Curve>>,
|
||||
/// `rSurfaces`: the stand-alone surfaces, with their attribute keys.
|
||||
pub surfaces: Vec<Keyed<Surface>>,
|
||||
/// `rBooleanTreeNodes`: the Boolean tree nodes (see `SmMerge::BooleanTreeNodes`).
|
||||
pub boolean_tree_nodes: Vec<i64>,
|
||||
/// `rBreps`: the B-rep models.
|
||||
pub breps: Vec<Brep>,
|
||||
}
|
||||
|
||||
/// The objects' identities and attributes of a part read (upstream: the heap and
|
||||
/// the objects' addresses).
|
||||
pub struct PartEnv<'a> {
|
||||
/// Owner of every attribute read and of every object's attribute list.
|
||||
pub attributes: &'a mut AttributeStore,
|
||||
/// The identity of each topology entity of the B-rep with the given index (in
|
||||
/// file order).
|
||||
pub key_of: &'a dyn Fn(usize, EntityRef) -> ObjectRef,
|
||||
/// The `SmTopology` records of the topology objects built, by identity.
|
||||
pub records: &'a mut BTreeMap<ObjectKey, Topology>,
|
||||
/// The next free identity for a geometry object (read or copied); incremented per
|
||||
/// object.
|
||||
pub next_key: u64,
|
||||
}
|
||||
|
||||
/// `*rDB.GetInStreamPtr()` (upstream dereferences `NULL` without a stream).
|
||||
fn stream(db: &mut DatabaseReader) -> Result<&mut InputStream> {
|
||||
db.in_stream_mut()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_PART))
|
||||
}
|
||||
|
||||
/// `fin.getline(sBuff, MAXSIZE)`.
|
||||
fn line(db: &mut DatabaseReader) -> Result<Vec<u8>> {
|
||||
Ok(stream(db)?.getline(MAXSIZE))
|
||||
}
|
||||
|
||||
/// `std::isspace` in the "C" locale.
|
||||
fn is_space(c: u8) -> bool {
|
||||
c == b' ' || (b'\t'..=b'\r').contains(&c)
|
||||
}
|
||||
|
||||
fn skip_space(s: &[u8]) -> &[u8] {
|
||||
let n = s.iter().take_while(|&&c| is_space(c)).count();
|
||||
s.get(n..).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// `sscanf(line, "<prefix>%ld...", &x)`: the converted value, or `None` when `line`
|
||||
/// does not match `prefix` or holds no number where the conversion starts (nothing
|
||||
/// is stored).
|
||||
fn scan_long(line: &[u8], prefix: &[u8]) -> Option<i64> {
|
||||
let mut rest = line;
|
||||
for &f in prefix {
|
||||
if is_space(f) {
|
||||
rest = skip_space(rest);
|
||||
} else {
|
||||
let (&c, tail) = rest.split_first()?;
|
||||
if c != f {
|
||||
return None;
|
||||
}
|
||||
rest = tail;
|
||||
}
|
||||
}
|
||||
// %ld
|
||||
rest = skip_space(rest);
|
||||
let (negative, digits) = match rest.split_first() {
|
||||
Some((b'-', tail)) => (true, tail),
|
||||
Some((b'+', tail)) => (false, tail),
|
||||
_ => (false, rest),
|
||||
};
|
||||
let n = digits.iter().take_while(|c| c.is_ascii_digit()).count();
|
||||
if n == 0 {
|
||||
return None;
|
||||
}
|
||||
// strtol: LONG_MIN / LONG_MAX on overflow
|
||||
let clamped = if negative { i64::MIN } else { i64::MAX };
|
||||
let mut value: i64 = 0;
|
||||
for &c in digits.get(..n)? {
|
||||
let d = i64::from(c - b'0');
|
||||
let next = if negative {
|
||||
value.checked_mul(10).and_then(|v| v.checked_sub(d))
|
||||
} else {
|
||||
value.checked_mul(10).and_then(|v| v.checked_add(d))
|
||||
};
|
||||
match next {
|
||||
Some(v) => value = v,
|
||||
None => return Some(clamped),
|
||||
}
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
|
||||
/// A header-declared count as a loop bound: rejected above `cap` or when the remaining
|
||||
/// input cannot hold `min_bytes` per item.
|
||||
fn checked_count(
|
||||
db: &DatabaseReader,
|
||||
count: u64,
|
||||
cap: u64,
|
||||
min_bytes: usize,
|
||||
op: &'static str,
|
||||
) -> Result<usize> {
|
||||
let too_big = SolidError::InvalidInput {
|
||||
operation: op,
|
||||
reason: "header-declared size exceeds the remaining input",
|
||||
};
|
||||
if count > cap {
|
||||
return Err(too_big);
|
||||
}
|
||||
let n = usize::try_from(count).map_err(|_| too_big)?;
|
||||
db.ensure_available(n, min_bytes, op)?;
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// The read position (`0` without a stream).
|
||||
fn position(db: &DatabaseReader) -> usize {
|
||||
db.in_stream().map_or(0, InputStream::position)
|
||||
}
|
||||
|
||||
/// Fail-closed check of item `i` of a counted loop, before it is read: from the second
|
||||
/// item on, the stream must still be good (not upstream).
|
||||
fn before_item(db: &DatabaseReader, i: usize, op: &'static str) -> Result<()> {
|
||||
if i > 0 && !db.in_stream().is_some_and(InputStream::good) {
|
||||
return Err(SolidError::invalid_input(
|
||||
op,
|
||||
"stream ended before the counted items",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fail-closed check after an item of a counted loop: it must have consumed input
|
||||
/// (not upstream).
|
||||
fn after_item(db: &DatabaseReader, start: usize, op: &'static str) -> Result<()> {
|
||||
if position(db) == start {
|
||||
return Err(SolidError::invalid_input(
|
||||
op,
|
||||
"counted item consumed no input",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `SmBrepData::ReadPartFromFile`: reads a part from the contents of a database file
|
||||
/// of `file_type` (see [`read_part_from_db`]).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of [`read_part_from_db`].
|
||||
pub fn read_part_from_file(
|
||||
k: &mut Kernel,
|
||||
data: Vec<u8>,
|
||||
file_type: FileType,
|
||||
env: &mut PartEnv<'_>,
|
||||
) -> Result<Part> {
|
||||
// create databaseIO for operation
|
||||
let mut db = DatabaseReader::new();
|
||||
db.open_for_read(data, file_type);
|
||||
|
||||
// pass the call along
|
||||
read_part_from_db(k, &mut db, env)
|
||||
}
|
||||
|
||||
/// `SmBrepData::ReadPartFromDB`: reads the part summary (curve, surface, tree node and
|
||||
/// B-rep counts; the curve count carries the database version and the surface count
|
||||
/// the healer version, each as millions), then the stand-alone curves (dimension 3)
|
||||
/// and surfaces, the Boolean tree nodes and the B-reps, each read with attributes and
|
||||
/// built by `MakeTopologyFromData(pBrepData, sAttributes, FALSE)`. For a version
|
||||
/// above 30 the attributes follow; each replaces its placeholder on every object
|
||||
/// using it, and the placeholders are deleted.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of [`read_curve_from_db`], [`read_surface_from_db`],
|
||||
/// [`BrepData::read_from_db`], [`make_topology_from_data`] and
|
||||
/// [`read_attribute_from_db`]; a short binary stream, a missing stream, a count
|
||||
/// exceeding the remaining input; [`SolidError::InvalidInput`] for an attribute
|
||||
/// index past the placeholder table or on a `NULL` slot.
|
||||
pub fn read_part_from_db(
|
||||
k: &mut Kernel,
|
||||
db: &mut DatabaseReader,
|
||||
env: &mut PartEnv<'_>,
|
||||
) -> Result<Part> {
|
||||
let ascii = db.file_type() == FileType::Ascii;
|
||||
let mut version: u64 = 30;
|
||||
let mut healer_version: u64 = 0;
|
||||
|
||||
// SmTArray<SmAttribute*> sAttributes;
|
||||
let mut all: Vec<Option<AttributeHandle>> = Vec::new();
|
||||
|
||||
db.begin_reading();
|
||||
|
||||
let mut total_curves: u64 = 0;
|
||||
let mut total_surfaces: u64 = 0;
|
||||
let mut total_breps: u64 = 0;
|
||||
let mut total_tree_nodes: u64 = 0;
|
||||
|
||||
// read counts for stand-alone curves
|
||||
if ascii {
|
||||
line(db)?; // //[Output Summary]
|
||||
if let Some(v) = scan_long(&line(db)?, b"// (1) ") {
|
||||
total_curves = v as u64;
|
||||
}
|
||||
} else {
|
||||
total_curves = db.read_ulong()?;
|
||||
}
|
||||
|
||||
// extract version number embedded within the curve count
|
||||
if total_curves > 1_000_000 {
|
||||
version = total_curves / 1_000_000;
|
||||
total_curves %= 1_000_000;
|
||||
db.set_version(version);
|
||||
}
|
||||
// else: WARN "Database being read has no lDBVersionNumber"
|
||||
|
||||
// read counts for stand-alone surfaces
|
||||
if ascii {
|
||||
if let Some(v) = scan_long(&line(db)?, b"// (2) ") {
|
||||
total_surfaces = v as u64;
|
||||
}
|
||||
} else {
|
||||
// version >= 40: ignore(sizeof(long long) - sizeof(long)) skips nothing on LP64
|
||||
total_surfaces = db.read_ulong()?;
|
||||
}
|
||||
|
||||
// extract Healer version number embedded within the Surface count
|
||||
if total_surfaces > 1_000_000 {
|
||||
healer_version = total_surfaces / 1_000_000;
|
||||
total_surfaces %= 1_000_000;
|
||||
}
|
||||
// else: WARN "Database being read has no lHealerVersion"
|
||||
|
||||
// read counts for stand-alone boolean trees, and brep Models
|
||||
if ascii {
|
||||
if let Some(v) = scan_long(&line(db)?, b"// (3) ") {
|
||||
total_tree_nodes = v as u64;
|
||||
}
|
||||
if let Some(v) = scan_long(&line(db)?, b"// (4) ") {
|
||||
total_breps = v as u64;
|
||||
}
|
||||
} else {
|
||||
total_tree_nodes = db.read_ulong()?;
|
||||
total_breps = db.read_ulong()?;
|
||||
}
|
||||
|
||||
// stand-alone curves - no approximations
|
||||
let geometry_bytes = MIN_GEOMETRY_BYTES;
|
||||
let n = checked_count(db, total_curves, MAX_PART_ITEMS, geometry_bytes, OP_PART)?;
|
||||
let mut curves = Vec::new();
|
||||
for i in 0..n {
|
||||
before_item(db, i, OP_PART)?;
|
||||
let start = position(db);
|
||||
if ascii {
|
||||
line(db)?;
|
||||
line(db)?; // [Curve #i]
|
||||
}
|
||||
let mut attrs = AttributeSink::new(&mut *env.attributes, &mut all, env.next_key);
|
||||
let c = read_curve_from_db(k, 3, version, &mut attrs, db, true)?;
|
||||
env.next_key = attrs.next_key();
|
||||
curves.push(c);
|
||||
after_item(db, start, OP_PART)?;
|
||||
}
|
||||
|
||||
// stand-alone surfaces - no approximations
|
||||
let n = checked_count(db, total_surfaces, MAX_PART_ITEMS, geometry_bytes, OP_PART)?;
|
||||
let mut surfaces = Vec::new();
|
||||
for j in 0..n {
|
||||
before_item(db, j, OP_PART)?;
|
||||
let start = position(db);
|
||||
if ascii {
|
||||
line(db)?;
|
||||
line(db)?; // [Surface #j]
|
||||
}
|
||||
let mut attrs = AttributeSink::new(&mut *env.attributes, &mut all, env.next_key);
|
||||
let s = read_surface_from_db(k, version, &mut attrs, db, true)?;
|
||||
env.next_key = attrs.next_key();
|
||||
surfaces.push(s);
|
||||
after_item(db, start, OP_PART)?;
|
||||
}
|
||||
|
||||
// boolean trees
|
||||
let node_bytes = if ascii { 1 } else { 8 };
|
||||
let n = checked_count(db, total_tree_nodes, MAX_PART_ITEMS, node_bytes, OP_PART)?;
|
||||
if ascii {
|
||||
// Read Line With title
|
||||
line(db)?;
|
||||
line(db)?;
|
||||
}
|
||||
let mut boolean_tree_nodes = Vec::new();
|
||||
for m in 0..n {
|
||||
before_item(db, m, OP_PART)?;
|
||||
let start = position(db);
|
||||
let node = if ascii {
|
||||
scan_long(&line(db)?, b"").unwrap_or(0)
|
||||
} else {
|
||||
db.read_long()?
|
||||
};
|
||||
boolean_tree_nodes.push(node);
|
||||
after_item(db, start, OP_PART)?;
|
||||
}
|
||||
|
||||
// ASCII white space
|
||||
if ascii {
|
||||
line(db)?;
|
||||
line(db)?;
|
||||
}
|
||||
|
||||
// Brep Models
|
||||
let n = checked_count(db, total_breps, MAX_PART_BREPS, MIN_BREP_BYTES, OP_PART)?;
|
||||
let mut breps = Vec::new();
|
||||
let mut table = PlaceholderTable::default();
|
||||
for i in 0..n {
|
||||
before_item(db, i, OP_PART)?;
|
||||
let start = position(db);
|
||||
if ascii {
|
||||
line(db)?;
|
||||
line(db)?;
|
||||
line(db)?; // [Brep #k]
|
||||
}
|
||||
|
||||
// allocate an empty and initialized SmBrep object
|
||||
let mut b = Brep::new_with_infinite_region()?;
|
||||
|
||||
// read the BrepData
|
||||
let mut attrs = AttributeSink::new(&mut *env.attributes, &mut all, env.next_key);
|
||||
let mut data = BrepData::read_from_db(k, version, &mut attrs, db, true)?;
|
||||
env.next_key = attrs.next_key();
|
||||
|
||||
// Set pBrepData Healer Version
|
||||
data.set_healer_version(healer_version);
|
||||
|
||||
// Build Brep Model from BrepData
|
||||
// FALSE: do not force creation of uv trim curves.
|
||||
table.update(env.attributes, &all, &data)?;
|
||||
let key_of = env.key_of;
|
||||
let key = move |e: EntityRef| key_of(i, e);
|
||||
let mut topology_env = TopologyEnv {
|
||||
all_attributes: &table.handles,
|
||||
attributes: &mut *env.attributes,
|
||||
key_of: &key,
|
||||
records: &mut *env.records,
|
||||
next_key: env.next_key,
|
||||
};
|
||||
let options = MakeTopologyOptions {
|
||||
make_uv_trim_curves: false,
|
||||
..MakeTopologyOptions::default()
|
||||
};
|
||||
make_topology_from_data(k, &mut b, &mut data, &mut topology_env, options)?;
|
||||
env.next_key = topology_env.next_key;
|
||||
breps.push(b);
|
||||
after_item(db, start, OP_PART)?;
|
||||
}
|
||||
|
||||
// Now read in attributes - the placeholders collected in the table are replaced
|
||||
// by the real attributes, which are in the same order in the file.
|
||||
if version > 30 {
|
||||
let replaced = replace_attributes(db, env.attributes, &all);
|
||||
// SmObjsDelete<SmAttribute *> sCleanAttributes(&sAttributes)
|
||||
let cleaned = delete_placeholders(env.attributes, all.iter().flatten().copied());
|
||||
replaced?;
|
||||
cleaned?;
|
||||
}
|
||||
if let Some(h) = table.unused {
|
||||
delete_placeholders(env.attributes, [h])?;
|
||||
}
|
||||
|
||||
db.end_reading();
|
||||
|
||||
Ok(Part {
|
||||
curves,
|
||||
surfaces,
|
||||
boolean_tree_nodes,
|
||||
breps,
|
||||
})
|
||||
}
|
||||
|
||||
/// The placeholder table as `MakeTopologyFromData` takes it, kept in step with the
|
||||
/// slots: each `NULL` slot is `unused` (made once, referenced by no record).
|
||||
#[derive(Debug, Default)]
|
||||
struct PlaceholderTable {
|
||||
handles: Vec<AttributeHandle>,
|
||||
unused: Option<AttributeHandle>,
|
||||
}
|
||||
|
||||
impl PlaceholderTable {
|
||||
/// The handle standing for a `NULL` slot.
|
||||
fn unused(&mut self, store: &mut AttributeStore) -> Result<AttributeHandle> {
|
||||
if let Some(h) = self.unused {
|
||||
return Ok(h);
|
||||
}
|
||||
let h = store.insert(Attribute::new(ids::UNKNOWN, Behavior::StandaloneReference))?;
|
||||
self.unused = Some(h);
|
||||
Ok(h)
|
||||
}
|
||||
|
||||
/// Brings the table up to date after B-rep `data` was read into `all`: appends the
|
||||
/// new slots, and updates the `NULL` slots the B-rep's records filled (a slot is
|
||||
/// filled only when an index naming it is read, so those records list it).
|
||||
fn update(
|
||||
&mut self,
|
||||
store: &mut AttributeStore,
|
||||
all: &[Option<AttributeHandle>],
|
||||
data: &BrepData,
|
||||
) -> Result<()> {
|
||||
// slots filled below the old end: only indices this B-rep's records name
|
||||
let old_len = self.handles.len();
|
||||
if self.unused.is_some() {
|
||||
for &i in record_indices(data) {
|
||||
let Ok(i) = usize::try_from(i) else {
|
||||
continue;
|
||||
};
|
||||
if i >= old_len {
|
||||
continue;
|
||||
}
|
||||
if let (Some(slot), Some(Some(h))) = (self.handles.get_mut(i), all.get(i)) {
|
||||
*slot = *h;
|
||||
}
|
||||
}
|
||||
}
|
||||
// new slots
|
||||
for slot in all.iter().skip(old_len) {
|
||||
let h = match slot {
|
||||
Some(h) => *h,
|
||||
None => self.unused(store)?,
|
||||
};
|
||||
self.handles.push(h);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The attribute indices of one record.
|
||||
fn topo(t: &TopologyData) -> core::slice::Iter<'_, u64> {
|
||||
t.attributes.iter()
|
||||
}
|
||||
|
||||
/// Every attribute index the records of `data` list (the B-rep's own included).
|
||||
fn record_indices(data: &BrepData) -> impl Iterator<Item = &u64> {
|
||||
data.attributes
|
||||
.iter()
|
||||
.chain(data.regions.iter().flat_map(|r| topo(&r.topology)))
|
||||
.chain(data.shells.iter().flat_map(|s| topo(&s.topology)))
|
||||
.chain(data.faceuses.iter().flat_map(|f| topo(&f.topology)))
|
||||
.chain(data.faces.iter().flat_map(|f| topo(&f.topology)))
|
||||
.chain(data.loops.iter().flat_map(|l| {
|
||||
topo(&l.topology)
|
||||
.chain(l.lu_upper_attributes.iter())
|
||||
.chain(l.lu_lower_attributes.iter())
|
||||
}))
|
||||
.chain(
|
||||
data.edgeuses
|
||||
.iter()
|
||||
.flat_map(|e| topo(&e.topology).chain(e.mate_attributes.iter())),
|
||||
)
|
||||
.chain(data.edges.iter().flat_map(|e| topo(&e.topology)))
|
||||
.chain(data.vertices.iter().flat_map(|v| topo(&v.topology)))
|
||||
}
|
||||
|
||||
/// `delete` of each placeholder still alive.
|
||||
fn delete_placeholders(
|
||||
store: &mut AttributeStore,
|
||||
handles: impl IntoIterator<Item = AttributeHandle>,
|
||||
) -> Result<()> {
|
||||
for h in handles {
|
||||
if store.is_live(h) {
|
||||
store.destroy(h)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The attribute section of [`read_part_from_db`]: the attribute count, then each
|
||||
/// attribute, which replaces the placeholder of the same index on every object using
|
||||
/// it (an attribute without users is dropped).
|
||||
fn replace_attributes(
|
||||
db: &mut DatabaseReader,
|
||||
store: &mut AttributeStore,
|
||||
all: &[Option<AttributeHandle>],
|
||||
) -> Result<()> {
|
||||
let ascii = db.file_type() == FileType::Ascii;
|
||||
|
||||
// read total attribute count (uninitialised upstream when nothing is stored)
|
||||
let mut total: u64 = 0;
|
||||
if ascii {
|
||||
line(db)?;
|
||||
if let Some(v) = scan_long(&line(db)?, b"") {
|
||||
total = v as u64;
|
||||
}
|
||||
} else {
|
||||
total = db.read_ulong()?;
|
||||
}
|
||||
|
||||
// read each attribute (bounded as the other counted sections)
|
||||
let min_bytes = if ascii {
|
||||
MIN_ATTRIBUTE_BYTES_ASCII
|
||||
} else {
|
||||
MIN_ATTRIBUTE_BYTES_BINARY
|
||||
};
|
||||
let total = checked_count(db, total, MAX_PART_ITEMS, min_bytes, OP_PART)?;
|
||||
for j in 0..total {
|
||||
before_item(db, j, OP_PART)?;
|
||||
let start = position(db);
|
||||
if ascii {
|
||||
// clear ascii white space
|
||||
line(db)?;
|
||||
line(db)?;
|
||||
}
|
||||
|
||||
// read the attribute
|
||||
let attribute = read_attribute_from_db(db)?;
|
||||
after_item(db, start, OP_PART)?;
|
||||
|
||||
// Replace and update attribute in sAttributes array
|
||||
let slot = all.get(j).ok_or(SolidError::InvalidInput {
|
||||
operation: OP_PART,
|
||||
reason: "attribute index past the attribute table",
|
||||
})?;
|
||||
let temp = slot.ok_or(SolidError::InvalidInput {
|
||||
operation: OP_PART,
|
||||
reason: "attribute table slot is NULL",
|
||||
})?;
|
||||
|
||||
// get every object using this attribute
|
||||
let users = store.users(temp)?;
|
||||
|
||||
// User may have been deleted by heal sequence in MakeTopologyFromData
|
||||
if users.is_empty() {
|
||||
continue; // delete pAttribute
|
||||
}
|
||||
let new = store.insert(attribute)?;
|
||||
// for every attribute user: substitute attribute
|
||||
for user in users {
|
||||
store.remove_attribute(user, temp, false)?;
|
||||
store.add_attribute(user, new)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `SmBrepData::ReadAttributeFromDB`: reads one attribute (id, behaviour and its
|
||||
/// longs, doubles and characters) and makes it with [`create_attribute`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// A short binary stream, a missing stream, a count exceeding the remaining input;
|
||||
/// those of [`create_attribute`].
|
||||
pub fn read_attribute_from_db(db: &mut DatabaseReader) -> Result<Attribute> {
|
||||
// uninitialised upstream when nothing is stored
|
||||
let mut id: u64 = 0;
|
||||
let mut behavior: i64 = 0;
|
||||
let mut num_long: u64 = 0;
|
||||
let mut num_double: u64 = 0;
|
||||
let mut num_char: u64 = 0;
|
||||
let mut longs = Vec::new();
|
||||
let mut doubles = Vec::new();
|
||||
let mut chars = Vec::new();
|
||||
|
||||
if db.file_type() == FileType::Ascii {
|
||||
let s = stream(db)?;
|
||||
if let Some(v) = s.extract_ulong() {
|
||||
id = v;
|
||||
}
|
||||
if let Some(v) = s.extract_long() {
|
||||
behavior = v;
|
||||
}
|
||||
if let Some(v) = s.extract_ulong() {
|
||||
num_long = v;
|
||||
}
|
||||
if let Some(v) = s.extract_ulong() {
|
||||
num_double = v;
|
||||
}
|
||||
if let Some(v) = s.extract_ulong() {
|
||||
num_char = v;
|
||||
}
|
||||
db.go_to_next_line();
|
||||
|
||||
// read the longs ("<n> " each: at least two bytes)
|
||||
let n = checked_count(db, num_long, MAX_ATTRIBUTE_VALUES, 2, OP_ATTRIBUTE)?;
|
||||
for i in 0..n {
|
||||
before_item(db, i, OP_ATTRIBUTE)?;
|
||||
let start = position(db);
|
||||
longs.push(stream(db)?.extract_long().unwrap_or(0));
|
||||
after_item(db, start, OP_ATTRIBUTE)?;
|
||||
}
|
||||
if num_long > 0 {
|
||||
db.go_to_next_line();
|
||||
}
|
||||
// read the doubles
|
||||
let n = checked_count(db, num_double, MAX_ATTRIBUTE_VALUES, 2, OP_ATTRIBUTE)?;
|
||||
for i in 0..n {
|
||||
before_item(db, i, OP_ATTRIBUTE)?;
|
||||
let start = position(db);
|
||||
doubles.push(stream(db)?.extract_double().unwrap_or(0.0));
|
||||
after_item(db, start, OP_ATTRIBUTE)?;
|
||||
}
|
||||
if num_double > 0 {
|
||||
db.go_to_next_line();
|
||||
}
|
||||
// read the characters
|
||||
let n = checked_count(db, num_char, MAX_ATTRIBUTE_VALUES, 1, OP_ATTRIBUTE)?;
|
||||
for i in 0..n {
|
||||
before_item(db, i, OP_ATTRIBUTE)?;
|
||||
let start = position(db);
|
||||
chars.push(stream(db)?.get().unwrap_or(b'0'));
|
||||
after_item(db, start, OP_ATTRIBUTE)?;
|
||||
}
|
||||
if num_char > 0 {
|
||||
db.go_to_next_line();
|
||||
}
|
||||
} else {
|
||||
id = db.read_ulong()?;
|
||||
behavior = db.read_long()?;
|
||||
num_long = db.read_ulong()?;
|
||||
num_double = db.read_ulong()?;
|
||||
num_char = db.read_ulong()?;
|
||||
|
||||
if num_long > 0 {
|
||||
let n = checked_count(db, num_long, MAX_ATTRIBUTE_VALUES, 8, OP_ATTRIBUTE)?;
|
||||
longs = db.read_longs(n)?.into_iter().map(|v| v as i64).collect();
|
||||
}
|
||||
if num_double > 0 {
|
||||
let n = checked_count(db, num_double, MAX_ATTRIBUTE_VALUES, 8, OP_ATTRIBUTE)?;
|
||||
doubles = db.read_doubles(n)?;
|
||||
}
|
||||
if num_char > 0 {
|
||||
let n = checked_count(db, num_char, MAX_ATTRIBUTE_VALUES, 1, OP_ATTRIBUTE)?;
|
||||
chars = db.read_characters(n)?;
|
||||
}
|
||||
}
|
||||
|
||||
create_attribute(id, behavior, &longs, &doubles, &chars)
|
||||
}
|
||||
|
||||
/// `SmAttribute::CreateAttribute`: a colour id ([`ids::COLOR`]) makes a vector
|
||||
/// attribute from the first three doubles, a tag id ([`ids::TAG`]) a tag attribute
|
||||
/// from the first eight longs, any other id a generic attribute holding the values.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::InvalidInput`] for a colour with fewer than 3 doubles, a tag with
|
||||
/// fewer than 8 longs or a behaviour that is not an `SmAttributeBehaviorType` value.
|
||||
pub fn create_attribute(
|
||||
attribute_id: u64,
|
||||
behavior: i64,
|
||||
longs: &[i64],
|
||||
doubles: &[f64],
|
||||
chars: &[u8],
|
||||
) -> Result<Attribute> {
|
||||
let behavior = i32::try_from(behavior)
|
||||
.map_err(|_| SolidError::invalid_input(OP_CREATE, "behaviour out of range"))
|
||||
.and_then(Behavior::from_code)?;
|
||||
match attribute_id {
|
||||
ids::COLOR => match doubles {
|
||||
[x, y, z, ..] => Ok(Attribute::vector3d(
|
||||
attribute_id,
|
||||
Vec3::new(*x, *y, *z),
|
||||
behavior,
|
||||
)),
|
||||
_ => Err(SolidError::invalid_input(
|
||||
OP_CREATE,
|
||||
"colour attribute without 3 doubles",
|
||||
)),
|
||||
},
|
||||
ids::TAG => match longs {
|
||||
[a, b, c, d, e, f, g, h, ..] => {
|
||||
let tag = TagData {
|
||||
primary_id: *a,
|
||||
primary_ordinal: *b,
|
||||
secondary_id_type: *c,
|
||||
secondary_id: *d,
|
||||
secondary_ordinal: *e,
|
||||
third_id_type: *f,
|
||||
third_id: *g,
|
||||
third_ordinal: *h,
|
||||
};
|
||||
Ok(Attribute::tag(attribute_id, tag, behavior))
|
||||
}
|
||||
_ => Err(SolidError::invalid_input(
|
||||
OP_CREATE,
|
||||
"tag attribute without 8 longs",
|
||||
)),
|
||||
},
|
||||
// Just create a generic attribute
|
||||
_ => Ok(Attribute::generic(
|
||||
attribute_id,
|
||||
behavior,
|
||||
longs,
|
||||
doubles,
|
||||
chars,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn scan(line: &str, prefix: &str) -> Option<i64> {
|
||||
scan_long(line.as_bytes(), prefix.as_bytes())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_long_follows_sscanf() {
|
||||
let p1 = "// (1) ";
|
||||
assert_eq!(scan("// (1) 32000000 Curves", p1), Some(32_000_000));
|
||||
assert_eq!(scan("// (1) -7 x", p1), Some(-7));
|
||||
assert_eq!(scan("// (2) 5", p1), None);
|
||||
assert_eq!(scan(" 12 // Total", ""), Some(12));
|
||||
assert_eq!(scan("x", ""), None);
|
||||
assert_eq!(scan("", ""), None);
|
||||
assert_eq!(scan("99999999999999999999", ""), Some(i64::MAX));
|
||||
assert_eq!(scan("-99999999999999999999", ""), Some(i64::MIN));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getline_follows_libstdcxx() {
|
||||
let mut s = InputStream::new(b"ab\n\ncd".to_vec());
|
||||
assert_eq!(s.getline(MAXSIZE), b"ab");
|
||||
assert!(s.good());
|
||||
assert_eq!(s.getline(MAXSIZE), b"");
|
||||
assert!(s.good());
|
||||
assert_eq!(s.getline(MAXSIZE), b"cd");
|
||||
assert!(s.eof());
|
||||
assert!(!s.fail());
|
||||
assert_eq!(s.getline(MAXSIZE), b"");
|
||||
assert!(s.fail());
|
||||
|
||||
// a full buffer before the newline fails and leaves the rest
|
||||
let mut s = InputStream::new(b"abcdef\n".to_vec());
|
||||
assert_eq!(s.getline(4), b"abc");
|
||||
assert!(s.fail());
|
||||
// exactly n - 1 bytes then the newline is fine
|
||||
let mut s = InputStream::new(b"abc\nx".to_vec());
|
||||
assert_eq!(s.getline(4), b"abc");
|
||||
assert!(s.good());
|
||||
}
|
||||
|
||||
fn key_of(_: usize, _: EntityRef) -> ObjectRef {
|
||||
ObjectRef {
|
||||
type_code: 0,
|
||||
key: ObjectKey(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn read(text: &str) -> Result<Part> {
|
||||
let mut k = Kernel::default();
|
||||
let mut store = AttributeStore::new();
|
||||
let mut records = BTreeMap::new();
|
||||
let mut env = PartEnv {
|
||||
attributes: &mut store,
|
||||
key_of: &key_of,
|
||||
records: &mut records,
|
||||
next_key: 1 << 50,
|
||||
};
|
||||
let data = text.as_bytes().to_vec();
|
||||
read_part_from_file(&mut k, data, FileType::Ascii, &mut env)
|
||||
}
|
||||
|
||||
const HEAD: &str = "//[Output Summary]\n// (1) 32000000 Curves\n// (2) 0 Surfaces\n";
|
||||
|
||||
#[test]
|
||||
fn empty_part_reads() -> Result<()> {
|
||||
let text = format!(
|
||||
"{HEAD}// (3) 2 Boolean Tree Nodes\n// (4) 0 Breps\n\n// Boolean Trees\n\
|
||||
7\n-3\n\n\n\n0 // Total number of attributes\n"
|
||||
);
|
||||
let part = read(&text)?;
|
||||
assert!(part.curves.is_empty());
|
||||
assert!(part.surfaces.is_empty());
|
||||
assert!(part.breps.is_empty());
|
||||
assert_eq!(part.boolean_tree_nodes, vec![7, -3]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counts_beyond_the_input_are_rejected() {
|
||||
let text = "//[Output Summary]\n// (1) 32999999 Curves\n";
|
||||
assert!(matches!(read(text), Err(SolidError::InvalidInput { .. })));
|
||||
let text = format!("{HEAD}// (3) 0 Boolean Tree Nodes\n// (4) 9000000000 Breps\n");
|
||||
assert!(matches!(read(&text), Err(SolidError::InvalidInput { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counted_loops_fail_closed() {
|
||||
// four tree nodes declared, the stream ends after two
|
||||
let text = format!(
|
||||
"{HEAD}// (3) 4 Boolean Tree Nodes\n// (4) 0 Breps\n\n// Boolean Trees\n7\n-3"
|
||||
);
|
||||
assert!(matches!(read(&text), Err(SolidError::InvalidInput { .. })));
|
||||
// more B-reps than the cap
|
||||
let text = format!("{HEAD}// (3) 0 Boolean Tree Nodes\n// (4) 100001 Breps\n");
|
||||
assert!(matches!(read(&text), Err(SolidError::InvalidInput { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attribute_past_the_table_is_rejected() {
|
||||
let text = format!(
|
||||
"{HEAD}// (3) 0 Boolean Tree Nodes\n// (4) 0 Breps\n\n// Boolean Trees\n\n\n\n\
|
||||
1 // Total number of attributes\n\nAttribute #0\n100 0 0 3 0\n1 2 3\n"
|
||||
);
|
||||
assert!(matches!(read(&text), Err(SolidError::InvalidInput { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_attribute_by_id() -> Result<()> {
|
||||
let rgb = [0.5, 0.25, 1.0];
|
||||
let c = create_attribute(ids::COLOR, 0, &[], &rgb, &[])?;
|
||||
assert_eq!(c.vector3d_value(), Some(Vec3::new(0.5, 0.25, 1.0)));
|
||||
assert!(create_attribute(ids::COLOR, 0, &[], &[0.5], &[]).is_err());
|
||||
let tag = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
let t = create_attribute(ids::TAG, 1, &tag, &[], &[])?;
|
||||
assert_eq!(t.behavior(), Behavior::Reference);
|
||||
assert!(create_attribute(ids::TAG, 0, &[1, 2], &[], &[]).is_err());
|
||||
let g = create_attribute(5000, 3, &[9], &[1.5], b"ab")?;
|
||||
assert_eq!(g.attribute_id(), 5000);
|
||||
assert!(create_attribute(5000, 9, &[], &[], &[]).is_err());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// difftest shim for unit `corpus.parity`: reads one upstream test model with
|
||||
// SmBrep::ReadFromFile (a B-rep file) or SmBrepData::ReadPartFromFile (a part file: one
|
||||
// that starts with `//[Output Summary]`) and measures every B-rep as the Rust side does
|
||||
// (crates/difftest/src/units/corpus_parity.rs, after crates/solidcore/examples/corpus):
|
||||
// the kind (IsManifoldSolid, IsSheet), the face, edge, vertex and loop counts, the area
|
||||
// (SmApiFaceComputeArea's per-face integration without its accuracy clamp) and, for a
|
||||
// solid, the volume (SmApiBrepComputeVolume). `sc_corpus_parity_roundtrip` measures each
|
||||
// B-rep after an ASCII write (SmBrep::WriteToFile) and read back instead.
|
||||
//
|
||||
// The objects read are leaked on purpose: the runner calls this in a child process that
|
||||
// exits right after (a crash or hang on one model must not stop the run).
|
||||
#include "sc_shim.h"
|
||||
#include "StdAfx.h"
|
||||
#include <SmApiQueries.h>
|
||||
#include <SmBrep.h>
|
||||
#include <SmBrepData.h>
|
||||
#include <SmContext.h>
|
||||
#include <SmCurve.h>
|
||||
#include <SmEdge.h>
|
||||
#include <SmExtent3d.h>
|
||||
#include <SmFace.h>
|
||||
#include <SmLoop.h>
|
||||
#include <SmSurface.h>
|
||||
#include <SmVertex.h>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
const SmContext& the_context()
|
||||
{
|
||||
static SmContext* p = new SmContext();
|
||||
return *p;
|
||||
}
|
||||
|
||||
// Whether the file starts with the part file header (as the Rust side tests it).
|
||||
bool is_part_file(const char* path)
|
||||
{
|
||||
static const char header[] = "//[Output Summary]";
|
||||
const size_t n = sizeof(header) - 1;
|
||||
char buf[sizeof(header)] = {0};
|
||||
FILE* f = std::fopen(path, "rb");
|
||||
if (f == nullptr)
|
||||
return false;
|
||||
size_t got = std::fread(buf, 1, n, f);
|
||||
std::fclose(f);
|
||||
return got == n && std::memcmp(buf, header, n) == 0;
|
||||
}
|
||||
|
||||
// SmApiFaceComputeArea without its [1e-4, 1e-1] accuracy clamp: face box midpoint as
|
||||
// origin, area estimate (max box side)^2 / 100, area only, made positive.
|
||||
SmStatus face_area(const SmFace* face, double accuracy, double& area)
|
||||
{
|
||||
SmExtent3d box;
|
||||
SmStatus st = face->CalculateBoundingBox(box);
|
||||
if (st != SM_SUCCESS)
|
||||
return st;
|
||||
SmVector3d size = box.GetSize();
|
||||
double d = smos_Max(size.z, smos_Max(size.x, size.y));
|
||||
double dv = 0.0;
|
||||
SmTArray<SmVector3d> moments;
|
||||
st = face->ComputePreciseProperties(SM_OT_SAME, accuracy, box.GetMid(), d * d / 100.0, 0.0,
|
||||
area, dv, moments, SM_PPF_AREA);
|
||||
if (area < 0.0)
|
||||
area = -area;
|
||||
return st;
|
||||
}
|
||||
|
||||
// One B-rep: kind (0 empty, 1 solid, 2 sheet, 3 other), faces, edges, vertices, loops,
|
||||
// measure status (0 or the first failing SmStatus), area, volume (NaN unless a solid).
|
||||
void measure(SmBrep* b, double accuracy, std::vector<double>& o)
|
||||
{
|
||||
SmTArray<SmFace*> faces;
|
||||
SmTArray<SmEdge*> edges;
|
||||
SmTArray<SmVertex*> vertices;
|
||||
b->GetFaces(faces);
|
||||
b->GetEdges(edges);
|
||||
b->GetVertices(vertices);
|
||||
ULONG loops = 0;
|
||||
for (ULONG i = 0; i < faces.GetSize(); i++)
|
||||
{
|
||||
SmTArray<SmLoop*> l;
|
||||
faces[i]->GetLoops(l);
|
||||
loops += l.GetSize();
|
||||
}
|
||||
int kind = 3;
|
||||
if (faces.GetSize() == 0 && edges.GetSize() == 0 && vertices.GetSize() == 0)
|
||||
kind = 0;
|
||||
else if (b->IsManifoldSolid())
|
||||
kind = 1;
|
||||
else if (b->IsSheet())
|
||||
kind = 2;
|
||||
|
||||
SmStatus status = SM_SUCCESS;
|
||||
double area = 0.0;
|
||||
for (ULONG i = 0; i < faces.GetSize() && status == SM_SUCCESS; i++)
|
||||
{
|
||||
double a = 0.0;
|
||||
status = face_area(faces[i], accuracy, a);
|
||||
area += a;
|
||||
}
|
||||
double volume = std::numeric_limits<double>::quiet_NaN();
|
||||
if (status == SM_SUCCESS && kind == 1)
|
||||
{
|
||||
double v = 0.0;
|
||||
SmApiStatus vs = SmApiBrepComputeVolume(b, accuracy, v);
|
||||
if (vs == SM_SUCCESS)
|
||||
volume = v;
|
||||
else
|
||||
status = (SmStatus)vs;
|
||||
}
|
||||
o.insert(o.end(), {(double)kind, (double)faces.GetSize(), (double)edges.GetSize(),
|
||||
(double)vertices.GetSize(), (double)loops,
|
||||
status == SM_SUCCESS ? 0.0 : (double)status, area, volume});
|
||||
}
|
||||
|
||||
// The B-reps of model file `path` (one from a B-rep file, any number from a part file);
|
||||
// the reader's status.
|
||||
SmStatus read_model(const SmContext& ctx, const char* path, std::vector<SmBrep*>& breps)
|
||||
{
|
||||
SmStatus st;
|
||||
if (is_part_file(path))
|
||||
{
|
||||
SmTArray<SmCurve*> curves;
|
||||
SmTArray<SmSurface*> surfaces;
|
||||
SmTArray<long> trees;
|
||||
SmTArray<SmBrep*> read;
|
||||
st = SmBrepData::ReadPartFromFile(ctx, path, curves, surfaces, trees, read, SM_ASCII);
|
||||
for (ULONG i = 0; i < read.GetSize(); i++)
|
||||
if (read[i] != nullptr)
|
||||
breps.push_back(read[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
SmBrep* b = new (ctx) SmBrep();
|
||||
st = b->ReadFromFile(ctx, path, SM_ASCII);
|
||||
breps.push_back(b);
|
||||
}
|
||||
return st;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Output: `read_status n` then 8 values per B-rep (see `measure`); `read_status` is 0 or
|
||||
// the reader's SmStatus (then `n` is 0).
|
||||
SC_EXPORT int32_t sc_corpus_parity_measure(const char* path, double accuracy, double* out,
|
||||
int64_t cap, int64_t* out_len)
|
||||
{
|
||||
if (path == nullptr || !(accuracy > 0.0) || !std::isfinite(accuracy))
|
||||
return SC_ERR_ARG;
|
||||
SC_TRY
|
||||
std::vector<SmBrep*> breps;
|
||||
SmStatus st = read_model(the_context(), path, breps);
|
||||
std::vector<double> o;
|
||||
if (st != SM_SUCCESS)
|
||||
{
|
||||
o.push_back((double)st);
|
||||
o.push_back(0.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
o.push_back(0.0);
|
||||
o.push_back((double)breps.size());
|
||||
for (SmBrep* b : breps)
|
||||
measure(b, accuracy, o);
|
||||
}
|
||||
return sc_copy_out(o.data(), (int64_t)o.size(), out, cap, out_len);
|
||||
SC_CATCH
|
||||
}
|
||||
|
||||
// The round trip of every B-rep of model file `path`: `SmBrep::WriteToFile` (SM_ASCII,
|
||||
// native geometry; `SmBrepData::FromBrep` then `WriteToFile`) to `tmp`, a new B-rep
|
||||
// read back from it with `ReadFromFile`, measured as `sc_corpus_parity_measure` does.
|
||||
// Output as `sc_corpus_parity_measure`'s, with the read-back B-reps' records; a B-rep
|
||||
// whose write or read back fails has the failing SmStatus as its record's status.
|
||||
SC_EXPORT int32_t sc_corpus_parity_roundtrip(const char* path, const char* tmp,
|
||||
double accuracy, double* out, int64_t cap,
|
||||
int64_t* out_len)
|
||||
{
|
||||
if (path == nullptr || tmp == nullptr || !(accuracy > 0.0) || !std::isfinite(accuracy))
|
||||
return SC_ERR_ARG;
|
||||
SC_TRY
|
||||
const SmContext& ctx = the_context();
|
||||
std::vector<SmBrep*> breps;
|
||||
SmStatus st = read_model(ctx, path, breps);
|
||||
std::vector<double> o;
|
||||
if (st != SM_SUCCESS)
|
||||
{
|
||||
o.push_back((double)st);
|
||||
o.push_back(0.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
o.push_back(0.0);
|
||||
o.push_back((double)breps.size());
|
||||
const double nan = std::numeric_limits<double>::quiet_NaN();
|
||||
for (SmBrep* b : breps)
|
||||
{
|
||||
SmStatus ws = b->WriteToFile(tmp, SM_ASCII, TRUE, FALSE, 0.0);
|
||||
SmBrep* back = new (ctx) SmBrep();
|
||||
SmStatus rs = ws == SM_SUCCESS ? back->ReadFromFile(ctx, tmp, SM_ASCII) : ws;
|
||||
if (rs == SM_SUCCESS)
|
||||
measure(back, accuracy, o);
|
||||
else
|
||||
o.insert(o.end(), {0.0, 0.0, 0.0, 0.0, 0.0, (double)rs, 0.0, nan});
|
||||
}
|
||||
std::remove(tmp);
|
||||
}
|
||||
return sc_copy_out(o.data(), (int64_t)o.size(), out, cap, out_len);
|
||||
SC_CATCH
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// difftest shim for the part reader (`topology::part_file`, outside the plan):
|
||||
// SmBrepData::ReadPartFromFile (source/SMLib/src/SmBrepData.cpp) on an upstream test file.
|
||||
//
|
||||
// sc_topology_part_file_run reads the part file at `path` and writes a flat list of
|
||||
// doubles (layout in crates/solidcore-ffi/src/units/topology_part_file.rs): the status,
|
||||
// then per stand-alone curve its type, dimension and natural interval, per surface its
|
||||
// type and natural UV domain, the Boolean tree nodes, and per B-rep its tolerance,
|
||||
// manifold-solid flag and region, shell, face, loop, edge and vertex counts.
|
||||
#include "sc_shim.h"
|
||||
|
||||
#include "StdAfx.h"
|
||||
#include <SmBrep.h>
|
||||
#include <SmBrepData.h>
|
||||
#include <SmContext.h>
|
||||
#include <SmCurve.h>
|
||||
#include <SmExtent1d.h>
|
||||
#include <SmExtent2d.h>
|
||||
#include <SmFace.h>
|
||||
#include <SmSurface.h>
|
||||
#include <SmTArray.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
SC_EXPORT int32_t sc_topology_part_file_run(const char* path, int32_t file_type, double* out,
|
||||
int64_t cap, int64_t* out_len)
|
||||
{
|
||||
if (path == NULL || file_type < 0 || file_type > 1)
|
||||
return SC_ERR_ARG;
|
||||
SC_TRY
|
||||
// one context for the process, never destroyed, as the corpus.parity shim's (whose
|
||||
// reads of the same part files succeed)
|
||||
static SmContext* context = new SmContext();
|
||||
const SmContext& ctx = *context;
|
||||
SmTArray<SmCurve*> curves;
|
||||
SmTArray<SmSurface*> surfaces;
|
||||
SmTArray<long> trees;
|
||||
SmTArray<SmBrep*> breps;
|
||||
SmStatus st = SmBrepData::ReadPartFromFile(ctx, path, curves, surfaces, trees, breps,
|
||||
file_type == 0 ? SM_ASCII : SM_BINARY);
|
||||
std::vector<double> o;
|
||||
// status: 0 for SM_SUCCESS (1000L), else the SmStatus (the layout of the FFI docs)
|
||||
o.push_back(st == SM_SUCCESS ? 0.0 : (double)st);
|
||||
if (st == SM_SUCCESS)
|
||||
{
|
||||
o.push_back((double)curves.GetSize());
|
||||
for (ULONG i = 0; i < curves.GetSize(); i++)
|
||||
{
|
||||
SmCurve* c = curves[i];
|
||||
SmExtent1d ivl = c->GetNaturalInterval();
|
||||
o.push_back((double)c->GetType());
|
||||
o.push_back((double)c->GetDim());
|
||||
o.push_back(ivl.GetMin());
|
||||
o.push_back(ivl.GetMax());
|
||||
}
|
||||
o.push_back((double)surfaces.GetSize());
|
||||
for (ULONG i = 0; i < surfaces.GetSize(); i++)
|
||||
{
|
||||
SmSurface* s = surfaces[i];
|
||||
SmExtent2d d = s->GetNaturalUVDomain();
|
||||
o.push_back((double)s->GetType());
|
||||
o.push_back(d.GetMin().x);
|
||||
o.push_back(d.GetMin().y);
|
||||
o.push_back(d.GetMax().x);
|
||||
o.push_back(d.GetMax().y);
|
||||
}
|
||||
o.push_back((double)trees.GetSize());
|
||||
for (ULONG i = 0; i < trees.GetSize(); i++)
|
||||
o.push_back((double)trees[i]);
|
||||
o.push_back((double)breps.GetSize());
|
||||
for (ULONG i = 0; i < breps.GetSize(); i++)
|
||||
{
|
||||
SmBrep* b = breps[i];
|
||||
SmTArray<SmRegion*> regions;
|
||||
SmTArray<SmShell*> shells;
|
||||
SmTArray<SmFace*> faces;
|
||||
SmTArray<SmEdge*> edges;
|
||||
SmTArray<SmVertex*> vertices;
|
||||
b->GetRegions(regions);
|
||||
b->GetShells(shells);
|
||||
b->GetFaces(faces);
|
||||
b->GetEdges(edges);
|
||||
b->GetVertices(vertices);
|
||||
ULONG loops = 0;
|
||||
for (ULONG f = 0; f < faces.GetSize(); f++)
|
||||
{
|
||||
SmTArray<SmLoop*> l;
|
||||
faces[f]->GetLoops(l);
|
||||
loops += l.GetSize();
|
||||
}
|
||||
o.push_back((double)b->GetTolerance());
|
||||
o.push_back(b->IsManifoldSolid() ? 1.0 : 0.0);
|
||||
o.push_back((double)regions.GetSize());
|
||||
o.push_back((double)shells.GetSize());
|
||||
o.push_back((double)faces.GetSize());
|
||||
o.push_back((double)loops);
|
||||
o.push_back((double)edges.GetSize());
|
||||
o.push_back((double)vertices.GetSize());
|
||||
}
|
||||
}
|
||||
// the part's objects are the caller's: release them
|
||||
for (ULONG i = 0; i < curves.GetSize(); i++)
|
||||
delete curves[i];
|
||||
for (ULONG i = 0; i < surfaces.GetSize(); i++)
|
||||
delete surfaces[i];
|
||||
for (ULONG i = 0; i < breps.GetSize(); i++)
|
||||
delete breps[i];
|
||||
return sc_copy_out(o.data(), (int64_t)o.size(), out, cap, out_len);
|
||||
SC_CATCH
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# corpus.parity: the ASCII round trip's volume drift is upstream's own (2026-10-23)
|
||||
|
||||
|
||||
- **What.** Nine corpus models (Solids/sphere, sphere, ssi_coincident_spheres,
|
||||
ssi_sph_plane1, blade_hub_1/2/3, cliff_1/2) read back after a write (`SmBrepData::FromBrep`,
|
||||
`WriteToFile` ASCII, `ReadFromFile`) with a volume 1.2e-5 to 7e-5 relative away from the
|
||||
read one, area nearly unchanged; the corpus example's round trip reports them.
|
||||
- **Evidence (Solids/sphere.smb).** C++ wrote the B-rep its own `ReadFromFile` made
|
||||
(`SmBrep::WriteToFile(path, SM_ASCII, TRUE, FALSE, 0.0)`, called in gdb in the
|
||||
corpus.parity C++ child); the port's bytes came from the corpus example's
|
||||
`BrepData::write_to_file`. The two files are byte-identical (13 571 bytes). Read back,
|
||||
C++ measures volume 523.5923997299045 and area 314.160106233197 (read: 523.5990772283121
|
||||
and 314.15938308003854); the port's own round trip and the port reading the C++ file
|
||||
give the same values bit for bit.
|
||||
- **Cause.** Upstream's writer prints doubles in fixed format (`%.16f`), which drops
|
||||
significant digits of small coordinates and weights; the read-back geometry differs
|
||||
slightly and so does the volume integral. The port writes the same bytes.
|
||||
- **Now.** corpus.parity round-trips every file whose read passes on both sides
|
||||
(`sc_corpus_parity_roundtrip`, children `cpp-roundtrip` / `rust-roundtrip`) and compares
|
||||
the read-back measures at `10 * ACCURACY`: the drift is expected, a difference between
|
||||
the sides is a regression.
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# topology.part_file: SmBrepData part reader and the B-rep writer were outside the plan (2026-10-23)
|
||||
|
||||
## General: `SmBrepData` part reader and the B-rep writer are missing from the plan (2026-10-23)
|
||||
|
||||
`harness/callgraph/units.json` lists only the single-B-rep read side of `SmBrepData`
|
||||
(`ReadFromFile`, `ReadFromDB`, `ReadCurveFromDB`, `ReadSurfaceFromDB`, `Get/SetHealerVersion`).
|
||||
Nothing reaches the part functions or the write side from the plan's test entry points,
|
||||
so `ReadPartFromFile` / `ReadPartFromDB` / `ReadAttributeFromDB` (and
|
||||
`SmAttribute::CreateAttribute`) were never units. Neither were `WriteToFile` / `WriteToDB`
|
||||
/ `WritePartToFile` / `WritePartToDB` / `WriteCurveToDB` / `WriteSurfaceToDB` /
|
||||
`WriteAttributeToDB`, the `SmDatabaseIO` `Write*` side or `FromBrep`. "Every unit green or
|
||||
partial" therefore does not mean the port can read upstream's 74 `.smp` test parts (plus
|
||||
`SmallBox.smb` and `Platform.smb`, which are part files too) or write any B-rep back.
|
||||
`topology::part_file` (fix/part-reader) ports the read side outside the plan:
|
||||
`read_part_from_file`, `read_part_from_db`, `read_attribute_from_db` and `create_attribute`,
|
||||
plus `InputStream::getline` and `DatabaseReader::read_characters`. The difftest unit `topology.part_file` (shim `topology_part_file.cpp`) compares it with
|
||||
upstream's `ReadPartFromFile`; the corpus runner (`examples/corpus`) exercises them on the real
|
||||
files. The writer (`FromBrep` plus the `Write*` functions) is `topology::brep_data_write` (fix/brep-writer, salvaged onto swarm/b1 2026-10-25).
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
#### WR-449. `Fillet/sphere_diff_3cylinders.smb`: one of six symmetric fillet faces integrates 0.6 too much area and 2.1 too much volume
|
||||
- **Severity:** Wrong result
|
||||
- **Area:** Mass properties
|
||||
- **Location:** the trim of edge 30 that `SmFace::CreateOrientedTrimBoundary` makes for face 7 when `SmFace::ComputePreciseProperties` (`source/SMLib/src/SmFace.cpp`) integrates it: `SmFace::CreateUVTrimCurves` -> `SmEdgeuse::GetOrCreateUVTrimCurve` -> `SmEdgeuse::CreateUVTrimCurve` (`source/SMLib/src/SmEdgeuse.cpp`, `DropCurve` / `DropAndTrimCurve`), with `sm_FixUVTrimCurves` (`SmFace.cpp`) called from `CreateUVTrimCurves`. Which of these steps yields the single cubic span is not yet pinned.
|
||||
- **What happens:** the six cone fillet faces (faces 2-7 in read order) are symmetric pairs about the origin. At accuracy 1e-9, with each face's own box midpoint as the area origin and the B-rep box midpoint (the origin) and area estimate of `SmBrep::ComputePreciseProperties` for the volume:
|
||||
|
||||
| Face | Box midpoint | C++ area | Port area | C++ volume term (`SM_OT_SAME`) | Port volume term |
|
||||
|------|--------------|----------|-----------|--------------------------------|------------------|
|
||||
| 2 | (-21.328, 0, 0) | 3158.691087366512 | same bits | 34760.371669123495 | same bits |
|
||||
| 3 | (21.328, 0, 0) | 3158.691158601684 | same bits | 34760.372446088266 | same bits |
|
||||
| 4 | (0, 0, -21.328) | 3158.695605607531 | same bits | 0 | 0 |
|
||||
| 5 | (0, 0, 21.328) | 3158.695947937785 | same bits | 0 | 0 |
|
||||
| 6 | (0, 29.110, 0) | 3158.690617780610 | same bits | 34760.369029003836 | same bits |
|
||||
| 7 | (0, -29.110, 0) | **3159.303327738010** | 3158.702340530602 | **34762.512347049167** | 34760.371712554756 |
|
||||
|
||||
Faces 0-1 (spheres) and 2-6 agree to the last bit. Face 7 alone differs: C++ is 0.601 larger in area and 2.141 larger in volume term, and both C++ values stand apart from the five symmetric siblings (area 3158.6906-3158.6959, volume term 34760.369-34760.372), which the port's face 7 matches. This one face is the whole of `corpus.parity`'s disagreement on the file: B-rep area 38236.6055 (C++) vs 38236.0045 (port), volume 169497.2899 vs 169499.4301 (1.26e-5 relative). The C++ face 7 value does not move with accuracy (B-rep area 38236.6109 at 1e-6, 38236.6055 at 1e-10).
|
||||
- **Independent check:** a fine inscribed mesh of the port's B-rep (solidcore-mesh, relative chord 4e-6, 2e-6 and 1e-6, Richardson-extrapolated) gives 3158.6966 for faces 2, 3 and 5, against the integrals 3158.6911-3158.6959, which supports the siblings' value; the mesher did not mesh faces 4, 6 and 7 at that run (since fixed in solidcore-mesh), so face 7 itself is judged by symmetry and the trims, not by the mesh.
|
||||
- **Cause (input to the integral):** the file carries no UV trim for most edge uses; both sides make them when the face is measured (gdb on the C++ child: no trim is made or changed during `ReadFromFile`; every `CreateUVTrimCurve` call comes from `ComputePreciseProperties`). The trim boundary face 7 is integrated over differs on one curve, curve 2: edge 30's, from (0.37738, 0), on the surface's v-seam, to (0.41329, 0.125) in UV. The port drops a degree-3 trim with 46 control points; C++ makes a degree-3 curve with 4 control points (one cubic span) over the same interval [0.49999995785315, 0.74999998663581] with the same first and last control points. This is a Rust/C++ divergence inside the ported on-demand trim creation (`CreateUVTrimCurve` / `DropCurve` / `sm_FixUVTrimCurves`); the diverging step is not yet pinned. Every other curve of face 7 and every curve of face 6 (a sibling, the control) match exactly (largest control point difference 0). The face integrated over C++'s single span gains 0.60 of area and 2.14 of volume term and stands apart from its siblings; over the port's 46-point drop it matches them. The port runs with the default `Kernel` (upstream's `sm_FixUVTrimCurves` path, `UpstreamFixes::uv_trim_corner_end_points_only` off).
|
||||
- **Trigger:** read `upstream/TestFiles/pt_TestFiles/Fillet/sphere_diff_3cylinders.smb` (`SmBrep::ReadFromFile`) and compute its area or volume (`corpus.parity`). Per-face probe: `crates/difftest/tests/probe_sphere_diff_faces.rs` (shim `harness/cpp/shim/corpus_faces.cpp`); mesh probe: `crates/solidcore-mesh/tests/probe_sphere_diff_mesh.rs`.
|
||||
- **How found:** corpus.parity disagreement; per-face values first from gdb on the two children (2026-10-23, recorded as WR-256 in the former shared archive, whose entry did not survive the split; the ID now names another record), then from the per-face probe and the trims probe (2026-10-26).
|
||||
- **Confirmed:** yes, case corpus.parity `Fillet/sphere_diff_3cylinders.smb` (C++ build, x86-64 Linux, `c9c5979`): C++'s face 7 deviates from its five symmetric siblings, which the port matches; the diverging step is in on-demand trim creation (`CreateUVTrimCurve` / `DropCurve` / `sm_FixUVTrimCurves`), not yet pinned. Evidence: the per-face probe (`probe_sphere_diff_faces`) and the trims probe (`probe_sphere_diff_trims`, shim `sc_corpus_faces_trims`), both in `crates/difftest/tests/probe_sphere_diff_faces.rs`.
|
||||
- **Rust port:** the same integrator and a port of the same trim creation, which here gives a different trim (the divergence above); its face 7 matches its siblings. `corpus.parity` lists the file in `KNOWN_CPP_DEFECTS` for its area and volume (every count and kind still compared).
|
||||
Reference in New Issue
Block a user