mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 20:02:16 +00:00
kernel: native and SMLib-import tests on the record; the adapter's tests go
native.rs's test module keeps its encode/decode, wire-record, hostile-input and appearance tests. Deleted with the adapter they tested: pcurves_on_planes_are_the_curves_mapped_exactly, pcurves_fit_curved_faces_within_tolerance, pcurves_of_seams_lie_a_period_apart and pcurve_pictures, with their helpers (pcurves_of, check_pcurves, check_faces, curved_bodies, seam_faces, draw). smlib_import_tests count shells on the record; their notes no longer speak of truck. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
+1
-386
@@ -283,6 +283,7 @@ mod smlib_import_tests;
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solidcraft_geom::Vec3;
|
||||
use std::sync::Arc;
|
||||
fn equal(a: &Body, b: &Body) {
|
||||
assert_eq!(a.face_count(), b.face_count());
|
||||
assert_eq!(a.face_sources, b.face_sources);
|
||||
@@ -419,390 +420,4 @@ mod tests {
|
||||
let restored = decode_native_body(&encode_native_body(&body).unwrap()).unwrap();
|
||||
equal(&body, &restored);
|
||||
}
|
||||
|
||||
/// A face's pcurves as the adapter writes them: its loops relative to its surface.
|
||||
fn pcurves_of(face: &truck_modeling::Face, tol: f64) -> Result<Vec<Vec<solidcraft_geom::brep::Curve2>>> {
|
||||
let record = adapter::face_alone(face, tol)?;
|
||||
Ok(record
|
||||
.loops
|
||||
.into_iter()
|
||||
.map(|l| match l {
|
||||
solidcraft_geom::brep::Loop::Edges { edges } => edges.into_iter().map(|e| e.pcurve).collect(),
|
||||
solidcraft_geom::brep::Loop::Vertex { .. } => Vec::new(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
/// Every pcurve of every face of `body` validates as the schema validator does, spans its
|
||||
/// curve's range, lifts onto the surface within `tol` (64 samples and every span midpoint),
|
||||
/// and each wire closes in (u, v): consecutive uses meet, or meet across a pole (the gap
|
||||
/// lifts to one point). Returns the wires whose closure is a whole number of periods (a loop
|
||||
/// round a pole or the surface), as (face, wire).
|
||||
fn check_pcurves(body: &Body, tol: f64) -> Vec<(usize, usize)> {
|
||||
check_faces(&body.solid.face_iter().cloned().collect::<Vec<_>>(), tol)
|
||||
}
|
||||
fn check_faces(faces: &[truck_modeling::Face], tol: f64) -> Vec<(usize, usize)> {
|
||||
use truck_modeling::{BoundedCurve, MetricSpace, ParametricCurve, ParametricSurface};
|
||||
let mut winding = Vec::new();
|
||||
for (f, face) in faces.iter().enumerate() {
|
||||
let surface = face.surface();
|
||||
let all = pcurves_of(face, tol).unwrap_or_else(|e| panic!("face {f}: {e}"));
|
||||
let wires = face.absolute_boundaries().clone();
|
||||
assert_eq!(all.len(), wires.len());
|
||||
let lift = |uv: [f64; 2]| surface.subs(uv[0], uv[1]);
|
||||
let mut areas = Vec::new();
|
||||
for (w, (pcs, wire)) in all.iter().zip(&wires).enumerate() {
|
||||
let ring: Vec<[f64; 2]> = pcs
|
||||
.iter()
|
||||
.zip(wire.edge_iter())
|
||||
.flat_map(|(pc, e)| {
|
||||
let [a, b] = pc.interval().unwrap();
|
||||
(0..16)
|
||||
.map(move |k| if e.orientation() { a + (b - a) * k as f64 / 16. } else { b - (b - a) * k as f64 / 16. })
|
||||
.map(|t| pc.eval(t).unwrap())
|
||||
})
|
||||
.collect();
|
||||
areas.push(
|
||||
(0..ring.len())
|
||||
.map(|i| {
|
||||
let (p, q) = (ring[i], ring[(i + 1) % ring.len()]);
|
||||
p[0] * q[1] - q[0] * p[1]
|
||||
})
|
||||
.sum::<f64>()
|
||||
* 0.5,
|
||||
);
|
||||
assert_eq!(pcs.len(), wire.len());
|
||||
let mut ends = Vec::new();
|
||||
for (i, (pc, edge)) in pcs.iter().zip(wire.edge_iter()).enumerate() {
|
||||
let at = format!("face {f} wire {w} edge {i}");
|
||||
let (knots, count, values): (&Vec<f64>, usize, Vec<f64>) = match pc {
|
||||
solidcraft_geom::brep::Curve2::Spline { knots, points } => (knots, points.len(), points.iter().flatten().copied().collect()),
|
||||
solidcraft_geom::brep::Curve2::RationalSpline { knots, homogeneous } => {
|
||||
assert!(homogeneous.iter().all(|h| h[2] >= 1e-12), "{at}: weight");
|
||||
(knots, homogeneous.len(), homogeneous.iter().flatten().copied().collect())
|
||||
}
|
||||
};
|
||||
let degree = knots.len() - count - 1;
|
||||
assert!((2..=4096).contains(&count) && (1..=16).contains(°ree) && degree < count, "{at}: size");
|
||||
assert!(knots.windows(2).all(|k| k[0] <= k[1]) && knots[degree] < knots[count], "{at}: knots");
|
||||
assert!(values.iter().chain(knots).all(|x| x.is_finite() && x.abs() <= 1e12), "{at}: numbers");
|
||||
let curve = edge.curve();
|
||||
let (t0, t1) = curve.range_tuple();
|
||||
assert_eq!(pc.interval(), Some([t0, t1]), "{at}: interval");
|
||||
let mut ts: Vec<f64> = (0..=64).map(|k| t0 + (t1 - t0) * k as f64 / 64.).collect();
|
||||
ts.extend(knots.windows(2).filter(|k| k[0] < k[1]).map(|k| 0.5 * (k[0] + k[1])));
|
||||
for t in ts {
|
||||
let uv = pc.eval(t).unwrap_or_else(|| panic!("{at}: eval at {t}"));
|
||||
// Truck's intersection points are its Newton iterate, within 1e-6 mm of
|
||||
// the intersection the pcurve follows.
|
||||
let slack = if matches!(curve, truck_modeling::Curve::IntersectionCurve(_)) { 1e-6 } else { 0. };
|
||||
let d = lift(uv).distance(curve.subs(t));
|
||||
assert!(d <= tol + slack, "{at}: lift error {d} at {t}");
|
||||
}
|
||||
let (a, b) = (pc.eval(t0).unwrap(), pc.eval(t1).unwrap());
|
||||
ends.push(if edge.orientation() { (a, b) } else { (b, a) });
|
||||
}
|
||||
let mut wraps = false;
|
||||
for k in 0..ends.len() {
|
||||
let (end, start) = (ends[k].1, ends[(k + 1) % ends.len()].0);
|
||||
let gap = (end[0] - start[0]).abs().max((end[1] - start[1]).abs());
|
||||
if gap <= 1e-9 {
|
||||
continue;
|
||||
}
|
||||
// A pole: the gap's ends and middle are one point in space.
|
||||
let mid = [0.5 * (end[0] + start[0]), 0.5 * (end[1] + start[1])];
|
||||
let one = lift(end).distance(lift(start)).max(lift(end).distance(lift(mid)));
|
||||
if one <= tol {
|
||||
continue;
|
||||
}
|
||||
let periods = [surface.u_period(), surface.v_period()];
|
||||
let whole = (0..2).all(|d| {
|
||||
let g = start[d] - end[d];
|
||||
g.abs() <= 1e-9 || periods[d].is_some_and(|p| (g / p - (g / p).round()).abs() <= 1e-9)
|
||||
});
|
||||
assert!(
|
||||
whole && lift(end).distance(lift(start)) <= tol,
|
||||
"face {f} wire {w}: open between uses {k} and {}: {end:?} {start:?}",
|
||||
(k + 1) % ends.len()
|
||||
);
|
||||
wraps = true;
|
||||
}
|
||||
if wraps {
|
||||
winding.push((f, w));
|
||||
}
|
||||
}
|
||||
// Loops are relative to the surface: the outer loop (the most area) turns
|
||||
// counter-clockwise in (u, v), holes clockwise, whichever side the face uses.
|
||||
if !winding.iter().any(|(wf, _)| *wf == f) {
|
||||
let outer = (0..areas.len()).max_by(|a, b| areas[*a].abs().total_cmp(&areas[*b].abs())).unwrap();
|
||||
for (w, area) in areas.iter().enumerate() {
|
||||
assert_eq!(*area > 0., w == outer, "face {f} wire {w}: turns the wrong way ({area})");
|
||||
}
|
||||
}
|
||||
}
|
||||
winding
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcurves_on_planes_are_the_curves_mapped_exactly() {
|
||||
use truck_modeling::{BoundedCurve, ParametricCurve, ParametricSurface};
|
||||
let cube = crate::box_solid(Vec3::new(-3., 1., 2.), Vec3::new(40., 30., 10.)).unwrap();
|
||||
assert!(check_pcurves(&cube, 1e-9).is_empty());
|
||||
// Bit for bit: each line's pcurve ends lift to the line's own ends.
|
||||
for face in cube.solid.face_iter() {
|
||||
let surface = face.surface();
|
||||
for (pcs, wire) in pcurves_of(face, 1e-9).unwrap().iter().zip(face.absolute_boundaries().clone()) {
|
||||
for (pc, edge) in pcs.iter().zip(wire.edge_iter()) {
|
||||
let solidcraft_geom::brep::Curve2::Spline { points, .. } = pc else { panic!("a line is a degree-1 spline") };
|
||||
let curve = edge.curve();
|
||||
let (t0, t1) = curve.range_tuple();
|
||||
assert_eq!(surface.subs(points[0][0], points[0][1]), curve.subs(t0));
|
||||
assert_eq!(surface.subs(points[1][0], points[1][1]), curve.subs(t1));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Arcs (rational) on the cylinder's caps.
|
||||
let cyl = crate::cylinder(Vec3::ZERO, Vec3::Z, 5., 40.).unwrap();
|
||||
for face in cyl.solid.face_iter().filter(|f| matches!(f.surface(), truck_modeling::Surface::Plane(_))) {
|
||||
assert!(pcurves_of(face, 1e-9).unwrap().iter().flatten().all(|c| matches!(c, solidcraft_geom::brep::Curve2::RationalSpline { .. })));
|
||||
}
|
||||
}
|
||||
|
||||
/// The kernel's own curved bodies: the cylinder (in halves), patch sphere, torus, a cone
|
||||
/// (apex), a revolved ball, a tube, the cylinder cut by a tilted box (intersection curves),
|
||||
/// a filleted box.
|
||||
fn curved_bodies() -> Vec<(&'static str, Body)> {
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Seg2, Vec2};
|
||||
use std::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
let turn = |outer: Loop2| crate::revolve(&Plane::XZ, &[Region2 { outer, holes: vec![] }], Vec2::ZERO, Vec2::Y, TAU).unwrap().pop().unwrap();
|
||||
let cyl = crate::cylinder(Vec3::ZERO, Vec3::Z, 5., 40.).unwrap();
|
||||
let block = crate::box_solid(Vec3::new(-20., -20., 20.), Vec3::new(20., 20., 60.)).unwrap();
|
||||
let block = crate::transform(&block, Vec3::ZERO, Vec3::new(0., 0., 20.), Vec3::X, 0.4).unwrap();
|
||||
let cut = crate::boolean(&cyl, &block, crate::BoolOp::Cut).unwrap().unwrap();
|
||||
let cube = crate::box_solid(Vec3::ZERO, Vec3::new(40., 30., 10.)).unwrap();
|
||||
let rounded = crate::fillet(&cube, &[Vec3::new(20., 0., 10.)], 2.).unwrap();
|
||||
let half = Loop2 {
|
||||
segs: vec![
|
||||
Seg2::Arc { center: Vec2::ZERO, radius: 6., start: -FRAC_PI_2, sweep: PI },
|
||||
Seg2::Line { a: Vec2::new(0., 6.), b: Vec2::new(0., -6.) },
|
||||
],
|
||||
};
|
||||
vec![
|
||||
("cylinder", cyl),
|
||||
("sphere", crate::sphere(Vec3::ZERO, 12.).unwrap()),
|
||||
("torus", crate::torus(Vec3::ZERO, 20., 4.).unwrap()),
|
||||
("cone", turn(Loop2::polygon(&[Vec2::new(0., 0.), Vec2::new(5., 0.), Vec2::new(0., 10.)]))),
|
||||
("ball", turn(half)),
|
||||
("tube", turn(Loop2::polygon(&[Vec2::new(3., 0.), Vec2::new(5., 0.), Vec2::new(5., 4.), Vec2::new(3., 4.)]))),
|
||||
("cut", cut),
|
||||
("fillet", rounded),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcurves_fit_curved_faces_within_tolerance() {
|
||||
for (name, body) in curved_bodies() {
|
||||
for tol in [1e-3, 1e-5, 1e-6] {
|
||||
let start = std::time::Instant::now();
|
||||
for face in body.solid.face_iter() {
|
||||
pcurves_of(face, tol).unwrap();
|
||||
}
|
||||
let took = start.elapsed();
|
||||
let winding = check_pcurves(&body, tol);
|
||||
eprintln!("{name} at {tol}: {took:?}; loops round the surface: {winding:?}");
|
||||
assert!(winding.is_empty(), "{name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Faces with seams, as imported faces have them (the kernel's own revolutions come in
|
||||
/// pieces): a cylinder whose seam line is used twice between two full circles, each a closed
|
||||
/// edge on one vertex, and a sphere bounded by one meridian used twice from pole to pole.
|
||||
fn seam_faces() -> Vec<(&'static str, truck_modeling::Face)> {
|
||||
use crate::face_params::iso_curve;
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Seg2, Vec2};
|
||||
use std::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
use truck_modeling::{self as mt, ParametricSurface};
|
||||
let turn = |outer: Loop2| crate::revolve(&Plane::XZ, &[Region2 { outer, holes: vec![] }], Vec2::ZERO, Vec2::Y, TAU).unwrap().pop().unwrap();
|
||||
let tube = turn(Loop2::polygon(&[Vec2::new(3., 0.), Vec2::new(5., 0.), Vec2::new(5., 4.), Vec2::new(3., 4.)]));
|
||||
let wall = tube
|
||||
.solid
|
||||
.face_iter()
|
||||
.map(|f| f.surface())
|
||||
.find(|s| matches!(s, mt::Surface::RevolutedCurve(_)) && (s.subs(0., 0.).x.hypot(s.subs(0., 0.).y) - 5.).abs() < 1e-9)
|
||||
.unwrap();
|
||||
let (p0, p1) = (mt::Vertex::new(wall.subs(0., 0.)), mt::Vertex::new(wall.subs(1., 0.)));
|
||||
let seam = mt::Edge::new_unchecked(&p0, &p1, iso_curve(&wall, true, 0., 0., 1.).unwrap());
|
||||
let low = mt::Edge::new_unchecked(&p0, &p0, iso_curve(&wall, false, 0., 0., TAU).unwrap());
|
||||
let high = mt::Edge::new_unchecked(&p1, &p1, iso_curve(&wall, false, 1., 0., TAU).unwrap());
|
||||
let cylinder = mt::Face::new_unchecked(vec![mt::Wire::from(vec![seam.clone(), high.clone(), seam.inverse(), low.inverse()])], wall.clone());
|
||||
let band = mt::Face::new_unchecked(vec![mt::Wire::from(vec![low]), mt::Wire::from(vec![high.inverse()])], wall);
|
||||
let half = Loop2 {
|
||||
segs: vec![
|
||||
Seg2::Arc { center: Vec2::ZERO, radius: 6., start: -FRAC_PI_2, sweep: PI },
|
||||
Seg2::Line { a: Vec2::new(0., 6.), b: Vec2::new(0., -6.) },
|
||||
],
|
||||
};
|
||||
let ball = turn(half);
|
||||
let round = ball.solid.face_iter().map(|f| f.surface()).find(|s| matches!(s, mt::Surface::RevolutedCurve(_))).unwrap();
|
||||
let (s0, s1) = (mt::Vertex::new(round.subs(0., 0.)), mt::Vertex::new(round.subs(1., 0.)));
|
||||
let meridian = mt::Edge::new_unchecked(&s0, &s1, iso_curve(&round, true, 0., 0., 1.).unwrap());
|
||||
let sphere = mt::Face::new_unchecked(vec![mt::Wire::from(vec![meridian.clone(), meridian.inverse()])], round);
|
||||
vec![("seam cylinder", cylinder), ("seam sphere", sphere), ("band", band)]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcurves_of_seams_lie_a_period_apart() {
|
||||
use solidcraft_geom::brep::Curve2;
|
||||
for (name, face) in seam_faces() {
|
||||
for bad in [0., -1., f64::NAN, f64::INFINITY] {
|
||||
assert!(pcurves_of(&face, bad).is_err());
|
||||
}
|
||||
for tol in [1e-3, 1e-6] {
|
||||
// A face round the surface without a seam: each loop closes a period apart.
|
||||
if name == "band" {
|
||||
assert_eq!(check_faces(std::slice::from_ref(&face), tol), [(0, 0), (0, 1)]);
|
||||
continue;
|
||||
}
|
||||
assert!(check_faces(std::slice::from_ref(&face), tol).is_empty(), "{name}");
|
||||
let wire = &pcurves_of(&face, tol).unwrap()[0];
|
||||
let at = |c: &Curve2, f: f64| {
|
||||
let [a, b] = c.interval().unwrap();
|
||||
c.eval(a + (b - a) * f).unwrap()
|
||||
};
|
||||
// The seam's two uses: the same curve on either side of the period.
|
||||
let (first, second) = if name == "seam cylinder" { (&wire[0], &wire[2]) } else { (&wire[0], &wire[1]) };
|
||||
for f in [0., 0.3, 1.] {
|
||||
let (a, b) = (at(first, f), at(second, f));
|
||||
assert!((a[0] - b[0]).abs() < 1e-9 && ((b[1] - a[1]).abs() - std::f64::consts::TAU).abs() < 1e-9, "{name}: {a:?} {b:?}");
|
||||
}
|
||||
for c in wire {
|
||||
let (lo, hi) = (0..=16).map(|k| at(c, k as f64 / 16.)[1]).fold((f64::MAX, f64::MIN), |(l, h), v| (l.min(v), h.max(v)));
|
||||
assert!(lo >= -1e-9 && hi <= std::f64::consts::TAU + 1e-9, "{name}: in the base period");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// With `SOLIDCRAFT_PCURVE_SVG=<dir>`, each test body's face loops in (u, v), one panel per
|
||||
/// face: the surface's base period framed, each edge use in its own colour with an arrow
|
||||
/// along the wire.
|
||||
#[test]
|
||||
fn pcurve_pictures() {
|
||||
let Ok(dir) = std::env::var("SOLIDCRAFT_PCURVE_SVG") else { return };
|
||||
let box_ = crate::box_solid(Vec3::ZERO, Vec3::new(40., 30., 10.)).unwrap();
|
||||
let mut sets: Vec<(String, Vec<truck_modeling::Face>)> = vec![("box".into(), box_.solid.face_iter().cloned().collect())];
|
||||
sets.extend(curved_bodies().into_iter().map(|(n, b)| (n.to_string(), b.solid.face_iter().cloned().collect())));
|
||||
sets.extend(seam_faces().into_iter().map(|(n, f)| (n.to_string(), vec![f])));
|
||||
for (name, faces) in sets {
|
||||
let svg = draw(&name, &faces, 1e-5);
|
||||
std::fs::write(format!("{dir}/{}.svg", name.replace(' ', "-")), svg).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(name: &str, faces: &[truck_modeling::Face], tol: f64) -> String {
|
||||
use std::fmt::Write;
|
||||
use std::ops::Bound::{Excluded, Included};
|
||||
use truck_modeling::ParametricSurface;
|
||||
const W: f64 = 300.;
|
||||
const COLOURS: [&str; 8] = ["#d62728", "#1f77b4", "#2ca02c", "#9467bd", "#ff7f0e", "#17becf", "#e377c2", "#8c564b"];
|
||||
let cols = faces.len().min(4);
|
||||
let rows = faces.len().div_ceil(4);
|
||||
let mut out = String::new();
|
||||
write!(out, r#"<svg xmlns="http://www.w3.org/2000/svg" width="{}" height="{}" font-family="sans-serif" font-size="12"><rect width="100%" height="100%" fill="white"/><text x="10" y="20" font-size="16">{name}: face loops in (u, v)</text>"#, cols as f64 * W, rows as f64 * W + 30.).unwrap();
|
||||
for (f, face) in faces.iter().enumerate() {
|
||||
let (ox, oy) = ((f % 4) as f64 * W, (f / 4) as f64 * W + 30.);
|
||||
let surface = face.surface();
|
||||
let wires = pcurves_of(face, tol).unwrap();
|
||||
let lines: Vec<Vec<(usize, bool, Vec<[f64; 2]>)>> = wires
|
||||
.iter()
|
||||
.zip(face.absolute_boundaries().clone())
|
||||
.map(|(pcs, wire)| {
|
||||
pcs.iter()
|
||||
.zip(wire.edge_iter())
|
||||
.enumerate()
|
||||
.map(|(i, (c, e))| {
|
||||
let [a, b] = c.interval().unwrap();
|
||||
(i, e.orientation(), (0..=64).map(|k| c.eval(a + (b - a) * k as f64 / 64.).unwrap()).collect())
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
// The frame: the base period along periodic directions, the surface's range along
|
||||
// bounded ones, else the loops' extent.
|
||||
let all: Vec<[f64; 2]> = lines.iter().flatten().flat_map(|l| l.2.clone()).collect();
|
||||
let ext = |d: usize| all.iter().map(|p| p[d]).fold((f64::MAX, f64::MIN), |(l, h), v| (l.min(v), h.max(v)));
|
||||
let (ru, rv) = surface.parameter_range();
|
||||
let range = |r: (std::ops::Bound<f64>, std::ops::Bound<f64>), d: usize| match r {
|
||||
(Included(a) | Excluded(a), Included(b) | Excluded(b)) => (a, b),
|
||||
_ => ext(d),
|
||||
};
|
||||
let frame = if matches!(surface, truck_modeling::Surface::Plane(_)) { [ext(0), ext(1)] } else { [range(ru, 0), range(rv, 1)] };
|
||||
let view = |d: usize| {
|
||||
let (e, r) = (ext(d), frame[d]);
|
||||
let (lo, hi) = (e.0.min(r.0), e.1.max(r.1));
|
||||
let pad = (hi - lo).max(1e-9) * 0.08;
|
||||
(lo - pad, hi + pad)
|
||||
};
|
||||
let (vu, vv) = (view(0), view(1));
|
||||
let px = |p: [f64; 2]| (ox + 50. + (p[0] - vu.0) / (vu.1 - vu.0) * (W - 70.), oy + W - 30. - (p[1] - vv.0) / (vv.1 - vv.0) * (W - 50.));
|
||||
let kind = match surface {
|
||||
truck_modeling::Surface::Plane(_) => "plane",
|
||||
truck_modeling::Surface::BSplineSurface(_) => "B-spline",
|
||||
truck_modeling::Surface::NurbsSurface(_) => "NURBS",
|
||||
truck_modeling::Surface::RevolutedCurve(_) => "revolution",
|
||||
};
|
||||
let periodic = match (surface.u_period(), surface.v_period()) {
|
||||
(Some(_), _) => ", u periodic",
|
||||
(_, Some(_)) => ", v periodic",
|
||||
_ => "",
|
||||
};
|
||||
let (a, b) = (px([frame[0].0, frame[1].1]), px([frame[0].1, frame[1].0]));
|
||||
write!(out, r##"<rect x="{}" y="{}" width="{}" height="{}" fill="#f4f4f4" stroke="#888" stroke-dasharray="4 3"/><text x="{}" y="{}">face {f}: {kind}{periodic}</text>"##, a.0, a.1, b.0 - a.0, b.1 - a.1, ox + 10., oy + 14.).unwrap();
|
||||
for (w, wire) in lines.iter().enumerate() {
|
||||
for (i, fwd, pts) in wire {
|
||||
let colour = COLOURS[(i + 3 * w) % COLOURS.len()];
|
||||
let path: Vec<String> = pts
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let q = px(*p);
|
||||
format!("{:.2} {:.2}", q.0, q.1)
|
||||
})
|
||||
.collect();
|
||||
write!(out, r#"<path d="M {}" fill="none" stroke="{colour}" stroke-width="2"/>"#, path.join(" L ")).unwrap();
|
||||
// An arrow at the middle, along the wire.
|
||||
let (m0, m1) = (px(pts[31]), px(pts[33]));
|
||||
let (dx, dy) = if *fwd { (m1.0 - m0.0, m1.1 - m0.1) } else { (m0.0 - m1.0, m0.1 - m1.1) };
|
||||
let len = dx.hypot(dy).max(1e-9);
|
||||
let (ux, uy) = (dx / len * 9., dy / len * 9.);
|
||||
let c = px(pts[32]);
|
||||
write!(
|
||||
out,
|
||||
r#"<polygon points="{:.2},{:.2} {:.2},{:.2} {:.2},{:.2}" fill="{colour}"/>"#,
|
||||
c.0 + ux,
|
||||
c.1 + uy,
|
||||
c.0 - ux - uy * 0.6,
|
||||
c.1 - uy + ux * 0.6,
|
||||
c.0 - ux + uy * 0.6,
|
||||
c.1 - uy - ux * 0.6
|
||||
)
|
||||
.unwrap();
|
||||
let s = px(if *fwd { pts[0] } else { pts[64] });
|
||||
write!(
|
||||
out,
|
||||
r#"<circle cx="{:.2}" cy="{:.2}" r="2.5" fill="black"/><text x="{:.2}" y="{:.2}" fill="{colour}">{w}.{i}</text>"#,
|
||||
s.0,
|
||||
s.1,
|
||||
c.0 + 6.,
|
||||
c.1 - 6.
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
let (l0, l1) = (px([frame[0].0, frame[1].0]), px([frame[0].1, frame[1].1]));
|
||||
write!(out, r##"<text x="{:.2}" y="{:.2}" fill="#555">u {:.3}</text><text x="{:.2}" y="{:.2}" fill="#555" text-anchor="end">{:.3}</text><text x="{:.2}" y="{:.2}" fill="#555" text-anchor="end">v {:.3}</text><text x="{:.2}" y="{:.2}" fill="#555" text-anchor="end">{:.3}</text>"##,
|
||||
l0.0, oy + W - 12., frame[0].0, l1.0, oy + W - 12., frame[0].1, ox + 46., l0.1, frame[1].0, ox + 46., l1.1 + 10., frame[1].1).unwrap();
|
||||
}
|
||||
out.push_str("</svg>");
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Truck bodies into SMLib through their schema 4 records: the kernel's primitives keep their
|
||||
//! analytic volume. Truck holds cylinders, spheres and tori as rational B-splines, which SMLib
|
||||
//! integrates to a few 1e-7 (truck's own mesh measure is 1e-4 off).
|
||||
//! Bodies into SMLib through their schema 4 records: the kernel's primitives keep their analytic
|
||||
//! volume.
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use solidcraft_geom::Vec3;
|
||||
@@ -44,8 +43,8 @@ fn revolutions_keep_their_analytic_volume() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A body SMLib made (analytic faces) comes back from truck as revolutions, and goes into SMLib
|
||||
/// again as the same analytic faces: a second boolean on a first one's result.
|
||||
/// A body SMLib made (analytic faces) comes back as a record and goes into SMLib again as the
|
||||
/// same analytic faces: a second boolean on a first one's result.
|
||||
#[test]
|
||||
fn smlib_results_go_back_into_smlib() {
|
||||
for (name, body, exact) in [
|
||||
@@ -62,8 +61,8 @@ fn smlib_results_go_back_into_smlib() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Truck's cylinders are circles extruded as rational splines: they go into SMLib as its
|
||||
/// cylinder, so a boolean with one intersects a plane with a cylinder, not with a spline.
|
||||
/// Extruded circles go into SMLib as its cylinder, so a boolean with one intersects a plane with
|
||||
/// a cylinder, not with a spline.
|
||||
#[test]
|
||||
fn extruded_circles_go_as_cylinders() {
|
||||
use solidcraft_smlib::flat;
|
||||
@@ -89,7 +88,7 @@ fn a_cavity_comes_back_from_smlib() {
|
||||
let cavity = box_solid(v(25., 25., 25.), v(30., 30., 30.)).unwrap();
|
||||
let r = super::smlib_body(&block).unwrap().boolean(&super::smlib_body(&cavity).unwrap(), solidcraft_smlib::BooleanOp::Subtract).unwrap().unwrap();
|
||||
let back = super::body_from_smlib(&r).unwrap();
|
||||
assert_eq!((back.solid.boundaries().len(), back.face_count()), (2, 12));
|
||||
assert_eq!((back.shell_count(), back.face_count()), (2, 12));
|
||||
let got = super::smlib_body(&back).unwrap().volume(1e-7).unwrap();
|
||||
assert!((got - (64_000.0 - 125.0)).abs() < 1e-6, "{got}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user