mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 11:05:56 +00:00
doc: names saved before format 14 find nothing; their features ask to reselect
Face and edge names stored before format 14 were worked out from geometry and open marked `?`. They were found among the names geometry gives today, as the builds that saved them found them, pieces by the one nearest the pick. They find nothing now: the feature holding one fails, asking for its reference to be picked again, which stores its exact name. The legacy face and edge names a body worked out for them are gone. The tests that found such names as saved now assert the request to reselect: a saved strip projection, a version 9 emboss on an annulus, a format 10 face sketch and the corpus design named with numbered pieces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -51,8 +51,9 @@ fn old_designs_open_and_evaluate_as_saved() {
|
||||
let name = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
|
||||
let want: Value = serde_json::from_slice(&std::fs::read(p.with_extension("expected.json")).unwrap()).unwrap();
|
||||
let mut s = open(&p);
|
||||
// A name saved before format 14 finds nothing: its feature asks for it to be picked again.
|
||||
for r in &s.model.results {
|
||||
if let Some(e) = &r.error {
|
||||
if let Some(e) = r.error.as_deref().filter(|e| solidcraft_engine::doc::reselect_parts(e).is_none()) {
|
||||
problems.push(format!("{name}: feature {}: {e}", r.id));
|
||||
}
|
||||
}
|
||||
@@ -94,23 +95,28 @@ fn old_designs_open_and_evaluate_as_saved() {
|
||||
}
|
||||
|
||||
/// Names saved before format 14 were worked out from geometry, pieces of a split face numbered by
|
||||
/// where their centres lay (`F2:top#2`). They keep naming the faces they named: marked as such,
|
||||
/// they are found as the build that saved them found them, without new notes.
|
||||
/// where their centres lay (`F2:top#2`). They open marked as such and find no face or edge today:
|
||||
/// each feature holding one fails, asking for its reference to be picked again, and the rest of
|
||||
/// the design builds.
|
||||
#[test]
|
||||
fn names_saved_before_exact_history_find_the_faces_they_named() {
|
||||
fn names_saved_before_exact_history_ask_for_their_faces_again() {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus/naming-split-pieces.solidcraft");
|
||||
let s = open(&path);
|
||||
let feature = |name: &str| s.doc.features.iter().find(|f| f.name == name).unwrap_or_else(|| panic!("{name}"));
|
||||
assert_eq!(feature("Round").edge_names, vec!["?F2:side:l2|F2:top#2".to_string()]);
|
||||
assert_eq!(feature("Bevel").edge_names, vec!["?F2:side:l4|F2:top#1".to_string()]);
|
||||
assert_eq!(feature("Raise").face_names, vec!["?F2:top#1".to_string()]);
|
||||
for r in &s.model.results {
|
||||
assert!(r.error.is_none() && r.warning.is_none(), "{}: {:?} {:?}", r.name, r.error, r.warning);
|
||||
let asked = |name: &str| {
|
||||
let r = s.model.result(feature(name).id).unwrap_or_else(|| panic!("{name}"));
|
||||
r.error.as_deref().and_then(solidcraft_engine::doc::reselect_parts).map(|(f, reference)| (f.to_string(), reference.to_string()))
|
||||
};
|
||||
assert_eq!(asked("Round"), Some(("Round".into(), "?F2:side:l2|F2:top#2".into())));
|
||||
assert_eq!(asked("Bevel"), Some(("Bevel".into(), "?F2:side:l4|F2:top#1".into())));
|
||||
assert_eq!(asked("Raise"), Some(("Raise".into(), "?F2:top#1".into())));
|
||||
for r in s.model.results.iter().filter(|r| !["Round", "Bevel", "Raise"].contains(&r.name.as_str())) {
|
||||
assert!(r.error.is_none(), "{}: {:?}", r.name, r.error);
|
||||
}
|
||||
// The raised piece is the left one, as saved (the stored point is on it).
|
||||
let state = s.model.state();
|
||||
let solid = |p: [f64; 3]| state.bodies.iter().any(|b| b.body.contains(solidcraft_engine::geom::Vec3::new(p[0], p[1], p[2])));
|
||||
assert!(solid([7.0, 15.0, 11.0]) && !solid([32.0, 15.0, 11.0]));
|
||||
assert!(!s.model.state().bodies.is_empty());
|
||||
}
|
||||
|
||||
/// Body names saved before format 14 name the pieces of a split body in the kernel's shell order.
|
||||
@@ -173,6 +179,7 @@ fn flat_face_attachments() -> Vec<(String, String, usize, Option<(u32, bool)>, O
|
||||
for p in corpus() {
|
||||
let name = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
|
||||
let mut s = open(&p);
|
||||
let failed_on_open: Vec<u64> = s.model.results.iter().filter(|r| r.error.is_some()).map(|r| r.id).collect();
|
||||
let state = s.model.state();
|
||||
let placed = s.placed_document();
|
||||
let mut picks = Vec::new();
|
||||
@@ -224,7 +231,8 @@ fn flat_face_attachments() -> Vec<(String, String, usize, Option<(u32, bool)>, O
|
||||
let known = known_without_attachment(&name, &body, normal, distance);
|
||||
out.push((name.clone(), body, index, known, problem));
|
||||
}
|
||||
assert!(s.model.results.iter().all(|r| r.error.is_none()), "{name}: left with errors after undo");
|
||||
let failed: Vec<u64> = s.model.results.iter().filter(|r| r.error.is_some()).map(|r| r.id).collect();
|
||||
assert_eq!(failed, failed_on_open, "{name}: undo leaves the features that failed on opening, and only those, failed");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -175,9 +175,7 @@ fn record_refs(out: &mut Vec<(String, bool)>, s: &Session, f: &Feature) {
|
||||
("not checked (a cylindrical emboss finds its face on its own body)".to_string(), true)
|
||||
} else {
|
||||
match naming::locate_face_on_body(&fst, f.kind.face_body(), n, p) {
|
||||
Some(hit) => {
|
||||
(format!("{}{}", face_label(&fst, hit.body, hit.face), flags(&[(!hit.verified, "geometric"), (hit.legacy, "legacy")])), true)
|
||||
}
|
||||
Some(hit) => (format!("{}{}", face_label(&fst, hit.body, hit.face), flags(&[(!hit.verified, "geometric")])), true),
|
||||
None => ("gone".into(), false),
|
||||
}
|
||||
};
|
||||
@@ -193,7 +191,7 @@ fn record_refs(out: &mut Vec<(String, bool)>, s: &Session, f: &Feature) {
|
||||
"{} edge {}{}",
|
||||
b.name,
|
||||
naming::edge_names(b).get(hit.edge).map(String::as_str).unwrap_or("?"),
|
||||
flags(&[(!hit.verified, "geometric"), (hit.legacy, "legacy")])
|
||||
flags(&[(!hit.verified, "geometric")])
|
||||
),
|
||||
true,
|
||||
),
|
||||
|
||||
@@ -230,7 +230,7 @@ fn regularized_shell_keeps_legacy_and_explicit_single_face_projections_closed()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_former_strip_projection_reports_lost_without_expanding_healthy_material() {
|
||||
fn saved_former_strip_projection_asks_for_its_face_again_without_expanding_healthy_material() {
|
||||
let (mut doc, _, _) = shell_document();
|
||||
let mut model = Model::new();
|
||||
model.evaluate(&doc);
|
||||
@@ -257,10 +257,9 @@ fn saved_former_strip_projection_reports_lost_without_expanding_healthy_material
|
||||
)
|
||||
.unwrap();
|
||||
sketch.links[0].face_name = Some(former.clone());
|
||||
let original = sketch.clone();
|
||||
let sk = doc
|
||||
.add_feature(
|
||||
FeatureKind::Sketch { plane: PlaneRef::Face { plane, at: Vec3::new(20., 6., 1.), name: Some(former), anchor: None }, sketch },
|
||||
FeatureKind::Sketch { plane: PlaneRef::Face { plane, at: Vec3::new(20., 6., 1.), name: Some(former.clone()), anchor: None }, sketch },
|
||||
Some("Saved strip sketch"),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -288,21 +287,13 @@ fn saved_former_strip_projection_reports_lost_without_expanding_healthy_material
|
||||
let mut cold = Model::new();
|
||||
cold.evaluate(&loaded);
|
||||
for current in [&model, &cold] {
|
||||
let warning = current.results.iter().find(|r| r.id == sk).unwrap().warning.as_deref().unwrap();
|
||||
assert!(warning.contains("no longer exists; last geometry retained"), "{warning}");
|
||||
let state = current.state();
|
||||
let solved = state.sketch(sk).unwrap();
|
||||
assert!(solved.sketch.links[0].lost);
|
||||
assert_eq!(solved.sketch.curves, original.curves);
|
||||
assert_eq!(solved.sketch.points, original.points);
|
||||
let material = state.bodies.iter().find(|body| body.feature == boss).unwrap();
|
||||
assert!(
|
||||
(solidcraft_kernel::measure(&material.body).unwrap().volume - 24.).abs() < 1e-8,
|
||||
"old24mm3 profile must not silently become120mm3 wall profile"
|
||||
);
|
||||
let bounds = material.mesh().bounds();
|
||||
assert!((bounds.min.x - width).abs() < 1e-8 && (bounds.max.x - width - 1.).abs() < 1e-8);
|
||||
assert!((bounds.max.y - 12.).abs() < 1e-8 && (bounds.max.z - 2.).abs() < 1e-8);
|
||||
// The name was saved before format 14 (marked): it finds no face, so the sketch on it
|
||||
// fails, asking for its face again, and so does the material made from it.
|
||||
let result = current.results.iter().find(|r| r.id == sk).unwrap();
|
||||
let reference = format!("{}{former}", crate::naming::LEGACY_MARK);
|
||||
assert_eq!(result.error.as_deref().and_then(crate::reselect_parts), Some(("Saved strip sketch", reference.as_str())), "{result:?}");
|
||||
assert!(current.results.iter().find(|r| r.id == boss).unwrap().error.is_some());
|
||||
assert!(current.state().bodies.iter().all(|body| body.feature != boss), "old 24 mm3 profile must not silently become the 120 mm3 wall");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6277,11 +6277,7 @@ fn face_plane(st: &ModelState, picked: &Plane, at: Vec3, name: Option<&str>) ->
|
||||
if !face.plane_normal.is_some_and(|fnrm| fnrm.dot(n) > 1.0 - 1e-6) {
|
||||
return Err(DocError::reselect(name));
|
||||
}
|
||||
// A name saved before format 14 is found as it was then, without a note.
|
||||
let note = if crate::naming::legacy_name(name).is_none()
|
||||
&& !crate::naming::face_name_exact(b, hit.face)
|
||||
&& (face.centroid - at).dot(n).abs() > tol
|
||||
{
|
||||
let note = if !crate::naming::face_name_exact(b, hit.face) && (face.centroid - at).dot(n).abs() > tol {
|
||||
// Named from geometry and moved off the picked plane: geometry alone decided.
|
||||
Some(format!("face `{name}` was found by geometry, not exact history; check it"))
|
||||
} else {
|
||||
|
||||
@@ -387,8 +387,8 @@ const JSON_STEPS: [JsonStep; HAND_WRITTEN as usize - 1] = [
|
||||
];
|
||||
|
||||
/// Stored face and edge names were worked out from geometry before format 14; they get the
|
||||
/// legacy mark (`?`, see `naming::LEGACY_MARK`) so they are found again as those builds found
|
||||
/// them, and an older design evaluates as it was saved. The step marks the fields that store
|
||||
/// legacy mark (`?`, see `naming::LEGACY_MARK`), which finds no face or edge: a feature holding
|
||||
/// one fails, asking for its reference to be picked again. The step marks the fields that store
|
||||
/// such a name, by their place in the schema:
|
||||
/// - each feature's `face_names`, and fillets' and chamfers' `edge_names` (other features' edge
|
||||
/// names are sheet metal's own);
|
||||
|
||||
@@ -37,9 +37,8 @@ fn bad(what: &str) -> DocError {
|
||||
DocError::Invalid(what.into())
|
||||
}
|
||||
|
||||
/// A design whose stored face or edge names were saved before format 14 (marked
|
||||
/// [`crate::naming::LEGACY_MARK`]) finds them through names worked out from geometry on demand,
|
||||
/// which a saved model does not keep.
|
||||
/// A design holding a face or edge name saved before format 14 (marked
|
||||
/// [`crate::naming::LEGACY_MARK`]): it saves without a model.
|
||||
fn has_legacy_names(doc: &Document) -> bool {
|
||||
let mark = format!("\"{}", crate::naming::LEGACY_MARK);
|
||||
doc.features.iter().any(|f| match &f.kind {
|
||||
|
||||
+37
-110
@@ -4,25 +4,24 @@
|
||||
//! side drawn in collinear pieces, has each as a name); else the one face answering to it as
|
||||
//! exactly: the named piece told apart by the faces around it, which still bound it. Nothing
|
||||
//! else is: when the named face was split into several pieces, when it was merged into another
|
||||
//! face, when only a face of the same stem is left, or when the name is on several bodies, the
|
||||
//! name finds nothing and the feature that uses it fails, asking for the reference to be picked
|
||||
//! again ([`crate::DocError::Reselect`]). Names saved before format 14 ([`LEGACY_MARK`]) are
|
||||
//! found among the names geometry gives, as the builds that saved them found them. A face whose
|
||||
//! name was worked out from geometry, found where the reference's point no longer lies, is
|
||||
//! reported: only geometry decided.
|
||||
//! face, when only a face of the same stem is left, when the name is on several bodies, or when
|
||||
//! the name was saved before format 14 ([`LEGACY_MARK`]), the name finds nothing and the feature
|
||||
//! that uses it fails, asking for the reference to be picked again
|
||||
//! ([`crate::DocError::Reselect`]). A face whose name was worked out from geometry, found where
|
||||
//! the reference's point no longer lies, is reported: only geometry decided.
|
||||
|
||||
use solidcraft_geom::{Mesh, Vec3};
|
||||
|
||||
use crate::history_names::{Match, Names};
|
||||
use crate::naming::{self, closest, edge_names, face_names, legacy_edge_names, legacy_face_names, strip_piece};
|
||||
use crate::piece_names::{strip_edge_number, strip_edge_piece};
|
||||
use crate::naming::{self, closest, edge_names, face_names, strip_piece};
|
||||
use crate::piece_names::strip_edge_number;
|
||||
use crate::{ModelBody, ModelState};
|
||||
|
||||
/// The mark on a name saved before format 14 (`?F2:top#2`). Those names were worked out from
|
||||
/// geometry, and a few of them name a face that exact history names differently (a tube's outer
|
||||
/// wall was named after the nearest sketch curve to its centre, its inner circle): such a name is
|
||||
/// found again among the names that geometry gives today, as the build that saved it did, so an
|
||||
/// older design evaluates as it was saved. Picking the face again stores its exact name.
|
||||
/// geometry, and some of them name a face that exact history names differently (a tube's outer
|
||||
/// wall was named after the nearest sketch curve to its centre, its inner circle): such a name
|
||||
/// finds no face, and the feature using it asks for the face to be picked again, which stores
|
||||
/// its exact name.
|
||||
pub const LEGACY_MARK: char = '?';
|
||||
|
||||
/// A name saved before format 14, without its mark (`None`: an exact name).
|
||||
@@ -42,16 +41,14 @@ pub struct EdgeHit {
|
||||
pub verified: bool,
|
||||
/// Whether the reference's point still lies on the edge.
|
||||
pub on_point: bool,
|
||||
/// The name was saved before format 14 and found as that build did ([`LEGACY_MARK`]).
|
||||
pub legacy: bool,
|
||||
}
|
||||
|
||||
/// The edge of a body a stored edge name finds: the edge with that name; else the one edge
|
||||
/// between the faces it names exactly (see [`exactly`]). `Err` explains when there is none: no
|
||||
/// such edge, or the named edge split into several.
|
||||
/// such edge, the named edge split into several, or a name saved before format 14.
|
||||
pub fn locate_edge(b: &ModelBody, name: &str, near: Vec3) -> Result<EdgeHit, String> {
|
||||
if let Some(old) = legacy_name(name) {
|
||||
return legacy_locate_edge(b, old, near).map_err(|_| format!("edge `{name}` no longer exists"));
|
||||
if legacy_name(name).is_some() {
|
||||
return Err(format!("edge `{name}` was named by a build before format 14"));
|
||||
}
|
||||
let m = b.mesh();
|
||||
let names = edge_names(b);
|
||||
@@ -64,9 +61,7 @@ pub fn locate_edge(b: &ModelBody, name: &str, near: Vec3) -> Result<EdgeHit, Str
|
||||
};
|
||||
let exact = |i: usize| -> Result<EdgeHit, String> {
|
||||
let on = edge_near(b, &m, i, near);
|
||||
mid(i)
|
||||
.map(|p| EdgeHit { edge: i, point: if on { near } else { p }, verified: verified(i), on_point: on, legacy: false })
|
||||
.ok_or_else(|| "edge".into())
|
||||
mid(i).map(|p| EdgeHit { edge: i, point: if on { near } else { p }, verified: verified(i), on_point: on }).ok_or_else(|| "edge".into())
|
||||
};
|
||||
if let Some(i) = names.iter().position(|n| n == name) {
|
||||
return exact(i);
|
||||
@@ -134,25 +129,6 @@ fn edge_mid(m: &Mesh, i: usize) -> Option<Vec3> {
|
||||
e.first().copied()
|
||||
}
|
||||
|
||||
/// [`locate_edge`] for a name saved before format 14 (without its mark), as those builds found
|
||||
/// it: the edge with that name among the names geometry gives, else the piece of it nearest
|
||||
/// `near`.
|
||||
fn legacy_locate_edge(b: &ModelBody, name: &str, near: Vec3) -> Result<EdgeHit, ()> {
|
||||
let m = b.mesh();
|
||||
let names = legacy_edge_names(b);
|
||||
if let Some(i) = names.iter().position(|n| n == name) {
|
||||
let on = edge_near(b, &m, i, near);
|
||||
return edge_mid(&m, i).map(|p| EdgeHit { edge: i, point: if on { near } else { p }, verified: false, on_point: on, legacy: true }).ok_or(());
|
||||
}
|
||||
let stem = strip_edge_piece(name);
|
||||
(0..names.len())
|
||||
.filter(|i| names.get(*i).is_some_and(|n| strip_edge_piece(n) == stem))
|
||||
.filter_map(|i| edge_mid(&m, i).map(|p| (p, i)))
|
||||
.min_by(|a, b| a.0.dist(near).total_cmp(&b.0.dist(near)))
|
||||
.map(|(p, i)| EdgeHit { edge: i, point: p, verified: false, on_point: false, legacy: true })
|
||||
.ok_or(())
|
||||
}
|
||||
|
||||
/// How well the stored edge name `wanted` (without its own number) names the edge between faces
|
||||
/// `a` and `b`: the weaker of its two faces' matches, read every way the name splits into two
|
||||
/// face names (a face name may hold `|` itself: a fillet face is named after its edge).
|
||||
@@ -192,15 +168,13 @@ pub struct FaceHit {
|
||||
/// Whether the reference's point still lies on the face (so a name worked out from geometry
|
||||
/// and the pick agree).
|
||||
pub on_point: bool,
|
||||
/// The name was saved before format 14 and found as that build did ([`LEGACY_MARK`]).
|
||||
pub legacy: bool,
|
||||
}
|
||||
|
||||
/// Why a stored face name finds no face.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Miss {
|
||||
/// No face has the name, or only a face it was merged into or a face of the same stem is
|
||||
/// left.
|
||||
/// left, or the name was saved before format 14.
|
||||
Gone,
|
||||
/// Several faces answer to it as well: the pieces of a split face, or the same face on
|
||||
/// several bodies.
|
||||
@@ -210,11 +184,7 @@ enum Miss {
|
||||
impl FaceHit {
|
||||
/// What a feature says about how it found a face by its stored name `name` (`None`: exactly).
|
||||
pub fn note(&self, name: &str) -> Option<String> {
|
||||
if !self.verified && !self.on_point && !self.legacy {
|
||||
Some(format!("face `{name}` was found by geometry, not exact history; check it"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
if !self.verified && !self.on_point { Some(format!("face `{name}` was found by geometry, not exact history; check it")) } else { None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,11 +290,7 @@ fn inside_nearest(m: &Mesh, face: usize, p: Vec3, margin: f64) -> Option<Vec3> {
|
||||
impl EdgeHit {
|
||||
/// What a feature says about how it found an edge by its stored name `name` (`None`: exactly).
|
||||
pub fn note(&self, name: &str) -> Option<String> {
|
||||
if !self.verified && !self.on_point && !self.legacy {
|
||||
Some(format!("edge `{name}` was found by geometry, not exact history; check it"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
if !self.verified && !self.on_point { Some(format!("edge `{name}` was found by geometry, not exact history; check it")) } else { None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,11 +300,12 @@ pub fn locate_face(st: &ModelState, name: &str, p: Vec3) -> Option<FaceHit> {
|
||||
if crate::sheet::split_sheet_face_name(name).is_some() {
|
||||
return sheet_locate(st, name, p).ok();
|
||||
}
|
||||
if legacy_name(name).is_some() {
|
||||
return None;
|
||||
}
|
||||
// Every body's names are read below: work out the ones not known yet on all cores first
|
||||
// (each body's names are its own, whichever thread works them out).
|
||||
if legacy_name(name).is_none() {
|
||||
naming::warm_names(st.bodies.iter().filter(|b| !b.body.is_mesh()));
|
||||
}
|
||||
naming::warm_names(st.bodies.iter().filter(|b| !b.body.is_mesh()));
|
||||
locate_in(st.bodies.iter().enumerate().filter(|(_, b)| !b.body.is_mesh()), name, p).ok()
|
||||
}
|
||||
|
||||
@@ -378,13 +345,13 @@ fn sheet_locate(st: &ModelState, name: &str, p: Vec3) -> Result<FaceHit, Miss> {
|
||||
_ => return Err(Miss::Several),
|
||||
};
|
||||
let (distance, point) = nearest_on_face(&b.mesh(), face, p).ok_or(Miss::Gone)?;
|
||||
Ok(FaceHit { body: bi, face, point, verified: true, on_point: distance <= (b.body.size() * 1e-3).max(1e-3), legacy: false })
|
||||
Ok(FaceHit { body: bi, face, point, verified: true, on_point: distance <= (b.body.size() * 1e-3).max(1e-3) })
|
||||
}
|
||||
|
||||
/// [`locate_face`] among `bodies` (with their indices): the one face the name finds there.
|
||||
fn locate_in<'a>(bodies: impl Iterator<Item = (usize, &'a ModelBody)>, name: &str, p: Vec3) -> Result<FaceHit, Miss> {
|
||||
if let Some(old) = legacy_name(name) {
|
||||
return legacy_locate(bodies, old, p).ok_or(Miss::Gone);
|
||||
if legacy_name(name).is_some() {
|
||||
return Err(Miss::Gone);
|
||||
}
|
||||
let mut best: Option<(Match, Vec<(usize, &ModelBody, usize)>)> = None;
|
||||
for (bi, b) in bodies {
|
||||
@@ -398,14 +365,7 @@ fn locate_in<'a>(bodies: impl Iterator<Item = (usize, &'a ModelBody)>, name: &st
|
||||
let (level, faces) = best.ok_or(Miss::Gone)?;
|
||||
let found = |(body, b, face): (usize, &ModelBody, usize)| {
|
||||
let (distance, point) = nearest_on_face(&b.mesh(), face, p)?;
|
||||
Some(FaceHit {
|
||||
body,
|
||||
face,
|
||||
point,
|
||||
verified: naming::names(b).verified(face),
|
||||
on_point: distance <= (b.body.size() * 1e-3).max(1e-3),
|
||||
legacy: false,
|
||||
})
|
||||
Some(FaceHit { body, face, point, verified: naming::names(b).verified(face), on_point: distance <= (b.body.size() * 1e-3).max(1e-3) })
|
||||
};
|
||||
if let [one] = faces.as_slice() {
|
||||
return found(*one).ok_or(Miss::Gone);
|
||||
@@ -424,54 +384,21 @@ fn locate_in<'a>(bodies: impl Iterator<Item = (usize, &'a ModelBody)>, name: &st
|
||||
}
|
||||
}
|
||||
|
||||
/// [`locate_face`] for a name saved before format 14 (without its mark), as those builds found
|
||||
/// it: among the names geometry gives, the face with that name (or a piece of the same face, if
|
||||
/// nearer `p`), the nearest point on it.
|
||||
fn legacy_locate<'a>(bodies: impl Iterator<Item = (usize, &'a ModelBody)>, name: &str, p: Vec3) -> Option<FaceHit> {
|
||||
let stem = strip_piece(name);
|
||||
let mut best: Option<(f64, usize, usize, Vec3, f64)> = None;
|
||||
for (bi, b) in bodies {
|
||||
let names = legacy_face_names(b);
|
||||
let tol = (b.body.size() * 1e-3).max(1e-3);
|
||||
let m = b.mesh();
|
||||
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
|
||||
let Some(n) = names.get(*f as usize) else { continue };
|
||||
let exact = n == name;
|
||||
if !exact && strip_piece(n) != stem {
|
||||
continue;
|
||||
}
|
||||
let Some([a, bb, c]) = m.tri(t) else { continue };
|
||||
let q = closest(p, a, bb, c);
|
||||
let d = q.dist(p) - if exact { 1e-9 } else { 0.0 };
|
||||
if d.is_finite() && best.is_none_or(|x| d < x.0) {
|
||||
best = Some((d, bi, *f as usize, q, tol));
|
||||
}
|
||||
}
|
||||
}
|
||||
best.map(|(d, body, face, point, tol)| FaceHit { body, face, point, verified: false, on_point: d <= tol, legacy: true })
|
||||
}
|
||||
|
||||
/// Whether face `index` of a body is exactly the face a stored name names (see [`exactly`]); for
|
||||
/// a name saved before format 14, that name among the names those builds gave.
|
||||
/// Whether face `index` of a body is exactly the face a stored name names (see [`exactly`]). A
|
||||
/// name saved before format 14 names no face.
|
||||
pub fn face_has_name(b: &ModelBody, index: usize, name: &str) -> bool {
|
||||
match legacy_name(name) {
|
||||
Some(old) => legacy_face_names(b).get(index).is_some_and(|n| n == old),
|
||||
None => crate::history_names::matches(&naming::names(b), index, name).is_some_and(exactly),
|
||||
}
|
||||
legacy_name(name).is_none() && crate::history_names::matches(&naming::names(b), index, name).is_some_and(exactly)
|
||||
}
|
||||
|
||||
/// The faces of a body that are the stored name's face or pieces of it (by stem), as the name's
|
||||
/// own era names them.
|
||||
/// The faces of a body that are the stored name's face or pieces of it (by stem). A name saved
|
||||
/// before format 14 has none.
|
||||
pub fn faces_with_stem(b: &ModelBody, name: &str) -> Vec<usize> {
|
||||
let (names, wanted) = match legacy_name(name) {
|
||||
Some(old) => (legacy_face_names(b), old),
|
||||
None => (face_names(b), name),
|
||||
};
|
||||
let stem = strip_piece(wanted);
|
||||
let aliases = |i: usize| {
|
||||
legacy_name(name).is_none()
|
||||
&& naming::names(b).info.get(i).is_some_and(|x| x.synonyms.iter().chain(&x.aliases).any(|a| strip_piece(a) == stem))
|
||||
};
|
||||
if legacy_name(name).is_some() {
|
||||
return Vec::new();
|
||||
}
|
||||
let names = face_names(b);
|
||||
let stem = strip_piece(name);
|
||||
let aliases = |i: usize| naming::names(b).info.get(i).is_some_and(|x| x.synonyms.iter().chain(&x.aliases).any(|a| strip_piece(a) == stem));
|
||||
(0..names.len()).filter(|i| names.get(*i).is_some_and(|n| strip_piece(n) == stem) || aliases(*i)).collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -42,9 +42,7 @@ pub struct NameCell {
|
||||
/// What the body's faces came from, when the feature that made it reports it.
|
||||
lineage: Option<Arc<Lineage>>,
|
||||
names: Arc<OnceLock<Arc<Names>>>,
|
||||
/// The names designs saved before format 14 gave its faces (see [`LEGACY_MARK`]).
|
||||
legacy: Arc<OnceLock<Arc<Vec<String>>>>,
|
||||
/// The body this one is a moved copy of (its legacy names are that body's).
|
||||
/// The body this one is a moved copy of (its names are that body's).
|
||||
copy_of: Option<Arc<ModelBody>>,
|
||||
/// How deep the chain of features that named it is, for names taken from a saved model
|
||||
/// (which has no origin): see [`NameCell::depth`].
|
||||
@@ -57,15 +55,11 @@ impl NameCell {
|
||||
}
|
||||
/// Names of a body made by `origin`'s feature, following `lineage` where it is known.
|
||||
pub(crate) fn traced(origin: Option<Arc<NamingOrigin>>, lineage: Option<Arc<Lineage>>) -> Self {
|
||||
NameCell { origin, lineage, names: Arc::new(OnceLock::new()), legacy: Arc::new(OnceLock::new()), copy_of: None, saved_depth: None }
|
||||
NameCell { origin, lineage, names: Arc::new(OnceLock::new()), copy_of: None, saved_depth: None }
|
||||
}
|
||||
/// Names already known, taken as they are.
|
||||
pub fn known(names: Arc<Vec<String>>) -> Self {
|
||||
let legacy = OnceLock::new();
|
||||
let _ = legacy.set(names.clone());
|
||||
let mut cell = Self::shared(Arc::new(Names::given(names)));
|
||||
cell.legacy = Arc::new(legacy);
|
||||
cell
|
||||
Self::shared(Arc::new(Names::given(names)))
|
||||
}
|
||||
/// The names of `body` (a moved copy of a body keeps its faces' names, and how sure they are).
|
||||
///
|
||||
@@ -102,7 +96,7 @@ impl NameCell {
|
||||
fn shared(names: Arc<Names>) -> Self {
|
||||
let cell = OnceLock::new();
|
||||
let _ = cell.set(names);
|
||||
NameCell { origin: None, lineage: None, names: Arc::new(cell), legacy: Arc::new(OnceLock::new()), copy_of: None, saved_depth: None }
|
||||
NameCell { origin: None, lineage: None, names: Arc::new(cell), copy_of: None, saved_depth: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +160,7 @@ const MAX_DEPTH: usize = 400;
|
||||
const SAMPLES: usize = 9;
|
||||
|
||||
/// Up to this many sample points per face where a sheet's panel kept in place names the face it is
|
||||
/// part of (see [`geometric_from`]).
|
||||
/// part of (see [`geometric_names`]).
|
||||
const PANEL_SAMPLES: usize = 256;
|
||||
|
||||
/// Unit normals of each face's triangles.
|
||||
@@ -420,35 +414,6 @@ pub(crate) fn names(b: &ModelBody) -> Arc<Names> {
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// The names builds before format 14 gave a body's faces: from geometry, over the inputs' names
|
||||
/// as those builds gave them, pieces of a split face numbered along the longest axis of their
|
||||
/// spread (`#1`, `#2`, …).
|
||||
pub(crate) fn legacy_face_names(b: &ModelBody) -> Arc<Vec<String>> {
|
||||
if let Some(source) = &b.names.copy_of {
|
||||
return legacy_face_names(source);
|
||||
}
|
||||
b.names
|
||||
.legacy
|
||||
.get_or_init(|| {
|
||||
let nf = face_count(b);
|
||||
let names = match b.names.origin.as_ref() {
|
||||
Some(o) if o.depth < MAX_DEPTH => {
|
||||
let mut names = geometric_from(b, o, nf, legacy_face_names);
|
||||
number_pieces(&mut names, &samples(&b.mesh(), nf), '#');
|
||||
names
|
||||
}
|
||||
_ => (0..nf).map(|k| format!("{}:face:{k}", b.name)).collect(),
|
||||
};
|
||||
Arc::new(names)
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// The names builds before format 14 gave a body's edges.
|
||||
pub(crate) fn legacy_edge_names(b: &ModelBody) -> Vec<String> {
|
||||
edge_names_from(b, &legacy_face_names(b))
|
||||
}
|
||||
|
||||
/// Pairs of faces of a body that share an edge (from its mesh's edges).
|
||||
pub(crate) fn mesh_adjacency(b: &ModelBody) -> Vec<(usize, usize)> {
|
||||
crate::history_names::face_pairs(b.mesh().edge_faces.iter().map(|fs| fs.iter().map(|f| *f as usize)))
|
||||
@@ -458,17 +423,12 @@ pub(crate) fn mesh_adjacency(b: &ModelBody) -> Vec<(usize, usize)> {
|
||||
/// geometry: where each face's sample points lie on the input's faces. Faces sharing a name are
|
||||
/// pieces of one, told apart afterwards.
|
||||
pub(crate) fn geometric_names(b: &ModelBody, o: &NamingOrigin, nf: usize) -> Vec<String> {
|
||||
geometric_from(b, o, nf, face_names)
|
||||
}
|
||||
|
||||
/// [`geometric_names`], the input bodies' faces named by `names_of`.
|
||||
fn geometric_from(b: &ModelBody, o: &NamingOrigin, nf: usize, names_of: fn(&ModelBody) -> Arc<Vec<String>>) -> Vec<String> {
|
||||
// A rigid Move changes coordinates, not topology or face ownership. Geometric
|
||||
// overlap with the unmoved input cannot recover the names at the new position.
|
||||
if matches!(o.feature.kind, FeatureKind::Move { .. })
|
||||
&& let Some(input) = o.before.body(&b.name)
|
||||
{
|
||||
return names_of(input).as_ref().clone();
|
||||
return face_names(input).as_ref().clone();
|
||||
}
|
||||
let m = b.mesh();
|
||||
let size = b.body.size().max(1e-6);
|
||||
@@ -505,7 +465,7 @@ fn geometric_from(b: &ModelBody, o: &NamingOrigin, nf: usize, names_of: fn(&Mode
|
||||
.take(64)
|
||||
.map(|x| {
|
||||
let n = face_count(x);
|
||||
(x, FaceIndex::new(&x.mesh(), n), names_of(x))
|
||||
(x, FaceIndex::new(&x.mesh(), n), face_names(x))
|
||||
})
|
||||
.collect();
|
||||
let fid = o.feature.id;
|
||||
@@ -520,7 +480,7 @@ fn geometric_from(b: &ModelBody, o: &NamingOrigin, nf: usize, names_of: fn(&Mode
|
||||
.iter()
|
||||
.filter(|x| !x.body.is_mesh())
|
||||
.take(64)
|
||||
.map(|x| (x, FaceIndex::new(&x.mesh(), face_count(x)), names_of(x)))
|
||||
.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() {
|
||||
@@ -1139,7 +1099,7 @@ mod tests {
|
||||
|
||||
fn same_hit(a: &FaceHit, b: &FaceHit) {
|
||||
assert_eq!((a.body, a.face, a.point), (b.body, b.face, b.point));
|
||||
assert_eq!((a.verified, a.on_point, a.legacy), (b.verified, b.on_point, b.legacy));
|
||||
assert_eq!((a.verified, a.on_point), (b.verified, b.on_point));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -120,8 +120,8 @@ fn legacy_v9_imported_emboss_anchor_and_ordinary_face_paint_replay_without_renam
|
||||
let anchored =
|
||||
solidcraft_doc::Feature { shell_piece_order: true, kernel_piece_order: true, touching: solidcraft_doc::Touching::OneBody, ..anchored };
|
||||
assert_eq!(reopened.doc.feature(anchored.id).unwrap(), &anchored);
|
||||
// Names saved before format 14 keep naming the face they named, marked so they are found as
|
||||
// the build that saved them found them.
|
||||
// Names saved before format 14 open with the legacy mark; the sketch is held by its anchor,
|
||||
// not by its name.
|
||||
let mark = |name: &Option<String>| name.as_ref().map(|n| format!("{}{n}", solidcraft_doc::naming::LEGACY_MARK));
|
||||
let mut saved_plane = saved_plane;
|
||||
if let solidcraft_doc::PlaneRef::Face { name, .. } = &mut saved_plane {
|
||||
@@ -175,7 +175,7 @@ fn certified_side_refuses_reused_body_name_from_a_new_producer() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_v9_annulus_outer_side_emboss_keeps_its_original_heuristic_anchor() {
|
||||
fn legacy_v9_annulus_outer_side_emboss_asks_for_its_face_again() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane":"XY"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center":[0,0],"radius":10,"id":"outer"}));
|
||||
@@ -200,19 +200,20 @@ fn legacy_v9_annulus_outer_side_emboss_keeps_its_original_heuristic_anchor() {
|
||||
let mut legacy: Value = serde_json::from_str(&s.doc.to_json()).unwrap();
|
||||
legacy["format"] = json!("solidcraft/9");
|
||||
// What a version 9 design stored for that pick: the centroid-family name of the outer wall
|
||||
// piece holding the point. It keeps finding that piece, as it did when saved.
|
||||
// piece holding the point, worked out from geometry. It names no face today (the outer wall
|
||||
// is one face, named after its own curve): the emboss fails and asks for its face again,
|
||||
// and the sleeve is built without it.
|
||||
for feature in legacy["features"].as_array_mut().unwrap() {
|
||||
if feature["id"] == json!(feature_id) {
|
||||
feature["face_names"] = json!(["F2:side:inner#4"]);
|
||||
}
|
||||
}
|
||||
let reopened = Session::new(solidcraft_doc::Document::from_json(&legacy.to_string()).unwrap());
|
||||
assert!(reopened.model.result(feature_id).unwrap().error.is_none());
|
||||
assert!(
|
||||
(reopened.model.state().body("Sleeve").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().measure().volume - volume)
|
||||
.abs()
|
||||
< 1e-8
|
||||
);
|
||||
let error = reopened.model.result(feature_id).unwrap().error.clone().unwrap();
|
||||
assert_eq!(solidcraft_doc::reselect_parts(&error), Some(("Raised Badge", "?F2:side:inner#4")), "{error}");
|
||||
let sleeve = reopened.model.state().body("Sleeve").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
assert!((sleeve - std::f64::consts::PI * 380.).abs() < 0.2, "volume {sleeve}");
|
||||
assert!(reopened.model.results.iter().filter(|r| r.id != feature_id).all(|r| r.error.is_none()), "{:?}", reopened.model.results);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+16
-23
@@ -2785,7 +2785,9 @@ mod naming {
|
||||
model.evaluate(&reopened);
|
||||
assert_eq!(model.state().sketch(reopened.find_feature("Top").unwrap().id).unwrap().plane.origin.z, 10.);
|
||||
assert!((model.state().bodies[0].mesh().bounds().max.z - 15.).abs() < 1e-6);
|
||||
// A historical format-10 attachment uses its saved generic face name and no strong anchor.
|
||||
// A format-10 attachment has only the face name that build worked out from geometry, and
|
||||
// no strong anchor: that name finds no face today. The sketch fails, asking for its face
|
||||
// again, and so does the peg on it; the block builds.
|
||||
let mut old = (*s.doc).clone();
|
||||
let solidcraft_doc::FeatureKind::Sketch { plane: solidcraft_doc::PlaneRef::Face { anchor, name, .. }, sketch } =
|
||||
&mut old.features.iter_mut().find(|f| f.name == "Top").unwrap().kind
|
||||
@@ -2793,6 +2795,7 @@ mod naming {
|
||||
panic!("fixture must contain a face sketch");
|
||||
};
|
||||
*anchor = None;
|
||||
let saved = name.clone().unwrap();
|
||||
for link in &mut sketch.links {
|
||||
if link.auto_face {
|
||||
link.auto_face = false;
|
||||
@@ -2800,30 +2803,20 @@ mod naming {
|
||||
}
|
||||
}
|
||||
old.format = "solidcraft/10".into();
|
||||
let mut legacy = Session::new(solidcraft_doc::Document::from_json(&old.to_json()).unwrap());
|
||||
assert_eq!(attached(&legacy), Some(10.));
|
||||
assert!((top(&mut legacy) - 15.).abs() < 1e-6);
|
||||
let legacy = Session::new(solidcraft_doc::Document::from_json(&old.to_json()).unwrap());
|
||||
let error = |s: &Session, name: &str| s.model.results.iter().find(|r| r.id == s.doc.find_feature(name).unwrap().id).unwrap().error.clone();
|
||||
let reference = format!("{}{saved}", solidcraft_doc::naming::LEGACY_MARK);
|
||||
assert_eq!(error(&legacy, "Top").as_deref().and_then(solidcraft_doc::reselect_parts), Some(("Top", reference.as_str())));
|
||||
assert!(error(&legacy, "Peg").is_some());
|
||||
assert!(error(&legacy, "Block").is_none());
|
||||
// An upstream join that trims the certified face: the attachment stays on what is left
|
||||
// of it and never follows the substitute roof (see face_tests for a face covered whole).
|
||||
for target in [&mut s, &mut legacy] {
|
||||
run(target, "timeline.roll_to", json!({"feature": "Block"}));
|
||||
run(target, "sketch.create", json!({"plane": "XY", "name": "B"}));
|
||||
run(target, "sketch.rectangle.two_point", json!({"p0": [8, -5], "p1": [45, 35]}));
|
||||
run(target, "sketch.finish", json!({}));
|
||||
run(target, "solid.extrude", json!({"sketch": "B", "distance": 20, "operation": "join", "name": "Boss"}));
|
||||
run(target, "timeline.roll_to", json!({}));
|
||||
}
|
||||
assert_eq!(attached(&legacy), Some(10.), "historical weak naming retains its saved geometry behavior");
|
||||
assert!((top(&mut legacy) - 20.).abs() < 1e-6);
|
||||
let legacy_reopened = Session::new(solidcraft_doc::Document::from_json(&legacy.doc.to_json()).unwrap());
|
||||
assert_eq!(attached(&legacy_reopened), Some(10.));
|
||||
assert!((legacy_reopened.model.state().bodies[0].mesh().bounds().max.z - 20.).abs() < 1e-6);
|
||||
let solidcraft_doc::FeatureKind::Sketch { plane: solidcraft_doc::PlaneRef::Face { anchor, .. }, .. } =
|
||||
&legacy_reopened.doc.find_feature("Top").unwrap().kind
|
||||
else {
|
||||
panic!("saved historical face reference must remain present");
|
||||
};
|
||||
assert!(anchor.is_none(), "opening old face references must not infer a strict attachment");
|
||||
run(&mut s, "timeline.roll_to", json!({"feature": "Block"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "B"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [8, -5], "p1": [45, 35]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "B", "distance": 20, "operation": "join", "name": "Boss"}));
|
||||
run(&mut s, "timeline.roll_to", json!({}));
|
||||
let failed = |s: &Session, name: &str| s.model.results.iter().find(|r| r.id == s.doc.find_feature(name).unwrap().id).unwrap().error.is_some();
|
||||
assert!(!failed(&s, "Top"));
|
||||
assert!(!failed(&s, "Peg"));
|
||||
|
||||
@@ -130,9 +130,9 @@ the blend records later features read). States share what they share: a body tha
|
||||
later features pass through unchanged, its face names and a solved sketch are each
|
||||
recorded once. Face names are kept as worked out (saving works out any not asked
|
||||
for yet), so a body taken from the file is named exactly as when it was saved.
|
||||
A design whose stored names predate format 14 (marked `?`) finds them through
|
||||
names worked out from geometry on demand, which the cache does not keep: it saves
|
||||
without a model.
|
||||
A design whose stored names predate format 14 (marked `?`) saves without a model:
|
||||
those names find no face or edge, and the features holding them ask for their
|
||||
references to be picked again.
|
||||
|
||||
Both entries are Deflate-compressed (level one) under either compression policy: the
|
||||
encoded bodies shrink about fourfold (an imported assembly) to tenfold (modelled parts),
|
||||
|
||||
Reference in New Issue
Block a user