Construction geometry on model vertices follows them when a parameter moves them (#568) (#2409)

Thanks @rlartot.
This commit is contained in:
rlartot
2026-10-19 22:11:38 +02:00
committed by GitHub
parent f42c068063
commit 31b41978fc
8 changed files with 102 additions and 15 deletions
@@ -1665,7 +1665,6 @@ fn a_tangent_plane_on_a_drill_tip() {
}
#[test]
#[ignore = "fails until #568 is fixed"]
fn a_plane_on_a_patterned_counterbore_after_a_thickness_change() {
let mut c = vec![json!({"command": "parameters.change", "params": {"name": "thick", "expression": "10 mm"}})];
c.extend(rectangle(json!("XY"), [0., 0.], [80., 30.]));
+5
View File
@@ -95,6 +95,11 @@ fn remap_geo(
}
}
GeoRef::Face { name: Some(n), .. } | GeoRef::Edge { name: Some(n), .. } => *n = remap_topology(n, ids, bodies),
GeoRef::Vertex { edges: Some(names), .. } => {
for n in names {
*n = remap_topology(n, ids, bodies);
}
}
GeoRef::ConstructPoint { name } | GeoRef::Axis { name } => mapped_name(name, names),
GeoRef::Plane { plane } => remap_plane(plane, ids, names, bodies, depth + 1)?,
_ => {}
+62 -7
View File
@@ -2,8 +2,9 @@
//! points and sketch entities, resolved on each evaluation, and the axes, points and planes
//! built from them (the CONSTRUCT panel).
//!
//! Face and edge references carry the persistent name of what was picked (see `naming`), so
//! they follow the model when earlier features change; the picked point is the fallback.
//! Face and edge references carry the persistent name of what was picked (see `naming`), and a
//! vertex the names of the edges that meet there, so they follow the model when earlier features
//! change; the picked point is the fallback.
use std::collections::BTreeMap;
@@ -23,8 +24,13 @@ pub enum GeoRef {
Point { p: Vec3 },
/// The design origin.
Origin,
/// A body vertex (the one nearest `at`).
Vertex { at: Vec3 },
/// A body vertex: where the edges named `edges` meet (the end they share), else the one
/// nearest `at`.
Vertex {
at: Vec3,
#[serde(default, skip_serializing_if = "Option::is_none")]
edges: Option<Vec<String>>,
},
/// A body edge: the one named `name`, else the one nearest `at`.
Edge {
at: Vec3,
@@ -170,6 +176,33 @@ fn edge_points(st: &ModelState, at: Vec3, name: &Option<String>) -> Result<Vec<V
}
}
/// The most edge names a vertex reference keeps.
const MAX_VERTEX_EDGES: usize = 16;
/// Where the edges named `names` meet: the end every one of them has, nearest `at` (two edges
/// between the same two vertices share both). `None` when an edge is gone or they share no end.
fn named_vertex(st: &ModelState, at: Vec3, names: &[String]) -> Option<Vec3> {
if names.len() < 2 || names.len() > MAX_VERTEX_EDGES {
return None;
}
let ends: Vec<[Vec3; 2]> = names
.iter()
.map(|n| {
// The edge with that name itself, never the one nearest the old pick.
let on = bodies(st).find_map(|b| crate::naming::find_edge(b, n, at).ok())?.0;
let pts = edge_points(st, on, &None).ok()?;
Some([*pts.first()?, *pts.last()?])
})
.collect::<Option<_>>()?;
let (first, rest) = ends.split_first()?;
let tol = |p: Vec3| 1e-6 * (1.0 + p.len());
first
.iter()
.copied()
.filter(|p| p.is_finite() && rest.iter().all(|e| e.iter().any(|q| q.dist(*p) <= tol(*p))))
.min_by(|p, q| p.dist(at).total_cmp(&q.dist(at)))
}
/// A polyline that is straight: its line.
fn as_line(pts: &[Vec3]) -> Option<(Vec3, Vec3)> {
let (a, b) = (*pts.first()?, *pts.last()?);
@@ -228,7 +261,10 @@ pub(crate) fn point(ctx: &Ctx, g: &GeoRef) -> Result<Vec3> {
match g {
GeoRef::Point { p } => Ok(*p),
GeoRef::Origin => Ok(Vec3::ZERO),
GeoRef::Vertex { at } => {
GeoRef::Vertex { at, edges } => {
if let Some(v) = edges.as_deref().and_then(|names| named_vertex(ctx.st, *at, names)) {
return Ok(v);
}
let mut best: Option<(f64, Vec3)> = None;
for b in bodies(ctx.st) {
let tol = (b.body.size() * 1e-3).max(1e-3);
@@ -553,13 +589,14 @@ impl GeoRef {
pub fn map_points(&mut self, pt: &mut dyn FnMut(&mut Vec3), plane: &mut dyn FnMut(&mut PlaneRef)) {
match self {
GeoRef::Point { p } => pt(p),
GeoRef::Vertex { at } | GeoRef::Edge { at, .. } | GeoRef::Face { at, .. } => pt(at),
GeoRef::Vertex { at, .. } | GeoRef::Edge { at, .. } | GeoRef::Face { at, .. } => pt(at),
GeoRef::Plane { plane: p } => plane(p),
_ => {}
}
}
/// Picked faces and edges get their persistent names (and only those still missing one).
/// Picked faces and edges get their persistent names, and picked vertices the names of the
/// edges that meet there (only those still missing them).
pub fn name_picks(&mut self, st: &ModelState) {
match self {
GeoRef::Face { at, name: name @ None } => {
@@ -574,6 +611,24 @@ impl GeoRef {
}
}
}
GeoRef::Vertex { at, edges: edges @ None } => {
for b in bodies(st) {
let tol = (b.body.size() * 1e-3).max(1e-3);
let m = b.mesh();
let ends_here = |e: &Vec<Vec3>| [e.first(), e.last()].into_iter().flatten().any(|q| q.dist(*at) <= tol);
let mut meet: Vec<String> =
m.edges.iter().zip(crate::naming::edge_names(b)).filter(|(e, n)| !n.is_empty() && ends_here(e)).map(|(_, n)| n).collect();
if meet.is_empty() {
continue;
}
meet.sort();
meet.dedup();
if (2..=MAX_VERTEX_EDGES).contains(&meet.len()) {
*edges = Some(meet);
}
break;
}
}
GeoRef::Plane { plane } => plane.name_picks(st),
_ => {}
}
@@ -0,0 +1,4 @@
//! A construction reference to a body vertex saves the names of the edges that meet there
//! (`edges` beside `at`), so a plane, axis or point on a vertex follows it when an earlier change
//! moves it. Older designs need no transformation: a vertex reference without `edges` is the
//! vertex nearest its point, as before. 0.15.x asks for an update instead of reading `edges`.
+1 -1
View File
@@ -99,7 +99,7 @@ fn geo(s: &Session, v: Option<&Value>, cmd: &str, what: &str) -> Result<GeoRef>
} else if let Some(at) = p3("edge") {
GeoRef::Edge { at, name: None }
} else if let Some(at) = p3("vertex") {
GeoRef::Vertex { at }
GeoRef::Vertex { at, edges: None }
} else if let Some(sk) = v.get("sketch") {
let sketch = sketch_id(s, Some(sk), cmd, what)?;
match (str_(v, "point"), str_(v, "curve")) {
+24
View File
@@ -282,6 +282,30 @@ fn construction_follows_the_model() {
assert!(on_line(o, d, Vec3::new(40.0, 30.0, 50.0)) && on_line(o, d, Vec3::ZERO));
}
/// #568: a plane through three vertices follows them when a parameter moves them. Two of them
/// sit on the box's end at x = 40; the box grows to 100 mm, so the vertices nearest the old
/// picks are the ones at x = 0, on a line with the first: found by the names of the edges that
/// meet there, they are the moved ones.
#[test]
fn a_plane_through_three_vertices_follows_them() {
let mut s = Session::default();
run(&mut s, "parameters.add", json!({"name": "len", "expression": "40 mm"}));
run(&mut s, "solid.box", json!({"length": "len", "width": 30, "height": 20, "body_name": "Box"}));
run(
&mut s,
"construct.plane.three_points",
json!({"a": {"vertex": [0, 0, 20]}, "b": {"vertex": [40, 0, 20]}, "c": {"vertex": [40, 30, 0]}, "name": "T"}),
);
run(&mut s, "parameters.change", json!({"name": "len", "expression": "100 mm"}));
let id = s.doc.features.iter().find(|f| f.name == "T").map(|f| f.id).unwrap_or_else(|| panic!("feature T"));
let error = s.model.result(id).and_then(|r| r.error.clone());
assert_eq!(error, None);
let t = plane(&s, "T");
for p in [Vec3::new(0.0, 0.0, 20.0), Vec3::new(100.0, 0.0, 20.0), Vec3::new(100.0, 30.0, 0.0)] {
assert!(on_plane(&t, p), "{p:?} is off {t:?}");
}
}
/// In a component moved 50 mm along X, picks given in the world land on its geometry, and the
/// axis and point move with the occurrence.
#[test]
+1 -1
View File
@@ -283,10 +283,10 @@ make a legacy face sketch there).
| 56 | #563 | Draft and tapered Extrude drop the faces' sources | `face.extrude` 14, `face.offset_plane` 14, `face.press_pull` 14, `face.sketch` 14 | 3 | oracle/05-taper-extrude `Frustum/F2:start` |
| 48 | #564 | The rim a Shell leaves has no source | `face.extrude` 12, `face.offset_plane` 12, `face.press_pull` 12, `face.sketch` 12 | 8 | demo/pi-case `Body1/F2:top` |
| 18 | #567 | Axis or tangent plane on a drill tip: no face at NaN | `curved.axis` 9, `curved.tangent_plane` 9 | 3 | oracle/18-hole-features `Plate/F5:face:15` |
| 9 | #568 | Construction geometry on model edges, faces and vertices is lost when a parameter moves it | `vertex.plane_three_points+edit` 5, `edge.normal_plane+edit` 2, `face.midplane+edit` 1, `face.shell+edit` 1 | 4 | demo/enclosure_steps `Enclosure/v[F2:top & F4:inner:F2:side:l4 & F4:inner:F3:blend:F2:side:l` |
| 9 | | Axis through a face the kernel sees as a cylinder that the command doesn't (sensor enclosure slot ends; not filed yet) | `curved.axis` 9 | 1 | examples/sensor_enclosure `Lid/F15:blend:F14:side:l2\|F14:side:l3` |
| 7 | | Other failures in the edit round: the hole cut and blend sizes above, and an extrude from the M0 box's top face whose profile isn't found again (not filed yet) | `face.hole+edit` 3, `face.extrude+edit` 1, `face.press_pull+edit` 1, `edge.chamfer+edit` 1 | 4 | corpus/m0-box-fillet-hole `Body1/F2:top` |
| 2 | | A helix edge projected along its axis is a point (expected) | `edge.project` 2 | 1 | oracle/50-sweep-tight-bend `Bend/F3:side:c1#1\|F3:side:c1#2~2` |
| 1 | #566 | Shell after the edit round: a corner whose faces don't meet the same way after the move | `face.shell+edit` 1 | 1 | demo/pi-case `Body1/F4:inner:F2:start#[…]#[F6:side:c2@F7.2]` |
Where the face history stands (`crates/kernel/src/history.rs`, [face attachments](face-attachments.md)):
a flat face takes an attached sketch when the body carries an exact source certificate for it
+5 -5
View File
@@ -1704,7 +1704,7 @@
"Enclosure/v[F2:top & F4:inner:F2:side:l3 & F4:inner:F3:blend:F2:side:l2|F2:side:l3]": "vertex.plane_three_points vertex.point",
"Enclosure/v[F2:top & F4:inner:F2:side:l3 & F4:inner:F3:blend:F2:side:l3|F2:side:l4]": "vertex.plane_three_points vertex.point",
"Enclosure/v[F2:top & F4:inner:F2:side:l4 & F4:inner:F3:blend:F2:side:l1|F2:side:l4]": "vertex.plane_three_points vertex.point",
"Enclosure/v[F2:top & F4:inner:F2:side:l4 & F4:inner:F3:blend:F2:side:l3|F2:side:l4]": "vertex.plane_three_points !vertex.plane_three_points+edit vertex.point",
"Enclosure/v[F2:top & F4:inner:F2:side:l4 & F4:inner:F3:blend:F2:side:l3|F2:side:l4]": "vertex.plane_three_points vertex.plane_three_points+edit vertex.point",
"Enclosure/v[F4:inner:F2:side:l1 & F4:inner:F2:start & F4:inner:F3:blend:F2:side:l1|F2:side:l2]": "vertex.plane_three_points vertex.point",
"Enclosure/v[F4:inner:F2:side:l1 & F4:inner:F2:start & F4:inner:F3:blend:F2:side:l1|F2:side:l4]": "vertex.plane_three_points vertex.point",
"Enclosure/v[F4:inner:F2:side:l2 & F4:inner:F2:start & F4:inner:F3:blend:F2:side:l1|F2:side:l2]": "vertex.plane_three_points vertex.point",
@@ -2082,7 +2082,7 @@
"Body1/v[F11:side:l5 & F11:side:l8 & F2:side:l2]": "vertex.plane_three_points vertex.point",
"Body1/v[F11:side:l5 & F11:side:l8 & F4:inner:F2:side:l2]": "vertex.plane_three_points vertex.point",
"Body1/v[F11:side:l6 & F11:side:l7 & F2:side:l2]": "vertex.plane_three_points vertex.point",
"Body1/v[F11:side:l6 & F11:side:l7 & F4:inner:F2:side:l2]": "vertex.plane_three_points !vertex.plane_three_points+edit vertex.point",
"Body1/v[F11:side:l6 & F11:side:l7 & F4:inner:F2:side:l2]": "vertex.plane_three_points vertex.plane_three_points+edit vertex.point",
"Body1/v[F11:side:l7 & F11:side:l8 & F2:side:l2]": "vertex.plane_three_points vertex.point",
"Body1/v[F11:side:l7 & F11:side:l8 & F4:inner:F2:side:l2]": "vertex.plane_three_points vertex.point",
"Body1/v[F13:side:l5 & F13:side:l6 & F2:start]": "vertex.plane_three_points vertex.point",
@@ -2158,7 +2158,7 @@
"Body1/v[F2:top & F4:inner:F2:side:l2 & F4:inner:F3:blend:F2:side:l1|F2:side:l2]": "vertex.plane_three_points vertex.point",
"Body1/v[F2:top & F4:inner:F2:side:l2 & F4:inner:F3:blend:F2:side:l2|F2:side:l3]": "vertex.plane_three_points vertex.point",
"Body1/v[F2:top & F4:inner:F2:side:l3 & F4:inner:F3:blend:F2:side:l2|F2:side:l3]": "vertex.plane_three_points vertex.point",
"Body1/v[F2:top & F4:inner:F2:side:l3 & F4:inner:F3:blend:F2:side:l3|F2:side:l4]": "vertex.plane_three_points !vertex.plane_three_points+edit vertex.point",
"Body1/v[F2:top & F4:inner:F2:side:l3 & F4:inner:F3:blend:F2:side:l3|F2:side:l4]": "vertex.plane_three_points vertex.plane_three_points+edit vertex.point",
"Body1/v[F2:top & F4:inner:F2:side:l4 & F4:inner:F3:blend:F2:side:l1|F2:side:l4]": "vertex.plane_three_points vertex.point vertex.point+edit",
"Body1/v[F2:top & F4:inner:F2:side:l4 & F4:inner:F3:blend:F2:side:l3|F2:side:l4]": "vertex.plane_three_points vertex.point",
"Body1/v[F4:inner:F2:side:l1 & F4:inner:F2:start#[F4:inner:F2:side:l1,F4:inner:F2:side:l2,F4:inner:F2:side:l3,F4:inner:F2:side:l4,F4:inner:F3:blend:F2:side:l1|F2:side:l2,F4:inner:F3:blend:F2:side:l1|F2:side:l4,F4:inner:F3:blend:F2:side:l2|F2:side:l3,F4:inner:F3:blend:F2:side:l3|F2:side:l4,F6:side:c1]#[F4:inner:F2:side:l1,F4:inner:F2:side:l2,F4:inner:F2:side:l3,F4:inner:F2:side:l4,F4:inner:F3:blend:F2:side:l1|F2:side:l2,F4:inner:F3:blend:F2:side:l1|F2:side:l4,F4:inner:F3:blend:F2:side:l2|F2:side:l3,F4:inner:F3:blend:F2:side:l3|F2:side:l4,F6:side:c1,F6:side:c1@F7.1,F6:side:c1@F7.2,F6:side:c1@F7.3] & F4:inner:F3:blend:F2:side:l1|F2:side:l2]": "vertex.plane_three_points vertex.point",
@@ -3591,7 +3591,7 @@
"Lid/F14:side:l4|F15:blend:F14:side:l1|F14:side:l4": "edge.axis edge.axis+edit edge.normal_plane edge.project",
"Lid/F14:side:l4|F15:blend:F14:side:l3|F14:side:l4": "edge.axis edge.normal_plane edge.project",
"Lid/F14:side:l4|F2:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
"Lid/F14:start": "face.angle_plane face.angle_plane+edit !face.draft_neutral !face.extrude !face.hole face.midplane face.offset_plane !face.press_pull face.shell !face.shell+edit face.sketch face.sketch+edit",
"Lid/F14:start": "face.angle_plane face.angle_plane+edit !face.draft_neutral !face.extrude !face.hole face.midplane face.offset_plane !face.press_pull face.shell face.shell+edit face.sketch face.sketch+edit",
"Lid/F14:start|F18:side:a17": "edge.chamfer edge.fillet edge.normal_plane edge.project",
"Lid/F14:start|F18:side:a18": "edge.chamfer edge.chamfer+edit edge.fillet edge.normal_plane edge.project",
"Lid/F14:start|F18:side:a19": "edge.chamfer edge.fillet edge.normal_plane edge.project edge.project+edit",
@@ -3758,7 +3758,7 @@
"Lid/v[F18:side:a18 & F18:side:l19 & F18:top]": "vertex.plane_three_points vertex.point",
"Lid/v[F18:side:a19 & F18:side:l19 & F18:top]": "vertex.plane_three_points vertex.point",
"Lid/v[F18:side:a19 & F18:side:l20 & F18:top]": "vertex.plane_three_points vertex.point",
"Lid/v[F18:side:a20 & F18:side:l17 & F18:top]": "vertex.plane_three_points !vertex.plane_three_points+edit vertex.point",
"Lid/v[F18:side:a20 & F18:side:l17 & F18:top]": "vertex.plane_three_points vertex.plane_three_points+edit vertex.point",
"Lid/v[F18:side:a20 & F18:side:l20 & F18:top]": "vertex.plane_three_points vertex.point",
"design": "load"
},