Sheet metal: the flat-pattern DXF writes ellipses and splines exactly (#1096)
sheet.export_dxf writes an AutoCAD 2000 drawing with ELLIPSE and SPLINE entities for ellipses, splines and text outlines instead of chords, with curves: arcs for machines that read only lines and arcs. Perforation circles go through the same writer. Closes #1089.
This commit is contained in:
1 parent
9a4e8fbd19
commit
764fcaa5a8
8 files changed
+972
-95
No files matched your search
@@ -60,7 +60,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
),
|
||||
CommandSpec::new("sheet.export_dxf", "Export Flat Pattern as DXF", export_dxf)
|
||||
.noundo()
|
||||
.params("body?, path? → DXF (mm): OUTLINE, BEND_UP and BEND_DOWN (centre lines) layers"),
|
||||
.params("body?, path?, curves?: exact|arcs (default exact: an ellipse as ELLIPSE, splines, text outlines and other Béziers as SPLINE; arcs: tangent arcs and lines within 0.001 mm, for machines that read only those) → DXF (AutoCAD 2000, mm): OUTLINE, BEND_UP and BEND_DOWN (centre lines) layers"),
|
||||
CommandSpec::new("sheet.export_bend_table", "Export Bend Table", export_bend_table)
|
||||
.noundo()
|
||||
.params("body?, format?: csv|json (default csv), path? → bend table in mm and degrees, including rule and flat centre-line coordinates; a bend that cut-outs split has a row for each piece"),
|
||||
@@ -491,8 +491,20 @@ fn flat_pattern(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
flat_json(&sh)
|
||||
}
|
||||
|
||||
/// A DXF (R12 entities, mm) of the flat pattern on named layers.
|
||||
pub fn flat_dxf(sh: &SheetBody) -> Result<String> {
|
||||
/// How the flat DXF writes the curves that are neither lines nor circular arcs.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum DxfCurves {
|
||||
/// Exactly: an ellipse as ELLIPSE, Béziers (spline pieces, text outlines) and the other conics as SPLINE.
|
||||
#[default]
|
||||
Exact,
|
||||
/// As tangent arcs and lines within 0.001 mm, for machines that read only those.
|
||||
Arcs,
|
||||
}
|
||||
|
||||
/// A DXF (AutoCAD 2000, mm) of the flat pattern on named layers: OUTLINE, BEND_UP and BEND_DOWN
|
||||
/// (centre lines), and one layer per pocket depth. Free-form curves as `curves` says.
|
||||
pub fn flat_dxf(sh: &SheetBody, curves: DxfCurves) -> Result<String> {
|
||||
use solidcraft_io::{DxfEntity, DxfLayer, DxfShape};
|
||||
let regions = sh.flat_regions()?;
|
||||
let pockets = sh.machining()?;
|
||||
let pocket_layer = |p: &solidcraft_doc::sheet::SheetPocket| {
|
||||
@@ -520,110 +532,63 @@ pub fn flat_dxf(sh: &SheetBody) -> Result<String> {
|
||||
if !perforation.is_empty() {
|
||||
layers.insert("PERFORATION".to_string());
|
||||
}
|
||||
let mut out = String::new();
|
||||
let mut w = |c: i32, v: &str| out.push_str(&format!("{c}\n{v}\n"));
|
||||
w(0, "SECTION");
|
||||
w(2, "HEADER");
|
||||
w(9, "$INSUNITS");
|
||||
w(70, "4");
|
||||
w(0, "ENDSEC");
|
||||
w(0, "SECTION");
|
||||
w(2, "TABLES");
|
||||
w(0, "TABLE");
|
||||
w(2, "LAYER");
|
||||
w(70, &layers.len().to_string());
|
||||
for name in &layers {
|
||||
let colour = match name.as_str() {
|
||||
"OUTLINE" => "7",
|
||||
"BEND_UP" => "1",
|
||||
"BEND_DOWN" => "5",
|
||||
_ => "3",
|
||||
};
|
||||
w(0, "LAYER");
|
||||
w(2, name);
|
||||
w(70, "0");
|
||||
w(62, colour);
|
||||
w(6, "CONTINUOUS");
|
||||
}
|
||||
w(0, "ENDTAB");
|
||||
w(0, "ENDSEC");
|
||||
w(0, "SECTION");
|
||||
w(2, "ENTITIES");
|
||||
let f = |x: f64| format!("{x:.6}");
|
||||
let line = |w: &mut dyn FnMut(i32, &str), layer: &str, a: Vec2, b: Vec2| {
|
||||
w(0, "LINE");
|
||||
w(8, layer);
|
||||
w(10, &f(a.x));
|
||||
w(20, &f(a.y));
|
||||
w(11, &f(b.x));
|
||||
w(21, &f(b.y));
|
||||
};
|
||||
let layers: Vec<DxfLayer> = layers
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
let color = match name.as_str() {
|
||||
"OUTLINE" => 7,
|
||||
"BEND_UP" => 1,
|
||||
"BEND_DOWN" => 5,
|
||||
_ => 3,
|
||||
};
|
||||
DxfLayer { name, color }
|
||||
})
|
||||
.collect();
|
||||
let mut loops: Vec<_> = regions.iter().flat_map(|r| std::iter::once(&r.outer).chain(&r.holes)).map(|lp| (lp, "OUTLINE".to_string())).collect();
|
||||
for p in &pockets {
|
||||
for lp in std::iter::once(&p.region.outer).chain(&p.region.holes) {
|
||||
loops.push((lp, pocket_layer(p)));
|
||||
}
|
||||
}
|
||||
let mut entities = Vec::new();
|
||||
for (h, layer) in loops {
|
||||
// A circle is often represented by two semicircles. Keep the whole analytic
|
||||
// boundary as one CIRCLE rather than approximating outer boundaries with chords.
|
||||
if let Some((center, radius)) = loop_circle(h) {
|
||||
w(0, "CIRCLE");
|
||||
w(8, &layer);
|
||||
w(10, &f(center.x));
|
||||
w(20, &f(center.y));
|
||||
w(40, &f(radius));
|
||||
entities.push(DxfEntity { layer, shape: DxfShape::Circle { c: center, r: radius } });
|
||||
continue;
|
||||
}
|
||||
for seg in &h.segs {
|
||||
match *seg {
|
||||
solidcraft_geom::Seg2::Arc { center, radius, start, sweep } => {
|
||||
if (sweep.abs() - std::f64::consts::TAU).abs() < 1e-9 {
|
||||
w(0, "CIRCLE");
|
||||
w(8, &layer);
|
||||
w(10, &f(center.x));
|
||||
w(20, &f(center.y));
|
||||
w(40, &f(radius));
|
||||
continue;
|
||||
}
|
||||
let (a0, a1) = if sweep >= 0.0 { (start, start + sweep) } else { (start + sweep, start) };
|
||||
w(0, "ARC");
|
||||
w(8, &layer);
|
||||
w(10, &f(center.x));
|
||||
w(20, &f(center.y));
|
||||
w(40, &f(radius));
|
||||
w(50, &f(a0.to_degrees().rem_euclid(360.0)));
|
||||
w(51, &f(a1.to_degrees().rem_euclid(360.0)));
|
||||
}
|
||||
other => {
|
||||
let pts = other.polyline(1e-3);
|
||||
for p in pts.windows(2) {
|
||||
line(&mut w, &layer, p[0], p[1]);
|
||||
}
|
||||
}
|
||||
let fitted;
|
||||
let h = match curves {
|
||||
DxfCurves::Arcs if !h.is_line_arc() => {
|
||||
fitted = h
|
||||
.fit_arcs(solidcraft_kernel::PLANAR_FIT_TOL)
|
||||
.ok_or_else(|| EngineError::Other("a flat-pattern curve could not be written as arcs".into()))?;
|
||||
&fitted
|
||||
}
|
||||
}
|
||||
_ => h,
|
||||
};
|
||||
// A whole ellipse is one ELLIPSE; every other segment is an entity of its own.
|
||||
entities.extend(solidcraft_io::loop_shapes(h).into_iter().map(|shape| DxfEntity { layer: layer.clone(), shape }));
|
||||
}
|
||||
for b in sh.bends() {
|
||||
line(&mut w, if b.up { "BEND_UP" } else { "BEND_DOWN" }, b.start, b.end);
|
||||
entities.push(DxfEntity { layer: if b.up { "BEND_UP" } else { "BEND_DOWN" }.into(), shape: DxfShape::Line(b.start, b.end) });
|
||||
}
|
||||
for (c, r) in &perforation {
|
||||
w(0, "CIRCLE");
|
||||
w(8, "PERFORATION");
|
||||
w(10, &c.x.to_string());
|
||||
w(20, &c.y.to_string());
|
||||
w(30, "0");
|
||||
w(40, &r.to_string());
|
||||
for (c, r) in perforation {
|
||||
entities.push(DxfEntity { layer: "PERFORATION".into(), shape: DxfShape::Circle { c, r } });
|
||||
}
|
||||
w(0, "ENDSEC");
|
||||
w(0, "EOF");
|
||||
Ok(out)
|
||||
Ok(solidcraft_io::write_dxf_drawing(&layers, &entities))
|
||||
}
|
||||
|
||||
fn export_dxf(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sheet.export_dxf";
|
||||
let curves = match str_(p, "curves").map(str::to_ascii_lowercase).as_deref() {
|
||||
None | Some("exact") => DxfCurves::Exact,
|
||||
Some("arcs") => DxfCurves::Arcs,
|
||||
Some(_) => return Err(bad(cmd, "`curves` must be exact or arcs")),
|
||||
};
|
||||
let sh = sheet_of(s, p, cmd)?;
|
||||
let text = flat_dxf(&sh)?;
|
||||
let text = flat_dxf(&sh, curves)?;
|
||||
if let Some(path) = str_(p, "path").filter(|x| !x.trim().is_empty()) {
|
||||
solidcraft_io::vfs::write(path, text.as_bytes()).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
return Ok(json!({"path": path, "bends": sh.bends().len()}));
|
||||
@@ -682,3 +647,7 @@ mod tests;
|
||||
#[cfg(test)]
|
||||
#[path = "sheet_convert_tests.rs"]
|
||||
mod convert_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sheet_dxf_tests.rs"]
|
||||
mod dxf_tests;
|
||||
@@ -637,7 +637,9 @@ fn flat_circles(s: &mut Session) -> Vec<(f64, [f64; 2])> {
|
||||
/// How many entities of each kind the flat pattern's DXF has.
|
||||
fn dxf_entities(s: &mut Session) -> (usize, usize, usize) {
|
||||
let dxf = run(s, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap_or("").replace("\r\n", "\n");
|
||||
let n = |kind: &str| dxf.matches(&format!("\n0\n{kind}\n")).count();
|
||||
// Group codes may be padded (` 0`): compare the code and value lines trimmed.
|
||||
let lines: Vec<&str> = dxf.lines().map(str::trim).collect();
|
||||
let n = |kind: &str| lines.chunks(2).filter(|pair| pair == &["0", kind]).count();
|
||||
(n("LINE"), n("ARC"), n("CIRCLE"))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
//! The flat pattern's DXF keeps ellipses and free-form curves as ELLIPSE and SPLINE entities.
|
||||
|
||||
use std::f64::consts::{PI, TAU};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_geom::{Seg2, Vec2};
|
||||
|
||||
use crate::Session;
|
||||
|
||||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||||
match s.execute(id, &p) {
|
||||
Ok(v) => v,
|
||||
Err(e) => panic!("{id} {p}: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A 100 × 60 base flange (the default 2.5 mm rule) with `curves` drawn in a sketch and cut through it.
|
||||
fn plate_with(curves: &[(&str, Value)]) -> Session {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Base"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [100, 60]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "sheet.flange", json!({"sketch": "Base"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Cut"}));
|
||||
for (command, params) in curves {
|
||||
run(&mut s, command, params.clone());
|
||||
}
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Cut", "through_all": true, "operation": "cut", "direction": "symmetric"}));
|
||||
s
|
||||
}
|
||||
|
||||
fn export(s: &mut Session, params: Value) -> String {
|
||||
run(s, "sheet.export_dxf", params)["dxf"].as_str().unwrap_or_default().to_string()
|
||||
}
|
||||
|
||||
/// The entities of a DXF text: type and group codes.
|
||||
type Entity = (String, Vec<(i32, String)>);
|
||||
|
||||
fn entities(dxf: &str) -> Vec<Entity> {
|
||||
let lines: Vec<&str> = dxf.lines().collect();
|
||||
let pairs: Vec<(i32, &str)> = lines.chunks(2).filter_map(|c| Some((c.first()?.trim().parse().ok()?, c.get(1)?.trim()))).collect();
|
||||
let start = pairs.iter().position(|p| *p == (2, "ENTITIES")).unwrap_or(0);
|
||||
let mut out: Vec<Entity> = Vec::new();
|
||||
for (code, value) in pairs.iter().skip(start + 1) {
|
||||
match (*code, *value) {
|
||||
(0, "ENDSEC") => break,
|
||||
(0, kind) => out.push((kind.to_string(), Vec::new())),
|
||||
_ => {
|
||||
if let Some(last) = out.last_mut() {
|
||||
last.1.push((*code, value.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn values(e: &Entity, code: i32) -> Vec<f64> {
|
||||
e.1.iter().filter(|(c, _)| *c == code).filter_map(|(_, v)| v.parse().ok()).collect()
|
||||
}
|
||||
|
||||
fn count(es: &[Entity], kind: &str) -> usize {
|
||||
es.iter().filter(|e| e.0 == kind).count()
|
||||
}
|
||||
|
||||
fn point(e: &Entity, code: i32) -> Vec2 {
|
||||
Vec2::new(values(e, code).first().copied().unwrap_or(f64::NAN), values(e, code + 10).first().copied().unwrap_or(f64::NAN))
|
||||
}
|
||||
|
||||
/// All loops of the flat pattern (outlines, cut-outs, islands).
|
||||
fn loops(s: &Session) -> Vec<solidcraft_geom::Loop2> {
|
||||
let regions = s.model.state().sheets[0].flat_regions().unwrap_or_default();
|
||||
regions.iter().flat_map(|r| std::iter::once(r.outer.clone()).chain(r.holes.clone())).collect()
|
||||
}
|
||||
|
||||
/// The profiles of the DXF read into a new sketch: their areas, largest first.
|
||||
fn reimported_areas(dxf: &str) -> Vec<f64> {
|
||||
let mut s = Session::default();
|
||||
let id = run(&mut s, "sketch.insert_dxf", json!({"text": dxf, "name": "Back"}))["sketch"].as_u64().unwrap_or(0);
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let mut areas: Vec<f64> = s.model.state().sketch(id).map(|ss| ss.profiles.iter().map(|p| p.area).collect()).unwrap_or_default();
|
||||
areas.sort_by(|a, b| b.total_cmp(a));
|
||||
areas
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_elliptic_cut_out_is_one_ellipse() {
|
||||
let mut s = plate_with(&[("sketch.ellipse", json!({"center": [40, 30], "major": [55, 30], "minor_radius": 8}))]);
|
||||
let dxf = export(&mut s, json!({}));
|
||||
let es = entities(&dxf);
|
||||
// The four lines of the outline and one ELLIPSE: no other entity.
|
||||
assert_eq!((count(&es, "LINE"), count(&es, "ELLIPSE"), es.len()), (4, 1, 5), "{:?}", es.iter().map(|e| &e.0).collect::<Vec<_>>());
|
||||
// By hand: centre (40, 30), the major semi-axis 15 along x (to (55, 30) or (25, 30)), the
|
||||
// minor one 8, so the ratio is 8/15; the whole turn runs from parameter 0 to 2π.
|
||||
let e = es.iter().find(|e| e.0 == "ELLIPSE").unwrap();
|
||||
let (c, major) = (point(e, 10), point(e, 11));
|
||||
assert!(c.dist(Vec2::new(40.0, 30.0)) < 1e-9 && (major.x.abs() - 15.0).abs() < 1e-9 && major.y.abs() < 1e-9, "{c:?} {major:?}");
|
||||
assert!((values(e, 40)[0] - 8.0 / 15.0).abs() < 1e-9 && values(e, 41)[0] == 0.0 && (values(e, 42)[0] - TAU).abs() < 1e-9);
|
||||
assert_eq!(values(e, 230), [1.0]);
|
||||
// Our reader gets the same ellipse back, and the same plate: 6000 mm² less π·15·8.
|
||||
let read = solidcraft_io::read_dxf(&dxf).unwrap();
|
||||
assert!(read.iter().any(|g| matches!(g, solidcraft_io::Geom2::Ellipse { c, major, minor }
|
||||
if c.dist(Vec2::new(40.0, 30.0)) < 1e-9 && (major.dist(*c) - 15.0).abs() < 1e-9 && (minor - 8.0).abs() < 1e-9)));
|
||||
let areas = reimported_areas(&dxf);
|
||||
assert!(areas.len() == 2 && (areas[0] - (6000.0 - PI * 120.0)).abs() < 1e-6 && (areas[1] - PI * 120.0).abs() < 1e-6, "{areas:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_spline_cut_out_is_the_splines_it_was() {
|
||||
let points = json!([[20, 15], [40, 45], [65, 40], [85, 20]]);
|
||||
let mut s = plate_with(&[("sketch.spline.fit_point", json!({"points": points})), ("sketch.line", json!({"points": [[85, 20], [20, 15]]}))]);
|
||||
let dxf = export(&mut s, json!({}));
|
||||
let es = entities(&dxf);
|
||||
// The cut-out's segments of the flat pattern, one entity each: its cubic pieces are SPLINEs
|
||||
// of degree 3 over the same four control points, its line a LINE.
|
||||
let hole: Vec<Seg2> = loops(&s).get(1).map(|l| l.segs.clone()).unwrap_or_default();
|
||||
let cubics: Vec<[Vec2; 4]> =
|
||||
hole.iter().filter_map(|g| if let Seg2::Cubic { p0, p1, p2, p3 } = g { Some([*p0, *p1, *p2, *p3]) } else { None }).collect();
|
||||
assert!(!cubics.is_empty() && cubics.len() + 1 == hole.len(), "{hole:?}");
|
||||
assert_eq!((count(&es, "LINE"), count(&es, "SPLINE"), es.len()), (5, cubics.len(), 5 + cubics.len()));
|
||||
for (spline, want) in es.iter().filter(|e| e.0 == "SPLINE").zip(&cubics) {
|
||||
assert_eq!((values(spline, 70), values(spline, 71), values(spline, 72), values(spline, 73)), (vec![8.0], vec![3.0], vec![8.0], vec![4.0]));
|
||||
assert_eq!(values(spline, 40), [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]);
|
||||
let (xs, ys) = (values(spline, 10), values(spline, 20));
|
||||
assert!(want.iter().enumerate().all(|(i, p)| (xs[i] - p.x).abs() < 1e-9 && (ys[i] - p.y).abs() < 1e-9), "{xs:?} {ys:?} {want:?}");
|
||||
}
|
||||
// The same plate comes back.
|
||||
let areas = reimported_areas(&dxf);
|
||||
let plate = s.model.state().sheets[0].flat_regions().unwrap_or_default()[0].area();
|
||||
assert!((areas[0] - plate).abs() < 1e-6, "{areas:?} {plate}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_is_splines_and_lines_not_chords() {
|
||||
let mut s = plate_with(&[("sketch.text", json!({"text": "AB", "at": [30, 20], "height": 12}))]);
|
||||
let dxf = export(&mut s, json!({}));
|
||||
let es = entities(&dxf);
|
||||
// One entity for each segment of the flat pattern: a straight one a LINE, a glyph's
|
||||
// quadratic and cubic curves SPLINEs of degree 2 and 3.
|
||||
let segs: Vec<Seg2> = loops(&s).iter().flat_map(|l| l.segs.clone()).collect();
|
||||
let n = |f: &dyn Fn(&Seg2) -> bool| segs.iter().filter(|g| f(g)).count();
|
||||
let (lines, quadratics, cubics) = (
|
||||
n(&|g| matches!(g, Seg2::Line { .. })),
|
||||
n(&|g| matches!(g, Seg2::Conic { w, .. } if (*w - 1.0).abs() < 1e-12)),
|
||||
n(&|g| matches!(g, Seg2::Cubic { .. })),
|
||||
);
|
||||
assert_eq!(lines + quadratics + cubics, segs.len(), "only lines and Béziers");
|
||||
assert_eq!((count(&es, "LINE"), count(&es, "SPLINE"), es.len()), (lines, quadratics + cubics, segs.len()));
|
||||
let degree = |d: f64| es.iter().filter(|e| e.0 == "SPLINE" && values(e, 71) == [d]).count();
|
||||
assert_eq!((degree(2.0), degree(3.0)), (quadratics, cubics));
|
||||
// The plate with the letters cut out comes back from the file with its area (the largest region; the islands inside the
|
||||
// letters come in no fixed order).
|
||||
let areas = reimported_areas(&dxf);
|
||||
let plate = s.model.state().sheets[0].flat_regions().unwrap_or_default().iter().map(|r| r.area()).fold(0.0, f64::max);
|
||||
assert!((areas[0] - plate).abs() < 1e-3, "{areas:?} {plate}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arcs_option_writes_only_lines_and_tangent_arcs_within_a_thousandth() {
|
||||
let mut s = plate_with(&[("sketch.ellipse", json!({"center": [40, 30], "major": [55, 30], "minor_radius": 8}))]);
|
||||
let dxf = export(&mut s, json!({"curves": "arcs"}));
|
||||
let es = entities(&dxf);
|
||||
assert!(es.iter().all(|e| e.0 == "LINE" || e.0 == "ARC"), "{:?}", es.iter().map(|e| &e.0).collect::<Vec<_>>());
|
||||
let arcs: Vec<(Vec2, f64, [Vec2; 2], Vec<Vec2>)> = es
|
||||
.iter()
|
||||
.filter(|e| e.0 == "ARC")
|
||||
.map(|e| {
|
||||
let (c, r, (a0, a1)) = (point(e, 10), values(e, 40)[0], (values(e, 50)[0].to_radians(), values(e, 51)[0].to_radians()));
|
||||
let sweep = (a1 - a0).rem_euclid(TAU);
|
||||
let samples = (0..=20).map(|k| c + Vec2::from_angle(a0 + sweep * f64::from(k) / 20.0) * r).collect::<Vec<Vec2>>();
|
||||
(c, r, [c + Vec2::from_angle(a0) * r, c + Vec2::from_angle(a1) * r], samples)
|
||||
})
|
||||
.collect();
|
||||
// Far fewer than the 336 chords of 0.001 mm.
|
||||
assert!(arcs.len() >= 8 && arcs.len() < 100, "{} arcs", arcs.len());
|
||||
// Distance to the ellipse ((x − 40)/15)² + ((y − 30)/8)² = 1, to first order |f| / |∇f|.
|
||||
let off = |p: Vec2| {
|
||||
let (x, y) = (p.x - 40.0, p.y - 30.0);
|
||||
let f = (x / 15.0).powi(2) + (y / 8.0).powi(2) - 1.0;
|
||||
f.abs() / Vec2::new(2.0 * x / 225.0, 2.0 * y / 64.0).len()
|
||||
};
|
||||
// (The joint of a biarc's two arcs lies within the tolerance of the curve, not on it.)
|
||||
for (_, _, ends, samples) in &arcs {
|
||||
let offs: Vec<f64> = samples.iter().map(|p| off(*p)).collect();
|
||||
assert!(offs.iter().all(|d| *d <= 1e-3 + 1e-6), "{ends:?}: {offs:?}");
|
||||
}
|
||||
// Tangent: at each joint the radii of the two arcs lie on one line.
|
||||
for (i, (c, r, ends, _)) in arcs.iter().enumerate() {
|
||||
for end in ends {
|
||||
let others: Vec<_> = arcs.iter().enumerate().filter(|(j, (_, _, e, _))| *j != i && e.iter().any(|q| q.dist(*end) < 1e-6)).collect();
|
||||
assert_eq!(others.len(), 1, "joint {end:?}");
|
||||
let (_, (c2, r2, _, _)) = others[0];
|
||||
assert!((*end - *c).cross(*end - *c2).abs() < 1e-6 * r * r2, "arcs are not tangent at {end:?}");
|
||||
}
|
||||
}
|
||||
// The plate, read back, is the exact one within the perimeter times 0.001 mm.
|
||||
let areas = reimported_areas(&dxf);
|
||||
assert!((areas[0] - (6000.0 - PI * 120.0)).abs() < 0.1, "{areas:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arcs_option_has_no_splines_for_text_and_the_curves_value_is_checked() {
|
||||
let mut s = plate_with(&[("sketch.text", json!({"text": "AB", "at": [30, 20], "height": 12}))]);
|
||||
let es = entities(&export(&mut s, json!({"curves": "ARCS"})));
|
||||
assert!(es.iter().all(|e| e.0 == "LINE" || e.0 == "ARC") && count(&es, "ARC") > 0);
|
||||
let exact = entities(&export(&mut s, json!({"curves": "exact"})));
|
||||
assert!(count(&exact, "SPLINE") > 0 && count(&exact, "ARC") == 0);
|
||||
assert!(s.execute("sheet.export_dxf", &json!({"curves": "chords"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bend_lines_and_layers_are_still_there() {
|
||||
let mut s = plate_with(&[("sketch.ellipse", json!({"center": [40, 30], "major": [55, 30], "minor_radius": 8}))]);
|
||||
run(&mut s, "sheet.flange", json!({"edges": [[50, 0, 2.5]], "height": 20}));
|
||||
let out = run(&mut s, "sheet.export_dxf", json!({}));
|
||||
let es = entities(out["dxf"].as_str().unwrap_or_default());
|
||||
let on = |layer: &str| es.iter().filter(|e| e.1.contains(&(8, layer.to_string()))).count();
|
||||
assert_eq!(on("BEND_UP") + on("BEND_DOWN"), out["bends"].as_u64().unwrap_or(0) as usize);
|
||||
assert!(on("BEND_UP") + on("BEND_DOWN") > 0 && count(&es, "ELLIPSE") == 1 && on("OUTLINE") == es.len() - on("BEND_UP") - on("BEND_DOWN"));
|
||||
}
|
||||
@@ -1519,7 +1519,7 @@ fn flat_dxf_preserves_exact_cutout_entities() {
|
||||
outer: Loop2 { segs: vec![Seg2::Arc { center: Vec2::new(40.0, 30.0), radius: 5.0, start: -PI, sweep: -2.0 * PI }] },
|
||||
holes: Vec::new(),
|
||||
}];
|
||||
let dxf = super::flat_dxf(&sh).unwrap();
|
||||
let dxf = super::flat_dxf(&sh, super::DxfCurves::Exact).unwrap();
|
||||
assert_eq!(dxf.matches("0\nCIRCLE\n").count(), 1, "{dxf}");
|
||||
assert!(!dxf.contains("0\nARC\n"), "a full circle must not become a zero-length ARC");
|
||||
let read = solidcraft_io::read_dxf(&dxf).unwrap();
|
||||
@@ -1531,10 +1531,10 @@ fn flat_dxf_preserves_exact_cutout_entities() {
|
||||
Seg2::Arc { center: Vec2::new(20.0, 10.0), radius: 10.0, start: 0.0, sweep: PI },
|
||||
],
|
||||
};
|
||||
let dxf = super::flat_dxf(&sh).unwrap();
|
||||
let dxf = super::flat_dxf(&sh, super::DxfCurves::Exact).unwrap();
|
||||
assert_eq!(dxf.matches("0\nLINE\n").count(), 5, "four outline lines and one exact cutout line");
|
||||
assert_eq!(dxf.matches("0\nARC\n").count(), 1);
|
||||
assert!(dxf.contains("50\n0.000000\n51\n180.000000\n"));
|
||||
assert!(dxf.contains(" 50\n0.0\n 51\n180.0\n"), "{dxf}");
|
||||
let read = solidcraft_io::read_dxf(&dxf).unwrap();
|
||||
assert_eq!(read.len(), 6);
|
||||
assert!(read.iter().any(|g| matches!(g, solidcraft_io::Geom2::Arc { c, a, b } if *c == Vec2::new(20.0, 10.0) && a.dist(Vec2::new(30.0, 10.0)) < 1e-9 && b.dist(Vec2::new(10.0, 10.0)) < 1e-9)));
|
||||
@@ -2921,7 +2921,11 @@ fn corner_reliefs_of_the_rule_reach_the_part_the_flat_pattern_and_the_dxf() {
|
||||
assert!(arcs.iter().any(|(c, r)| c.dist(Vec2::new(x, y)) < 1e-6 && (r - 4.0).abs() < 1e-6), "({x}, {y}): {arcs:?}");
|
||||
}
|
||||
} else {
|
||||
assert!(arcs.is_empty() && dxf.contains(&format!("{:.6}\n20\n{:.6}\n", s0 + 4.0, s0 + 4.0)), "{arcs:?}");
|
||||
let corner = Vec2::new(s0 + 4.0, s0 + 4.0);
|
||||
let read = solidcraft_io::read_dxf(&dxf).unwrap_or_default();
|
||||
let at_corner =
|
||||
|g: &solidcraft_io::Geom2| matches!(g, solidcraft_io::Geom2::Line(a, b) if a.dist(corner) < 1e-6 || b.dist(corner) < 1e-6);
|
||||
assert!(arcs.is_empty() && read.iter().any(at_corner), "{arcs:?}");
|
||||
}
|
||||
// Unfold and refold keep it, and a cut made folded still goes through: a Ø20 hole in the base.
|
||||
run(&mut s, "sheet.unfold", json!({}));
|
||||
@@ -2992,7 +2996,10 @@ fn a_tear_of_the_rule_reaches_the_part_the_flat_pattern_and_the_dxf() {
|
||||
// The DXF: the outline in straight lines, each cut one of them, and four bend lines.
|
||||
let dxf = run(&mut s, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap_or_default().to_string();
|
||||
assert_eq!(dxf.matches("0\nLINE\n").count(), outline.len() + 4, "{deg}");
|
||||
assert!(dxf.contains(&format!("{:.6}\n20\n{:.6}\n", x_end(s0), s0 - ba)), "{deg}");
|
||||
let cut_end = solidcraft_geom::Vec2::new(x_end(s0), s0 - ba);
|
||||
let read = solidcraft_io::read_dxf(&dxf).unwrap_or_default();
|
||||
let at_cut_end = |g: &solidcraft_io::Geom2| matches!(g, solidcraft_io::Geom2::Line(a, b) if a.dist(cut_end) < 1e-6 || b.dist(cut_end) < 1e-6);
|
||||
assert!(read.iter().any(at_cut_end), "{deg}");
|
||||
// Unfold and refold keep it, and a cut made folded still goes through: a Ø20 hole in the base.
|
||||
run(&mut s, "sheet.unfold", json!({}));
|
||||
assert!((volume(&mut s) - flat0 - want).abs() < 1e-6, "{deg}: unfolded {} - {flat0} vs {want}", volume(&mut s));
|
||||
|
||||
Reference in new issue
Block a user