mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
Merge pull request #2282 from Karl-VonRichter/difftest-expect-refused-manifold
difftest: expect.refused and expect.manifold for SolidCraft cases (prep for #2278)
This commit is contained in:
@@ -807,7 +807,18 @@ nice -n 10 cargo run -p difftest -- --solidcraft-cases '3526-*' # ids (<f
|
||||
void is a region of its own), `volume` (`value`, absolute `tol`, the `formula` it came
|
||||
from), `removed` (the same with the earlier body `from`: its volume minus `result`'s,
|
||||
for a result without a closed-form volume of its own), `cylinder_faces_max`,
|
||||
`sheet_corner` (signed end distances and flat-pattern invariants).
|
||||
`sheet_corner` (signed end distances and flat-pattern invariants), `manifold`
|
||||
(`result` must, or must not, be a closed manifold solid, `SmBrep::IsManifoldSolid`, so a
|
||||
blend that leaves an open body cannot pass on its volume alone), and `refused`
|
||||
(`step`, `error`: the kernel must refuse; the case passes when that step fails with an
|
||||
error containing `error`, fails when every step completes, and takes no other
|
||||
expectation). `refused` is the opposite of `refused_by`: there the refusal is a known
|
||||
defect and defers the case, here it is the right answer (a blend too large for its
|
||||
faces, for example `199-*/round_past_the_end_face_is_refused`). `refused.fit_size`
|
||||
(`value`, `tol`, `kind` `radius` (default), `scale` or `chord`, `source`) also requires the
|
||||
largest size that builds, which a refusal reports with `fillet_gn_section_generator`
|
||||
(`SolidError::FilletRefused`, WR-696): a refusal without a size fails it, and a refusal
|
||||
with one is still an `ERROR` in a case that expects none.
|
||||
* **Verdicts**: `PASS`, `FAIL`, `UNCHECKED` (no expectations were supplied, or no check
|
||||
failed but one cannot be measured
|
||||
yet: `cylinder_faces_max` until solidcore has a cylinder surface kind), `ERROR` (a step
|
||||
|
||||
@@ -30,6 +30,7 @@ fn summary(run: &Run) -> String {
|
||||
}
|
||||
Run::Deferred(d) => format!("deferred {}: {}", d.blocked_by, d.symbol),
|
||||
Run::Error(e) => format!("error {e}"),
|
||||
Run::Refused { message, fit } => format!("refused {message} (fit {})", fit.size),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use solidcore::blend::fillet_executive::api as fillet_api;
|
||||
use solidcore::blend::fillet_executive::FitKind;
|
||||
use solidcore::boolean::api_brep as boolean;
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
|
||||
@@ -35,6 +36,7 @@ use solidcore::mass::integrator::properties::{brep_volume, PropertyAttributes};
|
||||
use solidcore::primitive::api_curves as curves;
|
||||
use solidcore::primitive::api_primitives as primitives;
|
||||
use solidcore::primitive::primitive_creation::SweepOptions;
|
||||
use solidcore::query::api_queries::brep_is_manifold_solid;
|
||||
use solidcore::surface::surface::p2::SurfaceKind;
|
||||
use solidcore::surface::surface::Surface;
|
||||
use solidcore::sweep::api_brep as sweeps;
|
||||
@@ -495,6 +497,49 @@ pub struct Expect {
|
||||
/// Sheet-corner geometry and flat-pattern invariants from SolidCraft #3170.
|
||||
#[serde(default)]
|
||||
pub sheet_corner: Option<SheetCornerExpect>,
|
||||
/// The kernel must refuse: the case passes when step `step` fails with an error
|
||||
/// containing `error`, and fails when every step completes. Nothing else is checked
|
||||
/// then (there is no `result`). Unlike the case's `refused_by` (a known defect that
|
||||
/// defers the case), the refusal is the correct answer here.
|
||||
#[serde(default)]
|
||||
pub refused: Option<RefusedExpect>,
|
||||
/// Whether `result` must be a closed manifold solid (`SmBrep::IsManifoldSolid`): a
|
||||
/// blend that leaves an open or non-manifold body must not pass on its volume alone.
|
||||
#[serde(default)]
|
||||
pub manifold: Option<bool>,
|
||||
}
|
||||
|
||||
/// A refusal a case expects: the step that fails and text its error contains.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RefusedExpect {
|
||||
/// The `id` of the step that must fail.
|
||||
pub step: String,
|
||||
/// Text the step's error contains. Empty or whitespace-only text is invalid.
|
||||
pub error: String,
|
||||
/// The largest size that builds, which the refusal must report
|
||||
/// (`SolidError::FilletRefused`'s `fit`; needs `fillet_gn_section_generator`).
|
||||
#[serde(default)]
|
||||
pub fit_size: Option<FitSizeExpect>,
|
||||
}
|
||||
|
||||
/// A refusal's expected largest size that builds.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FitSizeExpect {
|
||||
/// The size.
|
||||
pub value: f64,
|
||||
/// Absolute tolerance.
|
||||
pub tol: f64,
|
||||
/// What it sizes (default `radius`).
|
||||
#[serde(default = "radius_kind")]
|
||||
pub kind: FitKindName,
|
||||
/// Where the value comes from (SolidCraft's test, the fork, a closed form).
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
fn radius_kind() -> FitKindName {
|
||||
FitKindName::Radius
|
||||
}
|
||||
|
||||
/// A valid folded solid, one connected non-overlapping flat pattern whose area
|
||||
@@ -864,6 +909,31 @@ pub fn validate(case: &Case) -> Result<(), String> {
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(r) = &case.expect.refused {
|
||||
if r.error.trim().is_empty() {
|
||||
return Err("refused needs the text its error contains".into());
|
||||
}
|
||||
if let Some(f) = &r.fit_size {
|
||||
if !(f.value.is_finite() && f.value >= 0.0 && f.tol.is_finite() && f.tol > 0.0) {
|
||||
return Err("refused fit_size value must be finite and >= 0, tol positive".into());
|
||||
}
|
||||
}
|
||||
if !case.steps.iter().any(|s| s.id() == r.step) {
|
||||
return Err(format!("refused names `{}`, which no step writes", r.step));
|
||||
}
|
||||
let e = &case.expect;
|
||||
if e.lumps.is_some()
|
||||
|| e.volume.is_some()
|
||||
|| e.removed.is_some()
|
||||
|| e.cylinder_faces_max.is_some()
|
||||
|| e.sheet_corner.is_some()
|
||||
|| e.manifold.is_some()
|
||||
{
|
||||
return Err(
|
||||
"a refused case has no result to check: drop its other expectations".into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(e) = &case.expect.sheet_corner {
|
||||
if !e.end_face_distances.iter().all(|x| x.is_finite())
|
||||
|| !e.distance_tol.is_finite()
|
||||
@@ -902,6 +972,8 @@ pub struct Measured {
|
||||
pub cylinder_faces: usize,
|
||||
/// The volume of the case's `removed.from` body minus `volume`, when it asks for it.
|
||||
pub removed: Option<f64>,
|
||||
/// `SmBrep::IsManifoldSolid`: a closed solid with every edge on two faces.
|
||||
pub manifold: bool,
|
||||
}
|
||||
|
||||
/// How a case's run ended.
|
||||
@@ -913,6 +985,54 @@ pub enum Run {
|
||||
Deferred(Deferral),
|
||||
/// A step returned an error other than `Unported`.
|
||||
Error(String),
|
||||
/// A fillet or chamfer step was refused as too large, with the largest size that
|
||||
/// builds it (`SolidError::FilletRefused`, opt-in `fillet_gn_section_generator`).
|
||||
/// Scored as an `ERROR`, except by a case that expects the refusal.
|
||||
Refused {
|
||||
/// The error, prefixed with the step as for [`Run::Error`].
|
||||
message: String,
|
||||
/// The largest size that builds the blend.
|
||||
fit: Fit,
|
||||
},
|
||||
}
|
||||
|
||||
/// A refusal's largest size that builds (`FitSize` without its edge).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Fit {
|
||||
/// The size: `> 0`, or `0` when no size builds.
|
||||
pub size: f64,
|
||||
/// What it sizes.
|
||||
pub kind: FitKindName,
|
||||
}
|
||||
|
||||
/// What a [`Fit`] sizes (`FitKind`), as a case names it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FitKindName {
|
||||
/// A constant ball's radius (a round's radius, an equal chamfer's solver radius).
|
||||
Radius,
|
||||
/// The factor on two unequal offsets.
|
||||
Scale,
|
||||
/// A chord width.
|
||||
Chord,
|
||||
}
|
||||
|
||||
impl FitKindName {
|
||||
fn of(kind: FitKind) -> Self {
|
||||
match kind {
|
||||
FitKind::Radius => Self::Radius,
|
||||
FitKind::Scale => Self::Scale,
|
||||
FitKind::Chord => Self::Chord,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Radius => "radius",
|
||||
Self::Scale => "scale",
|
||||
Self::Chord => "chord",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn p3(p: P3) -> Point3 {
|
||||
@@ -926,6 +1046,13 @@ fn solid(e: &SolidError) -> Run {
|
||||
blocked_by: (*unit).to_string(),
|
||||
symbol: (*upstream).to_string(),
|
||||
}),
|
||||
SolidError::FilletRefused { fit, .. } => Run::Refused {
|
||||
message: e.to_string(),
|
||||
fit: Fit {
|
||||
size: fit.size,
|
||||
kind: FitKindName::of(fit.kind),
|
||||
},
|
||||
},
|
||||
_ => Run::Error(e.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -1554,9 +1681,11 @@ fn measure(k: &Kernel, b: &mut Brep) -> Result<Measured, SolidError> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let manifold = brep_is_manifold_solid(b)?;
|
||||
Ok(Measured {
|
||||
volume,
|
||||
lumps,
|
||||
manifold,
|
||||
topology: [
|
||||
b.face_list().len(),
|
||||
b.edge_list().len(),
|
||||
@@ -1667,6 +1796,19 @@ pub fn run_rust_on(mut k: Kernel, case: &Case) -> Run {
|
||||
None => Run::Error(msg),
|
||||
};
|
||||
}
|
||||
Err(Run::Refused { message, fit }) => {
|
||||
let refused = case.refused_by.as_ref().filter(|r| {
|
||||
!r.error.trim().is_empty() && r.step == step.id() && message.contains(&r.error)
|
||||
});
|
||||
let message = format!("step {} (`{}`): {message}", n + 1, step.id());
|
||||
return match refused {
|
||||
Some(r) => Run::Deferred(Deferral {
|
||||
blocked_by: r.issue.clone(),
|
||||
symbol: message,
|
||||
}),
|
||||
None => Run::Refused { message, fit },
|
||||
};
|
||||
}
|
||||
Err(stop) => return stop,
|
||||
}
|
||||
}
|
||||
@@ -1694,7 +1836,8 @@ pub fn run_rust_on(mut k: Kernel, case: &Case) -> Run {
|
||||
/// One expectation checked.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Check {
|
||||
/// `lumps`, `volume`, `removed`, `cylinder_faces_max`, `sheet_corner`.
|
||||
/// `lumps`, `volume`, `removed`, `cylinder_faces_max`, `manifold`, `sheet_corner`,
|
||||
/// `refused`, `fit_size`.
|
||||
pub name: &'static str,
|
||||
/// `pass`, `fail` or `unchecked`.
|
||||
pub status: &'static str,
|
||||
@@ -1753,6 +1896,13 @@ pub fn checks(e: &Expect, m: &Measured) -> Vec<Check> {
|
||||
detail: format!("at most {n}, got {}", m.cylinder_faces),
|
||||
});
|
||||
}
|
||||
if let Some(want) = e.manifold {
|
||||
out.push(Check {
|
||||
name: "manifold",
|
||||
status: pass(m.manifold == want),
|
||||
detail: format!("expected {want}, got {}", m.manifold),
|
||||
});
|
||||
}
|
||||
if e.sheet_corner.is_some() {
|
||||
out.push(Check {
|
||||
name: "sheet_corner",
|
||||
@@ -1766,9 +1916,12 @@ pub fn checks(e: &Expect, m: &Measured) -> Vec<Check> {
|
||||
/// A run's verdict and its checks.
|
||||
/// A completed run without expectations is `UNCHECKED`, not a checked pass.
|
||||
pub fn verdict(e: &Expect, run: &Run) -> (&'static str, Vec<Check>) {
|
||||
if let Some(r) = &e.refused {
|
||||
return refusal_verdict(r, run);
|
||||
}
|
||||
match run {
|
||||
Run::Deferred(_) => ("DEFERRED", Vec::new()),
|
||||
Run::Error(_) => ("ERROR", Vec::new()),
|
||||
Run::Error(_) | Run::Refused { .. } => ("ERROR", Vec::new()),
|
||||
Run::Done(m) => {
|
||||
let c = checks(e, m);
|
||||
let v = if c.iter().any(|c| c.status == "fail") {
|
||||
@@ -1783,6 +1936,73 @@ pub fn verdict(e: &Expect, run: &Run) -> (&'static str, Vec<Check>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The verdict of a case that expects a refusal: `PASS` when step `r.step` failed with an
|
||||
/// error containing `r.error`, `FAIL` when every step completed or a step failed some
|
||||
/// other way, `DEFERRED` when unported code stopped it first.
|
||||
fn refusal_verdict(r: &RefusedExpect, run: &Run) -> (&'static str, Vec<Check>) {
|
||||
let check = |ok: bool, detail: String| Check {
|
||||
name: "refused",
|
||||
status: if ok { "pass" } else { "fail" },
|
||||
detail,
|
||||
};
|
||||
match run {
|
||||
Run::Deferred(_) => ("DEFERRED", Vec::new()),
|
||||
Run::Error(msg) | Run::Refused { message: msg, .. } => {
|
||||
// `run_rust_on` names the failing step as "step N (`id`): ...".
|
||||
let ok = msg.contains(&format!("(`{}`): ", r.step))
|
||||
&& !r.error.trim().is_empty()
|
||||
&& msg.contains(&r.error);
|
||||
let detail = format!(
|
||||
"expected `{}` to fail with `{}`, got: {msg}",
|
||||
r.step, r.error
|
||||
);
|
||||
let mut checks = vec![check(ok, detail)];
|
||||
if let Some(want) = &r.fit_size {
|
||||
let got = match run {
|
||||
Run::Refused { fit, .. } => Some(*fit),
|
||||
_ => None,
|
||||
};
|
||||
let fit_ok = got.is_some_and(|g| {
|
||||
g.kind == want.kind && (g.size - want.value).abs() <= want.tol
|
||||
});
|
||||
checks.push(Check {
|
||||
name: "fit_size",
|
||||
status: if fit_ok { "pass" } else { "fail" },
|
||||
detail: match got {
|
||||
Some(g) => format!(
|
||||
"expected {} {} +- {}, got {} {} (off by {})",
|
||||
want.kind.as_str(),
|
||||
want.value,
|
||||
want.tol,
|
||||
g.kind.as_str(),
|
||||
g.size,
|
||||
(g.size - want.value).abs()
|
||||
),
|
||||
None => format!(
|
||||
"expected {} {} +- {}, the refusal reports no size",
|
||||
want.kind.as_str(),
|
||||
want.value,
|
||||
want.tol
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
let all = checks.iter().all(|c| c.status == "pass");
|
||||
(if all { "PASS" } else { "FAIL" }, checks)
|
||||
}
|
||||
Run::Done(_) => (
|
||||
"FAIL",
|
||||
vec![check(
|
||||
false,
|
||||
format!(
|
||||
"expected `{}` to be refused, but every step completed",
|
||||
r.step
|
||||
),
|
||||
)],
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Options of a `--solidcraft-cases` run.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CasesOptions {
|
||||
@@ -1804,8 +2024,27 @@ fn run_entry(run: &Run, verdict: &str, checks: &[Check]) -> Value {
|
||||
json!({"blocked_by": d.blocked_by, "symbol": d.symbol}),
|
||||
);
|
||||
}
|
||||
Run::Error(msg) => {
|
||||
Run::Error(msg) | Run::Refused { message: msg, .. } => {
|
||||
e.insert("error".into(), json!(msg));
|
||||
if let Run::Refused { fit, .. } = run {
|
||||
e.insert(
|
||||
"fit".into(),
|
||||
json!({"size": crate::json::num(fit.size), "kind": fit.kind.as_str()}),
|
||||
);
|
||||
}
|
||||
if !checks.is_empty() {
|
||||
e.insert(
|
||||
"checks".into(),
|
||||
Value::Array(
|
||||
checks
|
||||
.iter()
|
||||
.map(
|
||||
|c| json!({"name": c.name, "status": c.status, "detail": c.detail}),
|
||||
)
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Run::Done(m) => {
|
||||
e.insert(
|
||||
@@ -1818,6 +2057,7 @@ fn run_entry(run: &Run, verdict: &str, checks: &[Check]) -> Value {
|
||||
"vertices": m.topology[2],
|
||||
"cylinder_faces": m.cylinder_faces,
|
||||
"removed": m.removed.map(crate::json::num),
|
||||
"manifold": m.manifold,
|
||||
}),
|
||||
);
|
||||
e.insert(
|
||||
@@ -1868,6 +2108,18 @@ pub fn run_cases(root: &Path, opts: &CasesOptions) -> ExitCode {
|
||||
format!("{}: {}", d.blocked_by, d.symbol)
|
||||
}
|
||||
Run::Error(msg) => msg.clone(),
|
||||
Run::Refused { message, fit } => {
|
||||
let fit_checks: Vec<String> = c
|
||||
.iter()
|
||||
.filter(|c| c.name == "fit_size")
|
||||
.map(|c| format!("fit_size {}: {}", c.status, c.detail))
|
||||
.collect();
|
||||
if fit_checks.is_empty() {
|
||||
format!("{message} [fit {} {}]", fit.kind.as_str(), fit.size)
|
||||
} else {
|
||||
format!("{message}; {}", fit_checks.join("; "))
|
||||
}
|
||||
}
|
||||
Run::Done(m) => {
|
||||
let [f, e, v] = m.topology;
|
||||
let checked = c
|
||||
@@ -2050,7 +2302,9 @@ mod tests {
|
||||
e.lumps.is_some()
|
||||
|| e.volume.is_some()
|
||||
|| e.removed.is_some()
|
||||
|| e.sheet_corner.is_some(),
|
||||
|| e.sheet_corner.is_some()
|
||||
|| e.refused.is_some()
|
||||
|| e.manifold.is_some(),
|
||||
"{}: nothing to check",
|
||||
l.id
|
||||
);
|
||||
@@ -2295,6 +2549,167 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refused_expectation_passes_only_on_that_steps_refusal() {
|
||||
let e: Expect = serde_json::from_value(json!({
|
||||
"refused": {"step": "result", "error": "status 1001"}
|
||||
}))
|
||||
.unwrap();
|
||||
let r = e.refused.as_ref().unwrap();
|
||||
let refused = Run::Error("step 2 (`result`): blend::x: upstream status 1001".into());
|
||||
assert_eq!(verdict(&e, &refused).0, "PASS");
|
||||
// another step, or another error, is not the refusal asked for
|
||||
let other_step = Run::Error("step 1 (`body`): upstream status 1001".into());
|
||||
assert_eq!(verdict(&e, &other_step).0, "FAIL");
|
||||
let other_error = Run::Error("step 2 (`result`): no body `x`".into());
|
||||
assert_eq!(verdict(&e, &other_error).0, "FAIL");
|
||||
// completing is the failure a refused case guards against
|
||||
let built = Run::Done(Measured {
|
||||
volume: 0.0,
|
||||
lumps: 0,
|
||||
topology: [7, 20, 14],
|
||||
cylinder_faces: 0,
|
||||
removed: None,
|
||||
manifold: false,
|
||||
});
|
||||
let (v, c) = verdict(&e, &built);
|
||||
assert_eq!(v, "FAIL");
|
||||
assert!(c[0].detail.contains("every step completed"), "{c:?}");
|
||||
let deferred = Run::Deferred(Deferral {
|
||||
blocked_by: "unit".into(),
|
||||
symbol: "Sm".into(),
|
||||
});
|
||||
assert_eq!(verdict(&e, &deferred).0, "DEFERRED");
|
||||
assert_eq!(r.step, "result");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refused_cases_take_no_result_expectations() {
|
||||
let case = |expect: Value| -> Case {
|
||||
serde_json::from_value(json!({
|
||||
"name": "c",
|
||||
"steps": [{"op": "box", "id": "result", "origin": [0.0, 0.0, 0.0],
|
||||
"size": [1.0, 1.0, 1.0]}],
|
||||
"expect": expect
|
||||
}))
|
||||
.unwrap()
|
||||
};
|
||||
assert!(validate(&case(
|
||||
json!({"refused": {"step": "result", "error": "1001"}})
|
||||
))
|
||||
.is_ok());
|
||||
assert!(validate(&case(json!({"refused": {"step": "result", "error": " "}}))).is_err());
|
||||
assert!(
|
||||
validate(&case(json!({"refused": {"step": "result", "error": "1001",
|
||||
"fit_size": {"value": -1.0, "tol": 1e-6, "source": "s"}}})))
|
||||
.is_err()
|
||||
);
|
||||
assert!(validate(&case(json!({"refused": {"step": "nope", "error": "1001"}}))).is_err());
|
||||
assert!(validate(&case(
|
||||
json!({"lumps": 1, "refused": {"step": "result", "error": "1001"}})
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifold_expectation_checks_the_result() {
|
||||
let e: Expect = serde_json::from_value(json!({"lumps": 1, "manifold": true})).unwrap();
|
||||
let m = |manifold: bool| {
|
||||
Run::Done(Measured {
|
||||
volume: 1.0,
|
||||
lumps: 1,
|
||||
topology: [6, 12, 8],
|
||||
cylinder_faces: 0,
|
||||
removed: None,
|
||||
manifold,
|
||||
})
|
||||
};
|
||||
assert_eq!(verdict(&e, &m(true)).0, "PASS");
|
||||
let (v, c) = verdict(&e, &m(false));
|
||||
assert_eq!(v, "FAIL");
|
||||
assert!(
|
||||
c.iter().any(|c| c.name == "manifold" && c.status == "fail"),
|
||||
"{c:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_round_taller_than_its_wall_is_refused() {
|
||||
// SolidCraft #190: an r 13 disc 2 high cannot take a round of 2.001 on its top
|
||||
// circle; the pinned kernel refuses it, and the refused case passes on that.
|
||||
let case: Case = serde_json::from_value(json!({
|
||||
"name": "disc-round-too-tall",
|
||||
"steps": [
|
||||
{"op": "cylinder", "id": "disc", "origin": [0.0, 0.0, 0.0], "radius": 13.0,
|
||||
"height": 2.0},
|
||||
{"op": "fillet", "id": "result", "body": "disc", "edges": [[0.0, 13.0, 2.0]],
|
||||
"radius": 2.001}
|
||||
],
|
||||
"expect": {"refused": {"step": "result", "error": "status 1001"}}
|
||||
}))
|
||||
.unwrap();
|
||||
validate(&case).unwrap();
|
||||
let run = run_rust(&case);
|
||||
assert_eq!(verdict(&case.expect, &run).0, "PASS", "{run:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refused_fit_size_is_checked_against_the_refusal() {
|
||||
let e: Expect = serde_json::from_value(json!({
|
||||
"refused": {"step": "result", "error": "status 1001",
|
||||
"fit_size": {"value": 3.0, "tol": 1e-6, "source": "s"}}
|
||||
}))
|
||||
.unwrap();
|
||||
let refused = |size: f64, kind: FitKindName| Run::Refused {
|
||||
message: "step 2 (`result`): x: upstream status 1001: refused".into(),
|
||||
fit: Fit { size, kind },
|
||||
};
|
||||
assert_eq!(verdict(&e, &refused(3.0, FitKindName::Radius)).0, "PASS");
|
||||
let (v, c) = verdict(&e, &refused(2.5, FitKindName::Radius));
|
||||
assert_eq!(v, "FAIL");
|
||||
assert!(
|
||||
c.iter().any(|c| c.name == "fit_size" && c.status == "fail"),
|
||||
"{c:?}"
|
||||
);
|
||||
assert_eq!(verdict(&e, &refused(3.0, FitKindName::Scale)).0, "FAIL");
|
||||
// a refusal without a size does not meet a fit_size expectation
|
||||
let plain = Run::Error("step 2 (`result`): x: upstream status 1001".into());
|
||||
let (v, c) = verdict(&e, &plain);
|
||||
assert_eq!(v, "FAIL");
|
||||
assert!(
|
||||
c.iter().any(|c| c.detail.contains("reports no size")),
|
||||
"{c:?}"
|
||||
);
|
||||
// and a refusal with a size is still an ERROR where no refusal is expected
|
||||
let none: Expect = serde_json::from_value(json!({"lumps": 1})).unwrap();
|
||||
assert_eq!(
|
||||
verdict(&none, &refused(3.0, FitKindName::Radius)).0,
|
||||
"ERROR"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_round_past_the_end_face_reports_its_fit() {
|
||||
// SolidCraft #199: r 3.5 on a 14 x 6 bar's vertical edges; with the generic
|
||||
// blend's checks the refusal reports the largest radius that builds, 3.
|
||||
let case: Case = serde_json::from_value(json!({
|
||||
"name": "bar-round-too-large",
|
||||
"fixes": ["fillet_refuse_overlapping_bands", "fillet_gn_section_generator"],
|
||||
"steps": [
|
||||
{"op": "box", "id": "bar", "origin": [14.0, 0.0, 0.0], "size": [14.0, 6.0, 10.0]},
|
||||
{"op": "fillet", "id": "result", "body": "bar",
|
||||
"edges": [[14.0, 0.0, 5.0], [28.0, 0.0, 5.0], [14.0, 6.0, 5.0], [28.0, 6.0, 5.0]],
|
||||
"radius": 3.5}
|
||||
],
|
||||
"expect": {"refused": {"step": "result", "error": "refused",
|
||||
"fit_size": {"value": 3.0, "tol": 1e-6, "source": "half the end face"}}}
|
||||
}))
|
||||
.unwrap();
|
||||
validate(&case).unwrap();
|
||||
let run = run_rust(&case);
|
||||
assert_eq!(verdict(&case.expect, &run).0, "PASS", "{run:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scoring() {
|
||||
let e = Expect {
|
||||
@@ -2306,6 +2721,8 @@ mod tests {
|
||||
}),
|
||||
cylinder_faces_max: None,
|
||||
sheet_corner: None,
|
||||
refused: None,
|
||||
manifold: None,
|
||||
removed: None,
|
||||
};
|
||||
let ok = Measured {
|
||||
@@ -2314,6 +2731,7 @@ mod tests {
|
||||
topology: [4, 4, 4],
|
||||
cylinder_faces: 0,
|
||||
removed: None,
|
||||
manifold: true,
|
||||
};
|
||||
assert_eq!(verdict(&e, &Run::Done(ok)).0, "PASS");
|
||||
// The bug: the whole intersect came out empty.
|
||||
@@ -2323,6 +2741,7 @@ mod tests {
|
||||
topology: [0, 0, 0],
|
||||
cylinder_faces: 0,
|
||||
removed: None,
|
||||
manifold: true,
|
||||
};
|
||||
let (v, c) = verdict(&e, &Run::Done(empty));
|
||||
assert_eq!(v, "FAIL");
|
||||
@@ -2361,6 +2780,7 @@ mod tests {
|
||||
topology: [6, 12, 8],
|
||||
cylinder_faces: 0,
|
||||
removed: None,
|
||||
manifold: true,
|
||||
});
|
||||
let (v, c) = verdict(&e, &run);
|
||||
assert!(c.is_empty());
|
||||
@@ -2442,6 +2862,7 @@ mod tests {
|
||||
topology: [1, 0, 0],
|
||||
cylinder_faces: 0,
|
||||
removed: None,
|
||||
manifold: true,
|
||||
})
|
||||
)
|
||||
.0,
|
||||
@@ -2571,6 +2992,7 @@ mod tests {
|
||||
topology: [6, 12, 8],
|
||||
cylinder_faces: 0,
|
||||
removed,
|
||||
manifold: true,
|
||||
};
|
||||
assert_eq!(verdict(&e, &Run::Done(m(Some(10.5)))).0, "PASS");
|
||||
assert_eq!(verdict(&e, &Run::Done(m(Some(12.0)))).0, "FAIL");
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"issue": "https://github.com/tangency-labs/solidcraft/issues/1080",
|
||||
"pr": "https://github.com/tangency-labs/solidcraft/pull/1124",
|
||||
"test": "crates/kernel/src/issue_1080_tests.rs: chamfer_into_a_pocket_is_refused_with_the_size_that_fits, fillet_into_a_pocket_is_refused_and_one_that_fits_builds, chamfers_crossing_each_other_are_refused, chamfers_meeting_at_a_corner_still_build",
|
||||
"summary": "On SolidCraft's earlier kernel, fillets and chamfers too large for the faces beside their edge (running into a pocket, or two blends crossing on a narrow face) were built through instead of refused (#1080). The fix refuses them with the largest size that fits; these cases keep the sizes that do fit building, with closed-form volumes: chamfers 2 and 4 and a round 3 on the edge beside a pocket 4 mm in, both top edges of a 4 mm rib at 1.5 (they would meet at 2), and the four top edges of a cube at 5, which mitre at the corners and must not be refused for overlapping there. Two more cases take the test's too-large sizes into the pocket (chamfer 6, round 5) with the cliff overflow (`fillet_refuse_rails_leaving_faces`, `fillet_cliff_overflow`, tangency-labs/solidcore#960): built as if the faces went on, removing exactly what lies in the body. The runner cannot require a refusal, so the rib's crossing chamfer and round of 3 are not ported, nor its SolidCraft-only volume guard; its #1668 and concentric-round tests are already 1668-round-both-sides-of-wall.json and tangency-labs/solidcore#683.",
|
||||
"summary": "On SolidCraft's earlier kernel, fillets and chamfers too large for the faces beside their edge (running into a pocket, or two blends crossing on a narrow face) were built through instead of refused (#1080). The fix refuses them with the largest size that fits; these cases keep the sizes that do fit building, with closed-form volumes: chamfers 2 and 4 and a round 3 on the edge beside a pocket 4 mm in, both top edges of a 4 mm rib at 1.5 (they would meet at 2), and the four top edges of a cube at 5, which mitre at the corners and must not be refused for overlapping there. Two more cases take the test's too-large sizes into the pocket (chamfer 6, round 5) with the cliff overflow (`fillet_refuse_rails_leaving_faces`, `fillet_cliff_overflow`, tangency-labs/solidcore#960): built as if the faces went on, removing exactly what lies in the body. The runner cannot require a refusal, so the rib's crossing chamfer and round of 3 are not ported, nor its SolidCraft-only volume guard; its #1668 and concentric-round tests are already 1668-round-both-sides-of-wall.json and tangency-labs/solidcore#683. The rib's crossing chamfer and round of 3 must be refused, reporting the largest size that builds, 2 (expect.refused with fit_size; `fillet_gn_section_generator`, `fillet_refuse_overlapping_bands`).",
|
||||
"cases": [
|
||||
{
|
||||
"name": "chamfer_beside_pocket_2",
|
||||
@@ -528,6 +528,112 @@
|
||||
"formula": "(1 - pi/4)*5^2*20 - 10*(5 - (sqrt(24)/2 + 12.5*asin(1/5))) (the round's corner less its part over the pocket, x 35..36, y 5..15)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rib_chamfers_3_cross_and_are_refused",
|
||||
"note": "Both top edges of the 4 mm rib, chamfer 3: past 2 the two blends' rails cross on the top face. With the generic blend's checks they are refused with the largest size that builds, 2 (WR-696, tangency-labs/solidcore#2278). With the pinned defaults they return success with a non-manifold body and the rib's volume gone (WR-460).",
|
||||
"fixes": [
|
||||
"fillet_refuse_overlapping_bands",
|
||||
"fillet_gn_section_generator"
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"op": "box",
|
||||
"id": "rib",
|
||||
"origin": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
40,
|
||||
4,
|
||||
20
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "chamfer",
|
||||
"id": "result",
|
||||
"body": "rib",
|
||||
"edges": [
|
||||
[
|
||||
20,
|
||||
0,
|
||||
20
|
||||
],
|
||||
[
|
||||
20,
|
||||
4,
|
||||
20
|
||||
]
|
||||
],
|
||||
"distance": 3.0
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"refused": {
|
||||
"step": "result",
|
||||
"error": "refused; the largest size that builds",
|
||||
"fit_size": {
|
||||
"value": 2.0,
|
||||
"tol": 1e-06,
|
||||
"source": "SolidCraft refuses it: 'the largest size that fits is 2 mm', half the 4 mm rib (chamfers_crossing_each_other_are_refused)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rib_rounds_3_cross_and_are_refused",
|
||||
"note": "Both top edges of the 4 mm rib, fillet 3: past 2 the two blends' rails cross on the top face. With the generic blend's checks they are refused with the largest size that builds, 2 (WR-696, tangency-labs/solidcore#2278). With the pinned defaults they return success with a non-manifold body and the rib's volume gone (WR-460).",
|
||||
"fixes": [
|
||||
"fillet_refuse_overlapping_bands",
|
||||
"fillet_gn_section_generator"
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"op": "box",
|
||||
"id": "rib",
|
||||
"origin": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
40,
|
||||
4,
|
||||
20
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "fillet",
|
||||
"id": "result",
|
||||
"body": "rib",
|
||||
"edges": [
|
||||
[
|
||||
20,
|
||||
0,
|
||||
20
|
||||
],
|
||||
[
|
||||
20,
|
||||
4,
|
||||
20
|
||||
]
|
||||
],
|
||||
"radius": 3.0
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"refused": {
|
||||
"step": "result",
|
||||
"error": "refused; the largest size that builds",
|
||||
"fit_size": {
|
||||
"value": 2.0,
|
||||
"tol": 1e-06,
|
||||
"source": "SolidCraft refuses it: 'the largest size that fits is 2 mm', half the 4 mm rib (chamfers_crossing_each_other_are_refused)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"issue": "https://github.com/tangency-labs/solidcraft/issues/1668",
|
||||
"pr": "https://github.com/tangency-labs/solidcraft/pull/1720",
|
||||
"test": "crates/kernel/src/issue_1080_tests.rs: rounds_on_both_sides_of_a_wall_build_as_one_fillet",
|
||||
"summary": "A 104 x 64 x 42 block less a 100 x 60 x 41 cavity open at the bottom (2 mm walls and roof), as in the test (the issue's 100 x 60 x 40 box shelled outward 2 mm). R5 on the outer top edge (y = 32, z = 42, length 104) together with R5 on the concave inner roof/wall edge below it (y = 30, z = 40, length 100) in one fillet was refused as too large (largest 4 mm), although the two built one after the other; #1720 made the fit check count the material the inner round adds. The outer round removes 25(1-pi/4) mm^2 along 104 mm, the inner one adds it along 100 mm: the cavity corner (30, 40) lies 3*sqrt(2) = 4.24 mm from the outer round's ball centre (27, 37), inside its radius, so the removed region stays in material and misses the added one. #1720's test also refused R5 on the outer edge alone; SolidCraft PR #2852 later let it build (it leaves the inner corner 0.76 mm inside the round and reaches it only past 2*sqrt(2)/(sqrt(2)-1) = 6.83 mm), and the test on main builds it. The R7 refusals (outer edge alone, and both edges) are not expressible in the runner. Tolerances are 0.1 % of the outer round's volume. Volumes are closed form.",
|
||||
"summary": "A 104 x 64 x 42 block less a 100 x 60 x 41 cavity open at the bottom (2 mm walls and roof), as in the test (the issue's 100 x 60 x 40 box shelled outward 2 mm). R5 on the outer top edge (y = 32, z = 42, length 104) together with R5 on the concave inner roof/wall edge below it (y = 30, z = 40, length 100) in one fillet was refused as too large (largest 4 mm), although the two built one after the other; #1720 made the fit check count the material the inner round adds. The outer round removes 25(1-pi/4) mm^2 along 104 mm, the inner one adds it along 100 mm: the cavity corner (30, 40) lies 3*sqrt(2) = 4.24 mm from the outer round's ball centre (27, 37), inside its radius, so the removed region stays in material and misses the added one. #1720's test also refused R5 on the outer edge alone; SolidCraft PR #2852 later let it build (it leaves the inner corner 0.76 mm inside the round and reaches it only past 2*sqrt(2)/(sqrt(2)-1) = 6.83 mm), and the test on main builds it. R7 on the outer edge alone breaks through the wall (#2299, tangency-labs/solidcore#2311): with fillet_refuse_rounds_breaking_through it is refused with its fit, and the fit builds. R7 on both edges is not covered. Tolerances are 0.1 % of the outer round's volume. Volumes are closed form.",
|
||||
"cases": [
|
||||
{
|
||||
"name": "shelled_block",
|
||||
@@ -178,6 +178,140 @@
|
||||
"formula": "104*64*42 - 100*60*40 - 25*(1-pi/4)*104 + 25*(1-pi/4)*100 (tol: 0.1 % of the outer round)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "round_7_outer_edge_refused_with_its_fit",
|
||||
"note": "#2299: R7 on the outer top edge alone. The ball centre (25, 35) is sqrt(2)*(7-2) = 7.07 mm from the inner corner (30, 40), so the round breaks through the wall. With the default flags the pinned port returns a self-intersecting body with the whole section removed (WR-707); fillet_refuse_rounds_breaking_through refuses it and reports the largest radius that stays inside the wall, less the fit margin.",
|
||||
"steps": [
|
||||
{
|
||||
"op": "box",
|
||||
"id": "outer",
|
||||
"origin": [
|
||||
-52,
|
||||
-32,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
104,
|
||||
64,
|
||||
42
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "box",
|
||||
"id": "cavity",
|
||||
"origin": [
|
||||
-50,
|
||||
-30,
|
||||
-1
|
||||
],
|
||||
"size": [
|
||||
100,
|
||||
60,
|
||||
41
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "difference",
|
||||
"id": "body",
|
||||
"a": "outer",
|
||||
"b": "cavity"
|
||||
},
|
||||
{
|
||||
"op": "fillet",
|
||||
"id": "result",
|
||||
"body": "body",
|
||||
"edges": [
|
||||
[
|
||||
0,
|
||||
32,
|
||||
42
|
||||
]
|
||||
],
|
||||
"radius": 7
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"refused": {
|
||||
"step": "result",
|
||||
"error": "refused; the largest size that builds",
|
||||
"fit_size": {
|
||||
"value": 6.818427124746189,
|
||||
"tol": 0.0001,
|
||||
"source": "2*sqrt(2)/(sqrt(2)-1) - 0.01: the radius at which the round reaches the inner corner (30, 40), less the 0.01 fit margin (tangency-labs/solidcore#2311)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fixes": [
|
||||
"fillet_refuse_rounds_breaking_through",
|
||||
"fillet_gn_section_generator"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "round_at_the_fit_builds",
|
||||
"note": "The fit #2299's refusal reports, 2*sqrt(2)/(sqrt(2)-1) - 0.01, builds on the outer edge: the round stays inside the wall.",
|
||||
"steps": [
|
||||
{
|
||||
"op": "box",
|
||||
"id": "outer",
|
||||
"origin": [
|
||||
-52,
|
||||
-32,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
104,
|
||||
64,
|
||||
42
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "box",
|
||||
"id": "cavity",
|
||||
"origin": [
|
||||
-50,
|
||||
-30,
|
||||
-1
|
||||
],
|
||||
"size": [
|
||||
100,
|
||||
60,
|
||||
41
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "difference",
|
||||
"id": "body",
|
||||
"a": "outer",
|
||||
"b": "cavity"
|
||||
},
|
||||
{
|
||||
"op": "fillet",
|
||||
"id": "result",
|
||||
"body": "body",
|
||||
"edges": [
|
||||
[
|
||||
0,
|
||||
32,
|
||||
42
|
||||
]
|
||||
],
|
||||
"radius": 6.818427124746189
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"lumps": 1,
|
||||
"manifold": true,
|
||||
"volume": {
|
||||
"value": 38514.387535910304,
|
||||
"tol": 1.0376124640896986,
|
||||
"formula": "104*64*42 - 100*60*40 - r^2*(1-pi/4)*104, r = 2*sqrt(2)/(sqrt(2)-1) - 0.01 (tol: 0.1 % of the round)"
|
||||
}
|
||||
},
|
||||
"fixes": [
|
||||
"fillet_refuse_rounds_breaking_through",
|
||||
"fillet_gn_section_generator"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"issue": "https://github.com/tangency-labs/solidcraft/issues/199",
|
||||
"pr": "https://github.com/tangency-labs/solidcraft/pull/317",
|
||||
"test": "crates/kernel/src/tests.rs: fillet_consuming_a_side_face_makes_a_full_round",
|
||||
"summary": "A round that uses up a whole side face must make a full round, not be refused (#199). A 14 x 6 x 10 bar has its four vertical edges rounded r 3: both 6 mm end faces are used up, each end becomes a half cylinder. With one end only, two rounds meet on that end face. A 6 x 6 x 10 post rounded r 3 on all four vertical edges is a cylinder of radius 3. Closed-form volumes; SolidCraft checked 2e-4 relative on a mesh, these check 1e-4 mm^3. The test's past-the-limit refusals are not ported.",
|
||||
"summary": "A round that uses up a whole side face must make a full round, not be refused (#199). A 14 x 6 x 10 bar has its four vertical edges rounded r 3: both 6 mm end faces are used up, each end becomes a half cylinder. With one end only, two rounds meet on that end face. A 6 x 6 x 10 post rounded r 3 on all four vertical edges is a cylinder of radius 3. Closed-form volumes; SolidCraft checked 2e-4 relative on a mesh, these check 1e-4 mm^3. The test's past-the-limit sizes (3.001, 3.5) must be refused (expect.refused); 3.5 needs fillet_refuse_crossing_rails, without which the pinned SMLib returns an empty, non-manifold body. With the generic blend's checks (`fillet_gn_section_generator`, `fillet_refuse_overlapping_bands`) the refusals report the largest radius that builds, 3 (expect.refused.fit_size).",
|
||||
"cases": [
|
||||
{
|
||||
"name": "both_ends_full_round",
|
||||
@@ -217,6 +217,243 @@
|
||||
"formula": "pi*3^2*10"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "round_past_the_end_face_is_refused",
|
||||
"note": "r 3.001 on the four vertical edges: past the 6 mm end faces, which have nothing left to use up. SolidCraft refuses it as larger than the neighbouring faces; so does solidcore, with the default flags and with every flag.",
|
||||
"steps": [
|
||||
{
|
||||
"op": "box",
|
||||
"id": "bar",
|
||||
"origin": [
|
||||
14,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
14,
|
||||
6,
|
||||
10
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "fillet",
|
||||
"id": "result",
|
||||
"body": "bar",
|
||||
"edges": [
|
||||
[
|
||||
14,
|
||||
0,
|
||||
5
|
||||
],
|
||||
[
|
||||
28,
|
||||
0,
|
||||
5
|
||||
],
|
||||
[
|
||||
14,
|
||||
6,
|
||||
5
|
||||
],
|
||||
[
|
||||
28,
|
||||
6,
|
||||
5
|
||||
]
|
||||
],
|
||||
"radius": 3.001
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"refused": {
|
||||
"step": "result",
|
||||
"error": "status 1001"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "round_well_past_the_end_face_is_refused",
|
||||
"note": "r 3.5 on the four vertical edges. With the default flags the pinned SMLib returns success with a non-manifold body and all of the bar's volume gone (WR-460); fillet_refuse_crossing_rails refuses it.",
|
||||
"fixes": [
|
||||
"fillet_refuse_crossing_rails"
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"op": "box",
|
||||
"id": "bar",
|
||||
"origin": [
|
||||
14,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
14,
|
||||
6,
|
||||
10
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "fillet",
|
||||
"id": "result",
|
||||
"body": "bar",
|
||||
"edges": [
|
||||
[
|
||||
14,
|
||||
0,
|
||||
5
|
||||
],
|
||||
[
|
||||
28,
|
||||
0,
|
||||
5
|
||||
],
|
||||
[
|
||||
14,
|
||||
6,
|
||||
5
|
||||
],
|
||||
[
|
||||
28,
|
||||
6,
|
||||
5
|
||||
]
|
||||
],
|
||||
"radius": 3.5
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"refused": {
|
||||
"step": "result",
|
||||
"error": "status 1001"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "round_3_001_refused_with_its_fit",
|
||||
"note": "r 3.001 on the four vertical edges with the generic blend's fold and room checks: refused, and the largest radius that builds, 3 (half the 6 mm end face), comes back on the refusal (WR-696, tangency-labs/solidcore#2278).",
|
||||
"fixes": [
|
||||
"fillet_refuse_overlapping_bands",
|
||||
"fillet_gn_section_generator"
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"op": "box",
|
||||
"id": "bar",
|
||||
"origin": [
|
||||
14,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
14,
|
||||
6,
|
||||
10
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "fillet",
|
||||
"id": "result",
|
||||
"body": "bar",
|
||||
"edges": [
|
||||
[
|
||||
14,
|
||||
0,
|
||||
5
|
||||
],
|
||||
[
|
||||
28,
|
||||
0,
|
||||
5
|
||||
],
|
||||
[
|
||||
14,
|
||||
6,
|
||||
5
|
||||
],
|
||||
[
|
||||
28,
|
||||
6,
|
||||
5
|
||||
]
|
||||
],
|
||||
"radius": 3.001
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"refused": {
|
||||
"step": "result",
|
||||
"error": "refused; the largest size that builds",
|
||||
"fit_size": {
|
||||
"value": 3.0,
|
||||
"tol": 1e-06,
|
||||
"source": "SolidCraft's fit reduces it to 3, half the 6 mm end face (fillet_consuming_a_side_face_makes_a_full_round)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "round_3_5_refused_with_its_fit",
|
||||
"note": "r 3.5 on the four vertical edges with the generic blend's fold and room checks: refused, and the largest radius that builds, 3 (half the 6 mm end face), comes back on the refusal (WR-696, tangency-labs/solidcore#2278).",
|
||||
"fixes": [
|
||||
"fillet_refuse_overlapping_bands",
|
||||
"fillet_gn_section_generator"
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"op": "box",
|
||||
"id": "bar",
|
||||
"origin": [
|
||||
14,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
14,
|
||||
6,
|
||||
10
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "fillet",
|
||||
"id": "result",
|
||||
"body": "bar",
|
||||
"edges": [
|
||||
[
|
||||
14,
|
||||
0,
|
||||
5
|
||||
],
|
||||
[
|
||||
28,
|
||||
0,
|
||||
5
|
||||
],
|
||||
[
|
||||
14,
|
||||
6,
|
||||
5
|
||||
],
|
||||
[
|
||||
28,
|
||||
6,
|
||||
5
|
||||
]
|
||||
],
|
||||
"radius": 3.5
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"refused": {
|
||||
"step": "result",
|
||||
"error": "refused; the largest size that builds",
|
||||
"fit_size": {
|
||||
"value": 3.0,
|
||||
"tol": 1e-06,
|
||||
"source": "SolidCraft's fit reduces it to 3, half the 6 mm end face (fillet_consuming_a_side_face_makes_a_full_round)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
{
|
||||
"issue": "https://github.com/tangency-labs/solidcraft/issues/871",
|
||||
"pr": "https://github.com/tangency-labs/solidcraft/pull/949",
|
||||
"test": "crates/kernel/src/issue_871_tests.rs: overlapping_rounds_are_refused, rounds_that_fit_are_closed",
|
||||
"summary": "A fillet that opened holes in the model must be refused, not accepted (#871). The part is a plate with a narrower block on top and a window through both; every edge of both end faces is rounded. At r 1 the rounds on the window's edges and on the block's sides overlap across the 1.26 mm wall between them: the pinned SMLib returns success with a non-manifold body and all of the part's volume gone (WR-460), fillet_refuse_crossing_rails refuses it. At r 0.5 they fit: one closed manifold solid (its volume has no closed form here).",
|
||||
"cases": [
|
||||
{
|
||||
"name": "overlapping_rounds_are_refused",
|
||||
"note": "r 1 on the 24 end-face edges: the rounds overlap across the 1.26 mm wall.",
|
||||
"fixes": [
|
||||
"fillet_refuse_crossing_rails"
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"op": "box",
|
||||
"id": "base",
|
||||
"origin": [
|
||||
9.5,
|
||||
8,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
14,
|
||||
21.5,
|
||||
10
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "box",
|
||||
"id": "top",
|
||||
"origin": [
|
||||
9.5,
|
||||
12.74437365538526,
|
||||
10
|
||||
],
|
||||
"size": [
|
||||
14,
|
||||
9.9483859782841,
|
||||
10
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "union",
|
||||
"id": "joined",
|
||||
"a": "base",
|
||||
"b": "top"
|
||||
},
|
||||
{
|
||||
"op": "box",
|
||||
"id": "window",
|
||||
"origin": [
|
||||
9.5,
|
||||
14,
|
||||
7
|
||||
],
|
||||
"size": [
|
||||
14,
|
||||
5.2,
|
||||
7.1
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "difference",
|
||||
"id": "part",
|
||||
"a": "joined",
|
||||
"b": "window"
|
||||
},
|
||||
{
|
||||
"op": "fillet",
|
||||
"id": "result",
|
||||
"body": "part",
|
||||
"edges": [
|
||||
[
|
||||
23.5,
|
||||
12.74437365538526,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
10.37218682769263,
|
||||
10.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
8.0,
|
||||
5.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
18.75,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
29.5,
|
||||
5.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
26.09637981683468,
|
||||
10.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
22.69275963366936,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
17.71856664452731,
|
||||
20.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
16.6,
|
||||
7.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
14.0,
|
||||
10.55
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
16.6,
|
||||
14.100000000000001
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
19.200000000000003,
|
||||
10.55
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
12.74437365538526,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
10.37218682769263,
|
||||
10.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
8.0,
|
||||
5.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
18.75,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
29.5,
|
||||
5.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
26.09637981683468,
|
||||
10.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
22.69275963366936,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
17.71856664452731,
|
||||
20.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
16.6,
|
||||
7.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
14.0,
|
||||
10.55
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
16.6,
|
||||
14.100000000000001
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
19.200000000000003,
|
||||
10.55
|
||||
]
|
||||
],
|
||||
"radius": 1.0
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"refused": {
|
||||
"step": "result",
|
||||
"error": "status 1001"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rounds_that_fit_are_closed",
|
||||
"note": "r 0.5, below half the wall: one closed manifold solid.",
|
||||
"steps": [
|
||||
{
|
||||
"op": "box",
|
||||
"id": "base",
|
||||
"origin": [
|
||||
9.5,
|
||||
8,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
14,
|
||||
21.5,
|
||||
10
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "box",
|
||||
"id": "top",
|
||||
"origin": [
|
||||
9.5,
|
||||
12.74437365538526,
|
||||
10
|
||||
],
|
||||
"size": [
|
||||
14,
|
||||
9.9483859782841,
|
||||
10
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "union",
|
||||
"id": "joined",
|
||||
"a": "base",
|
||||
"b": "top"
|
||||
},
|
||||
{
|
||||
"op": "box",
|
||||
"id": "window",
|
||||
"origin": [
|
||||
9.5,
|
||||
14,
|
||||
7
|
||||
],
|
||||
"size": [
|
||||
14,
|
||||
5.2,
|
||||
7.1
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "difference",
|
||||
"id": "part",
|
||||
"a": "joined",
|
||||
"b": "window"
|
||||
},
|
||||
{
|
||||
"op": "fillet",
|
||||
"id": "result",
|
||||
"body": "part",
|
||||
"edges": [
|
||||
[
|
||||
23.5,
|
||||
12.74437365538526,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
10.37218682769263,
|
||||
10.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
8.0,
|
||||
5.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
18.75,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
29.5,
|
||||
5.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
26.09637981683468,
|
||||
10.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
22.69275963366936,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
17.71856664452731,
|
||||
20.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
16.6,
|
||||
7.0
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
14.0,
|
||||
10.55
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
16.6,
|
||||
14.100000000000001
|
||||
],
|
||||
[
|
||||
23.5,
|
||||
19.200000000000003,
|
||||
10.55
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
12.74437365538526,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
10.37218682769263,
|
||||
10.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
8.0,
|
||||
5.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
18.75,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
29.5,
|
||||
5.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
26.09637981683468,
|
||||
10.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
22.69275963366936,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
17.71856664452731,
|
||||
20.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
16.6,
|
||||
7.0
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
14.0,
|
||||
10.55
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
16.6,
|
||||
14.100000000000001
|
||||
],
|
||||
[
|
||||
9.5,
|
||||
19.200000000000003,
|
||||
10.55
|
||||
]
|
||||
],
|
||||
"radius": 0.5
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"lumps": 1,
|
||||
"manifold": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user