mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 20:02:16 +00:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3b507c056 | |||
| 2bf58241fd | |||
| 62d372cc16 | |||
| c29190ec35 | |||
| 4e01031bc7 | |||
| d3902c954d | |||
| 653c16e6f5 | |||
| 92734d7eb5 | |||
| 820b7b6677 | |||
| 597b23ba8e | |||
| 608d07ecac | |||
| ad3ee05857 | |||
| 99953687c7 | |||
| 5d168a5e26 | |||
| e6db7be0af | |||
| 5136898b9d | |||
| 17de3569fd | |||
| aed0477033 | |||
| 970446dec0 | |||
| c5332faf4b | |||
| 303111e4ed | |||
| 4a4d9c31ce | |||
| 68c3ccb754 | |||
| 75efb8f0cf | |||
| f873056481 | |||
| 72109054c1 | |||
| 459937e524 | |||
| 38385ab5d3 | |||
| ea49dff48f | |||
| d5cd5f303b | |||
| f2ff6f0d4f | |||
| ee3ed66a4f | |||
| 78ed751627 | |||
| ce8b01fa30 | |||
| 21f00a9175 | |||
| 155f1f4702 | |||
| cde8be4a19 | |||
| bf51e62f7b | |||
| e24eb77894 | |||
| 6ba069d066 | |||
| 60393a7183 | |||
| e8caa1e910 | |||
| 1745e5c389 | |||
| 0b1cc8a8c2 | |||
| 7bba3092c6 | |||
| 01e2e939e9 | |||
| cf5e0e3c10 | |||
| 7577e032fb | |||
| 539d126489 | |||
| 4df3adc5a7 | |||
| 19455396be | |||
| 922ee28f37 | |||
| 9d4f780010 | |||
| 05f5e86011 | |||
| aa41c0b5de | |||
| 60923d8cc1 | |||
| 73416a9bf0 | |||
| 19014f38a0 | |||
| 4401ffb1f9 | |||
| 6cd4003adb | |||
| be0b1b39bd | |||
| a3165f62d2 | |||
| 2273379056 | |||
| 2c75cf424f | |||
| 3c16884f7c |
@@ -318,6 +318,10 @@ jobs:
|
||||
cargo test -p xtask ui_containers --locked
|
||||
- name: Sorted lists
|
||||
run: cargo xtask lists
|
||||
- name: Model decisions use exact geometry, not meshes
|
||||
run: |
|
||||
cargo xtask mesh-decisions
|
||||
cargo test -p xtask mesh_decisions --locked
|
||||
- name: Agent router and skills cite real files
|
||||
run: |
|
||||
cargo xtask skills
|
||||
|
||||
@@ -10,6 +10,7 @@ Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelle
|
||||
- No GPL/LGPL/AGPL code, not even reading it (OpenCASCADE, FreeCAD, SolveSpace, CadQuery, LibreCAD, QCAD): `cargo xtask licences --check`.
|
||||
- Every asset is original or openly licensed with a row in ATTRIBUTION.md; read `assets/AGENTS.md` before icon work: `cargo xtask assets`, `cargo xtask icons`.
|
||||
- Never crash: no unwrap/expect/panic/unsafe in non-test code (unsafe allowlist in crash-safety), errors are `Result`, input is hostile: clippy `-D warnings`, `hostile_params_never_panic`.
|
||||
- Model decisions (what an operation does, which bodies it touches, which face or edge a point or name means, depths, nearness) use exact geometry; a mesh is for display, export, previews and mesh bodies (`xtask/mesh-decisions.txt` only shrinks): `cargo xtask mesh-decisions`.
|
||||
- Everything is a command and programmatic calls never open dialogs: `skills/solidcraft-commands/SKILL.md`.
|
||||
- Layering (geom, sketch/kernel/render, doc, io, engine, ui-egui, apps); only `crates/kernel` uses truck: `cargo xtask layers`.
|
||||
- One outer UI container, one camera path (`navigation::handle`), colours from theme tokens: `cargo xtask ui-containers`.
|
||||
@@ -45,4 +46,3 @@ Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelle
|
||||
| A bug report, a crash, a wrong result | `skills/solidcraft-reporting/SKILL.md` |
|
||||
| Checking, porting, designs in git | `skills/solidcraft-checking/SKILL.md` |
|
||||
| Assemblies, joints, motion | `skills/solidcraft-mechanisms/SKILL.md` |
|
||||
>>>>>>> 4a3e0772a (Agents: AGENTS.md becomes a router to focused skills, with a staleness check (Refs #6439))
|
||||
|
||||
@@ -198,6 +198,28 @@ fn continuity_seams(b: &solidcraft_engine::doc::ModelBody, edges: &Value) -> Res
|
||||
}
|
||||
}
|
||||
|
||||
/// The orders a case's `continuity` asks for: `{order, edges, exact}` for its `edges` seams, or
|
||||
/// `seams: [{order, edges, exact}, ...]`, each read the same way, for seams that reach different
|
||||
/// orders (`edges` points picking each). Nothing where it asks for no order (the curvature-jump
|
||||
/// form, or none); an `order` that is not g0…g4 is an error.
|
||||
fn continuity_wants(cont: &Value) -> Vec<Result<(&Value, solidcraft_engine::kernel::Continuity, bool), String>> {
|
||||
fn one(w: &Value) -> Option<Result<(&Value, solidcraft_engine::kernel::Continuity, bool), String>> {
|
||||
let order = w.get("order").filter(|o| !o.is_null())?;
|
||||
let exact = w["exact"].as_bool().unwrap_or(false);
|
||||
Some(
|
||||
order
|
||||
.as_str()
|
||||
.and_then(solidcraft_engine::kernel::Continuity::parse)
|
||||
.map(|o| (&w["edges"], o, exact))
|
||||
.ok_or_else(|| format!("continuity `order` {order} is not g0…g4")),
|
||||
)
|
||||
}
|
||||
match cont.get("seams").and_then(Value::as_array) {
|
||||
Some(seams) => seams.iter().map(|w| one(w).unwrap_or_else(|| Err("a continuity seam with no `order`".to_owned()))).collect(),
|
||||
None => one(cont).into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The `continuity: {order, edges}` check of `b`: its `edges` seams measured as Continuity
|
||||
/// Analysis (`inspect.continuity`) measures them, at its default stations and tolerances, and the
|
||||
/// lowest order among them with the seam that has it (the worst: of those, the one whose first
|
||||
@@ -456,7 +478,8 @@ fn output_checks(c: &mut Checks, want: &Value, out: &str, results: &[Value], rel
|
||||
}
|
||||
|
||||
/// The word a `surface_types` check names a face's surface by, as the record holds it: an
|
||||
/// analytic kind, `bspline` (a B-spline or NURBS surface) or `revolution` (a revolved curve).
|
||||
/// analytic kind, `ball_patch` (a corner's patch on a sphere), `bspline` (a B-spline or NURBS
|
||||
/// surface) or `revolution` (a revolved curve).
|
||||
fn surface_word(s: &solidcraft_engine::geom::brep::Surface) -> &'static str {
|
||||
use solidcraft_engine::geom::brep::Surface as S;
|
||||
match s {
|
||||
@@ -464,6 +487,7 @@ fn surface_word(s: &solidcraft_engine::geom::brep::Surface) -> &'static str {
|
||||
S::Cylinder { .. } => "cylinder",
|
||||
S::Cone { .. } => "cone",
|
||||
S::Sphere { .. } => "sphere",
|
||||
S::BallPatch { .. } => "ball_patch",
|
||||
S::Torus { .. } => "torus",
|
||||
S::Spline { .. } | S::RationalSpline { .. } => "bspline",
|
||||
S::PlacedRevolution { .. } => "revolution",
|
||||
@@ -794,22 +818,22 @@ fn check_case(dir: &str, dumps: Option<&std::path::Path>, validation: solidcraft
|
||||
);
|
||||
}
|
||||
// The order every blend seam reaches, as Continuity Analysis measures it.
|
||||
if let Some(order) = want["continuity"].get("order").filter(|o| !o.is_null()) {
|
||||
let edges = &want["continuity"]["edges"];
|
||||
let exact = want["continuity"]["exact"].as_bool().unwrap_or(false);
|
||||
let which = if edges.is_array() { "picked" } else { edges.as_str().unwrap_or("blend") };
|
||||
let need = format!("{} {} at every {which} seam", if exact { "exactly" } else { ">=" }, order.as_str().unwrap_or("?"));
|
||||
match order.as_str().and_then(solidcraft_engine::kernel::Continuity::parse) {
|
||||
Some(o) => match continuity_order(b, o, edges, exact) {
|
||||
Ok((got, ok)) => c.push("continuity_order", got, json!(need), ok),
|
||||
Err(e) => c.push("continuity_order", json!(format!("{}: {e}", b.name)), json!(need), false),
|
||||
},
|
||||
None => c.push("continuity_order", json!(format!("continuity `order` {order} is not g0…g4")), json!("an order"), false),
|
||||
for wanted in continuity_wants(&want["continuity"]) {
|
||||
match wanted {
|
||||
Ok((edges, o, exact)) => {
|
||||
let which = if edges.is_array() { "picked" } else { edges.as_str().unwrap_or("blend") };
|
||||
let need = format!("{} {} at every {which} seam", if exact { "exactly" } else { ">=" }, o.as_str());
|
||||
match continuity_order(b, o, edges, exact) {
|
||||
Ok((got, ok)) => c.push("continuity_order", got, json!(need), ok),
|
||||
Err(e) => c.push("continuity_order", json!(format!("{}: {e}", b.name)), json!(need), false),
|
||||
}
|
||||
}
|
||||
Err(e) => c.push("continuity_order", json!(e), json!("an order"), false),
|
||||
}
|
||||
}
|
||||
// Curvature continuity across the seams round a point: the worst jump clear of the
|
||||
// seams' turns must be within the case's bound.
|
||||
if let Some(cont) = want.get("continuity").filter(|c| !c.is_null() && c.get("order").is_none()) {
|
||||
if let Some(cont) = want.get("continuity").filter(|c| !c.is_null() && c.get("order").is_none() && c.get("seams").is_none()) {
|
||||
let (near, within, clear, bound) =
|
||||
(v3(&cont["near"]), cont["within_mm"].as_f64(), cont["clear_mm"].as_f64(), cont["curvature_jump_per_mm"].as_f64());
|
||||
match (near, within, clear, bound) {
|
||||
@@ -1196,6 +1220,28 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
/// `continuity` asks for one order at every seam it picks, or per seam (`seams`), each entry
|
||||
/// read as the one-order form; the curvature-jump form asks for none.
|
||||
#[test]
|
||||
fn continuity_asks_for_one_order_or_one_per_seam() {
|
||||
use solidcraft_engine::kernel::Continuity;
|
||||
let read = |c: Value| {
|
||||
continuity_wants(&c).into_iter().map(|w| w.map(|(e, o, x)| (e.clone(), o, x))).collect::<Vec<Result<(Value, Continuity, bool), String>>>()
|
||||
};
|
||||
let g2 = Continuity::parse("g2").unwrap();
|
||||
let g3 = Continuity::parse("g3").unwrap();
|
||||
assert_eq!(read(json!({"order": "g2", "exact": true})), vec![Ok((Value::Null, g2, true))]);
|
||||
let (p, q) = (json!([[30.0, 31.3, 10.0]]), json!([[30.0, 29.4, 12.7]]));
|
||||
assert_eq!(
|
||||
read(json!({"seams": [{"order": "g2", "exact": true, "edges": p}, {"order": "g3", "edges": q}]})),
|
||||
vec![Ok((p.clone(), g2, true)), Ok((q, g3, false))]
|
||||
);
|
||||
assert!(read(json!({"near": [0.0, 0.0, 0.0], "within_mm": 1.0})).is_empty());
|
||||
assert!(read(Value::Null).is_empty());
|
||||
assert!(read(json!({"order": "g9"}))[0].is_err());
|
||||
assert!(read(json!({"seams": [{"edges": p}]}))[0].is_err(), "a seam must say its order");
|
||||
}
|
||||
|
||||
/// The flat pattern is graded on the last `sheet.flat_pattern` result: its size, its outline's
|
||||
/// area less the cut-outs', and its bend lines matched either way round, one for one.
|
||||
#[test]
|
||||
|
||||
@@ -317,7 +317,8 @@ fn a_model_saved_under_another_check_mode_is_evaluated_again() {
|
||||
/// later round at the corner where they meet blends them again with it whether the design was
|
||||
/// opened from its model or evaluated anew. Two edges of a 20 mm box rounded r2 (a mitre at the
|
||||
/// corner (20, 0, 20)), saved; opened, the upright edge there rounded r3: the corner all three
|
||||
/// rounded at once make, 10 faces and 7928.743041 mm³, the same body as the design evaluated anew.
|
||||
/// rounded at once make (SMLib's setback corner, kernel `three_rounds_at_a_corner`), 15 faces and
|
||||
/// 7928.142563938 mm³, the same body as the design evaluated anew.
|
||||
#[test]
|
||||
fn a_later_round_after_opening_blends_the_saved_rounds_again() {
|
||||
let mut built = Session::default();
|
||||
@@ -334,8 +335,8 @@ fn a_later_round_after_opening_blends_the_saved_rounds_again() {
|
||||
let st = s.model.state();
|
||||
let b = &st.body("Box").unwrap().body;
|
||||
let v = solidcraft_engine::kernel::measure(b).unwrap().volume;
|
||||
assert_eq!(b.face_count(), 10);
|
||||
assert!((v - 7928.743041).abs() < 1e-6, "{v}");
|
||||
assert_eq!(b.face_count(), 15);
|
||||
assert!((v - 7928.142563938).abs() < 1e-6, "{v}");
|
||||
}
|
||||
assert_eq!(model("later round", &cached), model("later round", &rebuilt));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"en": {
|
||||
"ui.fillet_error_setback_short": "The setback is too short for this corner's patch: it builds from {least} on. Try a longer Setback."
|
||||
},
|
||||
"ar": {
|
||||
"ui.fillet_error_setback_short": "مسافة الإرجاع قصيرة جدًا على رقعة هذه الزاوية: تُبنى ابتداءً من {least}. جرّب مسافة إرجاع أطول."
|
||||
},
|
||||
"de": {
|
||||
"ui.fillet_error_setback_short": "Der Rücksprung ist für das Eckstück zu kurz: Es lässt sich ab {least} bauen. Versuchen Sie einen längeren Rücksprung."
|
||||
},
|
||||
"he": {
|
||||
"ui.fillet_error_setback_short": "הנסיגה קצרה מדי עבור הטלאי של פינה זו: הוא נבנה החל מ-{least}. נסו נסיגה ארוכה יותר."
|
||||
},
|
||||
"ja": {
|
||||
"ui.fillet_error_setback_short": "このコーナーのパッチにはセットバックが短すぎます。{least} 以上で作れます。より長いセットバックを試してください。"
|
||||
},
|
||||
"ko": {
|
||||
"ui.fillet_error_setback_short": "이 코너의 패치에 비해 셋백이 너무 짧습니다. {least}부터 만들 수 있습니다. 더 긴 셋백을 시도해 보세요."
|
||||
},
|
||||
"zh": {
|
||||
"ui.fillet_error_setback_short": "对于此角点的面片,后退距离太短:从 {least} 起可以构建。请尝试更长的后退距离。"
|
||||
}
|
||||
}
|
||||
+15
-41
@@ -5,7 +5,7 @@ use super::*;
|
||||
use crate::EmbossFace;
|
||||
use crate::apply_point;
|
||||
use crate::sheet;
|
||||
use solidcraft_geom::{Aabb3, Loop2, Ring, Seg2};
|
||||
use solidcraft_geom::{Loop2, Ring, Seg2};
|
||||
|
||||
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
|
||||
match &f.kind {
|
||||
@@ -1025,15 +1025,17 @@ pub(super) fn sheet_primitive_cut(
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
// The first sheet the primitive meets decides the machining direction. Sheets clear of it
|
||||
// by their boxes are passed over; the one sheet left needs no boolean to show it is met (that
|
||||
// boolean came out empty against a sheet carrying a label's cut-outs, and the hole was then
|
||||
// cut into the solid without a place in the flat pattern).
|
||||
let tool_box = body_box(tool);
|
||||
let near: Vec<&sheet::SheetBody> = st
|
||||
.sheets
|
||||
.iter()
|
||||
.filter(|sh| sheet_eligible(f, st, &[], sh) && st.body(&sh.body).is_some_and(|mb| !boxes_apart(&mesh_box(mb), &tool_box)))
|
||||
.collect();
|
||||
// by their exact boxes ([`kernel::apart`]) are passed over; the one sheet left needs no
|
||||
// boolean to show it is met (that boolean came out empty against a sheet carrying a label's
|
||||
// cut-outs, and the hole was then cut into the solid without a place in the flat pattern).
|
||||
let mut near: Vec<&sheet::SheetBody> = Vec::new();
|
||||
for sh in st.sheets.iter().filter(|sh| sheet_eligible(f, st, &[], sh)) {
|
||||
if let Some(mb) = st.body(&sh.body)
|
||||
&& !kernel::apart(&mb.body, tool)?
|
||||
{
|
||||
near.push(sh);
|
||||
}
|
||||
}
|
||||
let mut normal = None;
|
||||
for sh in &near {
|
||||
if let Some(mb) = st.body(&sh.body)
|
||||
@@ -1344,9 +1346,6 @@ fn sheet_prism_cut(
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
// Boxes around the tools: a tool clear of a bend by its box needs no boolean to show it (a
|
||||
// label's letters against every bend and flange of a tray).
|
||||
let tool_boxes: Vec<Option<Aabb3>> = tools.iter().map(body_box).collect();
|
||||
let mut plans = Vec::new();
|
||||
let mut notes = Vec::new();
|
||||
'sheets: for original in &st.sheets {
|
||||
@@ -1366,12 +1365,10 @@ fn sheet_prism_cut(
|
||||
// is exact, and a cut that machines nothing there is checked against the solid after it.
|
||||
// (That boolean cost each label hundreds of milliseconds, and against a sheet already
|
||||
// carrying a label's cut-outs it came out empty: the next cut was dropped.)
|
||||
// A tool clear of a bend by its exact box (a label's letters against every bend and
|
||||
// flange of a tray) costs no intersection work: the boolean answers it from the boxes.
|
||||
for bends in sh.unmodeled_solids(plane)? {
|
||||
let bends_box = body_box(&bends);
|
||||
for (tool, tool_box) in tools.iter().zip(&tool_boxes) {
|
||||
if boxes_apart(&bends_box, tool_box) {
|
||||
continue;
|
||||
}
|
||||
for tool in tools {
|
||||
if kernel::boolean(&bends, tool, kernel::BoolOp::Intersect)?.is_some() {
|
||||
return Err(DocError::Invalid(
|
||||
"this machining intersects a folded bend or a nonparallel panel; unfold the sheet before machining that material".into(),
|
||||
@@ -1556,29 +1553,6 @@ fn sheet_prism_cut(
|
||||
Ok(handled)
|
||||
}
|
||||
|
||||
/// A box around `body`, grown past its mesh's deflection from the exact faces (and a hair
|
||||
/// more): bodies whose boxes are apart do not meet. None when it can't be worked out.
|
||||
fn body_box(body: &Body) -> Option<Aabb3> {
|
||||
let tol = (body.size() * 1e-3).max(1e-4);
|
||||
padded_box(&body.mesh(&solidcraft_kernel::MeshTolerance::chord(tol)).ok()?, tol)
|
||||
}
|
||||
|
||||
/// [`body_box`] of a model body, from the mesh it keeps.
|
||||
fn mesh_box(mb: &ModelBody) -> Option<Aabb3> {
|
||||
padded_box(&mb.mesh(), mb.display_tol())
|
||||
}
|
||||
|
||||
fn padded_box(mesh: &solidcraft_geom::Mesh, tol: f64) -> Option<Aabb3> {
|
||||
let b = mesh.bounds();
|
||||
(!b.is_empty() && b.min.is_finite() && b.max.is_finite()).then(|| b.inflated(2.0 * tol + 1e-6))
|
||||
}
|
||||
|
||||
/// Whether two boxes are known to be apart (an unknown box is never apart).
|
||||
fn boxes_apart(a: &Option<Aabb3>, b: &Option<Aabb3>) -> bool {
|
||||
let (Some(a), Some(b)) = (a, b) else { return false };
|
||||
a.max.x < b.min.x || b.max.x < a.min.x || a.max.y < b.min.y || b.max.y < a.min.y || a.max.z < b.min.z || b.max.z < a.min.z
|
||||
}
|
||||
|
||||
/// A cut's note on the pieces it parted from the base of `sh` (see
|
||||
/// [`sheet::SheetBody::parted_from_base`]; `material` is the sheet's flat material after the cut,
|
||||
/// `before` the sheet before it): how many, their area and where they lie in the flat pattern.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
//! A face may save `surface` as a `ball_patch` {center, radius}: the corner of three equal rounds
|
||||
//! as SMLib holds it, a rational patch on that sphere, always with its `nurbs`, the patch itself.
|
||||
//! An older design has these faces as plain NURBS faces and keeps them so. The last release
|
||||
//! refuses a design with this change (the newer format number).
|
||||
@@ -89,7 +89,9 @@ fn redefining_a_fillet_as_g2_on_sloped_ends_survives_undo_and_reopen() {
|
||||
assert_eq!(bodies.len(), 1);
|
||||
bodies.first().unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.0001)).unwrap().measure().volume
|
||||
};
|
||||
let expected = |r: f64| 12000.0 - 40.0 * 113.0 / 756.0 * r.powi(2) + 13711.0 / 486486.0 * r.powi(3);
|
||||
// The curvature section at its equal-area thumbweight removes (1 - pi/4) r² at the square
|
||||
// corner, first moment 0.0474559669443 r³ (Green's theorem on the section).
|
||||
let expected = |r: f64| 12000.0 - 40.0 * (1.0 - std::f64::consts::FRAC_PI_4) * r.powi(2) + 0.0474559669443 * r.powi(3);
|
||||
let actual = volume(&s);
|
||||
assert!((actual - expected(5.0)).abs() < 0.05, "{actual} vs {}", expected(5.0));
|
||||
let edited = s.doc.clone();
|
||||
|
||||
@@ -5770,3 +5770,51 @@ fn an_unfolded_sheets_flat_face_is_named_after_its_base_at_every_size() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A primitive cut passes over a sheet its exact box is clear of: that sheet comes back the very
|
||||
/// same body, and the sheet the cut meets gets the hole.
|
||||
#[test]
|
||||
fn a_primitive_cut_passes_over_a_sheet_clear_of_it_by_its_exact_box() {
|
||||
let mut s = Session::default();
|
||||
for (name, x) in [("Left", 0.0), ("Right", 100.2)] {
|
||||
run(&mut s, "solid.box", json!({"corner": [x, 0, 0], "length": 100, "width": 60, "height": 2.5, "body_name": name}));
|
||||
run(&mut s, "sheet.convert", json!({"body": name, "face": [x + 50.0, 30, 2.5]}));
|
||||
}
|
||||
let body = |s: &Session, name: &str| s.model.state().bodies.iter().find(|b| b.name == name).map(|b| b.body.clone()).unwrap();
|
||||
let left = body(&s, "Left");
|
||||
let right_volume = body_volume(&mut s, "Right");
|
||||
// 0.2 mm from the Left sheet's end: within the display mesh's tolerance of it, clear of its exact box.
|
||||
run(&mut s, "solid.cylinder", json!({"base": [102.9, 30, -1], "radius": 2.5, "height": 5, "operation": "cut"}));
|
||||
assert!(body(&s, "Left").same(&left), "the sheet clear of the cut is passed over");
|
||||
let holes = |s: &Session, name: &str| s.model.state().sheets.iter().find(|sh| sh.body == name).map(|sh| sh.holes.len());
|
||||
assert_eq!(holes(&s, "Left"), Some(0));
|
||||
assert_eq!(holes(&s, "Right"), Some(1));
|
||||
assert!((right_volume - body_volume(&mut s, "Right") - PI * 6.25 * 2.5).abs() < 1e-3);
|
||||
}
|
||||
|
||||
/// A cut that only grazes a bend's curved faces near the edge where the bend meets the base is
|
||||
/// checked against the bend and refused (the sheet is unchanged); one clear of the bend's exact
|
||||
/// box cuts the base.
|
||||
#[test]
|
||||
fn a_cut_grazing_a_bend_near_its_edge_is_refused_and_one_clear_of_its_exact_box_cuts() {
|
||||
// `plate_with_flange`: the bend (R 2.5, T 2.5) runs from y = 0 to y = 5, where the base
|
||||
// panel starts.
|
||||
for (y0, grazes) in [(4.9, true), (5.1, false)] {
|
||||
let mut s = Session::default();
|
||||
plate_with_flange(&mut s);
|
||||
let v0 = volume(&mut s);
|
||||
rectangle_tool(&mut s, "Slot", [40.0, y0], [60.0, 15.0]);
|
||||
let cut = json!({"sketch": "Slot", "through_all": true, "direction": "symmetric", "operation": "cut"});
|
||||
if grazes {
|
||||
let before = s.doc.to_json();
|
||||
let e = s.execute("solid.extrude", &cut).unwrap_err().to_string();
|
||||
assert!(e.contains("intersects a folded bend"), "{e}");
|
||||
assert_eq!(s.doc.to_json(), before);
|
||||
assert_eq!(volume(&mut s), v0);
|
||||
} else {
|
||||
run(&mut s, "solid.extrude", cut);
|
||||
assert!((v0 - volume(&mut s) - 20.0 * (15.0 - y0) * 2.5).abs() < 1e-3, "{}", v0 - volume(&mut s));
|
||||
assert_eq!(flat(&mut s)["cutouts"].as_array().map(Vec::len), Some(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,10 +91,10 @@ fn fillet_as_wide_as_a_face_rounds_it_off() {
|
||||
|
||||
/// The owner's ruling of 2026-10-30: two edges of a 20 mm box rounded r2 in one fillet (they
|
||||
/// meet in a mitre at the corner (20, 0, 20), the upright edge there sharp), then the upright edge
|
||||
/// r3 in a second fillet, whose tangent chain takes the mitre's seam with it. The corner is one
|
||||
/// smooth patch, the body rounding all three at once makes (kernel `fillet_radii` of the box with
|
||||
/// each edge's radius: 10 faces, 7928.743041 mm³); the r2 rounds keep their names, and the r3
|
||||
/// round and the corner patch are the second fillet's.
|
||||
/// r3 in a second fillet, whose tangent chain takes the mitre's seam with it. The corner is the
|
||||
/// body rounding all three at once makes (kernel `fillet_radii` of the box with each edge's
|
||||
/// radius, `three_rounds_at_a_corner`: SMLib's setback corner, 15 faces, 7928.142563938 mm³); the
|
||||
/// r2 rounds keep their names, and the r3 round and the corner's patches are the second fillet's.
|
||||
#[test]
|
||||
fn a_later_round_at_a_mitred_corner_is_blended_with_the_rounds_there() {
|
||||
let mut s = Session::default();
|
||||
@@ -116,16 +116,17 @@ fn a_later_round_at_a_mitred_corner_is_blended_with_the_rounds_there() {
|
||||
let second = run(&mut s, "solid.fillet", json!({"edges": [[20, 0, 10]], "radius": 3}))["feature"].as_u64().unwrap();
|
||||
assert!(s.model.results.iter().all(|r| r.error.is_none()), "{:?}", s.model.results);
|
||||
let v = volume(&mut s);
|
||||
assert!((v - 7928.743041).abs() < 1e-5, "{v}");
|
||||
assert!((v - 7928.142563938).abs() < 1e-6, "{v}");
|
||||
let faces = run(&mut s, "model.faces", json!({"body": "Box"}))["faces"].as_array().cloned().unwrap_or_default();
|
||||
assert_eq!(faces.len(), 10);
|
||||
assert_eq!(faces.len(), 15);
|
||||
let after: Vec<String> = rounds.iter().map(|p| named(&mut s, *p)).collect();
|
||||
assert_eq!(after, before);
|
||||
// The r3 round (halfway up, at 45°) and the corner patch: the two faces the second fillet made.
|
||||
// The r3 round (halfway up, at 45°) and the corner's patches: the faces the second fillet made.
|
||||
let k3 = 3.0 - 4.5_f64.sqrt();
|
||||
assert!(named(&mut s, solidcraft_geom::Vec3::new(20.0 - k3, k3, 10.0)).starts_with(&format!("F{second}:blend:")));
|
||||
let made = faces.iter().filter(|f| f["name"].as_str().is_some_and(|n| n.starts_with(&format!("F{second}:")))).count();
|
||||
assert_eq!(made, 2, "{faces:?}");
|
||||
// the r3 round and the corner's six patches (a setback patch per round, three in the triangle)
|
||||
assert_eq!(made, 7, "{faces:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+21
-7
@@ -94,10 +94,10 @@ pub struct Face {
|
||||
pub surface: Surface,
|
||||
/// The NURBS form the face is parameterised on, when it differs from `surface`'s own
|
||||
/// parameters: a cylinder, cone, sphere, torus or placed revolution as SMLib holds it (every
|
||||
/// SMLib analytic surface is a B-spline surface, its trims in that NURBS's parameters). When
|
||||
/// given, `domain` and every pcurve are in this surface's (u, v), and `surface` only says what
|
||||
/// it is; it is a `Spline` or `RationalSpline` and `surface` is a cylinder, cone, sphere, torus
|
||||
/// or placed revolution
|
||||
/// SMLib analytic surface is a B-spline surface, its trims in that NURBS's parameters), and a
|
||||
/// ball patch's own net. When given, `domain` and every pcurve are in this surface's (u, v),
|
||||
/// and `surface` only says what it is; it is a `Spline` or `RationalSpline` and `surface` is a
|
||||
/// cylinder, cone, sphere, ball patch, torus or placed revolution
|
||||
/// (docs/native-brep.md, "Surfaces").
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub nurbs: Option<Surface>,
|
||||
@@ -247,6 +247,10 @@ pub enum Surface {
|
||||
#[serde(default, skip_serializing_if = "is_zero")]
|
||||
tube_start: f64,
|
||||
},
|
||||
/// A rational patch on the sphere of `center` and `radius`: the corner of three equal rounds as
|
||||
/// SMLib holds it (`SmBallPatch`). It has no (u, v) of its own: the face's `nurbs` is the patch,
|
||||
/// and this says it lies on that sphere, whose offsets are exact and collapse at its centre.
|
||||
BallPatch { center: [f64; 3], radius: f64 },
|
||||
/// A tensor-product B-spline: `points[i][j]`, i along u (STEP `B_SPLINE_SURFACE_WITH_KNOTS`).
|
||||
Spline { u_knots: Vec<f64>, v_knots: Vec<f64>, points: Vec<Vec<[f64; 3]>> },
|
||||
/// A NURBS surface: homogeneous `[x*w, y*w, z*w, w]` coefficients.
|
||||
@@ -378,6 +382,8 @@ fn surface_natural(s: &Surface) -> [Natural; 2] {
|
||||
}
|
||||
Surface::Sphere { .. } => [Natural::Periodic, Natural::Range(-FRAC_PI_2, FRAC_PI_2)],
|
||||
Surface::Torus { .. } => [Natural::Periodic; 2],
|
||||
// A label: the face's (u, v) are its NURBS form's.
|
||||
Surface::BallPatch { .. } => [UNBOUNDED; 2],
|
||||
Surface::Spline { u_knots, v_knots, points } => {
|
||||
[knot_domain(u_knots, points.len()), knot_domain(v_knots, points.first().map_or(0, Vec::len))]
|
||||
}
|
||||
@@ -496,6 +502,10 @@ fn surface(s: &Surface, depth: usize, budget: &mut Budget) -> Result<()> {
|
||||
require(tube_start.is_finite() && tube_start.abs() <= std::f64::consts::TAU, "torus tube start")?;
|
||||
require(major_radius > minor_radius, "ring torus radii")
|
||||
}
|
||||
Surface::BallPatch { center, radius: r } => {
|
||||
points(&[*center], budget)?;
|
||||
radius(*r)
|
||||
}
|
||||
Surface::Spline { u_knots, v_knots, points: p } => grid(u_knots, v_knots, p, budget),
|
||||
Surface::RationalSpline { u_knots, v_knots, homogeneous: p } => {
|
||||
grid(u_knots, v_knots, p, budget)?;
|
||||
@@ -578,6 +588,7 @@ impl Face {
|
||||
let outward = match &self.surface {
|
||||
Surface::Cylinder { frame, .. } | Surface::Cone { frame, .. } => radial(frame).1,
|
||||
Surface::Sphere { frame, .. } => p - v3(frame.origin),
|
||||
Surface::BallPatch { center, .. } => p - v3(*center),
|
||||
Surface::Torus { frame, major_radius, .. } => {
|
||||
let (o, r) = radial(frame);
|
||||
p - (o + r.normalized()? * *major_radius)
|
||||
@@ -651,13 +662,15 @@ impl Brep {
|
||||
Surface::Cylinder { .. }
|
||||
| Surface::Cone { .. }
|
||||
| Surface::Sphere { .. }
|
||||
| Surface::BallPatch { .. }
|
||||
| Surface::Torus { .. }
|
||||
| Surface::PlacedRevolution { .. }
|
||||
),
|
||||
"NURBS form on a surface that is not a cylinder, cone, sphere, torus or revolution",
|
||||
"NURBS form on a surface that is not a cylinder, cone, sphere, ball patch, torus or revolution",
|
||||
)?;
|
||||
}
|
||||
// A cylinder, cone, sphere, torus or revolution is a label: its geometry is its form.
|
||||
// A cylinder, cone, sphere, ball patch, torus or revolution is a label: its geometry is
|
||||
// its form.
|
||||
require(
|
||||
f.nurbs.is_some()
|
||||
|| !matches!(
|
||||
@@ -665,10 +678,11 @@ impl Brep {
|
||||
Surface::Cylinder { .. }
|
||||
| Surface::Cone { .. }
|
||||
| Surface::Sphere { .. }
|
||||
| Surface::BallPatch { .. }
|
||||
| Surface::Torus { .. }
|
||||
| Surface::PlacedRevolution { .. }
|
||||
),
|
||||
"a cylinder, cone, sphere, torus or revolution without its NURBS form",
|
||||
"a cylinder, cone, sphere, ball patch, torus or revolution without its NURBS form",
|
||||
)?;
|
||||
let [u, v] = surface_natural(f.parameters());
|
||||
interval(&f.domain[0], u)?;
|
||||
|
||||
@@ -109,6 +109,8 @@ impl SurfaceEval {
|
||||
Surface::Torus { frame, major_radius, minor_radius, tube_start } => {
|
||||
Ok(SurfaceEval::Torus { axes: Axes::new(frame), major: *major_radius, minor: *minor_radius, tube: *tube_start })
|
||||
}
|
||||
// A label with no (u, v) of its own: its face evaluates its NURBS form.
|
||||
Surface::BallPatch { .. } => Err(unsupported("a ball patch's label (evaluate its NURBS form)")),
|
||||
Surface::Spline { u_knots, v_knots, points } => {
|
||||
let rows: Vec<Vec<[f64; 4]>> = points.iter().map(|r| r.iter().map(|p| [p[0], p[1], p[2], 1.0]).collect()).collect();
|
||||
eval(nurbs(u_knots, v_knots, &rows)?)
|
||||
|
||||
@@ -96,6 +96,7 @@ impl Rigid {
|
||||
Surface::Cone { frame: self.frame(frame), radius_at_origin: *radius_at_origin, half_angle: *half_angle }
|
||||
}
|
||||
Surface::Sphere { frame, radius } => Surface::Sphere { frame: self.frame(frame), radius: *radius },
|
||||
Surface::BallPatch { center, radius } => Surface::BallPatch { center: self.point(*center), radius: *radius },
|
||||
Surface::Torus { frame, major_radius, minor_radius, tube_start } => {
|
||||
Surface::Torus { frame: self.frame(frame), major_radius: *major_radius, minor_radius: *minor_radius, tube_start: *tube_start }
|
||||
}
|
||||
|
||||
+18
-13
@@ -224,6 +224,10 @@ pub fn fillet_removing(body: &Body, edges: &[Vec3], radius: f64) -> Result<(Body
|
||||
Ok((closed(body, b)?, gone))
|
||||
}
|
||||
|
||||
/// What a refusal of a setback too short for its corner says before the least that builds (in
|
||||
/// mm): SMLib certifies the corner's fill convex only from that setback on ([`fillet_setback`]).
|
||||
pub const SETBACK_AT_LEAST: &str = "the smallest setback that builds is ";
|
||||
|
||||
/// [`fillet`] with corner setbacks: each `(at, distance)` sets back the corner at the vertex
|
||||
/// nearest `at`, a vertex where every edge is rounded, so its patch starts `distance` along
|
||||
/// each of its edges from the vertex (one distance for all of a corner's edges: SMLib keeps one
|
||||
@@ -1173,15 +1177,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn g2_fillet_has_the_quintics_area() {
|
||||
fn g2_fillet_takes_off_what_the_round_does() {
|
||||
let body = crate::build::box_solid(Vec3::ZERO, Vec3::new(20.0, 20.0, 20.0)).unwrap();
|
||||
let out = fillet_g2(&body, &[Vec3::new(20.0, 0.0, 10.0)], 2.0).unwrap();
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
assert_eq!(out.face_count(), body.face_count() + 1);
|
||||
// Independent analytic area: integrating x(t) y'(t) for the quintic
|
||||
// gives 113/756 times setback² for a right-angle corner.
|
||||
// The curvature section's default thumbweight is equal area: on the round's rails it takes
|
||||
// off what the round does, (1 - pi/4) setback² at a right-angle corner.
|
||||
let volume = out.mesh(&crate::MeshTolerance::chord(0.0001)).unwrap().measure().volume;
|
||||
let expected = 8000.0 - (113.0 / 756.0) * 4.0 * 20.0;
|
||||
let expected = 8000.0 - (1.0 - std::f64::consts::FRAC_PI_4) * 4.0 * 20.0;
|
||||
assert!((volume - expected).abs() < 0.02, "{volume} vs {expected}");
|
||||
}
|
||||
|
||||
@@ -1199,7 +1203,7 @@ mod tests {
|
||||
let out = fillet_g2(&body, &[Vec3::new(10.0, 10.0, 10.0)], 2.0).unwrap();
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
let volume = out.mesh(&crate::MeshTolerance::chord(0.0001)).unwrap().measure().volume;
|
||||
let expected = 6000.0 + (113.0 / 756.0) * 4.0 * 20.0;
|
||||
let expected = 6000.0 + (1.0 - std::f64::consts::FRAC_PI_4) * 4.0 * 20.0;
|
||||
assert!((volume - expected).abs() < 0.02, "{volume} vs {expected}");
|
||||
let body = crate::build::box_solid(Vec3::ZERO, Vec3::new(20.0, 20.0, 20.0)).unwrap();
|
||||
let out = fillet_g2(&body, &[Vec3::new(20.0, 0.0, 10.0), Vec3::new(0.0, 20.0, 10.0)], 2.0).unwrap();
|
||||
@@ -1241,9 +1245,10 @@ mod tests {
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
assert_eq!(out.crossed_face_loops().unwrap(), 0);
|
||||
assert_eq!(out.face_count(), 7);
|
||||
// Analytic volume: the removed section has area 113 r² / 756
|
||||
// and first moment ∫y dA = 13711 r³ / 486486. Axial length is 40−(left+right)y.
|
||||
let removed = 40.0 * 113.0 / 756.0 * 25.0 - (left + right) * 13711.0 / 486486.0 * 125.0;
|
||||
// Analytic volume: the equal-area section removes (1 - pi/4) r², and its first
|
||||
// moment ∫y dA is 0.0474559669443 r³ (Green's theorem on the section at the
|
||||
// square corner's equal-area thumbweight). Axial length is 40−(left+right)y.
|
||||
let removed = 40.0 * (1.0 - std::f64::consts::FRAC_PI_4) * 25.0 - (left + right) * 0.0474559669443 * 125.0;
|
||||
let expected = 16000.0 - 4000.0 * (left + right) - removed;
|
||||
let volume = out.mesh(&crate::MeshTolerance::chord(0.0001)).unwrap().measure().volume;
|
||||
assert!((volume - expected).abs() < 0.05, "{volume} vs {expected}");
|
||||
@@ -1272,7 +1277,7 @@ mod tests {
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
assert_eq!(out.crossed_face_loops().unwrap(), 0);
|
||||
let volume = out.mesh(&crate::MeshTolerance::chord(0.0001)).unwrap().measure().volume;
|
||||
let expected = 6000.0 + 113.0 / 756.0 * 4.0 * 20.0;
|
||||
let expected = 6000.0 + (1.0 - std::f64::consts::FRAC_PI_4) * 4.0 * 20.0;
|
||||
assert!((volume - expected).abs() < 0.02, "{volume} vs {expected}");
|
||||
}
|
||||
|
||||
@@ -1287,12 +1292,12 @@ mod tests {
|
||||
assert!(fillet_g2(&body, &[Vec3::new(f64::NAN, 0.0, 0.0)], 2.0).is_err());
|
||||
assert!(fillet_g2(&body, &[Vec3::new(1000.0, 0.0, 0.0)], 2.0).is_err());
|
||||
// Wider than the faces (20), over their far edges, cliffs: cut as if they went on. The
|
||||
// section is the quintic from 25 out on each face, its inner control points a third and
|
||||
// two thirds of the way to the corner; it takes the region between it and the corner
|
||||
// inside the 20 x 20 section (clipped polygon of the curve, 2e5 pieces), along 20.
|
||||
// section is the equal-area one from 25 out on each face; it takes the region between it
|
||||
// and the corner inside the 20 x 20 section (the area under the curve clipped to the
|
||||
// square, 2e4 strips on a 4e5-piece polygon of the curve), along 20.
|
||||
let wide = fillet_g2(&body, &edge, 25.0).unwrap();
|
||||
let v = crate::measure(&wide).unwrap().volume;
|
||||
assert!((v - 6138.1606889014).abs() < 1e-6 * 6138.1606889014, "{v}");
|
||||
assert!((v - 5329.0618943703).abs() < 1e-5 * 5329.0618943703, "{v}");
|
||||
assert!(wide.validity().is_empty());
|
||||
}
|
||||
|
||||
|
||||
@@ -305,30 +305,6 @@ mod tests {
|
||||
8000.0 - full + (sum - union) * hh * hh
|
||||
}
|
||||
|
||||
/// What the strips leave of the corner block where a patch over the corner of rounds `ra`,
|
||||
/// `rb`, `rc` on a, b, c lies: beyond a's end section (y < rc), b's (x > 20 − rc) and c's end
|
||||
/// (z > 20 − max(ra, rb), where c's rails meet a's and b's), the box's material that no
|
||||
/// strip removes, by columns along z. Outside that block such a body is the strips' (its
|
||||
/// rounds are the strips' cylinders up to those ends), so its patch takes at most this
|
||||
/// beyond them.
|
||||
fn corner_block(ra: f64, rb: f64, rc: f64) -> f64 {
|
||||
let len = |d: f64, r: f64| if d < r { r - (r * r - (r - d) * (r - d)).max(0.0).sqrt() } else { 0.0 };
|
||||
let (n, depth) = (2000, ra.max(rb));
|
||||
let h = rc / n as f64;
|
||||
let mut kept = 0.0;
|
||||
for i in 0..n {
|
||||
let dx = (i as f64 + 0.5) * h;
|
||||
for j in 0..n {
|
||||
let dy = (j as f64 + 0.5) * h;
|
||||
if (rc - dx).powi(2) + (rc - dy).powi(2) > rc * rc && dx < rc && dy < rc {
|
||||
continue;
|
||||
}
|
||||
kept += depth - len(dx, ra).max(len(dy, rb)).min(depth);
|
||||
}
|
||||
}
|
||||
kept * h * h
|
||||
}
|
||||
|
||||
/// Largest angle (degrees) between the two faces' normals along edges between curved faces.
|
||||
fn kink(b: &Body) -> f64 {
|
||||
let rec = Faces::of(b).unwrap();
|
||||
@@ -515,29 +491,41 @@ mod tests {
|
||||
assert!(r.validity().is_empty(), "{:?}", r.validity());
|
||||
assert!((vol(&r) - sphere).abs() < 1e-11 * sphere, "{} vs {sphere}", vol(&r));
|
||||
assert!(kink(&r) < 0.01);
|
||||
// Different radii: a smooth patch over the corner, more than the strips take, and no more
|
||||
// than they leave in the block it lies in: its rounds end at a's section y = rc, b's
|
||||
// x = 20 − rc, and c's rails where they meet a's and b's (z = 20 − ra, z = 20 − rb), the
|
||||
// patch's four corners. (2, 2, 3): 1.168 beyond the strips, of the 11.86 the block keeps;
|
||||
// (2, 3, 4): 3.099 of 32.69.
|
||||
// Different radii: SMLib's setback corner (SmBlendSetback.h). Each round runs whole up to
|
||||
// its section square to its edge at its setback, the owner's rule's (at least 1.5 r, more
|
||||
// where the rails beside it cross later, scaled to the least at which the corner's G1 fill
|
||||
// is certified convex), and the hole between the three sections is filled by the setback
|
||||
// split: one setback patch per round and the triangle between them in three
|
||||
// quadrilaterals, so 6 planes, 3 rounds and 6 patches (15 faces), G1 across every seam.
|
||||
// Outside the box the sections span ([20 - d_b, 20] x [0, d_a] x [20 - d_c, 20], d each
|
||||
// edge's setback) the body is the three strips', so its volume is theirs within that box's.
|
||||
// The rounds a and b of one radius lie mirrored about the plane x + y = 20 with c: one
|
||||
// setback. (2, 2, 3): 3.6155, 3.6155, 4.6485; (2, 3, 4): 4.7745, 5.03975, 6.366 (a, b, c).
|
||||
for (rs, sel) in [((2.0, 2.0, 3.0), fillet(&ab, &[c], 3.0)), ((2.0, 3.0, 4.0), fillet_radii(&bx, &[(a, 2.0), (b, 3.0), (c, 4.0)]))] {
|
||||
let r = sel.unwrap();
|
||||
assert_eq!(r.face_count(), 10);
|
||||
assert_eq!(r.face_count(), 15);
|
||||
assert_eq!(r.unmeshed_faces(), 0);
|
||||
assert!(r.validity().is_empty(), "{:?}", r.validity());
|
||||
let k = kink(&r);
|
||||
assert!(k < 0.05, "kink {k}°");
|
||||
assert!(k < 0.01, "kink {k}°");
|
||||
let (ra, rb, rc) = rs;
|
||||
let ends = [
|
||||
Vec3::new(20.0 - ra, rc, 20.0),
|
||||
Vec3::new(20.0, rc, 20.0 - ra),
|
||||
Vec3::new(20.0 - rc, rb, 20.0),
|
||||
Vec3::new(20.0 - rc, 0.0, 20.0 - rb),
|
||||
];
|
||||
for p in ends {
|
||||
assert!(vertices(&r).iter().any(|q| q.dist(p) < 1e-9), "{rs:?}: no patch corner at {p:?}");
|
||||
let vs = vertices(&r);
|
||||
// An edge's setback: where its round's section meets both faces, at two vertices.
|
||||
let setback = |at: &dyn Fn(f64) -> [Vec3; 2], r: f64| -> Option<f64> {
|
||||
vs.iter()
|
||||
.flat_map(|p| [p.y, 20.0 - p.x, 20.0 - p.z])
|
||||
.find(|d| *d >= 1.5 * r - 1e-9 && *d < 20.0 && at(*d).iter().all(|q| vs.iter().any(|p| p.dist(*q) < 1e-9)))
|
||||
};
|
||||
let da = setback(&|d| [Vec3::new(20.0 - ra, d, 20.0), Vec3::new(20.0, d, 20.0 - ra)], ra);
|
||||
let db = setback(&|d| [Vec3::new(20.0 - d, 0.0, 20.0 - rb), Vec3::new(20.0 - d, rb, 20.0)], rb);
|
||||
let dc = setback(&|d| [Vec3::new(20.0 - rc, 0.0, 20.0 - d), Vec3::new(20.0, rc, 20.0 - d)], rc);
|
||||
eprintln!("{rs:?}: setbacks {da:?} {db:?} {dc:?}");
|
||||
let (Some(da), Some(db), Some(dc)) = (da, db, dc) else { panic!("{rs:?}: a round's setback section is missing: {da:?} {db:?} {dc:?}") };
|
||||
if ra == rb {
|
||||
assert!((da - db).abs() < 1e-9, "{da} vs {db}");
|
||||
}
|
||||
let (v, s) = (vol(&r), strips(ra, rb, rc));
|
||||
assert!(v < s + 0.05 && v > s - corner_block(ra, rb, rc), "{v} vs {s}");
|
||||
assert!((v - s).abs() < db * da * dc, "{v} vs {s}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -418,7 +418,15 @@ mod imp {
|
||||
"not supported yet: the {what} meets a corner of this kind, which SMLib's blends do not handle yet (where its edges \
|
||||
run on tangentially into each other, or meet in a way no blend corner builds)"
|
||||
)),
|
||||
SmError::NoRoom => KernelError::TooLarge(format!("the {what} is larger than the faces beside its edges allow")),
|
||||
// A setback too short for its corner's fill (SMLib certifies it convex only from
|
||||
// its least on) names that least; any other is a blend too large for its room.
|
||||
SmError::NoRoom => match solidcraft_smlib::blend_fit() {
|
||||
Some(solidcraft_smlib::Fit::SetbackAtLeast(least)) => KernelError::Invalid(format!(
|
||||
"the setback is too short for its corner: {}{least:.4} mm (the corner's patch is convex only from there)",
|
||||
crate::SETBACK_AT_LEAST
|
||||
)),
|
||||
_ => KernelError::TooLarge(format!("the {what} is larger than the faces beside its edges allow")),
|
||||
},
|
||||
e => failed(what, "operation", e),
|
||||
})?;
|
||||
// Checked here unless the evaluation's validation mode leaves it to the feature's end
|
||||
@@ -624,14 +632,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn a_linear_radius_runs_from_the_start_end() {
|
||||
// 3 at x = 0 to 1 at x = 20: (1 - pi/4) L (r0^2 + r0 r1 + r1^2) / 3 taken away, from
|
||||
// either end given as the start with the law reversed.
|
||||
// 3 at x = 0 to 1 at x = 20: the balls' union, a cone, takes 18.5377834 (their great
|
||||
// circles, (1 - pi/4) L (r0^2 + r0 r1 + r1^2) / 3, would take 18.5988), from either end
|
||||
// given as the start with the law reversed.
|
||||
let b = cube();
|
||||
let at = Vec3::new(10.0, 20.0, 20.0);
|
||||
let law = RadiusSpline { degree: 1, knots: vec![0.0, 0.0, 1.0, 1.0], radii: vec![3.0, 1.0] };
|
||||
let r = imp::variable(&b, at, Vec3::new(0.0, 20.0, 20.0), &law).unwrap();
|
||||
let want = 8000.0 - QUARTER * 20.0 * 13.0 / 3.0;
|
||||
assert!((volume(&r) - want).abs() < 0.05, "{}", volume(&r));
|
||||
let want = 8000.0 - 18.5377834;
|
||||
assert!((volume(&r) - want).abs() < 0.01, "{}", volume(&r));
|
||||
let back = imp::variable(&b, at, Vec3::new(20.0, 20.0, 20.0), &law.reversed()).unwrap();
|
||||
assert!((volume(&back) - volume(&r)).abs() < 1e-6 * 8000.0);
|
||||
}
|
||||
|
||||
@@ -545,16 +545,17 @@ mod tests {
|
||||
assert_eq!((less.eval(0.0), less.eval(1.0)), (2.0, 3.0));
|
||||
}
|
||||
|
||||
/// The material a variable fillet removes from a right-angled edge of length L, radius
|
||||
/// running r0 → r1: ∫ (1 − π/4) r(t)² dt = (1 − π/4) L (r0² + r0 r1 + r1²) / 3.
|
||||
/// The material a variable fillet removes from a right-angled edge, radius running r0 → r1:
|
||||
/// its balls' union (`removed_by_balls`; the balls' great circles, (1 − π/4) L (r0² + r0 r1 +
|
||||
/// r1²) / 3, take more), to 1e-4 of itself.
|
||||
#[test]
|
||||
fn variable_radius_on_a_box_edge() {
|
||||
let b = box_solid(Vec3::ZERO, Vec3::new(10.0, 20.0, 30.0)).unwrap();
|
||||
// The edge along y at x = 10, z = 30; radius 2 at y = 0, 5 at y = 20.
|
||||
let f = fillet_variable(&b, Vec3::new(10.0, 10.0, 30.0), Vec3::new(10.0, 0.0, 30.0), 2.0, 5.0).unwrap();
|
||||
let removed = (1.0 - std::f64::consts::FRAC_PI_4) * 20.0 * (4.0 + 10.0 + 25.0) / 3.0;
|
||||
let removed = removed_by_balls(20.0, &|u| 2.0 + 3.0 * u, &open, &[]);
|
||||
let v = measure(&f).unwrap().volume;
|
||||
assert!(rel(v, 6000.0 - removed) < 1e-4, "{v} vs {}", 6000.0 - removed);
|
||||
assert!(rel(6000.0 - v, removed) < 1e-4, "{} removed vs {removed}", 6000.0 - v);
|
||||
// Started from the other end: the same solid.
|
||||
let h = fillet_variable(&b, Vec3::new(10.0, 10.0, 30.0), Vec3::new(10.0, 20.0, 30.0), 5.0, 2.0).unwrap();
|
||||
assert!(rel(measure(&h).unwrap().volume, v) < 1e-9);
|
||||
@@ -564,37 +565,42 @@ mod tests {
|
||||
assert!(rel(measure(&g).unwrap().volume, measure(&k).unwrap().volume) < 1e-6);
|
||||
// Wider than the faces: past r = 10 the contact on the top leaves it across its far edge
|
||||
// x = 0 (past r = 30 the one on the side across its bottom), cliffs. The round is built as
|
||||
// if the faces went on, and what it removes is cut from the box: at each y the section of
|
||||
// the r x r square at the edge less the quarter disc about (10 - r, 30 - r), inside
|
||||
// [0, 10] x [0, 30], in closed form (a, b from the disc's centre: a, b in [0, r],
|
||||
// a^2 + b^2 >= r^2, a >= r - 10, b >= r - 30), integrated along y by Gauss-Legendre
|
||||
// between the kinks at r = 10 and 30: 2453.81232061 removed, 3546.18767939 left (to 1e-4,
|
||||
// as the variable rounds above).
|
||||
// if the faces went on, and what it removes is cut from the box: the balls' union inside
|
||||
// the box's section, 10 along the top and 30 down the side (the great circles' would take
|
||||
// 2453.81, near twice as much).
|
||||
let w = fillet_variable(&b, Vec3::new(10.0, 10.0, 30.0), Vec3::new(10.0, 0.0, 30.0), 2.0, 50.0).unwrap();
|
||||
let removed_w = removed_by_balls(20.0, &|u| 2.0 + 48.0 * u, &slab, &[20.0 * 8.0 / 48.0, 20.0 * 28.0 / 48.0]);
|
||||
let vw = measure(&w).unwrap().volume;
|
||||
assert!(rel(vw, 3546.1876793935) < 1e-4, "{vw} vs 3546.1876793935");
|
||||
assert!(rel(6000.0 - vw, removed_w) < 1e-4, "{} removed vs {removed_w}", 6000.0 - vw);
|
||||
}
|
||||
|
||||
/// A radius through a middle point: straight pieces (exact: each piece removes
|
||||
/// (1 − π/4) L (a² + ab + b²) / 3), and smooth (the removed volume integrates the law).
|
||||
/// A radius through a middle point, straight pieces 2 → 4 → 2: a corner in the law, where the
|
||||
/// two sides' rounds cross and each ends where they do; the balls' union takes 39.541034
|
||||
/// (the halves' great circles would take 40.0590).
|
||||
#[test]
|
||||
fn variable_radius_with_a_corner_through_a_middle_point() {
|
||||
let b = box_solid(Vec3::ZERO, Vec3::new(10.0, 20.0, 30.0)).unwrap();
|
||||
let (at, start) = (Vec3::new(10.0, 10.0, 30.0), Vec3::new(10.0, 0.0, 30.0));
|
||||
let f = fillet_variable_points(&b, at, start, &[(0.0, 2.0), (0.5, 4.0), (1.0, 2.0)], false).unwrap();
|
||||
let removed = removed_by_balls(20.0, &|u| 4.0 - 4.0 * (u - 0.5).abs(), &open, &[10.0]);
|
||||
assert!((removed - 39.541034).abs() < 1e-5, "{removed}");
|
||||
let v = measure(&f).unwrap().volume;
|
||||
assert!(rel(6000.0 - v, removed) < 1e-4, "{} removed vs {removed}", 6000.0 - v);
|
||||
assert_eq!(f.unmeshed_faces(), 0);
|
||||
}
|
||||
|
||||
/// A radius through a middle point, smooth: the removed volume is the balls' union.
|
||||
#[test]
|
||||
fn variable_radius_through_a_middle_point() {
|
||||
let b = box_solid(Vec3::ZERO, Vec3::new(10.0, 20.0, 30.0)).unwrap();
|
||||
let (at, start) = (Vec3::new(10.0, 10.0, 30.0), Vec3::new(10.0, 0.0, 30.0));
|
||||
let k = 1.0 - std::f64::consts::FRAC_PI_4;
|
||||
let pts = [(0.0, 2.0), (0.5, 4.0), (1.0, 2.0)];
|
||||
let f = fillet_variable_points(&b, at, start, &pts, false).unwrap();
|
||||
let removed = 2.0 * k * 10.0 * (4.0 + 8.0 + 16.0) / 3.0;
|
||||
let v = measure(&f).unwrap().volume;
|
||||
assert!(rel(v, 6000.0 - removed) < 1e-4, "{v} vs {}", 6000.0 - removed);
|
||||
assert_eq!(f.unmeshed_faces(), 0);
|
||||
let s = fillet_variable_points(&b, at, start, &pts, true).unwrap();
|
||||
let law = RadiusSpline::through(&pts, true);
|
||||
let n = 20000;
|
||||
let integral: f64 = (0..n).map(|i| law.eval((i as f64 + 0.5) / n as f64).powi(2)).sum::<f64>() / n as f64 * 20.0;
|
||||
assert!((law.eval(0.5) - 4.0).abs() < 1e-12 && (law.eval(0.0) - 2.0).abs() < 1e-12);
|
||||
let removed = removed(&law, 20.0);
|
||||
let v = measure(&s).unwrap().volume;
|
||||
assert!(rel(v, 6000.0 - k * integral) < 1e-4, "{v} vs {}", 6000.0 - k * integral);
|
||||
assert!(rel(6000.0 - v, removed) < 1e-4, "{} removed vs {removed}", 6000.0 - v);
|
||||
// Unsorted, from the other end: the same.
|
||||
let r = fillet_variable_points(&b, at, Vec3::new(10.0, 20.0, 30.0), &[(1.0, 2.0), (0.0, 2.0), (0.5, 4.0)], true).unwrap();
|
||||
assert!(rel(measure(&r).unwrap().volume, v) < 1e-9);
|
||||
@@ -606,16 +612,18 @@ mod tests {
|
||||
}
|
||||
|
||||
/// The radius running down to zero at one end: the blend closes to the corner, a three-sided
|
||||
/// face, and the end face there is untouched. Removed (1 − π/4) L r0² / 3 when linear.
|
||||
/// face, and the end face there is untouched. Removed: the balls' union (a cone to its apex
|
||||
/// when linear, 22.594877; the great circles' (1 − π/4) L r0² / 3 would take 22.8945).
|
||||
#[test]
|
||||
fn variable_radius_down_to_zero_at_one_end() {
|
||||
let b = box_solid(Vec3::ZERO, Vec3::new(10.0, 20.0, 30.0)).unwrap();
|
||||
let (at, start, end) = (Vec3::new(10.0, 10.0, 30.0), Vec3::new(10.0, 0.0, 30.0), Vec3::new(10.0, 20.0, 30.0));
|
||||
let k = 1.0 - std::f64::consts::FRAC_PI_4;
|
||||
let cone = removed_by_balls(20.0, &|u| 4.0 - 4.0 * u, &open, &[]);
|
||||
assert!((cone - 22.594877).abs() < 1e-5, "{cone}");
|
||||
for (r0, r1) in [(4.0, 0.0), (0.0, 4.0)] {
|
||||
let f = fillet_variable(&b, at, start, r0, r1).unwrap();
|
||||
let v = measure(&f).unwrap().volume;
|
||||
assert!(rel(v, 6000.0 - k * 20.0 * 16.0 / 3.0) < 1e-4, "{r0} → {r1}: {v}");
|
||||
assert!(rel(6000.0 - v, cone) < 1e-4, "{r0} → {r1}: {} removed vs {cone}", 6000.0 - v);
|
||||
assert_eq!(f.unmeshed_faces(), 0);
|
||||
let (faces, vertices) = faces_and_vertices(&f);
|
||||
assert_eq!(faces.len(), 7);
|
||||
@@ -628,10 +636,9 @@ mod tests {
|
||||
let pts = [(0.0, 3.0), (0.5, 4.0), (1.0, 0.0)];
|
||||
let s = fillet_variable_points(&b, at, start, &pts, true).unwrap();
|
||||
let law = RadiusSpline::through(&pts, true);
|
||||
let n = 20000;
|
||||
let integral: f64 = (0..n).map(|i| law.eval((i as f64 + 0.5) / n as f64).powi(2)).sum::<f64>() / n as f64 * 20.0;
|
||||
let removed = removed(&law, 20.0);
|
||||
let v = measure(&s).unwrap().volume;
|
||||
assert!(rel(v, 6000.0 - k * integral) < 1e-4, "{v} vs {}", 6000.0 - k * integral);
|
||||
assert!(rel(6000.0 - v, removed) < 1e-4, "{} removed vs {removed}", 6000.0 - v);
|
||||
// Zero at both ends, zero in the middle, a smooth radius dipping below zero, or a radius
|
||||
// too small to build: refused.
|
||||
assert!(fillet_variable(&b, at, start, 0.0, 0.0).is_err());
|
||||
@@ -664,11 +671,113 @@ mod tests {
|
||||
crate::boolean(&plate, &wall, crate::BoolOp::Union).unwrap().unwrap()
|
||||
}
|
||||
|
||||
/// The material a right-angled blend with the radius `law` removes along an edge of length L:
|
||||
/// ∫ (1 − π/4) r² over the edge.
|
||||
/// Simpson's rule for `f` on [a, b] in `n` (even) panels.
|
||||
fn simpson(f: impl Fn(f64) -> f64, a: f64, b: f64, n: usize) -> f64 {
|
||||
let h = (b - a) / n as f64;
|
||||
(0..=n)
|
||||
.map(|i| {
|
||||
f(a + i as f64 * h)
|
||||
* if i == 0 || i == n {
|
||||
1.0
|
||||
} else if i % 2 == 1 {
|
||||
4.0
|
||||
} else {
|
||||
2.0
|
||||
}
|
||||
})
|
||||
.sum::<f64>()
|
||||
* h
|
||||
/ 3.0
|
||||
}
|
||||
|
||||
/// The material a variable round removes from a right-angled edge of length `len`: what no
|
||||
/// ball reaches, the round being the union of its balls (their envelope; each ball touches it
|
||||
/// along the circle square to the line of centres, not its great circle, as the radius
|
||||
/// varies). The ball at s along the edge has radius `law(s / len)` (the law going on past the
|
||||
/// ends along its end slopes) and its centre that far from both faces. In each plane square
|
||||
/// to the edge at x, the ray from the edge at angle θ from the first face meets the union
|
||||
/// first at the least entry over the balls, and the corner up to there is taken, as far as
|
||||
/// `room(x, θ)` (the body's section along the ray; ∞ where it does not end): ∫ ρ²/2 dθ,
|
||||
/// integrated over x, both by Simpson's rule, split at `kinks` (where the law turns).
|
||||
pub(crate) fn removed_by_balls(len: f64, law: &dyn Fn(f64) -> f64, room: &dyn Fn(f64, f64) -> f64, kinks: &[f64]) -> f64 {
|
||||
let h = 1e-7;
|
||||
let (k0, k1) = ((law(h) - law(0.0)) / (h * len), (law(1.0) - law(1.0 - h)) / (h * len));
|
||||
let r = |s: f64| {
|
||||
if s < 0.0 {
|
||||
law(0.0) + k0 * s
|
||||
} else if s > len {
|
||||
law(1.0) + k1 * (s - len)
|
||||
} else {
|
||||
law(s / len)
|
||||
}
|
||||
};
|
||||
let big = (0..=1000).map(|i| law(i as f64 / 1000.0)).fold(0.0f64, f64::max);
|
||||
let entry = |x: f64, c: f64, sn: f64, s: f64| {
|
||||
let rr = r(s);
|
||||
let p = rr * (c + sn);
|
||||
let disc = p * p - (x - s) * (x - s) - rr * rr;
|
||||
if rr > 0.0 && disc >= 0.0 { p - disc.sqrt() } else { f64::INFINITY }
|
||||
};
|
||||
let rho = |x: f64, t: f64| {
|
||||
let (c, sn) = (t.cos(), t.sin());
|
||||
let (lo, hi, scan) = (x - 1.5 * big, x + 1.5 * big, 400);
|
||||
let step = (hi - lo) / scan as f64;
|
||||
let (mut best, mut at) = (f64::INFINITY, x);
|
||||
for i in 0..=scan {
|
||||
let s = lo + i as f64 * step;
|
||||
let e = entry(x, c, sn, s);
|
||||
if e < best {
|
||||
(best, at) = (e, s);
|
||||
}
|
||||
}
|
||||
let (mut a, mut b, g) = (at - step, at + step, (5f64.sqrt() - 1.0) / 2.0);
|
||||
for _ in 0..80 {
|
||||
let (m1, m2) = (b - g * (b - a), a + g * (b - a));
|
||||
if entry(x, c, sn, m1) < entry(x, c, sn, m2) {
|
||||
b = m2;
|
||||
} else {
|
||||
a = m1;
|
||||
}
|
||||
}
|
||||
best.min(entry(x, c, sn, 0.5 * (a + b)))
|
||||
};
|
||||
// The rays meet the union tangentially at the faces (ρ goes as the root of the angle
|
||||
// there): θ = π/4 (1 − cos(π q)) makes the integrand smooth in q.
|
||||
let area = |x: f64| {
|
||||
let q = std::f64::consts::PI;
|
||||
simpson(
|
||||
|u: f64| {
|
||||
let t = q / 4.0 * (1.0 - (q * u).cos());
|
||||
let p = rho(x, t).min(room(x, t));
|
||||
let p = if p.is_finite() { p } else { 0.0 };
|
||||
0.5 * p * p * q * q / 4.0 * (q * u).sin()
|
||||
},
|
||||
0.0,
|
||||
1.0,
|
||||
128,
|
||||
)
|
||||
};
|
||||
let mut ends = vec![0.0];
|
||||
ends.extend(kinks.iter().copied().filter(|k| *k > 0.0 && *k < len));
|
||||
ends.push(len);
|
||||
ends.windows(2).map(|w| simpson(area, w[0], w[1], 2 * ((64.0 * (w[1] - w[0]) / len).ceil() as usize).max(1))).sum()
|
||||
}
|
||||
|
||||
/// No end to the body's section: the faces go on past the round.
|
||||
fn open(_x: f64, _t: f64) -> f64 {
|
||||
f64::INFINITY
|
||||
}
|
||||
|
||||
/// The 10 x 20 x 30 box's section about its edge x = 10, z = 30: 10 along the top, 30 down
|
||||
/// the side.
|
||||
fn slab(_x: f64, t: f64) -> f64 {
|
||||
(10.0 / t.cos().max(1e-300)).min(30.0 / t.sin().max(1e-300))
|
||||
}
|
||||
|
||||
/// The material a right-angled blend with the radius `law` removes along an edge of length L
|
||||
/// where the faces go on past it: the union of its balls.
|
||||
fn removed(law: &RadiusSpline, len: f64) -> f64 {
|
||||
let n = 20000;
|
||||
(1.0 - std::f64::consts::FRAC_PI_4) * (0..n).map(|i| law.eval((i as f64 + 0.5) / n as f64).powi(2)).sum::<f64>() / n as f64 * len
|
||||
removed_by_balls(len, &|u| law.eval(u), &open, &[])
|
||||
}
|
||||
|
||||
/// The volume of a fine tessellation: at a blend closed to a point `measure` takes a
|
||||
@@ -749,15 +858,13 @@ mod tests {
|
||||
assert!(fillet_variable_spline(&b, at, start, &unclamped).is_err());
|
||||
let dips = RadiusSpline { radii: vec![2.0, 0.0, 0.0, 3.0, 2.0], ..law.clone() };
|
||||
assert!(fillet_variable_spline(&b, at, start, &dips).is_err());
|
||||
// A middle radius beyond the faces (up to 21.25 on the top 10 wide): the contact on the top
|
||||
// leaves it across its far edge, a cliff, and the round is cut as if the top went on: at
|
||||
// each y the r x r square at the edge less the quarter disc about (10 - r, 30 - r), inside
|
||||
// [0, 10] x [0, 30], in closed form, integrated along y (Gauss-Legendre, split at the
|
||||
// knot and where r crosses 10): 726.5334566720 removed.
|
||||
// A middle radius up to 21.25 (on the top 10 wide), rising and falling so fast that the
|
||||
// balls' envelope folds over itself at the peak: for the balls' family
|
||||
// F(y, p) = |p - c(y)|² - r(y)² the touch arc there has F_yy down to -8.88 (where the touch
|
||||
// circles move back along the edge). Refused, never built folded (the balls' great
|
||||
// circles, which never fold, built it to 726.53 removed against the union's 332.2).
|
||||
let big = RadiusSpline { radii: vec![2.0, 2.0, 40.0, 3.0, 2.0], ..law };
|
||||
let wide = fillet_variable_spline(&b, at, start, &big).unwrap();
|
||||
let vw = fine_volume(&wide);
|
||||
assert!(rel(vw, 6000.0 - 726.5334566720) < 1e-4, "{vw} vs {}", 6000.0 - 726.5334566720);
|
||||
assert!(matches!(fillet_variable_spline(&b, at, start, &big), Err(KernelError::TooLarge(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -468,7 +468,7 @@ impl SurfaceKind {
|
||||
S::Plane { .. } | S::PlaneThrough { .. } => SurfaceKind::Plane,
|
||||
S::Cylinder { .. } => SurfaceKind::Cylinder,
|
||||
S::Cone { .. } => SurfaceKind::Cone,
|
||||
S::Sphere { .. } => SurfaceKind::Sphere,
|
||||
S::Sphere { .. } | S::BallPatch { .. } => SurfaceKind::Sphere,
|
||||
S::Torus { .. } => SurfaceKind::Torus,
|
||||
S::Spline { .. } | S::RationalSpline { .. } | S::PlacedRevolution { .. } => SurfaceKind::Freeform,
|
||||
}
|
||||
|
||||
@@ -344,10 +344,11 @@ fn a_mixed_convexity_corner_is_refused() {
|
||||
}
|
||||
|
||||
/// Curvature-continuous rounds (`fillet_g2`, equivalent radius 5) of the three edges at the box
|
||||
/// corner: SMLib's own corner patch between them is only tangent to the rounds, its curvature
|
||||
/// jumping across their seams (it showed as a dome on the corner), so the corner is the G2 patch
|
||||
/// instead, filled between the curvature rounds' setback sections: one closed valid solid, the
|
||||
/// patch curvature-continuous to every round and face round it clear of the hole's corners.
|
||||
/// corner: SMLib's own corner between them is its setback corner, whose fill for curvature
|
||||
/// sections meets the rounds and faces curvature-continuously (SmBlendSetback.h: G2 for an order
|
||||
/// 2 section), as the G2 patch filled between the curvature rounds' setback sections does: each
|
||||
/// one closed valid solid, curvature-continuous across every seam round the corner clear of the
|
||||
/// hole's corners.
|
||||
#[test]
|
||||
fn curvature_rounds_meet_at_a_g2_corner() {
|
||||
let (body, edges) = box_corner();
|
||||
@@ -357,9 +358,9 @@ fn curvature_rounds_meet_at_a_g2_corner() {
|
||||
let v = crate::validity::validity(&g2);
|
||||
assert!(v.ok(), "{:?}", v);
|
||||
let (_, clear, n) = corner_seams(&g2, 0.15);
|
||||
let (_, smlib_clear, _) = corner_seams(&smlib_corner, 0.15);
|
||||
let (_, smlib_clear, smlib_n) = corner_seams(&smlib_corner, 0.15);
|
||||
assert!(n > 0 && clear <= CURVATURE_SHARE / 5.0, "{clear} over {n} stations");
|
||||
assert!(smlib_clear > 10.0 * CURVATURE_SHARE / 5.0, "SMLib's own corner patch: {smlib_clear}");
|
||||
assert!(smlib_n > 0 && smlib_clear <= CURVATURE_SHARE / 5.0, "SMLib's own corner: {smlib_clear} over {smlib_n} stations");
|
||||
// Both differ only within the setback cube (side 1.5 r = 7.5) at the corner.
|
||||
let (a, b) = (measure(&g2).unwrap().volume, measure(&smlib_corner).unwrap().volume);
|
||||
assert!((a - b).abs() < 7.5f64.powi(3), "{a} vs {b}");
|
||||
|
||||
@@ -44,8 +44,10 @@ fn a_setback_off_the_rounded_corner_is_refused() {
|
||||
}
|
||||
|
||||
/// A setback that does not reach past where the corner's rounds meet without one (on faces square
|
||||
/// to each other, the radius) is refused before SMLib runs, naming that least setback; a setback
|
||||
/// just past it builds.
|
||||
/// to each other, the radius) is refused before SMLib runs, naming that least setback. Past it, a
|
||||
/// setback still too short for SMLib's corner fill (certified convex only from a setback on,
|
||||
/// smlib_blend_fit_size's SMLIB_FIT_SETBACK_AT_LEAST) is refused naming the smallest that builds,
|
||||
/// at most twice the one asked for (the certificate scales the setbacks up to 2); that one builds.
|
||||
#[test]
|
||||
fn a_setback_not_past_the_radius_is_refused_naming_the_least() {
|
||||
for s in [2.0, 5.0] {
|
||||
@@ -54,7 +56,16 @@ fn a_setback_not_past_the_radius_is_refused_naming_the_least() {
|
||||
other => panic!("setback {s}: {:?}", other.map(|b| b.face_count())),
|
||||
}
|
||||
}
|
||||
let out = corner_case(Some(5.5)).unwrap();
|
||||
let least = match corner_case(Some(5.5)) {
|
||||
Err(KernelError::Invalid(why)) => {
|
||||
let named = why.split_once(SETBACK_AT_LEAST).and_then(|(_, rest)| rest.split_once(" mm")).and_then(|(n, _)| n.parse::<f64>().ok());
|
||||
named.unwrap_or_else(|| panic!("no least setback named: {why}"))
|
||||
}
|
||||
other => panic!("setback 5.5: {:?}", other.map(|b| b.face_count())),
|
||||
};
|
||||
eprintln!("setback 5.5 refused: the smallest that builds is {least}");
|
||||
assert!(least > 5.5 && least <= 11.0, "{least}");
|
||||
let out = corner_case(Some(least)).unwrap();
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
// The least the refusal names, for a form to show before it runs; none at no vertex.
|
||||
let b = box_solid(Vec3::new(0.0, 0.0, 0.0), Vec3::new(60.0, 40.0, 20.0)).unwrap();
|
||||
|
||||
@@ -344,6 +344,8 @@ fn iso_record(surface: &brep::Surface, along_u: bool, fixed: f64, (a, b): (f64,
|
||||
Surface::PlacedRevolution { .. } => {
|
||||
return Err(KernelError::Failed("not supported yet: an iso curve of a revolved surface record".into()));
|
||||
}
|
||||
// A label: the face's (u, v) are its NURBS form's, whose rows are its iso curves.
|
||||
Surface::BallPatch { .. } => return Err(KernelError::Invalid("an iso curve of a ball patch's label (take its NURBS form's)".into())),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -156,8 +156,8 @@ mod validity;
|
||||
pub mod watch;
|
||||
|
||||
pub use blend::{
|
||||
ChamferSide, blend_edges, chamfer, chamfer_max, chamfer_sides, fillet, fillet_each, fillet_g2, fillet_g2_corners, fillet_g2_with_g2_corners,
|
||||
fillet_least_setback, fillet_max, fillet_max_each, fillet_removing, fillet_setback, tangent_chain,
|
||||
ChamferSide, SETBACK_AT_LEAST, blend_edges, chamfer, chamfer_max, chamfer_sides, fillet, fillet_each, fillet_g2, fillet_g2_corners,
|
||||
fillet_g2_with_g2_corners, fillet_least_setback, fillet_max, fillet_max_each, fillet_removing, fillet_setback, tangent_chain,
|
||||
};
|
||||
pub use blend_corner::fillet_radii;
|
||||
pub use blend_variable::{RadiusSpline, Taper, fillet_chord, fillet_tapered, fillet_variable, fillet_variable_points, fillet_variable_spline, taper};
|
||||
@@ -202,7 +202,7 @@ pub use meshbody::{MAX_TRIANGLES, mesh_body};
|
||||
pub use native::{body_from_smlib, smlib_body};
|
||||
pub use native::{brep_record, decode_native_body, encode_native_body, native_preview_mesh};
|
||||
pub use offset::with_same_surface;
|
||||
pub use ops::{BoolOp, boolean, half_space, join_apart, place_matrix, split_by_plane, transform, transform_matrix, union_all, union_apart};
|
||||
pub use ops::{BoolOp, apart, boolean, half_space, join_apart, place_matrix, split_by_plane, transform, transform_matrix, union_all, union_apart};
|
||||
pub use point_path::{
|
||||
PathPiece, PointPath, SpaceBezier, along_path, curve_path, edge_path, face_section_along, project_onto_face, sketch_record_curve,
|
||||
sweep_point_path_with,
|
||||
|
||||
@@ -3485,8 +3485,8 @@
|
||||
1.0
|
||||
],
|
||||
[
|
||||
0.25000000000000006,
|
||||
0.7500000000000002
|
||||
0.25,
|
||||
0.7500000000000001
|
||||
]
|
||||
],
|
||||
"loops": [
|
||||
@@ -3509,8 +3509,8 @@
|
||||
0.25000000000000006
|
||||
],
|
||||
[
|
||||
-1.6653345369377353e-16,
|
||||
0.7500000000000002
|
||||
-1.6653345369377348e-16,
|
||||
0.75
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -3556,7 +3556,7 @@
|
||||
],
|
||||
[
|
||||
0.9999999999999998,
|
||||
0.75
|
||||
0.7499999999999998
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -3575,11 +3575,11 @@
|
||||
"points": [
|
||||
[
|
||||
0.9999999999999998,
|
||||
0.75
|
||||
0.7499999999999999
|
||||
],
|
||||
[
|
||||
-1.6653345369377353e-16,
|
||||
0.7500000000000002
|
||||
-1.665334536937735e-16,
|
||||
0.7500000000000001
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -3592,13 +3592,13 @@
|
||||
"homogeneous": [
|
||||
[
|
||||
[
|
||||
0.0,
|
||||
-3.552713678800501e-15,
|
||||
1.9999999999999964,
|
||||
10.0,
|
||||
1.0
|
||||
],
|
||||
[
|
||||
40.00000000000001,
|
||||
40.0,
|
||||
1.9999999999999964,
|
||||
10.0,
|
||||
1.0
|
||||
@@ -3606,13 +3606,13 @@
|
||||
],
|
||||
[
|
||||
[
|
||||
0.0,
|
||||
-2.512147933894045e-15,
|
||||
1.3778763926929864e-15,
|
||||
7.071067811865485,
|
||||
0.7071067811865489
|
||||
],
|
||||
[
|
||||
28.284271247461962,
|
||||
28.284271247461955,
|
||||
1.3778763926929864e-15,
|
||||
7.071067811865485,
|
||||
0.7071067811865489
|
||||
@@ -3620,13 +3620,13 @@
|
||||
],
|
||||
[
|
||||
[
|
||||
0.0,
|
||||
-3.552713678800501e-15,
|
||||
-4.440892098500626e-16,
|
||||
8.0,
|
||||
1.0
|
||||
],
|
||||
[
|
||||
40.00000000000001,
|
||||
40.0,
|
||||
-4.440892098500626e-16,
|
||||
8.0,
|
||||
1.0
|
||||
@@ -3643,32 +3643,32 @@
|
||||
1.0
|
||||
],
|
||||
"v_knots": [
|
||||
0.25000000000000006,
|
||||
0.25000000000000006,
|
||||
0.7500000000000002,
|
||||
0.7500000000000002
|
||||
0.25,
|
||||
0.25,
|
||||
0.7500000000000001,
|
||||
0.7500000000000001
|
||||
]
|
||||
},
|
||||
"surface": {
|
||||
"frame": {
|
||||
"origin": [
|
||||
0.0,
|
||||
2.0000000000000027,
|
||||
7.9999999999999964
|
||||
-20.0,
|
||||
2.000000000000002,
|
||||
7.999999999999997
|
||||
],
|
||||
"x_axis": [
|
||||
0.0,
|
||||
-3.1086244689504328e-15,
|
||||
-2.886579864025403e-15,
|
||||
1.0
|
||||
],
|
||||
"y_axis": [
|
||||
0.0,
|
||||
-1.0,
|
||||
-3.1086244689504328e-15
|
||||
-2.886579864025403e-15
|
||||
]
|
||||
},
|
||||
"kind": "cylinder",
|
||||
"radius": 2.0000000000000036
|
||||
"radius": 2.000000000000003
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -55,6 +55,7 @@ pub(crate) fn surf_of(b: &Brep, face: usize) -> Option<Surf> {
|
||||
Surf::Cylinder { o, a, r: *radius, convex: out }
|
||||
}
|
||||
Surface::Sphere { frame, radius } => Surf::Sphere { c: Vec3::from(frame.origin), r: *radius, convex: out },
|
||||
Surface::BallPatch { center, radius } => Surf::Sphere { c: Vec3::from(*center), r: *radius, convex: out },
|
||||
Surface::Cone { frame, radius_at_origin, half_angle } => {
|
||||
let (o, z) = axes(frame)?;
|
||||
let apex = o + z * (-radius_at_origin / half_angle.tan());
|
||||
|
||||
@@ -56,8 +56,9 @@ pub fn union_all(bodies: &[&Body]) -> Result<Body> {
|
||||
}
|
||||
|
||||
/// Whether B-rep bodies `a` and `b` are apart: their exact boxes, each grown by twice its
|
||||
/// record's tolerance (as [`union_apart`] takes them), do not meet.
|
||||
fn apart(a: &Body, b: &Body) -> Result<bool> {
|
||||
/// record's tolerance (as [`union_apart`] takes them), do not meet. Never for a mesh body. The
|
||||
/// way to skip work on bodies that cannot meet: a mesh's box is not the body's.
|
||||
pub fn apart(a: &Body, b: &Body) -> Result<bool> {
|
||||
if a.is_mesh() || b.is_mesh() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ fn kind(s: &brep::Surface) -> &'static str {
|
||||
brep::Surface::Cylinder { .. } => "cylinder",
|
||||
brep::Surface::Cone { .. } => "cone",
|
||||
brep::Surface::Sphere { .. } => "sphere",
|
||||
brep::Surface::BallPatch { .. } => "ball patch",
|
||||
brep::Surface::Torus { .. } => "torus",
|
||||
brep::Surface::Spline { .. } => "spline",
|
||||
brep::Surface::RationalSpline { .. } => "rational spline",
|
||||
|
||||
@@ -1974,8 +1974,15 @@ fn fillet_three_edges_at_a_corner() {
|
||||
let a = r * r * (1.0 - PI / 4.0);
|
||||
let want = a * (40.0 + 30.0 + 20.0) - 3.0 * a * r + (r * r * r - PI * r * r * r / 6.0);
|
||||
assert!(rel(removed, want) < 2e-3, "removed {removed} vs {want}");
|
||||
// The corner is one face: SMLib builds it as a rational patch exactly on the ball's sphere
|
||||
// (a ball patch, not SMLib's sphere), between the three cylinders and the six planes; the
|
||||
// record keeps its sphere, so it counts as one.
|
||||
let m = measure(&f).unwrap().merged;
|
||||
assert_eq!(m.face_types.get("sphere"), Some(&1), "{m:?}");
|
||||
assert_eq!(
|
||||
(m.faces, m.face_types.get("plane"), m.face_types.get("cylinder"), m.face_types.get("sphere")),
|
||||
(10, Some(&6), Some(&3), Some(&1)),
|
||||
"{m:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Rust target, SHA-256 of smlib-<target>.tar.gz in the release prebuilt-<SMLIB_REV> of
|
||||
# tangency-labs/smlib. Written by scripts/fetch-smlib.sh --pin; never edited by hand.
|
||||
aarch64-apple-darwin 3a782c2d9f89878fcb19c0ea9dfc29853204a6f6af37576722ed6334b06552c0
|
||||
aarch64-pc-windows-msvc f7a5cbe28c65a8d9dd88e22939ac3c12d5b54431ad16f1b0a1f6aa5882de2b9d
|
||||
aarch64-unknown-linux-gnu bb1166f65589ce2c385550708992d0798e010d6689952ae07232aeec438e5879
|
||||
i686-pc-windows-msvc cadf6d7c35b3e20345e064d4369c777d62561afac5cf5b7be52da6dd6f9aa785
|
||||
wasm32-unknown-unknown 250573fd99e9b109ae9f8ac16ecd4a5e55dfcd13c060763c89167bc2ee4bd4b8
|
||||
x86_64-apple-darwin 13cf77518ccbd9a3095e0d2eaabb7844c94079aa4ef88ef10fe5a2bf1eb18438
|
||||
x86_64-pc-windows-msvc 44b729d524b85de5b0aba2f24cdb6a9e449d644ef3ea4129dabbf5338b835018
|
||||
x86_64-unknown-linux-gnu 7fe1a151932370b6ab4dee1e3fad30f62e4b9d2a1ac7bd1ca423061a15276a09
|
||||
aarch64-apple-darwin b045d887901556d6c8f60a38fbbecdda765d8dafa6f7a5f72100a7deba3d1d6b
|
||||
aarch64-pc-windows-msvc c5dbd7dc9221e8638cc9e09273f9e2fe19720f0a02dd40c66d709c3d8effe0c0
|
||||
aarch64-unknown-linux-gnu f3ad86d6cd79570c428905548fabdb92005a78a9fc8d200d12047a2e972894ed
|
||||
i686-pc-windows-msvc 1e4e3999663f4cae113149bcf84d970264b776cd762fd2b7d2dfcf49f9f35772
|
||||
wasm32-unknown-unknown 33702afaa8af13e38c9aa8b894fa38f8e69b147c5cda0606c16b0c786dd46389
|
||||
x86_64-apple-darwin b41d081c90d0028d76d92c08c72465da39d52976ab416c878d5ff0ecb6eccb73
|
||||
x86_64-pc-windows-msvc 10252e3cd701be53697eea11ce68d29b244a2d25b677b57abf632f9f00bbc05b
|
||||
x86_64-unknown-linux-gnu e6a81eb955405c815e3efc05ac3ab6bd78df83e43d244a6b33bbb390a197c2db
|
||||
|
||||
@@ -1 +1 @@
|
||||
26cc45185b7ee6faba5b684f83d7bc2c53c1a559
|
||||
a18bbe523b9c7819f1a9da8c40a54c15f9936125
|
||||
|
||||
@@ -179,6 +179,7 @@ const I64_IDS: &[i32] = &[
|
||||
FACE_NURBS_V_ORDER,
|
||||
FACE_NURBS_FORM,
|
||||
EDGE_NURBS_FORM,
|
||||
FACE_DEFINITION_REAL_COUNT,
|
||||
];
|
||||
|
||||
/// The double arrays this mapping reads.
|
||||
@@ -232,6 +233,7 @@ const F64_IDS: &[i32] = &[
|
||||
FACE_TORUS_MINOR_RADIUS,
|
||||
EDGE_TOLERANCE,
|
||||
VERTEX_TOLERANCE,
|
||||
FACE_DEFINITION_REALS,
|
||||
];
|
||||
|
||||
// The kind values as the arrays hold them (int64); a local item shadows the glob import.
|
||||
@@ -241,6 +243,7 @@ const SURFACE_CYLINDER: i64 = flat::SURFACE_CYLINDER as i64;
|
||||
const SURFACE_CONE: i64 = flat::SURFACE_CONE as i64;
|
||||
const SURFACE_SPHERE: i64 = flat::SURFACE_SPHERE as i64;
|
||||
const SURFACE_TORUS: i64 = flat::SURFACE_TORUS as i64;
|
||||
const SURFACE_BALL_PATCH: i64 = flat::SURFACE_BALL_PATCH as i64;
|
||||
const CURVE_NURBS: i64 = flat::CURVE_NURBS as i64;
|
||||
const CURVE_LINE: i64 = flat::CURVE_LINE as i64;
|
||||
const CURVE_CIRCLE: i64 = flat::CURVE_CIRCLE as i64;
|
||||
@@ -302,7 +305,8 @@ fn frame(origin: [f64; 3], axis: [f64; 3], x: [f64; 3]) -> b::Frame {
|
||||
// ---- Geometry ---------------------------------------------------------------------------
|
||||
|
||||
/// The schema record of each face's surface, in face order, and its NURBS form when the arrays give
|
||||
/// one (`FACE_NURBS_FORM`: an analytic face with the NURBS SMLib holds it as).
|
||||
/// one (`FACE_NURBS_FORM`: an analytic face with the NURBS SMLib holds it as; a ball patch: its
|
||||
/// net, the face's own NURBS block, with its sphere from `FACE_DEFINITION_REALS`).
|
||||
fn surfaces(flat: View, n_faces: usize) -> Res<Vec<(b::Surface, Option<b::Surface>)>> {
|
||||
let kinds = flat.i(FACE_SURFACE_TYPE);
|
||||
if kinds.len() != n_faces {
|
||||
@@ -345,10 +349,13 @@ fn surfaces(flat: View, n_faces: usize) -> Res<Vec<(b::Surface, Option<b::Surfac
|
||||
if !forms.is_empty() && forms.len() != n_faces {
|
||||
return Err(invalid("face NURBS forms"));
|
||||
}
|
||||
let mut k = [0usize; 6];
|
||||
// The definitions, face by face: each defined face's count, then its reals from `defined`.
|
||||
let (counts, reals) = (flat.i(FACE_DEFINITION_REAL_COUNT), flat.f(FACE_DEFINITION_REALS));
|
||||
let mut defined = 0usize;
|
||||
let mut k = [0usize; 7];
|
||||
let mut out = Vec::with_capacity(n_faces);
|
||||
for (f, &kind) in kinds.iter().enumerate() {
|
||||
let slot = usize::try_from(kind).ok().filter(|s| *s < 6).ok_or_else(|| unsupported(format!("SMLib surface kind {kind}")))?;
|
||||
let slot = usize::try_from(kind).ok().filter(|s| *s < 7).ok_or_else(|| unsupported(format!("SMLib surface kind {kind}")))?;
|
||||
let i = at(&k, slot, "kind")?;
|
||||
if let Some(c) = k.get_mut(slot) {
|
||||
*c += 1;
|
||||
@@ -404,10 +411,25 @@ fn surfaces(flat: View, n_faces: usize) -> Res<Vec<(b::Surface, Option<b::Surfac
|
||||
tube_start: 0.0,
|
||||
},
|
||||
SURFACE_NURBS => next_nurbs(&mut nurbs_at)?,
|
||||
SURFACE_BALL_PATCH => {
|
||||
if at(counts, i, "ball patch definition")? != 4 {
|
||||
return Err(invalid("ball patch definition"));
|
||||
}
|
||||
let d = reals.get(defined..defined + 4).ok_or_else(|| invalid("ball patch definition"))?;
|
||||
defined += 4;
|
||||
b::Surface::BallPatch { center: [d[0], d[1], d[2]], radius: d[3] }
|
||||
}
|
||||
other => return Err(unsupported(format!("SMLib surface kind {other}"))),
|
||||
};
|
||||
// A NURBS face's own block came first; a form follows the analytic face's canonical entry.
|
||||
let form = if kind != SURFACE_NURBS && forms.get(f) == Some(&1) { Some(next_nurbs(&mut nurbs_at)?) } else { None };
|
||||
// A NURBS face's or a ball patch's own block came first (the patch's is its form); a form
|
||||
// follows the analytic face's canonical entry.
|
||||
let form = match kind {
|
||||
SURFACE_NURBS => None,
|
||||
SURFACE_BALL_PATCH if forms.get(f) == Some(&1) => return Err(invalid("ball patch with a NURBS form")),
|
||||
SURFACE_BALL_PATCH => Some(next_nurbs(&mut nurbs_at)?),
|
||||
_ if forms.get(f) == Some(&1) => Some(next_nurbs(&mut nurbs_at)?),
|
||||
_ => None,
|
||||
};
|
||||
// The record's torus is a ring torus. SMLib keeps horn and spindle tori too (a half disc
|
||||
// revolved about a line through its rim's end, its tube touching the axis, the major
|
||||
// radius a rounding below the minor): such a face is its NURBS form, unlabelled.
|
||||
|
||||
@@ -55,6 +55,67 @@ mod native_tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A 20 mm cube with every edge rounded r 2: its eight corners are SMLib's ball patches
|
||||
/// (`SmBallPatch`, flat kind 6). The record keeps each as a ball patch on the sphere of
|
||||
/// radius 2 about the corner pulled in by 2 along each axis, its net the NURBS form, and
|
||||
/// every point of the net on that sphere. Imported back, the patches are kind 6 again with
|
||||
/// the same definitions, bit for bit, and the body keeps its closed-form volume.
|
||||
#[test]
|
||||
fn a_rounded_cubes_ball_patches_go_round_with_their_spheres() {
|
||||
let (a, r) = (20.0, 2.0);
|
||||
let h = a / 2.0;
|
||||
let mids: Vec<[f64; 3]> = [(0.0, 0.0), (a, 0.0), (0.0, a), (a, a)].iter().flat_map(|&(p, q)| [[h, p, q], [p, h, q], [p, q, h]]).collect();
|
||||
let f = Brep::make_box([0.0; 3], [a; 3]).unwrap().fillet(&mids, 0.1, r).unwrap();
|
||||
let record = f.to_native().unwrap();
|
||||
record.validate().unwrap();
|
||||
let patches: Vec<(&b::Face, [f64; 3], f64)> = record
|
||||
.faces
|
||||
.iter()
|
||||
.filter_map(|face| match face.surface {
|
||||
b::Surface::BallPatch { center, radius } => Some((face, center, radius)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(patches.len(), 8, "one ball patch per corner");
|
||||
let mut centers: Vec<[f64; 3]> = patches.iter().map(|p| p.1).collect();
|
||||
centers.sort_by(|p, q| p.partial_cmp(q).unwrap());
|
||||
let inset = [r, a - r];
|
||||
let want: Vec<[f64; 3]> = (0..8).map(|i| [inset[i >> 2], inset[(i >> 1) & 1], inset[i & 1]]).collect();
|
||||
for (got, want) in centers.iter().zip(&want) {
|
||||
assert!((0..3).all(|k| (got[k] - want[k]).abs() < 1e-9), "centre {got:?}, the corner's {want:?}");
|
||||
}
|
||||
for (face, c, radius) in &patches {
|
||||
assert!((radius - r).abs() < 1e-12, "radius {radius}");
|
||||
assert!(matches!(face.nurbs, Some(b::Surface::RationalSpline { .. })), "a ball patch's form is its rational net");
|
||||
let eval = b::eval::SurfaceEval::new(face.parameters()).unwrap();
|
||||
let [[u0, u1], [v0, v1]] = face.domain;
|
||||
for (s, t) in [(0.25, 0.25), (0.5, 0.5), (0.75, 0.3), (0.1, 0.9)] {
|
||||
let q = eval.point([u0 + s * (u1 - u0), v0 + t * (v1 - v0)]).unwrap();
|
||||
let d = ((q.x - c[0]).powi(2) + (q.y - c[1]).powi(2) + (q.z - c[2]).powi(2)).sqrt();
|
||||
assert!((d - r).abs() < 1e-12 * a, "a point of the patch {} off its sphere", d - r);
|
||||
}
|
||||
}
|
||||
let back = Brep::from_record(&record, true).unwrap();
|
||||
let check = back.check(CheckLevel::All).unwrap();
|
||||
assert!(check.is_valid(), "{check}");
|
||||
let arrays = back.arrays().unwrap();
|
||||
let kinds = arrays.ints.get(&crate::flat::FACE_SURFACE_TYPE).cloned().unwrap_or_default();
|
||||
assert_eq!(kinds.iter().filter(|k| **k == i64::from(crate::flat::SURFACE_BALL_PATCH)).count(), 8);
|
||||
// The record numbers faces in canonical order, not SMLib's: the same definitions, each bit
|
||||
// for bit, in another order.
|
||||
let defs = |b: &Brep| {
|
||||
let reals = b.arrays().unwrap().reals.get(&crate::flat::FACE_DEFINITION_REALS).cloned().unwrap_or_default();
|
||||
let mut d: Vec<[u64; 4]> = reals.chunks(4).map(|c| [0, 1, 2, 3].map(|k| c[k].to_bits())).collect();
|
||||
d.sort_unstable();
|
||||
d
|
||||
};
|
||||
assert_eq!(defs(&back), defs(&f), "the definitions come back bit for bit");
|
||||
let b = a - 2.0 * r;
|
||||
let want = b.powi(3) + 6.0 * b * b * r + 3.0 * PI * r * r * b + 4.0 / 3.0 * PI * r.powi(3);
|
||||
let got = back.mass_properties(MEASURE_ACCURACY).unwrap().volume;
|
||||
assert!((got - want).abs() < 1e-6 * want, "volume {got} vs {want}");
|
||||
}
|
||||
|
||||
/// A tagged body's own history, renumbered with its export's order on both sides, is the
|
||||
/// identity in canonical order: the order says where each SMLib face and edge went.
|
||||
#[test]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! The operations block of `capi/include/smlib.h` (`smlib_boolean`, `smlib_boolean_union_all`, `smlib_boolean2d`, `smlib_fillet`,
|
||||
//! `smlib_chamfer`, `smlib_blend`, `smlib_blend_dropping`, `smlib_blend_setback`, `smlib_blend_setback_open`, `smlib_fillet_variable`, `smlib_chamfer_sides`, `smlib_shell`, `smlib_offset_faces`,
|
||||
//! `smlib_chamfer`, `smlib_blend`, `smlib_blend_dropping`, `smlib_blend_setback`, `smlib_blend_setback_open`, `smlib_blend_fit_size`, `smlib_fillet_variable`, `smlib_chamfer_sides`, `smlib_shell`, `smlib_offset_faces`,
|
||||
//! `smlib_thicken`, `smlib_draft`, `smlib_merge_faces`, `smlib_brep_solid_count`, `smlib_imprint`; the fork's `capi/src/smlib_ops.cpp`), declared by
|
||||
//! hand. Part of the FFI boundary, like its parent: called only through
|
||||
//! [`crate::ops`].
|
||||
@@ -31,6 +31,11 @@ pub const BLEND_CURVATURE: i32 = 1;
|
||||
pub const BLEND_CHORD: i32 = 2;
|
||||
pub const BLEND_CHAMFER: i32 = 3;
|
||||
|
||||
pub const FIT_SIZE: i32 = 0;
|
||||
pub const FIT_SETBACK: i32 = 1;
|
||||
pub const FIT_SCALE: i32 = 2;
|
||||
pub const FIT_SETBACK_AT_LEAST: i32 = 3;
|
||||
|
||||
/// Which selection operation: the C functions share one signature.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Pick {
|
||||
@@ -95,6 +100,7 @@ unsafe extern "C" {
|
||||
out: *mut *mut RawBrep,
|
||||
out_failed_point: *mut i64,
|
||||
) -> i32;
|
||||
fn smlib_blend_fit_size(out_size: *mut f64, out_what: *mut i32) -> i32;
|
||||
fn smlib_blend_dropping(
|
||||
brep: *const RawBrep,
|
||||
kind: i32,
|
||||
@@ -251,6 +257,19 @@ pub fn pick_op(kind: Pick, b: &Handle, points: &[[f64; 3]], tolerance: f64, size
|
||||
})
|
||||
}
|
||||
|
||||
/// `smlib_blend_fit_size`: the size that builds, and what it sizes (`FIT_*`), for the last blend
|
||||
/// call SMLib refused as too large or a setback as too short; `None` where it names none.
|
||||
#[cfg(feature = "native")]
|
||||
pub fn blend_fit_size() -> Option<(f64, i32)> {
|
||||
let (mut size, mut what) = (0.0f64, -1i32);
|
||||
(unsafe { smlib_blend_fit_size(&mut size, &mut what) } == OK).then_some((size, what))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "native"))]
|
||||
pub fn blend_fit_size() -> Option<(f64, i32)> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
pub fn thicken(b: &Handle, thickness: f64, symmetric: bool) -> Result<Handle, i32> {
|
||||
new_brep(|out| unsafe { smlib_thicken(b.raw(), thickness, i32::from(symmetric), out) })
|
||||
|
||||
@@ -39,6 +39,9 @@ pub const FACE_NURBS_V_ORDER: i32 = 31;
|
||||
pub const SOURCE: i32 = 32;
|
||||
pub const FACE_NURBS_FORM: i32 = 33;
|
||||
pub const EDGE_NURBS_FORM: i32 = 34;
|
||||
/// Per face whose kind carries a definition (a ball patch), in face order: how many reals of
|
||||
/// `FACE_DEFINITION_REALS` are its definition (a ball patch: 4).
|
||||
pub const FACE_DEFINITION_REAL_COUNT: i32 = 36;
|
||||
pub const TOLERANCE: i32 = 101;
|
||||
pub const EXTENT: i32 = 102;
|
||||
pub const FACE_RANGE: i32 = 103;
|
||||
@@ -104,6 +107,9 @@ pub const FACE_TORUS_MAJOR_RADIUS: i32 = 162;
|
||||
pub const FACE_TORUS_MINOR_RADIUS: i32 = 163;
|
||||
pub const EDGE_TOLERANCE: i32 = 164;
|
||||
pub const VERTEX_TOLERANCE: i32 = 165;
|
||||
/// The definitions counted in `FACE_DEFINITION_REAL_COUNT`, packed in face order: a ball patch's
|
||||
/// sphere's centre x, y, z and radius.
|
||||
pub const FACE_DEFINITION_REALS: i32 = 166;
|
||||
/// The largest EDGE_TOLERANCE or VERTEX_TOLERANCE import takes (smlib.h: "at most 1").
|
||||
pub const MAX_TOLERANCE: f64 = 1.0;
|
||||
/// `SMLIB_FLAT_MAX_LEN`: no array is longer.
|
||||
@@ -114,6 +120,9 @@ pub const SURFACE_CYLINDER: u8 = 2;
|
||||
pub const SURFACE_CONE: u8 = 3;
|
||||
pub const SURFACE_SPHERE: u8 = 4;
|
||||
pub const SURFACE_TORUS: u8 = 5;
|
||||
/// A rational patch on a sphere (SMLib's `SmBallPatch`): its NURBS block, range and trims as a
|
||||
/// NURBS face's, and its sphere in `FACE_DEFINITION_REALS`.
|
||||
pub const SURFACE_BALL_PATCH: u8 = 6;
|
||||
pub const CURVE_NURBS: u8 = 0;
|
||||
pub const CURVE_LINE: u8 = 1;
|
||||
pub const CURVE_CIRCLE: u8 = 2;
|
||||
|
||||
@@ -186,17 +186,19 @@ fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool, bare: &HashSet<(u32,
|
||||
a.i(flat::FACE_TRIM_TYPE).push(0);
|
||||
// A face with a NURBS form: what it is (its analytic entry), then the NURBS it is
|
||||
// parameterised on, whose range and trims these are (FACE_NURBS_FORM). A revolution
|
||||
// (SMLib has no class for one) is that form alone, a NURBS face.
|
||||
let form_is_face = f.nurbs.is_some() && matches!(f.surface, b::Surface::PlacedRevolution { .. });
|
||||
let spline = match &f.nurbs {
|
||||
Some(n) if form_is_face => surface(&mut a, n, f.domain, analytic)?,
|
||||
Some(n) => {
|
||||
// (SMLib has no class for one) is that form alone, a NURBS face. A ball patch is its form
|
||||
// too, as a NURBS face's block under its own kind, with its sphere as its definition.
|
||||
let form_is_face = f.nurbs.is_some() && matches!(f.surface, b::Surface::PlacedRevolution { .. } | b::Surface::BallPatch { .. });
|
||||
let spline = match (&f.nurbs, &f.surface) {
|
||||
(Some(n), b::Surface::BallPatch { center, radius }) => ball_patch(&mut a, n, *center, *radius, f.domain)?,
|
||||
(Some(n), _) if form_is_face => surface(&mut a, n, f.domain, analytic)?,
|
||||
(Some(n), _) => {
|
||||
surface(&mut a, &f.surface, f.domain, analytic)?;
|
||||
nurbs_block(&mut a, n)?;
|
||||
true
|
||||
}
|
||||
// SMLib's tube angle counts from the outer equator, the record's from its tube start.
|
||||
None => match f.surface {
|
||||
(None, _) => match f.surface {
|
||||
b::Surface::Torus { tube_start, .. } => {
|
||||
let [u, v] = f.domain;
|
||||
surface(&mut a, &f.surface, [u, v.map(|t| t + tube_start)], analytic)?
|
||||
@@ -337,6 +339,19 @@ fn nurbs_block(a: &mut Arrays, s: &b::Surface) -> R<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A ball patch's face: its net `n` as a NURBS face's block and range under the ball patch's kind,
|
||||
/// and its sphere as its definition (SMLib checks the net is on it). True: it carries trims.
|
||||
fn ball_patch(a: &mut Arrays, n: &b::Surface, center: [f64; 3], radius: f64, domain: [[f64; 2]; 2]) -> R<bool> {
|
||||
let [[u0, u1], [v0, v1]] = domain;
|
||||
a.i(flat::FACE_SURFACE_TYPE).push(i64::from(flat::SURFACE_BALL_PATCH));
|
||||
nurbs_block(a, n)?;
|
||||
a.r(flat::FACE_RANGE).extend([u0, v0, u1, v1]);
|
||||
a.i(flat::FACE_DEFINITION_REAL_COUNT).push(4);
|
||||
a.r3(flat::FACE_DEFINITION_REALS, center);
|
||||
a.r(flat::FACE_DEFINITION_REALS).push(radius);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// The face's surface and range; true for a B-spline surface (which carries trims).
|
||||
fn surface(a: &mut Arrays, s: &b::Surface, domain: [[f64; 2]; 2], analytic_planes: bool) -> R<bool> {
|
||||
let [[u0, u1], [v0, v1]] = domain;
|
||||
@@ -385,6 +400,7 @@ fn surface(a: &mut Arrays, s: &b::Surface, domain: [[f64; 2]; 2], analytic_plane
|
||||
return Ok(true);
|
||||
}
|
||||
b::Surface::PlacedRevolution { profile, origin, axis, matrix, forward } => revolution(a, profile, *origin, *axis, matrix, *forward, domain)?,
|
||||
b::Surface::BallPatch { .. } => return Err(Unsupported::Invalid("a ball patch without its NURBS form")),
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ pub mod trim;
|
||||
|
||||
pub use check::{CheckLevel, CheckReport};
|
||||
pub use measure::{MEASURE_ACCURACY, MassProperties, PointClass};
|
||||
pub use ops::{Blend, BooleanOp, ChamferSecond, Hole, HoleEdge, MAX_POINTS, MAX_UNION_BREPS};
|
||||
pub use ops::{Blend, BooleanOp, ChamferSecond, Fit, Hole, HoleEdge, MAX_POINTS, MAX_UNION_BREPS, blend_fit};
|
||||
|
||||
/// Why an SMLib call failed.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
||||
@@ -8,6 +8,35 @@
|
||||
|
||||
use crate::{Brep, Error, Result, error, ffi, init};
|
||||
|
||||
/// What size builds a blend SMLib refused ([`blend_fit`]): from the refusal's own cause, never by
|
||||
/// building again.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Fit {
|
||||
/// The largest size (a round's radius, a chamfer's distance, a chord's width) that builds.
|
||||
Size(f64),
|
||||
/// The largest setback of a set-back corner that fits (the one asked for is too long).
|
||||
Setback(f64),
|
||||
/// The factor (< 1) a two-distance chamfer's distances are both scaled by to build.
|
||||
Scale(f64),
|
||||
/// The smallest setback that builds the corner (the one asked for is too short: its fill is
|
||||
/// certified convex only from there).
|
||||
SetbackAtLeast(f64),
|
||||
}
|
||||
|
||||
/// The size that builds the last blend this process's SMLib refused as too large
|
||||
/// ([`Error::NoRoom`]) or whose setback it refused as too short; `None` where it names no single
|
||||
/// size. Read it right after the refused call: SMLib keeps only the last one.
|
||||
pub fn blend_fit() -> Option<Fit> {
|
||||
let (size, what) = ffi::ops::blend_fit_size()?;
|
||||
match what {
|
||||
ffi::ops::FIT_SIZE => Some(Fit::Size(size)),
|
||||
ffi::ops::FIT_SETBACK => Some(Fit::Setback(size)),
|
||||
ffi::ops::FIT_SCALE => Some(Fit::Scale(size)),
|
||||
ffi::ops::FIT_SETBACK_AT_LEAST => Some(Fit::SetbackAtLeast(size)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// How [`Brep::boolean`] combines two bodies.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum BooleanOp {
|
||||
|
||||
@@ -286,37 +286,30 @@ fn blends_take_a_size_per_edge() {
|
||||
assert!(c.blend(Blend::Round, &tops, &[2.0], 0.1).is_err());
|
||||
}
|
||||
|
||||
/// A radius running linearly from 3 to 1 along a cube edge: (1 - pi/4) L (r0² + r0 r1 + r1²) / 3
|
||||
/// taken away, whichever end is the start.
|
||||
/// A variable round is the union of its balls: in each plane square to the edge, the corner cut off
|
||||
/// by rays from it, each meeting the union first at the least entry over the balls, integrated
|
||||
/// along the edge (a linear law's balls envelop a cone). The balls' great circles, (1 - pi/4) r²
|
||||
/// per unit length, take more: 18.5988 for 3 -> 1, 39.0796 for 4, 1, 4 and 22.8945 for 4 -> 0.
|
||||
/// On the 20 cube's edge y = z = 20, 3 -> 1 takes 18.5377834 whichever end is the start.
|
||||
#[test]
|
||||
fn a_variable_round_follows_its_law() {
|
||||
let c = cube(20.0);
|
||||
let q = 1.0 - std::f64::consts::FRAC_PI_4;
|
||||
let knots = [0.0, 0.0, 1.0, 1.0];
|
||||
let r = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 1, &knots, &[3.0, 1.0]).unwrap();
|
||||
shot(&r, "variable-3-1");
|
||||
assert_close(volume(&r), 8000.0 - q * 20.0 * 13.0 / 3.0, 1e-5, "linear law");
|
||||
assert_close(volume(&r), 8000.0 - 18.5377834, 1e-5, "linear law");
|
||||
let back = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [20.0, 20.0, 20.0], 1, &knots, &[1.0, 3.0]).unwrap();
|
||||
assert_close(volume(&back), volume(&r), 1e-7, "reversed");
|
||||
assert!(c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 1, &[0.0, 0.5, 1.0, 1.0], &[3.0, 1.0]).is_err());
|
||||
assert!(matches!(c.fillet_variable([90.0, 90.0, 90.0], 0.1, [0.0; 3], 1, &knots, &[3.0, 1.0]), Err(Error::NoMatch { .. })));
|
||||
// A quadratic law with control radii 4, 1, 4: r(t) = 4(1 − t)² + 2t(1 − t) + 4t².
|
||||
let law = |t: f64| 4.0 * (1.0 - t).powi(2) + 2.0 * t * (1.0 - t) + 4.0 * t * t;
|
||||
let n = 4000;
|
||||
let r2 = (0..n).map(|i| law((i as f64 + 0.5) / n as f64).powi(2)).sum::<f64>() / n as f64;
|
||||
// A quadratic law with control radii 4, 1, 4, r(t) = 4(1 − t)² + 2t(1 − t) + 4t²: 39.019820.
|
||||
let quad = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 2, &[0.0, 0.0, 0.0, 1.0, 1.0, 1.0], &[4.0, 1.0, 4.0]).unwrap();
|
||||
shot(&quad, "variable-quadratic");
|
||||
assert_close(volume(&quad), 8000.0 - q * 20.0 * r2, 1e-5, "quadratic law");
|
||||
// Down to zero at the far end: the round closes to the edge's end point.
|
||||
assert_close(volume(&quad), 8000.0 - 39.019820, 1e-5, "quadratic law");
|
||||
// Down to zero at the far end: the round closes to the edge's end point, 22.594877.
|
||||
let zero = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 1, &knots, &[4.0, 0.0]).unwrap();
|
||||
shot(&zero, "variable-to-zero");
|
||||
assert_close(volume(&zero), 8000.0 - q * 20.0 * 16.0 / 3.0, 1e-5, "down to zero");
|
||||
// A law with a corner (4 → 2 → 4, straight on both halves): the round turns at the corner,
|
||||
// each half taking away (1 - pi/4) (L / 2) (16 + 8 + 4) / 3; the same points joined smoothly
|
||||
// (C1 cubic pieces, as RadiusSpline::through makes them) are built too.
|
||||
let corner = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 1, &[0.0, 0.0, 0.5, 1.0, 1.0], &[4.0, 2.0, 4.0]).unwrap();
|
||||
shot(&corner, "variable-corner-4-2-4");
|
||||
assert_close(volume(&corner), 8000.0 - q * 20.0 * 28.0 / 3.0, 1e-6, "corner law");
|
||||
assert_close(volume(&zero), 8000.0 - 22.594877, 1e-5, "down to zero");
|
||||
// A law down to zero inside the edge would pinch the round to a point: refused.
|
||||
let pinch = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 1, &[0.0, 0.0, 0.5, 1.0, 1.0], &[4.0, 0.0, 4.0]);
|
||||
assert!(pinch.is_err(), "a law pinching to zero inside the edge");
|
||||
@@ -325,6 +318,17 @@ fn a_variable_round_follows_its_law() {
|
||||
shot(&smooth.unwrap(), "variable-smooth-4-2-4");
|
||||
}
|
||||
|
||||
/// A law with a corner (4 → 2 → 4, straight on both halves) on the 20 cube's edge y = z = 20: the
|
||||
/// union of the balls, with the corner ball's sphere between the halves' touch circles, takes
|
||||
/// 39.542073 (the halves' great circles would take 40.0590).
|
||||
#[test]
|
||||
fn a_variable_round_with_a_corner_in_its_law_is_its_balls_union() {
|
||||
let c = cube(20.0);
|
||||
let corner = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 1, &[0.0, 0.0, 0.5, 1.0, 1.0], &[4.0, 2.0, 4.0]).unwrap();
|
||||
shot(&corner, "variable-corner-4-2-4");
|
||||
assert_close(volume(&corner), 8000.0 - 39.542073, 1e-5, "corner law");
|
||||
}
|
||||
|
||||
/// Unequal chamfers of a cube's top edge y = 20: 5 back along the face facing `first` most and 2
|
||||
/// along the other take away a triangle 5 × 2 / 2 along the 20 mm edge, either way round; 5 at
|
||||
/// 30° to the first face is 5 and 5 tan 30°.
|
||||
|
||||
@@ -135,7 +135,8 @@ impl Surface {
|
||||
let degree = [u_knots.len().checked_sub(nu + 1).ok_or(Error::Argument)?, v_knots.len().checked_sub(nv + 1).ok_or(Error::Argument)?];
|
||||
Surface::nurbs(degree, nu, nv, u_knots, v_knots, &flat, Some(&weights))
|
||||
}
|
||||
S::PlacedRevolution { .. } => Err(Error::Unsupported),
|
||||
// A ball patch's label has no (u, v): its face's NURBS form is the surface.
|
||||
S::PlacedRevolution { .. } | S::BallPatch { .. } => Err(Error::Unsupported),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -279,9 +279,16 @@ pub fn surface(s: &BSurface) -> R<(Surface, Map)> {
|
||||
(bspline_surface(u_knots, v_knots, points, Some(weights))?, Map::IDENTITY)
|
||||
}
|
||||
BSurface::PlacedRevolution { profile, origin, axis, matrix, forward } => revolution(profile, *origin, *axis, matrix, *forward)?,
|
||||
BSurface::BallPatch { .. } => return Err("a ball patch's label (its face is written as its NURBS form)".into()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether a face is written as its NURBS form, pcurves and all: a ball patch, which STEP has no
|
||||
/// entity for, is the B-spline surface it is parameterised on.
|
||||
fn as_form(face: &Face) -> bool {
|
||||
face.nurbs.is_some() && matches!(face.surface, BSurface::BallPatch { .. })
|
||||
}
|
||||
|
||||
/// A record pcurve in the STEP surface's parameters: its control points through `map` (a linear
|
||||
/// map, exact on a B-spline and on a NURBS's homogeneous coefficients).
|
||||
pub fn pcurve(c: &Curve2, map: Map) -> R<Curve> {
|
||||
@@ -455,6 +462,13 @@ impl BrepWriter<'_, '_, '_> {
|
||||
return Ok(s.clone());
|
||||
}
|
||||
let face = face_of(self.b, f)?;
|
||||
if as_form(face) {
|
||||
let s = (Arc::new(surface(face.parameters()).map_err(|m| format!("face {f}: {m}"))?.0), Map::IDENTITY);
|
||||
if let Some(slot) = self.surfaces.get_mut(f as usize) {
|
||||
*slot = Some(s.clone());
|
||||
}
|
||||
return Ok(s);
|
||||
}
|
||||
let (s, map) = surface(&face.surface).map_err(|m| format!("face {f}: {m}"))?;
|
||||
// A face on its NURBS form is written as the analytic surface it is, with no pcurves
|
||||
// (STEP's are in the angle); only which way its normal points carries over.
|
||||
@@ -511,7 +525,7 @@ impl BrepWriter<'_, '_, '_> {
|
||||
let mut associated = Vec::with_capacity(edge.uses.len());
|
||||
for u in &edge.uses {
|
||||
let face = face_of(b, u.face)?;
|
||||
if face.nurbs.is_some() {
|
||||
if face.nurbs.is_some() && !as_form(face) {
|
||||
continue;
|
||||
}
|
||||
let Some(Loop::Edges { edges }) = face.loops.get(u.r#loop as usize) else {
|
||||
|
||||
@@ -2665,7 +2665,7 @@ fn error_rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui: &mut
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum FilletRefusal {
|
||||
/// In these plain words.
|
||||
Plain(&'static str),
|
||||
Plain(String),
|
||||
/// Not again: the Setback field already flags the value and names the least it takes.
|
||||
InField,
|
||||
}
|
||||
@@ -2680,6 +2680,12 @@ pub(crate) fn fillet_plain_error(error: &str, setback_in_field: bool) -> Option<
|
||||
if setback_in_field && error.contains("the setback must be more than") {
|
||||
return Some(FilletRefusal::InField);
|
||||
}
|
||||
// A setback too short for its corner's patch: the least that builds, as the kernel names it
|
||||
if let Some((_, rest)) = error.split_once(solidcraft_engine::kernel::SETBACK_AT_LEAST)
|
||||
&& let Some(least) = rest.split_once(" mm").map(|(n, _)| n.trim()).filter(|n| n.parse::<f64>().is_ok())
|
||||
{
|
||||
return Some(FilletRefusal::Plain(crate::i18n::format("ui.fillet_error_setback_short", &[("least", &format!("{least} mm"))])));
|
||||
}
|
||||
let too_large = error.contains("larger than the faces beside its edges allow");
|
||||
let corner = error.contains("setback") || error.contains("G2 corner") || error.contains("corner patch");
|
||||
let kernel = error.contains("SMLib")
|
||||
@@ -2688,11 +2694,11 @@ pub(crate) fn fillet_plain_error(error: &str, setback_in_field: bool) -> Option<
|
||||
|| error.contains("kernel internal error")
|
||||
|| error.contains("the rounds would leave openings");
|
||||
if too_large {
|
||||
Some(FilletRefusal::Plain(tr("ui.fillet_error_too_large")))
|
||||
Some(FilletRefusal::Plain(tr("ui.fillet_error_too_large").to_string()))
|
||||
} else if corner && kernel {
|
||||
Some(FilletRefusal::Plain(tr("ui.fillet_error_corner")))
|
||||
Some(FilletRefusal::Plain(tr("ui.fillet_error_corner").to_string()))
|
||||
} else if kernel {
|
||||
Some(FilletRefusal::Plain(tr("ui.fillet_error_failed")))
|
||||
Some(FilletRefusal::Plain(tr("ui.fillet_error_failed").to_string()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1416,6 +1416,26 @@ mod tests {
|
||||
|
||||
/// A smooth-patch fillet reopens with its corner and setback, and OK writes them as saved; a
|
||||
/// setback list the dialog cannot show as one distance is kept as it is.
|
||||
/// A setback too short for SMLib's corner fill (past the least the field flags) is refused by
|
||||
/// the kernel naming the smallest that builds; the dialog says it in plain words, that length
|
||||
/// in them.
|
||||
#[test]
|
||||
fn a_setback_too_short_for_the_corner_says_the_least_that_builds() {
|
||||
let refused = format!(
|
||||
"invalid parameters for `solid.fillet`: the setback is too short for its corner: {}5.9620 mm (the corner's patch is convex only from there)",
|
||||
solidcraft_engine::kernel::SETBACK_AT_LEAST
|
||||
);
|
||||
match crate::dialogs::fillet_plain_error(&refused, true) {
|
||||
Some(crate::dialogs::FilletRefusal::Plain(plain)) => assert!(plain.contains("5.9620 mm"), "{plain}"),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
// A blend too large for its faces keeps its own words.
|
||||
assert_eq!(
|
||||
crate::dialogs::fillet_plain_error("the fillet is larger than the faces beside its edges allow", true),
|
||||
Some(crate::dialogs::FilletRefusal::Plain(crate::i18n::tr("ui.fillet_error_too_large").to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_saved_smooth_patch_reopens_with_its_setback() {
|
||||
for (setbacks, field) in [
|
||||
|
||||
@@ -139,6 +139,7 @@ Natural normals are ∂S/∂u × ∂S/∂v.
|
||||
| `cone` {frame, radius_at_origin, half_angle} | origin + v z + r(v) (cos u x + sin u y), r(v) = radius_at_origin + v tan(half_angle) | angle, periodic | height, on the side of the apex where r(v) ≥ 0 (a negative half angle narrows along z) |
|
||||
| `sphere` {frame, radius} | origin + r (cos v (cos u x + sin u y) + sin v z) | longitude, periodic | latitude, [−π/2, π/2] |
|
||||
| `torus` {frame, major_radius, minor_radius} | origin + (R + r cos v)(cos u x + sin u y) + r sin v z | longitude, periodic | meridian angle, periodic |
|
||||
| `ball_patch` {center, radius} | its `nurbs`: a label with no (u, v) of its own | its form's | its form's |
|
||||
| `spline` {u_knots, v_knots, points} | tensor-product B-spline, `points[i][j]` with i along u | u knot domain | v knot domain |
|
||||
| `rational_spline` {u_knots, v_knots, homogeneous} | the same, homogeneous coefficients | u knot domain | v knot domain |
|
||||
| `placed_revolution` {profile, origin, axis, matrix, forward} | M · R(angle) · profile(s): the profile revolved about origin and unit axis by the right-hand rule, then mapped by the column-major affine matrix M | `forward`: s, the profile's parameter; else the angle | `forward`: the angle, [0, 2π); else s |
|
||||
@@ -159,6 +160,15 @@ only says what the face is (axis, radius). Pcurves on such a face are SMLib's
|
||||
own trims, with nothing fitted. A STEP writer writes the analytic surface and
|
||||
the face's edges without pcurves.
|
||||
|
||||
A `ball_patch` face always carries `nurbs`: the rational patch on the sphere of
|
||||
`center` and `radius` that SMLib makes at the corner of three equal rounds
|
||||
(`SmBallPatch`, flat surface kind 6). The patch is its own net, not a piece of
|
||||
SMLib's sphere NURBS, so it goes back into SMLib as kind 6 with that sphere as
|
||||
its definition, and SMLib takes it only when every point of the net is on the
|
||||
sphere. The sphere is kept because its offsets are exact and collapse at its
|
||||
centre. A STEP writer, with no entity for it, writes the net as the face's
|
||||
B-spline surface, pcurves included.
|
||||
|
||||
### Parameter ranges
|
||||
|
||||
An edge interval and each side of a face domain are finite with min < max. In a
|
||||
|
||||
@@ -14,6 +14,7 @@ description: "Use when a SolidCraft change is slow, blocks the window, runs long
|
||||
## Measure before you claim a speed-up
|
||||
- Profile first. Time with `solidcraft-cli` from `main` and from your branch, alternating runs, after the load is low. On a shared machine check `cut -d' ' -f1 /proc/loadavg` and wait while it is over 24; build with `nice`.
|
||||
- Report medians with the range, the machine, and the cases. The pattern to follow is `docs/perf.md`: what was slow, why, the result, and proof that nothing changed (same bodies, volumes, areas and triangle counts).
|
||||
- A speed-up never skips work on a mesh's box: decide with the exact bounds (`Body::exact_bounds`, grown by the record's tolerance as `apart` in `crates/kernel/src/ops.rs` does). A mesh is for display, export, previews and mesh bodies.
|
||||
- Do not rewrite kernel code for speed: the kernel is being replaced, and kernel work is paused. Report a slow case with its file under the `kernel-perf` label (CONTRIBUTING.md, "Where help is wanted most").
|
||||
- Pull requests run only the tests for what they touch; geometry-heavy tests run in the merge queue. Use `cargo xtask test` with the `ci` profile locally (`skills/solidcraft-tests/SKILL.md`).
|
||||
|
||||
|
||||
@@ -7,13 +7,27 @@
|
||||
# directory each (`gh run download <run> -R tangency-labs/solidcraft -n <artifact> -D <dir>/<artifact>`
|
||||
# for junit and junit-gpu-1..4), run `cargo xtask known-failing --junit <dir>`, then drop each
|
||||
# FIXED line and add each REGRESSION line with its cause, from the test's failure message.
|
||||
# Baseline: develop 38fd4a4f6 (CI run 41157648345, x86_64 Linux), after the SMLib eaa998a pin.
|
||||
solidcraft-cli::action_matrix::action_matrix_sample 3 baseline actions fail on SMLib: 2 shells (Bad Loop Orient on issue-616-lid-ribs; status 1001 on filleted-shelled-box, shelled again open at an inner wall), 1 fillet (SmSurfOfRevolution Test #7 on oblique_cut_of_cylinder)
|
||||
# Baseline: develop 2bf58241f (CI run 41522001173, x86_64 Linux), after the SMLib a18bbe5 pin (v1.1.0-alpha).
|
||||
solidcraft-cli::action_matrix::action_matrix_sample 2 baseline actions fail on SMLib: 2 shells (Bad Loop Orient on issue-616-lid-ribs; status 1001 on filleted-shelled-box, shelled again open at an inner wall)
|
||||
solidcraft-cli::naming_corpus::corpus_designs_name_alike_every_build_and_fail_where_a_reference_is_gone oracle/83-boundary-loop-thicken does not load: step 8, SMLib thicken status 1001
|
||||
solidcraft-doc::eval::chain_tests::a_propagated_chain_that_cannot_build_says_how_to_blend_the_edge_alone the propagated chain now builds a round (9 faces), so there is no refusal to read, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-engine::tests::fillet_fit_taper_closes_to_a_point SMLib fillet status 1001 on the taper fit (radius 16 closing to 0)
|
||||
solidcraft-engine::tests::robustness::fillet_into_an_earlier_round R2 into the earlier round: volume 35923.387 against 35919.524, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::blend::corner_tests::a_round_ending_in_an_earlier_round_matches_both_together SMLib fillet status 1001 on the round that ends in the earlier one
|
||||
solidcraft-kernel::blend::corner_tests::corner_blends_hold_at_any_scale at 0.01x the arc case's rounds remove 72.783 (scaled back) against 72.713 at 1x (9.6e-4 off, 1e-4 allowed)
|
||||
solidcraft-kernel::blend::corner_tests::corner_blends_hold_at_any_scale at 0.01x a picked edge runs on tangentially into an unblended edge and SMLib's result is not a closed solid (the smooth-vertex End, tangency-labs/smlib#81)
|
||||
solidcraft-kernel::blend_corner::tests::a_round_next_to_a_round_of_another_radius the face count is 9, not the expected count, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::blend_corner::tests::issue2422_vertical_after_separate_bottom_rounds SMLib's check refuses the fillet: edge and vertex tolerance 0.97 against the body's 0.01 (#2422)
|
||||
solidcraft-kernel::blend_smlib::tests::smooth_edges_and_unfinished_tangent_chains_are_refused_saying_why the smooth edge builds (volume 7957.723) where a refusal is expected, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::corner_g2::tests::a_concave_corner_takes_the_g2_patch_too the concave G2 corner's curvature jump clear of the corners is 5.5e-2, above the bound, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::corner_g2::tests::a_long_setback_keeps_the_g2_patch_inside_the_box at set backs 11 to 15 the G2 corner patch bulges 2.6e-6 to 1.33e-3 past the box (the explicit corner is in progress)
|
||||
solidcraft-kernel::corner_g2::tests::curvature_rounds_meet_at_a_g2_corner the corner loop's fill misses its neighbours' curvature by 1.6e-3 (tolerance 1e-3), on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::corner_g2::tests::set_back_8_the_g2_corner_lies_between_the_hull_and_the_prisms set back 8: the G2 corner's distance check reads 0.0611, past its bound (volume 47370.642, in range), on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::corner_g2::tests::the_box_corner_g2_patch_is_a_closed_solid_curvature_continuous_to_its_neighbours the fitted G2 corner patch bulges 1.7e-5 past the box, which SMLib's exact tight box now sees (the explicit G2 corner builds it inside)
|
||||
solidcraft-kernel::tests::fillet_tabbed_plate_outer_loop_and_partial_selection_after_hole the arc and tab rounds together remove 9.20067 against 9.20420 (3.8e-4 off, 1e-5 allowed)
|
||||
solidcraft-kernel::issue_851_tests::blend_the_arc_top_edge_and_the_top_front_edge removes 59.148 against 60.122, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::issue_851_tests::fillet_the_arc_top_edge_and_the_front_upright_at_the_arc removes 31.497 against 32.310, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::issue_851_tests::fillet_the_top_front_edge_and_the_front_left_upright removes 42.236 against 43.187, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::issue_851_tests::fillet_the_top_front_edge_and_the_front_upright_at_the_arc removes 41.341 against 42.146, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::smooth_edges::tests::a_seam_split_in_two_edges_is_drawn_whole the side seam is one edge ([24]), not split in two, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::tests::fillet_tabbed_plate_outer_loop_and_partial_selection_after_hole the partial selection runs on tangentially into an unblended edge and SMLib's result is not a closed solid (the smooth-vertex End, tangency-labs/smlib#81)
|
||||
solidcraft-smlib::ops::tests::a_setback_corner_left_open_returns_its_hole the open corner returns holes of 9, 27 and 19 edges, not its one hole; SMLib's open_holes counts a seam edge its own face uses twice as free (fixed for v1.1.0-alpha.1)
|
||||
solidcraft-smlib::ops::tests::fillet_takes_exactly_the_picked_edges one round on the picked edge gives 9 faces, not the expected count, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
|
||||
@@ -109,7 +109,7 @@ the frozen truck-geometry PRs, a `kernel-paused` PR), for the report's coverage
|
||||
| `volume_mm3`, `area_mm2` as `[min, max]` | An inclusive range, where analysis only bounds the measure: a cut removes at most its tool's volume, a join adds at most the tool's. `method` derives both ends. |
|
||||
| `continuity` | `{order, edges}`: every seam `edges` names reaches `order` (`g0` … `g4`), as Continuity Analysis (`inspect.continuity`) measures it: from the faces' exact surfaces, at its default 33 stations a seam and its default tolerances (1 µm, 1°, 0.01 / mm, 0.001 / mm², 0.0001 / mm³), never loosened per case. A failure names the worst seam (lowest order; of those, the one furthest past the next order's tolerance), its faces, the order it reaches and that next order's worst value. `edges`: `blend` (default), every seam of a face a fillet or chamfer made (a round, a chamfer face or a corner patch) with the faces either side of the edge it replaced, and every seam of a corner patch with the rounds it joins: what the blend is built to meet (not where a round runs into an end face, nor where two rounds meet without a patch: that seam is the input's, tangent only where the rounded edges were); `corner`, those of them on a corner patch (a blend made from several replaced edges); or a list of points `[[x,y,z], …]`, the edges nearest them, as Continuity Analysis picks them. `exact: true`: every seam reaches `order` and not the next (a case built to an order proves the measurement reads that order, no more and no less). A constant round meets its faces G1 (curvature jumps by 1 / r), a `curvature` round G2, a chamfer G0; a `corner: g2` patch is built G2 to the rounds and faces round it. |
|
||||
| `continuity` (older form) | `{near, within_mm, clear_mm, curvature_jump_per_mm}`: over every tangent seam lying wholly within `within_mm` of `near` (a corner's patch and its neighbours), the curvature jump across it (the faces' normal curvatures square to the seam, from their exact surfaces, at 65 stations a seam) is at most `curvature_jump_per_mm` at every station at least `clear_mm` along the seam from a vertex where two of those seams turn (where a neighbour's own G1 seam ends, which no surface is G2 to both sides of). A rolling ball's corner jumps 1 / r everywhere. |
|
||||
| `surface_types` | The set of kinds the faces' surfaces are, over every body, as the records hold them: `plane`, `cylinder`, `cone`, `sphere`, `torus`, `bspline` (a B-spline or NURBS surface) or `revolution` (a revolved curve). Exact: none missing, none extra. |
|
||||
| `surface_types` | The set of kinds the faces' surfaces are, over every body, as the records hold them: `plane`, `cylinder`, `cone`, `sphere`, `torus`, `ball_patch` (a corner's patch on a sphere, its NURBS net), `bspline` (a B-spline or NURBS surface) or `revolution` (a revolved curve). Exact: none missing, none extra. |
|
||||
| `splines` | `[{degree: [p, q], spans?: [m, n]}, …]`: one entry per B-spline face, each matching one face (directions in either order); `spans`, the distinct knot intervals each way, only where the construction fixes them. |
|
||||
| `boundary_edges` | The edges only one face uses (a sheet's free boundary), over every body, as the records hold them; exact or `[min, max]`. |
|
||||
| `flat_pattern` | A sheet metal case's flat pattern, from the script's last `sheet.flat_pattern` result: `size_mm` ([w, h] of the outline), `area_mm2` (the outline's area less the cut-outs', relative tolerance as volume) and `bend_lines` (each `[[x, y], [x, y]]`, a bend's centre line in flat coordinates, matched one for one, either way round, and as many as the result has), positions within the box-diagonal tolerance. |
|
||||
|
||||
+4
-4
@@ -5,11 +5,11 @@
|
||||
],
|
||||
"what": "cut of a box by a swept G2 corner-removal tool along a projected edge chain (left-rear corner; the symmetric front-left chain works)",
|
||||
"source": "analytic",
|
||||
"method": "fillet: the documented curvature section, the quintic Bezier on setback s = 5 (controls p+s t1, p+2s/3 t1, p+s/3 t1, p+s/3 t2, p+2s/3 t2, p+s t2), cuts 113/21 * 25/36 = 3.73677 mm^2 off each vertical edge (Green on the Bezier): Vf = 8000 - 4*10*3.73677 = 7850.529. Tool section: the G2 blend between two lines is the degree 5 Hermite Bezier of the lines' own derivatives, controls (0.8,0) (0.8,0.04) (0.8,0.08) (0.08,0.8) (0.04,0.8) (0,0.8); the part of the profile in the material quadrant has area a = 0.360667 and first moment about the wall S = 0.102152. Swept along the corner curve (length 8.21299, total turn pi/2, curvature at most 0.269, so 1 - k*0.8 > 0 and the tube does not fold), it removes a*L - S*pi/2 = 2.8015; along l3, a straight bottom edge next to that corner (10 long on the left, 30 on the rear: the chain's naming fixes which, analysis does not), a*10 = 3.6067 or a*30 = 10.820. V lies between 7850.529 - 13.622 = 7836.907 and 7850.529 - 6.408 = 7844.121, widened by 1e-4 relative for the measurement: a cut that did nothing (7850.53) or a fillet not made falls outside. Box: the 40 x 20 x 10 box's (the fillets and the cut leave every extreme face)",
|
||||
"method": "fillet: the documented curvature section, the order-2 section at the equal-area thumbweight on setback s = 5 between square planes (cov-fillet-section-curvature-box-edge-r3), cuts (1 - pi/4) * 25 = 5.36504592 mm^2 off each vertical edge (Green's theorem on it): Vf = 8000 - 4*10*5.36504592 = 7785.398. Tool section: the G2 blend between two lines is the degree 5 Hermite Bezier of the lines' own derivatives, controls (0.8,0) (0.8,0.04) (0.8,0.08) (0.08,0.8) (0.04,0.8) (0,0.8); the part of the profile in the material quadrant has area a = 0.360667 and first moment about the wall S = 0.102152. Swept along the corner curve (length 8.21299, total turn pi/2, curvature at most 0.269, so 1 - k*0.8 > 0 and the tube does not fold), it removes a*L - S*pi/2 = 2.8015; along l3, a straight bottom edge next to that corner (10 long on the left, 30 on the rear: the chain's naming fixes which, analysis does not), a*10 = 3.6067 or a*30 = 10.820. V lies between 7785.398 - 13.622 = 7771.776 and 7785.398 - 6.408 = 7778.990, widened by 1e-4 relative for the measurement: a cut that did nothing (7785.40) or a fillet not made falls outside. Box: the 40 x 20 x 10 box's (the fillets and the cut leave every extreme face)",
|
||||
"body_count": 1,
|
||||
"volume_mm3": [
|
||||
7836.1,
|
||||
7844.9
|
||||
7771.0,
|
||||
7779.8
|
||||
],
|
||||
"bbox": {
|
||||
"min": [
|
||||
@@ -23,4 +23,4 @@
|
||||
10.0
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
]
|
||||
],
|
||||
"radius": 7,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
]
|
||||
],
|
||||
"radius": 8,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
]
|
||||
],
|
||||
"distance": 10,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"sources": [
|
||||
"tangency-labs/solidcraft#1207"
|
||||
],
|
||||
"what": "an edge that runs on tangentially into a round left out of the blend (tangent chain off) is rounded on past the round's end arc until it leaves the material",
|
||||
"source": "analytic",
|
||||
"method": "cube 20 (V0 = 8000), its top back edge (y=20, z=20) rounded r3: removes 20 (9 - 9 pi/4) = 38.628316. The end face's top edge (x=0, z=20, y 0..17) then rounded r1 alone, tangent_chain false: over y 0..17 it removes 17 (1 - pi/4) = 3.648231; at y=17 the edge goes on tangentially into the r3 round's end arc, and the r1 round's cylinder (axis x=1, z=19) runs on along y through the r3 round, cutting the part of its section under the round's surface z = 17 + sqrt(9 - (y-17)^2) until that surface drops below z=19 at y = 17 + sqrt(5): with t = sqrt(9 - (y-17)^2) - 2, the section's area under it is t - (t sqrt(1-t^2) + asin t)/2, integrated over y 17..17+sqrt(5) (Simpson, 200000 steps) = 0.218236. V = 8000 - 38.628316 - 3.648231 - 0.218236 = 7957.505202",
|
||||
"outcome": "solid",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 7957.505202,
|
||||
"continuity": {
|
||||
"order": "g1"
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"command": "solid.box",
|
||||
"params": {
|
||||
"length": 20,
|
||||
"width": 20,
|
||||
"height": 20,
|
||||
"body_name": "Cube"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.fillet",
|
||||
"params": {
|
||||
"edges": [
|
||||
[
|
||||
10,
|
||||
20,
|
||||
20
|
||||
]
|
||||
],
|
||||
"radius": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.fillet",
|
||||
"params": {
|
||||
"edges": [
|
||||
[
|
||||
0,
|
||||
8,
|
||||
20
|
||||
]
|
||||
],
|
||||
"radius": 1,
|
||||
"tangent_chain": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+5
-4
@@ -2,13 +2,14 @@
|
||||
"sources": [
|
||||
"tangency-labs/solidcraft#1207"
|
||||
],
|
||||
"what": "an edge that runs on tangentially into a round left out of the blend (tangent chain off) is rounded on past the round's end arc until it leaves the material",
|
||||
"what": "an edge that runs on tangentially into a round's end arc, its tangent chain rounded with it: the r1 round follows the end face's outline over the r3 round and down the back edge",
|
||||
"source": "analytic",
|
||||
"method": "cube 20 (V0 = 8000), its top back edge (y=20, z=20) rounded r3: removes 20 (9 - 9 pi/4) = 38.628316. The end face's top edge (x=0, z=20, y 0..17) then rounded r1 alone, tangent_chain false: over y 0..17 it removes 17 (1 - pi/4) = 3.648231; at y=17 the edge goes on tangentially into the r3 round's end arc, and the r1 round's cylinder (axis x=1, z=19) runs on along y through the r3 round, cutting the part of its section under the round's surface z = 17 + sqrt(9 - (y-17)^2) until that surface drops below z=19 at y = 17 + sqrt(5): with t = sqrt(9 - (y-17)^2) - 2, the section's area under it is t - (t sqrt(1-t^2) + asin t)/2, integrated over y 17..17+sqrt(5) (Simpson, 200000 steps) = 0.218236. V = 8000 - 38.628316 - 3.648231 - 0.218236 = 7957.505202",
|
||||
"method": "cube 20 (V0 = 8000), its top back edge (y=20, z=20) rounded r3: removes 20 (9 - 9 pi/4) = 38.628331. The end face's top edge (x=0, z=20, y 0..17) then rounded r1 with its tangent chain: at y = 17 it runs on tangentially into the r3 round's end arc on x = 0 (convex, a quarter circle about y = z = 17), which runs on at (0, 20, 17) into the back upright (x=0, y=20, z 0..17), so the chain is those three edges; it ends at the front face y = 0 and the bottom z = 0, each square to its edge, so each straight round ends in a plane section. With A = 1 - pi/4 the r1 spandrel and c = r (10 - 3 pi)/(3 (4 - pi)) = 0.223368 its centroid's distance from either face: the top edge and the upright remove 17 A each; along the arc the spandrel, in the half plane through the r3 axis, lies at radius 3 - u from the axis (u its distance from the round's surface), and is turned through the arc's quarter turn: (pi/2) A (3 - c) = 0.935991 (Pappus). V = 8000 - 38.628331 - 34 A - 0.935991 = 7953.139216. Faces: the cube's 6, the r3 round, and the r1 chain's three (cylinder, torus, cylinder): 10.",
|
||||
"outcome": "solid",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 7957.505202,
|
||||
"volume_mm3": 7953.139216025938,
|
||||
"continuity": {
|
||||
"order": "g1"
|
||||
}
|
||||
},
|
||||
"faces": 10
|
||||
}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@
|
||||
]
|
||||
],
|
||||
"radius": 1,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+3
-3
@@ -5,10 +5,10 @@
|
||||
],
|
||||
"what": "a curvature-continuous (G2) fillet of size 5 on a straight edge that ends on two slanted planar faces",
|
||||
"source": "analytic",
|
||||
"method": "prism over the polygon (0,0),(40,0),(30,20),(10,20), 20 tall: V0 = 600 * 20 = 12000. Fillet type curvature, size 5 on the top front edge (y=0, z=20, length 40): section is the quintic Bezier (control points at thirds of the setback s = 5 on each face, zero curvature at both contacts), area A = 3.73677 between the corner and the curve and first moment of y over it 3.52297. The end faces are the planes x = y/2 and x = 40 - y/2, so the strip at depth y is 40 - y long: removed = 40 A - int y dA = 145.9479. V = 12000 - removed = 11854.0521.",
|
||||
"method": "prism over the polygon (0,0),(40,0),(30,20),(10,20), 20 tall: V0 = 600 * 20 = 12000. Fillet type curvature, size 5 on the top front edge (y=0, z=20, length 40): the curvature section: the order-2 section at the equal-area thumbweight between the contacts set back s along two planes square to each other (cov-fillet-section-curvature-box-edge-r3), S(t) = f(t) (e + (1 - t) (rho1 - e)) + (1 - f(t)) (e + t (rho2 - e)), f = (1-t)^3 / ((1-t)^3 + t^3), e the corner: it cuts A = 0.214601836603 s^2 off the corner, its first moment about either face M = 0.0474559669443 s^3 (Green's theorem on S), here s = 5: A = 3.85481638 and the first moment of y over it 3.64691620. The end faces are the planes x = y/2 and x = 40 - y/2, so the strip at depth y is 40 - y long: removed = 40 A - int y dA = 150.5457392. V = 12000 - removed = 11849.4543. [the curvature section at the equal-area thumbweight (the default): on the same rails it takes off what the ball's circle does, so between square planes w = -0.5252123889 and it cuts A = (1 - pi/4) s^2 = 0.214601836603 s^2, first moment about either face M = 0.0474559669443 s^3 (Green's theorem on S); V = 11791.330159]",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 11854.052069321415,
|
||||
"volume_mm3": 11791.330159265486,
|
||||
"continuity": {
|
||||
"order": "g2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
]
|
||||
],
|
||||
"radius": 5,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
]
|
||||
],
|
||||
"distance": 2,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
]
|
||||
],
|
||||
"distance": 9.5,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@
|
||||
]
|
||||
],
|
||||
"distance": 2,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
]
|
||||
],
|
||||
"distance": 2,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
]
|
||||
],
|
||||
"distance": 5,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
]
|
||||
],
|
||||
"radius": 2,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
]
|
||||
],
|
||||
"radius": 2,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -86,7 +86,7 @@
|
||||
]
|
||||
],
|
||||
"radius": 2,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
]
|
||||
],
|
||||
"radius": 9.5,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
]
|
||||
],
|
||||
"radius": 10,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
]
|
||||
],
|
||||
"radius": 2,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
]
|
||||
],
|
||||
"radius": 2,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
]
|
||||
],
|
||||
"radius": 2,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
]
|
||||
],
|
||||
"radius": 2,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+6
-4
@@ -3,9 +3,9 @@
|
||||
"tangency-labs/solidcraft#851",
|
||||
"tangency-labs/solidcraft#854"
|
||||
],
|
||||
"what": "on #851's knife-edge part (tessellation/1263): the top front edge R2, then the front-left upright R2 as a fillet of its own, ending in the first round",
|
||||
"source": "invariants",
|
||||
"method": "the rolling ball of R2 fits on every one of these edges and where they meet; the corner between rounds that end in each other has no closed form here: one closed valid solid whose display mesh closes. Each round meets the faces it rolls on G1: graded at its seams' middles (the top front round's at z = 8 on the front and y = -19 on the top; the arc's at rho 23 on the top and rho 21, z = 8 on the wall; the uprights' at their contacts, z = 5). Where a round ends on a face or on another round the seam is a crease by construction and is not graded.",
|
||||
"what": "on #851's knife-edge part (tessellation/1263): the top front edge R2, then the front-left upright R2 with its tangent chain: up the first round's end arc and along the left top edge",
|
||||
"source": "analytic",
|
||||
"method": "plate x 0..41.1667, y -21..0 and the corner x 41.1667..62.1667 less the cylinder r 21 about (62.1667, -21), 10 high (tessellation/1263): V0 = 10 (62.1667 x 21 - pi 21^2/4) = 9591.394099. The top front edge rounded R2 first: no ball of R2 inside the part (centre y >= -19, z <= 8) reaches its spandrel (y in [-21,-19], z in [8,10], outside the circle about (-19, 8)), so it removes the spandrel's material, from x = 0 to the wall x_w(y) = 62.1667 - sqrt(441 - (y+21)^2): Int over the spandrel of x_w(y) = 35.344727 (Simpson in y = -19 - 2 sin t, 200000 steps). Then the front-left upright (x=0, y=-21, z 0..8) with its tangent chain: at z = 8 it runs on tangentially into the first round's end arc on x = 0 (a quarter circle about y = -19, z = 8, convex), which runs on at (0, -19, 10) into the left top edge (x=0, z=10, y -19..0), ending on the back face y = 0, square to it. The three are the corner's other two edges and the arc between: the result is the part with all three edges at (0, -21, 10) rolled by the R2 ball, its corner the ball's eighth about (2, -19, 8) (the arc's round is the torus of tube centre radius 0 about the first round's axis, the ball itself). With A = (4 - pi) the R2 spandrel: the upright removes 8 A and the left top edge 19 A; in the corner cube [0,2] x [-21,-19] x [8,10] the first round left the quarter cylinder 2 pi and the ball leaves its eighth 4 pi/3: 2 pi/3. V = 9591.394099 - 35.344727 - 27 A - 2 pi/3 = 9530.777979. Faces: the part's 6, the first round, and the chain's three (cylinder, sphere, cylinder): 10. Each round meets the faces it rolls on G1: graded at its seams' middles (the top front round's at z = 8 on the front and y = -19 on the top; the uprights' at their contacts, z = 5).",
|
||||
"body_count": 1,
|
||||
"watertight": true,
|
||||
"continuity": {
|
||||
@@ -32,5 +32,7 @@
|
||||
5
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"volume_mm3": 9530.777979288185,
|
||||
"faces": 10
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
]
|
||||
],
|
||||
"radius": 2,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -86,7 +86,7 @@
|
||||
]
|
||||
],
|
||||
"radius": 2,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
]
|
||||
],
|
||||
"radius": 2,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -86,7 +86,7 @@
|
||||
]
|
||||
],
|
||||
"radius": 2,
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
],
|
||||
"what": "a curvature-continuous (G2) fillet of size 6 on one edge of a 20 mm cube",
|
||||
"source": "analytic",
|
||||
"method": "type curvature: the section is the quintic Bezier with control points p+s t1, p+(2s/3) t1, p+(s/3) t1, p+(s/3) t2, p+(2s/3) t2, p+s t2 with setback s = radius = 6 (perpendicular faces), as documented for the feature (zero curvature where it meets the faces). The area between the corner and that curve, by Green on the Bezier polygon (200000 points), is 5.38095 mm^2; the edge is 20 long between two planar end faces square to it: V = 8000 - 20*5.38095 = 7892.3810. Faces: 6 + 1 = 7.",
|
||||
"method": "type curvature: the curvature section: the order-2 section at the equal-area thumbweight between the contacts set back s along two planes square to each other (cov-fillet-section-curvature-box-edge-r3), S(t) = f(t) (e + (1 - t) (rho1 - e)) + (1 - f(t)) (e + t (rho2 - e)), f = (1-t)^3 / ((1-t)^3 + t^3), e the corner: it cuts A = 0.214601836603 s^2 off the corner, its first moment about either face M = 0.0474559669443 s^3 (Green's theorem on S), here s = radius = 6 (perpendicular faces): A = 5.55093559 mm^2; the edge is 20 long between two planar end faces square to it: V = 8000 - 20*5.55093559 = 7888.9813. Faces: 6 + 1 = 7. [the curvature section at the equal-area thumbweight (the default): on the same rails it takes off what the ball's circle does, so between square planes w = -0.5252123889 and it cuts A = (1 - pi/4) s^2 = 0.214601836603 s^2, first moment about either face M = 0.0474559669443 s^3 (Green's theorem on S); V = 7845.486678]",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 7892.380952374643,
|
||||
"volume_mm3": 7845.486677646163,
|
||||
"faces": 7,
|
||||
"continuity": {
|
||||
"order": "g2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
],
|
||||
"radius": 5,
|
||||
"corner": "g2",
|
||||
"tangent_chain": false
|
||||
"tangent_chain": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -3,12 +3,37 @@
|
||||
# and on a listed case that passes: a fix removes its line in the same pull request.
|
||||
# Baseline: develop 242bbc89f with the SMLib fixes of 2026-10-29 (integration-overnight + b78bd4a), aarch64 Linux.
|
||||
1080-fillet-equal-slope-basin-1-mm-rim-loop-4 builds to the analytic volume (rel 5e-9), but the continuity check (g1 at every blend seam) takes in the mitre's creases, G0 by the mitre ruling: the case's continuity seams need the owner's word
|
||||
1080-fillet-notched-block-top-outline-r8 a blend seam grades G0 where g1 is wanted, and the volume is 10320.299 against 10198.718, on SMLib v1.1.0-alpha (smlib #124, the End at a smooth vertex); passed on f916a8d. Whether the End's G0 there is by design or a defect is being established
|
||||
1080-fillet-sloped-basin-1-mm-rim-r4-2-edges builds to the analytic volume (rel 6e-9), but the continuity check (g1 at every blend seam) takes in the mitre's creases, G0 by the mitre ruling: the case's continuity seams need the owner's word
|
||||
1080-fillet-sloped-basin-1-mm-rim-r4-4-edges builds to the analytic volume (rel 3e-9), but the continuity check (g1 at every blend seam) takes in the mitre's creases, G0 by the mitre ruling: the case's continuity seams need the owner's word
|
||||
1207-fillet-bar-top-edge-after-boss-arc-round-r0-5 SMLib's check refuses the round: internal coincident control points in its end rib on the boss, which starts where the round and the boss are tangent (the boss's intersection marched there in spans down to 6e-8 mm)
|
||||
1207-fillet-box-end-edge-into-unblended-round-partial-chain a blend seam grades G0 (1 of 5 seams) where g1 is wanted, at the end of the round's contact seam, on SMLib v1.1.0-alpha (smlib #124, the End at a smooth vertex); passed on f916a8d. Whether the End's G0 there is by design or a defect is being established
|
||||
2893-chamfer-distance-and-angle-past-the-wall-refused refused for another reason: SMLib SM_ERR_INVALID_INPUT, not a check saying the angle runs past the wall
|
||||
2893-chamfer-two-distances-elliptical-rim SMLib status 1001: the two arcs of the rim take their chamfer offsets each at its own midpoint, where the faces' angle differs (78.5 and 101.5 deg), and their chamfers do not meet at the tangent corners (held for the owner's ruling on unequal chamfers of a varying angle)
|
||||
3369-fillet-edge-ends-on-tangent-seam two blend seams grade G0 (2 of 4) where g1 is wanted, at the end of the round's contact seam, on SMLib v1.1.0-alpha (smlib #124, the End at a smooth vertex); passed on f916a8d. Whether the End's G0 there is by design or a defect is being established
|
||||
4782-fillet-bar-edge-wider-than-both-faces SMLib status 1001: a convex round past both its faces is not yet built by subtraction
|
||||
5158-fillet-adjacent-plate-edges-as-thick-as-plate a blend seam grades G0 where g1 is wanted, and the volume is 1682.854 against 1681.904, on SMLib v1.1.0-alpha (smlib #124, the End at a smooth vertex); passed on f916a8d. Whether the End's G0 there is by design or a defect is being established
|
||||
5158-fillet-round-just-past-a-10mm-face SMLib status 1001: the round passes 5e-6 under the box's top back edge, inside the body's tolerance (1e-5), and subtracting its removal region maps that edge to two of the region's faces (SmTopologyIntersector); 10.0001-10.04 open
|
||||
851-fillet-arc-then-top-front SMLib status 1001: the top front edge's tangent chain (the edge, the arc round's end curve on the front face, the upright under it) rounded R2; along the end curve the R2 ball sits on the arc round's own centre circle, so that piece is the ball about (39.2538, -19, 8) touching front, top and wall (a torus of tube-centre radius 0), the three-edge rolling-ball corner; the same piece on a plane end arc builds (851-fillet-top-front-then-front-left-upright)
|
||||
851-fillet-top-front-then-upright SMLib status 1001: the upright's tangent chain (the upright, the first round's end curve on the wall, the arc's top edge) rounded R2; along the end curve the R2 ball sits on the first round's axis, so that piece is the ball about (39.2538, -19, 8) touching front, top and wall (a torus of tube-centre radius 0), the three-edge rolling-ball corner; the upright alone builds, the upright with the end curve fails the same; the same piece on a plane end arc builds (851-fillet-top-front-then-front-left-upright)
|
||||
9-fillet-corner-curvature-with-reduce-fit not supported yet: solid.fillet builds corner g2 with the exact fit only
|
||||
9-fillet-corner-curvature-with-taper-fit not supported yet: solid.fillet builds corner g2 with the exact fit only
|
||||
cov-fillet-g2-corner-beside-a-cylinder not supported yet: the curvature fillet builds its sections only between planar faces ("a picked edge is not on the face beside it" on the cylinder's arc)
|
||||
cov-fillet-g2-five-edge-apex-corner refused: the default setback is shorter than where the rounds' contact lines meet (3.5056 mm from the apex); the corner should take a setback that works
|
||||
cov-fillet-g2-four-edge-apex-corner refused: the default setback is shorter than where the rounds' contact lines meet (3.9027 mm from the apex); the corner should take a setback that works
|
||||
cov-fillet-g2-mixed-corner not supported yet: SMLib sets back a corner only where its edges are all convex or all concave
|
||||
cov-fillet-g2-oblique-three-edge-corner the G2 corner patch meets the new order-2 section's rounds only G1 at one face seam (its volume holds); the corner step rebuilds the patch
|
||||
cov-fillet-g2-pocket-corner the G2 corner patch meets the new order-2 section's rounds only G1 at one face seam (its volume holds); the corner step rebuilds the patch
|
||||
cov-fillet-turn-swept-boss-base-r1 not supported yet: solid.fillet has no `turn`; the corners are mitred (12508.9676, the Mitre case's volume)
|
||||
cov-fillet-turn-swept-pocket-sharp-corner-refused-r1 not supported yet: solid.fillet has no `turn`, so the sharp concave corners are mitred, not refused
|
||||
cov-junction-boss-tilt-20-root-round SMLib fillet status 1001 on a cylinder tilted 20 degrees into the plate (also before the blend rework)
|
||||
cov-junction-tee-equal-round equal pipes' intersection is two plane ellipses crossing: their exact conic-pair rounds meet at the crossings in a corner the old corner code cannot join (built by the traced rounds before smlib#77); the corner step
|
||||
cov-junction-y-junction-120-round refused: the faces between the three intersection curves narrow to nothing at the two triple points, so the room check trips; a three-edge concave corner (the corner step)
|
||||
every-block-cylinder-tangent-concave-arc-r2-partial-chain a blend seam grades G0 where g1 is wanted, at the end of the round's contact seam, on SMLib v1.1.0-alpha (smlib #124, the End at a smooth vertex); passed on f916a8d. Whether the End's G0 there is by design or a defect is being established
|
||||
every-box-corner-three-edges-r3-curvature-setback-3-5 not supported on SMLib v1.1.0-alpha (smlib #125: setbacks only at the Setback's corners, the old setback paths deleted); passed on f916a8d. Whether this is a refusal that should name a size or a corner the Setback should build is being established
|
||||
every-fillet-after-fillet-curvature-front-then-side-r3-partial-chain a blend seam grades G0 where g2 is wanted, at the end of the round's contact seam, on SMLib v1.1.0-alpha (smlib #124, the End at a smooth vertex); passed on f916a8d. Whether the End's G0 there is by design or a defect is being established
|
||||
report-drafted-peg-front-and-concave-rounds-together-r2 not supported yet: SMLib refuses the corner where the drafted front top edge runs tangent into the concave arc (on the cone) as of no kind its blend corners take
|
||||
report-fillet-curvature-three-edges-at-bar-corner the G2 corner patch meets the new order-2 section's rounds only G1 at one face seam (its volume holds); the corner step rebuilds the patch
|
||||
report-fillet-curvature-three-edges-at-bar-corner-solver-residues the G2 corner patch meets the new order-2 section's rounds only G1 at one face seam (its volume holds); the corner step rebuilds the patch
|
||||
report-fillet-curvature-three-edges-at-bar-corner-solver-residues-outside-setback not supported on SMLib v1.1.0-alpha (smlib #125: setbacks only at the Setback's corners, the old setback paths deleted); passed on f916a8d. Whether this is a refusal that should name a size or a corner the Setback should build is being established
|
||||
report-fillet-g2-corner-of-bar-running-into-cylinder not supported on SMLib v1.1.0-alpha (smlib #125: setbacks only at the Setback's corners, the old setback paths deleted); passed on f916a8d. Whether this is a refusal that should name a size or a corner the Setback should build is being established
|
||||
report-fillet-g2-corner-of-bar-running-into-cylinder-outside-setback not supported on SMLib v1.1.0-alpha (smlib #125: setbacks only at the Setback's corners, the old setback paths deleted); passed on f916a8d. Whether this is a refusal that should name a size or a corner the Setback should build is being established
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"sources": [],
|
||||
"what": "two coaxial frusta joined, R10 to 6 over z 0..8 and R6 to 1 over z 8..9: the convex circle between their cones rounded r2",
|
||||
"source": "analytic",
|
||||
"method": "both cones turn about z: the ball's centre runs on a circle about z, so the round is the ball's arc turned about z: rational, of degrees (2, 2). V0 = the frusta, pi h (R^2 + R r + r^2) / 3; the round removes 2 pi times the first moment about z of its section (the ball R2 tangent to both profile lines): 5.065333649310.",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 1681.969921328409,
|
||||
"tolerance": {
|
||||
"rel": 1e-06
|
||||
},
|
||||
"surface_types": [
|
||||
"bspline",
|
||||
"cone",
|
||||
"plane"
|
||||
],
|
||||
"splines": [
|
||||
{
|
||||
"degree": [
|
||||
2,
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"continuity": {
|
||||
"order": "g1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"command": "solid.cone",
|
||||
"params": {
|
||||
"radius": 10,
|
||||
"height": 8,
|
||||
"top_radius": 6,
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"operation": "new",
|
||||
"body_name": "Top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.cone",
|
||||
"params": {
|
||||
"radius": 6,
|
||||
"height": 1,
|
||||
"top_radius": 1,
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
8
|
||||
],
|
||||
"operation": "join",
|
||||
"targets": [
|
||||
"Top"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.fillet",
|
||||
"params": {
|
||||
"edges": [
|
||||
[
|
||||
0,
|
||||
6,
|
||||
8
|
||||
]
|
||||
],
|
||||
"radius": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"sources": [],
|
||||
"what": "a cone (apex (0, 0, 10 sqrt 2), half-angle 45 deg) and a post R10 tilted 20 deg through the origin, both touching the ball R10 about the origin along circles that do not meet, joined: the concave edge where the cone leaves the post rounded r1",
|
||||
"source": "analytic",
|
||||
"method": "the offsets by 1 (the post R11, the cone with its apex sqrt 2 higher) both touch the ball R11 about the origin, so they meet in two plane conics (the cone and the post are each the ball's sphere less a multiple of the square of their contact circle's plane, and their difference is two planes); the spine is the conic round the post, and the round, between contact lines that are rational on both quadrics, is a rational B-spline of degrees (6, 2). V0 = the cone and the post less their overlap, about the cone's apex (where the cone gives no flux): the post's wall below the edge and its bottom cap: 21763.823377543333; the round adds 1.884007899126, (1/3) of the flux of the position through the boundary of the material the round adds: the round's surface as the ball's circles about the spine from contact to contact, the strips of the faces between the edge and the contact lines, each written out exactly and integrated by Gauss-Legendre quadrature (24 points a panel), about the apex. The round's faces, each of those degrees: a closed round is two faces (split at the seam of its closed surface).",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 21765.70738544246,
|
||||
"tolerance": {
|
||||
"rel": 1e-06
|
||||
},
|
||||
"surface_types": [
|
||||
"bspline",
|
||||
"cone",
|
||||
"cylinder",
|
||||
"plane"
|
||||
],
|
||||
"splines": [
|
||||
{
|
||||
"degree": [
|
||||
6,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"degree": [
|
||||
6,
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"continuity": {
|
||||
"order": "g1"
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"command": "solid.cone",
|
||||
"params": {
|
||||
"radius": 24.14213562373095,
|
||||
"height": 24.14213562373095,
|
||||
"top_radius": 0,
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
-10
|
||||
],
|
||||
"operation": "new",
|
||||
"body_name": "Hill"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.cylinder",
|
||||
"params": {
|
||||
"radius": 10,
|
||||
"height": 30,
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0.3420201433256687,
|
||||
0,
|
||||
0.9396926207859084
|
||||
],
|
||||
"operation": "join",
|
||||
"targets": [
|
||||
"Hill"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.fillet",
|
||||
"params": {
|
||||
"edges": [
|
||||
[
|
||||
1.46857451100787,
|
||||
10,
|
||||
4.034875307780737
|
||||
]
|
||||
],
|
||||
"radius": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"sources": [],
|
||||
"what": "two pipes touching at one point (R10 about y = 0, z = -10 and R5 about x = 0, z = -5, both touching z = 0 at the origin from below), joined and cut at z = -2: the concave arc of their edge on the +y side, from (4, 6, -2) down through (0, 10, -10) to (-4, 6, -2), rounded r1",
|
||||
"source": "analytic",
|
||||
"method": "the offsets by 1 (R11 and R6) touch at (0, 0, 1) as the pipes do at the origin, so they meet in a quartic with a node there, which is rational (the lines through the node meet it once more); the round is a rational B-spline of degrees (12, 2). V0 = the two cut pipes less their overlap, integral of 4 sqrt(25 - (z+5)^2) sqrt(100 - (z+10)^2) over z in [-10, -2]: 13437.742049099792; the round adds 4.961081486747, (1/3) of the flux of the position through the boundary of the material the round adds: the round's surface as the ball's circles about the spine from contact to contact, the strips of the faces between the edge and the contact lines, and its end pieces in the plane z = -2 (bounded by the round's section there), each written out exactly and integrated by Gauss-Legendre quadrature (24 points a panel). The round's faces, each of those degrees: four, as the body's topology splits the edge and the round.",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 13442.703130586538,
|
||||
"tolerance": {
|
||||
"rel": 1e-06
|
||||
},
|
||||
"surface_types": [
|
||||
"bspline",
|
||||
"cylinder",
|
||||
"plane"
|
||||
],
|
||||
"splines": [
|
||||
{
|
||||
"degree": [
|
||||
12,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"degree": [
|
||||
12,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"degree": [
|
||||
12,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"degree": [
|
||||
12,
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"continuity": {
|
||||
"order": "g1"
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"command": "solid.cylinder",
|
||||
"params": {
|
||||
"radius": 10,
|
||||
"height": 40,
|
||||
"base": [
|
||||
-20,
|
||||
0,
|
||||
-10
|
||||
],
|
||||
"axis": "X",
|
||||
"operation": "new",
|
||||
"body_name": "Pipes"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.cylinder",
|
||||
"params": {
|
||||
"radius": 5,
|
||||
"height": 40,
|
||||
"base": [
|
||||
0,
|
||||
-20,
|
||||
-5
|
||||
],
|
||||
"axis": "Y",
|
||||
"operation": "join",
|
||||
"targets": [
|
||||
"Pipes"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.box",
|
||||
"params": {
|
||||
"length": 100,
|
||||
"width": 100,
|
||||
"height": 50,
|
||||
"corner": [
|
||||
-50,
|
||||
-50,
|
||||
-2
|
||||
],
|
||||
"operation": "cut",
|
||||
"targets": [
|
||||
"Pipes"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.fillet",
|
||||
"params": {
|
||||
"edges": [
|
||||
[
|
||||
4.207354924039483,
|
||||
9.732263392975257,
|
||||
-7.701511529340699
|
||||
],
|
||||
[
|
||||
-4.207354924039483,
|
||||
9.732263392975257,
|
||||
-7.701511529340699
|
||||
]
|
||||
],
|
||||
"radius": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"sources": [],
|
||||
"what": "a frustum R10 narrowing at 10 deg, cut by the plane z = 15 + 0.3 x: its elliptical top rim rounded r1.5",
|
||||
"source": "analytic",
|
||||
"method": "the ball's centre runs on the cone moved 1.5 in (its apex 1.5 / sin 10 deg lower) and on the plane moved 1.5 down: a plane section of a cone, an ellipse (rho = K / (1 + 0.3 tan 10 deg cos phi) about the axis), so the round is a rational B-spline of degrees (4, 2). V0 = 3565.939077141579 (the bottom disc gives no flux about the origin, the cone X.n = 10 sin 10 deg / tan 10 deg over its area, the top X.n = 15 / sqrt 1.09 over its area); the round removes 16.021667699147, (1/3) of the flux of the position through the boundary of the material the round removes: the round's surface as the ball's circles about the spine from contact to contact, the strips of the faces between the edge and the contact lines, each written out exactly and integrated by Gauss-Legendre quadrature (24 points a panel). The round's faces, each of those degrees: a closed round is two faces (split at the seam of its closed surface).",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 3549.917409442432,
|
||||
"tolerance": {
|
||||
"rel": 1e-06
|
||||
},
|
||||
"surface_types": [
|
||||
"bspline",
|
||||
"cone",
|
||||
"plane"
|
||||
],
|
||||
"splines": [
|
||||
{
|
||||
"degree": [
|
||||
4,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"degree": [
|
||||
4,
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"continuity": {
|
||||
"order": "g1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"command": "solid.cone",
|
||||
"params": {
|
||||
"radius": 10,
|
||||
"height": 30,
|
||||
"top_radius": 4.710190578746051,
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"operation": "new",
|
||||
"body_name": "Post"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.cylinder",
|
||||
"params": {
|
||||
"radius": 100,
|
||||
"height": 100,
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
15
|
||||
],
|
||||
"axis": [
|
||||
-0.2873478855663454,
|
||||
0,
|
||||
0.9578262852211513
|
||||
],
|
||||
"operation": "cut",
|
||||
"targets": [
|
||||
"Post"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.fillet",
|
||||
"params": {
|
||||
"edges": [
|
||||
[
|
||||
5.013316645912501,
|
||||
5.013316645912501,
|
||||
16.503994993773752
|
||||
]
|
||||
],
|
||||
"radius": 1.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"sources": [],
|
||||
"what": "a cylinder R10 cut by the plane z = 20 + x/2: its elliptical top rim rounded r2",
|
||||
"source": "analytic",
|
||||
"method": "the ball's centre runs on the cylinder R8 and on the plane moved 2 down, so the spine is their ellipse; the contact lines are the ellipse moved along each face's normal, rational, and the round (the circles of the ball between them, rational quadratic arcs whose weights are rational in the ellipse's parameter) is a rational B-spline of degrees (4, 2). V0 = 2000 pi (the plane's tilt cancels round the axis); the round removes 68.775042921067, (1/3) of the flux of the position through the boundary of the material the round removes: the round's surface as the ball's circles about the spine from contact to contact, the strips of the faces between the edge and the contact lines, each written out exactly and integrated by Gauss-Legendre quadrature (24 points a panel). The round's faces, each of those degrees: a closed round is two faces (split at the seam of its closed surface).",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 6214.41026425852,
|
||||
"tolerance": {
|
||||
"rel": 1e-06
|
||||
},
|
||||
"surface_types": [
|
||||
"bspline",
|
||||
"cylinder",
|
||||
"plane"
|
||||
],
|
||||
"splines": [
|
||||
{
|
||||
"degree": [
|
||||
4,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"degree": [
|
||||
4,
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"continuity": {
|
||||
"order": "g1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"command": "solid.cylinder",
|
||||
"params": {
|
||||
"radius": 10,
|
||||
"height": 40,
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"operation": "new",
|
||||
"body_name": "Post"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.cylinder",
|
||||
"params": {
|
||||
"radius": 100,
|
||||
"height": 100,
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
20
|
||||
],
|
||||
"axis": [
|
||||
-0.4472135954999579,
|
||||
0,
|
||||
0.8944271909999159
|
||||
],
|
||||
"operation": "cut",
|
||||
"targets": [
|
||||
"Post"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.fillet",
|
||||
"params": {
|
||||
"edges": [
|
||||
[
|
||||
7.0710678118654755,
|
||||
7.0710678118654755,
|
||||
23.535533905932738
|
||||
]
|
||||
],
|
||||
"radius": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"sources": [],
|
||||
"what": "a ball R10 and a frustum about z through its centre (R5 at z = 0 to R3 at z = 20) joined: the concave circle where the cone leaves the ball rounded r3",
|
||||
"source": "analytic",
|
||||
"method": "the ball's centre (outside both) runs on the sphere R13 and on the cone moved 3 out, both about z: a circle, so the round is the ball's arc turned about z: rational, of degrees (2, 2). V0 = the ball and the frustum less their overlap (the frustum below the edge at z = 9.1265, the ball's cap above it): 4597.853510151037; the round adds 2 pi times the first moment of its section: 9.688315962175.",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 4607.541826113212,
|
||||
"tolerance": {
|
||||
"rel": 1e-06
|
||||
},
|
||||
"surface_types": [
|
||||
"bspline",
|
||||
"cone",
|
||||
"plane",
|
||||
"sphere"
|
||||
],
|
||||
"splines": [
|
||||
{
|
||||
"degree": [
|
||||
2,
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"continuity": {
|
||||
"order": "g1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"command": "solid.sphere",
|
||||
"params": {
|
||||
"radius": 10,
|
||||
"center": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"operation": "new",
|
||||
"body_name": "Ball"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.cone",
|
||||
"params": {
|
||||
"radius": 5,
|
||||
"height": 20,
|
||||
"top_radius": 3,
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"operation": "join",
|
||||
"targets": [
|
||||
"Ball"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.fillet",
|
||||
"params": {
|
||||
"edges": [
|
||||
[
|
||||
0,
|
||||
4.087346743853332,
|
||||
9.12653256146668
|
||||
]
|
||||
],
|
||||
"radius": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"sources": [],
|
||||
"what": "a ball R10 cut flat at z = 4: its rim rounded r2",
|
||||
"source": "analytic",
|
||||
"method": "the ball's centre runs on the sphere R8 and the plane z = 2: the circle of radius sqrt 60 about z, so the round is the ball's arc turned about z, a rational B-spline of degrees (2, 2). V0 = 4000 pi / 3 - 288 pi; the round removes 2 pi times the first moment about z of its section (Pappus), from contact (sqrt 60, 4) on the plane to (1.25 sqrt 60, 2.5) on the sphere: 22.492758031524. The round's faces, each of those degrees: a closed round is two faces (split at the seam of its closed surface).",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 3261.518762521006,
|
||||
"tolerance": {
|
||||
"rel": 1e-06
|
||||
},
|
||||
"surface_types": [
|
||||
"bspline",
|
||||
"plane",
|
||||
"sphere"
|
||||
],
|
||||
"splines": [
|
||||
{
|
||||
"degree": [
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"degree": [
|
||||
2,
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"continuity": {
|
||||
"order": "g1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"command": "solid.sphere",
|
||||
"params": {
|
||||
"radius": 10,
|
||||
"center": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"operation": "new",
|
||||
"body_name": "Ball"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.cylinder",
|
||||
"params": {
|
||||
"radius": 100,
|
||||
"height": 100,
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
4
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"operation": "cut",
|
||||
"targets": [
|
||||
"Ball"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.fillet",
|
||||
"params": {
|
||||
"edges": [
|
||||
[
|
||||
0,
|
||||
9.16515138991168,
|
||||
4
|
||||
]
|
||||
],
|
||||
"radius": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"what": "the corner of a half cylinder's top (the top's arc, its straight edge and the upright seam where the flat face meets the cylinder) filleted 2 as a curvature-continuous (G2) fillet",
|
||||
"source": "invariants",
|
||||
"method": "a half disc R10 (the arc from (10, 0) round to (-10, 0) and its diameter) extruded 10; the three edges at (10, 0, 10); the fillet type curvature (G2 sections and G2 corner patches): every seam of a round and of the corner patch with its neighbours G2, one valid body",
|
||||
"body_count": 1,
|
||||
"continuity": {
|
||||
"order": "g2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"command": "sketch.create",
|
||||
"params": {
|
||||
"plane": "XY",
|
||||
"name": "Half"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.arc.center_point",
|
||||
"params": {
|
||||
"center": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"start": [
|
||||
10,
|
||||
0
|
||||
],
|
||||
"end": [
|
||||
-10,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.line",
|
||||
"params": {
|
||||
"points": [
|
||||
[
|
||||
-10,
|
||||
0
|
||||
],
|
||||
[
|
||||
10,
|
||||
0
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.finish",
|
||||
"params": {}
|
||||
},
|
||||
{
|
||||
"command": "solid.extrude",
|
||||
"params": {
|
||||
"sketch": "Half",
|
||||
"distance": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.fillet",
|
||||
"params": {
|
||||
"edges": [
|
||||
[
|
||||
7.0710678118654755,
|
||||
7.0710678118654755,
|
||||
10
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
10
|
||||
],
|
||||
[
|
||||
10,
|
||||
0,
|
||||
5
|
||||
]
|
||||
],
|
||||
"radius": 2,
|
||||
"type": "curvature"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"what": "the 5 slant edges of a 5-sided pyramid filleted 2 as a curvature-continuous (G2) fillet: a convex corner of 5 edges at the apex",
|
||||
"source": "invariants",
|
||||
"method": "a regular 5-gon of circumradius 20 lofted to the apex (0, 0, 30); its 5 slant edges; the fillet type curvature (G2 sections and G2 corner patches): every seam of a round and of the corner patch with its neighbours G2, one valid body",
|
||||
"body_count": 1,
|
||||
"continuity": {
|
||||
"order": "g2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"command": "sketch.create",
|
||||
"params": {
|
||||
"plane": "XY",
|
||||
"name": "Base"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.line",
|
||||
"params": {
|
||||
"points": [
|
||||
[
|
||||
1.2246467991473533e-15,
|
||||
20.0
|
||||
],
|
||||
[
|
||||
-19.02113032590307,
|
||||
6.18033988749895
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.line",
|
||||
"params": {
|
||||
"points": [
|
||||
[
|
||||
-19.02113032590307,
|
||||
6.18033988749895
|
||||
],
|
||||
[
|
||||
-11.755705045849465,
|
||||
-16.180339887498945
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.line",
|
||||
"params": {
|
||||
"points": [
|
||||
[
|
||||
-11.755705045849465,
|
||||
-16.180339887498945
|
||||
],
|
||||
[
|
||||
11.755705045849458,
|
||||
-16.180339887498953
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.line",
|
||||
"params": {
|
||||
"points": [
|
||||
[
|
||||
11.755705045849458,
|
||||
-16.180339887498953
|
||||
],
|
||||
[
|
||||
19.021130325903073,
|
||||
6.180339887498944
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.line",
|
||||
"params": {
|
||||
"points": [
|
||||
[
|
||||
19.021130325903073,
|
||||
6.180339887498944
|
||||
],
|
||||
[
|
||||
1.2246467991473533e-15,
|
||||
20.0
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.finish",
|
||||
"params": {}
|
||||
},
|
||||
{
|
||||
"command": "sketch.create",
|
||||
"params": {
|
||||
"plane": {
|
||||
"origin": [
|
||||
0,
|
||||
0,
|
||||
30
|
||||
],
|
||||
"x_dir": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"y_dir": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"name": "Top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.point",
|
||||
"params": {
|
||||
"point": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"id": "apex"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.finish",
|
||||
"params": {}
|
||||
},
|
||||
{
|
||||
"command": "solid.loft",
|
||||
"params": {
|
||||
"sections": [
|
||||
{
|
||||
"sketch": "Base"
|
||||
},
|
||||
{
|
||||
"sketch": "Top",
|
||||
"point": "apex"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.fillet",
|
||||
"params": {
|
||||
"edges": [
|
||||
[
|
||||
6.123233995736766e-16,
|
||||
10.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
-9.510565162951535,
|
||||
3.090169943749475,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
-5.877852522924733,
|
||||
-8.090169943749473,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
5.877852522924729,
|
||||
-8.090169943749476,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
9.510565162951536,
|
||||
3.090169943749472,
|
||||
15.0
|
||||
]
|
||||
],
|
||||
"radius": 2,
|
||||
"type": "curvature"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"what": "the 4 slant edges of a 4-sided pyramid filleted 2 as a curvature-continuous (G2) fillet: a convex corner of 4 edges at the apex",
|
||||
"source": "invariants",
|
||||
"method": "a regular 4-gon of circumradius 20 lofted to the apex (0, 0, 30); its 4 slant edges; the fillet type curvature (G2 sections and G2 corner patches): every seam of a round and of the corner patch with its neighbours G2, one valid body",
|
||||
"body_count": 1,
|
||||
"continuity": {
|
||||
"order": "g2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"command": "sketch.create",
|
||||
"params": {
|
||||
"plane": "XY",
|
||||
"name": "Base"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.line",
|
||||
"params": {
|
||||
"points": [
|
||||
[
|
||||
1.2246467991473533e-15,
|
||||
20.0
|
||||
],
|
||||
[
|
||||
-20.0,
|
||||
2.4492935982947065e-15
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.line",
|
||||
"params": {
|
||||
"points": [
|
||||
[
|
||||
-20.0,
|
||||
2.4492935982947065e-15
|
||||
],
|
||||
[
|
||||
-3.673940397442059e-15,
|
||||
-20.0
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.line",
|
||||
"params": {
|
||||
"points": [
|
||||
[
|
||||
-3.673940397442059e-15,
|
||||
-20.0
|
||||
],
|
||||
[
|
||||
20.0,
|
||||
-4.898587196589413e-15
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.line",
|
||||
"params": {
|
||||
"points": [
|
||||
[
|
||||
20.0,
|
||||
-4.898587196589413e-15
|
||||
],
|
||||
[
|
||||
1.2246467991473533e-15,
|
||||
20.0
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.finish",
|
||||
"params": {}
|
||||
},
|
||||
{
|
||||
"command": "sketch.create",
|
||||
"params": {
|
||||
"plane": {
|
||||
"origin": [
|
||||
0,
|
||||
0,
|
||||
30
|
||||
],
|
||||
"x_dir": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"y_dir": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"name": "Top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.point",
|
||||
"params": {
|
||||
"point": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"id": "apex"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.finish",
|
||||
"params": {}
|
||||
},
|
||||
{
|
||||
"command": "solid.loft",
|
||||
"params": {
|
||||
"sections": [
|
||||
{
|
||||
"sketch": "Base"
|
||||
},
|
||||
{
|
||||
"sketch": "Top",
|
||||
"point": "apex"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.fillet",
|
||||
"params": {
|
||||
"edges": [
|
||||
[
|
||||
6.123233995736766e-16,
|
||||
10.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
-10.0,
|
||||
1.2246467991473533e-15,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
-1.8369701987210296e-15,
|
||||
-10.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
10.0,
|
||||
-2.4492935982947065e-15,
|
||||
15.0
|
||||
]
|
||||
],
|
||||
"radius": 2,
|
||||
"type": "curvature"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"what": "a step's corner (the concave step edge, the lower box's convex front top edge, the upper box's convex upright edge) filleted 2 as a curvature-continuous (G2) fillet",
|
||||
"source": "invariants",
|
||||
"method": "a 30 x 20 x 10 box with a 15 x 20 x 10 box joined on its top at x 0..15; the three edges at (15, 0, 10); the fillet type curvature (G2 sections and G2 corner patches): every seam of a round and of the corner patch with its neighbours G2, one valid body",
|
||||
"body_count": 1,
|
||||
"continuity": {
|
||||
"order": "g2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"command": "solid.box",
|
||||
"params": {
|
||||
"corner": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"length": 30,
|
||||
"width": 20,
|
||||
"height": 10,
|
||||
"body_name": "Step"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.box",
|
||||
"params": {
|
||||
"corner": [
|
||||
0,
|
||||
0,
|
||||
10
|
||||
],
|
||||
"length": 15,
|
||||
"width": 20,
|
||||
"height": 10,
|
||||
"operation": "join"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.fillet",
|
||||
"params": {
|
||||
"edges": [
|
||||
[
|
||||
15,
|
||||
10,
|
||||
10
|
||||
],
|
||||
[
|
||||
22.5,
|
||||
0,
|
||||
10
|
||||
],
|
||||
[
|
||||
15,
|
||||
0,
|
||||
15
|
||||
]
|
||||
],
|
||||
"radius": 2,
|
||||
"type": "curvature"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"what": "the three edges at a top corner of a block drafted 15 deg (an oblique convex corner) filleted 2 as a curvature-continuous (G2) fillet",
|
||||
"source": "invariants",
|
||||
"method": "a 30 x 20 rectangle extruded 10 with a 15 deg inward taper; the top front edge, the top left edge and the drafted edge between them at the corner near (0, 0, 10); the fillet type curvature (G2 sections and G2 corner patches): every seam of a round and of the corner patch with its neighbours G2, one valid body",
|
||||
"body_count": 1,
|
||||
"continuity": {
|
||||
"order": "g2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"command": "sketch.create",
|
||||
"params": {
|
||||
"plane": "XY",
|
||||
"name": "Base"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.line",
|
||||
"params": {
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
30,
|
||||
0
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.line",
|
||||
"params": {
|
||||
"points": [
|
||||
[
|
||||
30,
|
||||
0
|
||||
],
|
||||
[
|
||||
30,
|
||||
20
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.line",
|
||||
"params": {
|
||||
"points": [
|
||||
[
|
||||
30,
|
||||
20
|
||||
],
|
||||
[
|
||||
0,
|
||||
20
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.line",
|
||||
"params": {
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
20
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.finish",
|
||||
"params": {}
|
||||
},
|
||||
{
|
||||
"command": "solid.extrude",
|
||||
"params": {
|
||||
"sketch": "Base",
|
||||
"distance": 10,
|
||||
"taper": "-15 deg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.fillet",
|
||||
"params": {
|
||||
"edges": [
|
||||
[
|
||||
15,
|
||||
2.679491924311227,
|
||||
10
|
||||
],
|
||||
[
|
||||
2.679491924311227,
|
||||
10,
|
||||
10
|
||||
],
|
||||
[
|
||||
1.3397459621556136,
|
||||
1.3397459621556136,
|
||||
5
|
||||
]
|
||||
],
|
||||
"radius": 2,
|
||||
"type": "curvature"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user