mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
doc: a join keeps solids that only touch as separate bodies
A feature's touching parameter says what a join makes of solids that share no volume and no area: separate (the default) splits the joined body's pieces that share no face into bodies of their own, named, with history and lineage, as a cut's pieces are; one_body keeps one body of several pieces. The state records how the kept-apart pieces met (KeptSeparate: along an edge, at a point, apart) for the feature's notice. Older builds kept every join's pieces in one body: the pending format change join-touching-bodies gives each joining feature of an older design touching: one_body, so existing designs keep their bodies. kernel-cases 2896-join-arm-end-near-ring-wall-gap: the arm meets nothing, so it now stays a body of its own (body_count 2, one shell each). kernel-cases that join solids apart to make the body they test ask for it with touching: one_body: 3379 (the hub joined to the ring before the spokes reach it), 3455 (64 pegs in one body) and 3526 (a two-lump target for an intersect). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm (cherry picked from commit 2c2aaed86f4be60f7f88e40bc24b60de60085148)
This commit is contained in:
@@ -196,6 +196,32 @@ impl Operation {
|
||||
}
|
||||
}
|
||||
|
||||
/// What a join makes of solids that only touch, along an edge or at a point, or don't touch at
|
||||
/// all: they share no volume and no area, so nothing fuses them.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Touching {
|
||||
/// Each becomes a body of its own.
|
||||
#[default]
|
||||
Separate,
|
||||
/// They stay one body of several pieces.
|
||||
OneBody,
|
||||
}
|
||||
|
||||
impl Touching {
|
||||
pub fn parse(s: &str) -> Option<Touching> {
|
||||
Some(match s.to_ascii_lowercase().replace([' ', '-'], "_").as_str() {
|
||||
"separate" => Touching::Separate,
|
||||
"one_body" => Touching::OneBody,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn is_separate(&self) -> bool {
|
||||
*self == Touching::Separate
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Direction {
|
||||
@@ -2559,6 +2585,10 @@ pub struct Feature {
|
||||
/// its faces are named after them and references follow them through Update Links (#3330).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub link_faces: Vec<Vec<String>>,
|
||||
/// What a join of this feature makes of solids that only touch or don't touch: separate
|
||||
/// bodies (the default) or one body of several pieces.
|
||||
#[serde(default, skip_serializing_if = "Touching::is_separate")]
|
||||
pub touching: Touching,
|
||||
#[serde(flatten)]
|
||||
pub kind: FeatureKind,
|
||||
}
|
||||
@@ -3447,6 +3477,7 @@ impl Document {
|
||||
kernel_piece_order: false,
|
||||
inserted: false,
|
||||
link_faces: Vec::new(),
|
||||
touching: Touching::Separate,
|
||||
kind,
|
||||
};
|
||||
self.place_feature(f);
|
||||
|
||||
+195
-70
@@ -8,6 +8,7 @@ use std::hash::{Hash, Hasher};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solidcraft_geom::brep::part::Contact;
|
||||
use solidcraft_geom::tess::TessPlan;
|
||||
use solidcraft_geom::{Mesh, Plane, Region2, UnitVec3, Vec2, Vec3, regions_apart};
|
||||
use solidcraft_kernel::{self as kernel, Body, BoolOp};
|
||||
@@ -776,6 +777,11 @@ impl ModelState {
|
||||
self.fitted.get(&feature)?.iter().fold(None, |acc, r| Some(acc.map_or((*r, *r), |(lo, hi): (f64, f64)| (lo.min(*r), hi.max(*r)))))
|
||||
}
|
||||
|
||||
/// How the solids join `feature` kept as separate bodies met, when it kept any apart.
|
||||
pub fn kept_separate(&self, feature: u64) -> Option<KeptSeparate> {
|
||||
self.kept_separate.get(&feature).copied()
|
||||
}
|
||||
|
||||
/// Each edge fillet `feature` tapered (a point on it) with the radius spline it was given.
|
||||
pub fn tapered(&self, feature: u64) -> Option<&[(Vec3, kernel::Taper)]> {
|
||||
self.tapered.get(&feature).map(Vec::as_slice)
|
||||
@@ -824,6 +830,40 @@ pub struct ModelState {
|
||||
/// The radius spline of each edge a fillet tapered (a point on the edge with it, by feature
|
||||
/// id, see `FilletFit::Taper`).
|
||||
tapered: BTreeMap<u64, Vec<(Vec3, kernel::Taper)>>,
|
||||
/// How the solids a join kept as separate bodies met (by feature id), for its notice.
|
||||
kept_separate: BTreeMap<u64, KeptSeparate>,
|
||||
}
|
||||
|
||||
/// How the solids a join kept as separate bodies (`Touching::Separate`) met: the most any two
|
||||
/// of them shared, least first.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum KeptSeparate {
|
||||
/// Nothing: they don't touch.
|
||||
Apart,
|
||||
/// A point.
|
||||
AtPoint,
|
||||
/// A curve: an edge of both.
|
||||
AlongEdge,
|
||||
}
|
||||
|
||||
impl KeptSeparate {
|
||||
/// Its id in a feature's result: `apart`, `at_point` or `along_edge`.
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
KeptSeparate::Apart => "apart",
|
||||
KeptSeparate::AtPoint => "at_point",
|
||||
KeptSeparate::AlongEdge => "along_edge",
|
||||
}
|
||||
}
|
||||
|
||||
/// What a feature's result says about it.
|
||||
pub fn notice(self) -> &'static str {
|
||||
match self {
|
||||
KeptSeparate::Apart => "These bodies don't touch, so they were kept as separate bodies.",
|
||||
KeptSeparate::AtPoint => "These bodies only touch at a point, so they were kept as separate bodies.",
|
||||
KeptSeparate::AlongEdge => "These bodies only touch along an edge, so they were kept as separate bodies.",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelState {
|
||||
@@ -2935,66 +2975,16 @@ fn apply_traced_with(
|
||||
}
|
||||
match op {
|
||||
Operation::Join => {
|
||||
// Each tool joins every target it meets into one body, kept under the first one's
|
||||
// name; a tool that meets none joins the first target as a separate lump. A sheet
|
||||
// metal body keeps its own identity: only the first body met may be one.
|
||||
let mut live: Vec<String> = target_idx.iter().filter_map(|i| state.bodies.get(*i).map(|b| b.name.clone())).collect();
|
||||
// Each union keeps the lineage of what it joins, so the joined body's faces keep
|
||||
// their names (the tool's new faces are named by their roles).
|
||||
let union = |a: &Traced, b: &Traced| -> Result<Traced> {
|
||||
let u = kernel::boolean(&a.body, &b.body, BoolOp::Union)?.ok_or_else(|| DocError::Invalid("join produced nothing".into()))?;
|
||||
Ok(Traced::step(u, &[a, b]))
|
||||
};
|
||||
// One target takes every tool, whichever it meets: the result is the union of all of
|
||||
// them in one kernel call, checked once.
|
||||
if let [keep] = live.as_slice()
|
||||
&& tools.len() > 1
|
||||
{
|
||||
let Some(base) = state.body(keep).map(ModelBody::traced) else { return Ok(()) };
|
||||
let operands: Vec<&Traced> = std::iter::once(&base).chain(&tools).collect();
|
||||
let acc = union_all(&operands)?;
|
||||
if let Some(mb) = state.bodies.iter_mut().find(|b| b.name == *keep) {
|
||||
*mb = ModelBody::traced_new(mb.name.clone(), acc, mb.feature);
|
||||
let joined = join(state, tools, &target_idx)?;
|
||||
if f.touching == crate::Touching::Separate {
|
||||
let mut kept = None;
|
||||
for name in joined {
|
||||
kept = kept.max(separate_touching(state, &name)?);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
for tool in &tools {
|
||||
let mut met: Vec<String> = Vec::new();
|
||||
for name in &live {
|
||||
let Some(mb) = state.body(name) else { continue };
|
||||
let sheet = state.sheets.iter().any(|s| &s.body == name);
|
||||
if mb.body.is_mesh() || (!met.is_empty() && sheet) {
|
||||
continue;
|
||||
}
|
||||
// Each body is met where it is shown: the tool goes into its frame (#3383).
|
||||
if mb.body.meets(&tool_in(tool, tool_frame(name))?.body)? {
|
||||
met.push(name.clone());
|
||||
}
|
||||
if let Some(k) = kept {
|
||||
let most = state.kept_separate.entry(f.id).or_insert(k);
|
||||
*most = (*most).max(k);
|
||||
}
|
||||
let Some(keep) = met.first().or(live.first()).cloned() else { return Ok(()) };
|
||||
let Some(base) = state.body(&keep).map(ModelBody::traced) else { continue };
|
||||
let into_keep = tool_frame(&keep);
|
||||
let mut acc = union(&base, &tool_in(tool, into_keep)?)?;
|
||||
for name in met.iter().skip(1) {
|
||||
if let Some(b) = state.body(name) {
|
||||
// A body placed differently from the one kept joins it where it is shown.
|
||||
let from = match tool_frame(name) {
|
||||
Some(m) => crate::mat_inverse(&m),
|
||||
None => Some(crate::IDENTITY),
|
||||
};
|
||||
let m = from.map(|from| crate::mat_mul(&into_keep.unwrap_or(crate::IDENTITY), &from)).filter(|m| !crate::is_identity(m));
|
||||
acc = union(&acc, &tool_in(&b.traced(), m)?)?;
|
||||
}
|
||||
}
|
||||
let absorbed: std::collections::BTreeSet<String> = met.into_iter().skip(1).collect();
|
||||
if let Some(mb) = state.bodies.iter_mut().find(|b| b.name == keep) {
|
||||
*mb = ModelBody::traced_new(mb.name.clone(), acc, mb.feature);
|
||||
}
|
||||
state.bodies.retain(|b| !absorbed.contains(&b.name));
|
||||
for t in state.threads.iter_mut().filter(|t| absorbed.contains(&t.body)) {
|
||||
t.body = keep.clone();
|
||||
}
|
||||
live.retain(|n| !absorbed.contains(n));
|
||||
}
|
||||
}
|
||||
Operation::Cut | Operation::Intersect => {
|
||||
@@ -3071,18 +3061,7 @@ fn apply_traced_with(
|
||||
split.push(mb);
|
||||
continue;
|
||||
}
|
||||
// Each piece's faces are named after the whole's, worked out once for the whole
|
||||
// (so pieces of one face in different bodies are told apart).
|
||||
let lineages = crate::lineage::Lineage::lumps(&mb.body, mb.lineage().as_deref(), &pieces);
|
||||
for (k, (p, lineage)) in pieces.into_iter().zip(lineages).enumerate() {
|
||||
let name = if k == 0 {
|
||||
mb.name.clone()
|
||||
} else {
|
||||
(k..=k + taken.len()).map(|j| format!("{} ({j})", mb.name)).find(|n| !taken.contains(n)).unwrap_or_default()
|
||||
};
|
||||
taken.insert(name.clone());
|
||||
split.push(ModelBody::new(name, p, mb.feature).with_lineage(lineage));
|
||||
}
|
||||
split.extend(named_pieces(&mb, pieces, &mut taken));
|
||||
}
|
||||
state.bodies = split;
|
||||
}
|
||||
@@ -3091,6 +3070,152 @@ fn apply_traced_with(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Join `tools` into the bodies at `target_idx`: each tool joins every target it meets into one
|
||||
/// body, kept under the first one's name; a tool that meets none joins the first target as a
|
||||
/// separate piece. A sheet metal body keeps its own identity: only the first body met may be
|
||||
/// one. The names of the bodies the tools went into.
|
||||
fn join(state: &mut ModelState, tools: Vec<Traced>, target_idx: &[usize]) -> Result<Vec<String>> {
|
||||
let mut live: Vec<String> = target_idx.iter().filter_map(|i| state.bodies.get(*i).map(|b| b.name.clone())).collect();
|
||||
// Each union keeps the lineage of what it joins, so the joined body's faces keep
|
||||
// their names (the tool's new faces are named by their roles).
|
||||
let union = |a: &Traced, b: &Traced| -> Result<Traced> {
|
||||
let u = kernel::boolean(&a.body, &b.body, BoolOp::Union)?.ok_or_else(|| DocError::Invalid("join produced nothing".into()))?;
|
||||
Ok(Traced::step(u, &[a, b]))
|
||||
};
|
||||
// One target takes every tool, whichever it meets: the result is the union of all of
|
||||
// them in one kernel call, checked once.
|
||||
if let [keep] = live.as_slice()
|
||||
&& tools.len() > 1
|
||||
{
|
||||
let Some(base) = state.body(keep).map(ModelBody::traced) else { return Ok(Vec::new()) };
|
||||
// The tools go into the kept body's frame, where it is shown (#3383).
|
||||
let into_keep = tool_frame(keep);
|
||||
let tools = tools.iter().map(|tool| tool_in(tool, into_keep)).collect::<Result<Vec<_>>>()?;
|
||||
let operands: Vec<&Traced> = std::iter::once(&base).chain(&tools).collect();
|
||||
let acc = union_all(&operands)?;
|
||||
if let Some(mb) = state.bodies.iter_mut().find(|b| b.name == *keep) {
|
||||
*mb = ModelBody::traced_new(mb.name.clone(), acc, mb.feature);
|
||||
}
|
||||
return Ok(vec![keep.clone()]);
|
||||
}
|
||||
let mut joined = Vec::new();
|
||||
for tool in &tools {
|
||||
let mut met: Vec<String> = Vec::new();
|
||||
for name in &live {
|
||||
let Some(mb) = state.body(name) else { continue };
|
||||
let sheet = state.sheets.iter().any(|s| &s.body == name);
|
||||
if mb.body.is_mesh() || (!met.is_empty() && sheet) {
|
||||
continue;
|
||||
}
|
||||
// Each body is met where it is shown: the tool goes into its frame (#3383).
|
||||
if mb.body.meets(&tool_in(tool, tool_frame(name))?.body)? {
|
||||
met.push(name.clone());
|
||||
}
|
||||
}
|
||||
let Some(keep) = met.first().or(live.first()).cloned() else { return Ok(joined) };
|
||||
let Some(base) = state.body(&keep).map(ModelBody::traced) else { continue };
|
||||
let into_keep = tool_frame(&keep);
|
||||
let mut acc = union(&base, &tool_in(tool, into_keep)?)?;
|
||||
for name in met.iter().skip(1) {
|
||||
if let Some(b) = state.body(name) {
|
||||
// A body placed differently from the one kept joins it where it is shown.
|
||||
let from = match tool_frame(name) {
|
||||
Some(m) => crate::mat_inverse(&m),
|
||||
None => Some(crate::IDENTITY),
|
||||
};
|
||||
let m = from.map(|from| crate::mat_mul(&into_keep.unwrap_or(crate::IDENTITY), &from)).filter(|m| !crate::is_identity(m));
|
||||
acc = union(&acc, &tool_in(&b.traced(), m)?)?;
|
||||
}
|
||||
}
|
||||
let absorbed: std::collections::BTreeSet<String> = met.into_iter().skip(1).collect();
|
||||
if let Some(mb) = state.bodies.iter_mut().find(|b| b.name == keep) {
|
||||
*mb = ModelBody::traced_new(mb.name.clone(), acc, mb.feature);
|
||||
}
|
||||
state.bodies.retain(|b| !absorbed.contains(&b.name));
|
||||
for t in state.threads.iter_mut().filter(|t| absorbed.contains(&t.body)) {
|
||||
t.body = keep.clone();
|
||||
}
|
||||
live.retain(|n| !absorbed.contains(n));
|
||||
if !joined.contains(&keep) {
|
||||
joined.push(keep);
|
||||
}
|
||||
}
|
||||
Ok(joined)
|
||||
}
|
||||
|
||||
/// With `Touching::Separate`: the pieces of the joined body `name` that share no area with each
|
||||
/// other become bodies of their own, named as a cut's pieces are; pieces that share a face stay
|
||||
/// one body. How the pieces kept apart met, when it has any (a sheet metal or mesh body keeps
|
||||
/// its pieces).
|
||||
fn separate_touching(state: &mut ModelState, name: &str) -> Result<Option<KeptSeparate>> {
|
||||
let Some(i) = state.bodies.iter().position(|b| b.name == name) else { return Ok(None) };
|
||||
let Some(mb) = state.bodies.get(i).filter(|b| !b.body.is_mesh() && !state.sheets.iter().any(|s| s.body == name)) else { return Ok(None) };
|
||||
let n = mb.body.record().material_regions().len();
|
||||
if n < 2 {
|
||||
return Ok(None);
|
||||
}
|
||||
// Pieces sharing a face are one solid: each group is listed under its least piece.
|
||||
let contacts = mb.body.region_contacts()?;
|
||||
let mut root: Vec<usize> = (0..n).collect();
|
||||
let find = |root: &[usize], mut k: usize| {
|
||||
while let Some(&r) = root.get(k).filter(|&&r| r != k) {
|
||||
k = r;
|
||||
}
|
||||
k
|
||||
};
|
||||
for c in contacts.iter().filter(|c| c.contact == Contact::Face) {
|
||||
let [a, b] = c.regions.map(|k| find(&root, k));
|
||||
if let Some(slot) = root.get_mut(a.max(b)) {
|
||||
*slot = a.min(b);
|
||||
}
|
||||
}
|
||||
let mut groups: Vec<Vec<usize>> = Vec::new();
|
||||
let mut group_of: BTreeMap<usize, usize> = BTreeMap::new();
|
||||
for k in 0..n {
|
||||
let g = *group_of.entry(find(&root, k)).or_insert_with(|| {
|
||||
groups.push(Vec::new());
|
||||
groups.len() - 1
|
||||
});
|
||||
if let Some(list) = groups.get_mut(g) {
|
||||
list.push(k);
|
||||
}
|
||||
}
|
||||
if groups.len() < 2 {
|
||||
return Ok(None);
|
||||
}
|
||||
// The most any two of the separate pieces share (never a face).
|
||||
let kept = contacts
|
||||
.iter()
|
||||
.filter(|c| find(&root, c.regions[0]) != find(&root, c.regions[1]))
|
||||
.map(|c| if c.contact == Contact::Vertex { KeptSeparate::AtPoint } else { KeptSeparate::AlongEdge })
|
||||
.max()
|
||||
.unwrap_or(KeptSeparate::Apart);
|
||||
let pieces = mb.body.parts(&groups)?;
|
||||
let mut taken: std::collections::BTreeSet<String> = state.bodies.iter().map(|b| b.name.clone()).collect();
|
||||
let named = named_pieces(mb, pieces, &mut taken);
|
||||
state.bodies.splice(i..=i, named);
|
||||
Ok(Some(kept))
|
||||
}
|
||||
|
||||
/// The bodies the pieces of `mb` become: the first keeps its name, the others are numbered after
|
||||
/// it ("Bar (1)") on past the names already `taken`. Each piece's faces are named after the
|
||||
/// whole's, worked out once for the whole (so pieces of one face in different bodies are told
|
||||
/// apart).
|
||||
fn named_pieces(mb: &ModelBody, pieces: Vec<Body>, taken: &mut std::collections::BTreeSet<String>) -> Vec<ModelBody> {
|
||||
let lineages = crate::lineage::Lineage::lumps(&mb.body, mb.lineage().as_deref(), &pieces);
|
||||
let mut out = Vec::with_capacity(pieces.len());
|
||||
for (k, (p, lineage)) in pieces.into_iter().zip(lineages).enumerate() {
|
||||
let name = if k == 0 {
|
||||
mb.name.clone()
|
||||
} else {
|
||||
(k..=k + taken.len()).map(|j| format!("{} ({j})", mb.name)).find(|n| !taken.contains(n)).unwrap_or_default()
|
||||
};
|
||||
taken.insert(name.clone());
|
||||
out.push(ModelBody::new(name, p, mb.feature).with_lineage(lineage));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Distance that reaches through every target body from the sketch plane (either side).
|
||||
pub fn through_all_distance(st: &ModelState, plane: &Plane, targets: &[String]) -> Result<f64> {
|
||||
let mut far: f64 = 0.0;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//! A feature that joins saves what it makes of solids that only touch, along an edge or at a
|
||||
//! point, or don't touch at all: `touching`, `separate` (each becomes a body of its own, the
|
||||
//! default, not saved) or `one_body` (one body of several pieces). Older builds kept every join's
|
||||
//! pieces in one body, so this step gives each feature of an older design that can join
|
||||
//! `one_body`, which keeps its bodies as they were. The last release refuses a design with this
|
||||
//! change (the newer format number).
|
||||
use serde_json::Value;
|
||||
|
||||
/// Feature types that join without an `operation` saying so: a combine (whose `new` joins too),
|
||||
/// patterns and mirrors (which repeat a join), and the features that add material to a body.
|
||||
const JOINING: [&str; 8] = ["combine", "pattern", "mirror", "emboss", "rib", "boss", "snap_fit", "rest"];
|
||||
|
||||
pub fn step(v: &mut Value) -> crate::Result<()> {
|
||||
let Some(Value::Array(features)) = v.get_mut("features") else {
|
||||
return Ok(());
|
||||
};
|
||||
for feature in features {
|
||||
let joins = feature.get("type").and_then(Value::as_str).is_some_and(|t| JOINING.contains(&t))
|
||||
|| feature.get("operation").and_then(Value::as_str) == Some("join");
|
||||
if joins && let Some(f) = feature.as_object_mut() {
|
||||
f.insert("touching".into(), Value::from("one_body"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -75,11 +75,11 @@ pub use assembly::{
|
||||
pub use document::*;
|
||||
pub use eval::KeptResults;
|
||||
pub use eval::{
|
||||
EdgeRef, FeatureResult, Model, ModelBody, ModelState, PlacedBodies, RevolveExtent, SavedModel, SolvedSketch, ThreadInfo, blend_target,
|
||||
curve_pick_error, draft_corner_options, edge_names_for, edge_refs, face_point_names, face_point_state, hole_direction_at, import_body_names,
|
||||
map_wire, metric_thread_sizes, object_height, parse_metric_thread, pattern_matrices, revolve_extent, revolve_extent_with_convention,
|
||||
select_profiles, sketch_hole_direction, sketch_line_plane, state_in_frame, state_in_frame_at, suggested_thread, tangent_face_chain,
|
||||
through_all_distance, world_state, world_state_reusing,
|
||||
EdgeRef, FeatureResult, KeptSeparate, Model, ModelBody, ModelState, PlacedBodies, RevolveExtent, SavedModel, SolvedSketch, ThreadInfo,
|
||||
blend_target, curve_pick_error, draft_corner_options, edge_names_for, edge_refs, face_point_names, face_point_state, hole_direction_at,
|
||||
import_body_names, map_wire, metric_thread_sizes, object_height, parse_metric_thread, pattern_matrices, revolve_extent,
|
||||
revolve_extent_with_convention, select_profiles, sketch_hole_direction, sketch_line_plane, state_in_frame, state_in_frame_at, suggested_thread,
|
||||
tangent_face_chain, through_all_distance, world_state, world_state_reusing,
|
||||
};
|
||||
pub use expr::Expr;
|
||||
pub use names::{BodyName, FeatureId, FeatureName, SketchId, copy_name};
|
||||
|
||||
@@ -370,6 +370,8 @@ impl SavedModel {
|
||||
blend_edges: serde_json::from_value(field("blend_edges")?).map_err(invalid)?,
|
||||
fitted: serde_json::from_value(field("fitted")?).map_err(invalid)?,
|
||||
tapered: serde_json::from_value(field("tapered")?).map_err(invalid)?,
|
||||
// A join's notice is for the evaluation that made it, not kept with the cache.
|
||||
kept_separate: BTreeMap::new(),
|
||||
};
|
||||
held = held.saturating_add(st.bodies.len()).saturating_add(st.sketches.len()).saturating_add(st.sheets.len());
|
||||
if held > MAX_HELD {
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
],
|
||||
"what": "a join of a ring and an arm whose R5 convex tip stops 0.01 mm short of the ring's outer wall: nothing overlaps, so nothing fuses",
|
||||
"source": "analytic",
|
||||
"method": "ring pi*(30^2-24^2)*6 and arm 6*(350+12.5*pi) are 0.01 mm apart (arm tip at x=30.01): the join has nothing to fuse, so V is the sum of the two and the arm stays a separate shell. A combine's join of a tool that meets no target keeps it in the target as a separate lump (crates/doc eval.rs, Operation::Join; engine test join_meeting_nothing_keeps_the_first_body), and the tool body is consumed: one body (Ring) of two shells",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 8442.875567597792,
|
||||
"shells": 2
|
||||
"method": "ring pi*(30^2-24^2)*6 and arm 6*(350+12.5*pi) are 0.01 mm apart (arm tip at x=30.01): the join has nothing to fuse, so V is the sum of the two. Solids a join leaves apart (or touching only along an edge or at a point) stay separate bodies (owner decision 2026-10-22: a join's `touching` is `separate` by default; crates/doc eval.rs, separate_touching): the arm comes out of the combine as a body of its own beside Ring, the tool body consumed as before: two bodies of one shell each",
|
||||
"body_count": 2,
|
||||
"volume_mm3": 8442.875567597792
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
],
|
||||
"what": "a circular pattern (count 5) of a spoke thinner than the ring it joins (faces 0.5 mm inside the rim and hub faces)",
|
||||
"source": "analytic",
|
||||
"method": "ring pi*(66^2-63^2)*3 + hub pi*12^2*3 + 5 spokes. A spoke is the box 53x2x2.5 (x 11..64, z +-1.25, y 0.5..2.5; the issue starts it at x=12, tangent to the hub, which joins the hub by a line only, so it starts at 11 here to overlap it). It overlaps the ring by 5.021 and the hub by 4.891 (width * Int over z in -1.25..1.25 of 64 - sqrt(63^2-z^2) and of sqrt(12^2-z^2) - 11). V = ring + hub + 5*(265 - overlaps), one body",
|
||||
"method": "ring pi*(66^2-63^2)*3 + hub pi*12^2*3 + 5 spokes. A spoke is the box 53x2x2.5 (x 11..64, z +-1.25, y 0.5..2.5; the issue starts it at x=12, tangent to the hub, which joins the hub by a line only, so it starts at 11 here to overlap it). It overlaps the ring by 5.021 and the hub by 4.891 (width * Int over z in -1.25..1.25 of 64 - sqrt(63^2-z^2) and of sqrt(12^2-z^2) - 11). V = ring + hub + 5*(265 - overlaps), one body. The hub, apart from the ring when it is joined, is joined with touching: one_body, so the ring and hub stay one body for the spokes to join (a join keeps solids that share no volume as separate bodies by default, owner decision 2026-10-22).",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 6279.997172312351
|
||||
}
|
||||
|
||||
@@ -45,7 +45,8 @@
|
||||
"operation": "join",
|
||||
"targets": [
|
||||
"rim"
|
||||
]
|
||||
],
|
||||
"touching": "one_body"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
],
|
||||
"what": "one combine of 64 separate pegs that do not touch: 36 s on truck, growing 4-9x per doubling; must finish in seconds",
|
||||
"source": "analytic",
|
||||
"method": "64 non-touching 48-gon pegs of circumradius 2, 1 mm tall, 6 mm apart: each area 0.5*48*2^2*sin(2pi/48) = 12.5305, V = 64 * 12.5305 * 1. The join keeps them in one body (several lumps). A hang is a failure: time should grow with the number of bodies. One body with 64 shells",
|
||||
"method": "64 non-touching 48-gon pegs of circumradius 2, 1 mm tall, 6 mm apart: each area 0.5*48*2^2*sin(2pi/48) = 12.5305, V = 64 * 12.5305 * 1. The join keeps them in one body (several lumps). A hang is a failure: time should grow with the number of bodies. One body with 64 shells. The combine joins with touching: one_body, which keeps the pegs one body (by default a join keeps solids that share no volume as separate bodies, owner decision 2026-10-22).",
|
||||
"body_count": 1,
|
||||
"shells": 64,
|
||||
"volume_mm3": 801.9529
|
||||
|
||||
@@ -12889,7 +12889,8 @@
|
||||
"Peg (63)",
|
||||
"Peg (64)"
|
||||
],
|
||||
"operation": "join"
|
||||
"operation": "join",
|
||||
"touching": "one_body"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
],
|
||||
"what": "intersect of a two-lump target (two spheres joined while apart) with a box holding the bottom half of each",
|
||||
"source": "analytic",
|
||||
"method": "box x -10..40, y -10..10, z -10..0 holds the z<=0 half of each r=4 sphere (centres (0,0,0), (30,0,0)): V = 2 * (2/3) pi 4^3 = 268.083. The body must not be removed. One body with two shells",
|
||||
"method": "box x -10..40, y -10..10, z -10..0 holds the z<=0 half of each r=4 sphere (centres (0,0,0), (30,0,0)): V = 2 * (2/3) pi 4^3 = 268.083. The body must not be removed. One body with two shells. The second sphere is joined with touching: one_body, which makes the two-lump target (a join keeps solids that share no volume as separate bodies by default, owner decision 2026-10-22).",
|
||||
"body_count": 1,
|
||||
"shells": 2,
|
||||
"volume_mm3": 268.0826
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
"operation": "join",
|
||||
"targets": [
|
||||
"Balls"
|
||||
]
|
||||
],
|
||||
"touching": "one_body"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user