After the merge with main: its new code on develop's exact bounds, types and thread fields

- The sheet cut's cull (#10301) and the tapped wall's (#10227) use each
  body's exact box (`exact_box`), not its display mesh.
- model.threads replies with develop's thread fields: form, hand (right,
  left or both) and starts.
- surface.boundary's schema checks a network curve's continuity as the
  handler does, and words a list entry's error as it does.
- Main's new tests take develop's Hole fields and world_state's Result.
- mesh-decisions: contact_sweep's triangle check is now `depth` (main
  split `hits`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
This commit is contained in:
jyviko
2026-11-02 16:19:46 -05:00
parent 98a6f591eb
commit 5f4d700610
12 changed files with 79 additions and 29 deletions
+3 -3
View File
@@ -3058,9 +3058,9 @@ fn apply_traced_with(
if let Some(mb) = state.bodies.get(i).filter(|b| state.sheets.iter().any(|s| s.body == b.name)) {
let m = tool_frame(&mb.name);
if op == Operation::Cut && m.is_none() && !mb.body.is_mesh() && !tools.iter().any(|t| t.body.is_mesh()) {
let reach = reach.get_or_insert_with(|| tools.iter().map(|t| more::body_box(&t.body)).collect());
let reach = reach.get_or_insert_with(|| tools.iter().map(|t| more::exact_box(&t.body)).collect());
if !reach.is_empty()
&& let Some(target) = more::mesh_box(mb)
&& let Some(target) = more::exact_box(&mb.body)
&& reach.iter().all(|tool| more::boxes_apart(&Some(target), tool))
{
continue;
@@ -4348,7 +4348,7 @@ fn parallel_tapped_wall(st: &ModelState, entry: Vec3, inward: Vec3, radius: f64)
// (and the slant its axis may have off `inward` over the body's distance), and its
// faces lie in the body's exact box: a body whose box that line misses by more holds
// none, and its faces are not read one by one (#10227).
let far = more::exact_box(body).map_or(f64::INFINITY, |b| (b.min - entry).len().max((b.max - entry).len()) + b.diagonal());
let far = more::exact_box(&body.body).map_or(f64::INFINITY, |b| (b.min - entry).len().max((b.max - entry).len()) + b.diagonal());
if far.is_finite() && more::line_beyond_reach(body, entry, inward, radius * 1.01 + tol + 1e-3 + 2e-4 * far) {
continue;
}
+3 -1
View File
@@ -1419,7 +1419,9 @@ fn a_tapped_hole_reads_the_bodies_along_its_drill_line() {
points: None,
thread: Some("M6".into()),
thread_depth: None,
handedness: ThreadHandedness::Right,
hand: crate::thread_forms::ThreadHand::Right,
modeled: false,
ease_out: None,
to: None,
references: Vec::new(),
};
+6 -5
View File
@@ -1743,17 +1743,18 @@ fn sheet_prism_cut(
Ok(handled)
}
/// The exact box of `mb`, grown by twice its tolerance. None when it can't be worked out.
pub(super) fn exact_box(mb: &ModelBody) -> Option<Aabb3> {
let b = mb.body.exact_bounds().ok()?;
(!b.is_empty() && b.min.is_finite() && b.max.is_finite()).then(|| b.inflated(2.0 * mb.body.record().tolerance + 1e-6))
/// The exact box of `body`, grown by twice its tolerance: bodies whose boxes are apart do not
/// meet. None when it can't be worked out.
pub(super) fn exact_box(body: &Body) -> Option<Aabb3> {
let b = body.exact_bounds().ok()?;
(!b.is_empty() && b.min.is_finite() && b.max.is_finite()).then(|| b.inflated(2.0 * body.record().tolerance + 1e-6))
}
/// Whether the line through `at` along `dir` is known to pass farther than `reach` from the
/// exact box of `mb` ([`exact_box`]) everywhere, and so from every point of the body. Unknown is
/// never.
pub(super) fn line_beyond_reach(mb: &ModelBody, at: Vec3, dir: Vec3, reach: f64) -> bool {
let Some(b) = exact_box(mb) else { return false };
let Some(b) = exact_box(&mb.body) else { return false };
let b = b.inflated(reach);
let (mut lo, mut hi) = (f64::NEG_INFINITY, f64::INFINITY);
for (a, d, min, max) in [(at.x, dir.x, b.min.x, b.max.x), (at.y, dir.y, b.min.y, b.max.y), (at.z, dir.z, b.min.z, b.max.z)] {
+4 -1
View File
@@ -160,7 +160,10 @@ fn curvature_comb(s: &mut Session, p: &Value) -> Result<Value> {
if bool_(p, "clear").unwrap_or(false) {
s.comb = None;
s.revision += 1;
return reply(cmd, CurvatureCombReply::Cleared(AnalysisCleared { analysis: "curvature_comb".into(), cleared: true, param_warnings: Vec::new() }));
return reply(
cmd,
CurvatureCombReply::Cleared(AnalysisCleared { analysis: "curvature_comb".into(), cleared: true, param_warnings: Vec::new() }),
);
}
let add = bool_(p, "add").unwrap_or(false);
let shown = if add { s.comb.clone() } else { None };
@@ -1,8 +1,8 @@
use serde_json::{Value, json};
use super::{
CrashReport, CrashReportList, CrashReportParams, CrashReportsClearParams, CrashReportsCleared, CrashReportsParams, CrashTestMode, CrashTestParams,
ProfileParams, ValidationParams,
CrashReport, CrashReportList, CrashReportParams, CrashReportsClearParams, CrashReportsCleared, CrashReportsParams, CrashTestMode,
CrashTestParams, ProfileParams, ValidationParams,
};
use crate::Session;
use crate::schema::tests::{assert_keys, assert_strict_keeps_todays_errors, conforms};
+43 -5
View File
@@ -160,8 +160,12 @@ pub struct ModelThread {
pub pitch: f64,
/// Whether it is cut into a hole rather than on a shaft.
pub internal: bool,
/// Its form.
pub form: ThreadForm,
/// Which way it turns.
pub handedness: ThreadHand,
pub hand: ThreadHand,
/// How many starts it has.
pub starts: u32,
/// A point on the axis, which `start` and `end` are measured from.
pub axis_point: [f64; 3],
/// The axis direction, a unit vector.
@@ -174,14 +178,34 @@ pub struct ModelThread {
/// Which way a thread turns.
#[derive(Clone, Copy, Debug, Serialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
#[serde(rename_all = "snake_case")]
pub enum ThreadHand {
Right,
Left,
/// A right- and a left-handed groove crossing on one face (a reversing screw).
Both,
}
/// A thread form: ISO metric 60°, metric trapezoidal 30°, Acme 29°, metric buttress 3°/30°,
/// buttress inch 7°/45°, square, and the 55° and 60° pipe threads.
#[derive(Clone, Copy, Debug, Serialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ThreadForm {
Metric,
Trapezoidal,
Acme,
Buttress,
ButtressAsme,
Square,
Bspp,
Rp,
Bspt,
Npt,
}
impl From<&solidcraft_doc::ThreadInfo> for ModelThread {
fn from(t: &solidcraft_doc::ThreadInfo) -> Self {
use solidcraft_doc::thread_forms::{ThreadForm as Form, ThreadHand as Hand};
ModelThread {
feature: t.feature,
body: t.body.clone(),
@@ -190,10 +214,24 @@ impl From<&solidcraft_doc::ThreadInfo> for ModelThread {
radius: t.radius,
pitch: t.pitch,
internal: t.internal,
handedness: match t.handedness {
solidcraft_doc::ThreadHandedness::Right => ThreadHand::Right,
solidcraft_doc::ThreadHandedness::Left => ThreadHand::Left,
form: match t.form {
Form::Metric => ThreadForm::Metric,
Form::Trapezoidal => ThreadForm::Trapezoidal,
Form::Acme => ThreadForm::Acme,
Form::Buttress => ThreadForm::Buttress,
Form::ButtressAsme => ThreadForm::ButtressAsme,
Form::Square => ThreadForm::Square,
Form::Bspp => ThreadForm::Bspp,
Form::Rp => ThreadForm::Rp,
Form::Bspt => ThreadForm::Bspt,
Form::Npt => ThreadForm::Npt,
},
hand: match t.hand {
Hand::Right => ThreadHand::Right,
Hand::Left => ThreadHand::Left,
Hand::Both => ThreadHand::Both,
},
starts: t.starts,
axis_point: t.axis_point.into(),
axis: solidcraft_geom::Vec3::from(t.axis).into(),
start: t.start,
+1 -1
View File
@@ -59,7 +59,7 @@ fn model_lists_reply_with_their_schemas() {
assert!(r["faces"].as_array().into_iter().flatten().any(|f| f["sheet_face"].as_str().is_some_and(|n| n.starts_with("sheet:"))), "{r}");
let r = run(&mut s, "model.threads", json!({}));
matches(&threads, &r, false);
assert_eq!((r["threads"][0]["designation"].as_str(), r["threads"][0]["handedness"].as_str()), (Some("M8"), Some("right")), "{r}");
assert_eq!((r["threads"][0]["designation"].as_str(), r["threads"][0]["hand"].as_str()), (Some("M8"), Some("right")), "{r}");
matches(&edges, &run(&mut s, "model.edges", json!({"body": "Shaft", "bdy": 1})), true);
matches(&faces, &run(&mut s, "model.faces", json!({"body": "Shaft", "bdy": 1})), true);
+5 -1
View File
@@ -122,7 +122,11 @@ fn flag<'de, D: Deserializer<'de>>(d: D) -> Result<Option<bool>, D::Error> {
fn multiplier<'de, D: Deserializer<'de>>(d: D) -> Result<Option<f64>, D::Error> {
match Value::deserialize(d)? {
Value::Null => Ok(None),
v => v.as_f64().filter(|x| (0.01..=100.0).contains(x)).map(Some).ok_or_else(|| D::Error::custom(whole("`length` must be a multiplier 0.01…100"))),
v => v
.as_f64()
.filter(|x| (0.01..=100.0).contains(x))
.map(Some)
.ok_or_else(|| D::Error::custom(whole("`length` must be a multiplier 0.01…100"))),
}
}
@@ -1,13 +1,18 @@
use serde_json::{Value, json};
use super::{CenterOfMassParams, CentersOfMass, CurvatureCombParams, CurvatureCombReply, IsocurveParams, IsocurveReply, MinimumRadii, MinimumRadiusParams};
use super::{
CenterOfMassParams, CentersOfMass, CurvatureCombParams, CurvatureCombReply, IsocurveParams, IsocurveReply, MinimumRadii, MinimumRadiusParams,
};
use crate::Session;
use crate::schema::tests::{assert_keys, assert_strict_keeps_todays_errors, conforms};
use crate::schema::{ParamCheck, input_schema_for, output_schema_for};
#[test]
fn schemas_and_the_key_check_agree() {
assert_keys(input_schema_for::<CurvatureCombParams>(), &["add", "clear", "count", "curves", "density", "edges", "faces", "length", "scale", "sketch"]);
assert_keys(
input_schema_for::<CurvatureCombParams>(),
&["add", "clear", "count", "curves", "density", "edges", "faces", "length", "scale", "sketch"],
);
assert_keys(input_schema_for::<MinimumRadiusParams>(), &["curves", "edges", "faces", "sketch"]);
assert_keys(input_schema_for::<IsocurveParams>(), &["add", "clear", "count", "faces"]);
assert_keys(input_schema_for::<CenterOfMassParams>(), &["bodies"]);
+2 -5
View File
@@ -131,7 +131,7 @@ impl<'de> Deserialize<'de> for BoundaryContinuity {
.map(|x| x.as_str().and_then(Continuity::parse))
.collect::<Option<Vec<_>>>()
.map(BoundaryContinuity)
.ok_or_else(|| D::Error::custom(whole("`continuity` entries must be g0, g1, g2 or g3"))),
.ok_or_else(|| D::Error::custom(whole("a `continuity` entry must be g0, g1, g2 or g3"))),
_ => Err(D::Error::custom(whole("`continuity` must be one word, or one per curve"))),
}
}
@@ -191,10 +191,7 @@ fn network(key: &str, v: Value) -> Result<(), String> {
curves = params::string_list(CMD, item, "curves").map_err(message)?.len();
}
if let Some(g) = o.get("continuity").filter(|v| !v.is_null()) {
let g = g.as_str().and_then(Continuity::parse).ok_or_else(|| format!("{name}.continuity must be g0, g1 or g2"))?;
if g == Continuity::G3 {
return Err(format!("not supported yet: G3 on {name} (a curve network holds up to g2)"));
}
let g = params::continuity(CMD, Some(g), &format!("{name}.continuity"), solidcraft_kernel::Construction::Network).map_err(message)?;
if g != Continuity::G0 && !on_body {
return Err(format!("{name}: g1 and g2 apply to a curve on a body's edges (a surface's open edges)"));
}
+2 -2
View File
@@ -459,7 +459,7 @@ fn a_line_starts_on_a_curve_or_midpoint_and_keeps_that_snap_after_undo() {
assert!((actual[0] - expected[0]).abs() < 1e-5 && (actual[1] - expected[1]).abs() < 1e-5, "{actual:?}, expected {expected:?}");
}
let sid = h.app.session.active_sketch.unwrap();
let state = h.app.session.world_state();
let state = h.app.session.world_state().unwrap();
let sketch = &state.sketch(sid).unwrap().sketch;
let guide = sketch.curve_index("guide").unwrap();
assert!(
@@ -494,7 +494,7 @@ fn a_line_starts_at_a_curve_crossing_and_keeps_it_after_undo() {
let drawn = lines(&h);
assert_eq!(drawn.len(), 3, "{:?}", h.app.status);
assert_line(drawn[2], [-20.0, 10.0], end);
let state = h.app.session.world_state();
let state = h.app.session.world_state().unwrap();
let sketch = &state.sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch;
assert_eq!(
sketch.constraints.iter().filter(|c| matches!(c.kind, solidcraft_engine::sketch::ConstraintKind::PointOnCurve { .. })).count(),
+1 -1
View File
@@ -107,9 +107,9 @@ crates/engine/src/cmd/compare.rs:enclosed_volume measures a B-rep body's volume
crates/engine/src/cmd/component_clone.rs:build places pasted bodies by meshes
crates/engine/src/cmd/component_clone.rs:mesh_distance compares pasted bodies by mesh triangles
crates/engine/src/cmd/component_properties.rs:measure_placed measures a placed component by its display mesh
crates/engine/src/cmd/contact_sweep.rs:ContactGeometry::depth finds bodies passing through each other by mesh triangles
crates/engine/src/cmd/contact_sweep.rs:ContactGeometry::gap measures gaps by mesh triangles
crates/engine/src/cmd/contact_sweep.rs:ContactGeometry::gaps measures gaps by mesh triangles
crates/engine/src/cmd/contact_sweep.rs:ContactGeometry::hits finds bodies passing through each other by mesh triangles
crates/engine/src/cmd/contact_sweep.rs:ContactGeometry::placed places meshes for contact checks
crates/engine/src/cmd/contact_sweep.rs:contact_mesh tessellates bodies for contact checks
crates/engine/src/cmd/edit.rs:selection_item keeps a selection's face or edge index through the display mesh