mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 11:11:59 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f6fb78ea41 | |||
| 9a34227883 | |||
| ef49550c5e | |||
| a17e1d2c9b | |||
| c7ce50187d | |||
| 32528d4dcd | |||
| 9f4fa95e13 | |||
| cd2fe9920b | |||
| eda13321af | |||
| 794cfde212 | |||
| 0f8112b74d | |||
| 224e33033c | |||
| c2fe9216bd | |||
| 40e95b706d |
@@ -1093,8 +1093,11 @@ jobs:
|
||||
echo "Waiting for $pending run(s) of this commit to finish."
|
||||
sleep 30
|
||||
done
|
||||
# Runs with every job skipped (labels, before this job reported) are not a result.
|
||||
latest=$(jq -r '[.[] | select(.status == "completed" and .conclusion != "skipped")]
|
||||
# Only a run that passed or failed is a result: runs with every job skipped (labels, before
|
||||
# this job reported) are not, and neither are cancelled ones (a run replaced by a newer push
|
||||
# or label, or a fork run that waited for approval and was then cancelled, which has no
|
||||
# jobs). A cancelled run as "latest" failed this check for good and blocked a PR that passed.
|
||||
latest=$(jq -r '[.[] | select(.status == "completed" and (.conclusion == "success" or .conclusion == "failure"))]
|
||||
| sort_by(.created_at) | last | "\(.conclusion) \(.html_url)"' <<<"$runs")
|
||||
echo "The commit's latest run: $latest"
|
||||
echo "A label started this run: \`ci\` repeats the commit's latest result ($latest)." >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
@@ -35,12 +35,12 @@ fn inspect_schema(args: &[&str], env: Option<&str>) -> Result<Value, String> {
|
||||
fn compact_output_is_off_unless_asked() {
|
||||
let plain = inspect_schema(&[], None).unwrap();
|
||||
assert_eq!(plain["properties"]["sketches"]["type"], "boolean", "{plain}");
|
||||
assert!(plain["properties"].get("bodies").is_none(), "{plain}");
|
||||
assert_eq!(plain["properties"]["bodies"]["type"], json!(["boolean", "string"]), "no list of names: {plain}");
|
||||
assert_eq!(inspect_schema(&[], Some("0")).unwrap(), plain);
|
||||
|
||||
for (args, env) in [(&["--compact"][..], None), (&[][..], Some("1")), (&[][..], Some("true"))] {
|
||||
let compact = inspect_schema(args, env).unwrap();
|
||||
assert!(compact["properties"]["bodies"].is_object(), "{args:?} {env:?}: {compact}");
|
||||
assert!(compact["properties"]["bodies"]["type"].as_array().is_some_and(|t| t.contains(&json!("array"))), "{args:?} {env:?}: {compact}");
|
||||
}
|
||||
|
||||
let e = inspect_schema(&[], Some("maybe")).unwrap_err();
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"en": {
|
||||
"sketch.move_released": "Moved: {count} constraints tying it to what stays were let go ({names}).",
|
||||
"sketch.move_released_one": "Moved: {count} constraint tying it to what stays was let go ({names})."
|
||||
},
|
||||
"ar": {
|
||||
"sketch.move_released": "تم النقل: أُزيلت {count} قيود كانت تربطه بما بقي ({names}).",
|
||||
"sketch.move_released_one": "تم النقل: أُزيل {count} قيد كان يربطه بما بقي ({names})."
|
||||
},
|
||||
"de": {
|
||||
"sketch.move_released": "Verschoben: {count} Abhängigkeiten zum Rest wurden gelöst ({names}).",
|
||||
"sketch.move_released_one": "Verschoben: {count} Abhängigkeit zum Rest wurde gelöst ({names})."
|
||||
},
|
||||
"es": {
|
||||
"sketch.move_released": "Movido: se soltaron {count} restricciones que lo unían a lo que queda ({names}).",
|
||||
"sketch.move_released_one": "Movido: se soltó {count} restricción que lo unía a lo que queda ({names})."
|
||||
},
|
||||
"fr": {
|
||||
"sketch.move_released": "Déplacé : {count} contraintes qui le liaient au reste ont été relâchées ({names}).",
|
||||
"sketch.move_released_one": "Déplacé : {count} contrainte qui le liait au reste a été relâchée ({names})."
|
||||
},
|
||||
"he": {
|
||||
"sketch.move_released": "הוזז: {count} אילוצים שקשרו אותו למה שנשאר שוחררו ({names}).",
|
||||
"sketch.move_released_one": "הוזז: {count} אילוץ שקשר אותו למה שנשאר שוחרר ({names})."
|
||||
},
|
||||
"ja": {
|
||||
"sketch.move_released": "移動しました: 残る要素とつないでいた拘束 {count} 個を解除しました ({names})。",
|
||||
"sketch.move_released_one": "移動しました: 残る要素とつないでいた拘束 {count} 個を解除しました ({names})。"
|
||||
},
|
||||
"vi": {
|
||||
"sketch.move_released": "Đã di chuyển: đã bỏ {count} ràng buộc nối nó với phần còn lại ({names}).",
|
||||
"sketch.move_released_one": "Đã di chuyển: đã bỏ {count} ràng buộc nối nó với phần còn lại ({names})."
|
||||
},
|
||||
"zh": {
|
||||
"sketch.move_released": "已移动:已解除 {count} 个将其与保留部分相连的约束({names})。",
|
||||
"sketch.move_released_one": "已移动:已解除 {count} 个将其与保留部分相连的约束({names})。"
|
||||
}
|
||||
}
|
||||
@@ -1087,13 +1087,19 @@ pub enum FeatureKind {
|
||||
#[serde(default, skip_serializing_if = "RailFit::is_scale")]
|
||||
rail_fit: RailFit,
|
||||
},
|
||||
/// Sweep profiles along a chain of sketch curves.
|
||||
/// Sweep profiles along a chain of sketch curves, or along a chain of a body's edges.
|
||||
Sweep {
|
||||
sketch: SketchId,
|
||||
#[walk(profiles_of = sketch)]
|
||||
profiles: ProfileSel,
|
||||
path_sketch: SketchId,
|
||||
path: Vec<String>,
|
||||
/// The path on a body's edges, instead of the curves of `path_sketch` (#2216): the body
|
||||
/// and a point on each edge of the chain.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
path_body: Option<BodyName>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
path_edges: Vec<Position>,
|
||||
/// How the profile turns along the path (default: perpendicular to it).
|
||||
#[serde(default, skip_serializing_if = "SweepOrientation::is_perpendicular")]
|
||||
orientation: SweepOrientation,
|
||||
@@ -1815,6 +1821,12 @@ pub enum FeatureKind {
|
||||
curves: Vec<String>,
|
||||
path_sketch: SketchId,
|
||||
path: Vec<String>,
|
||||
/// The path on a body's edges, instead of the curves of `path_sketch` (#2216): the body
|
||||
/// and a point on each edge of the chain.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
path_body: Option<BodyName>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
path_edges: Vec<Position>,
|
||||
/// How the profile turns along the path (default: perpendicular to it).
|
||||
#[serde(default, skip_serializing_if = "SweepOrientation::is_perpendicular")]
|
||||
orientation: SweepOrientation,
|
||||
|
||||
+994
-133
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use solidcraft_kernel as kernel;
|
||||
|
||||
use super::{CachedEval, EvalCache, FeatureResult, KeptSlot, ModelBody, ModelState};
|
||||
use super::{CacheEntry, EvalCache, FeatureResult, KeptSlot, ModelBody, ModelState};
|
||||
|
||||
/// The evaluation's loop state just before a feature was evaluated: what a refusal of its check
|
||||
/// puts back before evaluating it again.
|
||||
@@ -61,7 +61,7 @@ pub(super) struct Checks {
|
||||
most: usize,
|
||||
running: VecDeque<Running>,
|
||||
/// Cache entries of features evaluated after a check that is still running, in order.
|
||||
held: Vec<(usize, u64, CachedEval)>,
|
||||
held: Vec<(usize, CacheEntry)>,
|
||||
/// Features whose check refused their bodies, to fail again as it did.
|
||||
refused: HashMap<usize, Failure>,
|
||||
}
|
||||
@@ -92,6 +92,11 @@ impl Checks {
|
||||
}
|
||||
|
||||
/// The failure feature `index` ends with, when its check refused it before.
|
||||
/// Whether feature `index`'s check refused its bodies, leaving the failure in place.
|
||||
pub fn refused_at(&self, index: usize) -> Option<&Failure> {
|
||||
self.refused.get(&index)
|
||||
}
|
||||
|
||||
pub fn refused(&mut self, index: usize) -> Option<Failure> {
|
||||
self.refused.remove(&index)
|
||||
}
|
||||
@@ -125,11 +130,11 @@ impl Checks {
|
||||
}
|
||||
|
||||
/// Stores the cache entry of feature `index` once no check before it, or of it, is running.
|
||||
pub fn store(&mut self, index: usize, key: u64, entry: CachedEval, cache: &Mutex<EvalCache>, kept: &KeptSlot) {
|
||||
pub fn store(&mut self, index: usize, entry: CacheEntry, cache: &Mutex<EvalCache>, kept: &KeptSlot) {
|
||||
if self.running.front().is_some_and(|r| r.index <= index) {
|
||||
self.held.push((index, key, entry));
|
||||
self.held.push((index, entry));
|
||||
} else {
|
||||
put(key, entry, cache, kept);
|
||||
entry.put(cache, kept);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,15 +155,15 @@ impl Checks {
|
||||
// What came after it was evaluated on bodies the check refuses: none of it stands.
|
||||
// The checks still running are left to end on their own.
|
||||
self.running.clear();
|
||||
self.held.retain(|(i, _, _)| *i < r.index);
|
||||
self.held.retain(|(i, _)| *i < r.index);
|
||||
return Some(Refused { index: r.index, saved: r.saved, failure: Failure { error, ..r.failure } });
|
||||
}
|
||||
let upto = self.running.front().map_or(usize::MAX, |next| next.index);
|
||||
for (i, key, entry) in std::mem::take(&mut self.held) {
|
||||
for (i, entry) in std::mem::take(&mut self.held) {
|
||||
if i < upto {
|
||||
put(key, entry, cache, kept);
|
||||
entry.put(cache, kept);
|
||||
} else {
|
||||
self.held.push((i, key, entry));
|
||||
self.held.push((i, entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,17 +176,9 @@ impl Checks {
|
||||
}
|
||||
}
|
||||
|
||||
fn put(key: u64, entry: CachedEval, cache: &Mutex<EvalCache>, kept: &KeptSlot) {
|
||||
if let Some(k) = &kept.0 {
|
||||
k.put(key, &entry);
|
||||
}
|
||||
if let Ok(mut m) = cache.lock() {
|
||||
m.put(key, entry);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::CachedEval;
|
||||
use super::*;
|
||||
use kernel::KernelError;
|
||||
|
||||
@@ -205,8 +202,9 @@ mod tests {
|
||||
Failure { error: KernelError::Failed(String::new()), warning: Some("kept".into()), import_report: None, repair: None, ms: 1.0 }
|
||||
}
|
||||
|
||||
fn entry() -> CachedEval {
|
||||
CachedEval { error: None, warning: None, import_report: None, repair: None, ms: 0.0, state: Arc::new(ModelState::default()), bytes: 1 }
|
||||
fn entry(key: u64) -> CacheEntry {
|
||||
let whole = CachedEval { error: None, warning: None, import_report: None, repair: None, ms: 0.0, state: Arc::new(ModelState::default()) };
|
||||
CacheEntry { key, whole, bytes: 1, change: None }
|
||||
}
|
||||
|
||||
fn checks() -> Checks {
|
||||
@@ -229,10 +227,10 @@ mod tests {
|
||||
let (cache, kept) = (Mutex::new(EvalCache::default()), KeptSlot(None));
|
||||
let mut c = checks();
|
||||
assert!(c.start(0, saved(), failure(), later(Ok(()))).is_ok());
|
||||
c.store(0, 10, entry(), &cache, &kept);
|
||||
c.store(0, entry(10), &cache, &kept);
|
||||
assert!(c.start(1, saved(), failure(), later(Err(KernelError::Failed("refused".into())))).is_ok());
|
||||
c.store(1, 11, entry(), &cache, &kept);
|
||||
c.store(2, 12, entry(), &cache, &kept);
|
||||
c.store(1, entry(11), &cache, &kept);
|
||||
c.store(2, entry(12), &cache, &kept);
|
||||
assert!(cache.lock().unwrap().is_empty(), "held while the checks run");
|
||||
let refused = c.settle(true, &cache, &kept).expect("the second check refuses");
|
||||
assert_eq!(refused.index, 1);
|
||||
@@ -240,7 +238,7 @@ mod tests {
|
||||
assert_eq!(refused.failure.warning.as_deref(), Some("kept"));
|
||||
{
|
||||
let m = cache.lock().unwrap();
|
||||
assert!(m.map.contains_key(&10) && !m.map.contains_key(&11) && !m.map.contains_key(&12));
|
||||
assert!(m.states.map.contains_key(&10) && !m.states.map.contains_key(&11) && !m.states.map.contains_key(&12));
|
||||
}
|
||||
c.refuse(1, refused.failure);
|
||||
assert!(c.refused(1).is_some() && c.refused(1).is_none());
|
||||
@@ -253,11 +251,11 @@ mod tests {
|
||||
fn passed_checks_store_what_waited() {
|
||||
let (cache, kept) = (Mutex::new(EvalCache::default()), KeptSlot(None));
|
||||
let mut c = checks();
|
||||
c.store(0, 10, entry(), &cache, &kept);
|
||||
c.store(0, entry(10), &cache, &kept);
|
||||
assert_eq!(cache.lock().unwrap().len(), 1);
|
||||
assert!(c.start(1, saved(), failure(), later(Ok(()))).is_ok());
|
||||
c.store(1, 11, entry(), &cache, &kept);
|
||||
c.store(2, 12, entry(), &cache, &kept);
|
||||
c.store(1, entry(11), &cache, &kept);
|
||||
c.store(2, entry(12), &cache, &kept);
|
||||
assert!(c.settle(true, &cache, &kept).is_none());
|
||||
assert_eq!(cache.lock().unwrap().len(), 3);
|
||||
}
|
||||
|
||||
@@ -379,8 +379,36 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceSweep { sketch, profiles, curves, path_sketch, path, orientation, twist, scale, rails, rail_fit } => {
|
||||
FeatureKind::SurfaceSweep {
|
||||
sketch,
|
||||
profiles,
|
||||
curves,
|
||||
path_sketch,
|
||||
path,
|
||||
path_body,
|
||||
path_edges,
|
||||
orientation,
|
||||
twist,
|
||||
scale,
|
||||
rails,
|
||||
rail_fit,
|
||||
} => {
|
||||
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?;
|
||||
// The path is a chain of a body's edges (#2216) or the curves of a sketch.
|
||||
if let Some(body) = path_body {
|
||||
let chains = sheet_chains(ss, profiles.as_ref(), curves)?;
|
||||
// The path runs on from the end nearest the middle of the curves.
|
||||
let pts: Vec<Vec2> = chains.iter().flatten().flat_map(|s| s.polyline(1e-2)).collect();
|
||||
let start = ss.plane.to_world(pts.iter().fold(Vec2::default(), |a, p| a + *p) * (1.0 / pts.len().max(1) as f64));
|
||||
let segs = crate::eval::edge_path_segments(st, body, path_edges, start, Some(&ss.plane))?;
|
||||
let path_start = segs.first().map(kernel::PathSeg::start).unwrap_or(start);
|
||||
let rails = crate::eval::rail_polylines(doc, st, rails, f.component, path_start)?;
|
||||
let ctl = crate::eval::sweep_control(vals, orientation, twist, scale, rails, *rail_fit)?;
|
||||
let body = kernel::sweep_sheet_with(&ss.plane, &chains, &segs, &ctl)?;
|
||||
let traced = crate::lineage::ChainRoles { sketch: ss, chains: &chains }.tool(body);
|
||||
push_sheet(st, f, traced);
|
||||
return Ok(());
|
||||
}
|
||||
let ps =
|
||||
st.sketch(path_sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
|
||||
// As Sweep: a 3D path sketch is followed in the feature's frame, the curves with it.
|
||||
|
||||
@@ -883,7 +883,7 @@ fn cut_with_flanks(
|
||||
kernel::boolean(&input.body, &sleeve, BoolOp::Union)?.ok_or_else(|| DocError::Invalid("narrowing the hole removed everything".into()))?;
|
||||
let unnamed = crate::lineage::Lineage::unnamed_faces(faces);
|
||||
let lineage = input.lineage.and_then(|l| crate::lineage::Lineage::step(&filled, &[&l, &unnamed]));
|
||||
input = Traced { body: filled, lineage };
|
||||
input = Traced { body: filled, lineage, recipe: None };
|
||||
groove_radius = r_min;
|
||||
}
|
||||
// The exact grooves: SMLib's screw sweeps, cut by one B-rep boolean.
|
||||
@@ -905,7 +905,7 @@ fn cut_with_flanks(
|
||||
kernel::boolean(&input.body, &cone, BoolOp::Cut)?.ok_or_else(|| DocError::Invalid("the chamfer removed everything".into()))?;
|
||||
let unnamed = crate::lineage::Lineage::unnamed_faces(faces);
|
||||
let lineage = input.lineage.and_then(|l| crate::lineage::Lineage::step(&cut, &[&l, &unnamed]));
|
||||
input = Traced { body: cut, lineage };
|
||||
input = Traced { body: cut, lineage, recipe: None };
|
||||
}
|
||||
}
|
||||
Ok(input)
|
||||
@@ -957,7 +957,7 @@ fn cut_with_flanks(
|
||||
// The groove's faces have no role to name them by.
|
||||
let groove = crate::lineage::Lineage::unnamed_faces(groove_faces);
|
||||
let lineage = input.lineage.and_then(|l| crate::lineage::Lineage::step(&nb, &[&l, &groove]));
|
||||
let nb = Traced { body: nb, lineage };
|
||||
let nb = Traced { body: nb, lineage, recipe: None };
|
||||
let nb = if continuous_flanks { nb } else { chamfer(nb)? };
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
//! A sweep saves the path it follows when the path is a chain of a body's edges: `path_body`
|
||||
//! (the body) and `path_edges` (a point on each edge of the chain), beside the curves of the
|
||||
//! path sketch a sweep saved before this change has. An older design has none and keeps its
|
||||
//! path of sketch curves, as it was built. The last release refuses a design that has it (it
|
||||
//! asks for an update) instead of opening it without the path.
|
||||
@@ -148,6 +148,9 @@ impl Lineage {
|
||||
pub(crate) struct Traced {
|
||||
pub body: Body,
|
||||
pub lineage: Option<Lineage>,
|
||||
/// How the feature built it, when that alone fixes it (a primitive from its numbers, a copy
|
||||
/// of one moved): what a cut by it is remembered by (see `ModelBody::cut`).
|
||||
pub recipe: Option<u64>,
|
||||
}
|
||||
|
||||
impl Traced {
|
||||
@@ -155,13 +158,13 @@ impl Traced {
|
||||
/// geometry, those of the bodies it is combined with by their history.
|
||||
pub fn unnamed(body: Body) -> Traced {
|
||||
let roles = vec![Vec::new(); body.face_count()];
|
||||
Traced { lineage: Some(Lineage::of(&body, Operand::Made(roles))), body }
|
||||
Traced { lineage: Some(Lineage::of(&body, Operand::Made(roles))), body, recipe: None }
|
||||
}
|
||||
|
||||
/// A body the feature built, its faces' roles given (one per face), else not known.
|
||||
pub fn made(body: Body, roles: Option<Vec<Vec<String>>>) -> Traced {
|
||||
match roles.filter(|r| r.len() == body.face_count()) {
|
||||
Some(roles) => Traced { lineage: Some(Lineage::of(&body, Operand::Made(roles))), body },
|
||||
Some(roles) => Traced { lineage: Some(Lineage::of(&body, Operand::Made(roles))), body, recipe: None },
|
||||
None => Traced::unnamed(body),
|
||||
}
|
||||
}
|
||||
@@ -170,14 +173,19 @@ impl Traced {
|
||||
/// which built it.
|
||||
pub fn copied(&self, copy: Body, feature: u64, instance: usize) -> Traced {
|
||||
let lineage = self.lineage.as_ref().and_then(|l| Lineage::step(©, &[&l.copied(feature, instance)]));
|
||||
Traced { body: copy, lineage }
|
||||
Traced { body: copy, lineage, recipe: None }
|
||||
}
|
||||
|
||||
/// This tool, built by `recipe`.
|
||||
pub fn with_recipe(self, recipe: Option<u64>) -> Traced {
|
||||
Traced { recipe, ..self }
|
||||
}
|
||||
|
||||
/// `result`, made by one kernel operation from `inputs` (in its operand order).
|
||||
pub fn step(result: Body, inputs: &[&Traced]) -> Traced {
|
||||
let lineages: Option<Vec<&Lineage>> = inputs.iter().map(|t| t.lineage.as_ref()).collect();
|
||||
let lineage = lineages.and_then(|l| Lineage::step(&result, &l));
|
||||
Traced { body: result, lineage }
|
||||
Traced { body: result, lineage, recipe: None }
|
||||
}
|
||||
|
||||
/// `result`, made by one kernel operation from this body alone. Where that operation's
|
||||
@@ -198,7 +206,7 @@ impl Traced {
|
||||
let stage = Arc::new(Operand::Stage(Box::new(Stage { body, lineage: l.clone(), names: OnceLock::new() })));
|
||||
Some(Lineage { history, operands: vec![stage] })
|
||||
});
|
||||
Traced { body: result, lineage }
|
||||
Traced { body: result, lineage, recipe: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,14 +579,14 @@ impl crate::ModelBody {
|
||||
Made::Traced(l) => Some(l.as_ref().clone()),
|
||||
Made::Unknown => None,
|
||||
};
|
||||
Traced { body: self.body.clone(), lineage }
|
||||
Traced { body: self.body.clone(), lineage, recipe: None }
|
||||
}
|
||||
|
||||
/// This body, as it was before the feature, as an operand named by `operand` (a moved copy of
|
||||
/// it, the cavity side of a shell grown from it); `None` once the feature has changed it.
|
||||
pub(crate) fn before_as(&self, operand: Operand) -> Traced {
|
||||
let lineage = matches!(self.made, Made::Before).then(|| Lineage::of(&self.body, operand));
|
||||
Traced { body: self.body.clone(), lineage }
|
||||
Traced { body: self.body.clone(), lineage, recipe: None }
|
||||
}
|
||||
|
||||
/// The lineage a feature left this body with, when it made or changed it with one.
|
||||
|
||||
@@ -944,8 +944,10 @@ impl Model {
|
||||
before = r.state.clone();
|
||||
continue;
|
||||
}
|
||||
let local = fingerprint(doc, 0, f, &vals, false, &model.import_digests) ^ placement_digest(doc, f, Some(&before));
|
||||
cache.put(
|
||||
let local = fingerprint(doc, 0, f, &vals, false, &model.import_digests)
|
||||
^ placement_digest(doc, f, Some(&before))
|
||||
^ construct_digest(doc, &vals, f, Some(&before));
|
||||
cache.states.put(
|
||||
cache_key(f, r.fingerprint, local, ctx, &before),
|
||||
CachedEval {
|
||||
error: None,
|
||||
@@ -953,9 +955,10 @@ impl Model {
|
||||
import_report: r.import_report.clone(),
|
||||
repair: r.repair.clone(),
|
||||
ms: r.ms,
|
||||
bytes: r.state.retained_bytes(),
|
||||
state: r.state.clone(),
|
||||
},
|
||||
r.state.retained_bytes(),
|
||||
r.ms,
|
||||
);
|
||||
before = r.state.clone();
|
||||
}
|
||||
|
||||
+397
-2
@@ -294,10 +294,11 @@ fn set_back_restores_everything_when_states_share_bodies() {
|
||||
m.evaluate(&doc);
|
||||
// Room for a few whole models: enough for both timelines' new bodies, not for every state
|
||||
// charged as a whole model.
|
||||
m.cache.lock().unwrap().budget = 3 * m.state().retained_bytes();
|
||||
m.cache.lock().unwrap().set_budget(3 * m.state().retained_bytes());
|
||||
doc.set_param("h", "12 mm", None, None).unwrap();
|
||||
m.evaluate(&doc);
|
||||
assert_eq!(m.last_recomputed, 30);
|
||||
// Only the box reading `h` is made again (#6183): the others' changes are reused.
|
||||
assert_eq!(m.last_recomputed, 1);
|
||||
doc.set_param("h", "10 mm", None, None).unwrap();
|
||||
m.evaluate(&doc);
|
||||
assert_eq!((m.last_recomputed, m.last_restored), (0, 30));
|
||||
@@ -447,6 +448,400 @@ fn rolled_results_never_mask_edits_or_disabled_reuse() {
|
||||
}
|
||||
}
|
||||
|
||||
fn add_in(doc: &mut Document, component: u64, name: &str, kind: serde_json::Value) {
|
||||
let kind: FeatureKind = serde_json::from_value(kind).unwrap();
|
||||
let id = doc.add_feature(kind, Some(name)).unwrap();
|
||||
doc.feature_mut(id).unwrap().component = component;
|
||||
}
|
||||
|
||||
/// A plate in one component, then a rod in another, slotted by a box cut patterned round it.
|
||||
fn two_parts(slot_targets: &[&str]) -> Document {
|
||||
let mut doc = Document::new("two parts");
|
||||
doc.set_param("w", "40 mm", None, None).unwrap();
|
||||
doc.set_param("slot", "1 mm", None, None).unwrap();
|
||||
let plate = doc.add_component(Some("Plate"), 0).unwrap();
|
||||
let rod = doc.add_component(Some("Rod"), 0).unwrap();
|
||||
add_in(
|
||||
&mut doc,
|
||||
plate,
|
||||
"Plate",
|
||||
serde_json::json!({"type": "box", "corner": ["0", "0", "0"], "length": "w", "width": "30", "height": "5", "operation": "new_body"}),
|
||||
);
|
||||
add_in(
|
||||
&mut doc,
|
||||
rod,
|
||||
"Rod",
|
||||
serde_json::json!({"type": "cylinder", "base": ["0", "0", "0"], "axis": [0.0, 0.0, 1.0], "radius": "4", "height": "20", "operation": "new_body"}),
|
||||
);
|
||||
add_in(
|
||||
&mut doc,
|
||||
rod,
|
||||
"Slot",
|
||||
serde_json::json!({"type": "box", "corner": ["3", "-0.5", "2"], "length": "2", "width": "slot", "height": "20", "operation": "cut"}),
|
||||
);
|
||||
for f in doc.features.iter_mut().filter(|f| f.name == "Plate" || f.name == "Rod") {
|
||||
f.body_names = vec![f.name.clone()];
|
||||
}
|
||||
if let Some(f) = doc.features.iter_mut().find(|f| f.name == "Slot") {
|
||||
f.participants = slot_targets.iter().map(|t| t.to_string()).collect();
|
||||
}
|
||||
let slot = doc.find_feature("Slot").map_or(0, |f| f.id);
|
||||
add_in(
|
||||
&mut doc,
|
||||
rod,
|
||||
"Slots",
|
||||
serde_json::json!({"type": "pattern", "features": [slot], "pattern": {"type": "circular", "origin": [0.0, 0.0, 0.0], "axis": [0.0, 0.0, 1.0], "count": "4", "angle": "360 deg"}}),
|
||||
);
|
||||
doc
|
||||
}
|
||||
|
||||
/// The bodies as an evaluation from scratch makes them: names, volumes, face names.
|
||||
fn summary(st: &ModelState) -> Vec<(String, u64, i64, Vec<String>)> {
|
||||
st.bodies
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let volume = measure(&b.body).unwrap().volume;
|
||||
(b.name.clone(), b.component, (volume * 1e6).round() as i64, crate::naming::face_names(b).to_vec())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn from_scratch(doc: &Document) -> Vec<(String, u64, i64, Vec<String>)> {
|
||||
let mut m = Model::new();
|
||||
m.no_cache = true;
|
||||
m.evaluate(doc);
|
||||
summary(&m.state())
|
||||
}
|
||||
|
||||
/// An edit in one component leaves another component's features after it as they were: they
|
||||
/// are restored, not recomputed, and give what evaluating from scratch gives.
|
||||
#[test]
|
||||
fn edit_in_one_component_keeps_the_others_results() {
|
||||
let mut doc = two_parts(&[]);
|
||||
let mut m = Model::new();
|
||||
m.evaluate(&doc);
|
||||
assert!(m.results.iter().all(|r| r.error.is_none()), "{:?}", m.results.iter().map(|r| &r.error).collect::<Vec<_>>());
|
||||
assert_eq!(m.last_recomputed, 4);
|
||||
doc.set_param("w", "50 mm", None, None).unwrap();
|
||||
m.evaluate(&doc);
|
||||
assert_eq!((m.last_recomputed, m.last_restored), (1, 3));
|
||||
assert_eq!(summary(&m.state()), from_scratch(&doc));
|
||||
// An edit in the rod's own component recomputes its features.
|
||||
doc.set_param("slot", "1.5 mm", None, None).unwrap();
|
||||
m.evaluate(&doc);
|
||||
assert_eq!((m.last_recomputed, m.last_restored), (2, 0));
|
||||
assert_eq!(summary(&m.state()), from_scratch(&doc));
|
||||
doc.set_param("w", "60 mm", None, None).unwrap();
|
||||
m.evaluate(&doc);
|
||||
assert_eq!((m.last_recomputed, m.last_restored), (1, 3));
|
||||
assert_eq!(summary(&m.state()), from_scratch(&doc));
|
||||
}
|
||||
|
||||
/// A sketch on a fixed plane, the new body extruded from it and that body moved read nothing
|
||||
/// of another component either.
|
||||
#[test]
|
||||
fn sketch_extrude_and_move_keep_their_results() {
|
||||
let mut doc = two_parts(&[]);
|
||||
let rod = doc.find_component("Rod").unwrap();
|
||||
let mut sk = Sketch::new();
|
||||
sk.add_circle(Vec2::new(20.0, 0.0), 2.0, None, None).unwrap();
|
||||
let id = doc.add_feature(FeatureKind::Sketch { plane: PlaneRef::Origin { name: "XY".into() }, sketch: sk }, Some("Peg sketch")).unwrap();
|
||||
doc.feature_mut(id).unwrap().component = rod;
|
||||
add_in(
|
||||
&mut doc,
|
||||
rod,
|
||||
"Peg",
|
||||
serde_json::json!({"type": "extrude", "sketch": id, "profiles": {"type": "all"}, "extent": {"convention": "total_distance", "direction": "positive", "distance": "8"}, "operation": "new_body"}),
|
||||
);
|
||||
if let Some(f) = doc.features.iter_mut().find(|f| f.name == "Peg") {
|
||||
f.body_names = vec!["Peg".into()];
|
||||
}
|
||||
add_in(&mut doc, rod, "Peg up", serde_json::json!({"type": "move", "bodies": ["Peg"], "translate": ["0", "0", "slot * 3"]}));
|
||||
let mut m = Model::new();
|
||||
m.evaluate(&doc);
|
||||
assert!(m.results.iter().all(|r| r.error.is_none()), "{:?}", m.results.iter().map(|r| &r.error).collect::<Vec<_>>());
|
||||
doc.set_param("w", "50 mm", None, None).unwrap();
|
||||
m.evaluate(&doc);
|
||||
assert_eq!((m.last_recomputed, m.last_restored), (1, 6));
|
||||
assert_eq!(summary(&m.state()), from_scratch(&doc));
|
||||
// The move follows its own parameter.
|
||||
doc.set_param("slot", "2 mm", None, None).unwrap();
|
||||
m.evaluate(&doc);
|
||||
assert_eq!((m.last_recomputed, m.last_restored), (3, 2));
|
||||
assert_eq!(summary(&m.state()), from_scratch(&doc));
|
||||
}
|
||||
|
||||
/// A feature that cuts a body of another component reads it: it is recomputed when that body
|
||||
/// changes, and so is what follows it in its own component.
|
||||
#[test]
|
||||
fn a_cut_of_another_components_body_follows_it() {
|
||||
let mut doc = two_parts(&["Plate", "Rod"]);
|
||||
let mut m = Model::new();
|
||||
m.evaluate(&doc);
|
||||
assert!(m.results.iter().all(|r| r.error.is_none()), "{:?}", m.results.iter().map(|r| &r.error).collect::<Vec<_>>());
|
||||
doc.set_param("w", "50 mm", None, None).unwrap();
|
||||
m.evaluate(&doc);
|
||||
assert_eq!((m.last_recomputed, m.last_restored), (3, 1));
|
||||
assert_eq!(summary(&m.state()), from_scratch(&doc));
|
||||
}
|
||||
|
||||
/// A pattern of a cut reads the bodies its source cuts, as the copies cut them: the source's
|
||||
/// participants, though the pattern names none and that body is in another component. The plate
|
||||
/// lies clear of every copy at first; moved into one by `w`, it must be cut again.
|
||||
#[test]
|
||||
fn a_pattern_reads_the_bodies_its_source_cuts() {
|
||||
let mut doc = two_parts(&["Plate"]);
|
||||
if let Some(f) = doc.features.iter_mut().find(|f| f.name == "Plate") {
|
||||
f.kind = serde_json::from_value(
|
||||
serde_json::json!({"type": "box", "corner": ["-20", "w", "0"], "length": "40", "width": "30", "height": "5", "operation": "new_body"}),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let mut m = Model::new();
|
||||
m.evaluate(&doc);
|
||||
assert!(m.results.iter().all(|r| r.error.is_none()), "{:?}", m.results.iter().map(|r| &r.error).collect::<Vec<_>>());
|
||||
assert_eq!(summary(&m.state()), from_scratch(&doc));
|
||||
// The copy turned a quarter round now reaches the plate.
|
||||
doc.set_param("w", "4 mm", None, None).unwrap();
|
||||
m.evaluate(&doc);
|
||||
assert_eq!(summary(&m.state()), from_scratch(&doc));
|
||||
}
|
||||
|
||||
/// A cut of several bodies cuts again only those that changed: the rod, which `w` does not
|
||||
/// move, keeps the very body its earlier cut gave, while the cap `w` sizes is cut again.
|
||||
#[test]
|
||||
fn a_cut_of_several_bodies_cuts_again_only_those_that_changed() {
|
||||
let mut doc = Document::new("rod and cap");
|
||||
doc.set_param("w", "6 mm", None, None).unwrap();
|
||||
let rod = doc.add_component(Some("Rod"), 0).unwrap();
|
||||
add_in(
|
||||
&mut doc,
|
||||
rod,
|
||||
"Rod",
|
||||
serde_json::json!({"type": "cylinder", "base": ["0", "0", "0"], "axis": [0.0, 0.0, 1.0], "radius": "4", "height": "20", "operation": "new_body"}),
|
||||
);
|
||||
add_in(
|
||||
&mut doc,
|
||||
rod,
|
||||
"Cap",
|
||||
serde_json::json!({"type": "box", "corner": ["-5", "-5", "20"], "length": "10", "width": "10", "height": "w", "operation": "new_body"}),
|
||||
);
|
||||
for f in doc.features.iter_mut().filter(|f| f.name == "Rod" || f.name == "Cap") {
|
||||
f.body_names = vec![f.name.clone()];
|
||||
}
|
||||
// The slot runs through both: the component's bodies are its targets.
|
||||
add_in(
|
||||
&mut doc,
|
||||
rod,
|
||||
"Slot",
|
||||
serde_json::json!({"type": "box", "corner": ["3", "-0.5", "2"], "length": "4", "width": "1", "height": "30", "operation": "cut"}),
|
||||
);
|
||||
let mut m = Model::new();
|
||||
m.evaluate(&doc);
|
||||
assert!(m.results.iter().all(|r| r.error.is_none()), "{:?}", m.results.iter().map(|r| &r.error).collect::<Vec<_>>());
|
||||
let cut_rod = m.state().body("Rod").unwrap().body.clone();
|
||||
doc.set_param("w", "8 mm", None, None).unwrap();
|
||||
m.evaluate(&doc);
|
||||
assert_eq!(m.last_recomputed, 2, "the cap and the slot");
|
||||
let st = m.state();
|
||||
assert!(st.body("Rod").unwrap().body.same(&cut_rod), "the rod's cut is the one made before");
|
||||
assert_eq!(summary(&st), from_scratch(&doc));
|
||||
}
|
||||
|
||||
/// A parameter that drives the first feature of the timeline, with the bodies after it each cut
|
||||
/// by a combine: only the first body and its cut are recomputed, every other cut is restored.
|
||||
#[test]
|
||||
fn a_combine_reads_only_its_target_and_tools() {
|
||||
let mut doc = Document::new("rows");
|
||||
doc.set_param("w", "20 mm", None, None).unwrap();
|
||||
let c = doc.add_component(Some("Rows"), 0).unwrap();
|
||||
for i in 0..3 {
|
||||
let x = (i * 30) as f64;
|
||||
let (body, tool, cut) = (format!("Body{i}"), format!("Tool{i}"), format!("Cut{i}"));
|
||||
let length = if i == 0 { "w".to_string() } else { "20".to_string() };
|
||||
add_in(
|
||||
&mut doc,
|
||||
c,
|
||||
&body,
|
||||
serde_json::json!({"type": "box", "corner": [x.to_string(), "0", "0"], "length": length, "width": "15", "height": "5", "operation": "new_body"}),
|
||||
);
|
||||
add_in(
|
||||
&mut doc,
|
||||
c,
|
||||
&tool,
|
||||
serde_json::json!({"type": "cylinder", "base": [(x + 7.0).to_string(), "7.5", "-1"], "axis": [0.0, 0.0, 1.0], "radius": "1.5", "height": "7", "operation": "new_body"}),
|
||||
);
|
||||
for f in doc.features.iter_mut().filter(|f| f.name == body || f.name == tool) {
|
||||
f.body_names = vec![f.name.clone()];
|
||||
}
|
||||
add_in(&mut doc, c, &cut, serde_json::json!({"type": "combine", "target": body, "tools": [tool], "operation": "cut"}));
|
||||
}
|
||||
let mut m = Model::new();
|
||||
m.evaluate(&doc);
|
||||
assert!(m.results.iter().all(|r| r.error.is_none()), "{:?}", m.results.iter().map(|r| &r.error).collect::<Vec<_>>());
|
||||
assert_eq!(m.last_recomputed, 9);
|
||||
doc.set_param("w", "32 mm", None, None).unwrap();
|
||||
m.evaluate(&doc);
|
||||
assert_eq!((m.last_recomputed, m.last_restored), (2, 7), "the first body and its cut");
|
||||
assert_eq!(summary(&m.state()), from_scratch(&doc));
|
||||
}
|
||||
|
||||
/// A circle sketch on `plane` in `component`, extruded by `distance` (`operation`), named
|
||||
/// after `name`.
|
||||
fn circle_extrude(doc: &mut Document, component: u64, name: &str, plane: PlaneRef, center: Vec2, r: f64, distance: &str, operation: &str) {
|
||||
let mut sk = Sketch::new();
|
||||
sk.add_circle(center, r, None, None).unwrap();
|
||||
let id = doc.add_feature(FeatureKind::Sketch { plane, sketch: sk }, Some(&format!("{name} sketch"))).unwrap();
|
||||
doc.feature_mut(id).unwrap().component = component;
|
||||
add_in(
|
||||
doc,
|
||||
component,
|
||||
name,
|
||||
serde_json::json!({"type": "extrude", "sketch": id, "profiles": {"type": "all"}, "extent": {"convention": "total_distance", "direction": "positive", "distance": distance}, "operation": operation}),
|
||||
);
|
||||
if let Some(f) = doc.features.iter_mut().find(|f| f.name == name).filter(|_| operation == "new_body") {
|
||||
f.body_names = vec![name.into()];
|
||||
}
|
||||
}
|
||||
|
||||
/// Three components on one timeline (#6183): A's post (height `A_h`) with the construction plane
|
||||
/// `A_top` above it when `plane`; B's post, cut by a hole from a sketch on a plane given by
|
||||
/// numbers and filleted; C's post, filleted twice (the last by `C_r`). Nothing in B or C reads A.
|
||||
fn three_parts(plane: bool) -> Document {
|
||||
let mut doc = Document::new("three parts");
|
||||
doc.set_param("A_h", "10 mm", None, None).unwrap();
|
||||
doc.set_param("C_r", "1 mm", None, None).unwrap();
|
||||
let [a, b, c] = ["A", "B", "C"].map(|n| doc.add_component(Some(n), 0).unwrap());
|
||||
let xy = || PlaneRef::Origin { name: "XY".into() };
|
||||
circle_extrude(&mut doc, a, "A post", xy(), Vec2::new(0.0, 0.0), 5.0, "A_h", "new_body");
|
||||
if plane {
|
||||
add_in(
|
||||
&mut doc,
|
||||
a,
|
||||
"A_top",
|
||||
serde_json::json!({"type": "construction_plane", "plane": {"type": "offset", "base": {"type": "origin", "name": "XY"}, "distance": "A_h + 5"}}),
|
||||
);
|
||||
}
|
||||
circle_extrude(&mut doc, b, "B post", xy(), Vec2::new(40.0, 0.0), 8.0, "12", "new_body");
|
||||
let top = PlaneRef::Offset { base: Box::new(xy()), distance: "12".into() };
|
||||
circle_extrude(&mut doc, b, "B hole", top, Vec2::new(40.0, 0.0), 3.0, "-4", "cut");
|
||||
add_in(&mut doc, b, "B round", serde_json::json!({"type": "fillet", "edges": [[48.0, 0.0, 0.0]], "radius": "1"}));
|
||||
circle_extrude(&mut doc, c, "C post", xy(), Vec2::new(80.0, 0.0), 6.0, "15", "new_body");
|
||||
add_in(&mut doc, c, "C round", serde_json::json!({"type": "fillet", "edges": [[86.0, 0.0, 15.0]], "radius": "1.5"}));
|
||||
add_in(&mut doc, c, "C foot", serde_json::json!({"type": "fillet", "edges": [[86.0, 0.0, 0.0]], "radius": "C_r"}));
|
||||
doc
|
||||
}
|
||||
|
||||
fn no_errors(m: &Model) {
|
||||
assert!(m.results.iter().all(|r| r.error.is_none()), "{:?}", m.results.iter().map(|r| (&r.name, &r.error)).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
/// An edit in component A recomputes A's features that use it and nothing in B or C (#6183):
|
||||
/// sketches, cuts on a plane given by numbers and fillets of their own body are kept, and the
|
||||
/// bodies match an evaluation from scratch.
|
||||
#[test]
|
||||
fn an_edit_in_one_component_recomputes_only_what_reads_it() {
|
||||
for plane in [false, true] {
|
||||
let mut doc = three_parts(plane);
|
||||
let mut m = Model::new();
|
||||
m.evaluate(&doc);
|
||||
no_errors(&m);
|
||||
for h in ["11 mm", "12 mm", "13 mm"] {
|
||||
doc.set_param("A_h", h, None, None).unwrap();
|
||||
m.evaluate(&doc);
|
||||
no_errors(&m);
|
||||
// The extrude, and the plane offset by `A_h`.
|
||||
assert_eq!(m.last_recomputed, 1 + usize::from(plane), "A_h = {h}, plane: {plane}");
|
||||
assert_eq!(summary(&m.state()), from_scratch(&doc));
|
||||
}
|
||||
// The last fillet recomputes alone, as before.
|
||||
doc.set_param("C_r", "2 mm", None, None).unwrap();
|
||||
m.evaluate(&doc);
|
||||
assert_eq!(m.last_recomputed, 1);
|
||||
assert_eq!(summary(&m.state()), from_scratch(&doc));
|
||||
// Back where it was: every result comes from the cache.
|
||||
doc.set_param("A_h", "10 mm", None, None).unwrap();
|
||||
doc.set_param("C_r", "1 mm", None, None).unwrap();
|
||||
m.evaluate(&doc);
|
||||
assert_eq!(summary(&m.state()), from_scratch(&doc));
|
||||
}
|
||||
}
|
||||
|
||||
/// A construction plane's edit recomputes only the features that use it: a sketch on it and what
|
||||
/// is made from that sketch, wherever they are in the timeline, and nothing else.
|
||||
#[test]
|
||||
fn a_construction_plane_edit_recomputes_only_its_users() {
|
||||
let mut doc = three_parts(true);
|
||||
let b = doc.find_component("B").unwrap();
|
||||
// A peg in B on A's plane: B now reads A through it.
|
||||
let a_top = doc.find_feature("A_top").unwrap().id;
|
||||
circle_extrude(&mut doc, b, "B peg", PlaneRef::Construction { feature: a_top.into() }, Vec2::new(40.0, 20.0), 2.0, "3", "new_body");
|
||||
let mut m = Model::new();
|
||||
m.evaluate(&doc);
|
||||
no_errors(&m);
|
||||
let total = m.results.len();
|
||||
doc.set_param("A_h", "11 mm", None, None).unwrap();
|
||||
m.evaluate(&doc);
|
||||
no_errors(&m);
|
||||
assert_eq!(m.last_recomputed, 4, "A's extrude and plane, the peg's sketch and extrude, of {total}");
|
||||
assert_eq!(summary(&m.state()), from_scratch(&doc));
|
||||
let peg = m.state().body("B peg").map(|b| b.body.outline_bounds().min.z);
|
||||
assert!(peg.is_some_and(|z| (z - 16.0).abs() < 1e-6), "the peg stands on the plane: {peg:?}");
|
||||
// Results restored whole after the plane carry the plane as it is now.
|
||||
let st = m.state();
|
||||
let top = st.construct.iter().find(|c| c.name == "A_top").map(|c| &c.geom);
|
||||
assert!(matches!(top, Some(crate::construct::ConstructGeom::Plane(p)) if (p.origin.z - 16.0).abs() < 1e-9), "{top:?}");
|
||||
let mut scratch = Model::new();
|
||||
scratch.no_cache = true;
|
||||
scratch.evaluate(&doc);
|
||||
assert_eq!(st.construct, scratch.state().construct);
|
||||
}
|
||||
|
||||
/// A coil sweeping a sketch's profile is made again when that sketch changes, not restored as
|
||||
/// it was (a coil of its own section reads no sketch).
|
||||
#[test]
|
||||
fn a_coil_follows_its_sketch() {
|
||||
let section = |r: f64| {
|
||||
let mut sk = Sketch::new();
|
||||
sk.add_circle(Vec2::new(20.0, 0.0), r, None, None).unwrap();
|
||||
FeatureKind::Sketch { plane: PlaneRef::Origin { name: "XZ".into() }, sketch: sk }
|
||||
};
|
||||
let mut doc = Document::new("coil");
|
||||
let sketch = doc.add_feature(section(1.0), None).unwrap();
|
||||
doc.add_feature(
|
||||
FeatureKind::Coil {
|
||||
base: point_expr(Vec3::new(0.0, 0.0, 0.0)),
|
||||
axis: Dir(Vec3::Z),
|
||||
sketch: Some(crate::SketchId(sketch)),
|
||||
profiles: ProfileSel::All,
|
||||
diameter: None,
|
||||
pitch: "8".into(),
|
||||
turns: "2".into(),
|
||||
section_size: None,
|
||||
section: Default::default(),
|
||||
position: 0,
|
||||
start_angle: None,
|
||||
clockwise: false,
|
||||
operation: Operation::NewBody,
|
||||
targets: Vec::new(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let mut m = Model::new();
|
||||
m.evaluate(&doc);
|
||||
no_errors(&m);
|
||||
let before = summary(&m.state());
|
||||
if let Some(f) = doc.feature_mut(sketch) {
|
||||
f.kind = section(2.0);
|
||||
}
|
||||
m.evaluate(&doc);
|
||||
no_errors(&m);
|
||||
assert_eq!(m.last_recomputed, 2, "the sketch and its coil");
|
||||
assert_eq!(summary(&m.state()), from_scratch(&doc));
|
||||
assert_ne!(summary(&m.state()), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn box_fillet_cut_timeline() {
|
||||
let (mut doc, _) = plate_doc();
|
||||
|
||||
@@ -64,7 +64,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.icon("sweep")
|
||||
.contract(&SWEEP_CONTRACT)
|
||||
.output(output_schema_for::<FeatureResult>)
|
||||
.params("sketch: profile sketch; profiles?; path_sketch: sketch; path: [curve ids in order]; orientation?: perpendicular (default: square to the path, turning with it) | parallel (as drawn: the profile moves without turning; the path must keep crossing its plane) | [x,y,z] (square to the path, its turn about the path fixed by this direction); twist?: angle expr (the profile turns this much about the path over its length; within 4 turns); scale?: expr (the profile's scale at the end, linear along the path; 0.01…100); rails?: [{sketch?: sketch (default path_sketch), curves: [curve ids in order]}] (guide rails, one or two, each meeting every plane square to the path once along it, from the end nearest the path's start: one rail turns the profile so its point where the rail starts stays on the rail and scales it with the rail's distance from the path; two rails carry it between them, its points where they start staying on both; rails take the default orientation, no twist and no scale, and a path without sharp corners); rail_fit?: scale (default: the profile grows and shrinks evenly) | stretch (only across to the rail, between the rails); operation?, touching?, targets?, name?, body_name?"),
|
||||
.params("sketch: profile sketch; profiles?; path_sketch: sketch; path: [curve ids in order]; path_body?: body name; path_edges?: [points on its edges] (the path is a chain of that body's edges, instead of a sketch's curves); orientation?: perpendicular (default: square to the path, turning with it) | parallel (as drawn: the profile moves without turning; the path must keep crossing its plane) | [x,y,z] (square to the path, its turn about the path fixed by this direction); twist?: angle expr (the profile turns this much about the path over its length; within 4 turns); scale?: expr (the profile's scale at the end, linear along the path; 0.01…100); rails?: [{sketch?: sketch (default path_sketch), curves: [curve ids in order]}] (guide rails, one or two, each meeting every plane square to the path once along it, from the end nearest the path's start: one rail turns the profile so its point where the rail starts stays on the rail and scales it with the rail's distance from the path; two rails carry it between them, its points where they start staying on both; rails take the default orientation, no twist and no scale, and a path without sharp corners); rail_fit?: scale (default: the profile grows and shrinks evenly) | stretch (only across to the rail, between the rails); operation?, touching?, targets?, name?, body_name?"),
|
||||
CommandSpec::new("solid.loft", "Loft", loft).at("SOLID", "CREATE").icon("loft").params("sections: [{sketch, profiles?} | {sketch, point: id} (an apex)] (2…50, in order; matching hole counts, no holes at an apex); rails?: [{sketch, curves: [ids in order]}] (one or two guide rails from the first section to the last, through every section's outline: the loft follows them between the sections; not with sections that have holes); rail_fit?: scale|stretch (default scale: with one rail the sections grow evenly with the rail's distance from their middle, with two with the distance between the rails; stretch: only that way); operation?, touching?, targets? (Join excludes hidden bodies from automatic targets; explicit targets or participants may include them), name?, body_name?"),
|
||||
CommandSpec::new("solid.box", "Box", crate::checked!("solid.box", prim_box))
|
||||
.at("SOLID", "CREATE")
|
||||
@@ -3635,13 +3635,9 @@ pub(crate) fn sketch_key(v: Option<&Value>) -> Option<String> {
|
||||
fn sweep(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "solid.sweep";
|
||||
let sketch = feature_sketch(s, p, cmd)?;
|
||||
let path_sketch = sketch_id(s, p.get("path_sketch"), cmd, "path_sketch")?;
|
||||
let path = string_list(cmd, p, "path")?;
|
||||
if path.is_empty() {
|
||||
return Err(bad(cmd, "`path` must list curve ids"));
|
||||
}
|
||||
let (path_sketch, path, path_body, path_edges) = sweep_path(s, p, cmd)?;
|
||||
let (orientation, twist, scale) = sweep_options(s, p, cmd)?;
|
||||
let (rails, rail_fit) = sweep_rails(s, p, cmd, path_sketch)?;
|
||||
let (rails, rail_fit) = sweep_rails(s, p, cmd, path_body.is_none().then_some(path_sketch))?;
|
||||
let operation = operation(p, cmd)?;
|
||||
let targets = face_join_targets(s, p, operation, &[sketch], cmd)?;
|
||||
add_feature(
|
||||
@@ -3652,6 +3648,8 @@ fn sweep(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
profiles: profiles(p, cmd)?,
|
||||
path_sketch: SketchId(path_sketch),
|
||||
path,
|
||||
path_body,
|
||||
path_edges,
|
||||
orientation,
|
||||
twist,
|
||||
scale,
|
||||
@@ -3663,6 +3661,35 @@ fn sweep(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
)
|
||||
}
|
||||
|
||||
/// A sweep's path (#2216): a chain of a body's edges (`path_body` and `path_edges`) or the
|
||||
/// curves of a sketch (`path_sketch` and `path`). The body's edges are a point on each edge of
|
||||
/// the chain; the path runs from the end nearest the profile.
|
||||
pub(super) fn sweep_path(s: &Session, p: &Value, cmd: &str) -> Result<(u64, Vec<String>, Option<BodyName>, Vec<Position>)> {
|
||||
if p.get("path_body").is_some_and(|b| !b.is_null()) {
|
||||
let body = str_(p, "path_body").ok_or_else(|| bad(cmd, "`path_body` must be a body name"))?;
|
||||
if s.model.state().body(body).is_none() {
|
||||
return Err(bad(cmd, format!("no body `{body}`")));
|
||||
}
|
||||
let edges = path_edge_points(p, cmd)?;
|
||||
return Ok((0, Vec::new(), Some(BodyName::from(body)), edges));
|
||||
}
|
||||
let path_sketch = sketch_id(s, p.get("path_sketch"), cmd, "path_sketch")?;
|
||||
let path = string_list(cmd, p, "path")?;
|
||||
if path.is_empty() {
|
||||
return Err(bad(cmd, "`path` must list curve ids"));
|
||||
}
|
||||
Ok((path_sketch, path, None, Vec::new()))
|
||||
}
|
||||
|
||||
/// The points on the edges of a sweep's path (`path_edges`), one on each edge of the chain.
|
||||
fn path_edge_points(p: &Value, cmd: &str) -> Result<Vec<Position>> {
|
||||
let a = p.get("path_edges").and_then(Value::as_array).ok_or_else(|| bad(cmd, "`path_edges` must be a list of points"))?;
|
||||
if a.is_empty() || a.len() > 1000 {
|
||||
return Err(bad(cmd, "the path needs 1…1000 edges"));
|
||||
}
|
||||
a.iter().map(|e| vec3(e).map(Position::from).ok_or_else(|| bad(cmd, "each `path_edges` entry must be a point [x,y,z]"))).collect()
|
||||
}
|
||||
|
||||
/// A sweep's `orientation`, `twist` and `scale` (solid.sweep and surface.sweep), checked: an
|
||||
/// orientation the sweep knows, a twist within four turns, an end scale within 0.01…100.
|
||||
pub(super) fn sweep_options(s: &Session, p: &Value, cmd: &str) -> Result<(SweepOrientation, Option<Expr>, Option<Expr>)> {
|
||||
@@ -3715,10 +3742,11 @@ pub(crate) fn sweep_orientation(v: &Value) -> std::result::Result<SweepOrientati
|
||||
}
|
||||
|
||||
/// A sweep's guide `rails` and `rail_fit` (solid.sweep and surface.sweep), checked: one or two
|
||||
/// rails, each a sketch (default: the path's) and 1…1000 of its curves; a fit of scale or
|
||||
/// stretch; and no orientation, twist or scale besides, since the rails set those.
|
||||
pub(super) fn sweep_rails(s: &Session, p: &Value, cmd: &str, path_sketch: u64) -> Result<(Vec<SweepRail>, RailFit)> {
|
||||
let (rails, rail_fit) = guide_rails(s, p, cmd, "sweep", Some(path_sketch))?;
|
||||
/// rails, each a sketch (default: the path's, when the path is a sketch's) and 1…1000 of its
|
||||
/// curves; a fit of scale or stretch; and no orientation, twist or scale besides, since the
|
||||
/// rails set those.
|
||||
pub(super) fn sweep_rails(s: &Session, p: &Value, cmd: &str, path_sketch: Option<u64>) -> Result<(Vec<SweepRail>, RailFit)> {
|
||||
let (rails, rail_fit) = guide_rails(s, p, cmd, "sweep", path_sketch)?;
|
||||
if !rails.is_empty() {
|
||||
for key in ["orientation", "twist", "scale"] {
|
||||
if p.get(key).is_some_and(|v| !v.is_null()) {
|
||||
|
||||
@@ -2784,3 +2784,31 @@ fn align_by_a_frame_picked_on_another_copy_moves_the_part_in_its_own_coordinates
|
||||
assert_bounds(&mut s, "Peg", [4., 4., 10.], [6., 6., 12.]);
|
||||
assert_bounds(&mut s, "Peg (Board:2/Header:1)", [104., 4., 10.], [106., 6., 12.]);
|
||||
}
|
||||
|
||||
/// A coil cut from a sketch follows that sketch when its points move, and the body it cuts when
|
||||
/// that changes upstream: its cached result is not taken back while either differs.
|
||||
#[test]
|
||||
fn a_sketch_coil_cut_follows_its_profile_and_target() {
|
||||
use std::f64::consts::PI;
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.cylinder", json!({"radius": 20, "height": 40, "body_name": "Shaft", "name": "ShaftFeature"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "YZ", "name": "Groove"}));
|
||||
// A 1 x 1 mm square, 18..19 mm from the axis.
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [18, 1], "p1": [19, 2]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let before = volume(&mut s);
|
||||
run(&mut s, "solid.coil", json!({"sketch": "Groove", "revolutions": 3, "pitch": 8, "operation": "cut", "targets": ["Shaft"]}));
|
||||
let removed = before - volume(&mut s);
|
||||
assert!(rel(removed, 2.0 * PI * 3.0 * 18.5) < 1e-3, "{removed}");
|
||||
// Widen the square inward to 17..19 mm: twice the area, centroid at 18 mm.
|
||||
run(&mut s, "sketch.edit", json!({"sketch": "Groove"}));
|
||||
run(&mut s, "sketch.move_point", json!({"point": "p1", "to": [17, 1]}));
|
||||
run(&mut s, "sketch.move_point", json!({"point": "p4", "to": [17, 2]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let removed = before - volume(&mut s);
|
||||
assert!(rel(removed, 2.0 * PI * 3.0 * 2.0 * 18.0) < 1e-3, "the groove kept the old profile: {removed}");
|
||||
// A thinner shaft upstream (radius 18): the groove now cuts only 17..18 mm of it.
|
||||
run(&mut s, "timeline.edit", json!({"feature": "ShaftFeature", "set": {"radius": "18"}}));
|
||||
let removed = PI * 18.0 * 18.0 * 40.0 - volume(&mut s);
|
||||
assert!(rel(removed, 2.0 * PI * 3.0 * 17.5) < 1e-3, "the groove kept the old shaft: {removed}");
|
||||
}
|
||||
|
||||
@@ -11,13 +11,14 @@ use solidcraft_sketch::CurveKind;
|
||||
|
||||
use super::CommandSpec;
|
||||
use crate::params::{bad, bool_, str_, string_list};
|
||||
use crate::schema::inspect::{BodiesDetail, DocumentBodies, TimelineDetail, TimelineRow};
|
||||
use crate::schema::inspect::{
|
||||
BodyFailure, BodyFeaturesParams, BodyFigures, BodyListParams, BodyMeasurement, BodyName, BodyNames, BodyOutcome, BoundingBox, CommandEntry,
|
||||
CommandsParams, CurveShape, DocumentBody, DocumentBodyDetail, DocumentInspectParams, DocumentInspectResult, DocumentParam, DocumentSketch,
|
||||
FaceCylinder, KernelCounts, ListedMaterial, MaterialAssignParams, MaterialAssignResult, MaterialListParams, MaterialListResult, MeasureParams,
|
||||
MeasureResult, MeasureTotal, Measured, MeasuredBodies, MeasuredBody, ModelBodyFeatures, ModelEdge, ModelEdges, ModelFace, ModelFaces,
|
||||
ModelThread, ModelThreads, ModelThreadsParams, SketchConstraint, SketchCurve, SketchInspectParams, SketchInspected, SketchPoint, SketchProfile,
|
||||
SketchWire, TimelineEntry, WireArcInfo,
|
||||
SketchWire, TimelineEntry, TimelineSummary, WireArcInfo,
|
||||
};
|
||||
use crate::schema::{TypedContract, output_schema_for};
|
||||
use crate::{EngineError, Result, Session};
|
||||
@@ -58,7 +59,11 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.noundo()
|
||||
.contract(&DOCUMENT_INSPECT_CONTRACT)
|
||||
.output(output_schema_for::<DocumentInspectResult>)
|
||||
.params("measure?: bool (include body measurements); sketch_counts?: bool (each sketch also gives its plane and its number of points and constraints)"),
|
||||
.params(
|
||||
"measure?: bool (include body measurements); sketch_counts?: bool (each sketch also gives its plane and its number of points and constraints); \
|
||||
timeline?: true (default) | \"summary\" (each feature as id, name, type, with error, warning, suppressed and rolled_back only when set) | false (left out); \
|
||||
bodies?: true (default) | \"names\" (only the body names, nothing measured) | false (left out)",
|
||||
),
|
||||
CommandSpec::new("document.revision", "Design Revision", revision).icon("inspect").noundo().params(
|
||||
"→ document_id (this open design's id: kept while it stays open, another for each design started or opened), \
|
||||
revision (moves whenever the design, what is hidden or a component's uncaptured position changes, \
|
||||
@@ -544,6 +549,8 @@ fn revision(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn inspect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let timeline_detail = TimelineDetail::read(p.get("timeline")).map_err(|e| bad("document.inspect", e))?;
|
||||
let bodies_detail = BodiesDetail::read(p.get("bodies")).map_err(|e| bad("document.inspect", e))?;
|
||||
let st = s.model.state();
|
||||
let (vals, perr) = s.doc.param_values();
|
||||
let params: Vec<DocumentParam> = s
|
||||
@@ -561,27 +568,41 @@ fn inspect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})
|
||||
.collect();
|
||||
let marker = s.doc.marker.unwrap_or(s.doc.features.len());
|
||||
let timeline: Vec<TimelineEntry> = s
|
||||
.doc
|
||||
.features
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| {
|
||||
let r = s.model.result(f.id);
|
||||
TimelineEntry {
|
||||
id: f.id,
|
||||
name: f.name.clone(),
|
||||
kind: f.kind.type_name().to_string(),
|
||||
suppressed: f.suppressed,
|
||||
rolled_back: i >= marker,
|
||||
error: r.and_then(|r| r.error.clone()),
|
||||
warning: r.and_then(|r| r.warning.clone()),
|
||||
ms: r.map(|r| r.ms),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let timeline: Option<Vec<TimelineRow>> = (timeline_detail != TimelineDetail::Left).then(|| {
|
||||
s.doc
|
||||
.features
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| {
|
||||
let r = s.model.result(f.id);
|
||||
let (error, warning) = (r.and_then(|r| r.error.clone()), r.and_then(|r| r.warning.clone()));
|
||||
if timeline_detail == TimelineDetail::Summary {
|
||||
return TimelineRow::Summary(TimelineSummary {
|
||||
id: f.id,
|
||||
name: f.name.clone(),
|
||||
kind: f.kind.type_name().to_string(),
|
||||
error,
|
||||
warning,
|
||||
suppressed: f.suppressed.then_some(true),
|
||||
rolled_back: (i >= marker).then_some(true),
|
||||
});
|
||||
}
|
||||
TimelineRow::Full(TimelineEntry {
|
||||
id: f.id,
|
||||
name: f.name.clone(),
|
||||
kind: f.kind.type_name().to_string(),
|
||||
suppressed: f.suppressed,
|
||||
rolled_back: i >= marker,
|
||||
error,
|
||||
warning,
|
||||
ms: r.map(|r| r.ms),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
let definitions = s.model.state();
|
||||
let with_measure = bool_(p, "measure").unwrap_or(false);
|
||||
// Only bodies given in full are measured.
|
||||
let with_measure = bool_(p, "measure").unwrap_or(false) && bodies_detail == BodiesDetail::Full;
|
||||
let with_counts = bool_(p, "sketch_counts").unwrap_or(false);
|
||||
// Each body is measured on its own: on all cores, in body order (as `inspect.measure`).
|
||||
let mut measured =
|
||||
@@ -601,6 +622,11 @@ fn inspect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
DocumentBody { detail, visible: s.body_visible(&b.name), removed_later: s.body_removed_later(&b.name).then_some(true) }
|
||||
})
|
||||
.collect();
|
||||
let bodies = match bodies_detail {
|
||||
BodiesDetail::Full => Some(DocumentBodies::Full(bodies)),
|
||||
BodiesDetail::Names => Some(DocumentBodies::Names(st.bodies.iter().map(|b| b.name.clone()).collect())),
|
||||
BodiesDetail::Left => None,
|
||||
};
|
||||
let sketches: Vec<DocumentSketch> = st
|
||||
.sketches
|
||||
.iter()
|
||||
|
||||
@@ -141,7 +141,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.at("SKETCH", "MODIFY")
|
||||
.icon("sketch_move")
|
||||
.enabled(in_sketch)
|
||||
.params(concat!("entities: [ids], translate?: [dx,dy], angle?: deg, center?: [x,y], copy?: bool, rotate_constraints?: bool (default true; false keeps constraints unchanged); a rotation turns the constraints tied to the sketch axes when all their geometry moves (horizontal and vertical relations and dimensions swap at odd quarter turns and stay at half turns, line angles turn with the line) and removes the horizontal and vertical ones that cannot hold at other angles; picking a text outline or linked point transforms its whole text source once, and copies remain editable text; copy also accepts drawn 3D curves, preserving exact arcs and height above the sketch plane", created_doc!())),
|
||||
.params(concat!("entities: [ids], translate?: [dx,dy], angle?: deg, center?: [x,y], copy?: bool, rotate_constraints?: bool (default true; false keeps constraints unchanged); a rotation turns the constraints tied to the sketch axes when all their geometry moves (horizontal and vertical relations and dimensions swap at odd quarter turns and stay at half turns, line angles turn with the line) and removes the horizontal and vertical ones that cannot hold at other angles; picking a text outline or linked point transforms its whole text source once, and copies remain editable text; copy also accepts drawn 3D curves, preserving exact arcs and height above the sketch plane; returns `released`: the constraints tying the moved geometry to what stays that the move let go, as {id, kind}", created_doc!())),
|
||||
CommandSpec::new("sketch.mirror", "Mirror", crate::checked!("sketch.mirror", mirror))
|
||||
.contract(&MIRROR_CONTRACT)
|
||||
.output(output_schema_for::<MirrorResult>)
|
||||
@@ -3792,6 +3792,7 @@ fn move_copy(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let rotate_constraints = bool_(p, "rotate_constraints").unwrap_or(true);
|
||||
let selection = string_list(cmd, p, "entities")?;
|
||||
let (planar, drawn) = if copy { drawn_copy_selection(s, p, selection)? } else { (selection, Vec::new()) };
|
||||
let mut released = Vec::new();
|
||||
let (out, info) = edit(s, p, cmd, false, |sk, doc| {
|
||||
let (mut cs, mut ps) =
|
||||
if planar.is_empty() && !drawn.is_empty() { (Vec::new(), Vec::new()) } else { entities(sk, &json!({"entities": planar}), cmd)? };
|
||||
@@ -3823,16 +3824,18 @@ fn move_copy(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Ok(ids);
|
||||
}
|
||||
with_ellipse_axes(sk, &mut cs);
|
||||
let held = holding(sk, &involved_points(sk, &cs, &ps));
|
||||
transform_in_place(sk, &cs, &ps, &f, 1.0, cmd)?;
|
||||
if rotate_constraints {
|
||||
let moved = involved_points(sk, &cs, &ps);
|
||||
rotate_axis_constraints(sk, doc, &moved, ang)?;
|
||||
}
|
||||
released = release_broken(sk, &held);
|
||||
cs.extend(text_curves);
|
||||
Ok(ids_of(sk, &cs))
|
||||
})?;
|
||||
s.doc_mut().prune_model_params();
|
||||
reply(cmd, MoveResult { curves: out, sketch: info, param_warnings: Vec::new() })
|
||||
reply(cmd, MoveResult { curves: out, released, sketch: info, param_warnings: Vec::new() })
|
||||
}
|
||||
|
||||
/// Scale, turn and move the entities in one step: scaled about `center`, then turned about it,
|
||||
@@ -3919,6 +3922,55 @@ fn scale_dimensions(sk: &mut Sketch, doc: &mut solidcraft_doc::Document, curves:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The constraints that tie the points about to move to points that stay (a corner on the
|
||||
/// origin, a distance to a line left behind) and hold now: their ids and type names. Constraints
|
||||
/// among the moved points alone (a rectangle's sides, a line's typed angle) move with them and
|
||||
/// are kept.
|
||||
fn holding(sk: &Sketch, moved: &[usize]) -> Vec<(String, &'static str)> {
|
||||
sk.constraints
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| {
|
||||
let touched = sk.constraint_points(&c.kind);
|
||||
touched.iter().any(|q| moved.contains(q)) && touched.iter().any(|q| !moved.contains(q))
|
||||
})
|
||||
.filter(|(ci, c)| solidcraft_sketch::constraint_residual(sk, *ci).is_some_and(|r| r <= broken_at(sk, &c.kind)))
|
||||
.map(|(_, c)| (c.id.clone(), c.kind.name()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// After a move: the constraints of `held` that no longer hold are let go. Kept, the solve
|
||||
/// would pull the moved geometry back to what stays and bend it (#2980). Returns every
|
||||
/// constraint of `held` the move let go, these and the origin holds the move itself dropped
|
||||
/// (#5034), as `{id, kind}` for the status line to name them.
|
||||
fn release_broken(sk: &mut Sketch, held: &[(String, &'static str)]) -> Vec<Value> {
|
||||
let broken: Vec<String> = sk
|
||||
.constraints
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| held.iter().any(|(id, _)| *id == c.id))
|
||||
.filter(|(ci, c)| solidcraft_sketch::constraint_residual(sk, *ci).is_some_and(|r| r > broken_at(sk, &c.kind)))
|
||||
.map(|(_, c)| c.id.clone())
|
||||
.collect();
|
||||
sk.constraints.retain(|c| !broken.contains(&c.id));
|
||||
held.iter().filter(|(id, _)| !sk.constraints.iter().any(|c| c.id == *id)).map(|(id, kind)| json!({"id": id, "kind": kind})).collect()
|
||||
}
|
||||
|
||||
/// A residual above this is a constraint that doesn't hold. A distance is judged against the size
|
||||
/// of the geometry the constraint itself touches, so a far-away point elsewhere in the sketch can't
|
||||
/// hide a broken one; the sine or cosine between two lines has no length to scale by.
|
||||
fn broken_at(sk: &Sketch, kind: &ConstraintKind) -> f64 {
|
||||
let direction_only = match *kind {
|
||||
ConstraintKind::Parallel { .. } | ConstraintKind::Perpendicular { .. } => true,
|
||||
ConstraintKind::Horizontal { l } | ConstraintKind::Vertical { l } => sk.curves.get(l).is_some_and(|c| c.infinite),
|
||||
_ => false,
|
||||
};
|
||||
if direction_only {
|
||||
return 1e-7;
|
||||
}
|
||||
1e-7 * sk.constraint_points(kind).iter().filter_map(|&q| sk.points.get(q)).map(|p| p.pos.len()).fold(1.0, f64::max)
|
||||
}
|
||||
|
||||
fn scale(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.scale";
|
||||
let k = num(p, "factor").filter(|k| *k > 1e-9 && *k < 1e6).ok_or_else(|| bad(cmd, "`factor` must be a positive number"))?;
|
||||
|
||||
@@ -5740,6 +5740,47 @@ fn split_failures_name_the_public_command() {
|
||||
assert!(!text.contains("sketch.modify"), "{text}");
|
||||
}
|
||||
|
||||
/// #2980: a rectangle drawn from the origin and a circle, all moved: they move as drawn. The
|
||||
/// corner's coincidence with the origin is let go instead of pulling that corner back and
|
||||
/// bending the rectangle; its own horizontals and verticals stay.
|
||||
#[test]
|
||||
fn moving_shapes_held_to_the_origin_moves_them_whole() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [20, -30]}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [-5, 20], "radius": 12}));
|
||||
let sk = sketch(&s);
|
||||
let at_origin = sk.constraints.iter().find(|c| matches!(c.kind, ConstraintKind::Coincident { .. })).map(|c| c.id.clone());
|
||||
assert!(at_origin.is_some(), "the rectangle's corner is held on the origin");
|
||||
let before: Vec<(String, Vec2)> = sk.points.iter().filter(|p| p.id != "origin").map(|p| (p.id.clone(), p.pos)).collect();
|
||||
let ids: Vec<String> = sk.curves.iter().map(|c| c.id.clone()).collect();
|
||||
let r = run(&mut s, "sketch.move", json!({"entities": ids, "translate": [15.5, 0]}));
|
||||
assert_eq!(r["released"], json!([{"id": at_origin.clone().unwrap(), "kind": "Coincident"}]), "{r}");
|
||||
let after = sketch(&s);
|
||||
for (id, p) in &before {
|
||||
let q = after.points.iter().find(|q| &q.id == id).map(|q| q.pos).unwrap();
|
||||
assert!(q.dist(*p + Vec2::new(15.5, 0.0)) < 1e-9, "{id} went from {p:?} to {q:?}");
|
||||
}
|
||||
assert_eq!(after.constraints.len(), sk.constraints.len() - 1, "only the origin's coincidence is let go");
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(sketch(&s).constraints.len(), sk.constraints.len(), "undo brings it back");
|
||||
}
|
||||
|
||||
/// A point far from the move says nothing about how exactly the moved line has to stay level with
|
||||
/// the line that stays: a short move still breaks that relation, so it is still let go.
|
||||
#[test]
|
||||
fn a_far_away_point_does_not_keep_a_constraint_the_move_broke() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
||||
let stays = ids(&run(&mut s, "sketch.line", json!({"points": [[5, 5], [15, 5]], "connect": false}))["curves"])[0].clone();
|
||||
let moved = ids(&run(&mut s, "sketch.line", json!({"points": [[30, 5], [30, 20]], "connect": false}))["curves"])[0].clone();
|
||||
run(&mut s, "sketch.constraint.horizontal_vertical", json!({"points": [format!("{moved}.start"), format!("{stays}.end")], "mode": "horizontal"}));
|
||||
let level = sketch(&s).constraints.iter().find(|c| matches!(c.kind, ConstraintKind::HorizontalPoints { .. })).map(|c| c.id.clone()).unwrap();
|
||||
run(&mut s, "sketch.point", json!({"point": [1e8, 0], "id": "far"}));
|
||||
let r = run(&mut s, "sketch.move", json!({"entities": [moved], "translate": [0, 1]}));
|
||||
assert_eq!(r["released"], json!([{"id": level, "kind": "Horizontal"}]), "{r}");
|
||||
}
|
||||
|
||||
/// #12294: a spline offset together with a line is trimmed and unlinked, so a distance that names a
|
||||
/// parameter drives nothing. The reply says so; a linked offset of the same parameter does not.
|
||||
#[test]
|
||||
|
||||
@@ -2504,7 +2504,9 @@ fn editing_a_mid_timeline_sketch_rebuilds_later_features_once_at_finish() {
|
||||
run(&mut s, "sketch.edit", json!({"sketch": id, "restore_marker": false}));
|
||||
assert_eq!(s.doc.marker, None);
|
||||
run(&mut s, "sketch.line", json!({"points": [[0, 20], [10, 20]]}));
|
||||
assert!(s.model.last_recomputed > 1, "in place, the later features are rebuilt");
|
||||
// Rebuilt, or made again from what they changed when what they read is as it was (#6183).
|
||||
let evaluated = s.model.last_recomputed + s.model.last_restored;
|
||||
assert!(evaluated > 1, "in place, the later features are evaluated: {evaluated}");
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
assert_eq!(s.doc.marker, None);
|
||||
assert!(s.execute("sketch.edit", &json!({"sketch": id, "restore_marker": true})).is_err());
|
||||
|
||||
@@ -83,7 +83,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.icon("surface_sweep")
|
||||
.contract(&SURFACE_SWEEP_CONTRACT)
|
||||
.output(output_schema_for::<FeatureResult>)
|
||||
.params("sketch?: id|name; curves?: [curve ids] and/or profiles? (as surface.extrude); path_sketch: sketch; path: [curve ids in order] (a path with corners, or 3D curves, gives ruled walls mitred at the corners); orientation?, twist?, scale?, rails?, rail_fit? (as solid.sweep); name?, body_name? (a surface body, no caps)"),
|
||||
.params("sketch?: id|name; curves?: [curve ids] and/or profiles? (as surface.extrude); path_sketch: sketch; path: [curve ids in order] (a path with corners, or 3D curves, gives ruled walls mitred at the corners); path_body?: body name; path_edges?: [points on its edges] (the path is a chain of that body's edges, instead of a sketch's curves); orientation?, twist?, scale?, rails?, rail_fit? (as solid.sweep); name?, body_name? (a surface body, no caps)"),
|
||||
CommandSpec::new("surface.loft", "Surface Loft", crate::checked!("surface.loft", surface_loft))
|
||||
.at("SURFACE", "CREATE")
|
||||
.icon("surface_loft")
|
||||
@@ -424,13 +424,12 @@ fn surface_sweep(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "surface.sweep";
|
||||
let sketch = feature_sketch(s, p, cmd)?;
|
||||
let (profiles, curves) = sheet_inputs(p, cmd)?;
|
||||
let path_sketch = sketch_id(s, p.get("path_sketch"), cmd, "path_sketch")?;
|
||||
let path = string_list(cmd, p, "path")?;
|
||||
if path.is_empty() || path.len() > 1000 {
|
||||
let (path_sketch, path, path_body, path_edges) = super::features::sweep_path(s, p, cmd)?;
|
||||
if path_body.is_none() && (path.is_empty() || path.len() > 1000) {
|
||||
return Err(bad(cmd, "`path` must list 1…1000 curve ids"));
|
||||
}
|
||||
let (orientation, twist, scale) = super::features::sweep_options(s, p, cmd)?;
|
||||
let (rails, rail_fit) = super::features::sweep_rails(s, p, cmd, path_sketch)?;
|
||||
let (rails, rail_fit) = super::features::sweep_rails(s, p, cmd, path_body.is_none().then_some(path_sketch))?;
|
||||
add_feature(
|
||||
s,
|
||||
p,
|
||||
@@ -440,6 +439,8 @@ fn surface_sweep(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
curves,
|
||||
path_sketch: SketchId(path_sketch),
|
||||
path,
|
||||
path_body,
|
||||
path_edges,
|
||||
orientation,
|
||||
twist,
|
||||
scale,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use std::f64::consts::PI;
|
||||
use std::f64::consts::{FRAC_1_SQRT_2, PI};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -193,6 +193,66 @@ fn a_sweep_round_a_closed_path_starts_where_the_profile_is() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #2216: a sweep whose path is a closed loop of a body's edges is followed from the profile,
|
||||
/// like a closed sketch path: the tube stays on the loop, within the profile's half width of
|
||||
/// it, instead of standing off it by the distance between the profile and the vertex the
|
||||
/// first picked edge happened to be on. A body's top face is a closed loop of eight edges; a
|
||||
/// 4 x 3 profile at the middle of one of them, square to it, swept round the whole loop.
|
||||
#[test]
|
||||
fn a_sweep_round_a_closed_body_edge_path_starts_where_the_profile_is() {
|
||||
let mut s = Session::default();
|
||||
// A rounded rectangle 60 x 40, corners r8, extruded 10: its top face is a closed loop of
|
||||
// eight edges, smooth all round.
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Loop"}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[-22, -20], [22, -20]], "infer": false, "ids": ["l1"]}));
|
||||
run(&mut s, "sketch.arc.center_point", json!({"center": [22, -12], "start": [22, -20], "end": [30, -12], "id": "a1"}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[30, -12], [30, 12]], "infer": false, "ids": ["l2"]}));
|
||||
run(&mut s, "sketch.arc.center_point", json!({"center": [22, 12], "start": [30, 12], "end": [22, 20], "id": "a2"}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[22, 20], [-22, 20]], "infer": false, "ids": ["l3"]}));
|
||||
run(&mut s, "sketch.arc.center_point", json!({"center": [-22, 12], "start": [-22, 20], "end": [-30, 12], "id": "a3"}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[-30, 12], [-30, -12]], "infer": false, "ids": ["l4"]}));
|
||||
run(&mut s, "sketch.arc.center_point", json!({"center": [-22, -12], "start": [-30, -12], "end": [-22, -20], "id": "a4"}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Loop", "distance": 10, "operation": "new", "body_name": "Slab"}));
|
||||
// A 4 x 3 profile at the middle of the loop's bottom edge, square to it.
|
||||
run(&mut s, "sketch.create", json!({"plane": {"origin": [0, -20, 10], "x_dir": [0, 1, 0], "y_dir": [0, 0, 1]}, "name": "Prof"}));
|
||||
run(&mut s, "sketch.rectangle.center", json!({"center": [0, 0], "corner": [2, 1.5]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
// The path: the loop's eight edges, one point on each, in order round it.
|
||||
let c = 8.0 * FRAC_1_SQRT_2;
|
||||
run(
|
||||
&mut s,
|
||||
"solid.sweep",
|
||||
json!({
|
||||
"sketch": "Prof",
|
||||
"path_body": "Slab",
|
||||
"path_edges": [
|
||||
[-11.0, -20.0, 10.0],
|
||||
[22.0 + c, -12.0 - c, 10.0],
|
||||
[30.0, 0.0, 10.0],
|
||||
[22.0 + c, 12.0 + c, 10.0],
|
||||
[-11.0, 20.0, 10.0],
|
||||
[-22.0 - c, 12.0 + c, 10.0],
|
||||
[-30.0, 0.0, 10.0],
|
||||
[-22.0 - c, -12.0 - c, 10.0]
|
||||
],
|
||||
"operation": "new",
|
||||
"body_name": "Tube"
|
||||
}),
|
||||
);
|
||||
let m = measure(&s, "Tube");
|
||||
// Pappus: the profile's area (4 x 3) times the loop's length.
|
||||
let want = 12.0 * (2.0 * 44.0 + 2.0 * 24.0 + 4.0 * (PI / 2.0) * 8.0);
|
||||
assert!((m.volume - want).abs() < want * 1e-3, "{} vs {want}", m.volume);
|
||||
// The tube stays on the loop: within the profile's half width (2) of it, the loop's own
|
||||
// bounds (-30..30 by -20..20) grown by it.
|
||||
for (got, want) in
|
||||
[(m.bbox.min.x, -32.0), (m.bbox.max.x, 32.0), (m.bbox.min.y, -22.0), (m.bbox.max.y, 22.0), (m.bbox.min.z, 8.5), (m.bbox.max.z, 11.5)]
|
||||
{
|
||||
assert!((got - want).abs() < 0.08, "{:?}", m.bbox);
|
||||
}
|
||||
}
|
||||
|
||||
/// #3604: a path whose first curve is a spline of several exact pieces, the next curve meeting
|
||||
/// it at its start, follows the spline back from its end: a curve is turned round whole, not
|
||||
/// piece by piece. A 2 x 2 profile at the spline's end (20, 0), swept along the spline through
|
||||
|
||||
@@ -453,10 +453,21 @@ pub struct SweepParams {
|
||||
/// whole drawn profile. The dialogs send `true`: they highlight the regions they pick.
|
||||
#[serde(default)]
|
||||
pub exact_regions: Option<bool>,
|
||||
/// The sketch holding the path, by id or name.
|
||||
pub path_sketch: SketchRef,
|
||||
/// The path's curve ids, in order along the path.
|
||||
pub path: StringList,
|
||||
/// The sketch holding the path, by id or name. Not needed when the path is a body's edges.
|
||||
#[serde(default, deserialize_with = "present", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "SketchRef")]
|
||||
pub path_sketch: Option<SketchRef>,
|
||||
/// The path's curve ids, in order along the path. Not needed when the path is a body's edges.
|
||||
#[serde(default, deserialize_with = "present", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "StringList")]
|
||||
pub path: Option<StringList>,
|
||||
/// The name of the body whose edges are the path, instead of a sketch's curves.
|
||||
#[serde(default, deserialize_with = "path_body_opt", skip_serializing_if = "Option::is_none")]
|
||||
pub path_body: Option<String>,
|
||||
/// The points on that body's edges that are the path, 1 to 1000.
|
||||
#[serde(default, deserialize_with = "present", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "PathEdgePoints")]
|
||||
pub path_edges: Option<PathEdgePoints>,
|
||||
#[serde(flatten, deserialize_with = "feature_common")]
|
||||
pub shape: SweepShape,
|
||||
/// How the sweep combines with existing bodies. Default: new.
|
||||
@@ -499,10 +510,22 @@ pub struct SurfaceSweepParams {
|
||||
/// whole drawn profile. The dialogs send `true`: they highlight the regions they pick.
|
||||
#[serde(default)]
|
||||
pub exact_regions: Option<bool>,
|
||||
/// The sketch holding the path, by id or name.
|
||||
pub path_sketch: SketchRef,
|
||||
/// The path's curve ids, in order along the path: 1 to 1000.
|
||||
pub path: StringList,
|
||||
/// The sketch holding the path, by id or name. Not needed when the path is a body's edges.
|
||||
#[serde(default, deserialize_with = "present", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "SketchRef")]
|
||||
pub path_sketch: Option<SketchRef>,
|
||||
/// The path's curve ids, in order along the path: 1 to 1000. Not needed when the path is a
|
||||
/// body's edges.
|
||||
#[serde(default, deserialize_with = "present", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "StringList")]
|
||||
pub path: Option<StringList>,
|
||||
/// The name of the body whose edges are the path, instead of a sketch's curves.
|
||||
#[serde(default, deserialize_with = "path_body_opt", skip_serializing_if = "Option::is_none")]
|
||||
pub path_body: Option<String>,
|
||||
/// The points on that body's edges that are the path, 1 to 1000.
|
||||
#[serde(default, deserialize_with = "present", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "PathEdgePoints")]
|
||||
pub path_edges: Option<PathEdgePoints>,
|
||||
#[serde(flatten, deserialize_with = "feature_common")]
|
||||
pub shape: SweepShape,
|
||||
#[serde(flatten, deserialize_with = "feature_common")]
|
||||
@@ -620,6 +643,53 @@ fn target_body<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `path_body`: the name of the body whose edges are the path, as the handler's `sweep_path`
|
||||
/// reads it: a body name, or `null` (which the handler reads as absent). Anything else is
|
||||
/// refused with the handler's message.
|
||||
fn path_body_opt<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
|
||||
match Value::deserialize(d)? {
|
||||
Value::Null => Ok(None),
|
||||
Value::String(name) => Ok(Some(name)),
|
||||
_ => Err(D::Error::custom(whole("`path_body` must be a body name"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// `path_edges`: the points on the edges of the path, as the handler's `path_edge_points` reads
|
||||
/// them: 1 to 1000 points, each `[x, y, z]` or `{x, y, z}` of finite numbers.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PathEdgePoints(pub Vec<Point3>);
|
||||
|
||||
impl<'de> Deserialize<'de> for PathEdgePoints {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
let v = Value::deserialize(d)?;
|
||||
let items = v.as_array().ok_or_else(|| D::Error::custom(whole("`path_edges` must be a list of points")))?;
|
||||
if items.is_empty() || items.len() > 1000 {
|
||||
return Err(D::Error::custom(whole("the path needs 1…1000 edges")));
|
||||
}
|
||||
items
|
||||
.iter()
|
||||
.map(|item| Point3::deserialize(item).map_err(|_| D::Error::custom(whole("each `path_edges` entry must be a point [x,y,z]"))))
|
||||
.collect::<Result<_, _>>()
|
||||
.map(PathEdgePoints)
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for PathEdgePoints {
|
||||
fn schema_name() -> std::borrow::Cow<'static, str> {
|
||||
"PathEdgePoints".into()
|
||||
}
|
||||
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
|
||||
let item = generator.subschema_for::<Point3>();
|
||||
json_schema!({
|
||||
"description": "The points on the body's edges that are the path, one on each edge of the chain: 1 to 1000.",
|
||||
"type": "array",
|
||||
"items": item,
|
||||
"minItems": 1,
|
||||
"maxItems": 1000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The handler's message for no tools.
|
||||
const NO_TOOLS: &str = "`tools` must list bodies";
|
||||
|
||||
|
||||
@@ -383,6 +383,8 @@ fn sweep_schemas_and_the_key_check_agree() {
|
||||
"operation",
|
||||
"participants",
|
||||
"path",
|
||||
"path_body",
|
||||
"path_edges",
|
||||
"path_sketch",
|
||||
"profiles",
|
||||
"sketch",
|
||||
@@ -392,12 +394,79 @@ fn sweep_schemas_and_the_key_check_agree() {
|
||||
solid.extend(shape);
|
||||
solid.sort_unstable();
|
||||
assert_keys(input_schema_for::<SweepParams>(), &solid);
|
||||
let mut surface = vec!["body_name", "body_names", "curves", "exact_regions", "name", "participants", "path", "path_sketch", "profiles", "sketch"];
|
||||
let mut surface = vec![
|
||||
"body_name",
|
||||
"body_names",
|
||||
"curves",
|
||||
"exact_regions",
|
||||
"name",
|
||||
"participants",
|
||||
"path",
|
||||
"path_body",
|
||||
"path_edges",
|
||||
"path_sketch",
|
||||
"profiles",
|
||||
"sketch",
|
||||
];
|
||||
surface.extend(shape);
|
||||
surface.sort_unstable();
|
||||
assert_keys(input_schema_for::<SurfaceSweepParams>(), &surface);
|
||||
}
|
||||
|
||||
/// #2216: a sweep's path may be a chain of a body's edges (`path_body` and `path_edges`), so
|
||||
/// the contracts take those keys: a call with them is not refused, and a key that is not one
|
||||
/// of the command's still is.
|
||||
#[test]
|
||||
fn sweep_path_on_a_body_is_accepted_and_unknown_keys_are_not() {
|
||||
let p = json!({"path_body": "Body1", "path_edges": [[0, 0, 0], [10, 0, 0]]});
|
||||
let solid = crate::schema::TypedContract::<SweepParams>::new();
|
||||
let surface = crate::schema::TypedContract::<SurfaceSweepParams>::new();
|
||||
assert!(crate::schema::Contract::check(&solid, &p).is_empty(), "{p}");
|
||||
assert!(crate::schema::Contract::check(&surface, &p).is_empty(), "{p}");
|
||||
// A key that is not one of the command's is still refused.
|
||||
let typo = json!({"path_body": "Body1", "path_edges": [[0, 0, 0]], "path_boddy": "Body1"});
|
||||
for issues in [crate::schema::Contract::check(&solid, &typo), crate::schema::Contract::check(&surface, &typo)] {
|
||||
assert_eq!(issues.len(), 1, "{typo}");
|
||||
assert_eq!(issues[0].path, "path_boddy", "{typo}");
|
||||
assert_eq!(issues[0].message, "unknown key `path_boddy`; did you mean `path_body`?", "{typo}");
|
||||
}
|
||||
// `path_body` is a body name, or `null` (which the handler reads as absent).
|
||||
for p in [json!({"path_body": null}), json!({"path_body": "Body1", "path_edges": [[0, 0, 0]]})] {
|
||||
assert!(crate::schema::Contract::check(&solid, &p).is_empty(), "{p}");
|
||||
assert!(crate::schema::Contract::check(&surface, &p).is_empty(), "{p}");
|
||||
}
|
||||
for p in [json!({"path_body": 5}), json!({"path_body": true}), json!({"path_body": []})] {
|
||||
for issues in [crate::schema::Contract::check(&solid, &p), crate::schema::Contract::check(&surface, &p)] {
|
||||
assert_eq!(issues.len(), 1, "{p}");
|
||||
assert_eq!(issues[0].path, "path_body", "{p}");
|
||||
assert_eq!(issues[0].message, "`path_body` must be a body name", "{p}");
|
||||
}
|
||||
}
|
||||
// `path_edges` is a list of 1 to 1000 points, as the handler's `path_edge_points` reads them.
|
||||
for (p, message) in [
|
||||
(json!({"path_edges": 5}), "`path_edges` must be a list of points"),
|
||||
(json!({"path_edges": []}), "the path needs 1…1000 edges"),
|
||||
(json!({"path_edges": [[0, 0, 0], [1]]}), "each `path_edges` entry must be a point [x,y,z]"),
|
||||
] {
|
||||
for issues in [crate::schema::Contract::check(&solid, &p), crate::schema::Contract::check(&surface, &p)] {
|
||||
assert_eq!(issues.len(), 1, "{p}");
|
||||
assert_eq!(issues[0].path, "path_edges", "{p}");
|
||||
assert_eq!(issues[0].message, message, "{p}");
|
||||
}
|
||||
}
|
||||
// The schemas the commands publish list both keys.
|
||||
for schema in [input_schema_for::<SweepParams>(), input_schema_for::<SurfaceSweepParams>()] {
|
||||
let schema = schema.as_value();
|
||||
assert_eq!(schema["properties"]["path_body"]["type"], json!(["string", "null"]), "{schema}");
|
||||
// `path_edges` is published as a `$ref` to its own definition, as `path` is.
|
||||
let name = schema["properties"]["path_edges"]["$ref"].as_str().unwrap_or_default().strip_prefix("#/$defs/").unwrap_or_default().to_string();
|
||||
let edges = &schema["$defs"][name.clone()];
|
||||
assert_eq!(edges["type"], "array", "{schema}");
|
||||
assert_eq!(edges["minItems"], 1, "{schema}");
|
||||
assert_eq!(edges["maxItems"], 1000, "{schema}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The calls both sweeps refuse today, as `sweep` and `surface_sweep` read them, each run on
|
||||
/// `sketch_rectangle`'s sketch (`Sketch1`, which is the path sketch too: the path's curves are
|
||||
/// only looked up when the feature is evaluated).
|
||||
|
||||
@@ -342,6 +342,98 @@ pub struct DocumentInspectParams {
|
||||
/// Give each sketch also its plane and its number of points and constraints. Default: false.
|
||||
#[serde(default)]
|
||||
pub sketch_counts: Option<bool>,
|
||||
/// How much of the timeline to give: true (the default) each feature with its state and
|
||||
/// timing; `"summary"` each as its id, name and type, with `error`, `warning`, `suppressed`
|
||||
/// and `rolled_back` only when it has them (a reply a large design keeps small); false leaves
|
||||
/// `timeline` out.
|
||||
#[serde(default)]
|
||||
pub timeline: Option<TimelineDetail>,
|
||||
/// How much of each body to give: true (the default) as `measure` says; `"names"` only their
|
||||
/// names, nothing measured; false leaves `bodies` out.
|
||||
#[serde(default)]
|
||||
pub bodies: Option<BodiesDetail>,
|
||||
}
|
||||
|
||||
/// `document.inspect`'s `timeline`: the whole timeline, a summary of it, or none.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TimelineDetail {
|
||||
Full,
|
||||
Summary,
|
||||
Left,
|
||||
}
|
||||
|
||||
const TIMELINE_FORMS: &str = "`timeline` must be true, false or \"summary\"";
|
||||
|
||||
impl TimelineDetail {
|
||||
/// `timeline` as the handler reads it: absent or `null` is the whole timeline.
|
||||
pub fn read(v: Option<&Value>) -> Result<Self, &'static str> {
|
||||
match v {
|
||||
None | Some(Value::Null) | Some(Value::Bool(true)) => Ok(TimelineDetail::Full),
|
||||
Some(Value::Bool(false)) => Ok(TimelineDetail::Left),
|
||||
Some(Value::String(s)) if s == "summary" => Ok(TimelineDetail::Summary),
|
||||
Some(_) => Err(TIMELINE_FORMS),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TimelineDetail {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
let v = Value::deserialize(d)?;
|
||||
TimelineDetail::read(Some(&v)).map_err(|e| D::Error::custom(whole(e)))
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for TimelineDetail {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"TimelineDetail".into()
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({
|
||||
"description": "true: every feature with its state and timing; \"summary\": id, name and type, with error, warning, suppressed and rolled_back only when set; false: no timeline.",
|
||||
"anyOf": [{"type": "boolean"}, {"type": "string", "enum": ["summary"]}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// `document.inspect`'s `bodies`: each body in full, only the names, or none.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum BodiesDetail {
|
||||
Full,
|
||||
Names,
|
||||
Left,
|
||||
}
|
||||
|
||||
const BODIES_FORMS: &str = "`bodies` must be true, false or \"names\"";
|
||||
|
||||
impl BodiesDetail {
|
||||
/// `bodies` as the handler reads it: absent or `null` is every body in full.
|
||||
pub fn read(v: Option<&Value>) -> Result<Self, &'static str> {
|
||||
match v {
|
||||
None | Some(Value::Null) | Some(Value::Bool(true)) => Ok(BodiesDetail::Full),
|
||||
Some(Value::Bool(false)) => Ok(BodiesDetail::Left),
|
||||
Some(Value::String(s)) if s == "names" => Ok(BodiesDetail::Names),
|
||||
Some(_) => Err(BODIES_FORMS),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for BodiesDetail {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
let v = Value::deserialize(d)?;
|
||||
BodiesDetail::read(Some(&v)).map_err(|e| D::Error::custom(whole(e)))
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for BodiesDetail {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"BodiesDetail".into()
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({
|
||||
"description": "true: each body as `measure` says; \"names\": only the body names, nothing measured; false: no bodies.",
|
||||
"anyOf": [{"type": "boolean"}, {"type": "string", "enum": ["names"]}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// What `document.inspect` returns.
|
||||
@@ -365,10 +457,13 @@ pub struct DocumentInspectResult {
|
||||
pub marker: usize,
|
||||
/// The design's parameters.
|
||||
pub params: Vec<DocumentParam>,
|
||||
/// Its features, in timeline order.
|
||||
pub timeline: Vec<TimelineEntry>,
|
||||
/// Its bodies.
|
||||
pub bodies: Vec<DocumentBody>,
|
||||
/// Its features, in timeline order (in short with `timeline: "summary"`); left out with
|
||||
/// `timeline: false`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timeline: Option<Vec<TimelineRow>>,
|
||||
/// Its bodies (only their names with `bodies: "names"`); left out with `bodies: false`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bodies: Option<DocumentBodies>,
|
||||
/// Its sketches.
|
||||
pub sketches: Vec<DocumentSketch>,
|
||||
/// What is selected, each as the selection commands take it.
|
||||
@@ -420,6 +515,44 @@ pub struct TimelineEntry {
|
||||
pub ms: Option<f64>,
|
||||
}
|
||||
|
||||
/// A feature of `document.inspect`'s timeline: in full, or in short with `timeline: "summary"`.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum TimelineRow {
|
||||
Full(TimelineEntry),
|
||||
Summary(TimelineSummary),
|
||||
}
|
||||
|
||||
/// A feature in short: what it is, and only the states it has.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct TimelineSummary {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
/// The feature's kind, such as `Extrude`.
|
||||
#[serde(rename = "type")]
|
||||
pub kind: String,
|
||||
/// Why it failed; present only when it did.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
/// Present only when it has one.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub warning: Option<String>,
|
||||
/// Present, and true, only when it is suppressed.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub suppressed: Option<bool>,
|
||||
/// Present, and true, only when the timeline marker has rolled it back.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rolled_back: Option<bool>,
|
||||
}
|
||||
|
||||
/// `document.inspect`'s bodies: each in full, or only their names with `bodies: "names"`.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum DocumentBodies {
|
||||
Full(Vec<DocumentBody>),
|
||||
Names(Vec<String>),
|
||||
}
|
||||
|
||||
/// A body of the design: measured with `measure`, else with the feature that made it.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct DocumentBody {
|
||||
|
||||
@@ -99,7 +99,7 @@ fn describe_returns_both_schemas() {
|
||||
|
||||
#[test]
|
||||
fn inspect_schemas_and_the_key_check_agree() {
|
||||
assert_keys(input_schema_for::<DocumentInspectParams>(), &["measure", "sketch_counts"]);
|
||||
assert_keys(input_schema_for::<DocumentInspectParams>(), &["bodies", "measure", "sketch_counts", "timeline"]);
|
||||
assert_keys(input_schema_for::<MeasureParams>(), &["bodies", "items", "names_only"]);
|
||||
assert_keys(input_schema_for::<CommandsParams>(), &[]);
|
||||
assert_keys(input_schema_for::<MaterialListParams>(), &[]);
|
||||
@@ -182,6 +182,15 @@ fn the_types_read_each_key_as_the_handler_does() {
|
||||
// document.inspect's flags are plain booleans, `null` is left out.
|
||||
assert_eq!(issues("document.inspect", json!({"measure": true, "sketch_counts": null})), none);
|
||||
assert_eq!(issues("document.inspect", json!({"measure": "yes"})), ["`measure` must be true or false"]);
|
||||
for ok in
|
||||
[json!({"timeline": "summary", "bodies": "names"}), json!({"timeline": false, "bodies": false}), json!({"timeline": true, "bodies": null})]
|
||||
{
|
||||
assert_eq!(issues("document.inspect", ok.clone()), none, "{ok}");
|
||||
}
|
||||
for bad in [json!("full"), json!(1), json!(["Box"])] {
|
||||
assert_eq!(issues("document.inspect", json!({ "timeline": bad.clone() })), ["`timeline` must be true, false or \"summary\""], "{bad}");
|
||||
assert_eq!(issues("document.inspect", json!({ "bodies": bad.clone() })), ["`bodies` must be true, false or \"names\""], "{bad}");
|
||||
}
|
||||
// Neither list command takes a key.
|
||||
assert_eq!(issues("engine.commands", json!({"x": 1})), ["unknown key `x`"]);
|
||||
assert_eq!(issues("material.list", json!({"x": 1})), ["unknown key `x`"]);
|
||||
@@ -234,7 +243,17 @@ fn the_schemas_take_what_the_handlers_read() {
|
||||
(json!({"sketch": true}), false),
|
||||
],
|
||||
);
|
||||
takes(input_schema_for::<DocumentInspectParams>(), &[(json!({"measure": true, "sketch_counts": false}), true), (json!({"measure": 1}), false)]);
|
||||
takes(
|
||||
input_schema_for::<DocumentInspectParams>(),
|
||||
&[
|
||||
(json!({"measure": true, "sketch_counts": false}), true),
|
||||
(json!({"measure": 1}), false),
|
||||
(json!({"timeline": "summary", "bodies": "names"}), true),
|
||||
(json!({"timeline": false, "bodies": true}), true),
|
||||
(json!({"timeline": "names"}), false),
|
||||
(json!({"bodies": ["Block"]}), false),
|
||||
],
|
||||
);
|
||||
takes(input_schema_for::<CommandsParams>(), &[(json!({}), true), (json!({"x": 1}), false)]);
|
||||
}
|
||||
|
||||
@@ -325,6 +344,81 @@ fn document_inspect_replies_match_its_schema() {
|
||||
assert!(r["bodies"].as_array().is_some_and(|b| b.iter().any(|b| b["removed_later"] == true)), "{r}");
|
||||
}
|
||||
|
||||
/// `timeline` and `bodies` make a large design's reply small (#14162): a summary row has only
|
||||
/// what the feature is and the states it has, `"names"` lists only body names and measures
|
||||
/// nothing, `false` leaves either out. The default reply is as it was; a bad value is refused
|
||||
/// by the handler too, with the check off.
|
||||
#[test]
|
||||
fn document_inspect_gives_a_short_timeline_and_body_names_when_asked() {
|
||||
let mut s = Session { param_check: ParamCheck::Warn, ..Session::default() };
|
||||
design(&mut s);
|
||||
// A feature that fails: a box whose length goes to zero.
|
||||
run(&mut s, "parameters.change", json!({"name": "t", "expression": "5 mm"}));
|
||||
run(&mut s, "solid.box", json!({"length": "t", "width": 5, "height": 5, "corner": [80, 0, 0], "body_name": "Broken"}));
|
||||
run(&mut s, "parameters.change", json!({"name": "t", "expression": "0 mm"}));
|
||||
let keys = |row: &Value| -> Vec<String> {
|
||||
let mut k: Vec<String> = row.as_object().map(|o| o.keys().cloned().collect()).unwrap_or_default();
|
||||
k.sort();
|
||||
k
|
||||
};
|
||||
let full = run(&mut s, "document.inspect", json!({"measure": true}));
|
||||
for row in full["timeline"].as_array().unwrap() {
|
||||
assert_eq!(keys(row), ["error", "id", "ms", "name", "rolled_back", "suppressed", "type", "warning"], "the default row: {row}");
|
||||
}
|
||||
assert_eq!(run(&mut s, "document.inspect", json!({"measure": true, "timeline": true, "bodies": true})), full);
|
||||
let failing = full["timeline"].as_array().unwrap().iter().filter(|f| !f["error"].is_null()).count();
|
||||
assert_eq!(failing, 1, "the zero-length box fails: {full}");
|
||||
|
||||
let short = run(&mut s, "document.inspect", json!({"measure": true, "timeline": "summary", "bodies": "names"}));
|
||||
matches::<DocumentInspectResult>("document.inspect", &short, false);
|
||||
let rows = short["timeline"].as_array().unwrap();
|
||||
assert_eq!(rows.len(), full["timeline"].as_array().unwrap().len(), "{short}");
|
||||
for (row, was) in rows.iter().zip(full["timeline"].as_array().unwrap()) {
|
||||
let mut want = vec!["id", "name", "type"];
|
||||
if !was["error"].is_null() {
|
||||
want.push("error");
|
||||
}
|
||||
if !was["warning"].is_null() {
|
||||
want.push("warning");
|
||||
}
|
||||
want.sort();
|
||||
assert_eq!(keys(row), want, "{row} from {was}");
|
||||
assert_eq!(
|
||||
(&row["id"], &row["name"], &row["type"], &row.get("error")),
|
||||
(&was["id"], &was["name"], &was["type"], &was.get("error").filter(|e| !e.is_null()))
|
||||
);
|
||||
}
|
||||
let names: Vec<&Value> = full["bodies"].as_array().unwrap().iter().map(|b| &b["name"]).collect();
|
||||
assert_eq!(short["bodies"].as_array().unwrap().iter().collect::<Vec<_>>(), names, "names only, nothing measured: {short}");
|
||||
let size = |r: &Value| r["timeline"].to_string().len() + r["bodies"].to_string().len();
|
||||
assert!(size(&short) * 3 < size(&full), "timeline and bodies: {} vs {} bytes", size(&short), size(&full));
|
||||
// Everything else is as it was.
|
||||
for key in ["params", "sketches", "undo", "marker", "selection"] {
|
||||
assert_eq!(short[key], full[key], "{key}");
|
||||
}
|
||||
|
||||
let none = run(&mut s, "document.inspect", json!({"timeline": false, "bodies": false}));
|
||||
matches::<DocumentInspectResult>("document.inspect", &none, false);
|
||||
assert!(none.get("timeline").is_none() && none.get("bodies").is_none(), "{none}");
|
||||
assert_eq!(none["params"], full["params"]);
|
||||
|
||||
// A rolled-back or suppressed feature says so in its summary row.
|
||||
run(&mut s, "timeline.roll_to", json!({"feature": "Shapes"}));
|
||||
let rolled = run(&mut s, "document.inspect", json!({"timeline": "summary"}));
|
||||
assert!(rolled["timeline"].as_array().unwrap().iter().any(|f| f["rolled_back"] == true), "{rolled}");
|
||||
assert!(rolled["timeline"].as_array().unwrap().iter().all(|f| f.get("rolled_back").is_none_or(|r| r == true)), "{rolled}");
|
||||
|
||||
// The handler refuses a bad value itself (with the check off as well).
|
||||
let mut off = Session { param_check: ParamCheck::Off, ..Session::default() };
|
||||
for (p, want) in [
|
||||
(json!({"timeline": "full"}), "`timeline` must be true, false or \"summary\""),
|
||||
(json!({"bodies": ["Block"]}), "`bodies` must be true, false or \"names\""),
|
||||
] {
|
||||
let e = off.execute("document.inspect", &p).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(e.contains(want), "{p}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `inspect.measure` in each form: bodies (with sheet metal and a material), names only, one
|
||||
/// item and two, in Warn without warnings. Everything it ignores with `items` is ignored.
|
||||
#[test]
|
||||
|
||||
@@ -1482,6 +1482,9 @@ pub struct MirrorResult {
|
||||
pub struct MoveResult {
|
||||
/// The curves moved, or with `copy` the copies made, by id.
|
||||
pub curves: Vec<String>,
|
||||
/// The constraints tying the moved geometry to what stays that the move let go, as
|
||||
/// `{id, kind}`.
|
||||
pub released: Vec<Value>,
|
||||
/// The sketch's solve: `{solved, dof}`.
|
||||
pub sketch: Value,
|
||||
/// In the warn parameter-check mode: what the call's parameters got wrong.
|
||||
|
||||
@@ -481,6 +481,18 @@ mod imp {
|
||||
KernelError::TooLarge(format!("the {what} is larger than the faces beside its edges allow{beside}; {fits}"))
|
||||
}
|
||||
SmError::Unported(why) => KernelError::Invalid(format!("not supported yet: {why}")),
|
||||
// Not too large: a longer setback. Named as building only where solidcore's fill
|
||||
// is the corner; where another patch replaces it (a G2 corner) it is what
|
||||
// solidcore's corner needs, which that patch may not fit.
|
||||
SmError::SetbackTooShort { least, at, builds } => {
|
||||
let at = at.map(|[x, y, z]| format!(" at ({x:.3}, {y:.3}, {z:.3})")).unwrap_or_default();
|
||||
let least = crate::blend::trimmed_mm(least);
|
||||
KernelError::Invalid(if builds {
|
||||
format!("the setback{at} of the {what} is too short for its corner's fill; {}{least} mm", crate::blend::SETBACK_AT_LEAST)
|
||||
} else {
|
||||
format!("the setback{at} of the {what} is too short for its corner's fill: solidcore's corner needs a setback of at least {least} mm")
|
||||
})
|
||||
}
|
||||
e => failed(what, "operation", e),
|
||||
})?;
|
||||
// Checked here unless the evaluation's validation mode leaves it to the feature's end
|
||||
|
||||
@@ -372,3 +372,20 @@ fn curvature_rounds_meet_at_a_g2_corner() {
|
||||
let (a, b) = (measure(&g2).unwrap().volume, measure(&smlib_corner).unwrap().volume);
|
||||
assert!((a - b).abs() < 7.5f64.powi(3), "{a} vs {b}");
|
||||
}
|
||||
|
||||
/// fillets/every-box-corner-three-edges-r3-curvature-setback-3-5 (tangency-labs/solidcraft#13913):
|
||||
/// the 40 x 30 x 20 box's corner (40, 30, 20), its three edges rounded r3, set back 3.5. The
|
||||
/// corner's fill is certified convex only from a setback of 3.5595 (solidcore's
|
||||
/// `SetbackTooShort`, from the fill's convex test), so 3.5 is refused naming it. The G2 patch
|
||||
/// replaces solidcore's fill here, so 3.5595 is named as what solidcore's corner needs, never as
|
||||
/// a setback that builds: the patch does not fit there (it misses the rounds' curvature by
|
||||
/// 2.09e-3 against 1.67e-3, measured).
|
||||
#[test]
|
||||
fn a_setback_too_short_for_the_fill_names_what_solidcores_corner_needs() {
|
||||
let body = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap();
|
||||
let edges = [Vec3::new(20.0, 30.0, 20.0), Vec3::new(40.0, 15.0, 20.0), Vec3::new(40.0, 30.0, 10.0)];
|
||||
let corner = Vec3::new(40.0, 30.0, 20.0);
|
||||
let short = fillet_g2_corners(&body, &edges, 3.0, &[(corner, 3.5)]).unwrap_err().to_string();
|
||||
assert!(short.contains("is too short for its corner's fill: solidcore's corner needs a setback of at least 3.5595 mm"), "{short}");
|
||||
assert!(!short.contains("builds"), "{short}");
|
||||
}
|
||||
|
||||
@@ -74,3 +74,22 @@ fn a_setback_not_past_the_radius_is_refused_naming_the_least() {
|
||||
assert!(least.is_some_and(|l| (l - 5.0).abs() < 1e-9), "{least:?}");
|
||||
assert_eq!(crate::fillet_least_setback(&b, Vec3::new(30.0, 20.0, 10.0), 5.0).unwrap(), None);
|
||||
}
|
||||
|
||||
/// tangency-labs/solidcraft#13913: the 40 x 30 x 20 box's corner (40, 30, 20), its three edges
|
||||
/// rounded r3, set back 3.5. The corner's fill (solidcore's own here, no patch replaces it) is
|
||||
/// certified convex only from a setback of 3.5595 (solidcore's `SetbackTooShort`, from the fill's
|
||||
/// convex test), so 3.5 is refused naming it as the smallest that builds; set back that far, it
|
||||
/// builds, a valid body.
|
||||
#[test]
|
||||
fn a_setback_too_short_for_the_fill_is_refused_naming_the_one_that_builds() {
|
||||
let b = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap();
|
||||
let edges = [Vec3::new(20.0, 30.0, 20.0), Vec3::new(40.0, 15.0, 20.0), Vec3::new(40.0, 30.0, 10.0)];
|
||||
let corner = Vec3::new(40.0, 30.0, 20.0);
|
||||
let why = match fillet_setback(&b, &edges, 3.0, &[(corner, 3.5)]) {
|
||||
Err(KernelError::Invalid(why)) => why,
|
||||
other => panic!("setback 3.5: {:?}", other.map(|b| b.face_count())),
|
||||
};
|
||||
assert!(why.contains(&format!("{SETBACK_AT_LEAST}3.5595 mm")), "{why}");
|
||||
let out = fillet_setback(&b, &edges, 3.0, &[(corner, 3.5595)]).unwrap();
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
}
|
||||
|
||||
+134
-2
@@ -962,6 +962,138 @@ fn check_parameters_tries_values_and_puts_the_design_back() {
|
||||
assert!(d["timeline"].as_array().unwrap().iter().all(|f| f["error"].is_null()), "{d}");
|
||||
}
|
||||
|
||||
/// `bodies` and `summary` keep `check_parameters`'s reply small on a large design (#14162): only
|
||||
/// the bodies named are measured and their cylinders listed, and `measure` is the measure
|
||||
/// tool's summary. Without them the reply is as before; a bad `bodies` changes nothing.
|
||||
#[test]
|
||||
fn check_parameters_reports_only_the_bodies_asked_for_in_summary() {
|
||||
let mut s = server();
|
||||
parametric_box_with_cylinder(&mut s);
|
||||
tool(&mut s, "execute", json!({"command": "solid.box", "params": {"length": 10, "width": 10, "height": 10, "corner": [100, 0, 0]}}));
|
||||
tool(&mut s, "execute", json!({"command": "solid.cylinder", "params": {"radius": 2, "height": 10, "base": [200, 0, 0]}}));
|
||||
let names: Vec<String> =
|
||||
tool(&mut s, "measure", json!({}))["bodies"].as_array().unwrap().iter().map(|b| b["name"].as_str().unwrap().to_string()).collect();
|
||||
assert_eq!(names.len(), 3, "{names:?}");
|
||||
let before = tool(&mut s, "inspect_design", json!({"measure": false, "sketches": false}));
|
||||
|
||||
// The default: every body measured in full, every cylinder.
|
||||
let all = tool(&mut s, "check_parameters", json!({"set": {"len": 60}, "report": ["measure", "cylinders"]}));
|
||||
assert_eq!(all["measure"]["body_count"], 3, "{all}");
|
||||
assert!(all["measure"]["bodies"][0]["area_mm2"].is_number() && all["measure"]["bodies"][0]["kernel"].is_object(), "full measure: {all}");
|
||||
assert_eq!(all["cylinders"].as_array().map(Vec::len), Some(2), "{all}");
|
||||
|
||||
let one = tool(&mut s, "check_parameters", json!({"set": {"len": 60}, "report": ["measure", "cylinders"], "bodies": ["Body1"], "summary": true}));
|
||||
assert_eq!(one["restored"], true, "{one}");
|
||||
let m = &one["measure"];
|
||||
assert_eq!(m["bodies"].as_array().map(Vec::len), Some(1), "{one}");
|
||||
let mut keys: Vec<&String> = m["bodies"][0].as_object().unwrap().keys().collect();
|
||||
keys.sort();
|
||||
assert_eq!(keys, ["bbox", "closed", "faces", "name", "volume_mm3"], "the measure tool's summary: {one}");
|
||||
assert_eq!(m["bodies"][0]["name"], "Body1");
|
||||
let v = m["total"]["volume_mm3"].as_f64().unwrap();
|
||||
assert!(rel(v, box_with_cylinder_volume(60.0)) < 5e-4, "{v}");
|
||||
assert_eq!(m["bodies"][0]["bbox"], all["measure"]["bodies"][0]["bbox"], "{one}");
|
||||
let cyl = one["cylinders"].as_array().unwrap();
|
||||
assert!(cyl.len() == 1 && cyl[0]["body"] == "Body1", "{one}");
|
||||
assert!(one.to_string().len() * 3 < all.to_string().len(), "{} vs {} bytes", one.to_string().len(), all.to_string().len());
|
||||
// `summary` on its own summarises every body.
|
||||
let sum = tool(&mut s, "check_parameters", json!({"set": {"len": 60}, "report": ["measure"], "summary": true}));
|
||||
assert_eq!(sum["measure"]["bodies"].as_array().map(Vec::len), Some(3), "{sum}");
|
||||
assert!(sum["measure"]["bodies"][0].get("area_mm2").is_none(), "{sum}");
|
||||
|
||||
// A body the design doesn't have: an error, and the design is put back.
|
||||
for report in [json!(["measure"]), json!(["cylinders"])] {
|
||||
let r = raw_tool(&mut s, "check_parameters", json!({"set": {"len": 60}, "report": report, "bodies": ["Nope"]}));
|
||||
assert_eq!(r["result"]["isError"], json!(true), "{report}: {r}");
|
||||
let body: Value = serde_json::from_str(r["result"]["content"][0]["text"].as_str().unwrap()).unwrap();
|
||||
assert_eq!(body["restored"], true, "{body}");
|
||||
let e = body["error"].as_str().unwrap();
|
||||
assert!(e.contains("no body `Nope`") && e.contains("Body1"), "{body}");
|
||||
}
|
||||
// A `bodies` that is not a list of names, or a `summary` that is not a flag: refused first.
|
||||
for args in [json!({"set": {"len": 60}, "bodies": "Body1"}), json!({"set": {"len": 60}, "summary": "yes"})] {
|
||||
let r = raw_tool(&mut s, "check_parameters", args.clone());
|
||||
assert_eq!(r["error"]["code"], -32602, "{args}: {r}");
|
||||
}
|
||||
for args in [json!({"set": {"len": 60}, "bodies": []}), json!({"set": {"len": 60}, "bodies": [5]}), json!({"set": {"len": 60}, "bodies": [""]})] {
|
||||
let r = raw_tool(&mut s, "check_parameters", args.clone());
|
||||
assert_eq!(r["result"]["isError"], json!(true), "{args}: {r}");
|
||||
assert!(r["result"]["content"][0]["text"].as_str().unwrap().contains("`bodies` must be a list of body names"), "{args}: {r}");
|
||||
}
|
||||
assert!(rel(volume(&mut s), box_with_cylinder_volume(40.0) + 1000.0 + PI * 4.0 * 10.0) < 5e-4);
|
||||
let after = tool(&mut s, "inspect_design", json!({"measure": false, "sketches": false}));
|
||||
assert_eq!(after["undo"], before["undo"], "{after}");
|
||||
}
|
||||
|
||||
/// `timeline` and `bodies` keep `inspect_design` small on a large design (#14162), with and
|
||||
/// without compact output and as a batch step; left out, the call and its reply are as before.
|
||||
#[test]
|
||||
fn inspect_design_gives_a_timeline_summary_and_body_names_when_asked() {
|
||||
for opts in [crate::Options::default(), crate::Options { compact: true }] {
|
||||
let mut s = Server::new(Box::new(Headless::default())).with_options(opts);
|
||||
parametric_box_with_cylinder(&mut s);
|
||||
tool(&mut s, "execute", json!({"command": "solid.box", "params": {"length": 10, "width": 10, "height": 10, "corner": [100, 0, 0]}}));
|
||||
let base = json!({"measure": true, "sketches": false, "joints": false});
|
||||
let with = |extra: Value| {
|
||||
let mut a = base.clone();
|
||||
a.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone());
|
||||
a
|
||||
};
|
||||
let full = tool(&mut s, "inspect_design", base.clone());
|
||||
assert_eq!(tool(&mut s, "inspect_design", with(json!({"timeline": true, "bodies": true}))), full, "{opts:?}");
|
||||
assert!(full["timeline"][0]["ms"].is_number() && full["bodies"][0]["volume_mm3"].is_number(), "{full}");
|
||||
|
||||
let short = tool(&mut s, "inspect_design", with(json!({"timeline": "summary", "bodies": "names"})));
|
||||
let rows = short["timeline"].as_array().unwrap();
|
||||
assert_eq!(rows.len(), full["timeline"].as_array().unwrap().len(), "{short}");
|
||||
for row in rows {
|
||||
let mut keys: Vec<&String> = row.as_object().unwrap().keys().collect();
|
||||
keys.sort();
|
||||
assert_eq!(keys, ["id", "name", "type"], "{opts:?}: {row}");
|
||||
}
|
||||
assert_eq!(short["bodies"], json!(["Body1", "Body2"]), "{opts:?}: {short}");
|
||||
assert_eq!(short["params"], full["params"]);
|
||||
let gone = tool(&mut s, "inspect_design", with(json!({"timeline": false, "bodies": false})));
|
||||
assert!(gone.get("timeline").is_none() && gone.get("bodies").is_none(), "{opts:?}: {gone}");
|
||||
|
||||
// A value the tool doesn't take is refused before anything is sent.
|
||||
for bad in [json!({"timeline": "full"}), json!({"timeline": 1})] {
|
||||
let r = raw_tool(&mut s, "inspect_design", bad.clone());
|
||||
assert_eq!(r["error"]["code"], -32602, "{opts:?} {bad}: {r}");
|
||||
}
|
||||
// `bodies` takes a list with compact output, so its check names every form.
|
||||
let r = raw_tool(&mut s, "inspect_design", json!({"bodies": "all"}));
|
||||
if opts.compact {
|
||||
assert_eq!(r["result"]["isError"], json!(true), "{r}");
|
||||
assert!(r["result"]["content"][0]["text"].as_str().unwrap().contains("list of body names, true, false or \"names\""), "{r}");
|
||||
} else {
|
||||
assert_eq!(r["error"]["code"], -32602, "{r}");
|
||||
}
|
||||
|
||||
// As a batch step.
|
||||
let r = tool(
|
||||
&mut s,
|
||||
"batch",
|
||||
json!({"commands": [{"command": "inspect_design", "params": {"measure": false, "timeline": "summary", "bodies": "names"}}]}),
|
||||
);
|
||||
let step = &r["results"][0];
|
||||
assert_eq!(step["bodies"], json!(["Body1", "Body2"]), "{opts:?}: {r}");
|
||||
assert!(step["timeline"][0].get("ms").is_none(), "{opts:?}: {r}");
|
||||
}
|
||||
|
||||
// Without the new arguments, document.inspect is asked exactly as before, so an app from
|
||||
// before them answers as it always did.
|
||||
let mut b = Answers { calls: Vec::new(), answer: Box::new(|_, _| Ok(json!({"timeline": [], "bodies": [], "sketches": []}))) };
|
||||
crate::tools::call_tool(&mut b, "inspect_design", &json!({"sketches": false, "joints": false}));
|
||||
crate::tools::call_tool_with(&mut b, "inspect_design", &json!({"joints": false}), crate::Options { compact: true });
|
||||
crate::tools::call_tool_with(&mut b, "inspect_design", &json!({"joints": false, "bodies": ["B"]}), crate::Options { compact: true });
|
||||
let sent: Vec<&Value> = b.calls.iter().filter(|(m, _)| m == "document.inspect").map(|(_, p)| p).collect();
|
||||
assert_eq!(
|
||||
sent,
|
||||
[&json!({"measure": true}), &json!({"measure": true, "sketch_counts": true}), &json!({"measure": false, "sketch_counts": true})]
|
||||
);
|
||||
}
|
||||
|
||||
/// An unknown parameter name, a bad expression or a bad report list is an error that leaves the
|
||||
/// design untouched; `set` that is not an object is refused by argument checking; the tool is
|
||||
/// annotated as an edit (it changes the design and puts it back) and is not a batch step.
|
||||
@@ -4074,7 +4206,7 @@ fn inspect_design_is_unchanged_without_compact_output() {
|
||||
let defs = tool_definitions();
|
||||
let def = defs.iter().find(|d| d["name"] == "inspect_design").unwrap();
|
||||
assert_eq!(def["inputSchema"]["properties"]["sketches"]["type"], "boolean", "{def}");
|
||||
assert!(def["inputSchema"]["properties"].get("bodies").is_none(), "{def}");
|
||||
assert_eq!(def["inputSchema"]["properties"]["bodies"]["type"], json!(["boolean", "string"]), "no list of names: {def}");
|
||||
let listed = call(&mut s, 1, "tools/list", json!({}));
|
||||
assert!(listed["result"]["tools"].as_array().unwrap().iter().any(|t| t == def), "tools/list serves the default definition");
|
||||
|
||||
@@ -4169,7 +4301,7 @@ impl crate::Backend for AppWithPrefs {
|
||||
fn inspect_design_is_compact(s: &mut Server) -> bool {
|
||||
let listed = call(s, 1, "tools/list", json!({}));
|
||||
let def = listed["result"]["tools"].as_array().and_then(|t| t.iter().find(|d| d["name"] == "inspect_design")).cloned().unwrap_or_default();
|
||||
def["inputSchema"]["properties"]["bodies"].is_object()
|
||||
def["inputSchema"]["properties"]["bodies"]["type"].as_array().is_some_and(|t| t.contains(&json!("array")))
|
||||
}
|
||||
|
||||
/// The app's Preferences turn compact output on for a bridged server; it asks until the app
|
||||
|
||||
+106
-17
@@ -237,7 +237,8 @@ pub fn tool_definitions_with(opts: Options) -> Vec<Value> {
|
||||
kernel splits one cylinder into merged (origin is the axis point closest to the world origin). The reply ends with \
|
||||
`restored: true`; if the design could not be put back the call is an error with `restored: false` and the values \
|
||||
still applied. An unknown parameter name is an error and nothing changes. Replaces the loop set_parameter, \
|
||||
measure, body_topology, set_parameter back.",
|
||||
measure, body_topology, set_parameter back. On a large design keep the reply small with `bodies` (only those \
|
||||
bodies are measured and their cylinders listed) and `summary: true` (measure as the measure tool's summary).",
|
||||
object(
|
||||
json!({
|
||||
"set": {
|
||||
@@ -250,6 +251,8 @@ pub fn tool_definitions_with(opts: Options) -> Vec<Value> {
|
||||
"items": {"type": "string", "enum": REPORTS},
|
||||
"description": "What to report: measure, cylinders, errors (default all three)",
|
||||
},
|
||||
"bodies": {"type": "array", "items": {"type": "string"}, "description": "Only these bodies, by name, for measure and cylinders (default: all)"},
|
||||
"summary": {"type": "boolean", "description": "measure as the measure tool's `summary: true`: per body only name, volume_mm3, bbox {min, max}, faces and closed, and total.volume_mm3 (default false: the full reply)"},
|
||||
}),
|
||||
&["set"],
|
||||
),
|
||||
@@ -452,12 +455,15 @@ fn inspect_design_tool(opts: Options) -> Value {
|
||||
counts), sketches (curves with ids and coordinates, points, constraints, profiles, degrees of freedom, solve \
|
||||
status), assembly joints (what each origin is attached to: occurrence, the feature, face, edge or sketch \
|
||||
curve its face names refer to, whether it resolves and why not; `execute` `joint.inspect {joint}` gives one), \
|
||||
the active sketch, selection and undo depth.",
|
||||
the active sketch, selection and undo depth. On a large design keep the reply small with `timeline: \"summary\"` \
|
||||
(each feature as id, name, type, and error or warning only when it has one) or false, and `bodies: \"names\"` or false.",
|
||||
object(
|
||||
json!({
|
||||
"measure": {"type": "boolean", "description": "Measure bodies (default true)"},
|
||||
"sketches": {"type": "boolean", "description": "Full sketch contents (default true; false = summary only)"},
|
||||
"joints": {"type": "boolean", "description": "Joint definitions as joint.inspect gives them, when the design has joints (default true)"},
|
||||
"timeline": timeline_arg(),
|
||||
"bodies": {"type": ["boolean", "string"], "enum": [true, false, "names"], "description": BODIES_ARG},
|
||||
}),
|
||||
&[],
|
||||
),
|
||||
@@ -473,7 +479,9 @@ fn inspect_design_tool(opts: Options) -> Value {
|
||||
plane, counts of points, curves, constraints and profiles, degrees of freedom, solve status); their full contents \
|
||||
(curves with ids and coordinates, points, constraints, profiles) only for the sketches named in `sketches`, or \
|
||||
all with `sketches: true` (large: a 720-point sketch is some 200 KB). Assembly joints: what each origin is \
|
||||
attached to (`execute` `joint.inspect {joint}` gives one). Arguments: measure, sketches, bodies, joints.",
|
||||
attached to (`execute` `joint.inspect {joint}` gives one). On a large design keep the reply small with \
|
||||
`timeline: \"summary\"` (each feature as id, name, type, and error or warning only when it has one) or false, and \
|
||||
`bodies: \"names\"` or false. Arguments: measure, sketches, bodies, timeline, joints.",
|
||||
object(
|
||||
json!({
|
||||
"measure": {"type": "boolean", "description": "Measure bodies (default true)"},
|
||||
@@ -482,8 +490,13 @@ fn inspect_design_tool(opts: Options) -> Value {
|
||||
"items": {"type": ["string", "integer"]},
|
||||
"description": "Which sketches in full: a list of sketch names or ids, true (all of them), or false / \"summary\" (none: summaries only, the default)",
|
||||
},
|
||||
"bodies": {"type": "array", "items": {"type": "string"}, "description": "Only these bodies, by name (default: all)"},
|
||||
"bodies": {
|
||||
"type": ["array", "boolean", "string"],
|
||||
"items": {"type": "string"},
|
||||
"description": "Only these bodies, by name (default: all); or \"names\" (only the body names, nothing measured) or false (no bodies)",
|
||||
},
|
||||
"joints": {"type": "boolean", "description": "Joint definitions as joint.inspect gives them, when the design has joints (default true)"},
|
||||
"timeline": timeline_arg(),
|
||||
}),
|
||||
&[],
|
||||
),
|
||||
@@ -491,6 +504,18 @@ fn inspect_design_tool(opts: Options) -> Value {
|
||||
)
|
||||
}
|
||||
|
||||
/// `inspect_design`'s `timeline` argument, as `document.inspect` takes it.
|
||||
fn timeline_arg() -> Value {
|
||||
json!({
|
||||
"type": ["boolean", "string"],
|
||||
"enum": [true, false, "summary"],
|
||||
"description": "true (default): every feature with its state and timing; \"summary\": each feature as id, name and type, with error, warning, suppressed and rolled_back only when set; false: no timeline",
|
||||
})
|
||||
}
|
||||
|
||||
/// `inspect_design`'s `bodies` argument without compact output.
|
||||
const BODIES_ARG: &str = "true (default): every body as `measure` says; \"names\": only the body names, nothing measured; false: no bodies";
|
||||
|
||||
/// A tool result: content blocks plus the error flag. JSON is pretty-printed, or compact with
|
||||
/// compact output.
|
||||
fn text_result(v: &Value, is_error: bool, opts: Options) -> Value {
|
||||
@@ -849,6 +874,14 @@ fn measure_params(a: &Map<String, Value>) -> Value {
|
||||
p
|
||||
}
|
||||
|
||||
/// `inspect_design`'s arguments as `document.inspect` takes them: `measure`, and `keys` only when
|
||||
/// given, so an app from before them is asked as it always was.
|
||||
fn inspect_params(a: &Map<String, Value>, measure: bool, keys: &[&str]) -> Value {
|
||||
let mut p = pick(a, keys);
|
||||
p["measure"] = json!(measure);
|
||||
p
|
||||
}
|
||||
|
||||
/// `summary` narrows measured bodies; a `names_only` list has nothing to narrow.
|
||||
fn measure_summary_wanted(a: &Map<String, Value>) -> bool {
|
||||
let flag = |k: &str| a.get(k).and_then(Value::as_bool).unwrap_or(false);
|
||||
@@ -875,7 +908,7 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Map<String, Value>) -> Result<V
|
||||
}
|
||||
"inspect_design" => {
|
||||
let measure = a.get("measure").and_then(Value::as_bool).unwrap_or(true);
|
||||
let mut d = b.call("document.inspect", json!({"measure": measure}))?;
|
||||
let mut d = b.call("document.inspect", inspect_params(a, measure, &["timeline", "bodies"]))?;
|
||||
if a.get("sketches").and_then(Value::as_bool).unwrap_or(true) {
|
||||
let ids: Vec<Value> = d["sketches"].as_array().map(|v| v.iter().map(|s| s["id"].clone()).collect()).unwrap_or_default();
|
||||
let full: Vec<Value> = ids
|
||||
@@ -1121,7 +1154,10 @@ fn batch_step(step: &Value) -> Result<(String, Value, Option<Narrow>), String> {
|
||||
}
|
||||
mapped("inspect.compare_picture", p)
|
||||
}
|
||||
"inspect_design" => mapped("document.inspect", json!({"measure": a.get("measure").and_then(Value::as_bool).unwrap_or(true)})),
|
||||
"inspect_design" => {
|
||||
let keys: &[&str] = if a.get("bodies").is_some_and(Value::is_array) { &["timeline"] } else { &["timeline", "bodies"] };
|
||||
mapped("document.inspect", inspect_params(a, a.get("measure").and_then(Value::as_bool).unwrap_or(true), keys))
|
||||
}
|
||||
"undo" => mapped("edit.undo", json!({})),
|
||||
"redo" => mapped("edit.redo", json!({})),
|
||||
"export" => mapped("file.export", pick(a, &["path", "format", "bodies", "mesh_prefix", "effort", "velocity"])),
|
||||
@@ -1184,7 +1220,13 @@ fn name_list(names: &[String]) -> String {
|
||||
/// sketches are asked for in full (by name or id, or all), and only the bodies asked for.
|
||||
fn inspect_design_compact(b: &mut dyn Backend, a: &Map<String, Value>) -> Result<Value, String> {
|
||||
let measure = a.get("measure").and_then(Value::as_bool).unwrap_or(true);
|
||||
let bodies: Option<Vec<String>> = match a.get("bodies").filter(|v| !v.is_null()) {
|
||||
// `bodies` as a list keeps only those bodies; true, false or "names" go to document.inspect.
|
||||
if let Some(Value::String(form)) = a.get("bodies")
|
||||
&& form != "names"
|
||||
{
|
||||
return Err("inspect_design: `bodies` must be a list of body names, true, false or \"names\"".into());
|
||||
}
|
||||
let bodies: Option<Vec<String>> = match a.get("bodies").filter(|v| v.is_array()) {
|
||||
None => None,
|
||||
Some(v) => Some(
|
||||
v.as_array()
|
||||
@@ -1193,7 +1235,10 @@ fn inspect_design_compact(b: &mut dyn Backend, a: &Map<String, Value>) -> Result
|
||||
),
|
||||
};
|
||||
// With `bodies`, only those are measured.
|
||||
let mut d = b.call("document.inspect", json!({"measure": measure && bodies.is_none(), "sketch_counts": true}))?;
|
||||
let keys: &[&str] = if bodies.is_some() { &["timeline"] } else { &["timeline", "bodies"] };
|
||||
let mut p = inspect_params(a, measure && bodies.is_none(), keys);
|
||||
p["sketch_counts"] = json!(true);
|
||||
let mut d = b.call("document.inspect", p)?;
|
||||
if let Some(want) = bodies {
|
||||
let all: Vec<Value> = d["bodies"].as_array().cloned().unwrap_or_default();
|
||||
let names: Vec<String> = all.iter().filter_map(|x| x["name"].as_str().map(str::to_string)).collect();
|
||||
@@ -1868,7 +1913,7 @@ fn compact(mut v: Value) -> Value {
|
||||
/// changed a parameter to see its effect spent three to five calls on this loop, and one saved
|
||||
/// the design with the test values still in it.
|
||||
fn check_parameters(b: &mut dyn Backend, a: &Map<String, Value>, opts: Options) -> Value {
|
||||
let (changes, reports) = match check_parameters_args(a) {
|
||||
let (changes, reports, narrow) = match check_parameters_args(a) {
|
||||
Ok(x) => x,
|
||||
Err(e) => return text_result(&Value::String(e), true, opts),
|
||||
};
|
||||
@@ -1881,7 +1926,7 @@ fn check_parameters(b: &mut dyn Backend, a: &Map<String, Value>, opts: Options)
|
||||
let e = "check_parameters: this app has no edit.checkpoint to put the design back with; use set_parameter and measure, and set the values back yourself";
|
||||
return text_result(&json!(e), true, opts);
|
||||
};
|
||||
let outcome = try_parameters(b, &changes, &reports);
|
||||
let outcome = try_parameters(b, &changes, &reports, &narrow);
|
||||
let restore = exec(b, "edit.restore_checkpoint", json!({"checkpoint": id}));
|
||||
match (outcome, restore) {
|
||||
(Ok(mut reply), Ok(_)) => {
|
||||
@@ -1904,9 +1949,17 @@ fn check_parameters(b: &mut dyn Backend, a: &Map<String, Value>, opts: Options)
|
||||
}
|
||||
}
|
||||
|
||||
/// How small `check_parameters`'s reports are: only these bodies (`None`: all), and `measure`
|
||||
/// summarised as the measure tool's `summary: true`.
|
||||
struct ReportScope {
|
||||
bodies: Option<Vec<String>>,
|
||||
summary: bool,
|
||||
}
|
||||
|
||||
/// `check_parameters`'s arguments, checked before anything is sent: the values to try (a
|
||||
/// number or an expression each, as `set_parameter` takes its `value`) and what to report.
|
||||
fn check_parameters_args(a: &Map<String, Value>) -> Result<(Map<String, Value>, Vec<&'static str>), String> {
|
||||
/// number or an expression each, as `set_parameter` takes its `value`), what to report and how
|
||||
/// small.
|
||||
fn check_parameters_args(a: &Map<String, Value>) -> Result<(Map<String, Value>, Vec<&'static str>, ReportScope), String> {
|
||||
let set = a.get("set").and_then(Value::as_object).ok_or("check_parameters: `set` must be an object of parameter name: value")?;
|
||||
if set.is_empty() {
|
||||
return Err("check_parameters: `set` is empty; give at least one parameter name: value".into());
|
||||
@@ -1939,7 +1992,22 @@ fn check_parameters_args(a: &Map<String, Value>) -> Result<(Map<String, Value>,
|
||||
out
|
||||
}
|
||||
};
|
||||
Ok((changes, reports))
|
||||
let bodies = match a.get("bodies").filter(|v| !v.is_null()) {
|
||||
None => None,
|
||||
Some(v) => {
|
||||
let names = v
|
||||
.as_array()
|
||||
.and_then(|l| l.iter().map(|n| n.as_str().map(str::trim).filter(|n| !n.is_empty()).map(str::to_string)).collect::<Option<Vec<_>>>())
|
||||
.filter(|l| !l.is_empty())
|
||||
.ok_or("check_parameters: `bodies` must be a list of body names")?;
|
||||
Some(names)
|
||||
}
|
||||
};
|
||||
let summary = match a.get("summary").filter(|v| !v.is_null()) {
|
||||
None => false,
|
||||
Some(v) => v.as_bool().ok_or("check_parameters: `summary` must be true or false")?,
|
||||
};
|
||||
Ok((changes, reports, ReportScope { bodies, summary }))
|
||||
}
|
||||
|
||||
/// Refuse a name the design has no parameter of, before anything changes. An app without
|
||||
@@ -1960,14 +2028,32 @@ fn known_parameters(b: &mut dyn Backend, changes: &Map<String, Value>) -> Result
|
||||
|
||||
/// Apply the values and read back what was asked for: what each parameter is now, the
|
||||
/// features rebuilt, and the reports.
|
||||
fn try_parameters(b: &mut dyn Backend, changes: &Map<String, Value>, reports: &[&str]) -> Result<Value, String> {
|
||||
fn try_parameters(b: &mut dyn Backend, changes: &Map<String, Value>, reports: &[&str], scope: &ReportScope) -> Result<Value, String> {
|
||||
let (values, recomputed) = apply_parameters(b, changes)?;
|
||||
let mut reply = json!({"set": changes, "values": values, "recomputed": recomputed});
|
||||
// The bodies and each feature's state: one cheap read serves `errors` and `cylinders`.
|
||||
let design = if reports.iter().any(|r| *r != "measure") { Some(b.call("document.inspect", json!({"measure": false}))?) } else { None };
|
||||
let design = if reports.iter().any(|r| *r != "measure") || scope.bodies.is_some() {
|
||||
Some(b.call("document.inspect", json!({"measure": false}))?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// A body named in `bodies` that the design doesn't have (now) is an error that names them.
|
||||
if let (Some(want), Some(d)) = (&scope.bodies, &design) {
|
||||
let names: Vec<String> = d["bodies"].as_array().into_iter().flatten().filter_map(|body| body["name"].as_str()).map(str::to_string).collect();
|
||||
if let Some(missing) = want.iter().find(|n| !names.contains(n)) {
|
||||
return Err(format!("check_parameters: no body `{missing}` (bodies: {})", name_list(&names)));
|
||||
}
|
||||
}
|
||||
for r in reports {
|
||||
match *r {
|
||||
"measure" => reply["measure"] = exec(b, "inspect.measure", json!({}))?,
|
||||
"measure" => {
|
||||
let p = match &scope.bodies {
|
||||
Some(names) => json!({"bodies": names}),
|
||||
None => json!({}),
|
||||
};
|
||||
let m = exec(b, "inspect.measure", p)?;
|
||||
reply["measure"] = if scope.summary { measure_summary(m) } else { m };
|
||||
}
|
||||
"errors" => {
|
||||
if let Some(d) = &design {
|
||||
let pick = |key: &str| -> Value {
|
||||
@@ -1982,8 +2068,11 @@ fn try_parameters(b: &mut dyn Backend, changes: &Map<String, Value>, reports: &[
|
||||
}
|
||||
"cylinders" => {
|
||||
if let Some(d) = &design {
|
||||
let bodies: Vec<String> =
|
||||
let mut bodies: Vec<String> =
|
||||
d["bodies"].as_array().into_iter().flatten().filter_map(|body| body["name"].as_str()).map(str::to_string).collect();
|
||||
if let Some(want) = &scope.bodies {
|
||||
bodies.retain(|n| want.contains(n));
|
||||
}
|
||||
reply["cylinders"] = Value::Array(cylinders(b, &bodies));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +301,6 @@ pub(crate) fn fixes() -> UpstreamFixes {
|
||||
f.bend_volume_bbox_contains_wide_angle_and_inf_u = true;
|
||||
f.bend_volume_trim_param_box_across_period = true;
|
||||
f.degen_param_tiebreak_uses_actual_param = true;
|
||||
f.cylinder_plane_section_trims_exact = true;
|
||||
f.local_surface_intersect_guards_max_boundary_retry = true;
|
||||
f.curve_sweep_closed_path_joins_ends = true;
|
||||
f.validate_tolerances_fixes_reported_gaps = true;
|
||||
|
||||
@@ -25,6 +25,7 @@ use std::sync::PoisonError;
|
||||
use solidcore::EntityRef;
|
||||
use solidcore::SolidError;
|
||||
use solidcore::blend::blend_corner::BlendCornerEvent;
|
||||
use solidcore::blend::blend_refusal::{BlendRefusal, SetbackTooShortWhy};
|
||||
use solidcore::blend::chamfer_side::faces_along;
|
||||
use solidcore::blend::fillet_corner::p3::MIXED_CORNER_RAILS_CROSS;
|
||||
use solidcore::blend::fillet_executive::p1::{FilletLaw, FilletLawKind};
|
||||
@@ -82,6 +83,7 @@ pub(super) fn status(e: SolidError) -> Error {
|
||||
e if is_mass_heal_or_geom_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
|
||||
e if is_sweep_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
|
||||
e if is_query_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
|
||||
e if is_topology_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
|
||||
SolidError::InvalidInput { .. } => Error::Status(INVALID_INPUT),
|
||||
SolidError::Unsupported { reason, .. } if reason == MIXED_CORNER_RAILS_CROSS => Error::Unported(MIXED_CORNER_UNSUPPORTED),
|
||||
e if is_boolean_failure(&e) => boolean_status(e),
|
||||
@@ -149,6 +151,12 @@ fn is_query_failure(e: &SolidError) -> bool {
|
||||
e.query_failure().is_some()
|
||||
}
|
||||
|
||||
/// A topology routine's failure (solidcore's `TopologyFailure`): it stands for the status the
|
||||
/// routine returned (`Upstream` before solidcore described it), and passes on as that status.
|
||||
fn is_topology_failure(e: &SolidError) -> bool {
|
||||
e.topology_failure().is_some()
|
||||
}
|
||||
|
||||
/// `size_ok`.
|
||||
fn size_ok(x: f64) -> bool {
|
||||
x.is_finite() && x > 0.0 && x <= 1e6
|
||||
@@ -334,14 +342,29 @@ fn face_set(b: &Brep) -> solidcore::Result<HashSet<FaceId>> {
|
||||
/// the cliff overflow on, solidcore makes the fork's choice, and its refusals (each with its own
|
||||
/// reason) are [`Error::NoRoom`] like the fork's.
|
||||
///
|
||||
/// A setback too short for its corner's fill, which solidcore refuses as data naming the least
|
||||
/// setback its fill takes (`BlendRefusal::SetbackTooShort`, from the fill's convex or concave
|
||||
/// test), is [`Error::SetbackTooShort`] with that setback and the corner's point; `replaced`
|
||||
/// (the session marks the corners' patches for another patch to replace) says the setback is
|
||||
/// only what solidcore's corner needs, not one shown to build. Its other reason, a setback
|
||||
/// short of where the rails cross, names a setback that passes that test only, so it stays
|
||||
/// unsized.
|
||||
///
|
||||
/// A blend too large that solidcore sizes (the session's fit, as the fork's capi reads it with
|
||||
/// `smlib_blend_fit_size`, or [`SolidError::FilletRefused`] carrying it) is
|
||||
/// [`Error::TooLarge`] with that size, its edge's point
|
||||
/// filled in by the caller from the edge returned with it ([`sized_on`]); a corner solidcore
|
||||
/// refuses as not supported (the mixed corner whose rails cross, solidcore#2256) is
|
||||
/// [`Error::Unported`] naming it.
|
||||
fn blend_failure(s: &FilletSession<'_>, e: SolidError, corner: bool) -> (Error, Option<EdgeId>) {
|
||||
fn blend_failure(s: &FilletSession<'_>, e: SolidError, corner: bool, replaced: bool) -> (Error, Option<EdgeId>) {
|
||||
let info = s.error_info();
|
||||
if let SolidError::BlendRefused {
|
||||
refusal: BlendRefusal::SetbackTooShort { least, why: SetbackTooShortWhy::ConvexFill | SetbackTooShortWhy::ConcaveFill, .. },
|
||||
..
|
||||
} = e
|
||||
{
|
||||
return (Error::SetbackTooShort { least, at: info.location.map(|p| [p.x, p.y, p.z]), builds: !replaced }, None);
|
||||
}
|
||||
let (fit, reason) = match &e {
|
||||
SolidError::FilletRefused { fit, reason, .. } => (Some(*fit), reason.or(info.no_room_reason())),
|
||||
_ if info.code == FilletErrorType::NoRoom as i32 => (s.fit_size(), info.no_room_reason()),
|
||||
@@ -567,6 +590,9 @@ fn blend_edges_stopping(
|
||||
step = corners(&mut s, section, setbacks);
|
||||
}
|
||||
s.set_mark_corner_patches(patches.is_some());
|
||||
// the corners' patches marked for another patch to replace: solidcore's fill is then
|
||||
// not the corner ([`blend_failure`])
|
||||
let replaced = patches.is_some();
|
||||
match step {
|
||||
// The marks go with the session: read them before it drops.
|
||||
Ok(Ok(set_back)) => match s.do_filleting(k, None) {
|
||||
@@ -580,7 +606,7 @@ fn blend_edges_stopping(
|
||||
*m = s.mitred_edges();
|
||||
}
|
||||
}
|
||||
let (why, edge) = blend_failure(&s, e, corner);
|
||||
let (why, edge) = blend_failure(&s, e, corner, replaced);
|
||||
fit_edge = edge;
|
||||
Ok(Err(Err(why)))
|
||||
}
|
||||
@@ -589,7 +615,7 @@ fn blend_edges_stopping(
|
||||
Err(e) => match refused_turn {
|
||||
Some(why) => Ok(Err(Err(why))),
|
||||
None => {
|
||||
let (why, edge) = blend_failure(&s, e, corner);
|
||||
let (why, edge) = blend_failure(&s, e, corner, replaced);
|
||||
fit_edge = edge;
|
||||
Ok(Err(Err(why)))
|
||||
}
|
||||
@@ -1517,4 +1543,21 @@ mod tests {
|
||||
assert!(matches!(status(query(QueryFailure::Missing("the face's surface"))), Error::Status(1006)));
|
||||
assert!(matches!(status(query(QueryFailure::InvalidValue("a vector of zero length has no direction"))), Error::Status(1007)));
|
||||
}
|
||||
|
||||
/// A topology routine's failure passes on as the status it replaced, as the `Upstream` it
|
||||
/// replaced did, whether or not it names an entity.
|
||||
#[test]
|
||||
fn topology_failures_pass_on_as_their_status() {
|
||||
use solidcore::error::Failure;
|
||||
use solidcore::error::topology::TopologyFailure;
|
||||
let topology = |t| Failure::new("topology::edge", t).into_error();
|
||||
assert!(matches!(status(topology(TopologyFailure::Failed("the loop does not close"))), Error::Status(1001)));
|
||||
assert!(matches!(status(topology(TopologyFailure::Missing("the edge's curve"))), Error::Status(1006)));
|
||||
assert!(matches!(status(topology(TopologyFailure::InvalidValue("a zero normal"))), Error::Status(1007)));
|
||||
assert!(matches!(status(topology(TopologyFailure::Inconsistent("two outer loops"))), Error::Status(1005)));
|
||||
assert!(matches!(status(topology(TopologyFailure::DropFailed { status: 1010 })), Error::Status(1010)));
|
||||
let at =
|
||||
Failure::new("topology::vertexuse", TopologyFailure::Failed("x")).at(solidcore::EntityRef::Vertex(VertexId::from_index(2))).into_error();
|
||||
assert!(matches!(status(at), Error::Status(1001)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,13 @@ pub enum Error {
|
||||
/// (solidcore's fit, `SolidError::FilletRefused`): the size, what it sizes, a point at the
|
||||
/// middle of the edge it was found for, and why there is no room, when the refusal names them.
|
||||
TooLarge { size: f64, kind: FitKind, at: Option<[f64; 3]>, why: Option<RoomReason> },
|
||||
/// A corner's setback too short for its fill: solidcore's `BlendRefusal::SetbackTooShort`
|
||||
/// from the fill's test, the corner's fill not certified at the setback asked. `least` is
|
||||
/// the least setback solidcore's fill takes, `at` the corner's point when solidcore gives
|
||||
/// it. `builds`: solidcore's fill is the corner, so a setback of `least` builds it; where
|
||||
/// another patch replaces it (a G2 corner), `least` is only what solidcore's corner needs,
|
||||
/// not a setback shown to build.
|
||||
SetbackTooShort { least: f64, at: Option<[f64; 3]>, builds: bool },
|
||||
/// A shell, offset or thicken whose wall is thicker than a face curves: the face's offset
|
||||
/// folds (solidcore's `OffsetFailure::Folds`). The largest wall that works,
|
||||
/// when solidcore names one.
|
||||
@@ -192,6 +199,17 @@ impl std::fmt::Display for Error {
|
||||
}
|
||||
write!(f, "; the largest {} that fits is {size}", kind.word())
|
||||
}
|
||||
Error::SetbackTooShort { least, at, builds } => {
|
||||
write!(f, "the setback is too short for the corner's fill")?;
|
||||
if let Some([x, y, z]) = at {
|
||||
write!(f, " at ({x:.3}, {y:.3}, {z:.3})")?;
|
||||
}
|
||||
if *builds {
|
||||
write!(f, "; the smallest setback that builds is {least}")
|
||||
} else {
|
||||
write!(f, ": solidcore's corner needs a setback of at least {least}")
|
||||
}
|
||||
}
|
||||
Error::WallFolds { largest } => {
|
||||
write!(f, "the wall is thicker than a face curves, so the face's offset folds")?;
|
||||
match largest {
|
||||
|
||||
@@ -1062,7 +1062,7 @@ impl Dialog {
|
||||
let [rail, rail2] = crate::dialogs_sweep::rail_inputs();
|
||||
vec![
|
||||
SelInput::new("Profile", PROFILES, true),
|
||||
SelInput::new("Path", CURVES, true),
|
||||
SelInput::new("Path", CURVES | EDGES, true),
|
||||
crate::dialogs_sweep::direction_input(),
|
||||
rail,
|
||||
rail2,
|
||||
@@ -1636,6 +1636,18 @@ impl Dialog {
|
||||
Some(FaceSnap::new(s, body, *index, &near))
|
||||
}
|
||||
|
||||
/// The sketch a sweep's profile is in, when the active input is the sweep's path: a
|
||||
/// sweep's path may never be a curve of the profile's own sketch, so such a curve is
|
||||
/// never a valid pick for the path (#2216). `None` when the dialog is not a sweep's
|
||||
/// path input, or when the profile is not picked yet.
|
||||
fn sweep_path_profile_sketch(&self, s: &Session) -> Option<u64> {
|
||||
let sweep = matches!(self.kind, Kind::Sweep { .. }) || matches!(self.kind, Kind::Surface(crate::dialogs_surface::Surf::Sweep(_)));
|
||||
if !sweep || self.active != 1 {
|
||||
return None;
|
||||
}
|
||||
crate::dialogs_sweep::profile_sketch(&self.inputs.first()?.items, s.active_sketch)
|
||||
}
|
||||
|
||||
/// The selection a hit would add to the active input, given the face under the cursor
|
||||
/// ([`Dialog::face_snap`] of the same hits). A face place picked on or near a sketch point that
|
||||
/// lies on that face (a circle's centre) is taken at the point, as a points input picks it.
|
||||
@@ -1647,6 +1659,15 @@ impl Dialog {
|
||||
if self.auto.is_some() {
|
||||
return auto::candidate(s, h);
|
||||
}
|
||||
// A sweep's path is never a curve of the profile's own sketch: the command refuses such
|
||||
// a path ("the path curves must be in one other sketch"), so the pick does not stop on
|
||||
// one and falls through to the next hit, the body's edge under the cursor (#2216).
|
||||
if let Some(sketch) = self.sweep_path_profile_sketch(s)
|
||||
&& let Hit::SketchCurve { sketch: other, .. } = h
|
||||
&& *other == sketch
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if let Some(target) = self.extrude_target(s, h) {
|
||||
return Some(target);
|
||||
}
|
||||
@@ -5783,9 +5804,16 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
||||
need(0, "a profile")?;
|
||||
need(1, "the path")?;
|
||||
let (sketch, idx) = profiles();
|
||||
let path: Vec<String> = sels(d, 1).curve_ids();
|
||||
let path_sketch = picks_sketch(app, sels(d, 1), sketch.as_u64()).ok_or("the path curves must be in one sketch")?;
|
||||
let mut p = json!({"sketch": sketch, "profiles": idx, "path_sketch": path_sketch, "path": path, "operation": OPS.get(*operation).copied().unwrap_or("new")});
|
||||
let mut p = json!({"sketch": sketch, "profiles": idx, "operation": OPS.get(*operation).copied().unwrap_or("new")});
|
||||
// The path is a chain of a body's edges or the curves of a sketch (#2216).
|
||||
if let Some((body, edges)) = crate::dialogs_sweep::edge_path(sels(d, 1))? {
|
||||
p["path_body"] = json!(body);
|
||||
p["path_edges"] = json!(edges);
|
||||
} else {
|
||||
let path: Vec<String> = sels(d, 1).curve_ids();
|
||||
p["path_sketch"] = json!(picks_sketch(app, sels(d, 1), sketch.as_u64()).ok_or("the path curves must be in one sketch")?);
|
||||
p["path"] = json!(path);
|
||||
}
|
||||
crate::dialogs_sweep::params(app, opts, sels(d, 2).first(), [sels(d, 3), sels(d, 4)], &mut p)?;
|
||||
("solid.sweep", p)
|
||||
}
|
||||
@@ -7710,7 +7738,22 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
}
|
||||
d
|
||||
}
|
||||
FeatureKind::Sweep { sketch, profiles, path_sketch, path, operation, targets, orientation, twist, scale, rails, rail_fit, .. } => {
|
||||
FeatureKind::Sweep {
|
||||
sketch,
|
||||
profiles,
|
||||
path_sketch,
|
||||
path,
|
||||
path_body,
|
||||
path_edges,
|
||||
operation,
|
||||
targets,
|
||||
orientation,
|
||||
twist,
|
||||
scale,
|
||||
rails,
|
||||
rail_fit,
|
||||
..
|
||||
} => {
|
||||
let mut d = start("solid.sweep")?;
|
||||
let (opts, direction) = crate::dialogs_sweep::SweepOpts::of(orientation, twist, scale, *rail_fit, &feature_frame(s, f.component));
|
||||
d.kind = Kind::Sweep { operation: op_index(operation), opts };
|
||||
@@ -7725,7 +7768,12 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
inp.items = items.into_iter().map(|index| Sel::Profile { sketch: sketch.0, index }).collect();
|
||||
}
|
||||
if let Some(inp) = d.inputs.get_mut(1) {
|
||||
inp.items = path.iter().map(|id| crate::selection::curve_pick(id, path_sketch.0, s.active_sketch)).collect();
|
||||
// The path is a chain of a body's edges or the curves of a sketch (#2216).
|
||||
// A body's edge is looked for on the body the path was picked on (#2216).
|
||||
inp.items = match path_body {
|
||||
Some(body) => path_edges.iter().filter_map(|p| edge_sel_on(s, Some(body.0.as_str()), p.0)).collect(),
|
||||
None => path.iter().map(|id| crate::selection::curve_pick(id, path_sketch.0, s.active_sketch)).collect(),
|
||||
};
|
||||
}
|
||||
if !targets.is_empty() {
|
||||
d.extra.insert("targets".into(), json!(targets));
|
||||
|
||||
@@ -229,7 +229,7 @@ fn inputs(k: &Surf) -> Vec<SelInput> {
|
||||
let [rail, rail2] = crate::dialogs_sweep::rail_inputs();
|
||||
vec![
|
||||
SelInput::new("Profiles or curves", PROFILES | CURVES, true),
|
||||
SelInput::new("Path", CURVES, true),
|
||||
SelInput::new("Path", CURVES | EDGES, true),
|
||||
crate::dialogs_sweep::direction_input(),
|
||||
rail,
|
||||
rail2,
|
||||
@@ -580,11 +580,17 @@ pub fn commands(app: &SolveApp, k: &Surf, inputs: &[SelInput], extra: &Map<Strin
|
||||
need(0, "profiles or curves to sweep")?;
|
||||
need(1, "the path")?;
|
||||
let mut p = sheet_picks(app, items(inputs, 0))?;
|
||||
let path: Vec<String> = items(inputs, 1).curve_ids();
|
||||
p["path_sketch"] = json!(
|
||||
crate::dialogs::picks_sketch(app, items(inputs, 1), p["sketch"].as_u64()).ok_or("the path curves must be in one other sketch")?
|
||||
);
|
||||
p["path"] = json!(path);
|
||||
// The path is a chain of a body's edges or the curves of a sketch (#2216).
|
||||
if let Some((body, edges)) = crate::dialogs_sweep::edge_path(items(inputs, 1))? {
|
||||
p["path_body"] = json!(body);
|
||||
p["path_edges"] = json!(edges);
|
||||
} else {
|
||||
let path: Vec<String> = items(inputs, 1).curve_ids();
|
||||
p["path_sketch"] = json!(
|
||||
crate::dialogs::picks_sketch(app, items(inputs, 1), p["sketch"].as_u64()).ok_or("the path curves must be in one other sketch")?
|
||||
);
|
||||
p["path"] = json!(path);
|
||||
}
|
||||
crate::dialogs_sweep::params(app, opts, items(inputs, 2).first(), [items(inputs, 3), items(inputs, 4)], &mut p)?;
|
||||
("surface.sweep", p)
|
||||
}
|
||||
@@ -930,14 +936,33 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
|
||||
}
|
||||
return Some((Kind::Surface(k), ins, extra));
|
||||
}
|
||||
FeatureKind::SurfaceSweep { sketch, profiles, curves, path_sketch, path, orientation, twist, scale, rails, rail_fit, .. } => {
|
||||
FeatureKind::SurfaceSweep {
|
||||
sketch,
|
||||
profiles,
|
||||
curves,
|
||||
path_sketch,
|
||||
path,
|
||||
path_body,
|
||||
path_edges,
|
||||
orientation,
|
||||
twist,
|
||||
scale,
|
||||
rails,
|
||||
rail_fit,
|
||||
..
|
||||
} => {
|
||||
let (opts, direction) = crate::dialogs_sweep::SweepOpts::of(orientation, twist, scale, *rail_fit, frame);
|
||||
let [rail, rail2] = crate::dialogs_sweep::rail_sels(rails, s.active_sketch);
|
||||
(
|
||||
Surf::Sweep(opts),
|
||||
vec![
|
||||
sheet_sels(app, sketch.0, profiles.as_ref(), curves),
|
||||
path.iter().map(|id| crate::selection::curve_pick(id, path_sketch.0, s.active_sketch)).collect(),
|
||||
// The path is a chain of a body's edges or the curves of a sketch (#2216).
|
||||
// A body's edge is looked for on the body the path was picked on (#2216).
|
||||
match path_body {
|
||||
Some(body) => path_edges.iter().filter_map(|p| crate::dialogs::edge_sel_on(s, Some(body.0.as_str()), p.0)).collect(),
|
||||
None => path.iter().map(|id| crate::selection::curve_pick(id, path_sketch.0, s.active_sketch)).collect(),
|
||||
},
|
||||
direction.into_iter().collect(),
|
||||
rail,
|
||||
rail2,
|
||||
|
||||
@@ -94,6 +94,45 @@ pub fn rail_inputs() -> [SelInput; 2] {
|
||||
[SelInput::new("Rail", CURVES, true), SelInput::new("Second Rail", CURVES, true)]
|
||||
}
|
||||
|
||||
/// The path picked as a chain of a body's edges: the body and a point on each edge (#2216).
|
||||
/// `None` when the path is the curves of a sketch, which is the other way a path is picked.
|
||||
pub fn edge_path(picks: &[Sel]) -> Result<Option<(String, Vec<Value>)>, String> {
|
||||
let edges: Vec<&Sel> = picks.iter().filter(|x| matches!(x, Sel::Edge { .. })).collect();
|
||||
if edges.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
if edges.len() != picks.len() {
|
||||
return Err("the path is either the curves of a sketch or the edges of a body".into());
|
||||
}
|
||||
let Some(Sel::Edge { body, .. }) = edges.first().copied() else { return Ok(None) };
|
||||
if edges.iter().any(|x| !matches!(x, Sel::Edge { body: b, .. } if b == body)) {
|
||||
return Err("the path's edges must be of one body".into());
|
||||
}
|
||||
let points =
|
||||
edges.iter().filter_map(|x| if let Sel::Edge { point, .. } = x { Some(crate::selection::point_value(*point)) } else { None }).collect();
|
||||
Ok(Some((body.clone(), points)))
|
||||
}
|
||||
|
||||
/// The sketch a sweep's profile is in, when the profile is picked: the sketch of the
|
||||
/// profile's regions, or the sketch of the curves picked as the profile. `None` when the
|
||||
/// profile is not picked yet, or when it is in two sketches at once.
|
||||
pub fn profile_sketch(items: &[Sel], active: Option<u64>) -> Option<u64> {
|
||||
let mut sketch: Option<u64> = None;
|
||||
for x in items {
|
||||
let sk = match x {
|
||||
Sel::Profile { sketch, .. } => Some(*sketch),
|
||||
Sel::SketchCurve { sketch, .. } => (*sketch).or(active),
|
||||
_ => None,
|
||||
};
|
||||
let Some(sk) = sk else { continue };
|
||||
if sketch.is_some_and(|s| s != sk) {
|
||||
return None;
|
||||
}
|
||||
sketch = Some(sk);
|
||||
}
|
||||
sketch
|
||||
}
|
||||
|
||||
/// A feature's stored rails as the rail inputs' selections (`active`: the sketch being edited).
|
||||
pub fn rail_sels(rails: &[SweepRail], active: Option<u64>) -> [Vec<Sel>; 2] {
|
||||
let sels =
|
||||
|
||||
@@ -1678,6 +1678,9 @@ impl SolveApp {
|
||||
if v.get("continuous").and_then(Value::as_bool) != Some(true) {
|
||||
self.session.echo(language.format("ui.command_done", &[("command", label)]));
|
||||
}
|
||||
if id == "sketch.move" {
|
||||
released_status(self, language, v);
|
||||
}
|
||||
if id == "file.new" {
|
||||
// `file.new` starts from millimetres. Units of new designs still apply (#1205).
|
||||
prefs::apply_new_design(self);
|
||||
@@ -3882,6 +3885,24 @@ fn finite_input(raw: &mut egui::RawInput) {
|
||||
});
|
||||
}
|
||||
|
||||
/// After a sketch Move that let go of constraints tying the moved geometry to what stays (a
|
||||
/// corner on the origin; #2980): the status line names them, and its Undo button undoes the move.
|
||||
fn released_status(app: &mut SolveApp, language: solidcraft_ui_text::Locale, v: &Value) {
|
||||
let Some(released) = v.get("released").and_then(Value::as_array).filter(|r| !r.is_empty()) else { return };
|
||||
let mut names: Vec<&str> = Vec::new();
|
||||
for kind in released.iter().filter_map(|c| c.get("kind").and_then(Value::as_str)).take(64) {
|
||||
let name = language.label(if kind.ends_with("Dimension") { "Dimension" } else { kind });
|
||||
if !names.contains(&name) {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
let count = released.len().to_string();
|
||||
let id = if released.len() == 1 { "sketch.move_released_one" } else { "sketch.move_released" };
|
||||
let msg = language.format(id, &[("count", &count), ("names", &names.join(", "))]);
|
||||
app.set_status(msg, false);
|
||||
app.status_undo = true;
|
||||
}
|
||||
|
||||
/// An edit in place command's result, saying when it waits for the end of the agent's turn.
|
||||
fn with_queued(mut result: Value, queued: bool) -> Value {
|
||||
if queued && let Some(o) = result.as_object_mut() {
|
||||
|
||||
@@ -551,6 +551,31 @@ fn a_plain_move_takes_the_geometry_with_it() {
|
||||
let ys: Vec<f64> = sk.points.iter().filter(|p| corners.contains(&p.id)).map(|p| p.pos.y).collect();
|
||||
assert_eq!(ys.len(), 4, "{ys:?}");
|
||||
assert!(ys.iter().all(|y| (*y - 15.0).abs() < 1e-6 || (*y - 25.0).abs() < 1e-6), "the rectangle moved up by 5 mm: {ys:?}");
|
||||
assert!(!h.app.status_undo, "nothing tied it to what stays, so nothing is let go: {:?}", h.app.status);
|
||||
}
|
||||
|
||||
/// #2980: a rectangle drawn from the origin moves whole, and the status line says its corner's
|
||||
/// coincidence with the origin was let go; its Undo button undoes the move.
|
||||
#[test]
|
||||
fn a_move_that_lets_go_of_the_origin_says_so_and_offers_undo() {
|
||||
let mut h = rectangle();
|
||||
let sid = h.app.session.active_sketch.unwrap();
|
||||
h.step(&json!({"start": "sketch.move"})).unwrap();
|
||||
let picks: Vec<Sel> = curve_ids(&h).iter().map(|id| Sel::SketchCurve { id: id.clone(), sketch: None }).collect();
|
||||
{
|
||||
let d = h.app.dialog.as_mut().unwrap();
|
||||
d.inputs[0].items = picks;
|
||||
d.kind = Kind::SketchModify(Mod::Move { x: "15 mm".into(), y: "0 mm".into(), angle: "0 deg".into(), copy: false });
|
||||
}
|
||||
h.frames(2);
|
||||
h.step(&json!({"key": "Enter"})).unwrap();
|
||||
h.settle();
|
||||
let (msg, _, error) = h.app.status.clone().expect("the move says what it let go");
|
||||
assert!(!error, "{msg}");
|
||||
assert!(msg.contains("1 constraint") && msg.contains("Coincident"), "{msg}");
|
||||
assert!(h.app.status_undo, "the message offers Undo");
|
||||
let xs: Vec<f64> = h.app.session.model.state().sketch(sid).unwrap().sketch.points.iter().filter(|p| p.id != "origin").map(|p| p.pos.x).collect();
|
||||
assert!(xs.iter().all(|x| (*x - 15.0).abs() < 1e-6 || (*x - 35.0).abs() < 1e-6), "the rectangle moved whole: {xs:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -101,6 +101,41 @@ fn editing_sweeps_keeps_their_options() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #2216: the path of a sweep is the path the user chose: an edge of a body can be the path, as
|
||||
/// a sketch's curve is. The pick goes into the Path input and the sweep is built with it.
|
||||
#[test]
|
||||
fn a_sweep_takes_a_body_edge_as_its_path() {
|
||||
let mut h = Harness::new();
|
||||
h.app.set_origin_shown(false);
|
||||
// A block whose upright corner edge is the path, and a square profile at its foot.
|
||||
h.app.run("solid.box", json!({"length": 40, "width": 20, "height": 10, "corner": [-40, 0, 0]})).unwrap();
|
||||
h.app.run("sketch.create", json!({"plane":"XY","name":"Q"})).unwrap();
|
||||
h.app.run("sketch.rectangle.two_point", json!({"p0":[-5,-5],"p1":[5,5]})).unwrap();
|
||||
h.app.run("sketch.finish", json!({})).unwrap();
|
||||
let sketch = h.app.session.doc.find_feature("Q").unwrap().id;
|
||||
h.step(&json!({"call":"ui.start","params":{"command":"solid.sweep"}})).unwrap();
|
||||
h.frames(3);
|
||||
let d = h.app.dialog.as_mut().unwrap();
|
||||
d.inputs[0].items = vec![Sel::Profile { sketch, index: 0 }];
|
||||
// The Path input is the one the click goes to.
|
||||
d.active = 1;
|
||||
let Kind::Sweep { operation, .. } = &mut d.kind else { panic!("{:?}", d.kind) };
|
||||
*operation = 0;
|
||||
h.frames(2);
|
||||
// The corner edge of the block, where the cursor is.
|
||||
let at = h.at(&json!({"world": [0.0, 0.0, 5.0]})).unwrap();
|
||||
h.step(&json!({"move": at})).unwrap();
|
||||
h.step(&json!({"click": at})).unwrap();
|
||||
let path = h.app.dialog.as_ref().unwrap().inputs[1].items.clone();
|
||||
assert!(matches!(path.as_slice(), [Sel::Edge { .. }]), "the path took the edge: {path:?}");
|
||||
h.step(&json!({"widget":"OK"})).unwrap();
|
||||
let refused = h.app.dialog.as_ref().and_then(|d| d.error.clone());
|
||||
assert!(refused.is_none(), "the sweep was refused: {refused:?}");
|
||||
assert!(h.app.dialog.is_none(), "{:?}", h.app.session.log);
|
||||
let f = h.app.session.doc.features.last().unwrap();
|
||||
assert!(matches!(f.kind, FeatureKind::Sweep { .. }), "{:?}", f.kind);
|
||||
}
|
||||
|
||||
/// A rail picked in the dialog reaches the command with its sketch, the fit with it; editing the
|
||||
/// sweep brings both back and keeps them.
|
||||
#[test]
|
||||
@@ -140,3 +175,26 @@ fn a_rail_picked_in_the_dialog_is_kept_through_an_edit() {
|
||||
assert!(h.app.dialog.is_none(), "{:?}", h.app.session.log);
|
||||
assert_eq!(rails(&h), (stored, fit));
|
||||
}
|
||||
|
||||
/// #2216: a sweep whose path is a body's edge comes back on the body it was picked on, even
|
||||
/// when another body has an edge at the same place: the path must not change on an edit.
|
||||
#[test]
|
||||
fn a_sweep_path_edge_is_restored_on_the_body_it_was_picked_on() {
|
||||
let mut h = Harness::new();
|
||||
// Two blocks that share the upright edge at (0, 20): the path is on the second one.
|
||||
h.app.run("solid.box", json!({"length": 40, "width": 20, "height": 10, "corner": [-40, 0, 0], "body_name": "B"})).unwrap();
|
||||
h.app.run("solid.box", json!({"length": 40, "width": 20, "height": 10, "corner": [-40, 20, 0], "body_name": "A"})).unwrap();
|
||||
// A square profile at the origin.
|
||||
h.app.run("sketch.create", json!({"plane":"XY","name":"Q"})).unwrap();
|
||||
h.app.run("sketch.rectangle.two_point", json!({"p0":[-5,-5],"p1":[5,5]})).unwrap();
|
||||
h.app.run("sketch.finish", json!({})).unwrap();
|
||||
// The sweep's path is the shared edge, on the body A.
|
||||
h.app.run("solid.sweep", json!({"sketch":"Q","path_body":"A","path_edges":[[0,20,5]],"operation":"new","name":"Swept"})).unwrap();
|
||||
h.step(&json!({"call":"ui.editFeature","params":{"feature":"Swept"}})).unwrap();
|
||||
h.frames(8);
|
||||
let path = h.app.dialog.as_ref().unwrap().inputs[1].items.clone();
|
||||
match path.as_slice() {
|
||||
[Sel::Edge { body, .. }] => assert_eq!(body, "A", "the path was restored on another body"),
|
||||
other => panic!("the path was not restored as an edge: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,7 +650,7 @@ outlasted its wait can be cancelled from the connection that sent it: 60 s, or 3
|
||||
| `engine.commands` | — | every command: id, label, tab, panel, icon, shortcut (the user's current first key, or null when cleared), params doc, enabled |
|
||||
| `document.revision` | — | `{document_id, revision}`: the active design's id and a number that moves whenever the design changes, for programs that follow it ([Following the design](#following-the-design)) |
|
||||
| `control.subscribe` | — | `{document_id, revision}`, as `document.revision`; the connection is then sent a `design.changed` line whenever they change ([Following the design](#following-the-design)) |
|
||||
| `document.inspect` | `measure?`: bool, `sketch_counts?`: bool (default false) | parameters, timeline (errors, timings), bodies, sketches, selection (assembly joints: `engine.execute` `joint.inspect {joint?}`, which reads only: each joint's two origins with the occurrence, the snap as saved, the feature, face, edge or sketch curve its names refer to, and whether it resolves or why not; kind, offset, angle, flip, values, limits, expressions and motion links); with `sketch_counts` each sketch also gives its `plane` and its number of `points` and `constraints` |
|
||||
| `document.inspect` | `measure?`: bool, `sketch_counts?`: bool (default false), `timeline?`: `true` (default), `"summary"` (each feature as `id`, `name`, `type`, with `error`, `warning`, `suppressed` and `rolled_back` only when set) or `false` (left out), `bodies?`: `true` (default), `"names"` (only the body names, nothing measured) or `false` (left out) | parameters, timeline (errors, timings), bodies, sketches, selection (assembly joints: `engine.execute` `joint.inspect {joint?}`, which reads only: each joint's two origins with the occurrence, the snap as saved, the feature, face, edge or sketch curve its names refer to, and whether it resolves or why not; kind, offset, angle, flip, values, limits, expressions and motion links); with `sketch_counts` each sketch also gives its `plane` and its number of `points` and `constraints` |
|
||||
| `mcp.options` | — | `{"compact": bool}`: the output the MCP server bridged to this app should give its agents (Preferences › AI › Agents through MCP, `ui.prefs` `mcp_compact`, default false; [MCP](mcp.md#compact-output)) |
|
||||
| `document.export` | — | `{name, revision, dirty, format, text}`: the active design as its file holds it, for a host that keeps the files itself ([browser command API](browser-api.md)). `text` is the design's JSON, what a `.json` design file holds; `format` is `"json"`; `revision` is the one `document.revision` answers for this state. Only reads |
|
||||
| `document.import` | `text`: a design's JSON; `name?`: the name it takes | `{name, features, errors, revision}`: replaces the active design with `text`, timeline and parameters as saved; it opens saved, with a new `document_id` and nothing to undo. It does not ask about unsaved changes in the design it replaces: that is the caller's to decide. A text that does not read changes nothing. Waits behind a file loading or a design change, and evaluates on the window thread in the frame it runs |
|
||||
|
||||
+5
-5
@@ -172,8 +172,8 @@ The command line can only turn it on: with the preference on, a server started w
|
||||
contents: a sketch of 720 points is some 4 KB in the summary and over 80 KB in full. `sketches`
|
||||
takes a list of sketch names or ids to return in full (as `execute` `sketch.inspect` gives
|
||||
them), `true` for all of them, or `false` / `"summary"` (the default). `bodies` (a list of
|
||||
names) keeps only those bodies and measures only them. An unknown sketch or body name is an
|
||||
error that lists the names there are.
|
||||
names) keeps only those bodies and measures only them; `"names"` and `false` work as without
|
||||
compact output. An unknown sketch or body name is an error that lists the names there are.
|
||||
- Tool results that are JSON are compact, not pretty-printed.
|
||||
|
||||
```sh
|
||||
@@ -326,7 +326,7 @@ The `skill_reference` tool returns these documents from the copies built into So
|
||||
| `list_commands` | `filter?`, `prefix?`, `panel?`, `compact?`, `fields?`, `limit?`, `offset?` | The command catalog: id, label, tab/panel, shortcut, parameter docs, enabled now. `filter` matches id, label, tab and panel. `prefix` keeps the ids that start with it (`"sketch."`) and `panel` the commands of one whole toolbar panel or tab (`"Modify"`), both ignoring case. `compact: true` keeps only `id` and `label` of each entry, about an eighth of the size; `fields` keeps the keys it lists instead. `limit` and `offset` page the matches, and the reply becomes `{commands, total, offset, next_offset}` (`next_offset` is null on the last page). With none of these the reply is the whole catalog, about 350 KB. |
|
||||
| `describe_command` | `id` | One command's parameter docs (`params`) and, for commands with a typed contract (every new command, and more of the older ones as they are converted), the JSON Schemas (2020-12) of its parameters (`input`) and of a successful result (`output`); `null` for the others. The same as `execute` `engine.describe {command}`. |
|
||||
| `execute` | `command`, `params?`, `verbose?` (default true), `expect?` | Run any command with JSON parameters (`list_commands` shows them). Failures leave the design unchanged. A feature command replies with the feature (`feature`, `name`), every body (`bodies`) and the bodies it created, changed and removed (`created_bodies`, `changed_bodies`, `removed_bodies`). `verbose: false` uses the same compact reply as `batch`. `expect` checks the design after the command, as it does for `batch` (see below). |
|
||||
| `batch` | `commands: [{command, params}]` or `path` (a script file, see below), `rollback?` (default true), `verbose?` (default true), `timings?`, `profile?`, `coalesce_parameters?` (default false), `expect?` | Run steps in order, stop at the first error and undo the completed steps. The reply names the failing step (`failed.index`, `failed.command`, `failed.error`), every result before it, `rolled_back` (true only when a rollback was asked for and the design is where the batch started: its completed steps were undone, or nothing ran; false when `rollback: false` or when the design could not go back), `undone`, `applied` (the indices of the steps whose changes are in the design) and `not_run` (the steps never sent). A step may name a tool instead of a command, with that tool's arguments as `params`: `set_parameter` (runs `parameters.change`; `value` or `expression`), `list_commands` (`engine.commands`, narrowed by its arguments as the tool narrows it), `describe_command` (`engine.describe`), `execute` (`{command, params}`), `measure` (`summary: true` summarised as the tool summarises it), `compare_picture` (`inspect.compare_picture`, numbers only: no overlay PNG), `inspect_design` (`document.inspect`: each sketch as its counts, not its full contents, and no joints), `undo`, `redo`, `export`, `save`, `open`, `new_design` and `crash_reports`. `screenshot`, `body_topology` (use `model.edges` and `model.faces`), `batch`, `check_parameters`, `pending`, `running` and `skill_reference` can't be steps, nor can `component.edit_in_place` and `component.finish_edit`, however a step names them, as they go to another design's tab (send them with the `execute` tool on their own): such a batch fails before anything is sent. So does a step the batch would certainly refuse at its turn (#5853): a step without a command, an unknown command id (the error names the closest ids) or, with `--param-check strict`, parameters that break a command's contract. A headless session checks this itself; an app checks it against its own commands when it gets the batch, and a bridged server at `--param-check strict` checks the contracts before it sends the batch. `verbose: false` keeps the reply small in large designs: feature steps leave out the full body list (`created_bodies`, `changed_bodies`, `removed_bodies` stay) and sketch steps the ids they repeat outside `created`. Bridged to an app that has `engine.batch`, the whole batch is one control request (one frame of the app). Older apps, and apps showing the agent cursor, get the steps one by one. `timings: true` adds `timings: [{index, command, ms}]` for every step run, the failing one included, and `total_ms`. `profile: true` also adds each step's `profile: {boolean_ms, tessellate_ms, other_ms}`, its time in kernel booleans, in tessellation and the rest. Steps are timed where they run: in the app when bridged. An app without `engine.batch` is timed step by step, without `profile`. `coalesce_parameters: true` sets each run of consecutive plain parameter edits (`parameters.change` or `set_parameter` with a name and a value, or `changes`) together, so the design rebuilds once per run instead of once per edit. Steps between runs see the design up to date. Each step keeps its own result (`coalesced`: the run's length), and the run is one undo step. The run skips the states between its edits, so an edit refused on its own can go through with the run's later ones. A refused run goes step by step, naming the refused step. |
|
||||
| `batch` | `commands: [{command, params}]` or `path` (a script file, see below), `rollback?` (default true), `verbose?` (default true), `timings?`, `profile?`, `coalesce_parameters?` (default false), `expect?` | Run steps in order, stop at the first error and undo the completed steps. The reply names the failing step (`failed.index`, `failed.command`, `failed.error`), every result before it, `rolled_back` (true only when a rollback was asked for and the design is where the batch started: its completed steps were undone, or nothing ran; false when `rollback: false` or when the design could not go back), `undone`, `applied` (the indices of the steps whose changes are in the design) and `not_run` (the steps never sent). A step may name a tool instead of a command, with that tool's arguments as `params`: `set_parameter` (runs `parameters.change`; `value` or `expression`), `list_commands` (`engine.commands`, narrowed by its arguments as the tool narrows it), `describe_command` (`engine.describe`), `execute` (`{command, params}`), `measure` (`summary: true` summarised as the tool summarises it), `compare_picture` (`inspect.compare_picture`, numbers only: no overlay PNG), `inspect_design` (`document.inspect` with its `measure`, `timeline` and `bodies`: each sketch as its counts, not its full contents, and no joints), `undo`, `redo`, `export`, `save`, `open`, `new_design` and `crash_reports`. `screenshot`, `body_topology` (use `model.edges` and `model.faces`), `batch`, `check_parameters`, `pending`, `running` and `skill_reference` can't be steps, nor can `component.edit_in_place` and `component.finish_edit`, however a step names them, as they go to another design's tab (send them with the `execute` tool on their own): such a batch fails before anything is sent. So does a step the batch would certainly refuse at its turn (#5853): a step without a command, an unknown command id (the error names the closest ids) or, with `--param-check strict`, parameters that break a command's contract. A headless session checks this itself; an app checks it against its own commands when it gets the batch, and a bridged server at `--param-check strict` checks the contracts before it sends the batch. `verbose: false` keeps the reply small in large designs: feature steps leave out the full body list (`created_bodies`, `changed_bodies`, `removed_bodies` stay) and sketch steps the ids they repeat outside `created`. Bridged to an app that has `engine.batch`, the whole batch is one control request (one frame of the app). Older apps, and apps showing the agent cursor, get the steps one by one. `timings: true` adds `timings: [{index, command, ms}]` for every step run, the failing one included, and `total_ms`. `profile: true` also adds each step's `profile: {boolean_ms, tessellate_ms, other_ms}`, its time in kernel booleans, in tessellation and the rest. Steps are timed where they run: in the app when bridged. An app without `engine.batch` is timed step by step, without `profile`. `coalesce_parameters: true` sets each run of consecutive plain parameter edits (`parameters.change` or `set_parameter` with a name and a value, or `changes`) together, so the design rebuilds once per run instead of once per edit. Steps between runs see the design up to date. Each step keeps its own result (`coalesced`: the run's length), and the run is one undo step. The run skips the states between its edits, so an edit refused on its own can go through with the run's later ones. A refused run goes step by step, naming the refused step. |
|
||||
|
||||
**`expect`** on `batch` and `execute` turns the verify step into part of the build. An agent
|
||||
computes the volume, body count or bounding box a stage should produce before building it;
|
||||
@@ -347,11 +347,11 @@ refused before anything runs. `expect` is an argument of the tool, not a key of
|
||||
carrying one is treated as any step with a key the batch does not know. Volume and body count
|
||||
say nothing about which kernel built the design, which is the point: the check holds whatever
|
||||
builds it.
|
||||
| `inspect_design` | `measure?` (default true), `sketches?` (default true), `joints?` (default true); with [compact output](#compact-output) `sketches?` (names or ids, `true`, or `false` / `"summary"`: the default) and `bodies?` | Design properties (`metadata`: project, author… and other tools' keys; set them with `execute` `document.metadata.set`), parameters, timeline (type, suppressed, rolled back, error, warning, timing), bodies (volume, area, centre of mass, bbox, face/edge/vertex counts, visible; `removed_later: true` while a sketch is being edited on a rolled-back timeline and a later feature deletes the body, which is then not shown), sketches (curves, points, constraints, profiles, DOF, solve status, visible), joints when the design has any (as `joint.inspect` gives them: each origin's occurrence, component and path, the snap as saved, the feature, face, edge or sketch curve (`sketch: {id, name, curve}`) its persistent names refer to, `resolves` and the `error` when it is lost; kind, offset, angle, flip, values, limits, expressions, motion links; `execute` `joint.inspect {joint}` gives one joint), selection, undo depth. Hide helper sketches, bodies or construction planes with `execute` `browser.visibility` (`{items, visible}` or `{folder, visible}`), and joint glyphs with `{joints: [id or name], visible}` or `{folder: "joints", visible}` (a hidden joint still constrains and drives; `joint.list` reports each joint's `visible`). |
|
||||
| `inspect_design` | `measure?` (default true), `sketches?` (default true), `joints?` (default true), `timeline?` (`true`: the default; `"summary"`: each feature as `id`, `name` and `type`, with `error`, `warning`, `suppressed` and `rolled_back` only when it has them; `false`: no timeline), `bodies?` (`true`: the default; `"names"`: only the body names, nothing measured; `false`: no bodies); with [compact output](#compact-output) `sketches?` (names or ids, `true`, or `false` / `"summary"`: the default) and `bodies?` also takes a list of names | On a design of hundreds of features and bodies, `timeline: "summary"` and `bodies: "names"` (or `false`) keep the reply small enough for an agent's tool output. Design properties (`metadata`: project, author… and other tools' keys; set them with `execute` `document.metadata.set`), parameters, timeline (type, suppressed, rolled back, error, warning, timing), bodies (volume, area, centre of mass, bbox, face/edge/vertex counts, visible; `removed_later: true` while a sketch is being edited on a rolled-back timeline and a later feature deletes the body, which is then not shown), sketches (curves, points, constraints, profiles, DOF, solve status, visible), joints when the design has any (as `joint.inspect` gives them: each origin's occurrence, component and path, the snap as saved, the feature, face, edge or sketch curve (`sketch: {id, name, curve}`) its persistent names refer to, `resolves` and the `error` when it is lost; kind, offset, angle, flip, values, limits, expressions, motion links; `execute` `joint.inspect {joint}` gives one joint), selection, undo depth. Hide helper sketches, bodies or construction planes with `execute` `browser.visibility` (`{items, visible}` or `{folder, visible}`), and joint glyphs with `{joints: [id or name], visible}` or `{folder: "joints", visible}` (a hidden joint still constrains and drives; `joint.list` reports each joint's `visible`). |
|
||||
| `measure` | `body?` or `items?`, `summary?` (default false) | Area (mm²), bbox, topology counts and `closed` per body. Volume (mm³), volume centre of mass, density and mass are reported only for closed, consistently oriented bodies (null or omitted for open surfaces). Volume/mass totals skip open bodies; `total.open_bodies_skipped` gives their count. Area and topology totals include all bodies. With `items` (one or two selections as `execute` takes them: `face`, `edge`, `vertex`, `sketch_curve`, `profile`), the picked geometry instead: faces and edges need only `type`, `body` and either `point` or `index` (from `model.faces` / `model.edges`). See below. `summary: true` keeps one short object per body (`name`, `volume_mm3`, `bbox: {min, max}`, `faces`, `closed`; a body that could not be measured keeps its `error`), `body_count` and `total.volume_mm3`, nothing else: what a stage check against computed numbers needs, at a fraction of the size. It applies to the bodies reply, not to `items`. |
|
||||
| `body_topology` | `body`, `faces?` (`all`, `cylinders`; default `all`) | Edges (index, midpoint, length, ends) and faces (index, name, area, centroid, normal, `point`, and for a cylindrical face its `cylinder`: `origin`, `axis`, `radius`). An edge midpoint is the `[x,y,z]` that picks that edge for fillet and chamfer. A face's `point` lies on it and picks it (its centroid may not: that of a face with a hole is in the hole); a face's `name` picks it for the commands that take faces (`face.delete`, `solid.shell`, `solid.draft`, `solid.offset_face`, `solid.split_face`, `solid.replace_face`, and `face` of `sheet.convert`). `faces: "cylinders"` replies `cylinders` instead of `edges` and `faces`: one entry per cylinder, `{radius, origin, axis, extent, names}`, the halves the kernel splits a cylinder into merged (same radius, same axis line; `origin` and `axis` as the first half's `cylinder` gives them), `extent` the `[min, max]` distance along `axis` from `origin` that the body's points on the cylinder span (the faces' `point`s and the ends of the edges lying on it; left out when none were found), and `names` the merged faces' names, any of which picks the face. Enough to check where every hole and boss is, in a few lines instead of the whole topology. |
|
||||
| `set_parameter` | `name`, `value` or `expression` (number or expression), `unit?`, `comment?` | Create or change a user parameter and recompute. `expression` is another name for `value` (the one `parameters.change` uses): give one of them, or both only when they agree; neither, or two that differ, is an error. Reports `recomputed` (features rebuilt), `restored` (features whose earlier result for the same inputs was reused, e.g. a value set back) and features that now fail. An expression `new_name = expression` renames the parameter as it sets it; in any other tool's expression parameter it adds the user parameter `new_name` and the input uses it (`defined`, `definitions` in the reply). |
|
||||
| `check_parameters` | `set: {name: value}` (a number or an expression each, as `set_parameter`'s `value`), `report?` (`measure`, `cylinders`, `errors`; default all three) | Try parameter values without keeping them: one `edit.checkpoint`, every value set in one `parameters.change {changes}` (one rebuild; an app without `changes` gets one change per name), the reports read, then `edit.restore_checkpoint`, so the design, its parameters and its undo history are exactly as before. The reply has `values` (each parameter's value with the tried expressions in), `recomputed`, `measure` (volume, bbox, bodies and faces per body, as the `measure` tool reports them), `errors` and `warnings` (the features that fail or warn: `{feature, name, error}`) with `parameter_errors` (the parameters that don't evaluate), `cylinders` (every cylindrical face of every body as `{body, radius, origin, axis, faces, name, area}`; the halves a kernel splits one cylinder into are one entry, `origin` is the axis point closest to the world origin and `axis` points so that its first non-zero component is positive) and `restored: true`. A name the design has no parameter of, a value that is not a number or a string, an expression that does not evaluate or an unknown report item is an error, and nothing changes. If the checkpoint cannot be restored the call is an error with `restored: false`, `restore_error` and a note that the tried values are still in the design: set them back before saving. Replaces the loop `set_parameter`, `measure`, `body_topology`, `set_parameter` back, which cost every agent session three to five calls and up to 16 s of rebuilds. Not a `batch` step. |
|
||||
| `check_parameters` | `set: {name: value}` (a number or an expression each, as `set_parameter`'s `value`), `report?` (`measure`, `cylinders`, `errors`; default all three), `bodies?` (a list of body names: only those are measured and their cylinders listed; default all), `summary?` (`measure` as the `measure` tool's `summary: true`; default false) | Try parameter values without keeping them: one `edit.checkpoint`, every value set in one `parameters.change {changes}` (one rebuild; an app without `changes` gets one change per name), the reports read, then `edit.restore_checkpoint`, so the design, its parameters and its undo history are exactly as before. The reply has `values` (each parameter's value with the tried expressions in), `recomputed`, `measure` (volume, bbox, bodies and faces per body, as the `measure` tool reports them), `errors` and `warnings` (the features that fail or warn: `{feature, name, error}`) with `parameter_errors` (the parameters that don't evaluate), `cylinders` (every cylindrical face of every body as `{body, radius, origin, axis, faces, name, area}`; the halves a kernel splits one cylinder into are one entry, `origin` is the axis point closest to the world origin and `axis` points so that its first non-zero component is positive) and `restored: true`. On a large design, `bodies` with the bodies a change touches and `summary: true` keep the reply small. A name the design has no parameter of, a value that is not a number or a string, an expression that does not evaluate, an unknown report item or a `bodies` that is not a list of names is an error, and nothing changes; a body name the design does not have after the change is an error after the design is put back. If the checkpoint cannot be restored the call is an error with `restored: false`, `restore_error` and a note that the tried values are still in the design: set them back before saving. Replaces the loop `set_parameter`, `measure`, `body_topology`, `set_parameter` back, which cost every agent session three to five calls and up to 16 s of rebuilds. Not a `batch` step. |
|
||||
| `screenshot` | `view?` (iso, top, front, back, bottom, left, right, home, fit, or a named view saved with `view.save`), `camera?` (`{target, yaw, pitch, distance, fov?}`, as `view.apply`), `path?`, `width?`, `height?`, `source?` (window, model), `grid?`, `axes?` | A PNG image content block. Headless: a render with the viewport's pipelines on a headless GPU device (see `solidcraft-cli gpu`; with no adapter, the CPU rasterizer, without analysis colours), fitted to the model, or from the named view or camera; the reply's `renderer` says which (`gpu`, `gpu-software`, `cpu`, with a `warning` for `cpu`). Bridged: the app window (default) or a model render with the app's camera; a named view or camera moves the app's viewport there first (`execute` `view.apply` does the same without a screenshot). `grid: false` and `axes: false` leave the ground grid (it lies on Z 0 and cuts through a part that straddles it) and the origin's axes out of a model render; axes hidden with `browser.visibility` stay out whatever `axes` says. Bridged, a screenshot that gives `grid` or `axes` is a model render, which follows the app's Grid and Origin switches for what it does not give; `source: "window"` with either is an error. A window that is minimised or on another macOS Space presents no frame: with no `source` the reply is a model render and says so (`fallback`); `source: "window"` is an error saying why, at once. Agents that don't need the app's chrome can ask for `source: "model"`. |
|
||||
| `compare_picture` | `reference`, `view?`, `camera?`, `bodies?`, `crop_px?`, `crop_fraction?`, `part_fills_crop?`, `fit?` (width, height, both; default width), `rows?` | Compare the model's outline with the part in a reference picture (a PNG or JPEG inside the server's folders, read through the same path rules as `screenshot`). The compare is outline only: holes are not compared — both the picture's and the model's holes are filled. The model's outline is `inspect.silhouette` seen from `view` (default front) or `camera`; the picture's part is found by growing the background in from the border through small colour steps, filling holes and keeping the largest piece, so a backdrop's shading stays background and labels on a board drop out; a part touching the border, a white part on a light grey backdrop, and a part on a transparent background all separate. `crop_px` (`[x, y, w, h]` in pixels) or `crop_fraction` (the same as fractions 0..1) takes one view off a board; with `part_fills_crop: true` the crop box is the part itself and no background search runs, for a crop that tightly frames the part; `fit` names the dimension the one uniform scale comes from, never stretched per axis (`both` scales by width and reports `aspect_mismatch`). Returns an image content block (the overlay PNG: model red, picture blue) and text: `overlap` (0..1) and the model's top, bottom, left and right edges against the picture's in mm at nine stations, positive where the model reaches past the picture, null where a station misses one of the two. As a `batch` step it runs `inspect.compare_picture` without the overlay. |
|
||||
| `export` | `path`, `format?` (step, stp, iges, igs, 3mf, 3mf-tagged, stl, stla, obj, urdf), `bodies?`, `mesh_prefix?`, `effort?`, `velocity?` | STEP, IGES, 3MF or mesh export; URDF writes the whole assembly with its meshes next to it (`docs/urdf.md`). |
|
||||
|
||||
@@ -432,17 +432,17 @@
|
||||
{"case":"owned-first-free-second-x2","deferred":null,"error":null,"ints":{"api_status_result_vertex_edge_face_counts":[0,1,4,4,1],"body0_topology":{"blocks":[[128403,5122478]],"n":91},"body0_vertex_edge_face_counts":[6,7,2],"result_topology":{"blocks":[[71196,1456742]],"n":49}},"topology":null,"values":{"body0_geometry":{"blocks":[[357.93012000000004,357.93012000000004,34368.9378]],"n":186},"fillet_point_step":[0.00001],"result_geometry":{"blocks":[[272.7895363968895,275.98953639688955,15165.51307875397]],"n":106}}}
|
||||
{"case":"piecewise-box-all-edges","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,24,48,26],"mass_status":[0],"result_topology":{"blocks":[[281705,35379780],[282350,36569579],[339856,48748751],[166379,10487033]],"n":896}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2148.2123683100876,5987.2916904551985,5987.2916904551985,4.99999988854144,10.000000001645972,15.000000196509351,2593031.078814324,1994679.8755745701,997175.0943071517,898093.7835769497,449046.86731625785,299364.57777508243],"result_geometry":{"blocks":[[1520.4713199999997,1520.4713199999997,188532.65884000002],[1408.9205999999995,1408.9205999999995,190506.36739999996],[1596.4445858846063,1596.4445858846063,170258.39134182647],[1262.1075306155155,1262.1075306155155,178249.57720935484],[1481.0099715619724,1481.0099715619724,180524.1140389652],[628.4919545649445,628.4919545649445,49464.01854071646]],"n":1428},"selected_edge_before":{"blocks":[[169486.80000000005,169486.80000000005,16693632.500000002]],"n":204}}}
|
||||
{"case":"piecewise-box-edge0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,10,15,7],"mass_status":[0],"result_topology":{"blocks":[[300940,40015510],[15123,181476]],"n":278}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2195.6006651155526,5998.927028705683,5998.927028705683,4.999125706075418,9.999999999999998,14.997337012940344,2598898.4137614993,1998936.5572220604,999752.0177820763,899679.3036553598,449682.8953313119,299893.903180732],"result_geometry":{"blocks":[[1685.56037,1685.56037,223932.77024999997],[1347.1354418185138,1347.1354418185138,121403.39432023243]],"n":436},"selected_edge_before":{"blocks":[[14178.4,14178.4,71995.6]],"n":17}}}
|
||||
{"case":"piecewise-box-edges-0-1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,11,17,8],"mass_status":[0],"result_topology":{"blocks":[[284004,35944727],[75310,1915144]],"n":315}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2193.4192487489036,5998.402474529421,5998.402474529421,4.999135272986161,9.999135276538652,14.996034953928016,2598222.3723900267,1998451.2841356825,999527.7971654396,899367.5101854707,449606.2438251754,299842.8930973649],"result_geometry":{"blocks":[[1665.4304699999996,1665.4304699999996,221822.30508],[1890.9918541256386,1890.9918541256386,221967.93953914358]],"n":494},"selected_edge_before":{"blocks":[[28364.100000000002,28364.100000000002,385304.5999999999]],"n":34}}}
|
||||
{"case":"piecewise-box-edges-0-1-2","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,13,20,9],"mass_status":[0],"result_topology":{"blocks":[[281070,35292146],[135619,7463383]],"n":369}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2189.01991272372,5997.329503235369,5997.329503235369,5.000009645091854,9.999135121832559,14.998697940955562,2598079.2900545467,1998451.2417698312,999384.7114566567,899366.3151418399,449606.23457063275,299841.6927871981],"result_geometry":{"blocks":[[1564.7906399999995,1564.7906399999995,192024.22804999998],[1771.8682850342875,1771.8682850342875,228433.84759239157],[375.6084472728377,375.6084472728377,11298.604991329901]],"n":574},"selected_edge_before":{"blocks":[[42424.50000000001,42424.50000000001,933855.7999999999]],"n":51}}}
|
||||
{"case":"piecewise-box-edges-0-1-2-3","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,14,22,10],"mass_status":[0],"result_topology":{"blocks":[[281081,35291552],[178907,13396646]],"n":406}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2184.692479171914,5996.268482235584,5996.268482235584,5.000874681746329,9.999154675732406,14.996063356677329,2596993.1492674337,1997503.4193458795,999246.3482068322,899052.7360357245,449602.69860910985,299840.5284213158],"result_geometry":{"blocks":[[1618.9707399999993,1618.9707399999993,204492.54252],[1691.6832990227765,1691.6832990227765,237079.15151635028],[755.4829190227764,755.4829190227764,45547.76643946094]],"n":632},"selected_edge_before":{"blocks":[[56575.90000000001,56575.90000000001,1727245.7999999998]],"n":68}}}
|
||||
{"case":"piecewise-box-edges-0-1-3","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,12,19,9],"mass_status":[0],"result_topology":{"blocks":[[282062,35557630],[120525,5428448]],"n":352}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2189.0918151972965,5997.341453529632,5997.341453529632,5.00000000018908,9.999154826967516,14.993400369874573,2597136.231602912,1997503.4617117303,999389.4339156136,899053.9310793541,449602.7078636525,299841.7287314825],"result_geometry":{"blocks":[[1645.5005699999992,1645.5005699999992,213375.26965],[1939.5246983704803,1939.5246983704803,245426.90568415445],[325.2666414935852,325.2666414935852,7140.405604196788]],"n":552},"selected_edge_before":{"blocks":[[42515.50000000001,42515.50000000001,938120.7999999999]],"n":51}}}
|
||||
{"case":"piecewise-box-edges-0-1-4","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,13,20,9],"mass_status":[0],"result_topology":{"blocks":[[281066,35293133],[135619,7493377]],"n":369}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2189.0199151121546,5997.329618318214,5997.329618318214,4.998260641565335,9.999135121849157,14.998697776742159,2598079.3128011464,1998346.3587016147,999279.8363305995,899366.3225543492,449605.0634077394,299736.8050348302],"result_geometry":{"blocks":[[1574.7906399999995,1574.7906399999995,204727.64805],[1857.5662360800861,1857.5662360800861,232699.87460511783],[404.667422795737,404.667422795737,12923.330868410616]],"n":574},"selected_edge_before":{"blocks":[[42456.50000000001,42456.50000000001,935275.7999999999]],"n":51}}}
|
||||
{"case":"piecewise-box-edges-0-1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,11,17,8],"mass_status":[0],"result_topology":{"blocks":[[284004,35944727],[75310,1915144]],"n":315}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2193.419249198952,5998.402543655564,5998.402543655564,4.99913530767552,9.999135315245166,14.996035044941893,2598222.424924777,1998451.3270221106,999527.8172865901,899367.5314050158,449606.25647167955,299842.8994597478],"result_geometry":{"blocks":[[1665.4304699999996,1665.4304699999996,221822.30508],[1890.9918541256386,1890.9918541256386,221967.93953914358]],"n":494},"selected_edge_before":{"blocks":[[28364.100000000002,28364.100000000002,385304.5999999999]],"n":34}}}
|
||||
{"case":"piecewise-box-edges-0-1-2","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,13,20,9],"mass_status":[0],"result_topology":{"blocks":[[281070,35292146],[135619,7463383]],"n":369}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2189.0199131737613,5997.329572360888,5997.329572360888,5.00000967977676,9.999135160546013,14.998698031954126,2598079.3425888564,1998451.284655832,999384.7315776577,899366.3363612375,449606.2472169817,299841.6991495148],"result_geometry":{"blocks":[[1564.7906399999995,1564.7906399999995,192024.22804999998],[1771.8682850342875,1771.8682850342875,228433.84759239157],[375.6084472728377,375.6084472728377,11298.604991329901]],"n":574},"selected_edge_before":{"blocks":[[42424.50000000001,42424.50000000001,933855.7999999999]],"n":51}}}
|
||||
{"case":"piecewise-box-edges-0-1-2-3","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,14,22,10],"mass_status":[0],"result_topology":{"blocks":[[281081,35291552],[178907,13396646]],"n":406}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2184.692479196941,5996.268608473818,5996.268608473818,5.000874681796552,9.999154702511552,14.996063525027136,2596993.2392652063,1997503.4939705362,999246.3740760277,899052.7688364687,449602.7130239249,299840.535530175],"result_geometry":{"blocks":[[1618.9707399999993,1618.9707399999993,204492.54252],[1691.6832990227765,1691.6832990227765,237079.15151635028],[755.4829190227764,755.4829190227764,45547.76643946093]],"n":632},"selected_edge_before":{"blocks":[[56575.90000000001,56575.90000000001,1727245.7999999998]],"n":68}}}
|
||||
{"case":"piecewise-box-edges-0-1-3","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,12,19,9],"mass_status":[0],"result_topology":{"blocks":[[282062,35557630],[120525,5428448]],"n":352}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2189.0918152223308,5997.341579768433,5997.341579768433,5.0000000002583205,9.999154853741858,14.99340053825121,2597136.321601103,1997503.5363367992,999389.4597849513,899053.9638802367,449602.7222786213,299841.7358404071],"result_geometry":{"blocks":[[1645.5005699999992,1645.5005699999992,213375.26965],[1939.5246983704803,1939.5246983704803,245426.90568415445],[325.2666414935852,325.2666414935852,7140.405604196789]],"n":552},"selected_edge_before":{"blocks":[[42515.50000000001,42515.50000000001,938120.7999999999]],"n":51}}}
|
||||
{"case":"piecewise-box-edges-0-1-4","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,13,20,9],"mass_status":[0],"result_topology":{"blocks":[[281066,35293133],[135619,7493377]],"n":369}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2189.019915562196,5997.329687443733,5997.329687443733,4.998260676270399,9.99913516056261,14.998697867740724,2598079.365335456,1998346.4015876157,999279.8564516006,899366.3437737467,449605.07605408836,299736.8113971468],"result_geometry":{"blocks":[[1574.7906399999995,1574.7906399999995,204727.64805],[1857.5662360800861,1857.5662360800861,232699.87460511783],[404.667422795737,404.667422795737,12923.330868410616]],"n":574},"selected_edge_before":{"blocks":[[42456.50000000001,42456.50000000001,935275.7999999999]],"n":51}}}
|
||||
{"case":"piecewise-box-edges-0-1-5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,13,21,10],"mass_status":[0],"result_topology":{"blocks":[[281103,35302907],[164799,11047122]],"n":389}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2186.927542491318,5996.802024908977,5996.802024908977,4.997830456109917,9.996496130707685,14.996056253577054,2597114.533350623,1997819.9847686589,998738.2640617043,898892.744354468,449370.1921925912,299528.1643968848],"result_geometry":{"blocks":[[1570.2290272728394,1570.2290272728394,200548.9019686273],[2142.1570887709854,2142.1570887709854,260597.11983203166],[807.4549603446917,807.4549603446917,42737.197926989495]],"n":610},"selected_edge_before":{"blocks":[[42541.200000000004,42541.200000000004,939138.4999999999]],"n":51}}}
|
||||
{"case":"piecewise-box-edges-0-2-4","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,14,21,9],"mass_status":[0],"result_topology":{"blocks":[[281058,35289347],[149733,9675652]],"n":386}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2186.801995455387,5996.78120120067,5996.78120120067,4.999125443296737,10.0,15.002663775702318,2598612.2718372885,1998831.589422293,999360.9712385165,899676.9209806592,449681.7056593938,299786.6148080584],"result_geometry":{"blocks":[[1561.1807099999999,1561.1807099999999,189427.53649],[1491.221526500123,1491.221526500123,201581.72797224633],[416.7405982500614,416.7405982500614,15131.537904869176]],"n":596},"selected_edge_before":{"blocks":[[42331.200000000004,42331.200000000004,931491.2000000001]],"n":51}}}
|
||||
{"case":"piecewise-box-edges-0-3-4","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,14,21,9],"mass_status":[0],"result_topology":{"blocks":[[281060,35290575],[149731,9675749]],"n":386}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2186.8019961310783,5996.781316283521,5996.781316283521,4.999125393243989,10.0,14.997336060095394,2597653.8497805377,1997873.152054675,999360.9866164098,899357.4467920518,449678.1294518669,299786.6175596406],"result_geometry":{"blocks":[[1641.1807099999999,1641.1807099999999,214491.13648999998],[1726.157244545675,1726.157244545675,228052.47213443523],[505.4984572728377,505.4984572728377,20637.704149483186]],"n":596},"selected_edge_before":{"blocks":[[42422.200000000004,42422.200000000004,934209.2000000001]],"n":51}}}
|
||||
{"case":"piecewise-box-edges-0-3-8","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,13,20,9],"mass_status":[0],"result_topology":{"blocks":[[281051,35290522],[135621,7613727]],"n":369}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2184.727874924877,5996.25664549761,5996.25664549761,4.9986976890843735,10.002634517011648,14.994701497219586,2597324.7029052833,1997349.6971433728,999452.70587383,899355.9892449622,449444.1331736153,299890.9369265837],"result_geometry":{"blocks":[[1565.4850817499384,1565.4850817499384,194549.45138292658],[1827.4949436174356,1827.4949436174356,236486.59459581494],[368.9073307727147,368.9073307727147,11806.49976413873]],"n":574},"selected_edge_before":{"blocks":[[42449.9,42449.9,935416.9]],"n":51}}}
|
||||
{"case":"piecewise-box-edges-0-4-5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,12,19,9],"mass_status":[0],"result_topology":{"blocks":[[282064,35560469],[120512,5488359]],"n":352}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2184.7997759340906,5996.268427350089,5996.268427350089,4.996958109030092,9.997385172407903,14.999999999770953,2597655.9256746494,1998204.3837119415,998721.7828580401,899205.075967518,449446.5321135234,299475.99965963187],"result_geometry":{"blocks":[[1590.8805699999998,1590.8805699999998,206311.76964999997],[1722.005814870604,1722.005814870604,225171.24223200197],[229.76775799370822,229.76775799370822,5512.505154889019]],"n":552},"selected_edge_before":{"blocks":[[42447.9,42447.9,936229.9]],"n":51}}}
|
||||
{"case":"piecewise-box-edges-0-4-8","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,12,19,9],"mass_status":[0],"result_topology":{"blocks":[[282058,35560490],[120512,5548758]],"n":352}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2184.7997787557056,5996.268618208877,5996.268618208877,4.996958286790571,10.00261482750887,14.999999835721244,2598283.14761315,1998204.4331087447,999348.9978626971,899675.471003479,449446.5546596551,299786.06337586057],"result_geometry":{"blocks":[[1653.43057,1653.43057,218710.69465],[1611.8578638248057,1611.8578638248057,214533.45630541348],[170.70878247080896,170.70878247080896,3448.4694385393723]],"n":552},"selected_edge_before":{"blocks":[[42390.9,42390.9,933574.9]],"n":51}}}
|
||||
{"case":"piecewise-box-edges-0-3-8","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,13,20,9],"mass_status":[0],"result_topology":{"blocks":[[281051,35290522],[135621,7613727]],"n":369}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2184.7278762493693,5996.256649739666,5996.256649739666,4.998697692472137,10.002634510063238,14.994701502407121,2597324.7050961093,1997349.6997409267,999452.7062808687,899355.9892620939,449444.1341003187,299890.93693437806],"result_geometry":{"blocks":[[1565.4850817499384,1565.4850817499384,194549.45138292658],[1827.4949436174356,1827.4949436174356,236486.59459581494],[368.9073307727147,368.9073307727147,11806.499764138733]],"n":574},"selected_edge_before":{"blocks":[[42449.9,42449.9,935416.9]],"n":51}}}
|
||||
{"case":"piecewise-box-edges-0-4-5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,12,19,9],"mass_status":[0],"result_topology":{"blocks":[[282064,35560469],[120512,5488359]],"n":352}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2184.799778852964,5996.268453352739,5996.268453352739,4.996958136677199,9.997385184344427,14.999999975255333,2597655.934237956,1998204.390217179,998721.7915321472,899205.079325668,449446.5355407508,299476.00330866454],"result_geometry":{"blocks":[[1590.8805699999998,1590.8805699999998,206311.76964999997],[1722.005814870604,1722.005814870604,225171.24223200197],[229.76775799370816,229.76775799370816,5512.505154889019]],"n":552},"selected_edge_before":{"blocks":[[42447.9,42447.9,936229.9]],"n":51}}}
|
||||
{"case":"piecewise-box-edges-0-4-8","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,12,19,9],"mass_status":[0],"result_topology":{"blocks":[[282058,35560490],[120512,5548758]],"n":352}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[2184.7997803056023,5996.268625366811,5996.268625366811,4.996958292641284,10.002614815795091,14.999999837350689,2598283.150028192,1998204.4362258078,999348.9985649497,899675.4710250348,449446.5558118165,299786.06338945584],"result_geometry":{"blocks":[[1653.43057,1653.43057,218710.69465],[1611.8578638248057,1611.8578638248057,214533.45630541348],[170.70878247080896,170.70878247080896,3448.4694385393723]],"n":552},"selected_edge_before":{"blocks":[[42390.9,42390.9,933574.9]],"n":51}}}
|
||||
{"case":"piecewise-box-union-edge0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,14,21,9],"mass_status":[0],"result_topology":{"blocks":[[281030,35285715],[149733,9405715]],"n":386}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1395.2787577197364,2997.8539804807074,2997.8539804807074,8.325141626537949,4.999999999999998,8.332300145008215,399723.2685707611,598955.3863642174,399089.04982550326,124895.09578136423,174585.1139762589,124787.79481591206],"result_geometry":{"blocks":[[1233.7603800000002,1237.7603800000002,152224.06892000002],[1515.9239290913513,1515.9239290913513,190713.82542215477],[492.70687454567553,492.70687454567553,20904.85264560849]],"n":596},"selected_edge_before":{"blocks":[[14124.7,14124.7,71324.8]],"n":17}}}
|
||||
{"case":"piecewise-file-box1x1x2-all-edges","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,24,48,26],"mass_status":[0],"result_topology":{"blocks":[[281655,35367371],[282394,36571903],[339874,48751383],[166379,10488820]],"n":896}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[9.301946796716265,1.9670030359376038,1.9670030359376038,0.4999999757117533,0.5000000023282882,0.9999999889311246,3.2649512274596106,3.264951206141764,1.3042573024479265,0.9835015156707282,0.9835014821227895,0.49175073738674835],"result_geometry":{"blocks":[[169.09732000000005,169.09732000000005,24724.796840000003],[201.96660000000008,201.96660000000008,26031.633400000002],[223.61603409316808,223.61603409316808,29392.600049603403],[154.7047288569563,154.7047288569563,19852.58046882097],[140.8898877438963,140.8898877438963,18417.74472272804],[61.05298967334533,61.05298967334533,4403.824695076887]],"n":1428},"selected_edge_before":{"blocks":[[168284.96,168284.96,16577552.679999998]],"n":204}}}
|
||||
{"case":"piecewise-file-corner_box-edge0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,12,18,8],"mass_status":[0],"result_topology":{"blocks":[[283042,35768340],[90425,3301920]],"n":332}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[363.9406719491066,456.15627618938925,456.15627618938925,-9.947136643740814,-10.031783733055853,3.463209983289835,54561.755980063244,57010.4999506741,96957.38069533654,-15889.87384062821,-15776.640045661255,45464.797440208735],"result_geometry":{"blocks":[[-579.2715841839397,1117.6045067796942,-56209.917269069556],[-721.7168946021704,1219.8011007022376,-89905.96773033352],[-26.711752494161882,29.931752494161888,-63.200354594176744]],"n":516},"selected_edge_before":{"blocks":[[13936.390000000001,14106.789999999999,69223.95000000001]],"n":17}}}
|
||||
@@ -734,13 +734,13 @@
|
||||
{"case":"stubs4-delete-edge-bottle-e7-r0.1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,26,40,16],"mass_status":[0],"result_topology":{"blocks":[[281457,35341216],[282271,36580789],[241977,24565779]],"n":730}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[33.11819078265203,11.06683396778218,11.06683396778218,-1.760928723326559e-6,-3.0260239843351257e-6,2.0788625844461937,64.79258473459024,66.6733018328832,5.277123465358535,-0.00010373432038354095,-0.00006130940949992112,0.000020822473328764406],"result_geometry":{"blocks":[[912.7406198383134,996.1574227558623,139865.57295882053],[1710.5690534181747,1900.7696584750158,195545.28934387196],[536.122128285832,587.7649824357395,53296.94403726827],[1116.695548284241,1296.7760762038654,136279.5389856879],[81.3031298885769,99.10219410214427,4292.377944642321]],"n":1112},"selected_edge_before":{"blocks":[[14051.529624477327,14057.561884221679,70319.4561279686]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-bottle-e8-r0.1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,30,46,18],"mass_status":[0],"result_topology":{"blocks":[[281565,35348141],[282248,36573784],[281433,35534770],[75941,2465074]],"n":838}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[33.092189313827404,11.065538714487161,11.065538714487161,0.0000622406023526173,0.00007277133924516161,2.0786483671717058,64.77225137393687,66.65299972676581,5.2760623972232965,0.003174621703595437,0.002706404871912829,-0.000419088373760436],"result_geometry":{"blocks":[[1012.7275130339575,1074.9337253969743,177512.6290813119],[1573.7785975265047,1755.7148619372406,178792.2288309686],[496.58950478253786,552.5215294143144,49503.18261364769],[770.3300237135163,856.5776485732445,131486.84241311345],[665.6859717446745,880.6518783618482,60628.31514328711]],"n":1272},"selected_edge_before":{"blocks":[[14038.722642146662,14068.791020398014,70258.3923721516]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-bottle-e8-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,26,40,16],"mass_status":[0],"result_topology":{"blocks":[[281457,35341216],[282271,36580789],[241977,24565779]],"n":730}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[33.11819078265203,11.06683396778218,11.06683396778218,-1.760928723326559e-6,-3.0260239843351257e-6,2.0788625844461937,64.79258473459024,66.6733018328832,5.277123465358535,-0.00010373432038354095,-0.00006130940949992112,0.000020822473328764406],"result_geometry":{"blocks":[[912.7406198383134,996.1574227558623,139865.57295882053],[1710.5690534181747,1900.7696584750158,195545.28934387196],[536.122128285832,587.7649824357395,53296.94403726827],[1116.695548284241,1296.7760762038654,136279.5389856879],[81.3031298885769,99.10219410214427,4292.377944642321]],"n":1112},"selected_edge_before":{"blocks":[[14038.722642146662,14068.791020398014,70258.3923721516]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-sphere_diff_3cylinders-e1-r0.1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,27,42,10],"mass_status":[0],"result_topology":{"blocks":[[281419,35082812],[281727,35941052],[180344,21182036]],"n":746}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38227.395059015704,169498.7732384385,169498.7732384385,-6.535659138986239e-8,-0.00003563790501734367,0.0000928556864676971,200903771.63067678,200903771.3397121,200904532.08968392,244.59158844656497,5.232338913649665,0.241503304594386],"result_geometry":{"blocks":[[-58.23628471091258,3250.9242591803145,29063.769625591456],[393.33231003009774,2283.686082563766,-7485.558712097773],[54.3322308528417,2638.6291261731308,-12862.653024015211],[86.16354011877796,6653.958193762183,135560.55669982277],[307.87749844737357,601.1563507918659,-374.5579420689765]],"n":1044},"selected_edge_before":{"blocks":[[13958.23095813032,14252.330774181872,68955.23539952865]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-sphere_diff_3cylinders-e1-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,27,42,10],"mass_status":[0],"result_topology":{"blocks":[[281419,35082812],[281727,35941052],[180344,21182036]],"n":746}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38192.715671829144,169488.1582510577,169488.1582510577,-6.494126079125619e-8,-0.0009206701740508934,0.00274459932488353,200882123.38385296,200882123.08720684,200899301.73179385,6595.485066629833,5.230879115565467,0.24143739618381177],"result_geometry":{"blocks":[[-55.04275585407373,3250.5421029715994,29403.507339166164],[393.33231003009774,2283.686082563766,-7485.558712097773],[59.80457242487494,2637.390611484507,-11679.935762591926],[95.4154962082632,6655.504607381506,136003.50648764067],[310.7884030645661,601.840793865024,-341.5176823541287]],"n":1044},"selected_edge_before":{"blocks":[[13958.23095813032,14252.330774181872,68955.23539952865]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-sphere_diff_3cylinders-e2-r0.1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,25,39,8],"mass_status":[0],"result_topology":{"blocks":[[281287,35309495],[282813,36661718],[106749,9735716]],"n":689}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38236.60646754423,169497.36934847521,169497.36934847521,-0.0002442434704982383,0.0002497767560673066,0.00008722033752664722,200904177.28570384,200904009.3586459,200903635.36792448,-219.85349823628647,211.7087893819534,802.8540378054577],"result_geometry":{"blocks":[[-272.3531330514662,3207.2965625494535,-62268.30896168256],[843.8053053338348,2629.357987451512,142865.50468114667],[-241.66997710761865,2297.9845592191455,-3820.746445578192],[-187.22231839074738,2183.8077466814675,-34648.73480807489]],"n":946},"selected_edge_before":{"blocks":[[13858.829248638478,14254.330768459826,68064.0472827436]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-sphere_diff_3cylinders-e4-r0.1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,27,42,10],"mass_status":[0],"result_topology":{"blocks":[[281352,35067373],[281728,35993813],[180344,22405812]],"n":746}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38227.39456904653,169498.76972793616,169498.76972793616,2.6024526311482366e-6,-0.00004058406227797934,-0.00012379011426735353,200903767.51041195,200903767.2197315,200904523.4664989,-272.1274098810829,5.223001848333946,0.24234996175311443],"result_geometry":{"blocks":[[178.65343403658443,3199.193276601,-10792.272973688872],[836.4691645295133,2411.465829505752,28928.316368092885],[557.8207081571363,2556.207707436294,134325.61177020936],[1817.453258524948,6341.339323303655,283989.51878237503],[570.3190541490152,608.3812476683969,3725.693560280998]],"n":1044},"selected_edge_before":{"blocks":[[14217.170757083568,14255.330774181872,72285.45278592095]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-sphere_diff_3cylinders-e4-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,27,42,10],"mass_status":[0],"result_topology":{"blocks":[[281353,35067637],[281729,35994049],[180344,22405839]],"n":746}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38192.71518710443,169488.15866987692,169488.15866987692,2.6031860063084604e-6,-0.0009258732176402615,-0.0027748621531443066,200882121.18941358,200882120.89301014,200899286.57691622,-6624.323367454232,5.224558750878024,0.24218614949575126],"result_geometry":{"blocks":[[175.4642895640224,3198.806822926908,-10836.351438469621],[835.6467220106982,2410.6433869869375,28870.044915666193],[560.4920595330373,2558.8790588121956,134983.0654784917],[1813.7793522564266,6339.869114381589,283835.87668517046],[571.0793751171644,609.8451751171975,3722.804971869619]],"n":1044},"selected_edge_before":{"blocks":[[14217.170757083568,14255.330774181872,72285.45278592095]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-sphere_diff_3cylinders-e5-r0.1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,25,39,8],"mass_status":[0],"result_topology":{"blocks":[[281287,35309495],[282813,36661718],[106749,9735716]],"n":689}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38236.60646754423,169497.36934847521,169497.36934847521,-0.0002442434704982383,0.0002497767560673066,0.00008722033752664722,200904177.28570384,200904009.3586459,200903635.36792448,-219.85349823628647,211.7087893819534,802.8540378054577],"result_geometry":{"blocks":[[-272.3531330514662,3207.2965625494535,-62268.30896168256],[843.8053053338348,2629.357987451512,142865.50468114667],[-241.66997710761865,2297.9845592191455,-3820.746445578192],[-187.22231839074738,2183.8077466814675,-34648.73480807489]],"n":946},"selected_edge_before":{"blocks":[[14117.769047591726,14257.330768459826,71394.2646691359]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-sphere_diff_3cylinders-e5-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,25,39,8],"mass_status":[0],"result_topology":{"blocks":[[281287,35309495],[282813,36661718],[106749,9735716]],"n":689}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38236.60646754423,169497.36934847521,169497.36934847521,-0.0002442434704982383,0.0002497767560673066,0.00008722033752664722,200904177.28570384,200904009.3586459,200903635.36792448,-219.85349823628647,211.7087893819534,802.8540378054577],"result_geometry":{"blocks":[[-272.3531330514662,3207.2965625494535,-62268.30896168256],[843.8053053338348,2629.357987451512,142865.50468114667],[-241.66997710761865,2297.9845592191455,-3820.746445578192],[-187.22231839074738,2183.8077466814675,-34648.73480807489]],"n":946},"selected_edge_before":{"blocks":[[14117.769047591726,14257.330768459826,71394.2646691359]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-sphere_diff_3cylinders-e1-r0.1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,27,42,10],"mass_status":[0],"result_topology":{"blocks":[[281419,35082812],[281727,35941052],[180344,21182036]],"n":746}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38227.39497970966,169498.7717735628,169498.7717735628,4.623255766698147e-8,-0.000033746260310659826,0.0000928568898489775,200903770.69357702,200903770.61891612,200904530.42059574,243.04915579724195,5.212332724264934,0.2782477575709461],"result_geometry":{"blocks":[[-58.23630198317341,3250.9242419080533,29063.768644527045],[393.3322996667531,2283.6860722004217,-7485.5603598700045],[54.33222394394517,2638.629119264234,-12862.653473093282],[86.16354011877796,6653.958193762183,135560.55669982277],[307.87749844737357,601.1563507918659,-374.5579420689765]],"n":1044},"selected_edge_before":{"blocks":[[13958.23095813032,14252.330774181872,68955.23539952865]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-sphere_diff_3cylinders-e1-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,27,42,10],"mass_status":[0],"result_topology":{"blocks":[[281419,35082812],[281727,35941052],[180344,21182036]],"n":746}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38192.71559252309,169488.156786182,169488.156786182,4.6654877060792845e-8,-0.0009187784185204046,0.002744600551259024,200882122.4467532,200882122.36641085,200899300.06270567,6593.9426339805395,5.210872926180737,0.2781818491603719],"result_geometry":{"blocks":[[-55.042773126334566,3250.5420856993383,29403.506358101753],[393.3322996667531,2283.6860722004217,-7485.5603598700045],[59.80456551597842,2637.3906045756103,-11679.936211669989],[95.4154962082632,6655.504607381506,136003.50648764067],[310.7884030645661,601.840793865024,-341.5176823541287]],"n":1044},"selected_edge_before":{"blocks":[[13958.23095813032,14252.330774181872,68955.23539952865]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-sphere_diff_3cylinders-e2-r0.1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,25,39,8],"mass_status":[0],"result_topology":{"blocks":[[281287,35309495],[282813,36661718],[106749,9735716]],"n":689}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38236.60639321238,169497.36786896354,169497.36786896354,-0.0002441298476250882,0.00025166980068552417,0.00008722084201979407,200904176.34575313,200904008.6290078,200903633.68539098,-221.39393452112498,211.68654766482535,802.8847324176213],"result_geometry":{"blocks":[[-272.3531330514662,3207.2965625494535,-62268.30896168256],[843.8053053338348,2629.357987451512,142865.50468114667],[-241.66997710761865,2297.9845592191455,-3820.746445578192],[-187.22231839074738,2183.8077466814675,-34648.73480807489]],"n":946},"selected_edge_before":{"blocks":[[13858.829248638478,14254.330768459826,68064.0472827436]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-sphere_diff_3cylinders-e4-r0.1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,27,42,10],"mass_status":[0],"result_topology":{"blocks":[[281352,35067373],[281728,35993813],[180344,22405812]],"n":746}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38227.39448974048,169498.76826306045,169498.76826306045,2.714041805572479e-6,-0.00003869241757486415,-0.0001237889127583873,200903766.5733122,200903766.49893552,200904521.79741073,-273.66984253039135,5.202995658949216,0.27909441472967467],"result_geometry":{"blocks":[[178.6534167643236,3199.193259328739,-10792.27395475327],[836.4691576206195,2411.4658225968583,28928.31526957864],[557.8206977937889,2556.2076970729468,134325.61103441205],[1817.453258524948,6341.339323303655,283989.51878237503],[570.3190541490152,608.3812476683969,3725.693560280998]],"n":1044},"selected_edge_before":{"blocks":[[14217.170757083568,14255.330774181872,72285.45278592095]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-sphere_diff_3cylinders-e4-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,27,42,10],"mass_status":[0],"result_topology":{"blocks":[[281353,35067637],[281729,35994049],[180344,22405839]],"n":746}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38192.71510779838,169488.15720500122,169488.15720500122,2.7147821669452055e-6,-0.0009239814621594171,-0.002774860974473181,200882120.25231382,200882120.17221415,200899284.90782803,-6625.8658001035255,5.204552561493293,0.2789306024723115],"result_geometry":{"blocks":[[175.4642722917617,3198.806805654647,-10836.352419534018],[835.6467151018045,2410.6433800780437,28870.04381715195],[560.4920491696898,2558.8790484488477,134983.06474269443],[1813.7793522564266,6339.869114381589,283835.87668517046],[571.0793751171644,609.8451751171975,3722.804971869619]],"n":1044},"selected_edge_before":{"blocks":[[14217.170757083568,14255.330774181872,72285.45278592095]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-sphere_diff_3cylinders-e5-r0.1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,25,39,8],"mass_status":[0],"result_topology":{"blocks":[[281287,35309495],[282813,36661718],[106749,9735716]],"n":689}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38236.60639321238,169497.36786896354,169497.36786896354,-0.0002441298476250882,0.00025166980068552417,0.00008722084201979407,200904176.34575313,200904008.6290078,200903633.68539098,-221.39393452112498,211.68654766482535,802.8847324176213],"result_geometry":{"blocks":[[-272.3531330514662,3207.2965625494535,-62268.30896168256],[843.8053053338348,2629.357987451512,142865.50468114667],[-241.66997710761865,2297.9845592191455,-3820.746445578192],[-187.22231839074738,2183.8077466814675,-34648.73480807489]],"n":946},"selected_edge_before":{"blocks":[[14117.769047591726,14257.330768459826,71394.2646691359]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-sphere_diff_3cylinders-e5-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,25,39,8],"mass_status":[0],"result_topology":{"blocks":[[281287,35309495],[282813,36661718],[106749,9735716]],"n":689}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38236.60639321238,169497.36786896354,169497.36786896354,-0.0002441298476250882,0.00025166980068552417,0.00008722084201979407,200904176.34575313,200904008.6290078,200903633.68539098,-221.39393452112498,211.68654766482535,802.8847324176213],"result_geometry":{"blocks":[[-272.3531330514662,3207.2965625494535,-62268.30896168256],[843.8053053338348,2629.357987451512,142865.50468114667],[-241.66997710761865,2297.9845592191455,-3820.746445578192],[-187.22231839074738,2183.8077466814675,-34648.73480807489]],"n":946},"selected_edge_before":{"blocks":[[14117.769047591726,14257.330768459826,71394.2646691359]],"n":17}}}
|
||||
{"case":"stubs4-open_shell-e0-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,16,21,6],"mass_status":[1007],"result_topology":{"blocks":[[325212,41883600],[60327,1599127]],"n":311}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[],"result_geometry":{"blocks":[[-1097.5615101691037,19602.9089607915,-184867.1316369425],[1582.0715457351475,17732.074813669384,255090.98593747205],[186.0159067879023,2930.3497457866056,6127.925961962178]],"n":550},"selected_edge_before":{"blocks":[[14053.433886125349,15365.754072155007,69967.22438595541]],"n":17}}}
|
||||
{"case":"stubs4-open_shell-e5-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,18,24,7],"mass_status":[1007],"result_topology":{"blocks":[[337343,43924563],[105628,5738038]],"n":359}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[],"result_geometry":{"blocks":[[-864.3564813200322,19670.27954000918,-134824.70274138806],[723.2226425349643,17264.932632287437,-1723.289599615673],[17.17198454715698,8601.974044519407,921.0235462851215]],"n":630},"selected_edge_before":{"blocks":[[13840.385369163707,15140.593760262442,67689.83797353065]],"n":17}}}
|
||||
{"case":"stubs4-rollover-bague-e1-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,4,8,4],"mass_status":[0],"result_topology":{"blocks":[[172450,12382265]],"n":146}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[514.6144027764909,225.66889284512877,225.66889284512877,-0.32303313010240103,6.889210187890638,-0.011379984931174108,18666.065838694758,10297.324325608877,28464.959061288333,-24.85414539881951,828.174064739097,-147.40253326904755],"result_geometry":{"blocks":[[270.59669473184863,660.8580131060972,19448.783514605828]],"n":232},"selected_edge_before":{"blocks":[[14038.124898347744,14088.951879332903,70292.40573517486]],"n":17}}}
|
||||
@@ -771,8 +771,8 @@
|
||||
{"case":"sweep4-bottle-e29-r0.1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,26,40,16],"mass_status":[0],"result_topology":{"blocks":[[281457,35341216],[282271,36580789],[241977,24565779]],"n":730}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[33.11819078265203,11.06683396778218,11.06683396778218,-1.760928723326559e-6,-3.0260239843351257e-6,2.0788625844461937,64.79258473459024,66.6733018328832,5.277123465358535,-0.00010373432038354095,-0.00006130940949992112,0.000020822473328764406],"result_geometry":{"blocks":[[912.7406198383134,996.1574227558623,139865.57295882053],[1710.5690534181747,1900.7696584750158,195545.28934387196],[536.122128285832,587.7649824357395,53296.94403726827],[1116.695548284241,1296.7760762038654,136279.5389856879],[81.3031298885769,99.10219410214427,4292.377944642321]],"n":1112},"selected_edge_before":{"blocks":[[14056.32667686996,14056.532787646805,70289.10626586332]],"n":17}}}
|
||||
{"case":"sweep4-brepForFillet2-all-r0.1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,10,15,7],"mass_status":[0],"result_topology":{"blocks":[[286213,36387994],[30108,196385]],"n":278}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1440.9799345699366,3616.4117197382475,3616.4117197382475,3.4576417013586815,6.968403815007568,11.462456451394527,855617.9720066136,741334.0091506396,324410.44028196693,303891.37025483977,98600.05653986205,79223.04263413792],"result_geometry":{"blocks":[[1058.0112074993297,1200.4221207267126,134552.7622124501],[576.414142594088,675.4997380276822,62041.95297490219]],"n":436},"selected_edge_before":{"blocks":[[211488.93573851514,211626.8236529697,26222606.564571798]],"n":255}}}
|
||||
{"case":"sweep4-native-crash-extrusion1002-all-r0.1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,2,3,3],"mass_status":[0],"result_topology":{"blocks":[[87211,2632979]],"n":60}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1828.3123418766857,4539.209557183511,4539.209557183511,-23.164276352530862,6.718006159263185,5.0,625550.1252623487,2775651.333995856,3098587.4887793036,152472.18881672568,-525737.5230232404,-851367.1886541296],"result_geometry":{"blocks":[[-265.93930395024375,984.5940213531953,-16331.908320443214]],"n":116},"selected_edge_before":{"blocks":[[41993.3485266974,42459.791701173235,923773.8634014014]],"n":51}}}
|
||||
{"case":"sweep4-native-crash-sphere_diff_3cylinders-e30-r0.1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1006,25,39,8],"mass_status":[0],"result_topology":{"blocks":[[281287,35309495],[282813,36661718],[106749,9735716]],"n":689}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38236.60646754423,169497.36934847521,169497.36934847521,-0.0002442434704982383,0.0002497767560673066,0.00008722033752664722,200904177.28570384,200904009.3586459,200903635.36792448,-219.85349823628647,211.7087893819534,802.8540378054577],"result_geometry":{"blocks":[[-272.3531330514662,3207.2965625494535,-62268.30896168256],[843.8053053338348,2629.357987451512,142865.50468114667],[-241.66997710761865,2297.9845592191455,-3820.746445578192],[-187.22231839074738,2183.8077466814675,-34648.73480807489]],"n":946},"selected_edge_before":{"blocks":[[14043.344676254792,14208.387727110645,69814.76280349465]],"n":17}}}
|
||||
{"case":"sweep4-sphere_diff_3cylinders-all-r0.1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1007,25,39,8],"mass_status":[0],"result_topology":{"blocks":[[281287,35309495],[282813,36661718],[106749,9735716]],"n":689}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38236.60646754423,169497.36934847521,169497.36934847521,-0.0002442434704982383,0.0002497767560673066,0.00008722033752664722,200904177.28570384,200904009.3586459,200903635.36792448,-219.85349823628647,211.7087893819534,802.8540378054577],"result_geometry":{"blocks":[[-272.3531330514662,3207.2965625494535,-62268.30896168256],[843.8053053338348,2629.357987451512,142865.50468114667],[-241.66997710761865,2297.9845592191455,-3820.746445578192],[-187.22231839074738,2183.8077466814675,-34648.73480807489]],"n":946},"selected_edge_before":{"blocks":[[210500.9244698742,213738.57714575977,26159842.25571068],[210998.88080540716,213017.77850297175,25928388.234741364],[126612.02279058412,128034.1177995765,8997007.19306596]],"n":663}}}
|
||||
{"case":"sweep4-native-crash-sphere_diff_3cylinders-e30-r0.1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1006,25,39,8],"mass_status":[0],"result_topology":{"blocks":[[281287,35309495],[282813,36661718],[106749,9735716]],"n":689}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38236.60639321238,169497.36786896354,169497.36786896354,-0.0002441298476250882,0.00025166980068552417,0.00008722084201979407,200904176.34575313,200904008.6290078,200903633.68539098,-221.39393452112498,211.68654766482535,802.8847324176213],"result_geometry":{"blocks":[[-272.3531330514662,3207.2965625494535,-62268.30896168256],[843.8053053338348,2629.357987451512,142865.50468114667],[-241.66997710761865,2297.9845592191455,-3820.746445578192],[-187.22231839074738,2183.8077466814675,-34648.73480807489]],"n":946},"selected_edge_before":{"blocks":[[14043.344676254792,14208.387727110645,69814.76280349465]],"n":17}}}
|
||||
{"case":"sweep4-sphere_diff_3cylinders-all-r0.1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1007,25,39,8],"mass_status":[0],"result_topology":{"blocks":[[281287,35309495],[282813,36661718],[106749,9735716]],"n":689}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38236.60639321238,169497.36786896354,169497.36786896354,-0.0002441298476250882,0.00025166980068552417,0.00008722084201979407,200904176.34575313,200904008.6290078,200903633.68539098,-221.39393452112498,211.68654766482535,802.8847324176213],"result_geometry":{"blocks":[[-272.3531330514662,3207.2965625494535,-62268.30896168256],[843.8053053338348,2629.357987451512,142865.50468114667],[-241.66997710761865,2297.9845592191455,-3820.746445578192],[-187.22231839074738,2183.8077466814675,-34648.73480807489]],"n":946},"selected_edge_before":{"blocks":[[210500.9244698742,213738.57714575977,26159842.25571068],[210998.88080540716,213017.77850297175,25928388.234741364],[126612.02279058412,128034.1177995765,8997007.19306596]],"n":663}}}
|
||||
{"case":"sweep5-bague-e4-r1.0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,4,8,4],"mass_status":[0],"result_topology":{"blocks":[[172450,12382265]],"n":146}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[514.6144027764909,225.66889284512877,225.66889284512877,-0.32303313010240103,6.889210187890638,-0.011379984931174108,18666.065838694758,10297.324325608877,28464.959061288333,-24.85414539881951,828.174064739097,-147.40253326904755],"result_geometry":{"blocks":[[270.59669473184863,660.8580131060972,19448.783514605828]],"n":232},"selected_edge_before":{"blocks":[[14036.56915572996,14077.922178408788,70220.03171369614]],"n":17}}}
|
||||
{"case":"sweep5-bottle-e29-r1.0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,26,40,16],"mass_status":[0],"result_topology":{"blocks":[[281457,35341216],[282271,36580789],[241977,24565779]],"n":730}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[33.11819078265203,11.06683396778218,11.06683396778218,-1.760928723326559e-6,-3.0260239843351257e-6,2.0788625844461937,64.79258473459024,66.6733018328832,5.277123465358535,-0.00010373432038354095,-0.00006130940949992112,0.000020822473328764406],"result_geometry":{"blocks":[[912.7406198383134,996.1574227558623,139865.57295882053],[1710.5690534181747,1900.7696584750158,195545.28934387196],[536.122128285832,587.7649824357395,53296.94403726827],[1116.695548284241,1296.7760762038654,136279.5389856879],[81.3031298885769,99.10219410214427,4292.377944642321]],"n":1112},"selected_edge_before":{"blocks":[[14056.32667686996,14056.532787646805,70289.10626586332]],"n":17}}}
|
||||
{"case":"sweep5-ext_w_hole-e14-r1.0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,16,24,10],"mass_status":[0],"result_topology":{"blocks":[[281265,35232906],[207181,19279604]],"n":440}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[284.98981133796997,233.92041218558995,233.92041218558995,4.121235785968235,3.3787490092953405,2.503170966827912,5809.813379408206,7087.081330486026,8992.812613690214,1976.9990940072303,2415.986012872389,3117.945568427183],"result_geometry":{"blocks":[[615.0139314107776,692.0181326716923,77614.46178832269],[553.4290978015259,574.8568236138705,71081.83901310955],[320.69517748498475,440.66148084137205,29369.56517126522]],"n":676},"selected_edge_before":{"blocks":[[14078.987775637732,14078.987775637732,70515.81832691318]],"n":17}}}
|
||||
@@ -780,7 +780,7 @@
|
||||
{"case":"sweep5-letter_g-e1-r1.0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,22,33,13],"mass_status":[0],"result_topology":{"blocks":[[281269,35110418],[273706,35051752],[105878,4647860]],"n":602}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[859.1654222934296,573.7963922938876,573.7963922938876,4.1075319803360015,1.032877124993837,1.0089007655935105,37903.49284699352,22576.86790739706,58933.83628289546,564.1449745370736,2367.7024338498604,2148.3374122781797],"result_geometry":{"blocks":[[410.9003920176803,934.6923177929822,36540.264544020014],[523.9494009864142,872.1294547530237,69254.79224726776],[162.03970552776553,776.643810703214,-2105.211447933161],[195.32187706423616,644.7134636031989,11312.360336402684]],"n":916},"selected_edge_before":{"blocks":[[14012.150359744432,14071.96337274414,70007.57227090707]],"n":17}}}
|
||||
{"case":"sweep5-letter_g-e10-r1.0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,23,35,14],"mass_status":[0],"result_topology":{"blocks":[[281451,35195912],[271658,33821865],[151262,8469425]],"n":639}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[855.4849128070394,572.1869950561518,572.1869950561518,4.062092426401452,1.0718459524323554,0.9889092657858304,37554.111360504445,22226.43508373305,58285.07609206202,657.6053858511173,2288.4843358448393,2332.4782960288526],"result_geometry":{"blocks":[[366.9326348999855,889.7222034741029,40965.30377578439],[461.5924976248298,805.912969168919,52003.376920442846],[373.93451694747125,801.5315902348118,19905.788703570855],[348.6104518237293,1150.1935647599728,41499.928440952826]],"n":974},"selected_edge_before":{"blocks":[[14000.017695808663,14139.892654430238,70165.97445144223]],"n":17}}}
|
||||
{"case":"sweep5-nurb_box1-all-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,18,36,20],"mass_status":[0],"result_topology":{"blocks":[[282087,35410313],[285717,37291821],[241338,17627434]],"n":674}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1572.53612249555,3793.25316935935,3793.25316935935,-2.157052609544275,23.56413204673117,3.9132219409601245,2294549.826634634,289165.65166244045,2426241.121954471,350450.3463127255,-32664.68319419244,-192339.64549405224],"result_geometry":{"blocks":[[1246.7299816533673,2261.4022016892022,152740.67909260615],[883.3336903231568,1707.0215927527775,133159.3521173068],[1132.6368715334706,1719.448954536655,177070.3161360207],[924.3807460051536,1754.7015106013766,107176.86948552766],[401.3856922651747,447.4315611824733,11727.670701557305]],"n":1080},"selected_edge_before":{"blocks":[[126990.00055073624,127451.28851121065,9289622.563160447]],"n":153}}}
|
||||
{"case":"sweep5-sphere_diff_3cylinders-e29-r1.0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,25,39,8],"mass_status":[0],"result_topology":{"blocks":[[281287,35309495],[282813,36661718],[106749,9735716]],"n":689}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38236.60646754423,169497.36934847521,169497.36934847521,-0.0002442434704982383,0.0002497767560673066,0.00008722033752664722,200904177.28570384,200904009.3586459,200903635.36792448,-219.85349823628647,211.7087893819534,802.8540378054577],"result_geometry":{"blocks":[[-272.3531330514662,3207.2965625494535,-62268.30896168256],[843.8053053338348,2629.357987451512,142865.50468114667],[-241.66997710761865,2297.9845592191455,-3820.746445578192],[-187.22231839074738,2183.8077466814675,-34648.73480807489]],"n":946},"selected_edge_before":{"blocks":[[14089.869197752294,14207.754380872619,70438.50520744263]],"n":17}}}
|
||||
{"case":"sweep5-sphere_diff_3cylinders-e29-r1.0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,25,39,8],"mass_status":[0],"result_topology":{"blocks":[[281287,35309495],[282813,36661718],[106749,9735716]],"n":689}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[38236.60639321238,169497.36786896354,169497.36786896354,-0.0002441298476250882,0.00025166980068552417,0.00008722084201979407,200904176.34575313,200904008.6290078,200903633.68539098,-221.39393452112498,211.68654766482535,802.8847324176213],"result_geometry":{"blocks":[[-272.3531330514662,3207.2965625494535,-62268.30896168256],[843.8053053338348,2629.357987451512,142865.50468114667],[-241.66997710761865,2297.9845592191455,-3820.746445578192],[-187.22231839074738,2183.8077466814675,-34648.73480807489]],"n":946},"selected_edge_before":{"blocks":[[14089.869197752294,14207.754380872619,70438.50520744263]],"n":17}}}
|
||||
{"case":"two-faces-one-body-x0","deferred":null,"error":null,"ints":{"api_status_result_vertex_edge_face_counts":[0,1,4,4,1],"body0_topology":{"blocks":[[256990,21318399]],"n":180},"body0_vertex_edge_face_counts":[12,14,4],"result_topology":{"blocks":[[71196,1456742]],"n":49}},"topology":null,"values":{"body0_geometry":{"blocks":[[524.74019,594.8001899999997,69442.36691000001],[236.72004999999996,259.94005,12502.132969999997]],"n":372},"fillet_point_step":[0.00001],"result_geometry":{"blocks":[[273.09000000000003,276.2900000000001,15193.250000000005]],"n":106}}}
|
||||
{"case":"two-faces-one-body-x2","deferred":null,"error":null,"ints":{"api_status_result_vertex_edge_face_counts":[0,1,4,4,1],"body0_topology":{"blocks":[[256990,21318399]],"n":180},"body0_vertex_edge_face_counts":[12,14,4],"result_topology":{"blocks":[[71196,1456742]],"n":49}},"topology":null,"values":{"body0_geometry":{"blocks":[[524.74019,594.8001899999997,69442.36691000001],[236.72004999999996,259.94005,12502.132969999997]],"n":372},"fillet_point_step":[0.00001],"result_geometry":{"blocks":[[272.7895363968895,275.98953639688955,15165.51307875397]],"n":106}}}
|
||||
{"case":"two-faces-two-bodies-x0","deferred":null,"error":null,"ints":{"api_status_result_vertex_edge_face_counts":[0,1,4,4,1],"body0_topology":{"blocks":[[128403,5122478]],"n":91},"body0_vertex_edge_face_counts":[6,7,2],"body1_topology":{"blocks":[[128403,5122478]],"n":91},"body1_vertex_edge_face_counts":[6,7,2],"result_topology":{"blocks":[[71196,1456742]],"n":49}},"topology":null,"values":{"body0_geometry":{"blocks":[[357.93012000000004,357.93012000000004,34368.9378]],"n":186},"body1_geometry":{"blocks":[[403.53011999999995,496.81012000000004,38642.6178]],"n":186},"fillet_point_step":[0.00001],"result_geometry":{"blocks":[[273.09000000000003,276.2900000000001,15193.250000000005]],"n":106}}}
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@
|
||||
{"case":"Fillet__shell_fillet.smb","deferred":null,"error":null,"ints":{"check":[0],"edges":[9],"faces":[3],"kind":[2],"loops":[3],"rt_check":[0],"rt_edges":[9],"rt_faces":[3],"rt_kind":[2],"rt_loops":[3],"rt_shells":[1],"rt_vertices":[7],"shells":[1],"vertices":[7]},"topology":null,"values":{"area":[300.0],"rt_area":[300.0],"rt_volume":["NaN"],"volume":["NaN"]}}
|
||||
{"case":"Fillet__shell_fillet2.smb","deferred":null,"error":null,"ints":{"check":[2],"edges":[10],"faces":[3],"kind":[2],"loops":[3],"rt_check":[2],"rt_edges":[10],"rt_faces":[3],"rt_kind":[2],"rt_loops":[3],"rt_shells":[1],"rt_vertices":[8],"shells":[1],"vertices":[8]},"topology":null,"values":{"area":[89.56453664827363],"rt_area":[89.56453664827362],"rt_volume":["NaN"],"volume":["NaN"]}}
|
||||
{"case":"Fillet__sphere_dif_box.smb","deferred":null,"error":null,"ints":{"check":[2],"edges":[15],"faces":[6],"kind":[2],"loops":[7],"rt_check":[2],"rt_edges":[15],"rt_faces":[6],"rt_kind":[2],"rt_loops":[7],"rt_shells":[1],"rt_vertices":[10],"shells":[1],"vertices":[10]},"topology":null,"values":{"area":[700.7752631524191],"rt_area":[700.7752631524193],"rt_volume":["NaN"],"volume":["NaN"]}}
|
||||
{"case":"Fillet__sphere_diff_3cylinders.smb","deferred":null,"error":null,"ints":{"check":[4],"edges":[39],"faces":[8],"kind":[1],"loops":[10],"rt_check":[4],"rt_edges":[39],"rt_faces":[8],"rt_kind":[1],"rt_loops":[10],"rt_shells":[2],"rt_vertices":[25],"shells":[2],"vertices":[25]},"topology":null,"values":{"area":[38236.6057087192],"rt_area":[38236.605708719195],"rt_volume":[169497.31811078507],"volume":[169497.31811078504]}}
|
||||
{"case":"Fillet__sphere_diff_3cylinders.smb","deferred":null,"error":null,"ints":{"check":[4],"edges":[39],"faces":[8],"kind":[1],"loops":[10],"rt_check":[4],"rt_edges":[39],"rt_faces":[8],"rt_kind":[1],"rt_loops":[10],"rt_shells":[2],"rt_vertices":[25],"shells":[2],"vertices":[25]},"topology":null,"values":{"area":[38236.605488862486],"rt_area":[38236.60548886247],"rt_volume":[169497.32069403236],"volume":[169497.32069403233]}}
|
||||
{"case":"Fillet__sphere_plane.smb","deferred":null,"error":null,"ints":{"check":[0],"edges":[4],"faces":[2],"kind":[2],"loops":[3],"rt_check":[0],"rt_edges":[4],"rt_faces":[2],"rt_kind":[2],"rt_loops":[3],"rt_shells":[1],"rt_vertices":[3],"shells":[1],"vertices":[3]},"topology":null,"values":{"area":[309.8222235539387],"rt_area":[309.82222355393867],"rt_volume":["NaN"],"volume":["NaN"]}}
|
||||
{"case":"Fillet__test1.smb","deferred":null,"error":null,"ints":{"check":[2],"edges":[3],"faces":[3],"kind":[1],"loops":[3],"rt_check":[2],"rt_edges":[3],"rt_faces":[3],"rt_kind":[1],"rt_loops":[3],"rt_shells":[2],"rt_vertices":[2],"shells":[2],"vertices":[2]},"topology":null,"values":{"area":[579.1336692771525],"rt_area":[579.1336692771525],"rt_volume":[811.4006064513735],"volume":[811.4006064513738]}}
|
||||
{"case":"Fillet__test4.smb","deferred":null,"error":null,"ints":{"check":[0],"edges":[13],"faces":[6],"kind":[1],"loops":[7],"rt_check":[0],"rt_edges":[13],"rt_faces":[6],"rt_kind":[1],"rt_loops":[7],"rt_shells":[2],"rt_vertices":[10],"shells":[2],"vertices":[10]},"topology":null,"values":{"area":[412.5110519281873],"rt_area":[412.5110519281874],"rt_volume":[645.1938974355568],"volume":[645.1938974355569]}}
|
||||
|
||||
+10
-10
@@ -3588,25 +3588,25 @@
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu18","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[["-inf","inf","-inf"],["-inf","inf","NaN"],[98.97259689183244,"inf","NaN"],[1.25001,1.25001,3.00005]],"n":773}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu18_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"]],"n":683}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu21","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"],[-44.467192069168405,204.2987727004271,-448.2981475686357]],"n":551}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu21_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[["-inf","inf","-inf"],[14049.679912312768,14530.591308975509,223741.5281999481]],"n":341}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu21_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"],[-44.467192069168405,204.2987727004271,-448.2981475686357]],"n":551}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu24","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[13647.907401829725,"inf","NaN"]],"n":104}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu24_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[13647.907429515177,"inf","NaN"]],"n":104}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu27","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[29.518459439146298,"inf","NaN"]],"n":134}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu27_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[29.518459439146298,"inf","NaN"]],"n":134}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu3","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[92.25001001364402,"inf","NaN"]],"n":134}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu30","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[["-inf","inf","-inf"],[53.13585495904329,"inf","NaN"]],"n":503}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu30_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"]],"n":398}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu30","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[6.7812498163778825,"inf","NaN"],[46.354605142665406,50.354605142665406,1250.8996385610471]],"n":304}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu30_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[42242.3991041244,42799.65158359198,2998132.0276400466]],"n":210}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu33","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[14585.238159199165,"inf","NaN"]],"n":104}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu33_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[14585.238186884617,"inf","NaN"]],"n":104}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu36","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[["-inf","inf","-inf"],[53.01085433804352,"inf","NaN"]],"n":473}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu36_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"]],"n":443}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu39","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[["-inf","inf","-inf"],[54.8858558304779,"inf","NaN"]],"n":473}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu39_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"]],"n":443}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu36","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[52.260844402800764,"inf","NaN"],[0.750009935242753,4.750009935242753,11.750129245523434]],"n":269}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu36_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[42404.0717875365,42800.47180477368,2446584.1067375746]],"n":170}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu39","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[54.135845807867504,"inf","NaN"],[0.750010022610394,4.750010022610393,4.750130248714335]],"n":269}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu39_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[42693.93243213361,42699.93243213361,2364539.923423812]],"n":170}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu3_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[91.25001001364402,"inf","NaN"]],"n":134}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu42","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[["-inf","inf","-inf"],[13959.932844740102,14521.185307202177,10835.443479884205]],"n":326}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu42_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[["-inf","inf","-inf"]],"n":221}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu45","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"],[12.531249771808213,"inf","NaN"],[64.42272077414648,68.42272077414648,1952.3528589533628]],"n":818}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu45_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"]],"n":758}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu42_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[["-inf","inf","-inf"],[13959.932844740102,14521.185307202177,10835.443479884205]],"n":326}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu45","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[0.380126001095241,"inf","NaN"],[76.95397054595469,"inf","NaN"]],"n":409}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu45_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[41890.2236008958,42743.3884665193,2860011.8445197754]],"n":210}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu6","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[86.28205982056194,"inf","NaN"]],"n":134}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu6_rebuild","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[[86.28205983008033,"inf","NaN"]],"n":134}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_create_eu9","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"state":{"blocks":[["-inf","inf","-inf"],["-inf","inf","NaN"],[139.75498040225685,"inf","NaN"],[2.75001,2.75001,8.25005]],"n":773}}}
|
||||
|
||||
+15
-15
@@ -1556,29 +1556,29 @@
|
||||
{"case":"own_reader_shell_fillet_uv_trim_f9","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[0.0,"inf","NaN"],[56.0,"inf","NaN"]],"n":375}}}
|
||||
{"case":"own_reader_shell_fillet_uv_trim_f9_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[0.0,"inf","NaN"],[49.0,"inf","NaN"]],"n":375}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f0","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[140.00072126277675,140.00072126277675,24388.03204726756],[198.0,198.0,23912.0],[199.0,199.0,26789.0],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.622623539557346,"inf","NaN"],[0.607026269686191,"inf","NaN"],[14.01322224723673,"inf","NaN"],[0.377376469747195,"inf","NaN"],[0.385986062171385,"inf","NaN"],[1.256306757648062,"inf","NaN"],[3.181495842996486,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[4.937500090441592,"inf","NaN"],[14060.260845480463,"inf","NaN"],[0.547888570513785,"inf","NaN"],[1.0000000109152118,"inf","NaN"],[17.96875,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[1.097052291376683,"inf","NaN"],[2.875,"inf","NaN"],[3069.4529915631347,3303.4529915631347,672191.0512914367],[-47.0,47.0,-1128.0]],"n":7727}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f0_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[139.10557885695604,139.10557885695604,23371.627901390864],[197.0,197.0,25083.0],[168.0,168.0,21348.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","NaN"],[3334.0,3560.0,353163.0]],"n":12197}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f0_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[139.0011642299982,139.0011642299982,23365.053309694275],[197.0,197.0,25083.0],[168.0,168.0,21348.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[14084.254063440378,"inf","NaN"],[56306.55491583431,"inf","NaN"],[14062.673929926055,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],[28138.694439688006,"inf","NaN"],[14071.372760298467,"inf","NaN"],[2925.8787305941823,"inf","NaN"],[413.0,451.0,4588.0]],"n":8214}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f1","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[140.00072120657632,140.00072120657632,24388.032046986555],[198.0,198.0,23912.0],[199.0,199.0,26789.0],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.622623539557346,"inf","NaN"],[0.607026269686191,"inf","NaN"],[14.01322224723673,"inf","NaN"],[0.377376469747195,"inf","NaN"],[0.385986062171385,"inf","NaN"],[1.256306757648062,"inf","NaN"],[3.181495842996486,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[4.937500090441592,"inf","NaN"],[14060.260845480463,"inf","NaN"],[0.547888570513785,"inf","NaN"],[1.0000000109152118,"inf","NaN"],[17.96875,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[1.097052291376683,"inf","NaN"],[2.875,"inf","NaN"],[3069.4529915631347,3303.4529915631347,672191.0512914367],[-47.0,47.0,-1128.0]],"n":7727}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f10","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[139.00083890245563,139.00083890245563,24241.037765654626],[204.0,204.0,24695.0],[192.0,192.0,26194.0],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],[0.0,"inf","NaN"],[2.781250200667249,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.38748000215816,"inf","NaN"],[21.138362449521406,"inf","NaN"],[1.243693230685293,"inf","NaN"],[0.9781105429883359,"inf","NaN"],[0.8509580229634299,"inf","NaN"],[0.500000045220796,"inf","NaN"],[21.625000113051968,"inf","NaN"],[0.0,"inf","NaN"],[14016.782049820562,"inf","NaN"],[9.875,"inf","NaN"],[0.6282699396503251,"inf","NaN"],[0.7021114516176461,"inf","NaN"],[1.518312948768537,"inf","NaN"],[1.326503849178321,"inf","NaN"],[0.495223657589985,"inf","NaN"],[9.245019560826721,"inf","NaN"],[46.375,"inf","NaN"],[0.0,"inf","NaN"],[0.46875,"inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[318.8787313593015,"inf","NaN"],[3259.0,3395.0,121020.0]],"n":9071}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f10_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[140.12274387359562,140.12274387359562,23516.06493457978],[201.0,201.0,25607.0],[160.0,160.0,20186.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],[3569.0,3741.0,258360.0]],"n":12671}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f10","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[139.00076514876932,139.00076514876932,24241.03341323568],[204.0,204.0,24695.0],[192.0,192.0,26194.0],["-inf","inf","-inf"],["-inf","inf","-inf"],[0.627831893428508,"inf","NaN"],[1.544811645066965,"inf","NaN"],[0.0,"inf","NaN"],[0.612247494073334,"inf","NaN"],[0.99111464250743,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[14.635845496698849,"inf","NaN"],[6.712974886753875,"inf","NaN"],[0.615004748898924,"inf","NaN"],[0.344853503209816,"inf","NaN"],[14041.422008285488,"inf","NaN"],[0.0,"inf","NaN"],[3.375,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[1.153037575152758,"inf","NaN"],[13.8125,"inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],[3586.0,3860.0,410896.0]],"n":8384}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f10_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[140.0011931083634,140.0011931083634,23509.054620652263],[201.0,201.0,25607.0],[160.0,160.0,20186.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[6.121268640698485,"inf","NaN"],[56317.23011873284,56337.23011873284,5636798.7421218455],[28117.502186244674,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],[14085.281018069867,"inf","NaN"],[14050.12126861679,"inf","NaN"],[2382.8787305941823,"inf","NaN"],[1390.0,1452.0,23501.0]],"n":9258}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f11","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[145.0007664060987,145.0007664060987,25339.03420368681],[200.0,200.0,24243.0],[193.0,193.0,26127.0],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.622623539557346,"inf","NaN"],[0.7376583805978728,"inf","NaN"],[0.614013934136671,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[4.562499946543236,"inf","NaN"],[8.875000283160691,"inf","NaN"],[0.930228325578023,"inf","NaN"],[0.6903596033974411,"inf","NaN"],[1.50138541536323,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.363858838416999,"inf","NaN"],[0.636141222460545,"inf","NaN"],[0.633230701817902,"inf","NaN"],[0.432484675676463,"inf","NaN"],[4.5,"inf","NaN"],[0.234375,"inf","NaN"],[0.0,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],[2735.0,2875.0,87270.0]],"n":8810}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f11_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[144.1057009654512,144.1057009654512,24322.633660813342],[199.0,199.0,25414.0],[158.0,158.0,19878.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],[14036.24999997544,"inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[130.56263336428816,130.56263336428816,21473.189991225765],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],[-1.7976931348623157e+308,"inf","NaN"],[2512.0,2616.0,88020.0]],"n":12875}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f1_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[139.1055788007556,139.1055788007556,23371.627901109863],[197.0,197.0,25083.0],[168.0,168.0,21432.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","NaN"],[3334.0,3560.0,353163.0]],"n":12197}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f11_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[144.00120937332017,144.00120937332017,24316.05546611353],[199.0,199.0,25414.0],[158.0,158.0,19878.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[14084.254063440378,"inf","NaN"],[42295.27686814744,"inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],[28141.269605346326,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],[1.655983376773202,"inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[-1.7976931348623157e+308,1.7976931348623157e+308,"-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],[28162.055977092834,"inf","NaN"],[760.8787305941823,"inf","NaN"],[2994.0,3126.0,146930.0]],"n":9312}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f1_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[139.00116417379775,139.00116417379775,23365.053309413273],[197.0,197.0,25083.0],[168.0,168.0,21432.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[14084.254063440378,"inf","NaN"],[56306.55491583431,"inf","NaN"],[14062.673929926055,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],[28138.694439688006,"inf","NaN"],[14071.372760298467,"inf","NaN"],[2925.8787305941823,"inf","NaN"],[413.0,451.0,4588.0]],"n":8214}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f2","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[139.0007501411419,139.0007501411419,24241.033358225548],[204.0,204.0,24695.0],[192.0,192.0,26194.0],["-inf","inf","-inf"],["-inf","inf","-inf"],[2.501354898858864,"inf","NaN"],[0.604926952308133,"inf","NaN"],[2.4687497794120334,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[14100.953960418106,"inf","NaN"],[28.538292044498498,"inf","NaN"],[0.506306759122957,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[7.937499721721261,"inf","NaN"],[9.12126861678851,"inf","NaN"],[5.383609579994628,"inf","NaN"],[0.651503341011033,"inf","NaN"],[0.0,"inf","NaN"],[14014.717950179438,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[25.359375,"inf","NaN"],[0.703125,"inf","NaN"],[0.931687912458321,"inf","NaN"],[1.168427994014178,"inf","NaN"],[8.749999975440772,"inf","NaN"],[0.624499212435294,"inf","NaN"],[14089.327991563134,"inf","NaN"],[27.91329261475549,"inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],[3418.8787313593016,"inf","NaN"],[159.0,259.0,-1061.0]],"n":8756}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f2_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[140.105667184024,140.105667184024,23515.631592332105],[201.0,201.0,25607.0],[160.0,160.0,20186.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],[3569.0,3741.0,258360.0]],"n":12671}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f2_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[140.0011931083634,140.0011931083634,23509.054620652263],[201.0,201.0,25607.0],[160.0,160.0,20186.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[6.121268640698485,"inf","NaN"],[56317.23011873284,56337.23011873284,5636798.7421218455],[28117.502186244674,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],[14085.281018069867,"inf","NaN"],[14050.12126861679,"inf","NaN"],[2382.8787305941823,"inf","NaN"],[1390.0,1452.0,23501.0]],"n":9258}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f3","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[145.0007664060987,145.0007664060987,25339.03420368681],[200.0,200.0,24243.0],[193.0,193.0,26127.0],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.622623539557346,"inf","NaN"],[0.7376583805978728,"inf","NaN"],[0.614013934136671,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[4.562499946543236,"inf","NaN"],[8.875000283160691,"inf","NaN"],[0.930228325578023,"inf","NaN"],[0.6903596033974411,"inf","NaN"],[1.50138541536323,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.363858838416999,"inf","NaN"],[0.636141222460545,"inf","NaN"],[0.633230701817902,"inf","NaN"],[0.432484675676463,"inf","NaN"],[4.5,"inf","NaN"],[0.234375,"inf","NaN"],[0.0,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],[2735.0,2875.0,87270.0]],"n":8810}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f3_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[144.1057009654512,144.1057009654512,24322.633660813342],[199.0,199.0,25414.0],[158.0,158.0,19878.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],[14036.24999997544,"inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[130.56263336428816,130.56263336428816,21473.189991225765],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],[-1.7976931348623157e+308,"inf","NaN"],[2512.0,2616.0,88020.0]],"n":12875}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f3_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[144.00120937332017,144.00120937332017,24316.05546611353],[199.0,199.0,25414.0],[158.0,158.0,19878.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[14084.254063440378,"inf","NaN"],[42295.27686814744,"inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],[28141.269605346326,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],[1.655983376773202,"inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[-1.7976931348623157e+308,1.7976931348623157e+308,"-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],[28162.055977092834,"inf","NaN"],[760.8787305941823,"inf","NaN"],[2994.0,3126.0,146930.0]],"n":9312}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f4","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[140.00072135808495,140.00072135808495,24388.0320477441],[200.0,200.0,24309.0],[197.0,197.0,26521.0],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.622623539557346,"inf","NaN"],[0.607026269686191,"inf","NaN"],[14.01322224723673,"inf","NaN"],[0.377376469747195,"inf","NaN"],[0.385986062171385,"inf","NaN"],[1.256306757648062,"inf","NaN"],[3.181495842996486,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[4.937500090441592,"inf","NaN"],[14060.260845480463,"inf","NaN"],[0.547888570513785,"inf","NaN"],[1.0000000109152118,"inf","NaN"],[17.96875,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[1.097052291376683,"inf","NaN"],[2.875,"inf","NaN"],[500.5370248582064,"inf","NaN"],[2756.0,2898.0,94206.0]],"n":7787}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f4_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[139.10557895226424,139.10557895226424,23371.627901867407],[199.0,199.0,25480.0],[166.0,166.0,20776.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],[3589.8787305941823,"inf","NaN"]],"n":12257}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f4_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[139.0011643253064,139.0011643253064,23365.053310170817],[199.0,199.0,25480.0],[166.0,166.0,20776.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[14084.254063440378,"inf","NaN"],[56306.554915839115,"inf","NaN"],[14062.673929926055,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],[28138.694439681436,"inf","NaN"],[28165.852016445562,"inf","NaN"],[822.8787305941823,"inf","NaN"],[2767.0,2879.0,110670.0]],"n":8274}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f5","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[142.000721358085,142.000721358085,24895.0320477441],[198.0,198.0,23912.0],[197.0,197.0,26485.0],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.622623539557346,"inf","NaN"],[0.607026269686191,"inf","NaN"],[14.01322224723673,"inf","NaN"],[0.377376469747195,"inf","NaN"],[0.385986062171385,"inf","NaN"],[1.256306757648062,"inf","NaN"],[3.181495842996486,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[4.937500090441592,"inf","NaN"],[0.0,"inf","NaN"],[0.38717430653571,"inf","NaN"],[4.499999983627182,"inf","NaN"],[0.651503341011033,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[6.0619667862920394,"inf","NaN"],[0.703125,"inf","NaN"],[5.296875,"inf","NaN"],[1.094016564548872,"inf","NaN"],[1.3967114396209679,"inf","NaN"],[520.3279915631344,"inf","NaN"],[2622.0,2764.0,76116.0]],"n":7787}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f5_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[141.10558024264185,141.10558024264185,23878.627909385268],[197.0,197.0,25083.0],[166.0,166.0,20812.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],[3455.8787305941823,"inf","NaN"]],"n":12257}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f6","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[140.00076641651498,140.00076641651498,24388.034119569627],[200.0,200.0,24192.0],[201.0,201.0,26638.0],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.622623539557346,"inf","NaN"],[0.607026269686191,"inf","NaN"],[14.01322224723673,"inf","NaN"],[0.377376469747195,"inf","NaN"],[0.385986062171385,"inf","NaN"],[1.256306757648062,"inf","NaN"],[3.181495842996486,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[4.937500090441592,"inf","NaN"],[14060.260845480463,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],[0.665993714641963,"inf","NaN"],[0.461203992965499,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[49.5,"inf","NaN"],[33.65625,"inf","NaN"],[1.6875,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[1.34424468953775,"inf","NaN"],[44.327991563134425,"inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[3678.121267875579,"inf","NaN"]],"n":9662}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f6_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[139.10571597255495,139.10571597255495,23371.63430992081],[200.0,200.0,25374.0],[165.0,165.0,20382.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[3904.8787305941823,"inf","NaN"]],"n":13292}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f7","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[140.0623998215131,140.0623998215131,24391.43445984626],[201.0,201.0,24053.0],[201.0,201.0,26526.0],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.622623539557346,"inf","NaN"],[0.607026269686191,"inf","NaN"],[14.01322224723673,"inf","NaN"],[0.377376469747195,"inf","NaN"],[0.385986062171385,"inf","NaN"],[1.256306757648062,"inf","NaN"],[3.181495842996486,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[4.937500090441592,"inf","NaN"],[14060.260845480463,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[16.0625,"inf","NaN"],[0.363858838416999,"inf","NaN"],[0.500000030430587,"inf","NaN"],[0.572121238856122,"inf","NaN"],[0.369679771188574,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[50.449411381702596,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[570.0,636.0,19197.0]],"n":9509}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f7_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[139.11529157001132,139.11529157001132,23371.66906573886],[200.0,200.0,25224.0],[166.0,166.0,20277.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],[3275.0,3381.0,148616.0]],"n":13139}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f5_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[141.00116432530643,141.00116432530643,23872.053310170817],[197.0,197.0,25083.0],[166.0,166.0,20812.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[14084.254063440378,"inf","NaN"],[56306.55491583431,"inf","NaN"],[2.237658380597873,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[14297.875133394718,14297.875133394718,3604808.0328150094],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],[14068.622353373079,"inf","NaN"],[28165.852016442896,"inf","NaN"],[939.8787305941823,"inf","NaN"],[2516.0,2632.0,97743.0]],"n":8274}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f6","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[140.00072094027206,140.00072094027206,24388.03204537571],[200.0,200.0,24192.0],[201.0,201.0,26638.0],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.622623539557346,"inf","NaN"],[0.607026269686191,"inf","NaN"],[14.01322224723673,"inf","NaN"],[0.377376469747195,"inf","NaN"],[0.385986062171385,"inf","NaN"],[1.256306757648062,"inf","NaN"],[3.181495842996486,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[4.937500090441592,"inf","NaN"],[14060.260845480463,"inf","NaN"],[0.9020594442071661,"inf","NaN"],[17.3125,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.915993752113529,"inf","NaN"],[1.6334696869106948,"inf","NaN"],[1.403137529880131,"inf","NaN"],[1.344016601396374,"inf","NaN"],[0.346675075588845,"inf","NaN"],[0.543641066739557,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[14068.477678136038,"inf","NaN"],[1068.1212678755792,"inf","NaN"],[2610.0,2734.0,86404.0]],"n":8027}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f6_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[139.00116390749355,139.00116390749355,23365.053307802424],[200.0,200.0,25374.0],[165.0,165.0,20382.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[14084.254063440378,"inf","NaN"],[56306.55491583431,"inf","NaN"],[14062.673929926055,"inf","NaN"],[2.181495263843738,"inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[28144.931779506347,"inf","NaN"],[14077.582677298837,"inf","NaN"],[42223.03098605711,"inf","NaN"],[1695.8787305941823,"inf","NaN"],[2209.0,2305.0,85330.0]],"n":8514}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f7","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[140.06236498915212,140.06236498915212,24391.43266929326],[201.0,201.0,24053.0],[201.0,201.0,26526.0],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.622623539557346,"inf","NaN"],[0.607026269686191,"inf","NaN"],[14.01322224723673,"inf","NaN"],[0.377376469747195,"inf","NaN"],[0.385986062171385,"inf","NaN"],[1.256306757648062,"inf","NaN"],[3.181495842996486,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[4.937500090441592,"inf","NaN"],[14060.260845480463,"inf","NaN"],["-inf","inf","NaN"],[0.0,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.902947696154476,"inf","NaN"],[47.5,"inf","NaN"],[31.78125,"inf","NaN"],[1.4375,"inf","NaN"],[0.372590255638126,"inf","NaN"],[0.60249719256876,"inf","NaN"],[14242.74999997544,"inf","NaN"],[0.0,"inf","NaN"],[46.327991563134425,"inf","NaN"],[42205.79708914593,"inf","NaN"],[3873.0,4045.0,276980.0]],"n":8079}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f7_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[139.06280795637355,139.06280795637355,23368.453931719978],[200.0,200.0,25224.0],[166.0,166.0,20277.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[14084.254063440378,"inf","NaN"],[56306.55491583431,"inf","NaN"],[14062.673929926055,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[28120.42436499621,"inf","NaN"],[14066.528261468744,"inf","NaN"],[0.0,"inf","NaN"],[42426.38879510533,"inf","NaN"],[3966.0,4116.0,277007.0]],"n":8566}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f8","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[140.00072126277675,140.00072126277675,24388.03204726756],[198.0,198.0,23912.0],[199.0,199.0,26789.0],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.622623539557346,"inf","NaN"],[0.607026269686191,"inf","NaN"],[14.01322224723673,"inf","NaN"],[0.377376469747195,"inf","NaN"],[0.385986062171385,"inf","NaN"],[1.256306757648062,"inf","NaN"],[3.181495842996486,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[4.937500090441592,"inf","NaN"],[14060.260845480463,"inf","NaN"],[0.547888570513785,"inf","NaN"],[1.0000000109152118,"inf","NaN"],[17.96875,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[1.097052291376683,"inf","NaN"],[2.875,"inf","NaN"],[3069.4529915631347,3303.4529915631347,672191.0512914367],[-47.0,47.0,-1128.0]],"n":7727}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f8_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[139.10557885695604,139.10557885695604,23371.627901390864],[197.0,197.0,25083.0],[168.0,168.0,21348.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","NaN"],[3334.0,3560.0,353163.0]],"n":12197}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f8_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[139.0011642299982,139.0011642299982,23365.053309694275],[197.0,197.0,25083.0],[168.0,168.0,21348.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[14084.254063440378,"inf","NaN"],[56306.55491583431,"inf","NaN"],[14062.673929926055,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],[28138.694439688006,"inf","NaN"],[14071.372760298467,"inf","NaN"],[2925.8787305941823,"inf","NaN"],[413.0,451.0,4588.0]],"n":8214}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f9","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[140.00072120657632,140.00072120657632,24388.032046986555],[198.0,198.0,23912.0],[199.0,199.0,26789.0],[1.7976931348623157e+308,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[0.622623539557346,"inf","NaN"],[0.607026269686191,"inf","NaN"],[14.01322224723673,"inf","NaN"],[0.377376469747195,"inf","NaN"],[0.385986062171385,"inf","NaN"],[1.256306757648062,"inf","NaN"],[3.181495842996486,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[4.937500090441592,"inf","NaN"],[14060.260845480463,"inf","NaN"],[0.547888570513785,"inf","NaN"],[1.0000000109152118,"inf","NaN"],[17.96875,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[1.7976931348623157e+308,"inf","NaN"],[0.0,"inf","NaN"],[-1.7976931348623157e+308,"inf","NaN"],[1.097052291376683,"inf","NaN"],[2.875,"inf","NaN"],[3069.4529915631347,3303.4529915631347,672191.0512914367],[-47.0,47.0,-1128.0]],"n":7727}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f9_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[139.1055788007556,139.1055788007556,23371.627901109863],[197.0,197.0,25083.0],[168.0,168.0,21432.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","NaN"],["-inf","inf","NaN"],[3334.0,3560.0,353163.0]],"n":12197}}}
|
||||
{"case":"own_reader_sphere_diff_3cylinders_uv_trim_f9_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[139.00116417379775,139.00116417379775,23365.053309413273],[197.0,197.0,25083.0],[168.0,168.0,21432.0],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[14084.254063440378,"inf","NaN"],[56306.55491583431,"inf","NaN"],[14062.673929926055,"inf","NaN"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],[28138.694439688006,"inf","NaN"],[14071.372760298467,"inf","NaN"],[2925.8787305941823,"inf","NaN"],[413.0,451.0,4588.0]],"n":8214}}}
|
||||
{"case":"own_reader_sphere_plane_uv_trim_f0","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[49.33551538373308,"inf","NaN"],[176.55357558408133,176.55357558408133,25441.291313931735],[53.93754585814637,67.93754585814636,1690.6767390580662]],"n":3905}}}
|
||||
{"case":"own_reader_sphere_plane_uv_trim_f0_rebuild","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","NaN"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[9.0,11.0,8.0]],"n":3074}}}
|
||||
{"case":"own_reader_sphere_plane_uv_trim_f1","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.0,"inf","NaN"],[0.9769584498411612,"inf","NaN"],[82.44288867234621,82.44288867234621,13276.878940856855],[191.40678970377306,207.40678970377306,25133.62396542679]],"n":2813}}}
|
||||
|
||||
+3
-1
@@ -79,7 +79,9 @@ fn outcome(e: &SolidError, notes: &mut Vec<String>) -> Result<Outcome, Deferred>
|
||||
SolidError::InvalidInput { .. } => {
|
||||
Outcome::Code(f64::from(solidcore::error::SM_ERR_INVALID_INPUT))
|
||||
}
|
||||
SolidError::Upstream { code, .. } => Outcome::Code(f64::from(*code)),
|
||||
up_err if solidcore::error::upstream_status(up_err).is_some() => Outcome::Code(f64::from(
|
||||
solidcore::error::upstream_status(up_err).unwrap_or(-1),
|
||||
)),
|
||||
other => {
|
||||
if notes.len() < 20 {
|
||||
notes.push(other.to_string());
|
||||
|
||||
@@ -1547,16 +1547,15 @@ mod tests {
|
||||
let eu = model.b[0].add_edgeuse_record(Edgeuse::default())?;
|
||||
let mut point = Point3::new(7., 8., 9.);
|
||||
let mut normal = Point3::new(3., 2., 1.);
|
||||
assert!(matches!(
|
||||
model.binormal(
|
||||
assert!(model
|
||||
.binormal(
|
||||
(BrepId::from_index(0), eu),
|
||||
0.5,
|
||||
false,
|
||||
&mut point,
|
||||
&mut normal
|
||||
),
|
||||
Err(SolidError::Upstream { code: 1007, .. })
|
||||
));
|
||||
)
|
||||
.is_err_and(|e| solidcore::error::upstream_status(&e) == Some(1007)));
|
||||
assert_eq!(point, Point3::new(7., 8., 9.));
|
||||
assert_eq!(normal, Point3::new(3., 2., 1.));
|
||||
Ok(())
|
||||
|
||||
@@ -176,7 +176,7 @@ fn output(raw: &[f64]) -> Result<Output, HarnessError> {
|
||||
/// The status code of a wrapper error (`SM_SUCCESS` is `0`).
|
||||
fn status(e: &SolidError) -> Option<f64> {
|
||||
match e {
|
||||
SolidError::Upstream { code, .. } => Some(f64::from(*code)),
|
||||
up_err if solidcore::error::upstream_status(up_err).is_some() => Some(f64::from(solidcore::error::upstream_status(up_err).unwrap_or(-1))),
|
||||
SolidError::InvalidInput { .. } => Some(1007.),
|
||||
_ => None,
|
||||
}
|
||||
@@ -393,7 +393,7 @@ fn put_trims(k: &Kernel, o: &mut Vec<f64>, b: &Brep) -> SolidResult<()> {
|
||||
match c.evaluate_point(k, d.evaluate(f)) {
|
||||
Ok(p) => o.extend([0.0, p.x, p.y, p.z]),
|
||||
Err(e) if e.is_unported() => return Err(e),
|
||||
Err(SolidError::Upstream { code, .. }) => o.push(f64::from(code)),
|
||||
Err(up_err) if solidcore::error::upstream_status(&up_err).is_some() => o.push(f64::from(solidcore::error::upstream_status(&up_err).unwrap_or(-1))),
|
||||
Err(_) => o.push(-1.0),
|
||||
}
|
||||
}
|
||||
@@ -687,7 +687,7 @@ fn write_rejection(s: &WriteSrc) -> Option<String> {
|
||||
fn write_status(e: &SolidError) -> i64 {
|
||||
match e {
|
||||
SolidError::InvalidInput { .. } => 1007,
|
||||
SolidError::Upstream { code, .. } => i64::from(*code),
|
||||
up_err if solidcore::error::upstream_status(up_err).is_some() => i64::from(solidcore::error::upstream_status(up_err).unwrap_or(-1)),
|
||||
_ => -1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ fn output(raw: &[f64]) -> Result<Output, HarnessError> {
|
||||
/// The status code of a wrapper error (`SM_SUCCESS` is `0`).
|
||||
fn status(e: &SolidError) -> Option<f64> {
|
||||
match e {
|
||||
SolidError::Upstream { code, .. } => Some(f64::from(*code)),
|
||||
up_err if solidcore::error::upstream_status(up_err).is_some() => Some(f64::from(solidcore::error::upstream_status(up_err).unwrap_or(-1))),
|
||||
SolidError::InvalidInput { .. } => Some(1007.),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -516,8 +516,8 @@ fn update_rust(k: &mut Kernel, b: &mut Brep, i: &Inputs) -> Result<Output, Harne
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(solidcore::SolidError::Upstream { code, .. }) => {
|
||||
out.push(f64::from(code));
|
||||
Err(up_err) if solidcore::error::upstream_status(&up_err).is_some() => {
|
||||
out.push(f64::from(solidcore::error::upstream_status(&up_err).unwrap_or(-1)));
|
||||
return update_output(&out);
|
||||
}
|
||||
Err(e) => return Ok(Output::unported(&e).unwrap_or_else(|| Output::err(e.to_string()))),
|
||||
|
||||
@@ -161,7 +161,7 @@ fn to_output(items: &[Item]) -> Output {
|
||||
/// Upstream status of a Rust error.
|
||||
fn status(e: &SolidError) -> i64 {
|
||||
match e {
|
||||
SolidError::Upstream { code, .. } => i64::from(*code),
|
||||
up_err if solidcore::error::upstream_status(up_err).is_some() => i64::from(solidcore::error::upstream_status(up_err).unwrap_or(-1)),
|
||||
_ => -1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ pub(super) fn status(r: SolidResult<()>) -> SolidResult<f64> {
|
||||
match r {
|
||||
Ok(()) => Ok(0.),
|
||||
Err(e) if e.is_unported() => Err(e),
|
||||
Err(SolidError::Upstream { code, .. }) => Ok(f64::from(code)),
|
||||
Err(up_err) if solidcore::error::upstream_status(&up_err).is_some() => Ok(f64::from(solidcore::error::upstream_status(&up_err).unwrap_or(-1))),
|
||||
// a failure in solidcore's terms (#2615): its C++ comparison status
|
||||
Err(e @ SolidError::Failed(_)) => Ok(f64::from(e.sm_status().unwrap_or(solidcore::error::SM_ERR))),
|
||||
Err(SolidError::InvalidInput { .. } | SolidError::PortInvariant { .. }) => Ok(1007.),
|
||||
|
||||
@@ -133,7 +133,7 @@ fn path(i: &Inputs) -> Result<String, HarnessError> {
|
||||
/// The status code of a section call (`SM_SUCCESS` is `0`).
|
||||
fn status(e: &SolidError) -> Option<f64> {
|
||||
match e {
|
||||
SolidError::Upstream { code, .. } => Some(f64::from(*code)),
|
||||
up_err if solidcore::error::upstream_status(up_err).is_some() => Some(f64::from(solidcore::error::upstream_status(up_err).unwrap_or(-1))),
|
||||
SolidError::InvalidInput { .. } => Some(1007.),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ fn output(open_status: i64, recs: &[Rec]) -> Output {
|
||||
fn status_of(e: &SolidError) -> Result<i64, HarnessError> {
|
||||
match e {
|
||||
SolidError::InvalidInput { .. } | SolidError::PortInvariant { .. } => Ok(ST_SHORT),
|
||||
SolidError::Upstream { code, .. } => Ok(i64::from(*code)),
|
||||
up_err if solidcore::error::upstream_status(up_err).is_some() => Ok(i64::from(solidcore::error::upstream_status(up_err).unwrap_or(-1))),
|
||||
other => Err(HarnessError::new(format!("unexpected error {other}"))),
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -793,7 +793,7 @@ fn ix<I: Copy>(o: Option<I>, f: impl Fn(I) -> u32) -> f64 {
|
||||
fn status_of(r: &Result<(), SolidError>) -> Result<Option<f64>, SolidError> {
|
||||
match r {
|
||||
Ok(()) => Ok(Some(0.0)),
|
||||
Err(SolidError::Upstream { code, .. }) => Ok(Some(f64::from(*code))),
|
||||
Err(up_err) if solidcore::error::upstream_status(up_err).is_some() => Ok(Some(f64::from(solidcore::error::upstream_status(up_err).unwrap_or(-1)))),
|
||||
Err(SolidError::InvalidInput { .. }) => Ok(None),
|
||||
Err(e) => Err(e.clone()),
|
||||
}
|
||||
@@ -1429,7 +1429,7 @@ fn run_rust_op(b: &mut Brep, op: &OpIn) -> Result<Output, Fail> {
|
||||
ix(r.radial_edgeuse, EdgeuseId::index),
|
||||
ix(r.coincident_faceuse, FaceuseId::index),
|
||||
]),
|
||||
Err(SolidError::Upstream { code, .. }) => out.extend([f64::from(code), -1.0, -1.0]),
|
||||
Err(up_err) if solidcore::error::upstream_status(&up_err).is_some() => out.extend([f64::from(solidcore::error::upstream_status(&up_err).unwrap_or(-1)), -1.0, -1.0]),
|
||||
Err(SolidError::InvalidInput { .. }) => out.push(NULL_DEREF),
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
|
||||
@@ -839,7 +839,7 @@ fn eu_state(k: &Kernel, b: &Brep, eu: Option<EdgeuseId>, out: &mut Vec<f64>) ->
|
||||
|
||||
fn status_of(e: &SolidError) -> f64 {
|
||||
match e {
|
||||
SolidError::Upstream { code, .. } => f64::from(*code),
|
||||
up_err if solidcore::error::upstream_status(up_err).is_some() => f64::from(solidcore::error::upstream_status(up_err).unwrap_or(-1)),
|
||||
SolidError::InvalidInput { .. } | SolidError::PortInvariant { .. } => NULL_DEREF as f64,
|
||||
_ => -9.0,
|
||||
}
|
||||
|
||||
@@ -448,7 +448,7 @@ fn run_rust_op(
|
||||
});
|
||||
let code = match status {
|
||||
Ok(()) => Ok(0.0),
|
||||
Err(SolidError::Upstream { code, .. }) => Ok(f64::from(code)),
|
||||
Err(up_err) if solidcore::error::upstream_status(&up_err).is_some() => Ok(f64::from(solidcore::error::upstream_status(&up_err).unwrap_or(-1))),
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
code.map(|c| {
|
||||
@@ -463,7 +463,7 @@ fn run_rust_op(
|
||||
OpIn::Rebuild { .. } => {
|
||||
let status = match eu2::rebuild_uv_trim_curve(&k, b, eu) {
|
||||
Ok(()) => Ok(0.0),
|
||||
Err(SolidError::Upstream { code, .. }) => Ok(f64::from(code)),
|
||||
Err(up_err) if solidcore::error::upstream_status(&up_err).is_some() => Ok(f64::from(solidcore::error::upstream_status(&up_err).unwrap_or(-1))),
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
status.and_then(|s| {
|
||||
|
||||
@@ -154,7 +154,7 @@ fn output_of(body: &[f64]) -> Output {
|
||||
/// The status a failed Rust op reports (`None`: not an upstream status).
|
||||
fn status_of(e: &SolidError) -> Option<f64> {
|
||||
match e {
|
||||
SolidError::Upstream { code, .. } => Some(f64::from(*code)),
|
||||
up_err if solidcore::error::upstream_status(up_err).is_some() => Some(f64::from(solidcore::error::upstream_status(up_err).unwrap_or(-1))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -257,7 +257,7 @@ fn fail(e: &SolidError) -> Fail {
|
||||
fn status<T>(r: &Result<T, SolidError>) -> Result<f64, Fail> {
|
||||
match r {
|
||||
Ok(_) => Ok(0.0),
|
||||
Err(SolidError::Upstream { code, .. }) => Ok(f64::from(*code)),
|
||||
Err(up_err) if solidcore::error::upstream_status(up_err).is_some() => Ok(f64::from(solidcore::error::upstream_status(up_err).unwrap_or(-1))),
|
||||
Err(SolidError::InvalidInput { .. } | SolidError::PortInvariant { .. }) => Ok(SM_ERR_INVALID_INPUT),
|
||||
Err(e) => Err(fail(e)),
|
||||
}
|
||||
|
||||
+1
-1
@@ -500,7 +500,7 @@ fn put_use_state(o: &mut Vec<f64>, b: &Brep) {
|
||||
fn status(r: Result<(), SolidError>) -> Result<Option<f64>, Fail> {
|
||||
match r {
|
||||
Ok(()) => Ok(Some(0.0)),
|
||||
Err(SolidError::Upstream { code, .. }) => Ok(Some(f64::from(code))),
|
||||
Err(up_err) if solidcore::error::upstream_status(&up_err).is_some() => Ok(Some(f64::from(solidcore::error::upstream_status(&up_err).unwrap_or(-1)))),
|
||||
Err(e) => slot(Err(e)).map(|_: Option<()>| None),
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -296,7 +296,7 @@ fn rust_ray(brep: &solidcore::topology::Brep, input: &RayInput) -> Result<Output
|
||||
return Ok(deferred);
|
||||
}
|
||||
match error {
|
||||
solidcore::error::SolidError::Upstream { code, .. } => code,
|
||||
up_err if solidcore::error::upstream_status(&up_err).is_some() => solidcore::error::upstream_status(&up_err).unwrap_or(-1),
|
||||
error => return Err(failure(error)),
|
||||
}
|
||||
}
|
||||
@@ -534,7 +534,6 @@ fn rust_transfer_records(
|
||||
) -> Result<Output, HarnessError> {
|
||||
use solidcore::topology::shell::{faceuse_sibling_links, transfer_children, ShellMembers};
|
||||
use solidcore::topology::{EdgeId, EdgeuseOwner, VertexuseOwner};
|
||||
use solidcore::SolidError;
|
||||
let region = brep
|
||||
.infinite_region()
|
||||
.ok_or_else(|| failure("fixture has no infinite region"))?;
|
||||
@@ -631,7 +630,7 @@ fn rust_transfer_records(
|
||||
let to = ShellId::from_index(u32::try_from(to).map_err(failure)?);
|
||||
pre_status.push(match transfer_children(&mut kernel, brep, from, to, None) {
|
||||
Ok(()) => 0,
|
||||
Err(SolidError::Upstream { code, .. }) => i64::from(code),
|
||||
Err(up_err) if solidcore::error::upstream_status(&up_err).is_some() => i64::from(solidcore::error::upstream_status(&up_err).unwrap_or(-1)),
|
||||
Err(error) => return Output::unported(&error).ok_or_else(|| failure(error)),
|
||||
});
|
||||
}
|
||||
@@ -673,7 +672,7 @@ fn rust_transfer_records(
|
||||
let status =
|
||||
match transfer_children(&mut kernel, brep, source, recipient, candidates.as_deref()) {
|
||||
Ok(()) => 0,
|
||||
Err(SolidError::Upstream { code, .. }) => i64::from(code),
|
||||
Err(up_err) if solidcore::error::upstream_status(&up_err).is_some() => i64::from(solidcore::error::upstream_status(&up_err).unwrap_or(-1)),
|
||||
Err(error) => return Output::unported(&error).ok_or_else(|| failure(error)),
|
||||
};
|
||||
let transfer_marks = mark_delta(&kernel, counters);
|
||||
|
||||
@@ -12,7 +12,6 @@ use solidcore::geom::context::MarkLock;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::topology::topology_traverser::{self as tr, Entity, Marks};
|
||||
use solidcore::topology::{ArenaId, EdgeId, EdgeuseId, FaceId, FaceuseId, ShellId, VertexId};
|
||||
use solidcore::SolidError;
|
||||
|
||||
/// Runner for the owned topology traversals.
|
||||
pub struct Runner;
|
||||
@@ -281,7 +280,7 @@ impl UnitDiff for Runner {
|
||||
return Ok(out);
|
||||
}
|
||||
match e {
|
||||
SolidError::Upstream { code, .. } => status = code,
|
||||
up_err if solidcore::error::upstream_status(&up_err).is_some() => status = solidcore::error::upstream_status(&up_err).unwrap_or(-1),
|
||||
_ => return Err(HarnessError::new(e.to_string())),
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -907,7 +907,7 @@ fn fail(e: &SolidError) -> Fail {
|
||||
fn status<T>(r: &Result<T, SolidError>) -> Result<f64, Fail> {
|
||||
match r {
|
||||
Ok(_) => Ok(0.0),
|
||||
Err(SolidError::Upstream { code, .. }) => Ok(f64::from(*code)),
|
||||
Err(up_err) if solidcore::error::upstream_status(up_err).is_some() => Ok(f64::from(solidcore::error::upstream_status(up_err).unwrap_or(-1))),
|
||||
Err(e) => Err(fail(e)),
|
||||
}
|
||||
}
|
||||
@@ -1066,7 +1066,7 @@ fn put_samples(k: &Kernel, o: &mut Vec<f64>, c: &Curve) {
|
||||
}
|
||||
Err(e) => {
|
||||
o.push(match e {
|
||||
SolidError::Upstream { code, .. } => f64::from(code),
|
||||
up_err if solidcore::error::upstream_status(&up_err).is_some() => f64::from(solidcore::error::upstream_status(&up_err).unwrap_or(-1)),
|
||||
_ => -1.0,
|
||||
});
|
||||
o.extend([0.0, 0.0, 0.0]);
|
||||
|
||||
@@ -546,7 +546,7 @@ fn slot<T>(r: Result<T, SolidError>, f: impl FnOnce(T) -> i64) -> Result<i64, So
|
||||
/// An upstream status: `0` for success, the `SmStatus` of an upstream error.
|
||||
fn status_of(e: &SolidError) -> Result<f64, SolidError> {
|
||||
match e {
|
||||
SolidError::Upstream { code, .. } => Ok(f64::from(*code)),
|
||||
up_err if solidcore::error::upstream_status(up_err).is_some() => Ok(f64::from(solidcore::error::upstream_status(up_err).unwrap_or(-1))),
|
||||
other => Err(other.clone()),
|
||||
}
|
||||
}
|
||||
@@ -861,7 +861,7 @@ fn run_rust_op(b: &mut Brep, op: &OpIn) -> Result<Output, Fail> {
|
||||
.map(|g| vec![0.0, g.map_or(-1.0, |g| flag(g.is_init()))]),
|
||||
3 => match vu1::calc_vertex_face_gap(&k, b, v, *want_out, *f) {
|
||||
Ok(g) => Ok(vec![0.0, g.map_or(-1.0, |g| flag(g.is_init()))]),
|
||||
Err(e @ SolidError::Upstream { .. }) => {
|
||||
Err(e) if solidcore::error::upstream_status(&e).is_some() => {
|
||||
// the output was reset before the failing step
|
||||
Ok(vec![status_of(&e)?, if *want_out { 0.0 } else { -1.0 }])
|
||||
}
|
||||
|
||||
+7
-3
@@ -4,7 +4,7 @@ use solidcore::kernel::Kernel;
|
||||
use solidcore::math::{Point2, Vec3};
|
||||
use solidcore::topology::brep::p1;
|
||||
use solidcore::topology::Brep;
|
||||
use solidcore::{Result, SolidError};
|
||||
use solidcore::Result;
|
||||
|
||||
fn point(out: &mut Vec<f64>, value: Vec3) {
|
||||
out.extend([value.x, value.y, value.z]);
|
||||
@@ -17,7 +17,9 @@ fn sample(out: &mut Vec<f64>, value: Result<Vec3>) -> Result<()> {
|
||||
point(out, p);
|
||||
}
|
||||
Err(e) if e.is_unported() => return Err(e),
|
||||
Err(SolidError::Upstream { code, .. }) => out.push(f64::from(code)),
|
||||
Err(up_err) if solidcore::error::upstream_status(&up_err).is_some() => out.push(f64::from(
|
||||
solidcore::error::upstream_status(&up_err).unwrap_or(-1),
|
||||
)),
|
||||
Err(_) => out.push(-1.0),
|
||||
}
|
||||
Ok(())
|
||||
@@ -143,7 +145,9 @@ pub fn append_pyramid_rays(k: &mut Kernel, brep: &mut Brep, out: &mut Vec<f64>)
|
||||
let status = match attempt.status {
|
||||
Ok(()) => 0.0,
|
||||
Err(e) if e.is_unported() => return Err(e),
|
||||
Err(SolidError::Upstream { code, .. }) => f64::from(code),
|
||||
Err(up_err) if solidcore::error::upstream_status(&up_err).is_some() => {
|
||||
f64::from(solidcore::error::upstream_status(&up_err).unwrap_or(-1))
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let mut trims = 0;
|
||||
|
||||
+3
-1
@@ -41,7 +41,9 @@ pub(crate) fn put_trim_curve(
|
||||
for i in 0..=2u8 {
|
||||
match c.evaluate_point(k, ivl.evaluate(f64::from(i) / 2.0)) {
|
||||
Ok(p) => o.extend([0.0, p.x, p.y, p.z]),
|
||||
Err(SolidError::Upstream { code, .. }) => o.push(f64::from(code)),
|
||||
Err(up_err) if solidcore::error::upstream_status(&up_err).is_some() => o.push(
|
||||
f64::from(solidcore::error::upstream_status(&up_err).unwrap_or(-1)),
|
||||
),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -343,7 +343,9 @@ fn refused(b: &Brep, refusal: &BlendRefusal) -> Option<Error> {
|
||||
tolerance,
|
||||
smallest: smallest_radius,
|
||||
},
|
||||
BlendRefusal::SetbackTooShort { vertex: v, least } => Error::SetbackTooShort {
|
||||
BlendRefusal::SetbackTooShort {
|
||||
vertex: v, least, ..
|
||||
} => Error::SetbackTooShort {
|
||||
vertex: vertex(v)?,
|
||||
least,
|
||||
},
|
||||
|
||||
+3
-2
@@ -198,8 +198,9 @@ pub enum Error {
|
||||
/// The least radius that rounds it.
|
||||
smallest: f64,
|
||||
},
|
||||
/// A corner's setback that stops short of where the rails of the rounds beside an edge
|
||||
/// cross it (the kernel's `BlendRefusal::SetbackTooShort`).
|
||||
/// A corner's setback too short for the corner: it stops short of where the rails of the
|
||||
/// rounds beside an edge cross it, or its fill is not certified at it (the kernel's
|
||||
/// `BlendRefusal::SetbackTooShort`, whose `why` says which).
|
||||
SetbackTooShort {
|
||||
/// The corner's vertex index.
|
||||
vertex: usize,
|
||||
|
||||
+16
-23
@@ -1226,15 +1226,19 @@ fn a_setback_too_long_at_a_corner_is_refused_by_the_kernel() {
|
||||
}
|
||||
|
||||
/// Cancel stops a long fillet promptly (solidcore#3247): a 2 mm round along one helical edge of
|
||||
/// a 20-turn square coil, with its tangent chain (the whole edge), runs for minutes. Cancelled
|
||||
/// from another thread a second in, the call returns `Cancelled` within 500 ms (about 5 ms
|
||||
/// measured on a desktop; the bound leaves room for shared CI runners). nextest runs each test
|
||||
/// in a process of its own, so no other test's call holds the session's kernel meanwhile.
|
||||
/// a 20-turn square coil, with its tangent chain (the whole edge), runs for minutes. Measured in
|
||||
/// the token's polls, not in time: a token cancelled from its `m`-th poll
|
||||
/// (`CancelToken::after_polls`) stops the fillet there. It returns `Cancelled`, which also shows
|
||||
/// the fillet was still running at poll `m`, and the token is polled exactly once after the poll
|
||||
/// that cancelled it: by the session as the call returns, asking whether it was cancelled
|
||||
/// (`session::run_with`, or `with_body` deciding whether to keep its edit). Any further poll
|
||||
/// would be work done after the cancel. `m` is a quarter of the 15,473 polls the fillet made in
|
||||
/// its first second, measured on SolidCraft 9a342278 (cancel-timing round 1). Should the fillet
|
||||
/// get fast enough to finish before `m`, this fails with `Ok`: then pick a longer case.
|
||||
#[test]
|
||||
fn a_cancelled_long_fillet_stops_promptly() {
|
||||
use solidcore_ops::cancel::{within, CancelToken};
|
||||
use solidcore_ops::sweep::{screw_sweep, Curve};
|
||||
use std::time::{Duration, Instant};
|
||||
all_fixes();
|
||||
let corners = [
|
||||
[19.0, 0.0, 0.0],
|
||||
@@ -1273,23 +1277,12 @@ fn a_cancelled_long_fillet_stops_promptly() {
|
||||
})
|
||||
.unwrap();
|
||||
let chain = tangent_chain(&coil, &[edge], 0.1).unwrap();
|
||||
let token = CancelToken::new();
|
||||
let watched = token.clone();
|
||||
let worker = std::thread::spawn(move || {
|
||||
let r = within(&watched, || {
|
||||
blend(&coil, Blend::Round, &chain, &[2.0], &[], Corners::Filled)
|
||||
});
|
||||
(r.map(|_| ()), Instant::now())
|
||||
let m: u64 = 15_473 / 4;
|
||||
let token = CancelToken::after_polls(m);
|
||||
let r = within(&token, || {
|
||||
blend(&coil, Blend::Round, &chain, &[2.0], &[], Corners::Filled)
|
||||
});
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
let cancelled = Instant::now();
|
||||
token.cancel();
|
||||
let (r, returned) = worker.join().unwrap();
|
||||
let took = returned.saturating_duration_since(cancelled);
|
||||
eprintln!("the fillet returned {took:?} after Cancel");
|
||||
assert_eq!(r, Err(Error::Cancelled));
|
||||
assert!(
|
||||
took < Duration::from_millis(500),
|
||||
"returned {took:?} after Cancel"
|
||||
);
|
||||
eprintln!("the fillet cancelled at {m}, polled {:?}", token.polls());
|
||||
assert_eq!(r.map(|_| ()), Err(Error::Cancelled));
|
||||
assert_eq!(token.polls(), Some(m + 1));
|
||||
}
|
||||
|
||||
+19
-21
@@ -255,15 +255,16 @@ fn a_dome_1e_8_off_its_circle_joins_to_its_mirror() {
|
||||
|
||||
/// Cancel stops the full check of a long coil cut (solidcore#3247): a 20-turn square coil cut
|
||||
/// from a cylinder, the case of tangency-labs/solidcraft#3962, whose check takes seconds (most
|
||||
/// of the cut's time, in the edges' curve analyses). Cancelled from another thread part way,
|
||||
/// the check returns `Cancelled` within 500 ms (a few ms on a desktop; the bound leaves room
|
||||
/// for shared CI runners). nextest runs each test in a process of its own, so no other test's
|
||||
/// call holds the session's kernel meanwhile.
|
||||
/// of the cut's time, in the edges' curve analyses). Measured in the token's polls, not in
|
||||
/// time: a counting token (`CancelToken::after_polls`) first counts the polls of the whole
|
||||
/// check, `whole`; a token cancelled from a quarter of them then stops a second check there.
|
||||
/// It returns `Cancelled`, and the token is polled exactly once after the poll that cancelled
|
||||
/// it: by the session as the call returns, asking whether it was cancelled (`run_with`, after
|
||||
/// the kernel's call). Any further poll would be work done after the cancel.
|
||||
#[test]
|
||||
fn the_check_of_a_long_coil_cut_stops_on_cancel() {
|
||||
use solidcore_ops::cancel::{within, CancelToken};
|
||||
use solidcore_ops::sweep::{screw_sweep, Curve};
|
||||
use std::time::{Duration, Instant};
|
||||
let corners = [
|
||||
[19.0, 0.0, 12.0],
|
||||
[32.0, 0.0, 12.0],
|
||||
@@ -288,21 +289,18 @@ fn the_check_of_a_long_coil_cut_stops_on_cancel() {
|
||||
.unwrap();
|
||||
let stock = solidcore_ops::make_cylinder([0.0; 3], 20.0, 304.5).unwrap();
|
||||
let cut = boolean(stock, coil, BooleanOp::Subtract).unwrap().unwrap();
|
||||
let token = CancelToken::new();
|
||||
let watched = token.clone();
|
||||
let worker = std::thread::spawn(move || {
|
||||
let r = within(&watched, || cut.check(CheckLevel::All));
|
||||
(r.map(|_| ()), Instant::now())
|
||||
});
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
let cancelled = Instant::now();
|
||||
token.cancel();
|
||||
let (r, returned) = worker.join().unwrap();
|
||||
let took = returned.saturating_duration_since(cancelled);
|
||||
eprintln!("the check returned {took:?} after Cancel");
|
||||
assert_eq!(r, Err(Error::Cancelled));
|
||||
assert!(
|
||||
took < Duration::from_millis(500),
|
||||
"returned {took:?} after Cancel"
|
||||
let probe = CancelToken::after_polls(u64::MAX);
|
||||
within(&probe, || cut.check(CheckLevel::All)).unwrap();
|
||||
let whole = probe.polls().unwrap();
|
||||
let n = whole / 4;
|
||||
// cancelled well inside the check, not at its last poll
|
||||
assert!(n >= 1 && whole >= 2 * n, "the check polls {whole} times");
|
||||
let token = CancelToken::after_polls(n);
|
||||
let r = within(&token, || cut.check(CheckLevel::All));
|
||||
eprintln!(
|
||||
"the check polls {whole} times; cancelled at {n}, polled {:?}",
|
||||
token.polls()
|
||||
);
|
||||
assert_eq!(r.map(|_| ()), Err(Error::Cancelled));
|
||||
assert_eq!(token.polls(), Some(n + 1));
|
||||
}
|
||||
|
||||
+18
-1
@@ -8,7 +8,7 @@ use std::sync::Arc;
|
||||
use crate::geom::{Associated, Curve, CurveForm, KnotSpec, Logical, P3, PreferredRep, Surface, SurfaceCurveKind, Trim};
|
||||
use crate::p21::{self, Exchange};
|
||||
use crate::read::{Reader, Units};
|
||||
use crate::write::{GeomWriter, Writer, real};
|
||||
use crate::write::{GeomWriter, Writer, real, string};
|
||||
|
||||
const MM: Units = Units { len: 1.0, ang: 1.0 };
|
||||
|
||||
@@ -90,6 +90,23 @@ fn round_trip_surface(s: &Surface, params: &[(f64, f64)]) {
|
||||
assert_eq!(w2.data(), w.data(), "a second write differs");
|
||||
}
|
||||
|
||||
/// ISO 10303-21 `\X2\` is UCS-2: a character outside the BMP is one `\X4\` code point, never a
|
||||
/// surrogate pair (#9089).
|
||||
#[test]
|
||||
fn string_writes_characters_outside_the_bmp_as_x4() {
|
||||
assert_eq!(string("it's"), "'it''s'");
|
||||
assert_eq!(string("é"), "'\\X2\\00E9\\X0\\'");
|
||||
assert_eq!(string("\u{FFFF}"), "'\\X2\\FFFF\\X0\\'");
|
||||
assert_eq!(string("\u{10000}"), "'\\X4\\00010000\\X0\\'");
|
||||
assert_eq!(string("🚀"), "'\\X4\\0001F680\\X0\\'");
|
||||
assert_eq!(string("\u{10FFFF}"), "'\\X4\\0010FFFF\\X0\\'");
|
||||
assert_eq!(string("é🚀中"), "'\\X2\\00E9\\X0\\\\X4\\0001F680\\X0\\\\X2\\4E2D\\X0\\'");
|
||||
for name in ["🚀", "Ünïcødé 🚀 中文", "a'b\\🚀", "\u{10000}\u{10FFFF}"] {
|
||||
let ex = file(&format!("#1=A({});", string(name)));
|
||||
assert_eq!(ex.get(1).and_then(|e| e.params().first()).and_then(|p| p.as_str()), Some(name));
|
||||
}
|
||||
}
|
||||
|
||||
const TS: [f64; 5] = [0.0, 0.3, 1.0, FRAC_PI_2, 2.5];
|
||||
const UVS: [(f64, f64); 4] = [(0.0, 0.0), (0.4, 0.2), (FRAC_PI_2, 1.0), (2.0, -0.5)];
|
||||
|
||||
|
||||
+6
-7
@@ -30,7 +30,8 @@ pub fn real(x: f64) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// A string literal: quotes doubled, non-ASCII as `\X2\` UTF-16 runs.
|
||||
/// A string literal: quotes doubled, non-ASCII as `\X2\` runs (UCS-2, four hex digits) for the
|
||||
/// Basic Multilingual Plane and `\X4\` runs (eight hex digits) above it.
|
||||
pub fn string(s: &str) -> String {
|
||||
let mut o = String::from("'");
|
||||
for c in s.chars() {
|
||||
@@ -39,13 +40,11 @@ pub fn string(s: &str) -> String {
|
||||
'\\' => o.push_str("\\\\"),
|
||||
c if c.is_ascii() && !c.is_ascii_control() => o.push(c),
|
||||
c if c.is_ascii_control() => {}
|
||||
c if u32::from(c) <= 0xFFFF => {
|
||||
let _ = write!(o, "\\X2\\{:04X}\\X0\\", u32::from(c));
|
||||
}
|
||||
c => {
|
||||
o.push_str("\\X2\\");
|
||||
let mut buf = [0u16; 2];
|
||||
for u in c.encode_utf16(&mut buf) {
|
||||
let _ = write!(o, "{u:04X}");
|
||||
}
|
||||
o.push_str("\\X0\\");
|
||||
let _ = write!(o, "\\X4\\{:08X}\\X0\\", u32::from(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ use crate::blend::blend_corner::ball::{solver_of_edgeuse_or_mate, BallSolvers};
|
||||
use crate::blend::blend_corner::on_face::{self, Clearance};
|
||||
use crate::blend::blend_corner::swept::fmt_g;
|
||||
use crate::blend::blend_corner::{BlendCorner, BlendCornerEvent};
|
||||
use crate::blend::blend_refusal::{BlendRefusal, SetbackTooLongWhy};
|
||||
use crate::blend::blend_refusal::{BlendRefusal, SetbackTooLongWhy, SetbackTooShortWhy};
|
||||
use crate::blend::blend_setback::{
|
||||
bisect_thousandths, certified_scale, containment, distances, fill_hole_counted,
|
||||
fill_hole_probe, spring_curve, Edge, Face, FaceKind, Fill, FillStats, Probe, Round,
|
||||
@@ -1360,6 +1360,7 @@ pub fn make_setback_topology(
|
||||
.typed(BlendRefusal::SetbackTooShort {
|
||||
vertex: vtx,
|
||||
least: most,
|
||||
why: SetbackTooShortWhy::RailsCross,
|
||||
})));
|
||||
}
|
||||
corner.ease_dist = a.ease;
|
||||
@@ -1435,7 +1436,18 @@ pub fn make_setback_topology(
|
||||
fmt_g(scale.scale * a.setback)
|
||||
)
|
||||
};
|
||||
return Ok(Err(SetbackRefusal::no_room(why, Some(builds))));
|
||||
let refusal = SetbackRefusal::no_room(why, Some(builds));
|
||||
// as data where a setback builds: the least one (none fits otherwise, so none
|
||||
// is named)
|
||||
return Ok(Err(if fits_room {
|
||||
refusal.typed(BlendRefusal::SetbackTooShort {
|
||||
vertex: vtx,
|
||||
least: builds,
|
||||
why: SetbackTooShortWhy::ConvexFill,
|
||||
})
|
||||
} else {
|
||||
refusal
|
||||
}));
|
||||
}
|
||||
for (s, &m) in sbs.iter_mut().zip(&most) {
|
||||
*s = smin(scale.scale * *s, m);
|
||||
@@ -1957,7 +1969,17 @@ fn grow_curved<S: SetbackSolvers>(
|
||||
fmt_g(hi * a.setback)
|
||||
)
|
||||
};
|
||||
return Ok(Err(SetbackRefusal::no_room(why, Some(builds))));
|
||||
let refusal = SetbackRefusal::no_room(why, Some(builds));
|
||||
// as data where a setback builds: the least one (none fits otherwise, so none is
|
||||
// named)
|
||||
return Ok(Err(match corner.vertex.filter(|_| fits_room) {
|
||||
Some(vertex) => refusal.typed(BlendRefusal::SetbackTooShort {
|
||||
vertex,
|
||||
least: builds,
|
||||
why: SetbackTooShortWhy::ConcaveFill,
|
||||
}),
|
||||
None => refusal,
|
||||
}));
|
||||
}
|
||||
Ok(Ok(ends))
|
||||
}
|
||||
|
||||
+26
-4
@@ -63,14 +63,17 @@ pub enum BlendRefusal {
|
||||
/// The least radius that rounds it.
|
||||
smallest_radius: f64,
|
||||
},
|
||||
/// A setback asked at `vertex` that stops short of where the rails of the rounds beside
|
||||
/// one of its edges cross it: `least` from the vertex
|
||||
/// ([`crate::blend::blend_setback::distances`]).
|
||||
/// A setback asked at `vertex` too short for the corner: `least` is the least setback the
|
||||
/// refusing test takes, and `why` says which test refused it and whether a setback of
|
||||
/// `least` builds the corner by it.
|
||||
SetbackTooShort {
|
||||
/// The corner's vertex.
|
||||
vertex: VertexId,
|
||||
/// The least setback: the farthest the rails beside an edge cross it.
|
||||
/// The least setback the refusing test takes ([`SetbackTooShortWhy`] says which and
|
||||
/// how).
|
||||
least: f64,
|
||||
/// Which test refused it.
|
||||
why: SetbackTooShortWhy,
|
||||
},
|
||||
/// A setback asked at `vertex` that does not fit on the corner's edges, or on its rounds
|
||||
/// where a far corner cuts them: one below `largest` fits. `why` says which test
|
||||
@@ -97,6 +100,25 @@ pub enum BlendRefusal {
|
||||
},
|
||||
}
|
||||
|
||||
/// Which test refused a setback as too short ([`BlendRefusal::SetbackTooShort`]).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum SetbackTooShortWhy {
|
||||
/// It stops short of where the rails of the rounds beside one of the corner's edges cross
|
||||
/// the edge ([`crate::blend::blend_setback::distances`]). `least` is the farthest they
|
||||
/// cross it from the vertex. A setback of `least` passes this test only: the corner's
|
||||
/// later tests may still refuse it.
|
||||
RailsCross,
|
||||
/// At a Setback corner, its G1 fill is not certified convex at it. `least` is the setback
|
||||
/// at the least scale of it, on a grid of thousandths, at which the fill is certified
|
||||
/// convex; with its ease it lies within the corner's rooms.
|
||||
ConvexFill,
|
||||
/// At a Setback corner beside a curved face, its fill is not certified no more concave
|
||||
/// than its faces at it. `least` is the setback at the least scale of it, on a grid of
|
||||
/// thousandths, at which it is; with its ease it lies within the corner's rooms.
|
||||
ConcaveFill,
|
||||
}
|
||||
|
||||
/// Which test refused a setback as too long ([`BlendRefusal::SetbackTooLong`]).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
|
||||
+431
-58
@@ -6,6 +6,11 @@
|
||||
//! Deviations: `sm_SbTNonneg` recurses to its `depth` argument (at most
|
||||
//! [`super::graph::GRAPH_DEPTH`] from its callers), the fork's own bound.
|
||||
//! `std::min_element` is the first least coefficient by `<`, as the fork's.
|
||||
//! `sm_SbTMulFast`'s binomials are read from a table ([`Binomials`]) built by the same
|
||||
//! function, a factor shared by many products is scaled by them once ([`t_scaled`]), and
|
||||
//! `sm_SbTDet4`'s minors of rows that recur are taken once ([`det4_minors`]): the same
|
||||
//! operations on the same values in the same order, so the same bits, without evaluating
|
||||
//! two binomials per coefficient of every factor and product.
|
||||
|
||||
use super::{
|
||||
binomial, el, el_mut, el_ref, euclid, sm_err, smax, tensor_elevate, Face, Net, Tensor,
|
||||
@@ -133,59 +138,127 @@ pub(super) fn t_pieces(a: &Tensor, levels: usize) -> Result<Vec<Tensor>> {
|
||||
Ok(pieces)
|
||||
}
|
||||
|
||||
/// `sm_SbTMulFast`: `a b`, as `sm_SbTMul` but the binomials taken once: the coefficients
|
||||
/// scaled by them, convolved, and unscaled.
|
||||
pub(super) fn t_mul_fast(a: &Tensor, b: &Tensor) -> Result<Tensor> {
|
||||
/// The binomial coefficients `C(n, i)` for `i` in `0 ..= n`, each as [`binomial`] gives it,
|
||||
/// kept per `n` once asked: the products' scalings read them per coefficient.
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct Binomials {
|
||||
rows: Vec<Vec<f64>>,
|
||||
/// The [`binomial`] evaluations made (the work the table saves is counted, never timed).
|
||||
evaluations: usize,
|
||||
}
|
||||
|
||||
impl Binomials {
|
||||
/// The row `C(n, 0 ..= n)`.
|
||||
fn row(&mut self, n: usize) -> Result<&[f64]> {
|
||||
while self.rows.len() <= n {
|
||||
let m = self.rows.len();
|
||||
let row = (0..=m)
|
||||
.map(|i| binomial(m, i))
|
||||
.collect::<Result<Vec<f64>>>()?;
|
||||
self.evaluations = self.evaluations.saturating_add(row.len());
|
||||
self.rows.push(row);
|
||||
}
|
||||
self.rows.get(n).map(Vec::as_slice).ok_or_else(sm_err)
|
||||
}
|
||||
}
|
||||
|
||||
/// A polynomial's coefficients scaled by their binomials, `c[i, j] C(p, i) C(q, j)`, as
|
||||
/// [`t_mul_fast`] scales its factors: a factor used in many products is scaled once.
|
||||
pub(super) struct ScaledTensor {
|
||||
p: usize,
|
||||
q: usize,
|
||||
c: Vec<f64>,
|
||||
}
|
||||
|
||||
/// `a`'s coefficients scaled by their binomials (`sm_SbTMulFast`'s first step).
|
||||
pub(super) fn t_scaled(a: &Tensor, binomials: &mut Binomials) -> Result<ScaledTensor> {
|
||||
let width = a.q + 1;
|
||||
if a.c.len() != (a.p + 1) * width {
|
||||
return Err(sm_err());
|
||||
}
|
||||
let bq = binomials.row(a.q)?.to_vec();
|
||||
let bp = binomials.row(a.p)?;
|
||||
let mut c = Vec::new();
|
||||
for (i, &bpi) in bp.iter().enumerate() {
|
||||
for (&x, &bqj) in row(&a.c, i, width)?.iter().zip(&bq) {
|
||||
c.push(x * bpi * bqj);
|
||||
}
|
||||
}
|
||||
Ok(ScaledTensor { p: a.p, q: a.q, c })
|
||||
}
|
||||
|
||||
/// Row `i` of coefficients `c` stored `width` to a row.
|
||||
fn row(c: &[f64], i: usize, width: usize) -> Result<&[f64]> {
|
||||
let start = i * width;
|
||||
c.get(start..start + width).ok_or_else(sm_err)
|
||||
}
|
||||
|
||||
/// The product of two scaled polynomials, convolved and unscaled (`sm_SbTMulFast`'s
|
||||
/// second and third steps): the same operations, in the same order, as [`t_mul_fast`].
|
||||
pub(super) fn t_mul_scaled(
|
||||
a: &ScaledTensor,
|
||||
b: &ScaledTensor,
|
||||
binomials: &mut Binomials,
|
||||
) -> Result<Tensor> {
|
||||
let mut c = t_make(a.p + b.p, a.q + b.q);
|
||||
let mut sa = vec![0.0; a.c.len()];
|
||||
let mut sb = vec![0.0; b.c.len()];
|
||||
let (aw, bw, cw) = (a.q + 1, b.q + 1, c.q + 1);
|
||||
for i in 0..=a.p {
|
||||
for j in 0..=a.q {
|
||||
*el_mut(&mut sa, i * (a.q + 1) + j)? =
|
||||
el(&a.c, i * (a.q + 1) + j)? * binomial(a.p, i)? * binomial(a.q, j)?;
|
||||
}
|
||||
}
|
||||
for i in 0..=b.p {
|
||||
for j in 0..=b.q {
|
||||
*el_mut(&mut sb, i * (b.q + 1) + j)? =
|
||||
el(&b.c, i * (b.q + 1) + j)? * binomial(b.p, i)? * binomial(b.q, j)?;
|
||||
}
|
||||
}
|
||||
for i in 0..=a.p {
|
||||
for j in 0..=a.q {
|
||||
let av = el(&sa, i * (a.q + 1) + j)?;
|
||||
for (j, &av) in row(&a.c, i, aw)?.iter().enumerate() {
|
||||
if av == 0.0 {
|
||||
continue;
|
||||
}
|
||||
for k in 0..=b.p {
|
||||
let row = (i + k) * (c.q + 1) + j;
|
||||
for l in 0..=b.q {
|
||||
*el_mut(&mut c.c, row + l)? += av * el(&sb, k * (b.q + 1) + l)?;
|
||||
let start = (i + k) * cw + j;
|
||||
let out = c.c.get_mut(start..start + bw).ok_or_else(sm_err)?;
|
||||
for (cv, &bv) in out.iter_mut().zip(row(&b.c, k, bw)?) {
|
||||
*cv += av * bv;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for i in 0..=c.p {
|
||||
for j in 0..=c.q {
|
||||
let f = binomial(c.p, i)? * binomial(c.q, j)?;
|
||||
*el_mut(&mut c.c, i * (c.q + 1) + j)? /= f;
|
||||
let bq = binomials.row(c.q)?.to_vec();
|
||||
let bp = binomials.row(c.p)?;
|
||||
for (i, &bpi) in bp.iter().enumerate() {
|
||||
let start = i * cw;
|
||||
let out = c.c.get_mut(start..start + cw).ok_or_else(sm_err)?;
|
||||
for (x, &bqj) in out.iter_mut().zip(&bq) {
|
||||
let f = bpi * bqj;
|
||||
*x /= f;
|
||||
}
|
||||
}
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
/// `sm_SbTMulFast`: `a b`, as `sm_SbTMul` but the binomials taken once: the coefficients
|
||||
/// scaled by them, convolved, and unscaled.
|
||||
pub(super) fn t_mul_fast(a: &Tensor, b: &Tensor) -> Result<Tensor> {
|
||||
t_mul_with(a, b, &mut Binomials::default())
|
||||
}
|
||||
|
||||
/// [`t_mul_fast`] with its binomials from `binomials`.
|
||||
fn t_mul_with(a: &Tensor, b: &Tensor, binomials: &mut Binomials) -> Result<Tensor> {
|
||||
let sa = t_scaled(a, binomials)?;
|
||||
let sb = t_scaled(b, binomials)?;
|
||||
t_mul_scaled(&sa, &sb, binomials)
|
||||
}
|
||||
|
||||
/// `sm_SbTDet3`: the determinant of a 3 x 3 matrix of polynomials.
|
||||
pub(super) fn t_det3(m: [[&Tensor; 3]; 3]) -> Result<Tensor> {
|
||||
t_det3_with(m, &mut Binomials::default())
|
||||
}
|
||||
|
||||
/// [`t_det3`] with its binomials from `binomials`.
|
||||
fn t_det3_with(m: [[&Tensor; 3]; 3], binomials: &mut Binomials) -> Result<Tensor> {
|
||||
let mut s = t_make(0, 0);
|
||||
for j in 0..3 {
|
||||
let (j1, j2) = ((j + 1) % 3, (j + 2) % 3);
|
||||
let [r0, r1, r2] = m;
|
||||
let minor = t_axpy(
|
||||
&t_mul_fast(el(&r1, j1)?, el(&r2, j2)?)?,
|
||||
&t_mul_with(el(&r1, j1)?, el(&r2, j2)?, binomials)?,
|
||||
-1.0,
|
||||
&t_mul_fast(el(&r1, j2)?, el(&r2, j1)?)?,
|
||||
&t_mul_with(el(&r1, j2)?, el(&r2, j1)?, binomials)?,
|
||||
)?;
|
||||
s = t_axpy(&s, 1.0, &t_mul_fast(el(&r0, j)?, &minor)?)?;
|
||||
s = t_axpy(&s, 1.0, &t_mul_with(el(&r0, j)?, &minor, binomials)?)?;
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
@@ -304,15 +377,28 @@ pub(super) fn minor_without<'a>(
|
||||
])
|
||||
}
|
||||
|
||||
/// `sm_SbTDet4`: the determinant of a 4 x 4 matrix of polynomials (rows `m[r]`), along its
|
||||
/// last row.
|
||||
fn t_det4(m: [&[Tensor; 4]; 4]) -> Result<Tensor> {
|
||||
/// `sm_SbTDet4`'s minors: the 3 x 3 minors of the first three rows `rows` of a 4 x 4 matrix
|
||||
/// of polynomials, column `c` struck out in minor `c`.
|
||||
fn det4_minors(rows: [&[Tensor; 4]; 3], binomials: &mut Binomials) -> Result<[Tensor; 4]> {
|
||||
let mut minors: [Tensor; 4] = Default::default();
|
||||
for (c, minor) in minors.iter_mut().enumerate() {
|
||||
*minor = t_det3_with(minor_without(rows, c)?, binomials)?;
|
||||
}
|
||||
Ok(minors)
|
||||
}
|
||||
|
||||
/// `sm_SbTDet4`: the determinant along its last row `last`, from the minors of its other
|
||||
/// rows ([`det4_minors`]): a determinant whose first three rows recur takes their minors
|
||||
/// once.
|
||||
fn t_det4_along(
|
||||
last: &[Tensor; 4],
|
||||
minors: &[Tensor; 4],
|
||||
binomials: &mut Binomials,
|
||||
) -> Result<Tensor> {
|
||||
let mut s = t_make(0, 0);
|
||||
let [r0, r1, r2, r3] = m;
|
||||
for c in 0..4 {
|
||||
let pm = minor_without([r0, r1, r2], c)?;
|
||||
for (c, minor) in minors.iter().enumerate() {
|
||||
let sign = if (3 + c) % 2 == 0 { 1.0 } else { -1.0 };
|
||||
s = t_axpy(&s, sign, &t_mul_fast(el_ref(r3, c)?, &t_det3(pm)?)?)?;
|
||||
s = t_axpy(&s, sign, &t_mul_with(el_ref(last, c)?, minor, binomials)?)?;
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
@@ -325,17 +411,13 @@ pub(super) fn fundamentals(net: &Net, e: Vec3) -> Result<[Tensor; 4]> {
|
||||
let rows = rows_of(net, e)?;
|
||||
let [huu, huv, hvv] = &rows.hx;
|
||||
let last = [huu, huv, hvv, &rows.one];
|
||||
let mut binomials = Binomials::default();
|
||||
// the minors of H, Hu, Hv, the same for every last row: taken once
|
||||
let minors = det4_minors([&rows.h, &rows.hu, &rows.hv], &mut binomials)?;
|
||||
let mut out: [Tensor; 4] = Default::default();
|
||||
for (k, slot) in out.iter_mut().enumerate() {
|
||||
for (slot, row) in out.iter_mut().zip(last) {
|
||||
// along the last row: det = sum_c (-1)^(3 + c) last[c] minor(3, c)
|
||||
let mut s = t_make(0, 0);
|
||||
for c in 0..4 {
|
||||
let pm = minor_without([&rows.h, &rows.hu, &rows.hv], c)?;
|
||||
let minor = t_det3(pm)?;
|
||||
let sign = if (3 + c) % 2 == 0 { 1.0 } else { -1.0 };
|
||||
s = t_axpy(&s, sign, &t_mul_fast(el_ref(el(&last, k)?, c)?, &minor)?)?;
|
||||
}
|
||||
*slot = s;
|
||||
*slot = t_det4_along(row, &minors, &mut binomials)?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@@ -422,36 +504,73 @@ pub(super) struct AffineFundamentals {
|
||||
/// `E = (e, 0)`, `det(H, Hu, Hv, X) = det(H0, H0u, H0v, X0) + sum_r Z_r det(.., E in row r,
|
||||
/// ..)` (two rows along E give 0): affine in Z.
|
||||
pub(super) fn affine_fundamentals(p: &AffinePatch, e: Vec3) -> Result<AffineFundamentals> {
|
||||
affine_fundamentals_with(p, e, &mut Binomials::default())
|
||||
}
|
||||
|
||||
/// [`affine_fundamentals`] with its binomials from `binomials`. Every product is
|
||||
/// [`t_mul_fast`]'s, in its order, but a factor in many products is scaled by its binomials
|
||||
/// once (the cofactors, each unknown's share and its partials), the minors of `H, Hu, Hv`
|
||||
/// are taken once for `o`, `l`, `m` and `n`, and the binomials are read from the table: the
|
||||
/// same operations on the same values, so the same bits.
|
||||
fn affine_fundamentals_with(
|
||||
p: &AffinePatch,
|
||||
e: Vec3,
|
||||
binomials: &mut Binomials,
|
||||
) -> Result<AffineFundamentals> {
|
||||
let rows = rows_of(&p.base, e)?;
|
||||
let o = t_det4([&rows.h, &rows.hu, &rows.hv, &rows.one])?;
|
||||
let base_minors = det4_minors([&rows.h, &rows.hu, &rows.hv], binomials)?;
|
||||
let o = t_det4_along(&rows.one, &base_minors, binomials)?;
|
||||
// each unknown's share and its first partials, the same for l, m and n: scaled once
|
||||
let mut z_scaled = Vec::new();
|
||||
for zt in &p.z {
|
||||
let (zu, zv) = (t_du(zt)?, t_dv(zt)?);
|
||||
let scaled = [
|
||||
t_scaled(zt, binomials)?,
|
||||
t_scaled(&zu, binomials)?,
|
||||
t_scaled(&zv, binomials)?,
|
||||
];
|
||||
z_scaled.push((zu, zv, scaled));
|
||||
}
|
||||
let mut q0: [Tensor; 3] = Default::default();
|
||||
let mut qj: [Vec<Tensor>; 3] = Default::default();
|
||||
for k in 0..3 {
|
||||
let hx = el_ref(&rows.hx, k)?;
|
||||
let prows = [&rows.h, &rows.hu, &rows.hv, hx];
|
||||
*el_mut(&mut q0, k)? = t_det4(prows)?;
|
||||
*el_mut(&mut q0, k)? = t_det4_along(hx, &base_minors, binomials)?;
|
||||
// the cofactors: row r replaced by E (row 3's is o)
|
||||
let mut cof: [Tensor; 4] = Default::default();
|
||||
for r in 0..3 {
|
||||
let mut pr = prows;
|
||||
*el_mut(&mut pr, r)? = &rows.one;
|
||||
*el_mut(&mut cof, r)? = t_det4(pr)?;
|
||||
let [r0, r1, r2, r3] = pr;
|
||||
let minors = det4_minors([r0, r1, r2], binomials)?;
|
||||
*el_mut(&mut cof, r)? = t_det4_along(r3, &minors, binomials)?;
|
||||
}
|
||||
cof[3] = o.clone();
|
||||
// each cofactor is a factor of every unknown's products: scaled once
|
||||
let mut cof_scaled = Vec::new();
|
||||
for c in &cof {
|
||||
cof_scaled.push(t_scaled(c, binomials)?);
|
||||
}
|
||||
let [c0, c1, c2, c3] = cof_scaled.as_slice() else {
|
||||
return Err(sm_err());
|
||||
};
|
||||
let out = el_mut(&mut qj, k)?;
|
||||
out.clear();
|
||||
for zt in &p.z {
|
||||
let zu = t_du(zt)?;
|
||||
let zv = t_dv(zt)?;
|
||||
for (zu, zv, [st, su, sv]) in &z_scaled {
|
||||
let zx = match k {
|
||||
0 => t_du(&zu)?,
|
||||
1 => t_dv(&zu)?,
|
||||
_ => t_dv(&zv)?,
|
||||
0 => t_du(zu)?,
|
||||
1 => t_dv(zu)?,
|
||||
_ => t_dv(zv)?,
|
||||
};
|
||||
let mut s = t_mul_fast(zt, &cof[0])?;
|
||||
s = t_axpy(&s, 1.0, &t_mul_fast(&zu, &cof[1])?)?;
|
||||
s = t_axpy(&s, 1.0, &t_mul_fast(&zv, &cof[2])?)?;
|
||||
s = t_axpy(&s, 1.0, &t_mul_fast(&zx, &cof[3])?)?;
|
||||
let mut s = t_mul_scaled(st, c0, binomials)?;
|
||||
s = t_axpy(&s, 1.0, &t_mul_scaled(su, c1, binomials)?)?;
|
||||
s = t_axpy(&s, 1.0, &t_mul_scaled(sv, c2, binomials)?)?;
|
||||
s = t_axpy(
|
||||
&s,
|
||||
1.0,
|
||||
&t_mul_scaled(&t_scaled(&zx, binomials)?, c3, binomials)?,
|
||||
)?;
|
||||
out.push(s);
|
||||
}
|
||||
}
|
||||
@@ -584,3 +703,257 @@ pub(super) fn certified_convex(
|
||||
}
|
||||
Ok(Ok(()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::cell::Cell;
|
||||
|
||||
thread_local! {
|
||||
/// The binomials the reference products evaluate.
|
||||
static EVALUATIONS: Cell<usize> = const { Cell::new(0) };
|
||||
}
|
||||
|
||||
fn counted_binomial(n: usize, k: usize) -> f64 {
|
||||
EVALUATIONS.with(|e| e.set(e.get() + 1));
|
||||
binomial(n, k).unwrap()
|
||||
}
|
||||
|
||||
/// `sm_SbTMulFast` as it was ported before the table: each coefficient's binomials
|
||||
/// evaluated where it is scaled and unscaled.
|
||||
fn reference_mul(a: &Tensor, b: &Tensor) -> Tensor {
|
||||
let mut c = t_make(a.p + b.p, a.q + b.q);
|
||||
let mut sa = vec![0.0; a.c.len()];
|
||||
let mut sb = vec![0.0; b.c.len()];
|
||||
for i in 0..=a.p {
|
||||
for j in 0..=a.q {
|
||||
sa[i * (a.q + 1) + j] =
|
||||
a.c[i * (a.q + 1) + j] * counted_binomial(a.p, i) * counted_binomial(a.q, j);
|
||||
}
|
||||
}
|
||||
for i in 0..=b.p {
|
||||
for j in 0..=b.q {
|
||||
sb[i * (b.q + 1) + j] =
|
||||
b.c[i * (b.q + 1) + j] * counted_binomial(b.p, i) * counted_binomial(b.q, j);
|
||||
}
|
||||
}
|
||||
for i in 0..=a.p {
|
||||
for j in 0..=a.q {
|
||||
let av = sa[i * (a.q + 1) + j];
|
||||
if av == 0.0 {
|
||||
continue;
|
||||
}
|
||||
for k in 0..=b.p {
|
||||
let row = (i + k) * (c.q + 1) + j;
|
||||
for l in 0..=b.q {
|
||||
c.c[row + l] += av * sb[k * (b.q + 1) + l];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for i in 0..=c.p {
|
||||
for j in 0..=c.q {
|
||||
let f = counted_binomial(c.p, i) * counted_binomial(c.q, j);
|
||||
c.c[i * (c.q + 1) + j] /= f;
|
||||
}
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
fn reference_det3(m: [[&Tensor; 3]; 3]) -> Tensor {
|
||||
let mut s = t_make(0, 0);
|
||||
for j in 0..3 {
|
||||
let (j1, j2) = ((j + 1) % 3, (j + 2) % 3);
|
||||
let [r0, r1, r2] = m;
|
||||
let minor = t_axpy(
|
||||
&reference_mul(r1[j1], r2[j2]),
|
||||
-1.0,
|
||||
&reference_mul(r1[j2], r2[j1]),
|
||||
)
|
||||
.unwrap();
|
||||
s = t_axpy(&s, 1.0, &reference_mul(r0[j], &minor)).unwrap();
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn reference_det4(m: [&[Tensor; 4]; 4]) -> Tensor {
|
||||
let mut s = t_make(0, 0);
|
||||
let [r0, r1, r2, r3] = m;
|
||||
for (c, last) in r3.iter().enumerate() {
|
||||
let pm = minor_without([r0, r1, r2], c).unwrap();
|
||||
let sign = if (3 + c) % 2 == 0 { 1.0 } else { -1.0 };
|
||||
s = t_axpy(&s, sign, &reference_mul(last, &reference_det3(pm))).unwrap();
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// `sm_SbAffineFundamentals` as it was ported before the table: every product of every
|
||||
/// determinant and of every unknown's share taken afresh.
|
||||
fn reference_affine_fundamentals(p: &AffinePatch, e: Vec3) -> AffineFundamentals {
|
||||
let rows = rows_of(&p.base, e).unwrap();
|
||||
let o = reference_det4([&rows.h, &rows.hu, &rows.hv, &rows.one]);
|
||||
let mut q0: [Tensor; 3] = Default::default();
|
||||
let mut qj: [Vec<Tensor>; 3] = Default::default();
|
||||
for k in 0..3 {
|
||||
let prows = [&rows.h, &rows.hu, &rows.hv, &rows.hx[k]];
|
||||
q0[k] = reference_det4(prows);
|
||||
let mut cof: [Tensor; 4] = Default::default();
|
||||
for r in 0..3 {
|
||||
let mut pr = prows;
|
||||
pr[r] = &rows.one;
|
||||
cof[r] = reference_det4(pr);
|
||||
}
|
||||
cof[3] = o.clone();
|
||||
for zt in &p.z {
|
||||
let zu = t_du(zt).unwrap();
|
||||
let zv = t_dv(zt).unwrap();
|
||||
let zx = match k {
|
||||
0 => t_du(&zu).unwrap(),
|
||||
1 => t_dv(&zu).unwrap(),
|
||||
_ => t_dv(&zv).unwrap(),
|
||||
};
|
||||
let mut s = reference_mul(zt, &cof[0]);
|
||||
s = t_axpy(&s, 1.0, &reference_mul(&zu, &cof[1])).unwrap();
|
||||
s = t_axpy(&s, 1.0, &reference_mul(&zv, &cof[2])).unwrap();
|
||||
s = t_axpy(&s, 1.0, &reference_mul(&zx, &cof[3])).unwrap();
|
||||
qj[k].push(s);
|
||||
}
|
||||
}
|
||||
AffineFundamentals { q0, qj, o }
|
||||
}
|
||||
|
||||
/// A fixed-seed generator of coefficients in [-1, 1), with exact zeros (both signs)
|
||||
/// sprinkled in: the products skip a zero scaled coefficient.
|
||||
struct Lcg(u64);
|
||||
|
||||
impl Lcg {
|
||||
fn next(&mut self) -> f64 {
|
||||
self.0 = self
|
||||
.0
|
||||
.wrapping_mul(6364136223846793005)
|
||||
.wrapping_add(1442695040888963407);
|
||||
let x = ((self.0 >> 11) as f64) / ((1u64 << 53) as f64) * 2.0 - 1.0;
|
||||
match self.0 % 11 {
|
||||
0 => 0.0,
|
||||
1 => -0.0,
|
||||
_ => x,
|
||||
}
|
||||
}
|
||||
|
||||
fn tensor(&mut self, p: usize, q: usize) -> Tensor {
|
||||
let mut t = t_make(p, q);
|
||||
for c in &mut t.c {
|
||||
*c = self.next();
|
||||
}
|
||||
t
|
||||
}
|
||||
}
|
||||
|
||||
fn same_bits(a: &Tensor, b: &Tensor) {
|
||||
assert_eq!((a.p, a.q), (b.p, b.q));
|
||||
for (x, y) in a.c.iter().zip(&b.c) {
|
||||
assert_eq!(x.to_bits(), y.to_bits(), "{x} vs {y}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn products_from_the_table_are_the_per_coefficient_products_bit_for_bit() {
|
||||
let mut rng = Lcg(1557);
|
||||
let mut binomials = Binomials::default();
|
||||
for (p, q, r, s) in [
|
||||
(0, 0, 0, 0),
|
||||
(0, 3, 2, 0),
|
||||
(1, 1, 9, 9),
|
||||
(3, 9, 12, 27),
|
||||
(9, 9, 9, 9),
|
||||
(2, 5, 0, 7),
|
||||
] {
|
||||
let (a, b) = (rng.tensor(p, q), rng.tensor(r, s));
|
||||
let want = reference_mul(&a, &b);
|
||||
same_bits(&t_mul_fast(&a, &b).unwrap(), &want);
|
||||
same_bits(&t_mul_with(&a, &b, &mut binomials).unwrap(), &want);
|
||||
let (sa, sb) = (
|
||||
t_scaled(&a, &mut binomials).unwrap(),
|
||||
t_scaled(&b, &mut binomials).unwrap(),
|
||||
);
|
||||
same_bits(&t_mul_scaled(&sa, &sb, &mut binomials).unwrap(), &want);
|
||||
}
|
||||
}
|
||||
|
||||
/// A rational net of degree (3, 7) about a point away from the origin, its weights in
|
||||
/// [0.5, 1.5], moved along e by five unknowns (four dense shares, one a single
|
||||
/// coefficient), as the setback fill's patches are.
|
||||
fn rational_patch(rng: &mut Lcg) -> AffinePatch {
|
||||
let (p, q) = (3, 7);
|
||||
let mut pts = Vec::new();
|
||||
for i in 0..=p {
|
||||
for j in 0..=q {
|
||||
let w = 1.0 + 0.5 * rng.next();
|
||||
let (x, y, z) = (
|
||||
f64::from(i as u32) + 0.1 * rng.next(),
|
||||
f64::from(j as u32) + 0.1 * rng.next(),
|
||||
0.3 * rng.next(),
|
||||
);
|
||||
pts.push([(x + 20.0) * w, (y - 7.0) * w, (z + 3.0) * w, w]);
|
||||
}
|
||||
}
|
||||
let mut z: Vec<Tensor> = (0..4).map(|_| rng.tensor(p, q)).collect();
|
||||
let mut single = t_make(p, q);
|
||||
single.c[9] = 1.0;
|
||||
z.push(single);
|
||||
AffinePatch {
|
||||
base: Net { p, q, pts },
|
||||
index: (0..z.len()).collect(),
|
||||
z,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn affine_fundamentals_are_the_forks_bit_for_bit_with_each_binomial_evaluated_once() {
|
||||
let mut rng = Lcg(14460);
|
||||
let patch = rational_patch(&mut rng);
|
||||
let e = Vec3::new(0.3, -0.2, 1.0);
|
||||
EVALUATIONS.with(|c| c.set(0));
|
||||
let want = reference_affine_fundamentals(&patch, e);
|
||||
let forks = EVALUATIONS.with(Cell::get);
|
||||
let mut binomials = Binomials::default();
|
||||
let got = affine_fundamentals_with(&patch, e, &mut binomials).unwrap();
|
||||
same_bits(&got.o, &want.o);
|
||||
for k in 0..3 {
|
||||
same_bits(&got.q0[k], &want.q0[k]);
|
||||
assert_eq!(got.qj[k].len(), want.qj[k].len());
|
||||
for (a, b) in got.qj[k].iter().zip(&want.qj[k]) {
|
||||
same_bits(a, b);
|
||||
}
|
||||
}
|
||||
// The work counted, never timed: one evaluation per binomial up to the products'
|
||||
// largest degree, where the fork's evaluates two per coefficient of every factor and
|
||||
// of every product.
|
||||
let largest = [&got.o, &got.q0[0], &got.q0[1], &got.q0[2]]
|
||||
.iter()
|
||||
.map(|t| t.p.max(t.q))
|
||||
.max()
|
||||
.unwrap();
|
||||
assert!(
|
||||
binomials.evaluations <= (largest + 1) * (largest + 2) / 2,
|
||||
"{} evaluations",
|
||||
binomials.evaluations
|
||||
);
|
||||
assert!(
|
||||
forks > 500 * binomials.evaluations,
|
||||
"{forks} against {}",
|
||||
binomials.evaluations
|
||||
);
|
||||
// and the curvature determinants of the net itself
|
||||
let net = &patch.base;
|
||||
let rows = rows_of(net, e).unwrap();
|
||||
let [huu, huv, hvv] = &rows.hx;
|
||||
let got = fundamentals(net, e).unwrap();
|
||||
for (k, last) in [huu, huv, hvv, &rows.one].into_iter().enumerate() {
|
||||
same_bits(
|
||||
&got[k],
|
||||
&reference_det4([&rows.h, &rows.hu, &rows.hv, last]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-4
@@ -42,6 +42,14 @@ fn at(m: &[f64], i: usize) -> Result<f64> {
|
||||
el(m, i)
|
||||
}
|
||||
|
||||
/// Row `i` of the row-major `C` with `n` columns: the scans read it as one slice, the
|
||||
/// same coefficients in the same order as indexing each.
|
||||
fn row_of(cm: &[f64], i: usize, n: usize) -> Result<&[f64]> {
|
||||
let start = i.checked_mul(n).ok_or_else(sm_err)?;
|
||||
let end = start.checked_add(n).ok_or_else(sm_err)?;
|
||||
cm.get(start..end).ok_or_else(sm_err)
|
||||
}
|
||||
|
||||
/// The dual method's state (`sm_SbDualQP`'s locals): `J` (`J' G J = I`, turned as
|
||||
/// constraints join), the solution, the active rows and their multipliers, and `R`. Kept
|
||||
/// between calls, it continues the method on more rows ([`dual_qp_rows`] beside a curved
|
||||
@@ -134,9 +142,10 @@ fn dual_run(
|
||||
let mut z = vec![0.0; n];
|
||||
let mut r = vec![0.0; n];
|
||||
let slack = |y: &[f64], i: usize| -> Result<f64> {
|
||||
let ci = row_of(cm, i, n)?;
|
||||
let mut s = -at(b, i)?;
|
||||
for (k, &yk) in y.iter().enumerate() {
|
||||
s += at(cm, i * n + k)? * yk;
|
||||
for (&c, &yk) in ci.iter().zip(y) {
|
||||
s += c * yk;
|
||||
}
|
||||
Ok(s)
|
||||
};
|
||||
@@ -332,11 +341,12 @@ pub(super) fn dual_qp(
|
||||
/// `(n + 1) eps (sum_k |C_ik y_k| + |b_i|)`, the bound of its dot product's rounding. Negative
|
||||
/// only where the row is violated by more than its rounding can account for.
|
||||
fn violation(cm: &[f64], b: &[f64], y: &[f64], i: usize, n: usize) -> Result<f64> {
|
||||
let ci = row_of(cm, i, n)?;
|
||||
let bi = at(b, i)?;
|
||||
let mut s = -bi;
|
||||
let mut size = bi.abs();
|
||||
for (k, &yk) in y.iter().enumerate() {
|
||||
let t = at(cm, i * n + k)? * yk;
|
||||
for (&c, &yk) in ci.iter().zip(y) {
|
||||
let t = c * yk;
|
||||
s += t;
|
||||
size += t.abs();
|
||||
}
|
||||
|
||||
+27
-12
@@ -345,25 +345,32 @@ mod tests {
|
||||
#[test]
|
||||
fn thousandth_scales_and_attempt_budget() {
|
||||
let kernel = Kernel::default();
|
||||
for (radii, setbacks, most, want, want_solves) in [
|
||||
([2.0, 3.0, 4.0], [4.5, 4.75, 6.0], None, 1.056, 22),
|
||||
([1.0, 1.5, 2.0], [2.25, 2.375, 3.0], None, 1.056, 26),
|
||||
// The last column: the QPs the search solved with each attempt's degrees least first,
|
||||
// the order before degree 9 first (the same counts on Linux and Windows x86-64). The
|
||||
// search must now solve fewer.
|
||||
for (radii, setbacks, most, want, least_first) in [
|
||||
([2.0, 3.0, 4.0], [4.5, 4.75, 6.0], None, 1.056, Some(35)),
|
||||
([1.0, 1.5, 2.0], [2.25, 2.375, 3.0], None, 1.056, Some(35)),
|
||||
(
|
||||
[2.0, 3.0, 4.0],
|
||||
[4.5, 4.75, 4.5],
|
||||
Some([20.0, 20.0, 4.5]),
|
||||
1.046,
|
||||
21,
|
||||
Some(39),
|
||||
),
|
||||
([2.0, 3.0, 4.0], [4.25; 3], None, 1.110, 30),
|
||||
([5.0; 3], [8.0; 3], None, 1.0, 3),
|
||||
// Eight of its twelve attempts certify below degree 9, each solving degree 9 first,
|
||||
// one QP more: it solves fewer than least first's 34 only where degree 9 decides
|
||||
// at least three of its four failing attempts, so no bound.
|
||||
([2.0, 3.0, 4.0], [4.25; 3], None, 1.110, None),
|
||||
// One certifying attempt, one QP more than least first's 2 (its budget further on).
|
||||
([5.0; 3], [8.0; 3], None, 1.0, None),
|
||||
// Every edge held at its original setback: no scale can certify.
|
||||
(
|
||||
[2.0, 3.0, 4.0],
|
||||
[4.5, 4.75, 6.0],
|
||||
Some([4.5, 4.75, 6.0]),
|
||||
0.0,
|
||||
2,
|
||||
Some(10),
|
||||
),
|
||||
] {
|
||||
let (faces, edges) = corner(radii, 0, false);
|
||||
@@ -386,17 +393,24 @@ mod tests {
|
||||
);
|
||||
assert_eq!(result.scale, want);
|
||||
assert!(result.attempts <= 12);
|
||||
// Degree 9 first, as the corner's own fill: an attempt that degree 9 proves
|
||||
// unmet solves that one programme (the fork's 3843ddb solves as many).
|
||||
assert_eq!(result.solves, want_solves, "QP count");
|
||||
// Degree 9 first, as the corner's own fill: a failing attempt that degree 9 proves
|
||||
// unmet solves that one programme, where least first solved five. Which failing
|
||||
// attempts' degree 9 is not proven infeasible, and so still solve five, differs
|
||||
// between targets and machines, so the count is held below least first's, not
|
||||
// pinned. Every attempt solves degree 9 at least.
|
||||
if let Some(least_first) = least_first {
|
||||
assert!(result.solves < least_first, "QP count {}", result.solves);
|
||||
}
|
||||
assert!(result.solves >= result.attempts);
|
||||
assert!(result.qp_rows_built > 0);
|
||||
assert!(result.qp_peak_rows_held <= result.qp_rows_built);
|
||||
assert!(result.qp_steps >= result.solves);
|
||||
// Work budgets leave room for floating-point branch differences while
|
||||
// rejecting the former full matrices (up to 693,051 retained rows) and, on
|
||||
// aarch64, the degrees below 9 a failing attempt tried (the fork's 20b1c01 took
|
||||
// 120,513 steps in one degree-7 programme of the first corner); the QP counts
|
||||
// above reject that order on every platform.
|
||||
// 120,513 steps in one degree-7 programme of the first corner). The QP bounds
|
||||
// above reject that order; a long programme below 9 in an attempt whose degree 9
|
||||
// is not proven infeasible is caught by the step budget only.
|
||||
assert!(result.qp_rows_built <= 12_000_000);
|
||||
assert!(result.qp_peak_rows_held <= 16_000);
|
||||
assert!(result.qp_steps <= 25_000);
|
||||
@@ -405,6 +419,7 @@ mod tests {
|
||||
// programme built 430,962 rows and retained 254,037 at peak. Its one
|
||||
// attempt solves degree 9 before the least degree that certifies: three
|
||||
// programmes, 22 steps on x86-64.
|
||||
assert!(result.solves <= 3);
|
||||
assert!(result.qp_rows_built <= 10_000);
|
||||
assert!(result.qp_peak_rows_held <= 2_000);
|
||||
assert!(result.qp_steps <= 32);
|
||||
|
||||
@@ -2471,7 +2471,10 @@ mod tests {
|
||||
b.edgeuse_mut(_side1_mate).unwrap().cw = Some(fil_radial);
|
||||
|
||||
let raw = p1::check_nx1_degeneracy(&k, &mut b, v, fil);
|
||||
assert!(matches!(raw, Err(SolidError::Upstream { .. })), "{raw:?}");
|
||||
assert!(
|
||||
raw.as_ref().is_err_and(|e| e.upstream_status().is_some()),
|
||||
"{raw:?}"
|
||||
);
|
||||
assert!(check_nx1_degeneracy(&k, &mut b, v, fil).unwrap());
|
||||
|
||||
// through Create: the status selects the degenerate corner ...
|
||||
|
||||
@@ -56,7 +56,7 @@ use super::form::binom;
|
||||
use super::interval::{cmax, round_down, round_up, Ivl};
|
||||
use super::series::{Ser, Trunc, VSer, MAX_S, V3};
|
||||
use super::station::{
|
||||
ders_basis, gn_evaluate, gn_evaluate_vn, gn_jacobian, gn_top_jets, knot, span, GnSystem, Nurbs,
|
||||
ders_basis, gn_evaluate, gn_jacobian, gn_top_jets, knot, span, GnIncremental, GnSystem, Nurbs,
|
||||
Partials,
|
||||
};
|
||||
use super::taylor_model::TmN;
|
||||
@@ -661,13 +661,21 @@ pub fn gn_enclose_tm(
|
||||
return Ok(None);
|
||||
}
|
||||
let mut sx = x0.map(Ser::constant);
|
||||
// each order's G takes the lower orders' series as the last one left them: they are
|
||||
// kept, and only the coefficients x's new ones reach are computed (bit for bit a
|
||||
// whole evaluation's)
|
||||
let mut series = GnIncremental::new(&sysk);
|
||||
let eps_solved = if n >= 1 { tr.e().min(n - 1) } else { tr.e() };
|
||||
for tot in 1..=(tr.s() + tr.e()) {
|
||||
let eps = eps_solved.min(tot);
|
||||
if tot > tr.s() + eps {
|
||||
continue;
|
||||
}
|
||||
let (sg, _) = tm_try!(gn_evaluate(&sysk, &sx, tr.computing(tot, eps)));
|
||||
let before = series.computed();
|
||||
let evaluated = series.evaluate(&sx, tr.computing(tot, eps));
|
||||
k.work_counters
|
||||
.add_feet_tm_coefficients(series.computed().saturating_sub(before));
|
||||
let (sg, _, _, _) = tm_try!(evaluated);
|
||||
for i in 0..=tr.s().min(tot) {
|
||||
let kk = tot - i;
|
||||
// the section's jets of order n are the face curve's
|
||||
@@ -712,7 +720,11 @@ pub fn gn_enclose_tm(
|
||||
}
|
||||
}
|
||||
}
|
||||
let (_, mut f, v, nrm) = tm_try!(gn_evaluate_vn(&sysk, &sx, tr));
|
||||
let before = series.computed();
|
||||
let evaluated = series.evaluate(&sx, tr);
|
||||
k.work_counters
|
||||
.add_feet_tm_coefficients(series.computed().saturating_sub(before));
|
||||
let (_, mut f, v, nrm) = tm_try!(evaluated);
|
||||
gn_top_jets(n, tr, &nrm, &mut f);
|
||||
let range_ser = |s: &Ser<Ft>| {
|
||||
let mut o = Ser::constant(IZ);
|
||||
|
||||
+133
-2
@@ -80,6 +80,10 @@ pub struct Trunc {
|
||||
degree: usize,
|
||||
/// The highest power of eps computed.
|
||||
eps: usize,
|
||||
/// The coefficients left as they are (bit [`Trunc::bit`] of `c[i][k]`), whatever the
|
||||
/// rest computes: an incremental evaluation's coefficients already computed
|
||||
/// ([`Trunc::skipping`]).
|
||||
skip: u64,
|
||||
}
|
||||
|
||||
impl Trunc {
|
||||
@@ -98,6 +102,7 @@ impl Trunc {
|
||||
e,
|
||||
degree: s + e,
|
||||
eps: e,
|
||||
skip: 0,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -111,13 +116,56 @@ impl Trunc {
|
||||
e: self.e,
|
||||
degree,
|
||||
eps,
|
||||
skip: self.skip,
|
||||
}
|
||||
}
|
||||
|
||||
/// This truncation, also leaving out the coefficients `c[i][k]` whose bit
|
||||
/// ([`Trunc::bit`]) `mask` sets: an operation leaves them as they are (an operation
|
||||
/// that returns a new series leaves them 0). Each coefficient computed is bit for bit
|
||||
/// the same as without the mask, as it depends only on the operands' coefficients
|
||||
/// (see [`Trunc`]); an incremental evaluation (`station::GnIncremental`) so computes
|
||||
/// only the coefficients it has not kept from an earlier one.
|
||||
#[must_use]
|
||||
pub const fn skipping(self, mask: u64) -> Self {
|
||||
Self {
|
||||
s: self.s,
|
||||
e: self.e,
|
||||
degree: self.degree,
|
||||
eps: self.eps,
|
||||
skip: self.skip | mask,
|
||||
}
|
||||
}
|
||||
|
||||
/// The bit of `c[i][k]` in a [`Trunc::skipping`] mask (`i <= MAX_S`, `k <= STORE_E`).
|
||||
#[must_use]
|
||||
pub const fn bit(i: usize, k: usize) -> u64 {
|
||||
1_u64 << (i * (STORE_E + 1) + k)
|
||||
}
|
||||
|
||||
/// Whether `c[i][k]` is computed.
|
||||
#[inline]
|
||||
const fn computes(self, i: usize, k: usize) -> bool {
|
||||
i <= self.s && k <= self.e && k <= self.eps && i + k <= self.degree
|
||||
#[must_use]
|
||||
pub const fn computes(self, i: usize, k: usize) -> bool {
|
||||
i <= self.s
|
||||
&& k <= self.e
|
||||
&& k <= self.eps
|
||||
&& i + k <= self.degree
|
||||
&& self.skip & Self::bit(i, k) == 0
|
||||
}
|
||||
|
||||
/// The [`Trunc::bit`]s of every coefficient computed.
|
||||
#[must_use]
|
||||
pub fn computed_mask(self) -> u64 {
|
||||
let mut m = 0_u64;
|
||||
for i in 0..=self.s {
|
||||
for k in 0..=self.e {
|
||||
if self.computes(i, k) {
|
||||
m |= Self::bit(i, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
/// The highest total degree `i + k` computed.
|
||||
@@ -229,6 +277,25 @@ impl<T: GnScalar> Ser<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies `src`'s coefficients that `tr` computes into `self`, leaving the rest: an
|
||||
/// incremental evaluation's update of a kept series with the coefficients just
|
||||
/// computed (or its accumulator's reset to its initial value there).
|
||||
pub fn merge_computed(&mut self, src: &Self, tr: Trunc) {
|
||||
for (i, (row, rs)) in self.c.iter_mut().zip(&src.c).enumerate().take(tr.s + 1) {
|
||||
for (k, (x, &y)) in row.iter_mut().zip(rs).enumerate().take(tr.e + 1) {
|
||||
if tr.computes(i, k) {
|
||||
*x = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The coefficients `tr` computes, the rest 0.
|
||||
#[must_use]
|
||||
pub fn masked(&self, tr: Trunc) -> Self {
|
||||
Self::from_fn(tr, |i, k| self.at(i, k))
|
||||
}
|
||||
|
||||
/// `sm_SerScale`: each coefficient times `f`.
|
||||
#[must_use]
|
||||
pub fn scale(&self, f: T, tr: Trunc) -> Self {
|
||||
@@ -305,6 +372,70 @@ impl<T: GnScalar> Ser<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Ser::inv_sqrt`] evaluated incrementally: the series it takes (z, its powers, the
|
||||
/// binomial sum and the result) are kept between calls, and a call computes only the
|
||||
/// coefficients its truncation computes ([`Trunc::skipping`]: the caller leaves out those
|
||||
/// whose operands' coefficients are unchanged). Its operations, their order and so every
|
||||
/// coefficient are [`Ser::inv_sqrt`]'s, bit for bit; the two must change together.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct InvSqrtSeries<T> {
|
||||
z: Ser<T>,
|
||||
/// `z^m`, `m = 0..`: `pow[0]` is 1.
|
||||
pow: Vec<Ser<T>>,
|
||||
sum: Ser<T>,
|
||||
out: Ser<T>,
|
||||
}
|
||||
|
||||
impl<T: GnScalar> Default for InvSqrtSeries<T> {
|
||||
fn default() -> Self {
|
||||
let zero = Ser::constant(T::of(0.0));
|
||||
Self {
|
||||
z: zero,
|
||||
pow: vec![Ser::constant(T::of(1.0))],
|
||||
sum: zero,
|
||||
out: zero,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: GnScalar> InvSqrtSeries<T> {
|
||||
/// `x^(-1/2)` as [`Ser::inv_sqrt`], its coefficients that `tr` computes updated (the
|
||||
/// rest kept from earlier calls). `x` must hold every coefficient below each one
|
||||
/// computed.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// As [`Ser::inv_sqrt`].
|
||||
pub fn update(&mut self, x: &Ser<T>, tr: Trunc) -> Result<&Ser<T>> {
|
||||
let x0 = x.at(0, 0);
|
||||
if !x0.is_positive() {
|
||||
return Err(SolidError::sm_err("blend::fillet_section::Ser::inv_sqrt"));
|
||||
}
|
||||
self.z.merge_computed(&x.scale(T::of(1.0) / x0, tr), tr);
|
||||
self.z.set(0, 0, T::of(0.0));
|
||||
self.sum.merge_computed(&Ser::constant(T::of(1.0)), tr);
|
||||
let mut binom = 1.0_f64;
|
||||
for m in 1..=(tr.s + tr.e) {
|
||||
binom *= (-0.5 - (m - 1) as f64) / m as f64;
|
||||
let Some(prev) = self.pow.get(m - 1) else {
|
||||
return Err(SolidError::sm_err("blend::fillet_section::Ser::inv_sqrt"));
|
||||
};
|
||||
let next = prev.mul_valued(&self.z, m - 1, 1, tr);
|
||||
if self.pow.len() <= m {
|
||||
self.pow.push(Ser::constant(T::of(0.0)));
|
||||
}
|
||||
let Some(pm) = self.pow.get_mut(m) else {
|
||||
return Err(SolidError::sm_err("blend::fillet_section::Ser::inv_sqrt"));
|
||||
};
|
||||
pm.merge_computed(&next, tr);
|
||||
self.sum.add_scaled_from(pm, T::of(binom), m, tr);
|
||||
}
|
||||
self.out
|
||||
.merge_computed(&self.sum.scale(T::of(1.0) / x0.sqrt(), tr), tr);
|
||||
Ok(&self.out)
|
||||
}
|
||||
}
|
||||
|
||||
/// `sm_V3`: a vector of `T`s.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct V3<T> {
|
||||
|
||||
+379
-1
@@ -11,7 +11,7 @@
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::series::{GnScalar, Ser, Trunc, VSer, MAX_E, MAX_K, MAX_S, V3};
|
||||
use super::series::{GnScalar, InvSqrtSeries, Ser, Trunc, VSer, MAX_E, MAX_K, MAX_S, STORE_E, V3};
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::kernel::Kernel;
|
||||
use crate::math::{Point2, Vec3};
|
||||
@@ -713,6 +713,247 @@ pub fn gn_evaluate_vn<T: GnScalar>(
|
||||
Ok(([g0, g1, g2, g3], f_out, *v0, n_out))
|
||||
}
|
||||
|
||||
/// [`gn_evaluate_vn`]'s values: G, the feet, V and the faces' unit normals.
|
||||
pub type GnValuesVN<T> = ([Ser<T>; 4], [VSer<T>; 2], VSer<T>, [VSer<T>; 2]);
|
||||
|
||||
/// One face's series that [`GnIncremental`] keeps between evaluations.
|
||||
#[derive(Clone, Debug)]
|
||||
struct GnFaceSeries<T> {
|
||||
/// `du^a`, `dv^a`: index 0 is 1.
|
||||
pu: Vec<Ser<T>>,
|
||||
pv: Vec<Ser<T>>,
|
||||
s: VSer<T>,
|
||||
su: VSer<T>,
|
||||
sv: VSer<T>,
|
||||
nrm: VSer<T>,
|
||||
nn: Ser<T>,
|
||||
inv: InvSqrtSeries<T>,
|
||||
w: Ser<T>,
|
||||
v: VSer<T>,
|
||||
n: VSer<T>,
|
||||
}
|
||||
|
||||
impl<T: GnScalar> GnFaceSeries<T> {
|
||||
fn new() -> Self {
|
||||
let z = Ser::constant(T::of(0.0));
|
||||
let vz = VSer { a: [z; 3] };
|
||||
let one = vec![Ser::constant(T::of(1.0))];
|
||||
Self {
|
||||
pu: one.clone(),
|
||||
pv: one,
|
||||
s: vz,
|
||||
su: vz,
|
||||
sv: vz,
|
||||
nrm: vz,
|
||||
nn: z,
|
||||
inv: InvSqrtSeries::default(),
|
||||
w: z,
|
||||
v: vz,
|
||||
n: vz,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `v.a[j]` merged from `src.a[j]` where `tr` computes.
|
||||
fn merge_v<T: GnScalar>(v: &mut VSer<T>, src: &VSer<T>, tr: Trunc) {
|
||||
for (a, b) in v.a.iter_mut().zip(&src.a) {
|
||||
a.merge_computed(b, tr);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`gn_evaluate_vn`] evaluated incrementally, for a caller that evaluates the same system
|
||||
/// again and again as it fills in the feet's series (the certificate's Taylor models
|
||||
/// solve them order by order, [`super::feet_tm::gn_enclose_tm`]): every series
|
||||
/// [`gn_evaluate_vn`] takes is kept, and an evaluation computes only the coefficients its
|
||||
/// truncation computes that no earlier one has computed from the same coefficients of x.
|
||||
/// Each coefficient depends only on x's coefficients at the same or lower powers of both
|
||||
/// sigma and eps (see [`Trunc`]), so a kept one stays valid until x changes in that
|
||||
/// quadrant, and the evaluation's operations, their order and so every value are
|
||||
/// [`gn_evaluate_vn`]'s, bit for bit: the two must change together (a test compares them).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GnIncremental<'s, 'a, T> {
|
||||
sys: &'s GnSystem<'a, T>,
|
||||
/// The orders of the kept series (`Trunc::s`, `Trunc::e`), none before the first.
|
||||
orders: Option<(usize, usize)>,
|
||||
/// The x the kept series were computed from.
|
||||
x: [Ser<T>; 4],
|
||||
/// The kept coefficients still valid ([`Trunc::bit`]s, the same for every series).
|
||||
valid: u64,
|
||||
faces: [GnFaceSeries<T>; 2],
|
||||
g: [Ser<T>; 4],
|
||||
/// `V - leader` (its dot product with the tangent reads all its lower coefficients).
|
||||
t: VSer<T>,
|
||||
/// The coefficients computed so far (each evaluation adds those of its series' it
|
||||
/// computes, counted once per position).
|
||||
computed: u64,
|
||||
}
|
||||
|
||||
impl<'s, 'a, T: GnScalar + PartialEq> GnIncremental<'s, 'a, T> {
|
||||
/// An evaluator of `sys` with nothing kept.
|
||||
#[must_use]
|
||||
pub fn new(sys: &'s GnSystem<'a, T>) -> Self {
|
||||
let z = Ser::constant(T::of(0.0));
|
||||
Self {
|
||||
sys,
|
||||
orders: None,
|
||||
x: [z; 4],
|
||||
valid: 0,
|
||||
faces: [GnFaceSeries::new(), GnFaceSeries::new()],
|
||||
g: [z; 4],
|
||||
t: VSer { a: [z; 3] },
|
||||
computed: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The positions `(i, k)` computed so far, summed over the evaluations: the work
|
||||
/// counter of the certificate's feet Taylor models. A [`gn_evaluate_vn`] call computes
|
||||
/// every position its truncation computes.
|
||||
#[must_use]
|
||||
pub const fn computed(&self) -> u64 {
|
||||
self.computed
|
||||
}
|
||||
|
||||
/// [`gn_evaluate_vn`]`(sys, x, tr)`, bit for bit.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// As [`gn_evaluate_vn`].
|
||||
pub fn evaluate(&mut self, x: &[Ser<T>; 4], tr: Trunc) -> Result<GnValuesVN<T>> {
|
||||
let out = self.evaluate_kept(x, tr);
|
||||
if out.is_err() {
|
||||
// the kept series are part way through an update
|
||||
self.valid = 0;
|
||||
self.orders = None;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn evaluate_kept(&mut self, x: &[Ser<T>; 4], tr: Trunc) -> Result<GnValuesVN<T>> {
|
||||
if self.orders != Some((tr.s(), tr.e())) {
|
||||
let computed = self.computed;
|
||||
*self = Self::new(self.sys);
|
||||
self.computed = computed;
|
||||
self.orders = Some((tr.s(), tr.e()));
|
||||
}
|
||||
// a coefficient of x that changed invalidates every kept one at or above both its
|
||||
// powers
|
||||
for (xn, xo) in x.iter().zip(&self.x) {
|
||||
for i in 0..=MAX_S {
|
||||
for k in 0..=STORE_E {
|
||||
if xn.at(i, k) != xo.at(i, k) {
|
||||
for i2 in i..=MAX_S {
|
||||
for k2 in k..=STORE_E {
|
||||
self.valid &= !Trunc::bit(i2, k2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.x = *x;
|
||||
let all = tr.computed_mask();
|
||||
let btr = tr.skipping(self.valid);
|
||||
self.computed = self
|
||||
.computed
|
||||
.saturating_add(u64::from(btr.computed_mask().count_ones()));
|
||||
let lm = tr.s() + tr.e();
|
||||
let top = lm.min(tr.degree());
|
||||
let zero = Ser::constant(T::of(0.0));
|
||||
let vzero = VSer { a: [zero; 3] };
|
||||
let sys = self.sys;
|
||||
let rows = x.chunks_exact(2).zip(sys.p).zip(sys.r.iter().zip(sys.side));
|
||||
for (fs, ((xf, pf), (&rf, sidef))) in self.faces.iter_mut().zip(rows) {
|
||||
let (Some(xu), Some(xv)) = (xf.first(), xf.get(1)) else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
};
|
||||
let mut du = *xu;
|
||||
let mut dv = *xv;
|
||||
du.set(0, 0, T::of(0.0));
|
||||
dv.set(0, 0, T::of(0.0));
|
||||
for a in 1..=top {
|
||||
let (Some(lu), Some(lv)) = (fs.pu.get(a - 1), fs.pv.get(a - 1)) else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
};
|
||||
let (nu, nv) = (
|
||||
lu.mul_valued(&du, a - 1, 1, btr),
|
||||
lv.mul_valued(&dv, a - 1, 1, btr),
|
||||
);
|
||||
if fs.pu.len() <= a {
|
||||
fs.pu.push(zero);
|
||||
fs.pv.push(zero);
|
||||
}
|
||||
let (Some(pua), Some(pva)) = (fs.pu.get_mut(a), fs.pv.get_mut(a)) else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
};
|
||||
pua.merge_computed(&nu, btr);
|
||||
pva.merge_computed(&nv, btr);
|
||||
}
|
||||
merge_v(&mut fs.s, &vzero, btr);
|
||||
merge_v(&mut fs.su, &vzero, btr);
|
||||
merge_v(&mut fs.sv, &vzero, btr);
|
||||
let mut fa = 1.0_f64;
|
||||
for (a, pua) in fs.pu.iter().enumerate().take(top + 1) {
|
||||
if a > 0 {
|
||||
fa *= a as f64;
|
||||
}
|
||||
let mut fb = 1.0_f64;
|
||||
for (b, pvb) in fs.pv.iter().enumerate().take(top - a + 1) {
|
||||
if b > 0 {
|
||||
fb *= b as f64;
|
||||
}
|
||||
let mono = pua.mul_valued(pvb, a, b, btr);
|
||||
let sf = T::of(1.0) / T::of(fa * fb);
|
||||
let cs = [pf.at(a, b), pf.at(a + 1, b), pf.at(a, b + 1)];
|
||||
let outs: [&mut VSer<T>; 3] = [&mut fs.s, &mut fs.su, &mut fs.sv];
|
||||
for (c, out) in cs.iter().zip(outs) {
|
||||
let cv = [c.x, c.y, c.z];
|
||||
for (o, &cj) in out.a.iter_mut().zip(&cv) {
|
||||
o.add_scaled_from(&mono, cj * sf, a + b, btr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
merge_v(&mut fs.nrm, &fs.su.cross(&fs.sv, btr), btr);
|
||||
fs.nn.merge_computed(&fs.nrm.dot(&fs.nrm, btr), btr);
|
||||
let inv_len = *fs.inv.update(&fs.nn, btr)?;
|
||||
let mut d = Ser::constant(T::of(rf * sidef));
|
||||
if let Some(law) = sys.law {
|
||||
for i in 0..=tr.s() {
|
||||
d.set(i, 0, law.at(i, 0) * T::of(sidef));
|
||||
}
|
||||
}
|
||||
if !sys.hold && tr.e() >= 1 {
|
||||
for i in 0..=tr.s() {
|
||||
d.set(i, 1, d.at(i, 0));
|
||||
}
|
||||
}
|
||||
fs.w.merge_computed(&inv_len.mul(&d, btr), btr);
|
||||
merge_v(&mut fs.v, &fs.s.add(&fs.nrm.times(&fs.w, btr), btr), btr);
|
||||
merge_v(&mut fs.n, &fs.nrm.times(&inv_len, btr), btr);
|
||||
}
|
||||
let [f0, f1] = &self.faces;
|
||||
let diff = f0.v.sub(&f1.v, btr);
|
||||
for (g, d) in self.g.iter_mut().zip(&diff.a) {
|
||||
g.merge_computed(d, btr);
|
||||
}
|
||||
merge_v(&mut self.t, &f0.v.sub(&sys.leader, btr), btr);
|
||||
if let Some(g3) = self.g.get_mut(3) {
|
||||
g3.merge_computed(&self.t.dot(&sys.tangent, btr), btr);
|
||||
}
|
||||
self.valid |= all;
|
||||
let mv = |v: &VSer<T>| VSer {
|
||||
a: v.a.each_ref().map(|c| c.masked(tr)),
|
||||
};
|
||||
let [f0, f1] = &self.faces;
|
||||
Ok((
|
||||
self.g.each_ref().map(|c| c.masked(tr)),
|
||||
[mv(&f0.s), mv(&f1.s)],
|
||||
mv(&f0.v),
|
||||
[mv(&f0.n), mv(&f1.n)],
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// `sm_GnTopJets` (3bc54a4): the section's jets of order `top` (its order n) and up, from
|
||||
/// the faces' order-n partials only. The feet's parameters are solved to order n - 1 in
|
||||
/// eps, and the face curve `C_i(tau) = S_i(x_i(tau))`, `x_i` so truncated, is a curve on
|
||||
@@ -1720,4 +1961,141 @@ mod tests {
|
||||
// 554 of the 2001 differences are not doubles: the case the construction is for
|
||||
assert_eq!(inexact, 554);
|
||||
}
|
||||
|
||||
/// [`GnIncremental`] fed as the feet's Taylor models feed it (x filled in order by
|
||||
/// order, each order evaluated with the lower ones' coefficients set and its own 0,
|
||||
/// then one whole evaluation), against [`gn_evaluate_vn`] at each call: equal bit for
|
||||
/// bit. Returns the positions it computed and those the whole evaluations compute.
|
||||
fn incremental_matches_whole<T: GnScalar + PartialEq + core::fmt::Debug>(
|
||||
mk: impl Fn(f64) -> T,
|
||||
s: usize,
|
||||
e: usize,
|
||||
n: usize,
|
||||
law: bool,
|
||||
hold: bool,
|
||||
) -> (u64, u64) {
|
||||
let mut seed = 0x9e37_79b9_7f4a_7c15_u64;
|
||||
let mut next = move || {
|
||||
seed = seed
|
||||
.wrapping_mul(6_364_136_223_846_793_005)
|
||||
.wrapping_add(1_442_695_040_888_963_407);
|
||||
(seed >> 11) as f64 / (1_u64 << 53) as f64 - 0.5
|
||||
};
|
||||
let mut v3 = |base: [f64; 3]| V3 {
|
||||
x: mk(base[0] + 0.3 * next()),
|
||||
y: mk(base[1] + 0.3 * next()),
|
||||
z: mk(base[2] + 0.3 * next()),
|
||||
};
|
||||
let mut faces = [Partials::default(), Partials::default()];
|
||||
for (f, pf) in faces.iter_mut().enumerate() {
|
||||
let sgn = if f == 0 { 1.0 } else { -1.0 };
|
||||
for a in 0..=MAX_K {
|
||||
for b in 0..=(MAX_K - a) {
|
||||
let base = match (a, b) {
|
||||
(1, 0) => [1.0, 0.0, 0.0],
|
||||
(0, 1) => [0.0, sgn, 0.2],
|
||||
_ => [0.0; 3],
|
||||
};
|
||||
pf.set(a, b, v3(base));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut ser = |c0: f64| {
|
||||
let mut x = Ser::constant(mk(c0));
|
||||
for i in 0..=MAX_S {
|
||||
for k in 0..=MAX_E {
|
||||
if i + k > 0 {
|
||||
x.set(i, k, mk(next()));
|
||||
}
|
||||
}
|
||||
}
|
||||
x
|
||||
};
|
||||
let leader = VSer {
|
||||
a: [ser(0.1), ser(0.2), ser(0.3)],
|
||||
};
|
||||
let tangent = VSer {
|
||||
a: [ser(0.0), ser(0.6), ser(0.8)],
|
||||
};
|
||||
let law_ser = ser(0.7);
|
||||
let [p0, p1] = &faces;
|
||||
let sys = GnSystem {
|
||||
p: [p0, p1],
|
||||
leader,
|
||||
tangent,
|
||||
r: [0.5, 0.75],
|
||||
side: [1.0, -1.0],
|
||||
hold,
|
||||
law: law.then_some(&law_ser),
|
||||
};
|
||||
let tr = Trunc::new(s, e).unwrap();
|
||||
let mut sx = [0.3, -0.2, 0.1, 0.4].map(|c| Ser::constant(mk(c)));
|
||||
let mut inc = GnIncremental::new(&sys);
|
||||
let mut whole = 0_u64;
|
||||
let eps_solved = if n >= 1 { e.min(n - 1) } else { e };
|
||||
for tot in 1..=(s + e) {
|
||||
let eps = eps_solved.min(tot);
|
||||
if tot > s + eps {
|
||||
continue;
|
||||
}
|
||||
let at = tr.computing(tot, eps);
|
||||
let want = gn_evaluate_vn(&sys, &sx, at).unwrap();
|
||||
whole += u64::from(at.computed_mask().count_ones());
|
||||
assert_eq!(inc.evaluate(&sx, at).unwrap(), want, "order {tot}");
|
||||
for i in 0..=s.min(tot) {
|
||||
let kk = tot - i;
|
||||
if kk > e || (n >= 1 && kk >= n) {
|
||||
continue;
|
||||
}
|
||||
for xc in &mut sx {
|
||||
xc.set(i, kk, mk(0.2 * next()));
|
||||
}
|
||||
}
|
||||
}
|
||||
let want = gn_evaluate_vn(&sys, &sx, tr).unwrap();
|
||||
whole += u64::from(tr.computed_mask().count_ones());
|
||||
assert_eq!(inc.evaluate(&sx, tr).unwrap(), want, "whole");
|
||||
// nothing changed: nothing to compute, the same values
|
||||
let before = inc.computed();
|
||||
assert_eq!(inc.evaluate(&sx, tr).unwrap(), want, "again");
|
||||
assert_eq!(inc.computed(), before);
|
||||
// a changed lower coefficient: computed again from it
|
||||
sx[1].set(1, 0, mk(0.05));
|
||||
assert_eq!(
|
||||
inc.evaluate(&sx, tr).unwrap(),
|
||||
gn_evaluate_vn(&sys, &sx, tr).unwrap()
|
||||
);
|
||||
(before, whole)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incremental_evaluation_is_the_whole_one_bit_for_bit() {
|
||||
let tm = |v: f64| {
|
||||
crate::blend::fillet_section::feet_tm::Ft::from_points(&[v, 0.01 * v, -0.005 * v])
|
||||
};
|
||||
for (s, e, n) in [(3, 2, 2), (2, 3, 2), (3, 0, 0), (2, 2, 0), (4, 1, 1)] {
|
||||
for (law, hold) in [(false, false), (true, false), (false, true)] {
|
||||
incremental_matches_whole(|v| v, s, e, n, law, hold);
|
||||
incremental_matches_whole(Ivl::from, s, e, n, law, hold);
|
||||
incremental_matches_whole(tm, s, e, n, law, hold);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The work counter of the feet's Taylor models: their order-by-order evaluations
|
||||
/// compute each coefficient of G at most twice (at its order with its own 0, and once
|
||||
/// it is set), not again at every later order.
|
||||
#[test]
|
||||
fn incremental_evaluation_computes_each_order_once() {
|
||||
// the certificate's orders: order 3 in sigma, 2 in eps, the jets of order 2 the
|
||||
// faces'
|
||||
let (inc, whole) = incremental_matches_whole(|v| v, 3, 2, 2, false, false);
|
||||
let full = u64::from(Trunc::new(3, 2).unwrap().computed_mask().count_ones());
|
||||
assert_eq!(full, 12);
|
||||
assert!(
|
||||
inc <= 2 * full,
|
||||
"{inc} computed, at most twice each of {full}"
|
||||
);
|
||||
assert_eq!((inc, whole), (19, 35));
|
||||
}
|
||||
}
|
||||
|
||||
+7
-14
@@ -1218,14 +1218,12 @@ mod tests_c1 {
|
||||
// EvaluateBinormal fails (the edge use is in no loop: SM_ERR_INVALID_INPUT) and
|
||||
// returns before RemoveLast
|
||||
let e = c1_collect_points(&mut ops, &mut cx, &mut st, 0).unwrap_err();
|
||||
assert_eq!(e.operation(), "topology::edgeuse::p3");
|
||||
assert_eq!(
|
||||
e,
|
||||
SolidError::Upstream {
|
||||
operation: "topology::edgeuse::p3",
|
||||
code: crate::error::SM_ERR_INVALID_INPUT,
|
||||
entity: None,
|
||||
}
|
||||
e.upstream_status(),
|
||||
Some(crate::error::SM_ERR_INVALID_INPUT)
|
||||
);
|
||||
assert_eq!(e.entity(), None);
|
||||
assert_eq!(ops.0.g1_knots, vec![0.5]);
|
||||
assert_eq!(st.split_tsect_pnt, None);
|
||||
}
|
||||
@@ -1295,14 +1293,9 @@ mod tests_c1 {
|
||||
// GetOrCreateUVTrimCurve builds the missing curve; CreateUVTrimCurve's failing
|
||||
// status (here NER) is SER'd
|
||||
let e = c1_collect_points(&mut ops, &mut cx, &mut st, 0).unwrap_err();
|
||||
assert_eq!(
|
||||
e,
|
||||
SolidError::Upstream {
|
||||
operation: "topology::edgeuse::p3",
|
||||
code: crate::error::SM_ERR_NULL_POINTER,
|
||||
entity: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(e.operation(), "topology::edgeuse::p3");
|
||||
assert_eq!(e.upstream_status(), Some(crate::error::SM_ERR_NULL_POINTER));
|
||||
assert_eq!(e.entity(), None);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+23
-5
@@ -31,6 +31,7 @@
|
||||
//! | [`sweep::SweepFailure`] | none: why a sweep or loft refuses, and the sweep routines' failures |
|
||||
//! | [`curve::CurveFailure`] | none: why a curve query refuses (#3184) |
|
||||
//! | [`query::QueryFailure`] | none: the query routines' failures |
|
||||
//! | [`topology::TopologyFailure`] | none: the topology routines' failures |
|
||||
//!
|
||||
//! NLib routines (`NL_*.cpp`) report failure by returning an error flag and storing an
|
||||
//! error number in the process-wide `NL_ERROR` record; solidcore returns a
|
||||
@@ -89,6 +90,7 @@ pub mod offset;
|
||||
pub mod primitive;
|
||||
pub mod query;
|
||||
pub mod sweep;
|
||||
pub mod topology;
|
||||
|
||||
use crate::topology::{
|
||||
BrepId, EdgeId, EdgeuseId, FaceId, FaceuseId, LoopId, LoopuseId, RegionId, ShellId, VertexId,
|
||||
@@ -637,7 +639,8 @@ impl core::fmt::Display for Failure {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "{}: {}", self.operation, self.reason)?;
|
||||
// a reason that names no entity of its own is followed by the failure's
|
||||
if let (Reason::Failed(_), Some(entity)) = (&self.reason, self.entity) {
|
||||
if let (Reason::Failed(_) | Reason::Topology(_), Some(entity)) = (&self.reason, self.entity)
|
||||
{
|
||||
write!(f, " ({entity})")?;
|
||||
}
|
||||
if let Some(cause) = &self.cause {
|
||||
@@ -681,6 +684,8 @@ pub enum Reason {
|
||||
Curve(curve::CurveFailure),
|
||||
/// A query failed.
|
||||
Query(query::QueryFailure),
|
||||
/// A topology routine failed.
|
||||
Topology(topology::TopologyFailure),
|
||||
}
|
||||
|
||||
impl Reason {
|
||||
@@ -698,7 +703,8 @@ impl Reason {
|
||||
| Self::Heal(_)
|
||||
| Self::Geom(_)
|
||||
| Self::Curve(_)
|
||||
| Self::Query(_) => None,
|
||||
| Self::Query(_)
|
||||
| Self::Topology(_) => None,
|
||||
Self::Offset(o) => o.entity(),
|
||||
Self::Boolean(b) => b.entity(),
|
||||
}
|
||||
@@ -721,6 +727,7 @@ impl Reason {
|
||||
Self::Geom(g) => g.comparison_status(),
|
||||
Self::Curve(c) => c.comparison_status(),
|
||||
Self::Query(q) => q.comparison_status(),
|
||||
Self::Topology(t) => t.comparison_status(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -741,6 +748,7 @@ impl Reason {
|
||||
Self::Geom(g) => g.kind(),
|
||||
Self::Curve(c) => c.kind(),
|
||||
Self::Query(q) => q.kind(),
|
||||
Self::Topology(t) => t.kind(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -761,6 +769,7 @@ impl core::fmt::Display for Reason {
|
||||
Self::Geom(g) => g.fmt(f),
|
||||
Self::Curve(c) => c.fmt(f),
|
||||
Self::Query(q) => q.fmt(f),
|
||||
Self::Topology(t) => t.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -837,6 +846,12 @@ impl From<query::QueryFailure> for Reason {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<topology::TopologyFailure> for Reason {
|
||||
fn from(t: topology::TopologyFailure) -> Self {
|
||||
Self::Topology(t)
|
||||
}
|
||||
}
|
||||
|
||||
/// A stable solidcore code for the kind of an error, from [`SolidError::kind`]: match
|
||||
/// on it (or on its [`ErrorKind::code`] or [`ErrorKind::name`]) instead of message
|
||||
/// text. Codes and names never change meaning once released; new kinds are added as
|
||||
@@ -1065,7 +1080,8 @@ impl SolidError {
|
||||
/// [`SolidError::Upstream`] does: its `code`, or the status of a failure that replaced
|
||||
/// an `Upstream` site ([`nurbs::BSplineFailure`], [`intersect::IntersectFailure`],
|
||||
/// [`primitive::PrimitiveFailure`], [`mass::MassFailure`], [`heal::HealFailure`],
|
||||
/// [`geom::GeomFailure`], [`query::QueryFailure`], a [`sweep::SweepFailure`] that
|
||||
/// [`geom::GeomFailure`], [`query::QueryFailure`], [`topology::TopologyFailure`], a
|
||||
/// [`sweep::SweepFailure`] that
|
||||
/// [replaced one](sweep::SweepFailure::replaces_upstream_status)). The callers that branch
|
||||
/// on an upstream status use it so that they treat both alike; it goes with `Upstream` in
|
||||
/// the last PR of the typed-errors work.
|
||||
@@ -1080,7 +1096,8 @@ impl SolidError {
|
||||
| Reason::Mass(_)
|
||||
| Reason::Heal(_)
|
||||
| Reason::Geom(_)
|
||||
| Reason::Query(_) => Some(f.status),
|
||||
| Reason::Query(_)
|
||||
| Reason::Topology(_) => Some(f.status),
|
||||
Reason::Sweep(w) if w.replaces_upstream_status() => Some(f.status),
|
||||
_ => None,
|
||||
},
|
||||
@@ -1302,7 +1319,8 @@ impl SolidError {
|
||||
/// `Upstream`'s code, or the status of a failure that replaced an `Upstream` site
|
||||
/// ([`nurbs::BSplineFailure`], [`intersect::IntersectFailure`],
|
||||
/// [`primitive::PrimitiveFailure`], [`mass::MassFailure`], [`heal::HealFailure`],
|
||||
/// [`geom::GeomFailure`], [`query::QueryFailure`], a [`sweep::SweepFailure`] that
|
||||
/// [`geom::GeomFailure`], [`query::QueryFailure`], [`topology::TopologyFailure`], a
|
||||
/// [`sweep::SweepFailure`] that
|
||||
/// [replaced one](sweep::SweepFailure::replaces_upstream_status)).
|
||||
///
|
||||
/// For difftest's comparison only: it is removed with `Upstream` and `sm_status` in the
|
||||
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
//! Why a topology routine fails, in solidcore's terms.
|
||||
//!
|
||||
//! Upstream: none. These describe failures of the topology subsystem (`crate::topology`:
|
||||
//! the B-rep constructor, cache and data streams, faces, edges, edge uses, loop uses,
|
||||
//! vertex uses, shells, trimming tools, cutting, mirroring, planar sections, vertex fans,
|
||||
//! projection, traversal and the file wrappers). Each also records, for difftest only,
|
||||
//! the status the C++ reports for it (crate-private `comparison_status`, read through
|
||||
//! [`crate::SolidError::sm_status`]).
|
||||
//!
|
||||
//! | Rust | Status difftest compares |
|
||||
//! |--------------------------------------|-------------------------------|
|
||||
//! | [`TopologyFailure::Failed`] | `SM_ERR` |
|
||||
//! | [`TopologyFailure::Missing`] | `SM_ERR_NULL_POINTER` |
|
||||
//! | [`TopologyFailure::InvalidValue`] | `SM_ERR_INVALID_INPUT` |
|
||||
//! | [`TopologyFailure::Inconsistent`] | `SM_ERR_ASSERT_FAILURE` |
|
||||
//! | [`TopologyFailure::OutOfMemory`] | `SM_ERR_OUT_OF_MEMORY` |
|
||||
//! | [`TopologyFailure::DropFailed`] | the drop's own recorded status |
|
||||
//!
|
||||
//! Deviations: not upstream. A failure says in a phrase (or by its variant) what went
|
||||
//! wrong; the failure's `operation` path says which routine, and its entity is the one
|
||||
//! the site it replaces named (a vertex use for the vertex-use routines, none
|
||||
//! elsewhere). The failures that are checks of a caller's arguments stay
|
||||
//! [`crate::SolidError::InvalidInput`]. The files with open work elsewhere (`brep/p*`,
|
||||
//! `local_operation`, `validation/{pointers,assert_valid,soundness,coincidence}`,
|
||||
//! `face/p2`, `face/p3`, `edgeuse/p2`, `topology.rs`, `proto_topology`) do not use these
|
||||
//! yet.
|
||||
|
||||
use super::{
|
||||
EntityRef, ErrorKind, Failure, Reason, SM_ERR, SM_ERR_ASSERT_FAILURE, SM_ERR_INVALID_INPUT,
|
||||
SM_ERR_NULL_POINTER, SM_ERR_OUT_OF_MEMORY,
|
||||
};
|
||||
|
||||
/// Why a topology routine fails.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum TopologyFailure {
|
||||
/// The routine could not continue, as the phrase says (for example "the loop does
|
||||
/// not close").
|
||||
Failed(&'static str),
|
||||
/// Something the routine needs is missing, as the phrase says (for example "the
|
||||
/// edge's curve").
|
||||
Missing(&'static str),
|
||||
/// A value the routine is given or computes with is not usable, as the phrase says
|
||||
/// (for example "the plane normal has zero length"). Upstream returns
|
||||
/// `SM_ERR_INVALID_INPUT` as a status here, so its kind is
|
||||
/// [`ErrorKind::InvalidInput`].
|
||||
InvalidValue(&'static str),
|
||||
/// A consistency check of the routine's own data did not hold, as the phrase says.
|
||||
Inconsistent(&'static str),
|
||||
/// Memory for the data could not be had, as the phrase says.
|
||||
OutOfMemory(&'static str),
|
||||
/// Dropping the edge use's curve onto its face failed earlier, with `status`, and the
|
||||
/// edge use keeps that failure.
|
||||
DropFailed {
|
||||
/// The status the drop recorded (a comparison detail).
|
||||
status: i32,
|
||||
},
|
||||
}
|
||||
|
||||
impl TopologyFailure {
|
||||
/// The status the C++ reports for the failure (a comparison detail).
|
||||
pub(crate) const fn comparison_status(&self) -> i32 {
|
||||
match self {
|
||||
Self::Failed(_) => SM_ERR,
|
||||
Self::Missing(_) => SM_ERR_NULL_POINTER,
|
||||
Self::InvalidValue(_) => SM_ERR_INVALID_INPUT,
|
||||
Self::Inconsistent(_) => SM_ERR_ASSERT_FAILURE,
|
||||
Self::OutOfMemory(_) => SM_ERR_OUT_OF_MEMORY,
|
||||
Self::DropFailed { status } => *status,
|
||||
}
|
||||
}
|
||||
|
||||
/// The failure's stable [`ErrorKind`]: [`ErrorKind::Missing`] for
|
||||
/// [`TopologyFailure::Missing`], [`ErrorKind::InvalidInput`] for
|
||||
/// [`TopologyFailure::InvalidValue`], the drop status's kind for
|
||||
/// [`TopologyFailure::DropFailed`], [`ErrorKind::Failed`] otherwise.
|
||||
#[must_use]
|
||||
pub const fn kind(&self) -> ErrorKind {
|
||||
ErrorKind::of_status(self.comparison_status())
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for TopologyFailure {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Failed(what) | Self::InvalidValue(what) => f.write_str(what),
|
||||
Self::Missing(what) => write!(f, "{what} is missing"),
|
||||
Self::Inconsistent(what) => write!(f, "a consistency check failed: {what}"),
|
||||
Self::OutOfMemory(what) => write!(f, "out of memory for {what}"),
|
||||
Self::DropFailed { .. } => {
|
||||
f.write_str("the edge use's curve could not be dropped onto its face")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::SolidError {
|
||||
/// [`crate::SolidError::Failed`] for a topology routine that fails with `failure`.
|
||||
#[must_use]
|
||||
pub(crate) fn topology(operation: &'static str, failure: TopologyFailure) -> Self {
|
||||
Failure::new(operation, failure).into_error()
|
||||
}
|
||||
|
||||
/// [`crate::SolidError::Failed`] for a topology routine that fails with `failure`
|
||||
/// about `entity`.
|
||||
#[must_use]
|
||||
pub(crate) fn topology_at(
|
||||
operation: &'static str,
|
||||
failure: TopologyFailure,
|
||||
entity: EntityRef,
|
||||
) -> Self {
|
||||
Failure::new(operation, failure).at(entity).into_error()
|
||||
}
|
||||
|
||||
/// [`crate::SolidError::Failed`] for a topology routine that could not continue, as
|
||||
/// the phrase says, with status [`SM_ERR`] (upstream `return SE(SM_ERR)`).
|
||||
#[must_use]
|
||||
pub(crate) fn topology_failed(operation: &'static str, what: &'static str) -> Self {
|
||||
Self::topology(operation, TopologyFailure::Failed(what))
|
||||
}
|
||||
|
||||
/// [`crate::SolidError::Failed`] for something a topology routine needs that is not
|
||||
/// there, as the phrase says, with status [`SM_ERR_NULL_POINTER`] (upstream
|
||||
/// `NER(ptr)` on a `NULL` pointer).
|
||||
#[must_use]
|
||||
pub(crate) fn topology_missing(operation: &'static str, what: &'static str) -> Self {
|
||||
Self::topology(operation, TopologyFailure::Missing(what))
|
||||
}
|
||||
|
||||
/// [`crate::SolidError::Failed`] for a value a topology routine computes with that has
|
||||
/// no defined result, as the phrase says, with status [`SM_ERR_INVALID_INPUT`]
|
||||
/// (upstream `return(SM_ERR_INVALID_INPUT)`, or `SER` of a vector operation that
|
||||
/// returns it).
|
||||
#[must_use]
|
||||
pub(crate) fn topology_invalid_value(operation: &'static str, what: &'static str) -> Self {
|
||||
Self::topology(operation, TopologyFailure::InvalidValue(what))
|
||||
}
|
||||
|
||||
/// The topology failure behind this error, for a [`crate::SolidError::Failed`] with a
|
||||
/// [`Reason::Topology`].
|
||||
#[must_use]
|
||||
pub fn topology_failure(&self) -> Option<&TopologyFailure> {
|
||||
match self {
|
||||
Self::Failed(f) => match &f.reason {
|
||||
Reason::Topology(t) => Some(t),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::SM_ERR_OUTSIDE_OF_DOMAIN;
|
||||
use crate::topology::VertexuseId;
|
||||
use crate::SolidError;
|
||||
|
||||
#[test]
|
||||
fn topology_failures_keep_the_status_kind_and_entity_of_the_upstream_they_replace() {
|
||||
let vu = Some(EntityRef::Vertexuse(VertexuseId::from_index(3)));
|
||||
let cases = [
|
||||
(TopologyFailure::Failed("x"), SM_ERR),
|
||||
(TopologyFailure::Missing("x"), SM_ERR_NULL_POINTER),
|
||||
(TopologyFailure::InvalidValue("x"), SM_ERR_INVALID_INPUT),
|
||||
(TopologyFailure::Inconsistent("x"), SM_ERR_ASSERT_FAILURE),
|
||||
(TopologyFailure::OutOfMemory("x"), SM_ERR_OUT_OF_MEMORY),
|
||||
(
|
||||
TopologyFailure::DropFailed {
|
||||
status: SM_ERR_OUTSIDE_OF_DOMAIN,
|
||||
},
|
||||
SM_ERR_OUTSIDE_OF_DOMAIN,
|
||||
),
|
||||
];
|
||||
for (failure, code) in cases {
|
||||
for entity in [None, vu] {
|
||||
let typed = match entity {
|
||||
Some(e) => SolidError::topology_at("topology::op", failure, e),
|
||||
None => SolidError::topology("topology::op", failure),
|
||||
};
|
||||
let before = SolidError::Upstream {
|
||||
operation: "topology::op",
|
||||
code,
|
||||
entity,
|
||||
};
|
||||
assert_eq!(typed.sm_status(), before.sm_status(), "{failure:?}");
|
||||
assert_eq!(typed.upstream_status(), Some(code), "{failure:?}");
|
||||
assert_eq!(super::super::upstream_status(&typed), Some(code));
|
||||
assert_eq!(typed.kind(), before.kind(), "{failure:?}");
|
||||
assert_eq!(typed.operation(), before.operation(), "{failure:?}");
|
||||
assert_eq!(typed.entity(), before.entity(), "{failure:?}");
|
||||
assert_eq!(typed.is_port_failure(), before.is_port_failure());
|
||||
assert_eq!(typed.is_status_answer(), before.is_status_answer());
|
||||
assert_eq!(
|
||||
typed.is_unported_or_cancelled(),
|
||||
before.is_unported_or_cancelled()
|
||||
);
|
||||
assert_eq!(typed.topology_failure(), Some(&failure));
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
SolidError::topology_missing("topology::op", "the edge's curve").to_string(),
|
||||
"topology::op: the edge's curve is missing"
|
||||
);
|
||||
assert_eq!(
|
||||
SolidError::topology_at(
|
||||
"topology::op",
|
||||
TopologyFailure::Failed("the vertex use has no point"),
|
||||
EntityRef::Vertexuse(VertexuseId::from_index(3)),
|
||||
)
|
||||
.to_string(),
|
||||
format!(
|
||||
"topology::op: the vertex use has no point ({})",
|
||||
EntityRef::Vertexuse(VertexuseId::from_index(3))
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
SolidError::topology_invalid_value("topology::op", "x").kind(),
|
||||
ErrorKind::InvalidInput
|
||||
);
|
||||
assert_eq!(
|
||||
SolidError::topology_failed("topology::op", "x").kind(),
|
||||
ErrorKind::Failed
|
||||
);
|
||||
assert_eq!(SolidError::sm_err("topology::op").topology_failure(), None);
|
||||
}
|
||||
|
||||
/// The converted topology routines describe their failures: their library code
|
||||
/// builds no bare `Upstream` status.
|
||||
#[test]
|
||||
fn the_topology_routines_build_no_bare_statuses() {
|
||||
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/topology");
|
||||
for rel in [
|
||||
"api_files.rs",
|
||||
"api_projection.rs",
|
||||
"brep/cutting.rs",
|
||||
"brep/mirror.rs",
|
||||
"brep/planar_section.rs",
|
||||
"brep/validation/assert_valid/seams.rs",
|
||||
"brep/vertex_fans.rs",
|
||||
"brep_cache.rs",
|
||||
"brep_constructor.rs",
|
||||
"brep_data.rs",
|
||||
"brep_data_write.rs",
|
||||
"database_io.rs",
|
||||
"edge.rs",
|
||||
"edgeuse/p1.rs",
|
||||
"edgeuse/p3.rs",
|
||||
"face/p1.rs",
|
||||
"geometry_write.rs",
|
||||
"loopuse.rs",
|
||||
"part_file.rs",
|
||||
"shell.rs",
|
||||
"topology_traverser.rs",
|
||||
"trimming_tools.rs",
|
||||
"trimming_tools/curve_loops.rs",
|
||||
"vertex/corner.rs",
|
||||
"vertexuse.rs",
|
||||
] {
|
||||
let text = std::fs::read_to_string(src.join(rel))
|
||||
.expect("read")
|
||||
.replace("\r\n", "\n");
|
||||
let library = text
|
||||
.find("\n#[cfg(test)]\nmod ")
|
||||
.map_or(text.as_str(), |at| &text[..at]);
|
||||
for needle in [
|
||||
"SolidError::sm_err(",
|
||||
"SolidError::null_pointer(",
|
||||
"SolidError::Upstream {",
|
||||
] {
|
||||
assert!(!library.contains(needle), "{needle} in {rel}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-17
@@ -1,6 +1,5 @@
|
||||
//! The exact UV curve of a curve lying on a cylinder: the opt-in replacement
|
||||
//! ([`UpstreamFixes::cylinder_plane_section_trims_exact`](crate::kernel::UpstreamFixes::cylinder_plane_section_trims_exact),
|
||||
//! issue #2109, WR-680) of the traced fit `SmSurface::DropCurve` makes for it.
|
||||
//! The exact UV curve of a curve lying on a cylinder: the replacement (WR-680) of the
|
||||
//! traced fit `SmSurface::DropCurve` makes for it.
|
||||
//!
|
||||
//! Upstream: none. `SmSurface::DropCurve` (`source/SMLib/src/SmSurfaceDropLib.cpp`,
|
||||
//! [`Surface::drop_curve_base`]) traces every curve on a surface other than a plane and
|
||||
@@ -8,7 +7,7 @@
|
||||
//!
|
||||
//! | Rust | Upstream |
|
||||
//! |--------------------------|---------------------------------------------------------|
|
||||
//! | `exact_uv_curve` (crate) | none: the opt-in fix's exact UV curve, tried before the trace |
|
||||
//! | `exact_uv_curve` (crate) | none: the exact UV curve, tried before the trace |
|
||||
//!
|
||||
//! **The construction.** The cylinder's NURB sweeps a rational quadratic circle along
|
||||
//! its axis with degree 1. On one Bezier segment of the circle, control points `q0`,
|
||||
@@ -1037,18 +1036,14 @@ mod tests {
|
||||
ObjectKind::Curve
|
||||
}
|
||||
}
|
||||
for enabled in [false, true] {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.cylinder_plane_section_trims_exact = enabled;
|
||||
let (s, domain) = cylinder(&k);
|
||||
let curve = degree_zero_curve();
|
||||
let interval = Extent1d::from_min_max(0.0, 1.0);
|
||||
let out = s.drop_curve_base::<Obj>(&k, &domain, &curve, &interval, 1e-6, false, None);
|
||||
assert_eq!(
|
||||
out.as_ref().err().and_then(SolidError::sm_status),
|
||||
Some(crate::error::SM_ERR_NULL_POINTER),
|
||||
"exact UV enabled: {enabled}"
|
||||
);
|
||||
}
|
||||
let k = Kernel::default();
|
||||
let (s, domain) = cylinder(&k);
|
||||
let curve = degree_zero_curve();
|
||||
let interval = Extent1d::from_min_max(0.0, 1.0);
|
||||
let out = s.drop_curve_base::<Obj>(&k, &domain, &curve, &interval, 1e-6, false, None);
|
||||
assert_eq!(
|
||||
out.as_ref().err().and_then(SolidError::sm_status),
|
||||
Some(crate::error::SM_ERR_NULL_POINTER)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+35
-38
@@ -46,13 +46,12 @@
|
||||
//! - **`bDegenSurfV`** tests the domain's `XLength`, as upstream (WR-100); with
|
||||
//! `Kernel::fixes.drop_curve_v_domain_degeneracy` it tests `YLength` instead (WR-100
|
||||
//! addendum 2026-10-31).
|
||||
//! - **Exact UV curve on a cylinder (opt-in).** With
|
||||
//! `Kernel::fixes.cylinder_plane_section_trims_exact` (WR-680, #2109), a curve lying
|
||||
//! on a cylinder (a plane section) gets its exact rational UV curve in its own
|
||||
//! parameter (`surface_drop_lib::cylinder_section`) once the domain checks pass,
|
||||
//! before the start point is dropped; the outputs and the record are those of a
|
||||
//! successful drop of that one curve. A curve it does not apply to is traced as
|
||||
//! upstream.
|
||||
//! - **Exact UV curve on a cylinder.** A curve lying on a cylinder (a plane section)
|
||||
//! gets its exact rational UV curve in its own parameter
|
||||
//! (`surface_drop_lib::cylinder_section`, WR-680) once the domain checks pass, before
|
||||
//! the start point is dropped, where upstream traces and fits a cubic to it; the
|
||||
//! outputs and the record are those of a successful drop of that one curve. A curve
|
||||
//! it does not apply to is traced as upstream.
|
||||
//! - **Temporary state.** The out-of-bounds convergence upstream enables on the surface
|
||||
//! for the call (`SmTemporaryChangeValue`) runs on an evaluation copy with the flag
|
||||
//! set. The surface cache flags (`m_bPointTestEnabled` off, `m_bProcessBoundaryCurves`
|
||||
@@ -765,37 +764,35 @@ impl Surface {
|
||||
fs.singularities = u64::from(singularities);
|
||||
fs.approx_singularities = u64::from(sings.approx_singular);
|
||||
|
||||
// Opt-in (#2109, WR-680): a curve lying on a cylinder (a plane section) gets its
|
||||
// exact rational UV curve in its own parameter instead of the traced fit; one
|
||||
// that does not check out is traced below as upstream.
|
||||
if k.fixes.cylinder_plane_section_trims_exact {
|
||||
if let Some(uv_curve) =
|
||||
super::cylinder_section::exact_uv_curve(k, this, &s_uv_domain, curve, &s_interval)?
|
||||
{
|
||||
let (drop_to_surf, approx_dev) =
|
||||
uv_curve_gap(k, this, &uv_curve, curve, approx_tol_3d)?;
|
||||
out.max_drop_to_surf = drop_to_surf;
|
||||
out.max_approx_dev = approx_dev;
|
||||
out.uv_curves.push(uv_curve);
|
||||
let path = path_of(record.as_ref());
|
||||
fs.set(
|
||||
&mut record,
|
||||
4,
|
||||
SM_SUCCESS,
|
||||
path,
|
||||
&FailOut {
|
||||
rd_max_drop: out.max_drop_to_surf,
|
||||
rd_max_dev: out.max_approx_dev,
|
||||
max_drop: out.max_drop_to_surf,
|
||||
solutions: None,
|
||||
ctrl_poly: None,
|
||||
knots: None,
|
||||
uv_curves: Some(&out.uv_curves),
|
||||
},
|
||||
);
|
||||
default_path(&mut record, CreateUvTrimCurvePathType::DropCurveOkay);
|
||||
return Ok(out);
|
||||
}
|
||||
// WR-680: a curve lying on a cylinder (a plane section) gets its exact rational
|
||||
// UV curve in its own parameter instead of the traced fit; one that does not
|
||||
// check out is traced below as upstream.
|
||||
if let Some(uv_curve) =
|
||||
super::cylinder_section::exact_uv_curve(k, this, &s_uv_domain, curve, &s_interval)?
|
||||
{
|
||||
let (drop_to_surf, approx_dev) =
|
||||
uv_curve_gap(k, this, &uv_curve, curve, approx_tol_3d)?;
|
||||
out.max_drop_to_surf = drop_to_surf;
|
||||
out.max_approx_dev = approx_dev;
|
||||
out.uv_curves.push(uv_curve);
|
||||
let path = path_of(record.as_ref());
|
||||
fs.set(
|
||||
&mut record,
|
||||
4,
|
||||
SM_SUCCESS,
|
||||
path,
|
||||
&FailOut {
|
||||
rd_max_drop: out.max_drop_to_surf,
|
||||
rd_max_dev: out.max_approx_dev,
|
||||
max_drop: out.max_drop_to_surf,
|
||||
solutions: None,
|
||||
ctrl_poly: None,
|
||||
knots: None,
|
||||
uv_curves: Some(&out.uv_curves),
|
||||
},
|
||||
);
|
||||
default_path(&mut record, CreateUvTrimCurvePathType::DropCurveOkay);
|
||||
return Ok(out);
|
||||
}
|
||||
|
||||
// set sStart's Curve Start Point, 1stDeriv, and 2nd Deriv
|
||||
|
||||
+150
-19
@@ -36,7 +36,7 @@
|
||||
//! already holds.
|
||||
|
||||
use core::cell::{Cell, RefCell};
|
||||
use core::sync::atomic::{AtomicBool, Ordering};
|
||||
use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::error::SolidError;
|
||||
@@ -164,6 +164,7 @@ pub struct WorkCounters {
|
||||
wall_body_copies: Cell<u64>,
|
||||
proto_edge_intersections: Cell<u64>,
|
||||
face_extreme_control_points: Cell<u64>,
|
||||
feet_tm_coefficients: Cell<u64>,
|
||||
}
|
||||
|
||||
impl WorkCounters {
|
||||
@@ -210,6 +211,15 @@ impl WorkCounters {
|
||||
self.face_extreme_control_points.get()
|
||||
}
|
||||
|
||||
/// Series coefficients of G the G^n certificate's feet Taylor models compute
|
||||
/// (`blend::fillet_section::feet_tm::gn_enclose_tm`): each position `(sigma^i, eps^k)`
|
||||
/// of the system's series that an evaluation computes, its order-by-order solve's and
|
||||
/// its last. Not the fork's.
|
||||
#[must_use]
|
||||
pub fn feet_tm_coefficients(&self) -> u64 {
|
||||
self.feet_tm_coefficients.get()
|
||||
}
|
||||
|
||||
/// Zeroes every count (`SmWorkCounters::Reset`).
|
||||
pub fn reset(&self) {
|
||||
self.face_pairs.set(0);
|
||||
@@ -217,6 +227,7 @@ impl WorkCounters {
|
||||
self.wall_body_copies.set(0);
|
||||
self.proto_edge_intersections.set(0);
|
||||
self.face_extreme_control_points.set(0);
|
||||
self.feet_tm_coefficients.set(0);
|
||||
}
|
||||
|
||||
/// Counts one face pair examined for intersection.
|
||||
@@ -247,6 +258,12 @@ impl WorkCounters {
|
||||
self.face_extreme_control_points
|
||||
.set(self.face_extreme_control_points.get().saturating_add(n));
|
||||
}
|
||||
|
||||
/// Counts `n` series coefficients a feet Taylor model computed.
|
||||
pub(crate) fn add_feet_tm_coefficients(&self, n: u64) {
|
||||
self.feet_tm_coefficients
|
||||
.set(self.feet_tm_coefficients.get().saturating_add(n));
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits the [`UpstreamFixes`] definition it wraps unchanged, plus
|
||||
@@ -570,20 +587,6 @@ pub struct UpstreamFixes {
|
||||
pub volume_lift_third_derivative_terms: bool,
|
||||
/// `SmSurface::DropCurve` tests the drop domain's own V length for `bDegenSurfV`, not the U length again, so a surface whose natural domain is degenerate only in V reports `SM_ERR_DEGENERATE_SURFACE` (`SM_CTC_DEGENSURFACE_FAIL`) and its failure record sets `bDegenSurfV` from V; upstream copies the U test, misses the early exit and goes on to drop (WR-100, 2026-10-31 addendum). Off by default for pinned C++ parity.
|
||||
pub drop_curve_v_domain_degeneracy: bool,
|
||||
/// `SmSurface::DropCurve` gives a curve lying on a cylinder (an `SmCylinder`, or an
|
||||
/// `SmCone` of zero semi-angle) its exact UV curve: a plane section (the ellipse,
|
||||
/// circle or line a plane cuts from the cylinder, as the boolean's plane/cylinder
|
||||
/// edges and an exact round's rails are) is a rational B-spline in the cylinder's
|
||||
/// rational parameters, built in the curve's own parameter with twice its degree,
|
||||
/// where upstream traces and fits a cubic within the drop tolerance: the trims of
|
||||
/// a tilted peg's rim sat 2e-6 to 1e-5 off their edge, and the cylinder face's area
|
||||
/// and the body's volume carried that error (WR-680, #2109; see
|
||||
/// `plan/upstream-issues/WR-680--cylinder-plane-section-uv-trims-are-fits.md`). It
|
||||
/// is used only where the UV curve lifts onto the curve within round-off and stays
|
||||
/// in the domain; an iso-parameter curve keeps the drop's own exact line, and a
|
||||
/// curve that crosses the seam or is not on the cylinder is traced as upstream.
|
||||
/// Off by default for pinned C++ parity.
|
||||
pub cylinder_plane_section_trims_exact: bool,
|
||||
/// `SmTorus::AssertValid` tests its V (minor) interval, not the U interval twice, and reports its U and V overlap tests as 3 and 4 (and `SmSurfOfRevolution`'s U test as 13), the ids their labels give them, where upstream reports them as 2 and 3 (and 2) (WR-538, #482; the fork's tangency-labs/smlib@0cd9d08).
|
||||
pub torus_check_reads_v_interval: bool,
|
||||
/// `SmTorus::AssertValid` reports a spindle torus whose V interval runs through an apex (`V = ±(90 + asin(R/r))`) as test 5, "Self Intersection", where upstream clears its result with no report and tests the U interval against `90 + acos(R/r)`, failing every spindle torus with a whole turn of U (WR-539; the fork's tangency-labs/smlib@daba678).
|
||||
@@ -1389,11 +1392,39 @@ const fn cancelled_error(operation: &'static str) -> SolidError {
|
||||
/// on any clone signals every [`Kernel`] configured with [`Kernel::set_cancel_token`], so
|
||||
/// one token can also cancel a group of jobs. [`Self::either`] makes a token that also fires
|
||||
/// with another one, for work started under two (a dialog's job of a design that may close).
|
||||
///
|
||||
/// Not upstream: a token made by [`Self::after_polls`] counts the times it is polled
|
||||
/// ([`Self::is_cancelled`], [`Self::check`], and so [`Kernel::is_cancelled`] and
|
||||
/// [`Kernel::check_cancelled`]) and turns cancelled at a given poll. That stops an operation
|
||||
/// at a point fixed by its own work, not by a clock: for deterministic tests that an
|
||||
/// operation stops where it is cancelled, and for an application's progress or abort UI
|
||||
/// (SolidCraft's), which can read how far an operation got ([`Self::polls`]) or stop it after
|
||||
/// a budget of work. A token made by [`Self::new`] counts nothing and pays nothing for it.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CancelToken {
|
||||
flag: Arc<AtomicBool>,
|
||||
/// Not upstream: the token this one also fires with ([`Self::either`]).
|
||||
also: Option<Arc<CancelToken>>,
|
||||
/// Not upstream: the polls counted, and the one it turns cancelled at
|
||||
/// ([`Self::after_polls`]); `None` for a token that counts nothing.
|
||||
polls: Option<Arc<PollCount>>,
|
||||
}
|
||||
|
||||
/// The polls a [`CancelToken::after_polls`] token counts.
|
||||
#[derive(Debug)]
|
||||
struct PollCount {
|
||||
/// The poll the token is cancelled from (1: the first).
|
||||
from: u64,
|
||||
/// The polls made so far.
|
||||
made: AtomicU64,
|
||||
}
|
||||
|
||||
impl PollCount {
|
||||
/// Counts one poll: whether the token is cancelled from it on.
|
||||
fn poll(&self) -> bool {
|
||||
let made = self.made.fetch_add(1, Ordering::Relaxed).saturating_add(1);
|
||||
made >= self.from
|
||||
}
|
||||
}
|
||||
|
||||
impl CancelToken {
|
||||
@@ -1408,9 +1439,39 @@ impl CancelToken {
|
||||
self.flag.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Clears the cancellation signal (`false`) so the token can be reused.
|
||||
/// Clears the cancellation signal (`false`) so the token can be reused. A token made by
|
||||
/// [`Self::after_polls`] starts counting its polls again from 0.
|
||||
pub fn reset(&self) {
|
||||
self.flag.store(false, Ordering::Relaxed);
|
||||
if let Some(p) = &self.polls {
|
||||
p.made.store(0, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Not upstream: an uncancelled token that counts its polls and is cancelled from its
|
||||
/// `n`-th on (`n` = 0 or 1: from the first). [`Self::cancel`] cancels it at once as any
|
||||
/// token's; `after_polls(u64::MAX)` only counts. Clones share the count.
|
||||
#[must_use]
|
||||
pub fn after_polls(n: u64) -> Self {
|
||||
Self {
|
||||
polls: Some(Arc::new(PollCount {
|
||||
from: n,
|
||||
made: AtomicU64::new(0),
|
||||
})),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Not upstream: how many times this token has been polled since it was made or last
|
||||
/// [`Self::reset`], for a token made by [`Self::after_polls`]; `None` for one made by
|
||||
/// [`Self::new`], which counts nothing. A token made by [`Self::either`] counts as the
|
||||
/// token it was made from (`self`): a poll of it is a poll of `self` and of the token it
|
||||
/// was joined with, and is counted by each of them that counts.
|
||||
#[must_use]
|
||||
pub fn polls(&self) -> Option<u64> {
|
||||
self.polls
|
||||
.as_deref()
|
||||
.map(|p| p.made.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
/// Not upstream: a token that is cancelled when `self` or `other` is. Cancelling it
|
||||
@@ -1424,16 +1485,22 @@ impl CancelToken {
|
||||
Some(more) => more.either(other),
|
||||
None => other.clone(),
|
||||
})),
|
||||
// a poll of the joined token is a poll of self: counted, and cancelling, as self's
|
||||
polls: self.polls.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether [`Self::cancel`] has been called since construction or the last
|
||||
/// [`Self::reset`] (on this token, or on a token it was joined with by [`Self::either`]).
|
||||
/// [`Self::reset`] (on this token, or on a token it was joined with by [`Self::either`]),
|
||||
/// or, for a token made by [`Self::after_polls`], this is its `n`-th poll or a later one.
|
||||
/// Every call is one poll of each counting token in the chain: none is skipped because
|
||||
/// an earlier one is already cancelled.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
self.flag.load(Ordering::Relaxed)
|
||||
|| self.also.as_deref().is_some_and(CancelToken::is_cancelled)
|
||||
let counted = self.polls.as_deref().is_some_and(PollCount::poll);
|
||||
let also = self.also.as_deref().is_some_and(CancelToken::is_cancelled);
|
||||
counted || also || self.flag.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Returns `Err(SolidError::Cancelled { operation })` when [`Self::is_cancelled`] is
|
||||
@@ -1711,6 +1778,70 @@ mod tests {
|
||||
assert!(a.is_cancelled() && !b.is_cancelled() && abc.is_cancelled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn after_polls_cancels_from_its_nth_poll_and_counts_every_poll() {
|
||||
assert_eq!(CancelToken::new().polls(), None);
|
||||
let t = CancelToken::after_polls(3);
|
||||
assert_eq!(t.polls(), Some(0));
|
||||
assert!(!t.is_cancelled());
|
||||
assert_eq!(t.check("kernel::test"), Ok(()));
|
||||
assert_eq!(
|
||||
t.check("kernel::test"),
|
||||
Err(SolidError::cancelled("kernel::test"))
|
||||
);
|
||||
// cancelled from the third poll on, and every poll counted
|
||||
assert!(t.is_cancelled());
|
||||
assert_eq!(t.polls(), Some(4));
|
||||
// a clone shares the count; a kernel's polls are the token's
|
||||
let mut k = Kernel::default();
|
||||
k.set_cancel_token(t.clone());
|
||||
t.reset();
|
||||
assert_eq!(t.polls(), Some(0));
|
||||
assert!(!k.is_cancelled());
|
||||
assert_eq!(k.check_cancelled("kernel::test"), Ok(()));
|
||||
assert!(k.is_cancelled());
|
||||
assert_eq!(t.polls(), Some(3));
|
||||
// from the first poll for 0 and 1; cancel() at once
|
||||
assert!(CancelToken::after_polls(0).is_cancelled());
|
||||
assert!(CancelToken::after_polls(1).is_cancelled());
|
||||
let c = CancelToken::after_polls(u64::MAX);
|
||||
assert!(!c.is_cancelled());
|
||||
c.cancel();
|
||||
assert!(c.is_cancelled());
|
||||
assert_eq!(c.polls(), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn either_counts_a_poll_on_each_counting_token_of_its_chain() {
|
||||
let (a, b) = (
|
||||
CancelToken::after_polls(u64::MAX),
|
||||
CancelToken::after_polls(2),
|
||||
);
|
||||
let ab = a.either(&b);
|
||||
assert!(!ab.is_cancelled());
|
||||
// the joined token counts as a, and its poll is one of a and one of b
|
||||
assert_eq!(
|
||||
(ab.polls(), a.polls(), b.polls()),
|
||||
(Some(1), Some(1), Some(1))
|
||||
);
|
||||
// b's second poll cancels b and the joined token, not a
|
||||
assert!(ab.is_cancelled());
|
||||
assert!(b.is_cancelled() && !a.is_cancelled());
|
||||
assert_eq!((a.polls(), b.polls()), (Some(3), Some(3)));
|
||||
// a token that counts nothing, joined with b: b counts the poll, the joined token none
|
||||
let plain = CancelToken::new().either(&b);
|
||||
assert!(plain.is_cancelled());
|
||||
assert_eq!((plain.polls(), b.polls()), (None, Some(4)));
|
||||
// a's countdown is the joined token's too, and an earlier cancel skips no count
|
||||
let (c, d) = (
|
||||
CancelToken::after_polls(1),
|
||||
CancelToken::after_polls(u64::MAX),
|
||||
);
|
||||
let cd = c.either(&d);
|
||||
assert!(cd.is_cancelled() && cd.is_cancelled());
|
||||
assert_eq!((c.polls(), d.polls()), (Some(2), Some(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_token_round_trip() {
|
||||
let mut k = Kernel::default();
|
||||
|
||||
+1
-1
@@ -1310,7 +1310,7 @@ mod tests {
|
||||
// the step-off reaches `EvaluateBinormal`, which returns `SM_ERR_INVALID_INPUT`
|
||||
// for an edge use outside a loop (without dereferencing it)
|
||||
assert!(
|
||||
matches!(r, Err(SolidError::Upstream { code: 1007, .. })),
|
||||
r.as_ref().is_err_and(|e| e.upstream_status() == Some(1007)),
|
||||
"{r:?}"
|
||||
);
|
||||
assert_eq!(RadialSectorClass::ZeroSectorMate.code(), 7);
|
||||
|
||||
+25
-20
@@ -26,7 +26,7 @@
|
||||
//! point ([`p5::turn_to_nurbs_plain`]: the kernel's identities and callbacks).
|
||||
//! - **Null B-rep.** A null B-rep is `None` (`SM_ERR_INVALID_INPUT`).
|
||||
//! - **Statuses.** The `SM_ERR_INVALID_INPUT` and `SM_ERR_OUT_OF_MEMORY` the wrappers
|
||||
//! return themselves are [`SolidError::Upstream`] with that code.
|
||||
//! return themselves are a [`TopologyFailure`](crate::error::topology::TopologyFailure) with that code.
|
||||
//! - **Appending a part.** Upstream appends the four arrays one after the other and
|
||||
//! resets them all when an append fails. The port reserves the four first and returns
|
||||
//! `SM_ERR_OUT_OF_MEMORY` with the arrays untouched (they are empty: anything else is
|
||||
@@ -38,7 +38,8 @@
|
||||
//! - `SM_ASSERT_VALID` is not ported.
|
||||
//! - **Recursion.** None.
|
||||
|
||||
use crate::error::{Result, SolidError, SM_ERR_INVALID_INPUT, SM_ERR_OUT_OF_MEMORY};
|
||||
use crate::error::topology::TopologyFailure;
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::geom::tol::USE_DEFAULT;
|
||||
use crate::kernel::Kernel;
|
||||
use crate::topology::brep::p10::TopologyEnv;
|
||||
@@ -57,13 +58,15 @@ const OP_SLOTS: &str = "topology::api_files::add_curve_output_slots";
|
||||
/// `SM_BIG_ULONG` (`SmMath.h`): the size no `SmTArray` reaches.
|
||||
pub const BIG_ULONG: u64 = 99_999_999;
|
||||
|
||||
/// An `SmApiStatus` the wrapper returns itself.
|
||||
const fn status(operation: &'static str, code: i32) -> SolidError {
|
||||
SolidError::Upstream {
|
||||
operation,
|
||||
code,
|
||||
entity: None,
|
||||
}
|
||||
/// The wrapper's own `SM_ERR_INVALID_INPUT`: an input it is given is not usable, as the
|
||||
/// phrase says.
|
||||
fn invalid(operation: &'static str, what: &'static str) -> SolidError {
|
||||
SolidError::topology_invalid_value(operation, what)
|
||||
}
|
||||
|
||||
/// The wrapper's own `SM_ERR_OUT_OF_MEMORY`, for the data the phrase names.
|
||||
fn out_of_memory(operation: &'static str, what: &'static str) -> SolidError {
|
||||
SolidError::topology(operation, TopologyFailure::OutOfMemory(what))
|
||||
}
|
||||
|
||||
/// `bAscii ? SM_ASCII : SM_BINARY`.
|
||||
@@ -93,7 +96,7 @@ pub fn read_brep_from_file(
|
||||
env: &mut TopologyEnv<'_>,
|
||||
) -> Result<Brep> {
|
||||
let Some(data) = data else {
|
||||
return Err(status(OP_READ_BREP, SM_ERR_INVALID_INPUT));
|
||||
return Err(invalid(OP_READ_BREP, "no data is given"));
|
||||
};
|
||||
|
||||
let mut brep = p3::new_brep(k, USE_DEFAULT, USE_DEFAULT)?;
|
||||
@@ -127,19 +130,19 @@ pub fn read_part_from_file(
|
||||
env: &mut PartEnv<'_>,
|
||||
) -> Result<()> {
|
||||
let Some(data) = data else {
|
||||
return Err(status(OP_READ_PART, SM_ERR_INVALID_INPUT));
|
||||
return Err(invalid(OP_READ_PART, "no data is given"));
|
||||
};
|
||||
if !out.curves.is_empty()
|
||||
|| !out.surfaces.is_empty()
|
||||
|| !out.boolean_tree_nodes.is_empty()
|
||||
|| !out.breps.is_empty()
|
||||
{
|
||||
return Err(status(OP_READ_PART, SM_ERR_INVALID_INPUT));
|
||||
return Err(invalid(OP_READ_PART, "the part to read into is not empty"));
|
||||
}
|
||||
|
||||
let part = part_file::read_part_from_file(k, data, file_type(ascii), env)?;
|
||||
|
||||
let oom = |_| status(OP_READ_PART, SM_ERR_OUT_OF_MEMORY);
|
||||
let oom = |_| out_of_memory(OP_READ_PART, "the part's contents");
|
||||
out.curves
|
||||
.try_reserve_exact(part.curves.len())
|
||||
.map_err(oom)?;
|
||||
@@ -172,7 +175,7 @@ pub fn write_brep_to_file(
|
||||
ascii: bool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let Some(brep) = brep else {
|
||||
return Err(status(OP_WRITE_BREP, SM_ERR_INVALID_INPUT));
|
||||
return Err(invalid(OP_WRITE_BREP, "no B-rep is given"));
|
||||
};
|
||||
brep_data_write::write_brep_to_file(k, brep, env, file_type(ascii))
|
||||
}
|
||||
@@ -185,7 +188,7 @@ pub fn write_brep_to_file(
|
||||
/// `SM_ERR_INVALID_INPUT` for no B-rep; those of [`p5::turn_to_nurbs_plain`].
|
||||
pub fn turn_to_nurbs(k: &mut Kernel, brep: Option<&mut Brep>) -> Result<()> {
|
||||
let Some(brep) = brep else {
|
||||
return Err(status(OP_TURN, SM_ERR_INVALID_INPUT));
|
||||
return Err(invalid(OP_TURN, "no B-rep is given"));
|
||||
};
|
||||
p5::turn_to_nurbs_plain(k, brep)
|
||||
}
|
||||
@@ -203,12 +206,13 @@ pub fn add_curve_output_slots<T>(curves: &mut Vec<Option<T>>, additional: u64) -
|
||||
return Ok(());
|
||||
}
|
||||
if additional >= BIG_ULONG.wrapping_sub(old_size) {
|
||||
return Err(status(OP_SLOTS, SM_ERR_OUT_OF_MEMORY));
|
||||
return Err(out_of_memory(OP_SLOTS, "the curve output slots"));
|
||||
}
|
||||
let n = usize::try_from(additional).map_err(|_| status(OP_SLOTS, SM_ERR_OUT_OF_MEMORY))?;
|
||||
let n = usize::try_from(additional)
|
||||
.map_err(|_| out_of_memory(OP_SLOTS, "the curve output slots"))?;
|
||||
curves
|
||||
.try_reserve_exact(n)
|
||||
.map_err(|_| status(OP_SLOTS, SM_ERR_OUT_OF_MEMORY))?;
|
||||
.map_err(|_| out_of_memory(OP_SLOTS, "the curve output slots"))?;
|
||||
curves.resize_with(curves.len() + n, || None);
|
||||
Ok(())
|
||||
}
|
||||
@@ -216,6 +220,7 @@ pub fn add_curve_output_slots<T>(curves: &mut Vec<Option<T>>, additional: u64) -
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::{SM_ERR_INVALID_INPUT, SM_ERR_OUT_OF_MEMORY};
|
||||
use crate::geom::attribute::{AttributeStore, ObjectKey};
|
||||
use crate::geom::object::ObjectRef;
|
||||
use crate::topology::topology::NotifyRecords;
|
||||
@@ -223,8 +228,8 @@ mod tests {
|
||||
|
||||
fn code(r: &Result<impl Sized>) -> Option<i32> {
|
||||
match r {
|
||||
Err(SolidError::Upstream { code, .. }) => Some(*code),
|
||||
_ => None,
|
||||
Err(e) => e.upstream_status(),
|
||||
Ok(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+29
-20
@@ -20,7 +20,7 @@
|
||||
//! dereferences a null B-rep (`SmApiCreateSilhouetteCurves` with a usable normal) the
|
||||
//! port returns [`SolidError::InvalidInput`].
|
||||
//! - **Statuses.** The `SM_ERR_INVALID_INPUT` and `SM_ERR` the wrappers return themselves
|
||||
//! are [`SolidError::Upstream`] with that code.
|
||||
//! are a [`TopologyFailure`](crate::error::topology::TopologyFailure) with that code.
|
||||
//! - **Overflowing normal (UB-169).** A normal whose squared length overflows unitizes
|
||||
//! to a zero vector. `SmApiProjectBrepOntoPlane` then ignores the failed
|
||||
//! `MakeUnitOrthoVectors` and places its plane with the uninitialised outputs; the port
|
||||
@@ -52,7 +52,7 @@
|
||||
use crate::blend::hcr::{Hcr, HcrOptions};
|
||||
use crate::curve::crv_on_surf::CrvOnSurf;
|
||||
use crate::curve::curve::{Curve, CurveKind};
|
||||
use crate::error::{ignored, Result, SolidError, SM_ERR, SM_ERR_INVALID_INPUT};
|
||||
use crate::error::{ignored, Result, SolidError};
|
||||
use crate::geom::assert_array::labels::{EDGEUSE_TYPE, EDGE_TYPE};
|
||||
use crate::geom::attribute::ObjectKey;
|
||||
use crate::geom::axis2_placement::Axis2Placement;
|
||||
@@ -74,18 +74,17 @@ use crate::EntityRef;
|
||||
const OP: &str = "topology::api_projection";
|
||||
const OP_PROJ: &str = "topology::api_projection::plane_projection";
|
||||
|
||||
/// An `SmApiStatus` the wrapper returns itself.
|
||||
const fn status(code: i32) -> SolidError {
|
||||
SolidError::Upstream {
|
||||
operation: OP,
|
||||
code,
|
||||
entity: None,
|
||||
}
|
||||
/// The wrapper's own `SM_ERR_INVALID_INPUT`: an input it is given is not usable, as the
|
||||
/// phrase says.
|
||||
fn invalid(what: &'static str) -> SolidError {
|
||||
SolidError::topology_invalid_value(OP, what)
|
||||
}
|
||||
|
||||
/// `sNormal.Unitize() != SM_SUCCESS` returns `SM_ERR_INVALID_INPUT`.
|
||||
fn unit_normal(normal: Vec3) -> Result<Vec3> {
|
||||
normal.unitize().map_err(|_| status(SM_ERR_INVALID_INPUT))
|
||||
normal
|
||||
.unitize()
|
||||
.map_err(|_| invalid("the plane normal has zero length"))
|
||||
}
|
||||
|
||||
/// `pBrep->CalculateBoundingBox(BBox)` with its status ignored (the box then stays
|
||||
@@ -165,7 +164,7 @@ pub fn project_brep_onto_plane(
|
||||
out: &mut Vec<Option<Curve>>,
|
||||
) -> Result<()> {
|
||||
let Some(brep) = brep else {
|
||||
return Err(status(SM_ERR_INVALID_INPUT));
|
||||
return Err(invalid("no B-rep is given"));
|
||||
};
|
||||
|
||||
let plane_normal = unit_normal(plane_normal)?;
|
||||
@@ -234,8 +233,8 @@ pub fn create_silhouette_curves(
|
||||
|
||||
let plane_normal = unit_normal(plane_normal)?;
|
||||
|
||||
let brep = brep
|
||||
.ok_or_else(|| SolidError::invalid_input(OP, "SmApiCreateSilhouetteCurves: null B-rep"))?;
|
||||
let brep =
|
||||
brep.ok_or_else(|| SolidError::invalid_input(OP, "silhouette curves: null B-rep"))?;
|
||||
let (_, length) = box_and_length(k, brep)?;
|
||||
|
||||
let approx_tol = length * 1.0e-4;
|
||||
@@ -284,7 +283,7 @@ pub fn project_curve_onto_brep(
|
||||
out: &mut Vec<Option<Curve>>,
|
||||
) -> Result<()> {
|
||||
let (Some(brep), Some(curve)) = (brep, curve) else {
|
||||
return Err(status(SM_ERR_INVALID_INPUT));
|
||||
return Err(invalid("no B-rep or curve is given"));
|
||||
};
|
||||
|
||||
// The kernel resets its output array. Keep caller-owned curves out of it.
|
||||
@@ -305,7 +304,10 @@ pub fn project_curve_onto_brep(
|
||||
|
||||
// check result
|
||||
if projected.is_empty() {
|
||||
return Err(status(SM_ERR));
|
||||
return Err(SolidError::topology_failed(
|
||||
OP,
|
||||
"the projection gives no curves",
|
||||
));
|
||||
}
|
||||
|
||||
let start_size = out.len();
|
||||
@@ -344,7 +346,7 @@ pub fn project_curve_onto_surface(
|
||||
outputs: SurfaceProjection<'_>,
|
||||
) -> Result<()> {
|
||||
let (Some(curve), Some(surface)) = (curve, surface) else {
|
||||
return Err(status(SM_ERR_INVALID_INPUT));
|
||||
return Err(invalid("no curve or surface is given"));
|
||||
};
|
||||
|
||||
let interval = curve.natural_interval();
|
||||
@@ -480,7 +482,10 @@ fn plane_projection(
|
||||
| CurveKind::Offset(_)
|
||||
| CurveKind::Conic(_)
|
||||
| CurveKind::CrvInVolume(_)
|
||||
| CurveKind::Composite(_) => Err(SolidError::sm_err(OP_PROJ)),
|
||||
| CurveKind::Composite(_) => Err(SolidError::topology_failed(
|
||||
OP_PROJ,
|
||||
"the curve kind has no plane projection",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,7 +539,10 @@ fn crv_on_surf_plane_projection(
|
||||
| CurveKind::Parabola(_)
|
||||
| CurveKind::Hyperbola(_)
|
||||
) {
|
||||
return Err(SolidError::null_pointer(OP_PROJ));
|
||||
return Err(SolidError::topology_missing(
|
||||
OP_PROJ,
|
||||
"the curve on surface's UV curve, as a B-spline",
|
||||
));
|
||||
}
|
||||
|
||||
// Lift this curve to a BSplineCurve
|
||||
@@ -554,11 +562,12 @@ fn crv_on_surf_plane_projection(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::SM_ERR_INVALID_INPUT;
|
||||
|
||||
fn code(r: &Result<()>) -> Option<i32> {
|
||||
match r {
|
||||
Err(SolidError::Upstream { code, .. }) => Some(*code),
|
||||
_ => None,
|
||||
Err(e) => e.upstream_status(),
|
||||
Ok(()) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-23
@@ -47,7 +47,7 @@ use std::collections::BTreeSet;
|
||||
|
||||
use crate::curve::curve::p7::PropertyAnalysisOptions;
|
||||
use crate::curve::curve::CurvePropertyType;
|
||||
use crate::error::{ignore_status, ignored, EntityRef, Result, SolidError, SM_ERR_INVALID_INPUT};
|
||||
use crate::error::{ignore_status, ignored, EntityRef, Result, SolidError};
|
||||
use crate::geom::attribute::{ids, Attribute, Behavior};
|
||||
use crate::geom::context::{MarkLock, ALL_MARKS};
|
||||
use crate::geom::extent3d::Extent3d;
|
||||
@@ -157,7 +157,7 @@ impl PlaneCutter {
|
||||
for f in faces {
|
||||
// locals for plane/surface intersections
|
||||
let surface = face2::surface(b, f)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_MERGE))?
|
||||
.ok_or_else(|| SolidError::topology_missing(OP_MERGE, "the face's surface"))?
|
||||
.clone_for_evaluation()?;
|
||||
let uv_domain = face2::uv_domain(b, f)?;
|
||||
|
||||
@@ -236,10 +236,9 @@ impl PlaneCutter {
|
||||
let vectors = [self.plane_normal];
|
||||
let solutions = {
|
||||
let e = b.edge(wire_edge)?;
|
||||
let curve = e
|
||||
.curve
|
||||
.as_ref()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_MERGE))?;
|
||||
let curve = e.curve.as_ref().ok_or_else(|| {
|
||||
SolidError::topology_missing(OP_MERGE, "the wire edge's curve")
|
||||
})?;
|
||||
// find wire/plane intersection points
|
||||
curve
|
||||
.global_property_analysis::<NoObject>(
|
||||
@@ -260,17 +259,19 @@ impl PlaneCutter {
|
||||
// For every solution - merge points not on the cutplane into TgtBrep
|
||||
for sol in solutions.as_slice() {
|
||||
let param = sol.start.param(0)?;
|
||||
let v1 = edge::vertex(b, wire_edge)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_MERGE))?;
|
||||
let v2 = edge::other_vertex(b, wire_edge, Some(v1))?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_MERGE))?;
|
||||
let v1 = edge::vertex(b, wire_edge)?.ok_or_else(|| {
|
||||
SolidError::topology_missing(OP_MERGE, "the wire edge's first vertex")
|
||||
})?;
|
||||
let v2 = edge::other_vertex(b, wire_edge, Some(v1))?.ok_or_else(|| {
|
||||
SolidError::topology_missing(OP_MERGE, "the wire edge's second vertex")
|
||||
})?;
|
||||
|
||||
// get edge endPoints, Curve, and SolutionPoint
|
||||
let new_v_pnt = b
|
||||
.edge(wire_edge)?
|
||||
.curve
|
||||
.as_ref()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_MERGE))?
|
||||
.ok_or_else(|| SolidError::topology_missing(OP_MERGE, "the wire edge's curve"))?
|
||||
.evaluate_point(k, param)?;
|
||||
|
||||
// when solutionPoint is not close to endPoints
|
||||
@@ -282,10 +283,16 @@ impl PlaneCutter {
|
||||
// split the wire edge with the solutionPoint
|
||||
let split = p9::make_vertex_split_edge(k, b, wire_edge, param, env, curve_ref)?;
|
||||
if !split.split {
|
||||
return Err(SolidError::sm_err(OP_MERGE));
|
||||
return Err(SolidError::topology_failed(
|
||||
OP_MERGE,
|
||||
"the wire edge could not be split at the plane",
|
||||
));
|
||||
}
|
||||
let (Some([_new_e1, new_e2]), Some(new_v)) = (split.edges, split.vertex) else {
|
||||
return Err(SolidError::null_pointer(OP_MERGE));
|
||||
return Err(SolidError::topology_missing(
|
||||
OP_MERGE,
|
||||
"the split's new edge or vertex",
|
||||
));
|
||||
};
|
||||
// SM_OLDTOL_LINE pNewV->SetTolerance(SM_XSECT_TO_ZONETOL3D(dXSectTol3d))
|
||||
let zone = tol::zone_tol3d(k, tol::zone_tol3d(k, xsect_tol_3d) / 2.0);
|
||||
@@ -421,7 +428,7 @@ impl PlaneCutter {
|
||||
b.edge(e)?
|
||||
.curve
|
||||
.as_ref()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_CLASSIFY))?
|
||||
.ok_or_else(|| SolidError::topology_missing(OP_CLASSIFY, "the edge's curve"))?
|
||||
.evaluate_point(k, ivl.evaluate(t))
|
||||
};
|
||||
let mut signed_dist = (point_at(0.45678)? - self.plane_point).dot(self.plane_normal);
|
||||
@@ -484,11 +491,14 @@ impl PlaneCutter {
|
||||
}
|
||||
|
||||
// for every edgeuse on the face's upward faceuse
|
||||
let fu = face2::upward_faceuse(b, f)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_CLASSIFY))?;
|
||||
let fu = face2::upward_faceuse(b, f)?.ok_or_else(|| {
|
||||
SolidError::topology_missing(OP_CLASSIFY, "the face's upward face use")
|
||||
})?;
|
||||
for eu in eu1::faceuse_edgeuses(b, fu, false, None)? {
|
||||
// get edgeuse midPoint binormal vector
|
||||
let e = eu1::edge(b, eu)?.ok_or_else(|| SolidError::null_pointer(OP_CLASSIFY))?;
|
||||
let e = eu1::edge(b, eu)?.ok_or_else(|| {
|
||||
SolidError::topology_missing(OP_CLASSIFY, "the edge use's edge")
|
||||
})?;
|
||||
let mid = edge::interval(b, e)?.evaluate(0.5);
|
||||
let binormal =
|
||||
eu3::evaluate_binormal(k, b, eu, mid, false, &eu3::BinormalOptions::default())?
|
||||
@@ -652,7 +662,7 @@ fn do_cut_body(
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `SM_ERR_INVALID_INPUT` ([`SolidError::Upstream`]) for a normal `Unitize` rejects,
|
||||
/// `SM_ERR_INVALID_INPUT` (a [`TopologyFailure`](crate::error::topology::TopologyFailure)) for a normal `Unitize` rejects,
|
||||
/// before `b` is touched; those of [`do_cut`].
|
||||
pub fn cut_notified(
|
||||
k: &mut Kernel,
|
||||
@@ -663,11 +673,10 @@ pub fn cut_notified(
|
||||
plane_normal: Vec3,
|
||||
) -> Result<()> {
|
||||
let Ok(normal) = plane_normal.unitize() else {
|
||||
return Err(SolidError::Upstream {
|
||||
operation: OP_CUT,
|
||||
code: SM_ERR_INVALID_INPUT,
|
||||
entity: None,
|
||||
});
|
||||
return Err(SolidError::topology_invalid_value(
|
||||
OP_CUT,
|
||||
"the plane normal has zero length",
|
||||
));
|
||||
};
|
||||
let mut cutter = PlaneCutter::new(plane_point, normal);
|
||||
let tolerance = b.tolerance();
|
||||
|
||||
+7
-2
@@ -99,7 +99,7 @@ pub fn mirror_with_notifications(
|
||||
}
|
||||
|
||||
fn null() -> SolidError {
|
||||
SolidError::null_pointer(OP)
|
||||
SolidError::topology_missing(OP, "a link of the B-rep's topology")
|
||||
}
|
||||
|
||||
fn mirror_in(
|
||||
@@ -374,7 +374,12 @@ pub fn transform_matrix_with_notifications(
|
||||
let (d_diag, mut ut) = match sv.sv_decompose() {
|
||||
Ok(r) => r,
|
||||
Err(e) if e.is_unported_or_cancelled() => return Err(e),
|
||||
Err(_) => return Err(SolidError::sm_err(OP)),
|
||||
Err(_) => {
|
||||
return Err(SolidError::topology_failed(
|
||||
OP,
|
||||
"the transform matrix could not be decomposed",
|
||||
))
|
||||
}
|
||||
};
|
||||
ut.transpose()?;
|
||||
let row = |m: &Matrix, r: usize| -> Result<Vec3> {
|
||||
|
||||
+2
-7
@@ -3963,13 +3963,8 @@ mod tests {
|
||||
fail_status: SM_ERR_INVALID_INPUT,
|
||||
..DropCurveFail::default()
|
||||
}));
|
||||
assert!(matches!(
|
||||
redistribute_fixture(&mut b, edge),
|
||||
Err(SolidError::Upstream {
|
||||
code: SM_ERR_INVALID_INPUT,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(redistribute_fixture(&mut b, edge)
|
||||
.is_err_and(|e| e.upstream_status() == Some(SM_ERR_INVALID_INPUT)));
|
||||
assert_eq!(seam_sides(&b, uses), [0.0, 1.0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,10 @@ pub fn create_plane_from_bbox(
|
||||
let bilinear = BSplineSurface::create_bilinear_surface(p00, p10, p01, p11)?;
|
||||
match Plane::is_nurb_surface_plane(k, &bilinear, 1.0) {
|
||||
Ok(Some(plane)) => Ok(Some(Surface::from(plane))),
|
||||
Ok(None) => Err(SolidError::sm_err(OP_PLANE)),
|
||||
Ok(None) => Err(SolidError::topology_failed(
|
||||
OP_PLANE,
|
||||
"the bounding box's corners are not on a plane",
|
||||
)),
|
||||
Err(e) if e.is_unported_or_cancelled() => Err(e),
|
||||
// (an `SER` inside reads as `TRUE` and leaves no plane)
|
||||
Err(_) => Ok(None),
|
||||
@@ -401,7 +404,7 @@ pub fn create_planar_section_curves(
|
||||
let uv_domain = face2::uv_domain(b, f)?;
|
||||
// the face's surface, as an evaluation copy (same owner and cache slot)
|
||||
let surface = face2::surface(b, f)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_SECTION))?
|
||||
.ok_or_else(|| SolidError::topology_missing(OP_SECTION, "the face's surface"))?
|
||||
.clone_for_evaluation()?;
|
||||
let face_tol = face2::tolerance(b, f)?;
|
||||
|
||||
@@ -499,7 +502,9 @@ pub fn create_planar_section_curves(
|
||||
// check for coincidence
|
||||
let face_curve =
|
||||
status_free(edge::curve(b, e).map(Option::<&Curve>::cloned), None)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_SECTION))?;
|
||||
.ok_or_else(|| {
|
||||
SolidError::topology_missing(OP_SECTION, "the edge's curve")
|
||||
})?;
|
||||
let edge_ivl = status_free(edge::interval(b, e), Extent1d::new())?;
|
||||
if is_full_coincidence(&face_curve, &edge_ivl)? {
|
||||
coin_with_embedded = true;
|
||||
|
||||
+6
-3
@@ -113,9 +113,12 @@ pub(super) fn surface_assert_valid(
|
||||
.face(f)?
|
||||
.surface
|
||||
.as_ref()
|
||||
.ok_or(SolidError::null_pointer(
|
||||
"topology::brep::validation::assert_valid",
|
||||
))?
|
||||
.ok_or_else(|| {
|
||||
SolidError::topology_missing(
|
||||
"topology::brep::validation::assert_valid",
|
||||
"the face's surface",
|
||||
)
|
||||
})?
|
||||
.clone_for_evaluation()?;
|
||||
let mut services = BrepFaceServices::new(&mut *b);
|
||||
surface.assert_valid_with_face_context(
|
||||
|
||||
+25
-12
@@ -23,7 +23,7 @@
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use crate::error::{Result, SolidError, SM_ERR_INVALID_INPUT};
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::topology::edgeuse::p1 as eu1;
|
||||
use crate::topology::{vertex, vertexuse, Brep, EdgeId, Vertex, VertexId, VertexuseId};
|
||||
|
||||
@@ -33,14 +33,19 @@ const OP_JOIN: &str = "topology::brep::vertex_fans::join_vertex_fans";
|
||||
/// `pVertex->Remove(pVU)`.
|
||||
fn remove_vertexuse(b: &mut Brep, v: VertexId, vu: VertexuseId, op: &'static str) -> Result<()> {
|
||||
if b.vertexuse(vu)?.vertex != Some(v) {
|
||||
return Err(SolidError::sm_err(op));
|
||||
return Err(SolidError::topology_failed(
|
||||
op,
|
||||
"the vertex use is not on the vertex",
|
||||
));
|
||||
}
|
||||
let index = b
|
||||
.vertex(v)?
|
||||
.vertexuses
|
||||
.iter()
|
||||
.position(|&id| id == vu)
|
||||
.ok_or_else(|| SolidError::sm_err(op))?;
|
||||
.ok_or_else(|| {
|
||||
SolidError::topology_failed(op, "the vertex use is not in the vertex's uses")
|
||||
})?;
|
||||
b.vertex_mut(v)?.vertexuses.remove(index);
|
||||
b.vertexuse_mut(vu)?.vertex = None;
|
||||
Ok(())
|
||||
@@ -55,7 +60,10 @@ fn post_insert_vertexuse(
|
||||
) -> Result<()> {
|
||||
b.vertex(v)?;
|
||||
if b.vertexuse(vu)?.vertex.is_some() {
|
||||
return Err(SolidError::sm_err(op));
|
||||
return Err(SolidError::topology_failed(
|
||||
op,
|
||||
"the vertex use is already on a vertex",
|
||||
));
|
||||
}
|
||||
b.vertex_mut(v)?.vertexuses.push(vu);
|
||||
b.vertexuse_mut(vu)?.vertex = Some(v);
|
||||
@@ -72,11 +80,12 @@ fn post_insert_vertexuse(
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `SM_ERR` ([`SolidError::Upstream`]) when a fan walk takes more steps than the vertex
|
||||
/// `SM_ERR` (a [`TopologyFailure`](crate::error::topology::TopologyFailure)) when a fan walk takes more steps than the vertex
|
||||
/// has uses; [`SolidError::null_pointer`] for an edge use without loop neighbour,
|
||||
/// radial or edge; [`SolidError::NotFound`] for a dangling id.
|
||||
pub fn split_vertex_by_fans(b: &mut Brep, v: VertexId) -> Result<Vec<VertexId>> {
|
||||
let null = || SolidError::null_pointer(OP_SPLIT);
|
||||
let null =
|
||||
|| SolidError::topology_missing(OP_SPLIT, "an edge use's loop neighbour, radial or edge");
|
||||
let uses = vertex::vertexuses(b, v)?;
|
||||
let mut starts = Vec::new();
|
||||
for &vu in &uses {
|
||||
@@ -107,7 +116,10 @@ pub fn split_vertex_by_fans(b: &mut Brep, v: VertexId) -> Result<Vec<VertexId>>
|
||||
// every step takes two vertex uses' edges: a fan is no longer than the
|
||||
// vertex's uses
|
||||
if steps > uses.len() {
|
||||
return Err(SolidError::sm_err(OP_SPLIT));
|
||||
return Err(SolidError::topology_failed(
|
||||
OP_SPLIT,
|
||||
"a fan walk takes more steps than the vertex has uses",
|
||||
));
|
||||
}
|
||||
// the edge use before, ending at the vertex
|
||||
let incoming = eu1::cw_edgeuse(b, eu)?.ok_or_else(null)?;
|
||||
@@ -163,14 +175,15 @@ pub fn split_vertex_by_fans(b: &mut Brep, v: VertexId) -> Result<Vec<VertexId>>
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `SM_ERR_INVALID_INPUT` ([`SolidError::Upstream`]) for the same vertex twice, a vertex
|
||||
/// `SM_ERR_INVALID_INPUT` (a [`TopologyFailure`](crate::error::topology::TopologyFailure)) for the same vertex twice, a vertex
|
||||
/// not in `b`, vertices apart or sharing an edge; [`SolidError::NotFound`] for a
|
||||
/// dangling id.
|
||||
pub fn join_vertex_fans(b: &mut Brep, survivor: VertexId, other: VertexId) -> Result<()> {
|
||||
let invalid = || SolidError::Upstream {
|
||||
operation: OP_JOIN,
|
||||
code: SM_ERR_INVALID_INPUT,
|
||||
entity: None,
|
||||
let invalid = || {
|
||||
SolidError::topology_invalid_value(
|
||||
OP_JOIN,
|
||||
"the vertices are the same, apart, not in the B-rep or share an edge",
|
||||
)
|
||||
};
|
||||
if survivor == other || b.vertex(survivor).is_err() || b.vertex(other).is_err() {
|
||||
return Err(invalid());
|
||||
|
||||
+41
-25
@@ -110,7 +110,10 @@ impl<O> BrepCache<O> {
|
||||
/// Other codes return upstream's `SM_ERR`.
|
||||
pub fn new(target_type: i64) -> Result<Self> {
|
||||
if target_type != BREP_TYPE && target_type != POLY_BREP_TYPE {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(
|
||||
OP,
|
||||
"the cache is only for a B-rep or a poly B-rep",
|
||||
));
|
||||
}
|
||||
Ok(Self {
|
||||
target_type,
|
||||
@@ -195,7 +198,9 @@ impl<O: Copy + PartialEq> BrepCache<O> {
|
||||
] {
|
||||
tree.set_owner_object(Some(TreeOwner::BrepCache));
|
||||
if let Some(geom) = geom {
|
||||
let top = tree.top_node().ok_or(SolidError::null_pointer(OP))?;
|
||||
let top = tree
|
||||
.top_node()
|
||||
.ok_or_else(|| SolidError::topology_missing(OP, "the tree's top node"))?;
|
||||
tree.node_mut(top)?.geom_type = geom;
|
||||
}
|
||||
}
|
||||
@@ -241,7 +246,10 @@ impl<O: Copy + PartialEq> BrepCache<O> {
|
||||
}
|
||||
for e in brep_edges(b)? {
|
||||
let data = b.edge(e)?;
|
||||
let curve = data.curve.as_ref().ok_or(SolidError::null_pointer(OP))?;
|
||||
let curve = data
|
||||
.curve
|
||||
.as_ref()
|
||||
.ok_or_else(|| SolidError::topology_missing(OP, "the edge's curve"))?;
|
||||
if curve.owner() == Some(keys(CacheObject::Edge(e))) {
|
||||
bbox = curve
|
||||
.calculate_bounding_box(
|
||||
@@ -255,7 +263,9 @@ impl<O: Copy + PartialEq> BrepCache<O> {
|
||||
true,
|
||||
)?
|
||||
.normal
|
||||
.ok_or(SolidError::null_pointer(OP))?;
|
||||
.ok_or_else(|| {
|
||||
SolidError::topology_missing(OP, "the edge curve's bounding box")
|
||||
})?;
|
||||
bbox.expand_absolute(data.tolerance);
|
||||
curves.add_to_spatial_tree(
|
||||
&bbox,
|
||||
@@ -266,7 +276,10 @@ impl<O: Copy + PartialEq> BrepCache<O> {
|
||||
}
|
||||
for f in brep_faces(b)? {
|
||||
let data = b.face(f)?;
|
||||
let surface = data.surface.as_ref().ok_or(SolidError::null_pointer(OP))?;
|
||||
let surface = data
|
||||
.surface
|
||||
.as_ref()
|
||||
.ok_or_else(|| SolidError::topology_missing(OP, "the face's surface"))?;
|
||||
if surface.owner() == Some(keys(CacheObject::Face(f))) {
|
||||
bbox = face_bounding_box(k, b, f)?;
|
||||
bbox.expand_absolute(data.tolerance);
|
||||
@@ -315,9 +328,9 @@ impl<O: Copy + PartialEq> BrepCache<O> {
|
||||
let owner = face
|
||||
.surface
|
||||
.as_ref()
|
||||
.ok_or(SolidError::null_pointer(OP))?
|
||||
.ok_or_else(|| SolidError::topology_missing(OP, "the face's surface"))?
|
||||
.face()?
|
||||
.ok_or(SolidError::null_pointer(OP))?;
|
||||
.ok_or_else(|| SolidError::topology_missing(OP, "the surface's owning face"))?;
|
||||
let owner = face_id(b, owner, keys)?;
|
||||
match face_bounding_box(k, b, owner) {
|
||||
Ok(bbox) => surface_box = bbox,
|
||||
@@ -369,8 +382,12 @@ impl<O: Copy + PartialEq> BrepCache<O> {
|
||||
] {
|
||||
if !bbox.has_negative_volume() {
|
||||
bbox.expand_absolute(100.0 * tol);
|
||||
let tree = tree.as_mut().ok_or(SolidError::null_pointer(OP))?;
|
||||
let top = tree.top_node().ok_or(SolidError::null_pointer(OP))?;
|
||||
let tree = tree
|
||||
.as_mut()
|
||||
.ok_or_else(|| SolidError::topology_missing(OP, "the cache's tree"))?;
|
||||
let top = tree
|
||||
.top_node()
|
||||
.ok_or_else(|| SolidError::topology_missing(OP, "the tree's top node"))?;
|
||||
if !bbox.is_contained_by(&tree.node(top)?.bbox, containment_tol) {
|
||||
crate::error::ignore_status(tree.add_to_spatial_tree(
|
||||
&bbox,
|
||||
@@ -402,9 +419,9 @@ fn resize_edge(
|
||||
let owner = data
|
||||
.curve
|
||||
.as_ref()
|
||||
.ok_or(SolidError::null_pointer(OP))?
|
||||
.ok_or_else(|| SolidError::topology_missing(OP, "the edge's curve"))?
|
||||
.edge(owner_of)?
|
||||
.ok_or(SolidError::null_pointer(OP))?;
|
||||
.ok_or_else(|| SolidError::topology_missing(OP, "the curve's owning edge"))?;
|
||||
let owner = edge_id(b, owner, keys)?;
|
||||
match crate::query::edge::calculate_bounding_box(
|
||||
k,
|
||||
@@ -426,8 +443,10 @@ fn resize_edge(
|
||||
Err(_) => {}
|
||||
}
|
||||
*curve_union = curve_union.union(curve_box);
|
||||
let first = edge::vertex(b, e)?.ok_or(SolidError::null_pointer(OP))?;
|
||||
let second = edge::other_vertex(b, e, Some(first))?.ok_or(SolidError::null_pointer(OP))?;
|
||||
let first = edge::vertex(b, e)?
|
||||
.ok_or_else(|| SolidError::topology_missing(OP, "the edge's first vertex"))?;
|
||||
let second = edge::other_vertex(b, e, Some(first))?
|
||||
.ok_or_else(|| SolidError::topology_missing(OP, "the edge's second vertex"))?;
|
||||
for v in [first, second] {
|
||||
let data = b.vertex(v)?;
|
||||
if data.tolerance > *tol {
|
||||
@@ -525,7 +544,8 @@ impl crate::intersect::topology_solver::p1::TopologyObjects<CacheObject> for Bre
|
||||
if object != CacheObject::Brep {
|
||||
return Err(SolidError::port_invariant(OP, "expected B-rep handle"));
|
||||
}
|
||||
self.cache.ok_or(SolidError::null_pointer(OP))
|
||||
self.cache
|
||||
.ok_or_else(|| SolidError::topology_missing(OP, "the B-rep cache"))
|
||||
}
|
||||
fn curve(&self, object: CacheObject) -> Result<&crate::curve::curve::Curve> {
|
||||
let CacheObject::Curve(e) = object else {
|
||||
@@ -535,7 +555,7 @@ impl crate::intersect::topology_solver::p1::TopologyObjects<CacheObject> for Bre
|
||||
.edge(e)?
|
||||
.curve
|
||||
.as_ref()
|
||||
.ok_or(SolidError::null_pointer(OP))
|
||||
.ok_or_else(|| SolidError::topology_missing(OP, "the edge's curve"))
|
||||
}
|
||||
fn analytic_curve_handle(&self, object: CacheObject) -> Option<CacheObject> {
|
||||
Some(object)
|
||||
@@ -601,7 +621,7 @@ impl crate::intersect::topology_solver::p1::TopologyObjects<CacheObject> for Bre
|
||||
.face(f)?
|
||||
.surface
|
||||
.as_ref()
|
||||
.ok_or(SolidError::null_pointer(OP))?;
|
||||
.ok_or_else(|| SolidError::topology_missing(OP, "the face's surface"))?;
|
||||
Ok(CacheObject::Surface(f))
|
||||
}
|
||||
fn surface(&self, object: CacheObject) -> Result<&crate::surface::surface::Surface> {
|
||||
@@ -612,7 +632,7 @@ impl crate::intersect::topology_solver::p1::TopologyObjects<CacheObject> for Bre
|
||||
.face(f)?
|
||||
.surface
|
||||
.as_ref()
|
||||
.ok_or(SolidError::null_pointer(OP))
|
||||
.ok_or_else(|| SolidError::topology_missing(OP, "the face's surface"))
|
||||
}
|
||||
fn face_uv_domain(&self, object: CacheObject) -> Result<crate::geom::extent2d::Extent2d> {
|
||||
let CacheObject::Face(f) = object else {
|
||||
@@ -848,10 +868,7 @@ mod tests {
|
||||
#[test]
|
||||
fn initialization_contract_and_atomic_build_failure() {
|
||||
let k = Kernel::default();
|
||||
assert!(matches!(
|
||||
BrepCache::<CacheObject>::new(0),
|
||||
Err(SolidError::Upstream { code: 1001, .. })
|
||||
));
|
||||
assert!(BrepCache::<CacheObject>::new(0).is_err_and(|e| e.upstream_status() == Some(1001)));
|
||||
let mut cache = BrepCache::new(BREP_TYPE).unwrap();
|
||||
assert!(cache.vertex_tree().is_none());
|
||||
let b = Brep::new();
|
||||
@@ -866,10 +883,9 @@ mod tests {
|
||||
.add_vertex(crate::math::Point3::new(1.0, 0.0, 0.0))
|
||||
.unwrap();
|
||||
open.add_edge(v0, v1).unwrap();
|
||||
assert!(matches!(
|
||||
cache.build_trees(&k, CacheSource::Brep(&open), |o| o, &arena_identity),
|
||||
Err(SolidError::Upstream { code: 1006, .. })
|
||||
));
|
||||
assert!(cache
|
||||
.build_trees(&k, CacheSource::Brep(&open), |o| o, &arena_identity)
|
||||
.is_err_and(|e| e.upstream_status() == Some(1006)));
|
||||
assert!(
|
||||
cache.vertex_tree().is_none()
|
||||
&& cache.curve_tree().is_none()
|
||||
|
||||
+57
-25
@@ -330,7 +330,10 @@ pub(crate) fn drop_limit(
|
||||
SolverOperation::Minimize,
|
||||
)?;
|
||||
if !dropped.success {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(
|
||||
OP,
|
||||
"the limit point does not drop onto the edge's curve",
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut t = dropped.parameter;
|
||||
@@ -404,7 +407,10 @@ impl BrepConstructor {
|
||||
pub fn start_brep(&mut self, k: &mut Kernel, model_size_estimate: f64) -> Result<&mut Brep> {
|
||||
let _ = model_size_estimate; // SM_REF1: not used
|
||||
if self.brep.is_some() {
|
||||
return Err(SolidError::null_pointer(OP));
|
||||
return Err(SolidError::topology_missing(
|
||||
OP,
|
||||
"a constructor with no open B-rep (one is already open)",
|
||||
));
|
||||
}
|
||||
let b = p3::new_brep(k, USE_DEFAULT, USE_DEFAULT)?;
|
||||
Ok(self.brep.insert(b))
|
||||
@@ -425,9 +431,14 @@ impl BrepConstructor {
|
||||
|| self.face.is_some()
|
||||
|| self.loop_.is_some()
|
||||
{
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(
|
||||
OP,
|
||||
"the B-rep still has an open region, shell, face or loop",
|
||||
));
|
||||
}
|
||||
self.brep.take().ok_or(SolidError::sm_err(OP))
|
||||
self.brep
|
||||
.take()
|
||||
.ok_or_else(|| SolidError::topology_failed(OP, "no B-rep is open"))
|
||||
}
|
||||
|
||||
/// `StartRegion`: a new region (a solid) whose shells will bound it from
|
||||
@@ -444,10 +455,13 @@ impl BrepConstructor {
|
||||
let _ = is_solid_region; // not used
|
||||
// Make sure the brep is open and there are no open regions.
|
||||
if self.region.is_some() {
|
||||
return Err(SolidError::null_pointer(OP));
|
||||
return Err(SolidError::topology_missing(
|
||||
OP,
|
||||
"a constructor with no open region (one is already open)",
|
||||
));
|
||||
}
|
||||
let Some(b) = self.brep.as_mut() else {
|
||||
return Err(SolidError::null_pointer(OP));
|
||||
return Err(SolidError::topology_missing(OP, "the open B-rep"));
|
||||
};
|
||||
let r = b.add_region_record(Region::default())?;
|
||||
b.post_insert_region(r)?;
|
||||
@@ -464,7 +478,10 @@ impl BrepConstructor {
|
||||
pub fn end_region(&mut self) -> Result<()> {
|
||||
// Make sure the region is open and all contained shells are closed
|
||||
if self.region.is_none() || self.outer_shell.is_some() || self.inner_shell.is_some() {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(
|
||||
OP,
|
||||
"no region is open, or a shell is still open",
|
||||
));
|
||||
}
|
||||
self.region = None;
|
||||
self.outer_region = None;
|
||||
@@ -481,10 +498,10 @@ impl BrepConstructor {
|
||||
/// a B-rep without infinite region (upstream dereferences it).
|
||||
pub fn start_shell(&mut self) -> Result<(ShellId, Option<ShellId>)> {
|
||||
if self.outer_shell.is_some() || self.inner_shell.is_some() {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(OP, "a shell is already open"));
|
||||
}
|
||||
let Some(b) = self.brep.as_mut() else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(OP, "no B-rep is open"));
|
||||
};
|
||||
|
||||
let outer = b.add_shell_record(Shell::default())?;
|
||||
@@ -524,10 +541,10 @@ impl BrepConstructor {
|
||||
/// `SM_ERR` without open shell or while a face is open; those of the stitching.
|
||||
pub fn end_shell(&mut self, k: &mut Kernel, stitch_shell: bool) -> Result<()> {
|
||||
let Some(outer) = self.outer_shell else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(OP, "no shell is open"));
|
||||
};
|
||||
if self.face.is_some() {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(OP, "a face is still open"));
|
||||
}
|
||||
|
||||
if stitch_shell {
|
||||
@@ -570,7 +587,7 @@ impl BrepConstructor {
|
||||
/// `SM_ERR` without B-rep or open shell.
|
||||
pub fn start_face(&mut self, outside_faceuse: OrientType) -> Result<FaceId> {
|
||||
let (Some(b), Some(outer)) = (self.brep.as_mut(), self.outer_shell) else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(OP, "no B-rep or shell is open"));
|
||||
};
|
||||
|
||||
// (the skeleton shell is set when a face use joins one)
|
||||
@@ -615,7 +632,10 @@ impl BrepConstructor {
|
||||
/// `SM_ERR` without open face or while a loop is open.
|
||||
pub fn end_face(&mut self) -> Result<()> {
|
||||
if self.face.is_none() || self.loop_.is_some() {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(
|
||||
OP,
|
||||
"no face is open, or a loop is still open",
|
||||
));
|
||||
}
|
||||
self.face = None;
|
||||
Ok(())
|
||||
@@ -629,7 +649,7 @@ impl BrepConstructor {
|
||||
/// `SM_ERR` without B-rep or open face.
|
||||
pub fn start_and_end_single_vertex_loop(&mut self, point: Point3) -> Result<LoopId> {
|
||||
if self.brep.is_none() || self.face.is_none() {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(OP, "no B-rep or face is open"));
|
||||
}
|
||||
let l = self.start_loop(OrientType::Same)?; // default behavior
|
||||
let b = open(&mut self.brep, "no B-rep")?;
|
||||
@@ -691,7 +711,7 @@ impl BrepConstructor {
|
||||
/// without its face uses.
|
||||
pub fn start_loop(&mut self, orientation_to_surface: OrientType) -> Result<LoopId> {
|
||||
let (Some(b), Some(f)) = (self.brep.as_mut(), self.face) else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(OP, "no B-rep or face is open"));
|
||||
};
|
||||
|
||||
self.edge = None;
|
||||
@@ -750,7 +770,7 @@ impl BrepConstructor {
|
||||
/// null link upstream dereferences; those of `IsClosed` and `EvaluatePoint`.
|
||||
pub fn end_loop(&mut self, k: &Kernel) -> Result<()> {
|
||||
let Some(l) = self.loop_ else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(OP, "no loop is open"));
|
||||
};
|
||||
let b = open(&mut self.brep, "EndLoop: no B-rep")?;
|
||||
|
||||
@@ -856,7 +876,7 @@ impl BrepConstructor {
|
||||
let cw = eu1::cw_edgeuse(b, eu)?.ok_or_else(|| null("EndLoop: null CW link"))?;
|
||||
let cw_mate = eu1::mate(b, cw)?;
|
||||
if cw_mate != Some(mate_ccw) {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(OP, "an edge use's clockwise neighbour's mate is not its mate's counter-clockwise neighbour"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -889,7 +909,7 @@ impl BrepConstructor {
|
||||
end_vertex: Option<VertexId>,
|
||||
) -> Result<EdgeId> {
|
||||
let Some(l) = self.loop_ else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(OP, "no loop is open"));
|
||||
};
|
||||
let b = open(&mut self.brep, "StartEdge: no B-rep")?;
|
||||
let (mut start_vertex, mut end_vertex) = (start_vertex, end_vertex);
|
||||
@@ -1074,15 +1094,18 @@ impl BrepConstructor {
|
||||
/// # Errors
|
||||
///
|
||||
/// `SM_ERR` without open edge or edge curve, or when a point does not drop;
|
||||
/// `SM_ERR_INVALID_INPUT` (as [`SolidError::Upstream`] of `SetMinMax`) for a
|
||||
/// `SM_ERR_INVALID_INPUT` (as a [`TopologyFailure`](crate::error::topology::TopologyFailure) of `SetMinMax`) for a
|
||||
/// decreasing interval; those of `DropPoint` and `EvaluatePoint`.
|
||||
pub fn set_limits(&mut self, k: &Kernel, start: &VertexNode, end: &VertexNode) -> Result<()> {
|
||||
let (Some(b), Some(e)) = (self.brep.as_mut(), self.edge) else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(OP, "no B-rep or edge is open"));
|
||||
};
|
||||
let rec = b.edge(e)?;
|
||||
let Some(curve) = rec.curve.as_ref() else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(
|
||||
OP,
|
||||
"the open edge has no curve",
|
||||
));
|
||||
};
|
||||
let tol = rec.tolerance;
|
||||
// SmTol::GetZoneTol3d(m_pEdge): the edge's tolerance
|
||||
@@ -1109,7 +1132,7 @@ impl BrepConstructor {
|
||||
/// `SM_ERR` without open edge.
|
||||
pub fn end_edge(&mut self) -> Result<()> {
|
||||
if self.edge.is_none() {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(OP, "no edge is open"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1163,14 +1186,20 @@ impl BrepConstructor {
|
||||
uv_curve: bool,
|
||||
) -> Result<()> {
|
||||
let (Some(b), Some(e), Some(eu)) = (self.brep.as_mut(), self.edge, self.edgeuse) else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(
|
||||
OP,
|
||||
"no B-rep, edge or edge use is open",
|
||||
));
|
||||
};
|
||||
let f = eu1::face(b, eu)?.ok_or_else(|| null("SetEdgeCurve: edge use without face"))?;
|
||||
|
||||
if !uv_curve {
|
||||
if b.edge(e)?.curve.is_some() {
|
||||
// Don't need to set curve of this edge it has already been done.
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::topology_failed(
|
||||
OP,
|
||||
"the edge already has its curve",
|
||||
));
|
||||
}
|
||||
let mut curve = curve.ok_or_else(|| null("SetEdgeCurve: no curve"))?;
|
||||
let edge_ref = notify_ref(EntityRef::Edge(e));
|
||||
@@ -1190,7 +1219,10 @@ impl BrepConstructor {
|
||||
.filter(|c| c.is_kind_of(BSPLINE_CURVE_TYPE));
|
||||
b.edgeuse_mut(eu)?.uv_trim_curve = uv;
|
||||
let (Some(uv), Some(natural)) = (b.edgeuse(eu)?.uv_trim_curve.as_ref(), natural) else {
|
||||
return Err(SolidError::null_pointer(OP));
|
||||
return Err(SolidError::topology_missing(
|
||||
OP,
|
||||
"the edge use's B-spline trim curve or the edge's natural curve",
|
||||
));
|
||||
};
|
||||
if b.edge(e)?.curve.is_some() {
|
||||
return Ok(());
|
||||
|
||||
+18
-8
@@ -271,7 +271,7 @@ fn is_ascii(db: &DatabaseReader) -> bool {
|
||||
/// `*rDB.GetInStreamPtr()` (upstream dereferences `NULL` without a stream).
|
||||
fn stream(db: &mut DatabaseReader) -> Result<&mut InputStream> {
|
||||
db.in_stream_mut()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_STREAM))
|
||||
.ok_or_else(|| SolidError::topology_missing(OP_STREAM, "the database's input stream"))
|
||||
}
|
||||
|
||||
/// `rFileIn >> ulong`: stores only when the extraction stores.
|
||||
@@ -1811,7 +1811,12 @@ pub fn read_curve_from_db(
|
||||
db.leave_nested();
|
||||
r?
|
||||
}
|
||||
_ => return Err(SolidError::sm_err(OP_CURVE)),
|
||||
_ => {
|
||||
return Err(SolidError::topology_failed(
|
||||
OP_CURVE,
|
||||
"the curve type is not one the database holds",
|
||||
))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1826,7 +1831,9 @@ pub fn read_curve_from_db(
|
||||
operation: OP_CURVE,
|
||||
reason: "attribute index past the attribute table",
|
||||
})?;
|
||||
let h = slot.ok_or_else(|| SolidError::null_pointer(OP_CURVE))?;
|
||||
let h = slot.ok_or_else(|| {
|
||||
SolidError::topology_missing(OP_CURVE, "the attribute in the attribute table")
|
||||
})?;
|
||||
attrs.store.add_attribute(key, h)?;
|
||||
}
|
||||
}
|
||||
@@ -1960,7 +1967,12 @@ pub fn read_surface_from_db(
|
||||
SS_UNKNOWN,
|
||||
)?)
|
||||
}
|
||||
_ => return Err(SolidError::sm_err(OP_SURFACE)),
|
||||
_ => {
|
||||
return Err(SolidError::topology_failed(
|
||||
OP_SURFACE,
|
||||
"the surface type is not one the database holds",
|
||||
))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2428,10 +2440,8 @@ mod tests {
|
||||
let mut attrs = AttributeSink::new(&mut store, &mut all, 0);
|
||||
// a version 31 line curve type is SM_ERR
|
||||
let mut db = ascii_reader("Object Type 14001\n");
|
||||
assert!(matches!(
|
||||
read_curve_from_db(&k, 3, 31, &mut attrs, &mut db, true),
|
||||
Err(SolidError::Upstream { code: 1001, .. })
|
||||
));
|
||||
assert!(read_curve_from_db(&k, 3, 31, &mut attrs, &mut db, true)
|
||||
.is_err_and(|e| e.upstream_status() == Some(1001)));
|
||||
// version 31 B-spline line segment with an attribute index into an empty slot
|
||||
let text = "Object Type 14033\n1\n1\n0\n3\n0 0 0\n1 1 1\n0\n0\n1\n1\n1 0\n";
|
||||
let mut db = ascii_reader(text);
|
||||
|
||||
+10
-6
@@ -572,7 +572,8 @@ pub fn write_curve_to_db(
|
||||
} else {
|
||||
// database versions 32 and before - limited to BSplineCurves
|
||||
db.write_type(crate::curve::curve::p2::BSPLINE_CURVE_TYPE, None);
|
||||
let b = bspline_curve_part(curve).ok_or_else(|| SolidError::null_pointer(OP_CURVE))?;
|
||||
let b = bspline_curve_part(curve)
|
||||
.ok_or_else(|| SolidError::topology_missing(OP_CURVE, "the curve's B-spline part"))?;
|
||||
b.write_to_db(db, version)?;
|
||||
}
|
||||
|
||||
@@ -618,12 +619,15 @@ pub fn write_surface_to_db(
|
||||
let base = off
|
||||
.extended_base_surface()
|
||||
.unwrap_or_else(|| off.base_surface());
|
||||
let b = bspline_surface_part(base)?.ok_or_else(|| SolidError::null_pointer(OP_SURFACE))?;
|
||||
let b = bspline_surface_part(base)?.ok_or_else(|| {
|
||||
SolidError::topology_missing(OP_SURFACE, "the offset surface's B-spline base")
|
||||
})?;
|
||||
b.write_to_db(db, version)?;
|
||||
} else {
|
||||
db.write_type(B_SPLINE_SURFACE_TYPE, None);
|
||||
let b =
|
||||
bspline_surface_part(surface)?.ok_or_else(|| SolidError::null_pointer(OP_SURFACE))?;
|
||||
let b = bspline_surface_part(surface)?.ok_or_else(|| {
|
||||
SolidError::topology_missing(OP_SURFACE, "the surface's B-spline part")
|
||||
})?;
|
||||
b.write_to_db(db, version)?;
|
||||
}
|
||||
|
||||
@@ -1120,7 +1124,7 @@ fn make_uv_curve_2d(c: &mut Curve) -> Result<()> {
|
||||
let cur = b
|
||||
.nurb
|
||||
.as_mut()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_UV))?;
|
||||
.ok_or_else(|| SolidError::topology_missing(OP_UV, "the UV curve's NURBS data"))?;
|
||||
let cpts = cur.cpts_mut();
|
||||
let first_z = cpts.first().map_or(NOZ, |p| p.z);
|
||||
if first_z != NOZ && cpts.iter().all(|p| p.z == 0.0) {
|
||||
@@ -1174,7 +1178,7 @@ pub fn write_brep_to_file(
|
||||
|
||||
/// `SM_ERR_NULL_POINTER` of [`BrepData::from_brep`].
|
||||
fn null() -> SolidError {
|
||||
SolidError::null_pointer(OP_FROM)
|
||||
SolidError::topology_missing(OP_FROM, "a link of the B-rep's topology")
|
||||
}
|
||||
|
||||
/// `sm_GetMap`: the record index of `e`, `SM_NO_OBJECT` when it is not mapped (or
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user