Convert to Sheet Metal takes bent parts as modelled: radius and chamfer bends, holes (#1314)
A Convert to Sheet Metal bend point can now lie on a bend modelled with its radius (a cylinder tangent to two flat faces) or in the middle of a chamfer; either becomes one bend at the faces' virtual sharp, with the rule's radius. Holes and engravings carry over. Bend points on edges convert as before. Recorded under format 21. Tests in cmd::sheet::convert_tests; the sheet-metal guide is updated.
This commit is contained in:
1 parent
849426e866
commit
d824d36821
7 files changed
+1480
-128
No files matched your search
@@ -38,10 +38,14 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.at("SHEET METAL", "CREATE")
|
||||
.icon("convert_sheet")
|
||||
.params(
|
||||
"body; face: [x,y,z] on its large flat face (a plate of even thickness), or that face's name from model.faces; rule?; \
|
||||
"body; face: [x,y,z] on its large flat face (a plate of even thickness; recesses in one face up to half the thickness deep, \
|
||||
such as an engraved label, become pockets of the sheet), or that face's name from model.faces; rule?; \
|
||||
bends?: [[x,y,z] on sharp edges] (a solid or a shelled body: `face` is the fixed face and the flat faces reached from it \
|
||||
across these edges become the sheet; a shell keeps its thickness, a solid takes the rule's inward; \
|
||||
edges between two of those faces that are no bends are cut open with the rule's gap)",
|
||||
edges between two of those faces that are no bends are cut open with the rule's gap; \
|
||||
a point in the middle of a chamfer between two faces, or on a bend modelled with its radius (a cylinder between \
|
||||
two flat faces tangent to it, either skin), makes it one bend of the rule's radius at their virtual sharp; \
|
||||
the faces' holes become cut-outs and their engravings, up to half the thickness deep, pockets)",
|
||||
),
|
||||
CommandSpec::new("sheet.manage_rules", "Sheet Metal Rules", rules).at("SHEET METAL", "MODIFY").icon("sheet_rules").params(
|
||||
"name (new or existing); thickness?, k_factor?, bend_radius?, relief_width?, relief_depth?, corner_relief?, hem_gap?, gap? (expressions; `Thickness` is the rule's; \
|
||||
@@ -60,7 +64,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
),
|
||||
CommandSpec::new("sheet.export_dxf", "Export Flat Pattern as DXF", export_dxf)
|
||||
.noundo()
|
||||
.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"),
|
||||
.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, and pockets on POCKET_<BOTTOM|TOP|INTERNAL>_Z<from>_TO_<to>_MM"),
|
||||
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"),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Convert to Sheet Metal: solid and shelled bodies with bend edges, and plates read from STEP
|
||||
//! files whose walls come in pieces.
|
||||
//! Convert to Sheet Metal: solid and shelled bodies with bend edges (with holes and round edges in
|
||||
//! their faces), plates read from STEP files whose walls come in pieces, and plates with recesses.
|
||||
|
||||
use std::f64::consts::{FRAC_PI_2, PI, SQRT_2};
|
||||
|
||||
@@ -386,6 +386,231 @@ fn bend_edges_changed_in_an_edit_are_the_ones_bent() {
|
||||
assert!((volume(&mut s) - expect).abs() < 2e-3 * expect, "{} vs {expect}", volume(&mut s));
|
||||
}
|
||||
|
||||
/// A channel 40 wide with walls 30 high (outside sizes), 2 thick and 100 long, its two corners
|
||||
/// drawn as chamfers, as a model without bend radii draws a bend: 4 along each outer face and 2
|
||||
/// along each inner one, the chords of an R 2 bend's outside (R + T) and inside (R).
|
||||
fn chamfered_channel(s: &mut Session) {
|
||||
let pts = [[0, 30], [0, 4], [4, 0], [36, 0], [40, 4], [40, 30], [38, 30], [38, 4], [36, 2], [4, 2], [2, 4], [2, 30]];
|
||||
run(s, "sketch.create", json!({"plane": "XY", "name": "Channel"}));
|
||||
run(s, "sketch.line", json!({"points": pts, "closed": true}));
|
||||
run(s, "sketch.finish", json!({}));
|
||||
run(s, "solid.extrude", json!({"sketch": "Channel", "distance": 100, "body_name": "Channel"}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_chamfer_picked_in_its_middle_is_one_bend_at_the_virtual_sharp() {
|
||||
// The rule's radius bends, not the one the chamfers stand for.
|
||||
let (t, k, r) = (2.0, 0.5, 3.0);
|
||||
let mut s = Session::default();
|
||||
rule(&mut s, "Test", t, k, r, 0.5);
|
||||
chamfered_channel(&mut s);
|
||||
// The middle of each outer chamfer.
|
||||
run(&mut s, "sheet.convert", json!({"body": "Channel", "face": [20, 0, 50], "bends": [[2, 2, 50], [38, 2, 50]]}));
|
||||
let rows = bend_rows(&mut s);
|
||||
assert!(rows.len() == 2 && rows.iter().all(|row| row.0 == 90 && row.1 == "up" && (row.2 - 100.0).abs() < 1e-6), "{rows:?}");
|
||||
// The outside lengths to the virtual sharps less a 90° deduction each:
|
||||
// 30 + 40 + 30 - 2 (2 (R + T) - π/2 (R + K T)) = 100 - 2 (10 - 2π) = 92.5664.
|
||||
let across = 100.0 - 2.0 * deduction(FRAC_PI_2, r, t, k);
|
||||
assert!((across - 92.566371).abs() < 1e-6);
|
||||
assert!(same_size(flat_size(&mut s), (across, 100.0), 1e-6), "{:?} vs {across}", flat_size(&mut s));
|
||||
// The envelope of the sharp corners stays; the chamfers are gone into the bends.
|
||||
let (lo, hi) = bbox(&mut s);
|
||||
assert!(
|
||||
lo.iter().all(|v| v.abs() < 1e-6) && (hi[0] - 40.0).abs() < 1e-6 && (hi[1] - 30.0).abs() < 1e-6 && (hi[2] - 100.0).abs() < 1e-6,
|
||||
"{lo:?} {hi:?}"
|
||||
);
|
||||
// Three flat strips and two quarter rounds on the neutral radius R + T/2.
|
||||
let expect = t * 100.0 * (2.0 * (30.0 - (r + t)) + (40.0 - 2.0 * (r + t)) + PI * (r + t / 2.0));
|
||||
assert!((volume(&mut s) - expect).abs() < 2e-3 * expect, "{} vs {expect}", volume(&mut s));
|
||||
// The design keeps the chamfers' points and reopens to the same sheet.
|
||||
let saved = s.doc.to_json();
|
||||
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&saved).unwrap());
|
||||
assert!(same_size(flat_size(&mut reopened), (across, 100.0), 1e-6), "{:?}", flat_size(&mut reopened));
|
||||
|
||||
// Picked on the inner skin, from the inside of the base: the same sheet, bent down.
|
||||
let mut b = Session::default();
|
||||
rule(&mut b, "Test", t, k, r, 0.5);
|
||||
chamfered_channel(&mut b);
|
||||
run(&mut b, "sheet.convert", json!({"body": "Channel", "face": [20, 2, 50], "bends": [[3, 3, 50], [37, 3, 50]]}));
|
||||
assert!(bend_rows(&mut b).iter().all(|row| row.0 == 90 && row.1 == "down"), "{:?}", bend_rows(&mut b));
|
||||
assert!(same_size(flat_size(&mut b), (across, 100.0), 1e-6), "{:?}", flat_size(&mut b));
|
||||
assert!((volume(&mut b) - expect).abs() < 2e-3 * expect, "{} vs {expect}", volume(&mut b));
|
||||
|
||||
// Points on a chamfer's edges pick those edges, as before: four 45° bends with the
|
||||
// chamfers as narrow faces of the sheet.
|
||||
let mut e = Session::default();
|
||||
rule(&mut e, "Test", t, k, r, 0.5);
|
||||
chamfered_channel(&mut e);
|
||||
run(&mut e, "sheet.convert", json!({"body": "Channel", "face": [20, 0, 50], "bends": [[0, 4, 50], [4, 0, 50], [36, 0, 50], [40, 4, 50]]}));
|
||||
let rows = bend_rows(&mut e);
|
||||
assert!(rows.len() == 4 && rows.iter().all(|row| row.0 == 45), "{rows:?}");
|
||||
// A chamfer and one of its edges at once is refused.
|
||||
let mut f = Session::default();
|
||||
rule(&mut f, "Test", t, k, r, 0.5);
|
||||
chamfered_channel(&mut f);
|
||||
let both = f.execute("sheet.convert", &json!({"body": "Channel", "face": [20, 0, 50], "bends": [[2, 2, 50], [4, 0, 50]]}));
|
||||
let msg = both.err().map(|x| x.to_string()).unwrap_or_default();
|
||||
assert!(msg.contains("not both"), "{msg}");
|
||||
}
|
||||
|
||||
/// A strip `t` thick along the polyline `outer` (its material to the left of the way the line
|
||||
/// runs, as [`section`]), extruded `len` along z as the body `Section`, its corners bent as a
|
||||
/// sheet metal model draws them: an arc of radius `ri` on the inside of each bend and one of
|
||||
/// `ri + t` on the outside, about one axis. Returns, per corner, a point in the middle of its
|
||||
/// outer arc and one in the middle of its inner arc.
|
||||
fn bent_section(s: &mut Session, outer: &[(f64, f64)], t: f64, ri: f64, len: f64) -> Vec<([f64; 3], [f64; 3])> {
|
||||
let n = outer.len();
|
||||
let dir = |i: usize| {
|
||||
let (a, b) = (outer[i], outer[i + 1]);
|
||||
let l = (b.0 - a.0).hypot(b.1 - a.1);
|
||||
((b.0 - a.0) / l, (b.1 - a.1) / l)
|
||||
};
|
||||
let left = |d: (f64, f64)| (-d.1, d.0);
|
||||
let at = |p: (f64, f64), d: (f64, f64), k: f64| (p.0 + d.0 * k, p.1 + d.1 * k);
|
||||
// Per corner: where its arc starts and ends on the line's skin and on the other skin, the
|
||||
// middles of those two arcs, and the middles of its outer and its inner arc.
|
||||
let mut corners = Vec::new();
|
||||
for (i, &c) in outer.iter().enumerate().take(n - 1).skip(1) {
|
||||
let (d0, d1) = (dir(i - 1), dir(i));
|
||||
let (cross, dot) = (d0.0 * d1.1 - d0.1 * d1.0, d0.0 * d1.0 + d0.1 * d1.1);
|
||||
let theta = cross.abs().atan2(dot);
|
||||
// Turning toward the material, this skin is the bend's outside.
|
||||
let (rp, ro) = if cross > 0.0 { (ri + t, ri) } else { (ri, ri + t) };
|
||||
let a = at(c, d0, -rp * (theta / 2.0).tan());
|
||||
let b = at(c, d1, rp * (theta / 2.0).tan());
|
||||
let centre = at(a, left(d0), if cross > 0.0 { rp } else { -rp });
|
||||
let (ai, bi) = (at(a, left(d0), t), at(b, left(d1), t));
|
||||
let l = (c.0 - centre.0).hypot(c.1 - centre.1);
|
||||
let mid = |r: f64| (centre.0 + (c.0 - centre.0) / l * r, centre.1 + (c.1 - centre.1) / l * r);
|
||||
corners.push((a, b, ai, bi, mid(rp), mid(ro), (mid(ri + t), mid(ri))));
|
||||
}
|
||||
let xy = |p: (f64, f64)| [p.0, p.1];
|
||||
run(s, "sketch.create", json!({"plane": "XY", "name": "Section"}));
|
||||
let mut from = outer[0];
|
||||
for &(a, b, _, _, m, ..) in &corners {
|
||||
run(s, "sketch.line", json!({"points": [xy(from), xy(a)]}));
|
||||
run(s, "sketch.arc.three_point", json!({"start": xy(a), "end": xy(b), "through": xy(m)}));
|
||||
from = b;
|
||||
}
|
||||
let (last, first) = (at(outer[n - 1], left(dir(n - 2)), t), at(outer[0], left(dir(0)), t));
|
||||
run(s, "sketch.line", json!({"points": [xy(from), xy(outer[n - 1]), xy(last)]}));
|
||||
from = last;
|
||||
for &(_, _, ai, bi, _, m, _) in corners.iter().rev() {
|
||||
run(s, "sketch.line", json!({"points": [xy(from), xy(bi)]}));
|
||||
run(s, "sketch.arc.three_point", json!({"start": xy(bi), "end": xy(ai), "through": xy(m)}));
|
||||
from = ai;
|
||||
}
|
||||
run(s, "sketch.line", json!({"points": [xy(from), xy(first), xy(outer[0])]}));
|
||||
run(s, "sketch.finish", json!({}));
|
||||
run(s, "solid.extrude", json!({"sketch": "Section", "distance": len, "body_name": "Section"}));
|
||||
let z = len / 2.0;
|
||||
corners.iter().map(|&(.., (m, mi))| ([m.0, m.1, z], [mi.0, mi.1, z])).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bend_modelled_with_its_radius_is_one_bend_of_the_rules_radius() {
|
||||
// An L: a base 60 and a leg 40 to the sharp corner outside, 2 thick and 100 long, its
|
||||
// corner bent R 2 inside and R 4 outside. The rule bends R 3: its radius, not the model's.
|
||||
let (t, k, r) = (2.0, 0.5, 3.0);
|
||||
let l = |s: &mut Session| {
|
||||
rule(s, "Test", t, k, r, 0.5);
|
||||
bent_section(s, &[(0.0, 40.0), (0.0, 0.0), (60.0, 0.0)], t, 2.0, 100.0)[0]
|
||||
};
|
||||
let mut s = Session::default();
|
||||
let (outer, inner) = l(&mut s);
|
||||
run(&mut s, "sheet.convert", json!({"body": "Section", "face": [30, 0, 50], "bends": [outer]}));
|
||||
let rows = bend_rows(&mut s);
|
||||
assert!(rows.len() == 1 && rows[0].0 == 90 && rows[0].1 == "up" && (rows[0].2 - 100.0).abs() < 1e-6, "{rows:?}");
|
||||
let allowance = flat(&mut s)["bends"][0]["allowance"].as_f64().unwrap_or(0.0);
|
||||
assert!((allowance - FRAC_PI_2 * (r + k * t)).abs() < 1e-9, "{allowance}");
|
||||
// The outside lengths to the virtual sharp less one 90° deduction:
|
||||
// 60 + 40 - (2 (R + T) - π/2 (R + K T)) = 100 - (10 - 2π) = 96.2832.
|
||||
let across = 100.0 - deduction(FRAC_PI_2, r, t, k);
|
||||
assert!((across - 96.283185).abs() < 1e-6);
|
||||
assert!(same_size(flat_size(&mut s), (across, 100.0), 1e-6), "{:?} vs {across}", flat_size(&mut s));
|
||||
// The envelope of the sharp corner stays; the round is the rule's, R + T outside.
|
||||
let (lo, hi) = bbox(&mut s);
|
||||
assert!(
|
||||
lo.iter().all(|v| v.abs() < 1e-6) && (hi[0] - 60.0).abs() < 1e-6 && (hi[1] - 40.0).abs() < 1e-6 && (hi[2] - 100.0).abs() < 1e-6,
|
||||
"{lo:?} {hi:?}"
|
||||
);
|
||||
// Two flat strips and a quarter round on the neutral radius R + T/2.
|
||||
let expect = t * 100.0 * ((60.0 - (r + t)) + (40.0 - (r + t)) + FRAC_PI_2 * (r + t / 2.0));
|
||||
assert!((volume(&mut s) - expect).abs() < 2e-3 * expect, "{} vs {expect}", volume(&mut s));
|
||||
// The design keeps the cylinder's point and reopens to the same sheet.
|
||||
let saved = s.doc.to_json();
|
||||
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&saved).unwrap());
|
||||
assert!(same_size(flat_size(&mut reopened), (across, 100.0), 1e-6), "{:?}", flat_size(&mut reopened));
|
||||
|
||||
// The inner cylinder is the same bend; and from the inside of the base, the sheet's other
|
||||
// skin is the fixed one, the bend turning down from it, picked on either cylinder: the
|
||||
// inside lengths to the inner sharp, 58 + 38, less 2 R plus the allowance, the same blank.
|
||||
assert!((58.0 + 38.0 - 2.0 * r + allowance - across).abs() < 1e-9);
|
||||
for (face, bend, direction) in [([30.0, 0.0, 50.0], inner, "up"), ([30.0, 2.0, 50.0], outer, "down"), ([30.0, 2.0, 50.0], inner, "down")] {
|
||||
let mut b = Session::default();
|
||||
l(&mut b);
|
||||
run(&mut b, "sheet.convert", json!({"body": "Section", "face": face, "bends": [bend]}));
|
||||
assert!(bend_rows(&mut b).iter().all(|row| row.0 == 90 && row.1 == direction), "{face:?} {bend:?}: {:?}", bend_rows(&mut b));
|
||||
assert!(same_size(flat_size(&mut b), (across, 100.0), 1e-6), "{face:?} {bend:?}: {:?}", flat_size(&mut b));
|
||||
assert!((volume(&mut b) - expect).abs() < 2e-3 * expect, "{face:?} {bend:?}: {} vs {expect}", volume(&mut b));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_channel_with_its_bends_modelled_folds_back_into_itself_under_its_own_radius() {
|
||||
// A channel 40 wide with walls 30 high to the sharp corners outside, 2 thick and 100 long,
|
||||
// bent R 2 inside, R 4 outside. Under a rule of that radius the sheet is the model.
|
||||
let (t, k, r) = (2.0, 0.44, 2.0);
|
||||
let channel = |s: &mut Session| {
|
||||
rule(s, "Test", t, k, r, 0.5);
|
||||
bent_section(s, &[(0.0, 30.0), (0.0, 0.0), (40.0, 0.0), (40.0, 30.0)], t, r, 100.0)
|
||||
};
|
||||
let mut s = Session::default();
|
||||
let corners = channel(&mut s);
|
||||
let solid = volume(&mut s);
|
||||
// Two flat walls, the base and two quarter rounds on the neutral radius R + T/2.
|
||||
let expect = t * 100.0 * (2.0 * (30.0 - (r + t)) + (40.0 - 2.0 * (r + t)) + PI * (r + t / 2.0));
|
||||
assert!((solid - expect).abs() < 2e-3 * expect, "{solid} vs {expect}");
|
||||
// One bend picked on its outer cylinder, the other on its inner one.
|
||||
run(&mut s, "sheet.convert", json!({"body": "Section", "face": [20, 0, 50], "bends": [corners[0].0, corners[1].1]}));
|
||||
let rows = bend_rows(&mut s);
|
||||
assert!(rows.len() == 2 && rows.iter().all(|row| row.0 == 90 && row.1 == "up" && (row.2 - 100.0).abs() < 1e-6), "{rows:?}");
|
||||
// 30 + 40 + 30 less two deductions, 2 (R + T) - π/2 (R + K T) each.
|
||||
let across = 100.0 - 2.0 * deduction(FRAC_PI_2, r, t, k);
|
||||
assert!(same_size(flat_size(&mut s), (across, 100.0), 1e-6), "{:?} vs {across}", flat_size(&mut s));
|
||||
// The same solid as before: its volume and its box.
|
||||
assert!((volume(&mut s) - solid).abs() < 1e-3 * solid, "{} vs {solid}", volume(&mut s));
|
||||
let (lo, hi) = bbox(&mut s);
|
||||
assert!(
|
||||
lo.iter().all(|v| v.abs() < 1e-6) && (hi[0] - 40.0).abs() < 1e-6 && (hi[1] - 30.0).abs() < 1e-6 && (hi[2] - 100.0).abs() < 1e-6,
|
||||
"{lo:?} {hi:?}"
|
||||
);
|
||||
// Unfold and refold keep it.
|
||||
run(&mut s, "sheet.unfold", json!({}));
|
||||
let (lo, hi) = bbox(&mut s);
|
||||
let flat_box = [(hi[0] - lo[0], hi[1] - lo[1]), (hi[0] - lo[0], hi[2] - lo[2]), (hi[1] - lo[1], hi[2] - lo[2])];
|
||||
assert!(flat_box.iter().any(|b| same_size(*b, (across, 100.0), 1e-6)), "{lo:?} {hi:?}");
|
||||
run(&mut s, "sheet.refold", json!({}));
|
||||
assert!((volume(&mut s) - solid).abs() < 1e-3 * solid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_filleted_edge_of_a_solid_block_is_a_bend() {
|
||||
// A block's bottom edge rounded R 5: the faces either side meet at the old sharp edge,
|
||||
// the rule's thickness taken inward, an L with a 100 base and a 30 wall outside.
|
||||
let (t, k, r) = (2.0, 0.44, 2.0);
|
||||
let mut s = Session::default();
|
||||
rule(&mut s, "Test", t, k, r, 0.5);
|
||||
run(&mut s, "solid.box", json!({"length": 100, "width": 60, "height": 30, "body_name": "Block"}));
|
||||
run(&mut s, "solid.fillet", json!({"edges": [[100, 30, 0]], "radius": 5}));
|
||||
let h = 5.0 * (1.0 - 0.5f64.sqrt());
|
||||
run(&mut s, "sheet.convert", json!({"body": "Block", "face": [50, 30, 0], "bends": [[100.0 - h, 30.0, h]]}));
|
||||
let rows = bend_rows(&mut s);
|
||||
assert!(rows.len() == 1 && rows[0].0 == 90 && rows[0].1 == "up" && (rows[0].2 - 60.0).abs() < 1e-6, "{rows:?}");
|
||||
let across = 130.0 - deduction(FRAC_PI_2, r, t, k);
|
||||
assert!(same_size(flat_size(&mut s), (across, 60.0), 1e-6), "{:?} vs {across}", flat_size(&mut s));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_flanges_and_cuts_work_on_a_converted_sheet() {
|
||||
let mut s = Session::default();
|
||||
@@ -803,3 +1028,467 @@ fn a_long_thin_strip_keeps_its_small_round_holes() {
|
||||
assert_eq!(dxf_entities(&mut s), (4, 0, 4), "{l}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A 60 x 40 x 1 plate on XY.
|
||||
fn plate(s: &mut Session) {
|
||||
run(s, "solid.box", json!({"length": 60, "width": 40, "height": 1, "body_name": "Plate"}));
|
||||
}
|
||||
|
||||
/// The plate with what `draw` sketches on its top cut `depth` deep into it (the profiles at
|
||||
/// `profiles`, all when none).
|
||||
fn engraved_plate(s: &mut Session, depth: f64, draw: impl Fn(&mut Session), profiles: &[[f64; 2]]) {
|
||||
plate(s);
|
||||
run(s, "sketch.create", json!({"plane": "XY", "offset": 1, "name": "Label"}));
|
||||
draw(s);
|
||||
run(s, "sketch.finish", json!({}));
|
||||
let mut cut = json!({"sketch": "Label", "distance": depth, "direction": "negative", "operation": "cut"});
|
||||
if !profiles.is_empty() {
|
||||
cut["profiles"] = profiles.iter().map(|p| json!({"point": p})).collect();
|
||||
}
|
||||
run(s, "solid.extrude", cut);
|
||||
}
|
||||
|
||||
fn label_rectangle(s: &mut Session) {
|
||||
run(s, "sketch.rectangle.two_point", json!({"p0": [10, 10], "p1": [30, 20]}));
|
||||
}
|
||||
|
||||
fn contains(s: &Session, p: [f64; 3]) -> bool {
|
||||
s.model.state().bodies.first().is_some_and(|b| b.body.contains(solidcraft_geom::Vec3::new(p[0], p[1], p[2])))
|
||||
}
|
||||
|
||||
/// The LINE entities a DXF draws on `layer` (group codes and values alternate, a line each).
|
||||
fn lines_on(dxf: &str, layer: &str) -> usize {
|
||||
let lines: Vec<&str> = dxf.lines().map(str::trim).collect();
|
||||
let mut in_line = false;
|
||||
let mut count = 0;
|
||||
for pair in lines.chunks(2) {
|
||||
match pair {
|
||||
["0", kind] => in_line = *kind == "LINE",
|
||||
["8", name] if in_line && *name == layer => count += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
/// A plate with an engraving converts: the engraving is a pocket of the sheet, so the sheet keeps
|
||||
/// the plate's shape, and its outline is the flat pattern's pocket, on the DXF's pocket layer.
|
||||
/// Picked on the engraved face, the pocket starts from the sheet's z = 0 (the face picked);
|
||||
/// picked on the other face, it ends at z = T.
|
||||
#[test]
|
||||
fn a_plate_with_an_engraving_converts_with_the_engraving_as_a_pocket() {
|
||||
for (pick, layer) in [([45.0, 30.0, 1.0], "POCKET_BOTTOM_Z0.000000_TO_0.100000_MM"), ([45.0, 30.0, 0.0], "POCKET_TOP_Z0.900000_TO_1.000000_MM")] {
|
||||
let mut s = Session::default();
|
||||
engraved_plate(&mut s, 0.1, label_rectangle, &[]);
|
||||
run(&mut s, "sheet.convert", json!({"body": "Plate", "face": pick}));
|
||||
assert!(s.model.results.iter().all(|r| r.error.is_none() && r.warning.is_none()), "{pick:?}: {:?}", s.model.results);
|
||||
// By hand: 60·40·1 less the engraving's 20·10·0.1; the faces' 2·60·40 + 2·(60 + 40)·1 and
|
||||
// the engraving's walls, 2·(20 + 10)·0.1.
|
||||
let m = run(&mut s, "inspect.measure", json!({}));
|
||||
assert!((m["total"]["volume_mm3"].as_f64().unwrap_or(f64::NAN) - 2380.0).abs() < 1e-6, "{pick:?}: {m}");
|
||||
assert!((m["total"]["area_mm2"].as_f64().unwrap_or(f64::NAN) - 5006.0).abs() < 1e-6, "{pick:?}: {m}");
|
||||
// It stays in the face it was cut into (the converted plate is as thick, wherever it lies).
|
||||
let (lo, hi) = bbox(&mut s);
|
||||
assert!((hi[2] - lo[2] - 1.0).abs() < 1e-9, "{pick:?}: {lo:?} {hi:?}");
|
||||
assert!(!contains(&s, [20.0, 15.0, hi[2] - 0.05]) && contains(&s, [20.0, 15.0, hi[2] - 0.15]), "{pick:?}");
|
||||
assert!(contains(&s, [45.0, 30.0, hi[2] - 0.05]), "{pick:?}");
|
||||
// The flat pattern: the whole plate, no cut-out, and the engraving as a pocket 0.1 deep.
|
||||
let f = flat(&mut s);
|
||||
assert!(same_size(flat_size(&mut s), (60.0, 40.0), 1e-9), "{f}");
|
||||
assert_eq!(f["cutouts"], json!([]), "{f}");
|
||||
let pockets = f["pockets"].as_array().cloned().unwrap_or_default();
|
||||
assert_eq!(pockets.len(), 1, "{f}");
|
||||
assert!((pockets[0]["depth_mm"].as_f64().unwrap_or(f64::NAN) - 0.1).abs() < 1e-9, "{f}");
|
||||
let corners: Vec<(f64, f64)> =
|
||||
pockets[0]["outline"].as_array().unwrap().iter().map(|p| (p[0].as_f64().unwrap(), p[1].as_f64().unwrap())).collect();
|
||||
let span = |k: fn(&(f64, f64)) -> f64| corners.iter().map(k).fold(f64::MIN, f64::max) - corners.iter().map(k).fold(f64::MAX, f64::min);
|
||||
assert!(same_size((span(|p| p.0), span(|p| p.1)), (20.0, 10.0), 1e-9), "{f}");
|
||||
// The DXF: the plate's outline and the engraving's, four lines each, on their layers.
|
||||
let dxf = run(&mut s, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap().to_string();
|
||||
assert_eq!((lines_on(&dxf, "OUTLINE"), lines_on(&dxf, layer)), (4, 4), "{pick:?}: {dxf}");
|
||||
// Nothing is stored for it: reopened, the design builds the same sheet.
|
||||
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
|
||||
assert!((volume(&mut reopened) - 2380.0).abs() < 1e-6);
|
||||
assert_eq!(flat(&mut reopened), f);
|
||||
}
|
||||
}
|
||||
|
||||
/// A plate large enough that its tessellation tolerance (1/1000 of its size, 0.72 mm here) is
|
||||
/// more than its engraving's depth keeps the engraving too.
|
||||
#[test]
|
||||
fn a_large_plate_keeps_an_engraving_shallower_than_its_tessellation_tolerance() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 600, "width": 400, "height": 1, "body_name": "Plate"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "offset": 1, "name": "Label"}));
|
||||
label_rectangle(&mut s);
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Label", "distance": 0.1, "direction": "negative", "operation": "cut"}));
|
||||
for pick in [[450.0, 300.0, 0.0], [450.0, 300.0, 1.0]] {
|
||||
let mut s = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
|
||||
run(&mut s, "sheet.convert", json!({"body": "Plate", "face": pick}));
|
||||
// 600·400·1 less the engraving's 20·10·0.1.
|
||||
assert!((volume(&mut s) - 239_980.0).abs() < 1e-3, "{pick:?}: {}", volume(&mut s));
|
||||
assert_eq!(flat(&mut s)["pockets"].as_array().map(Vec::len), Some(1), "{pick:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// An engraved ring (an "O": a 5 mm circle round a 3 mm island) is one pocket with a hole; the
|
||||
/// island stays at full thickness.
|
||||
#[test]
|
||||
fn an_engraved_ring_converts_as_a_pocket_round_its_island() {
|
||||
let mut s = Session::default();
|
||||
engraved_plate(
|
||||
&mut s,
|
||||
0.1,
|
||||
|s| {
|
||||
run(s, "sketch.circle.center", json!({"center": [20, 20], "radius": 5}));
|
||||
run(s, "sketch.circle.center", json!({"center": [20, 20], "radius": 3}));
|
||||
},
|
||||
&[[24.0, 20.0]],
|
||||
);
|
||||
// The extrude took the ring between the circles, not the island.
|
||||
assert!((volume(&mut s) - (2400.0 - 0.1 * PI * (25.0 - 9.0))).abs() < 1e-3, "{}", volume(&mut s));
|
||||
run(&mut s, "sheet.convert", json!({"body": "Plate", "face": [45, 30, 1]}));
|
||||
// The ring's circles are followed within 0.01 mm (1/100 of the thickness): its area within
|
||||
// 2/3 of that times the circumferences.
|
||||
let want = 2400.0 - 0.1 * PI * (25.0 - 9.0);
|
||||
assert!((volume(&mut s) - want).abs() < 0.1 * 0.01 * 2.0 / 3.0 * 2.0 * PI * 8.0, "{} vs {want}", volume(&mut s));
|
||||
let (_, hi) = bbox(&mut s);
|
||||
assert!(!contains(&s, [24.0, 20.0, hi[2] - 0.05]) && contains(&s, [20.0, 20.0, hi[2] - 0.05]));
|
||||
let f = flat(&mut s);
|
||||
assert_eq!(f["pockets"].as_array().map(Vec::len), Some(1), "{f}");
|
||||
assert_eq!(f["pockets"][0]["cutouts"].as_array().map(Vec::len), Some(1), "{f}");
|
||||
}
|
||||
|
||||
/// What a plate's recesses can't be is refused with the reason, and leaves the design as it was:
|
||||
/// a recess deeper than half the plate, recesses in both faces, a plate raised in places (its
|
||||
/// recesses larger than the face they are in); a plate with a boss is no plate, as before. A bend
|
||||
/// across a kept engraving is refused.
|
||||
#[test]
|
||||
fn recesses_a_plate_cannot_keep_are_refused_with_the_reason() {
|
||||
let err = |s: &mut Session, face: [f64; 3]| {
|
||||
s.execute("sheet.convert", &json!({"body": "Plate", "face": face})).err().map(|e| e.to_string()).unwrap_or_default()
|
||||
};
|
||||
let mut s = Session::default();
|
||||
engraved_plate(&mut s, 0.6, label_rectangle, &[]);
|
||||
let e = err(&mut s, [45.0, 30.0, 1.0]);
|
||||
assert!(e.contains("0.600 mm deep, more than half the plate's 1.000 mm"), "{e}");
|
||||
let mut s = Session::default();
|
||||
engraved_plate(&mut s, 0.1, label_rectangle, &[]);
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Under"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [40, 20], "p1": [50, 30]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Under", "distance": 0.1, "operation": "cut"}));
|
||||
let e = err(&mut s, [45.0, 10.0, 1.0]);
|
||||
assert!(e.contains("recesses in both faces"), "{e}");
|
||||
// A recess 0.3 mm deep leaving a 2 mm rim: rather a plate 0.3 mm thinner with a raised rim.
|
||||
let mut s = Session::default();
|
||||
engraved_plate(
|
||||
&mut s,
|
||||
0.3,
|
||||
|s| {
|
||||
run(s, "sketch.rectangle.two_point", json!({"p0": [2, 2], "p1": [58, 38]}));
|
||||
},
|
||||
&[],
|
||||
);
|
||||
let e = err(&mut s, [1.0, 1.0, 1.0]);
|
||||
assert!(e.contains("larger than the face they are in"), "{e}");
|
||||
// A 0.3 mm boss on the plate: the plate's top round it lies in no opening of the boss's top.
|
||||
let mut s = Session::default();
|
||||
plate(&mut s);
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "offset": 1, "name": "Boss"}));
|
||||
label_rectangle(&mut s);
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Boss", "distance": 0.3, "operation": "join"}));
|
||||
let e = err(&mut s, [20.0, 15.0, 1.3]);
|
||||
assert!(e.contains("not a plate of even thickness"), "{e}");
|
||||
assert!(s.model.state().sheets.is_empty());
|
||||
// A fold across a kept engraving; one clear of it bends.
|
||||
let mut s = Session::default();
|
||||
engraved_plate(&mut s, 0.1, label_rectangle, &[]);
|
||||
run(&mut s, "sheet.convert", json!({"body": "Plate", "face": [45, 30, 1]}));
|
||||
let (_, hi) = bbox(&mut s);
|
||||
let fold = |x: f64| json!({"points": [[x, 0, hi[2]], [x, 40, hi[2]]], "angle": 90});
|
||||
let e = s.execute("sheet.fold", &fold(20.0)).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(e.contains("crosses a bend zone"), "{e}");
|
||||
run(&mut s, "sheet.fold", fold(45.0));
|
||||
}
|
||||
|
||||
/// A bent part converted without bend edges is no plate, as before, whichever face is picked:
|
||||
/// the faces of its legs inside its extent are no recesses. An L of two 2 mm plates at 90° (its
|
||||
/// legs' faces lie beside its other faces, in no opening of them), a trapezoid section with
|
||||
/// returns (faces of its legs on both sides of its base) and a tapered tray (its floor lies in
|
||||
/// the opening of its rim, but filled it is no plate: its walls lean out).
|
||||
#[test]
|
||||
fn a_bent_part_without_bend_edges_is_still_no_plate() {
|
||||
let err = |s: &mut Session, body: &str, face: [f64; 3]| {
|
||||
s.execute("sheet.convert", &json!({"body": body, "face": face})).err().map(|e| e.to_string()).unwrap_or_default()
|
||||
};
|
||||
let no_plate = "not supported yet: converting bodies that are not a plate of even thickness without bend edges";
|
||||
let mut s = Session::default();
|
||||
section(&mut s, &[(0.0, 30.0), (0.0, 0.0), (50.0, 0.0)], 2.0, 40.0);
|
||||
for pick in [[25.0, 0.0, 20.0], [25.0, 2.0, 20.0], [1.0, 30.0, 20.0], [0.0, 15.0, 20.0], [2.0, 15.0, 20.0], [50.0, 1.0, 20.0]] {
|
||||
let e = err(&mut s, "Section", pick);
|
||||
assert!(e.contains(no_plate), "{pick:?}: {e}");
|
||||
}
|
||||
let mut s = Session::default();
|
||||
trapezoid_section(&mut s, 2.0);
|
||||
for pick in [[0.0, 0.0, 150.0], [0.0, 2.0, 150.0]] {
|
||||
let e = err(&mut s, "Section", pick);
|
||||
assert!(e.contains(no_plate), "{pick:?}: {e}");
|
||||
}
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Floor"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [200, 120]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Floor", "distance": 40, "taper": "18 deg", "body_name": "Tray"}));
|
||||
run(&mut s, "solid.shell", json!({"faces": [[100, 60, 40]], "thickness": 2}));
|
||||
let e = err(&mut s, "Tray", [100.0, 60.0, 0.0]);
|
||||
assert!(e.contains(no_plate), "{e}");
|
||||
assert!(s.model.state().sheets.is_empty());
|
||||
}
|
||||
|
||||
/// The flat pattern's cut-outs that are no whole circle (slots), each by the middle of its arcs'
|
||||
/// centres.
|
||||
fn flat_slots(s: &mut Session) -> Vec<[f64; 2]> {
|
||||
let shapes = flat(s)["cutout_shapes"].as_array().cloned().unwrap_or_default();
|
||||
let centre = |segs: &Vec<Value>| {
|
||||
let c: Vec<[f64; 2]> = segs.iter().filter_map(|g| Some([g["arc"]["center"][0].as_f64()?, g["arc"]["center"][1].as_f64()?])).collect();
|
||||
let n = c.len().max(1) as f64;
|
||||
[c.iter().map(|p| p[0]).sum::<f64>() / n, c.iter().map(|p| p[1]).sum::<f64>() / n]
|
||||
};
|
||||
shapes.iter().filter_map(|x| x["segments"].as_array()).map(centre).collect()
|
||||
}
|
||||
|
||||
/// A U channel 40 long (along z) and `t` thick: a web 100 wide on y = 0 and two legs 30 high,
|
||||
/// outside lengths to the sharp corners. Returns a point on each outside corner (the bends).
|
||||
fn channel(s: &mut Session, t: f64) -> Vec<[f64; 3]> {
|
||||
section(s, &[(0.0, 30.0), (0.0, 0.0), (100.0, 0.0), (100.0, 30.0)], t, 40.0)
|
||||
}
|
||||
|
||||
/// The channel folded with two 90° bends, less `cut` mm² of its faces: the web and the legs
|
||||
/// between their setbacks R + T, and two quarter rounds on the neutral radius R + T/2.
|
||||
fn channel_volume(t: f64, r: f64, cut: f64) -> f64 {
|
||||
t * (40.0 * ((100.0 - 2.0 * (r + t)) + 2.0 * (30.0 - (r + t)) + PI * (r + t / 2.0)) - cut)
|
||||
}
|
||||
|
||||
/// Holes and a slot in the faces of a channel converted by its bend edges are cut-outs of the
|
||||
/// flat pattern, where the bend allowance puts them: in the fixed face (the web) and in the bent
|
||||
/// ones (the legs). With bend edges, such faces were refused ("faces with curved edges").
|
||||
#[test]
|
||||
fn holes_and_a_slot_in_a_channels_faces_land_where_the_bends_put_them() {
|
||||
let (t, k, r) = (2.0, 0.4, 2.0);
|
||||
let mut s = Session::default();
|
||||
rule(&mut s, "Test", t, k, r, 0.5);
|
||||
let bends = channel(&mut s, t);
|
||||
// Two 6 mm holes in the web, 30 from either outside corner; one in the right leg and a slot
|
||||
// 6 wide and 20 long in the left one, each centred 18 up the leg's outside.
|
||||
run(&mut s, "solid.hole", json!({"position": [30, 0, 20], "diameter": 6}));
|
||||
run(&mut s, "solid.hole", json!({"position": [70, 0, 20], "diameter": 6}));
|
||||
run(&mut s, "solid.hole", json!({"position": [100, 18, 20], "diameter": 6, "depth": 3}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "YZ", "name": "Slot"}));
|
||||
run(&mut s, "sketch.slot.overall", json!({"p0": [18, 10], "p1": [18, 30], "width": 6}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Slot", "distance": 3, "operation": "cut"}));
|
||||
run(&mut s, "sheet.convert", json!({"body": "Section", "face": [50, 0, 5], "bends": bends}));
|
||||
let rows = bend_rows(&mut s);
|
||||
assert!(rows.len() == 2 && rows.iter().all(|row| row.0 == 90 && row.1 == "up" && (row.2 - 40.0).abs() < 1e-9), "{rows:?}");
|
||||
// T 2, R 2, K 0.4: BA = π/2 (R + K T) = 1.4 π, BD = 2 (R + T) - BA = 8 - 1.4 π = 3.6018; the
|
||||
// flat is the outside lengths less a BD per bend.
|
||||
let bd = deduction(FRAC_PI_2, r, t, k);
|
||||
assert!((bd - (8.0 - 1.4 * PI)).abs() < 1e-12);
|
||||
let across = 160.0 - 2.0 * bd;
|
||||
assert!(same_size(flat_size(&mut s), (across, 40.0), 1e-6), "{:?} vs {across}", flat_size(&mut s));
|
||||
// On one line across the flat: the right leg's hole, the web's two, 40 apart, and the slot.
|
||||
// From a leg's to the web's nearer one: 18 + 30 along the outside to the sharp, less the BD.
|
||||
let (circles, slots) = (flat_circles(&mut s), flat_slots(&mut s));
|
||||
assert!(circles.len() == 3 && circles.iter().all(|(radius, _)| (radius - 3.0).abs() < 1e-9) && slots.len() == 1, "{circles:?} {slots:?}");
|
||||
let mut centres: Vec<[f64; 2]> = circles.iter().map(|c| c.1).chain(slots).collect();
|
||||
centres.sort_by(|a, b| a[0].total_cmp(&b[0]));
|
||||
let gaps: Vec<f64> = centres.windows(2).map(|w| w[1][0] - w[0][0]).collect();
|
||||
assert!(centres.iter().all(|c| (c[1] - centres[0][1]).abs() < 1e-6), "{centres:?}");
|
||||
assert!(gaps.len() == 3 && gaps.iter().zip([48.0 - bd, 40.0, 48.0 - bd]).all(|(g, want)| (g - want).abs() < 1e-6), "{gaps:?}");
|
||||
// Folded, flat and folded again, less three holes of radius 3 and the slot (14 x 6 and its
|
||||
// round ends).
|
||||
let cut = 3.0 * PI * 9.0 + 14.0 * 6.0 + PI * 9.0;
|
||||
let v = volume(&mut s);
|
||||
let want = channel_volume(t, r, cut);
|
||||
assert!((v - want).abs() < 1e-4 * want, "{v} vs {want}");
|
||||
run(&mut s, "sheet.unfold", json!({}));
|
||||
let want = t * (across * 40.0 - cut);
|
||||
assert!((volume(&mut s) - want).abs() < 1e-4 * want, "{} vs {want}", volume(&mut s));
|
||||
run(&mut s, "sheet.refold", json!({}));
|
||||
assert!((volume(&mut s) - v).abs() < 1e-6 * v);
|
||||
// The DXF: a CIRCLE per hole, and the slot's round ends as ARCs.
|
||||
let (_, arcs, holes) = dxf_entities(&mut s);
|
||||
assert_eq!((arcs, holes), (2, 3));
|
||||
}
|
||||
|
||||
/// Round edges of a bent face stay arcs: the legs' free corners rounded by 5, and a notch of
|
||||
/// radius 4 in one leg's free edge. They were refused too.
|
||||
#[test]
|
||||
fn rounded_corners_and_a_notch_on_a_leg_stay_arcs_in_the_flat() {
|
||||
let (t, k, r) = (2.0, 0.4, 2.0);
|
||||
let mut s = Session::default();
|
||||
rule(&mut s, "Test", t, k, r, 0.5);
|
||||
let bends = channel(&mut s, t);
|
||||
run(&mut s, "solid.fillet", json!({"edges": [[1, 30, 0], [1, 30, 40], [99, 30, 0], [99, 30, 40]], "radius": 5}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "YZ", "name": "Notch"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [30, 20], "radius": 4}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Notch", "distance": 3, "operation": "cut"}));
|
||||
run(&mut s, "sheet.convert", json!({"body": "Section", "face": [50, 0, 5], "bends": bends}));
|
||||
let across = 160.0 - 2.0 * deduction(FRAC_PI_2, r, t, k);
|
||||
assert!(same_size(flat_size(&mut s), (across, 40.0), 1e-6), "{:?} vs {across}", flat_size(&mut s));
|
||||
// Each corner takes (1 - π/4) 5², the notch half a disc of radius 4.
|
||||
let cut = 4.0 * (1.0 - PI / 4.0) * 25.0 + PI * 16.0 / 2.0;
|
||||
let v = volume(&mut s);
|
||||
let want = channel_volume(t, r, cut);
|
||||
assert!((v - want).abs() < 1e-4 * want, "{v} vs {want}");
|
||||
run(&mut s, "sheet.unfold", json!({}));
|
||||
let want = t * (across * 40.0 - cut);
|
||||
assert!((volume(&mut s) - want).abs() < 1e-4 * want, "{} vs {want}", volume(&mut s));
|
||||
run(&mut s, "sheet.refold", json!({}));
|
||||
assert!((volume(&mut s) - v).abs() < 1e-6 * v);
|
||||
// The DXF: a quarter ARC at each corner and a half one at the notch, no cut-outs.
|
||||
let (_, arcs, holes) = dxf_entities(&mut s);
|
||||
assert_eq!((arcs, holes), (5, 0));
|
||||
}
|
||||
|
||||
/// A hole that reaches into a bend has no one place in the flat, and a pocket deeper than half
|
||||
/// the sheet is no engraving: both are refused, by their places. (The web's flat panel starts
|
||||
/// R + T = 4 from the sharp; the hole reaches from 2 to 8. The pocket is 1.5 deep in the 2 thick
|
||||
/// web.)
|
||||
#[test]
|
||||
fn a_hole_reaching_into_a_bend_and_a_pocket_are_refused_by_their_places() {
|
||||
let convert = |cut: &[(&str, Value)]| {
|
||||
let mut s = Session::default();
|
||||
rule(&mut s, "Test", 2.0, 0.4, 2.0, 0.5);
|
||||
let bends = channel(&mut s, 2.0);
|
||||
for (id, p) in cut {
|
||||
run(&mut s, id, p.clone());
|
||||
}
|
||||
let e = s.execute("sheet.convert", &json!({"body": "Section", "face": [50, 0, 5], "bends": bends})).err();
|
||||
assert!(s.model.state().sheets.is_empty());
|
||||
e.map(|e| e.to_string()).unwrap_or_default()
|
||||
};
|
||||
let e = convert(&[("solid.hole", json!({"position": [5, 0, 20], "diameter": 6}))]);
|
||||
assert!(e.contains("the hole at (5.000, 0.000, 20.000) reaches into a bend"), "{e}");
|
||||
let e = convert(&[
|
||||
("sketch.create", json!({"plane": "XZ", "name": "Pocket"})),
|
||||
("sketch.rectangle.two_point", json!({"p0": [40, -15], "p1": [60, -25]})),
|
||||
("sketch.finish", json!({})),
|
||||
("solid.extrude", json!({"sketch": "Pocket", "distance": 1.5, "operation": "cut"})),
|
||||
]);
|
||||
assert!(e.contains("the pocket at (50.000, 0.000, 20.000) in a face of the sheet ends 1.500 deep, more than half the sheet's 2.000"), "{e}");
|
||||
}
|
||||
|
||||
/// The bounding box (low corner, high corner) of a flat pattern pocket's outline.
|
||||
fn pocket_box(p: &Value) -> ([f64; 2], [f64; 2]) {
|
||||
let pts: Vec<[f64; 2]> = p["outline"].as_array().into_iter().flatten().filter_map(|q| Some([q[0].as_f64()?, q[1].as_f64()?])).collect();
|
||||
let lo = [pts.iter().map(|q| q[0]).fold(f64::MAX, f64::min), pts.iter().map(|q| q[1]).fold(f64::MAX, f64::min)];
|
||||
let hi = [pts.iter().map(|q| q[0]).fold(f64::MIN, f64::max), pts.iter().map(|q| q[1]).fold(f64::MIN, f64::max)];
|
||||
(lo, hi)
|
||||
}
|
||||
|
||||
/// A bent part as another system models it, its bends with their radius, with what its faces
|
||||
/// carry: a channel 80 wide with legs 30 high (outside, to the sharp corners), 2 thick, 60 long,
|
||||
/// bent R 2 inside and R 4 outside, with a 6 mm hole 18 up its right leg, a label 20 x 10
|
||||
/// engraved 0.5 deep into the outside of its web and one 10 x 20 into the inside of its left leg.
|
||||
/// Converted from the outside of the web by its outer cylinders under a rule of the model's
|
||||
/// radius, the sheet is the model again: the hole is a cut-out where the bend allowance puts it,
|
||||
/// and each label is a pocket on its own skin of the sheet.
|
||||
#[test]
|
||||
fn a_channel_with_its_bends_modelled_keeps_its_hole_and_engravings_on_either_skin() {
|
||||
let (t, k, r) = (2.0, 0.4, 2.0);
|
||||
let mut s = Session::default();
|
||||
rule(&mut s, "Test", t, k, r, 0.5);
|
||||
let corners = bent_section(&mut s, &[(0.0, 30.0), (0.0, 0.0), (80.0, 0.0), (80.0, 30.0)], t, r, 60.0);
|
||||
run(&mut s, "solid.hole", json!({"position": [80, 18, 20], "diameter": 6, "depth": 3}));
|
||||
// The web's label from its outside (y = 0) in, the leg's from its inside (x = 2) out.
|
||||
run(&mut s, "sketch.create", json!({"plane": "XZ", "name": "WebLabel"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [30, -35], "p1": [50, -45]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "WebLabel", "distance": 0.5, "operation": "cut"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "YZ", "offset": 2, "name": "LegLabel"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [12, 10], "p1": [22, 30]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "LegLabel", "distance": 0.5, "direction": "negative", "operation": "cut"}));
|
||||
// Two flat legs, the web and two quarter rounds on the neutral radius R + T/2, less the hole
|
||||
// and the labels.
|
||||
let cut = 9.0 * PI * t + 2.0 * 200.0 * 0.5;
|
||||
let solid = volume(&mut s);
|
||||
let model = t * 60.0 * (2.0 * (30.0 - (r + t)) + (80.0 - 2.0 * (r + t)) + PI * (r + t / 2.0)) - cut;
|
||||
assert!((solid - model).abs() < 2e-3 * model, "{solid} vs {model}");
|
||||
run(&mut s, "sheet.convert", json!({"body": "Section", "face": [65, 0, 10], "bends": [corners[0].0, corners[1].0]}));
|
||||
let rows = bend_rows(&mut s);
|
||||
assert!(rows.len() == 2 && rows.iter().all(|row| row.0 == 90 && row.1 == "up" && (row.2 - 60.0).abs() < 1e-6), "{rows:?}");
|
||||
// 30 + 80 + 30 less two deductions; the sheet is the solid again.
|
||||
let bd = deduction(FRAC_PI_2, r, t, k);
|
||||
let across = 140.0 - 2.0 * bd;
|
||||
assert!(same_size(flat_size(&mut s), (across, 60.0), 1e-6), "{:?} vs {across}", flat_size(&mut s));
|
||||
let v = volume(&mut s);
|
||||
assert!((v - solid).abs() < 1e-3 * solid, "{v} vs {solid}");
|
||||
// The labels stay in their skins: the web's in its outside, the leg's in its inside.
|
||||
assert!(!contains(&s, [40.0, 0.25, 40.0]) && contains(&s, [40.0, 0.75, 40.0]));
|
||||
assert!(!contains(&s, [1.75, 17.0, 20.0]) && contains(&s, [1.25, 17.0, 20.0]));
|
||||
// The flat: the hole, and a pocket per label, 0.5 deep from the web's skin (z 0) and from the
|
||||
// other one (z T). Along the outside from the web label's middle (40 in, 40 along) to the
|
||||
// hole's: 40 to the sharp and 18 up the leg, less a deduction, and 20 back along; to the leg
|
||||
// label's middle, 40 and 17 the other way.
|
||||
let circles = flat_circles(&mut s);
|
||||
assert!(circles.len() == 1 && (circles[0].0 - 3.0).abs() < 1e-9, "{circles:?}");
|
||||
let f = flat(&mut s);
|
||||
let pockets = f["pockets"].as_array().cloned().unwrap_or_default();
|
||||
assert_eq!(pockets.len(), 2, "{f}");
|
||||
let from = |p: &Value, z0: f64, z1: f64| {
|
||||
(p["z0_mm"].as_f64().unwrap_or(f64::NAN) - z0).abs() < 1e-9 && (p["z1_mm"].as_f64().unwrap_or(f64::NAN) - z1).abs() < 1e-9
|
||||
};
|
||||
let web = pockets.iter().find(|p| from(p, 0.0, 0.5)).expect("the web's label, from z 0");
|
||||
let leg = pockets.iter().find(|p| from(p, 1.5, 2.0)).expect("the leg's label, from z T");
|
||||
let middle = |p: &Value| {
|
||||
let (lo, hi) = pocket_box(p);
|
||||
([(lo[0] + hi[0]) / 2.0, (lo[1] + hi[1]) / 2.0], (hi[0] - lo[0], hi[1] - lo[1]))
|
||||
};
|
||||
let ((w, w_size), (l, l_size), h) = (middle(web), middle(leg), circles[0].1);
|
||||
assert!(same_size(w_size, (20.0, 10.0), 1e-6) && same_size(l_size, (10.0, 20.0), 1e-6), "{w_size:?} {l_size:?}");
|
||||
let (to_hole, to_leg) = ([h[0] - w[0], h[1] - w[1]], [l[0] - w[0], l[1] - w[1]]);
|
||||
assert!(same_size((to_hole[0].abs(), to_hole[1].abs()), (58.0 - bd, 20.0), 1e-6), "{to_hole:?}");
|
||||
assert!(same_size((to_leg[0].abs(), to_leg[1].abs()), (57.0 - bd, 20.0), 1e-6), "{to_leg:?}");
|
||||
assert!(to_hole[0] * to_leg[0] + to_hole[1] * to_leg[1] < 0.0, "{to_hole:?} {to_leg:?}");
|
||||
// The DXF: the hole a CIRCLE, each label on the pocket layer of its depth.
|
||||
assert_eq!(dxf_entities(&mut s).2, 1);
|
||||
let dxf = run(&mut s, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap_or_default().to_string();
|
||||
assert_eq!((lines_on(&dxf, "POCKET_BOTTOM_Z0.000000_TO_0.500000_MM"), lines_on(&dxf, "POCKET_TOP_Z1.500000_TO_2.000000_MM")), (4, 4));
|
||||
// Unfolded and folded again it keeps them.
|
||||
run(&mut s, "sheet.unfold", json!({}));
|
||||
let want = t * across * 60.0 - cut;
|
||||
assert!((volume(&mut s) - want).abs() < 1e-4 * want, "{} vs {want}", volume(&mut s));
|
||||
run(&mut s, "sheet.refold", json!({}));
|
||||
assert!((volume(&mut s) - v).abs() < 1e-6 * v);
|
||||
}
|
||||
|
||||
/// Chamfer bends beside holes: [`chamfered_channel`] with a 6 mm hole in its web 20 from the
|
||||
/// left sharp corner and one 18 up its right leg, the chamfers picked in their middles. The holes
|
||||
/// land from the virtual sharps, where the faces were taken out to.
|
||||
#[test]
|
||||
fn holes_beside_chamfer_bends_land_from_the_virtual_sharps() {
|
||||
let (t, k, r) = (2.0, 0.5, 3.0);
|
||||
let mut s = Session::default();
|
||||
rule(&mut s, "Test", t, k, r, 0.5);
|
||||
chamfered_channel(&mut s);
|
||||
run(&mut s, "solid.hole", json!({"position": [20, 0, 30], "diameter": 6}));
|
||||
run(&mut s, "solid.hole", json!({"position": [40, 18, 70], "diameter": 6, "depth": 3}));
|
||||
run(&mut s, "sheet.convert", json!({"body": "Channel", "face": [20, 0, 80], "bends": [[2, 2, 50], [38, 2, 50]]}));
|
||||
let bd = deduction(FRAC_PI_2, r, t, k);
|
||||
let across = 100.0 - 2.0 * bd;
|
||||
assert!(same_size(flat_size(&mut s), (across, 100.0), 1e-6), "{:?} vs {across}", flat_size(&mut s));
|
||||
// From the web's hole: 20 to the right sharp, 18 up the leg, less a deduction; 40 along.
|
||||
let circles = flat_circles(&mut s);
|
||||
assert!(circles.len() == 2 && circles.iter().all(|(radius, _)| (radius - 3.0).abs() < 1e-9), "{circles:?}");
|
||||
let d = [circles[1].1[0] - circles[0].1[0], circles[1].1[1] - circles[0].1[1]];
|
||||
assert!(same_size((d[0].abs(), d[1].abs()), (38.0 - bd, 40.0), 1e-6), "{d:?}");
|
||||
// Three flat strips and two quarter rounds on the neutral radius, less the holes.
|
||||
let expect = t * 100.0 * (2.0 * (30.0 - (r + t)) + (40.0 - 2.0 * (r + t)) + PI * (r + t / 2.0)) - 2.0 * t * 9.0 * PI;
|
||||
assert!((volume(&mut s) - expect).abs() < 2e-3 * expect, "{} vs {expect}", volume(&mut s));
|
||||
}
|
||||
Reference in new issue
Block a user