Persistent naming, step 2: revolved faces named by the section curve they sweep and their angle caps, box faces by side (+x … -z), cylinders by top/bottom/side, and pattern and mirror copies after their sources through the instance transforms (<source>@F<pattern>.<instance>)
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
72297047d5
commit
452ee5f9f9
3 files changed
+169
-23
No files matched your search
+12
-4
@@ -218,10 +218,10 @@ impl ModelState {
|
||||
|
||||
/// After a feature evaluated to this state from `before` with cache key `key`: bodies it
|
||||
/// made or changed, and sketches it made, take that key; the rest keep theirs.
|
||||
fn stamp(&mut self, before: &Arc<ModelState>, f: &Feature, key: u64) {
|
||||
fn stamp(&mut self, before: &Arc<ModelState>, f: &Feature, key: u64, mats: Vec<Mat>) {
|
||||
let feature = f.id;
|
||||
let depth = before.bodies.iter().filter_map(|b| b.names.origin.as_ref().map(|o| o.depth)).max().unwrap_or(0) + 1;
|
||||
let origin = Arc::new(crate::naming::NamingOrigin { feature: f.clone(), before: before.clone(), depth });
|
||||
let origin = Arc::new(crate::naming::NamingOrigin { feature: f.clone(), before: before.clone(), depth, mats });
|
||||
for b in &mut self.bodies {
|
||||
let kept = before.bodies.iter().find(|x| x.name == b.name && x.body.same(&b.body));
|
||||
match kept {
|
||||
@@ -528,7 +528,15 @@ impl Model {
|
||||
let ms = t0.elapsed().as_secs_f64() * 1000.0;
|
||||
let error = match r {
|
||||
Ok(()) => {
|
||||
next.stamp(&state, f, key);
|
||||
// Copies are named after their sources through the instance transforms.
|
||||
let mats = match &f.kind {
|
||||
FeatureKind::Pattern { pattern, .. } => pattern_transforms(&vals, &state, pattern).unwrap_or_default(),
|
||||
FeatureKind::Mirror { plane, .. } => {
|
||||
doc.resolve_plane(&vals, plane, 0).map(|pl| vec![mirror_matrix(&pl)]).unwrap_or_default()
|
||||
}
|
||||
_ => Vec::new(),
|
||||
};
|
||||
next.stamp(&state, f, key, mats);
|
||||
state = Arc::new(next);
|
||||
None
|
||||
}
|
||||
@@ -1007,7 +1015,7 @@ fn blend_target(state: &ModelState, body: &Option<String>, edges: &[Vec3]) -> Re
|
||||
})
|
||||
}
|
||||
|
||||
fn revolve_axis(ss: &SolvedSketch, axis: &AxisRef) -> Result<(Vec2, Vec2)> {
|
||||
pub(crate) fn revolve_axis(ss: &SolvedSketch, axis: &AxisRef) -> Result<(Vec2, Vec2)> {
|
||||
match axis {
|
||||
AxisRef::SketchLine { curve } => {
|
||||
let ci = ss.sketch.curve_index(curve).ok_or_else(|| DocError::Unknown(format!("sketch line `{curve}`")))?;
|
||||
|
||||
+127
-19
@@ -19,6 +19,8 @@ pub struct NamingOrigin {
|
||||
pub feature: Feature,
|
||||
pub before: Arc<ModelState>,
|
||||
pub depth: usize,
|
||||
/// Instance transforms of a pattern or mirror (its copies are named after their sources).
|
||||
pub mats: Vec<crate::Mat>,
|
||||
}
|
||||
|
||||
/// The lazily computed names of a body, shared by its clones.
|
||||
@@ -193,32 +195,70 @@ fn derive(b: &ModelBody, o: &NamingOrigin, nf: usize) -> Vec<String> {
|
||||
.collect();
|
||||
let fid = o.feature.id;
|
||||
let ns = normals(&m, nf);
|
||||
let mut names: Vec<String> = Vec::with_capacity(nf);
|
||||
for (fi, ps) in pts.iter().enumerate() {
|
||||
// Vote: the input face most samples lie on, if it holds at least half of them.
|
||||
let mut votes: Vec<(String, usize)> = Vec::new();
|
||||
for p in ps {
|
||||
for (_, idx, nm) in &inputs {
|
||||
if let Some(f) = idx.face_at(*p, tol)
|
||||
&& let Some(n) = nm.get(f)
|
||||
{
|
||||
match votes.iter_mut().find(|(v, _)| v == n) {
|
||||
Some(v) => v.1 += 1,
|
||||
None => votes.push((n.clone(), 1)),
|
||||
}
|
||||
break;
|
||||
}
|
||||
let mut names: Vec<Option<String>> = pts.iter().map(|ps| vote(ps, &inputs, tol, None)).collect();
|
||||
// A pattern or mirror copy: its faces lie on its source's faces moved by one instance.
|
||||
let found = names.iter().filter(|n| n.is_some()).count();
|
||||
if !o.mats.is_empty() && found * 2 < nf.max(1) {
|
||||
let all: Vec<(&ModelBody, FaceIndex, Arc<Vec<String>>)> = o
|
||||
.before
|
||||
.bodies
|
||||
.iter()
|
||||
.filter(|x| !x.body.is_mesh())
|
||||
.take(64)
|
||||
.map(|x| (x, FaceIndex::new(&x.mesh(), face_count(x)), face_names(x)))
|
||||
.collect();
|
||||
let mut best: Option<(usize, usize, Vec<Option<String>>)> = None;
|
||||
for (k, mat) in o.mats.iter().enumerate() {
|
||||
let Some(inv) = crate::mat_inverse(mat) else { continue };
|
||||
let got: Vec<Option<String>> = pts.iter().map(|ps| vote(ps, &all, tol, Some(&inv))).collect();
|
||||
let n = got.iter().filter(|x| x.is_some()).count();
|
||||
if n > found && best.as_ref().is_none_or(|(bn, _, _)| n > *bn) {
|
||||
best = Some((n, k, got));
|
||||
}
|
||||
}
|
||||
votes.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
|
||||
let inherited = votes.first().filter(|(_, n)| *n * 2 >= ps.len().max(1)).map(|(n, _)| strip_piece(n).to_string());
|
||||
let empty = Vec::new();
|
||||
names.push(inherited.unwrap_or_else(|| role_name(&o.feature, &o.before, ps, ns.get(fi).unwrap_or(&empty), fi, fid)));
|
||||
if let Some((_, k, got)) = best {
|
||||
names = got.into_iter().map(|n| n.map(|n| format!("{n}@F{fid}.{}", k + 1))).collect();
|
||||
}
|
||||
}
|
||||
let empty = Vec::new();
|
||||
let mut names: Vec<String> = names
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(fi, n)| {
|
||||
n.unwrap_or_else(|| {
|
||||
role_name(&o.feature, &o.before, pts.get(fi).map(Vec::as_slice).unwrap_or(&[]), ns.get(fi).unwrap_or(&empty), fi, fid)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
number_pieces(&mut names, &pts, '#');
|
||||
names
|
||||
}
|
||||
|
||||
/// The input face most of a face's sample points lie on (mapped by `map` first), if it holds
|
||||
/// at least half of them.
|
||||
fn vote(ps: &[Vec3], inputs: &[(&ModelBody, FaceIndex, Arc<Vec<String>>)], tol: f64, map: Option<&crate::Mat>) -> Option<String> {
|
||||
let mut votes: Vec<(String, usize)> = Vec::new();
|
||||
for p in ps {
|
||||
let p = match map {
|
||||
Some(m) => crate::apply_point(m, *p),
|
||||
None => *p,
|
||||
};
|
||||
for (_, idx, nm) in inputs {
|
||||
if let Some(f) = idx.face_at(p, tol)
|
||||
&& let Some(n) = nm.get(f)
|
||||
{
|
||||
match votes.iter_mut().find(|(v, _)| v == n) {
|
||||
Some(v) => v.1 += 1,
|
||||
None => votes.push((n.clone(), 1)),
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
votes.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
|
||||
votes.first().filter(|(_, n)| *n * 2 >= ps.len().max(1)).map(|(n, _)| strip_piece(n).to_string())
|
||||
}
|
||||
|
||||
/// A face name without its piece number (`#k`).
|
||||
pub fn strip_piece(n: &str) -> &str {
|
||||
strip_mark(n, '#')
|
||||
@@ -317,6 +357,57 @@ fn role_name(f: &Feature, before: &ModelState, ps: &[Vec3], ns: &[Vec3], fi: usi
|
||||
let q = ss.plane.to_local(c);
|
||||
nearest_curve(ss, Vec2::new(q.x, q.y)).map(|id| format!("F{fid}:side:{id}")).unwrap_or_else(|| format!("F{fid}:face:{fi}"))
|
||||
}
|
||||
FeatureKind::Revolve { sketch, axis, .. } => {
|
||||
let Some(ss) = before.sketch(*sketch) else { return format!("F{fid}:face:{fi}") };
|
||||
let Ok((o2, d2)) = crate::eval::revolve_axis(ss, axis) else { return format!("F{fid}:face:{fi}") };
|
||||
let (o, d) = (ss.plane.to_world(o2), (ss.plane.to_world(o2 + d2) - ss.plane.to_world(o2)).normalized().unwrap_or(Vec3::Z));
|
||||
// A flat face holding the axis is an angle cap: on the sketch plane, or the far one.
|
||||
let flat = !ns.is_empty() && ns.windows(2).all(|w| w[0].cross(w[1]).len() < 1e-6) && ns.iter().all(|m| m.dot(d).abs() < 1e-6);
|
||||
if flat {
|
||||
let on_plane = ps.iter().all(|p| (*p - ss.plane.origin).dot(ss.plane.normal()).abs() < 1e-6 * (1.0 + p.len()));
|
||||
return format!("F{fid}:{}", if on_plane { "start" } else { "end" });
|
||||
}
|
||||
// A swept face: turn its centre back into the sketch plane and find the curve.
|
||||
let q = c - o;
|
||||
let t = q.dot(d);
|
||||
let r = (q - d * t).len();
|
||||
let u3 = ss.plane.normal().cross(d).normalized().unwrap_or(Vec3::X);
|
||||
let best = [u3, u3 * -1.0]
|
||||
.iter()
|
||||
.filter_map(|u| {
|
||||
let w = ss.plane.to_local(o + d * t + *u * r);
|
||||
let q2 = Vec2::new(w.x, w.y);
|
||||
nearest_curve_dist(ss, q2)
|
||||
})
|
||||
.min_by(|a, b| a.0.total_cmp(&b.0));
|
||||
best.map(|(_, id)| format!("F{fid}:side:{id}")).unwrap_or_else(|| format!("F{fid}:face:{fi}"))
|
||||
}
|
||||
FeatureKind::Box { .. } => {
|
||||
let n = ns.first().copied().unwrap_or(Vec3::Z);
|
||||
let axis = |v: f64, p: &str, m: &str| {
|
||||
if v > 0.5 {
|
||||
Some(p.to_string())
|
||||
} else if v < -0.5 {
|
||||
Some(m.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
let role =
|
||||
axis(n.x, "+x", "-x").or_else(|| axis(n.y, "+y", "-y")).or_else(|| axis(n.z, "+z", "-z")).unwrap_or_else(|| format!("face:{fi}"));
|
||||
format!("F{fid}:{role}")
|
||||
}
|
||||
FeatureKind::Cylinder { axis, .. } => {
|
||||
let flat = !ns.is_empty() && ns.iter().all(|m| m.cross(*axis).len() < 1e-6);
|
||||
let role = if !flat {
|
||||
"side"
|
||||
} else if ns.first().is_some_and(|m| m.dot(*axis) > 0.0) {
|
||||
"top"
|
||||
} else {
|
||||
"bottom"
|
||||
};
|
||||
format!("F{fid}:{role}")
|
||||
}
|
||||
FeatureKind::Fillet { edges, .. } | FeatureKind::Chamfer { edges, .. } => {
|
||||
let k = edges.iter().enumerate().min_by(|a, b| a.1.dist(c).total_cmp(&b.1.dist(c))).map(|(k, _)| k).unwrap_or(0);
|
||||
match f.edge_names.get(k) {
|
||||
@@ -328,6 +419,23 @@ fn role_name(f: &Feature, before: &ModelState, ps: &[Vec3], ns: &[Vec3], fi: usi
|
||||
}
|
||||
}
|
||||
|
||||
/// The sketch curve (id) nearest a point in the sketch plane, and how near.
|
||||
fn nearest_curve_dist(ss: &crate::SolvedSketch, q: Vec2) -> Option<(f64, String)> {
|
||||
let mut best: Option<(f64, String)> = None;
|
||||
for (ci, c) in ss.sketch.curves.iter().enumerate() {
|
||||
if c.construction {
|
||||
continue;
|
||||
}
|
||||
for s in ss.sketch.segs(ci) {
|
||||
let d = seg_dist(&s, q);
|
||||
if best.as_ref().is_none_or(|(bd, _)| d < *bd) {
|
||||
best = Some((d, c.id.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
/// The sketch curve (id) nearest a point in the sketch plane.
|
||||
fn nearest_curve(ss: &crate::SolvedSketch, q: Vec2) -> Option<String> {
|
||||
let mut best: Option<(f64, String)> = None;
|
||||
|
||||
@@ -1266,4 +1266,34 @@ mod naming {
|
||||
let w = warning(&s, "Round").unwrap_or_default();
|
||||
assert!(w.contains("no longer exists"), "{w}");
|
||||
}
|
||||
|
||||
fn face_names(s: &mut Session, body: &str) -> Vec<String> {
|
||||
let f = run(s, "model.faces", json!({ "body": body }));
|
||||
f["faces"].as_array().into_iter().flatten().filter_map(|x| x["name"].as_str().map(str::to_string)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn primitives_revolves_and_pattern_copies_are_named() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "body_name": "B"}));
|
||||
let n = face_names(&mut s, "B");
|
||||
for want in ["F1:+x", "F1:-x", "F1:+y", "F1:-y", "F1:+z", "F1:-z"] {
|
||||
assert!(n.contains(&want.to_string()), "{want} in {n:?}");
|
||||
}
|
||||
run(&mut s, "PatternRectangular", json!({"bodies": ["B"], "dir1": [1, 0, 0], "count1": 3, "spacing1": 20}));
|
||||
let bodies: Vec<String> = s.model.state().bodies.iter().map(|b| b.name.clone()).collect();
|
||||
let copy = face_names(&mut s, &bodies[2]);
|
||||
assert!(copy.iter().all(|x| x.starts_with("F1:") && x.contains("@F2.")), "{copy:?}");
|
||||
// A revolved ring: the section's four lines name the four faces.
|
||||
let mut r = Session::default();
|
||||
run(&mut r, "SketchCreate", json!({"plane": "XZ"}));
|
||||
run(&mut r, "DrawPolyline", json!({"points": [[10, 0], [15, 0], [15, 5], [10, 5]], "closed": true, "ids": ["a", "b", "c", "d"]}));
|
||||
run(&mut r, "SketchStop", json!({}));
|
||||
run(&mut r, "Revolve", json!({"axis": "y", "angle": "270 deg"}));
|
||||
let body = r.model.state().bodies[0].name.clone();
|
||||
let n = face_names(&mut r, &body);
|
||||
for want in ["F2:side:a", "F2:side:b", "F2:side:c", "F2:side:d", "F2:start", "F2:end"] {
|
||||
assert!(n.iter().any(|x| x.starts_with(want)), "{want} in {n:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user