mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
* claim: #10171 * fix(select): resolve faces and edges from a point or index
This commit is contained in:
committed by
GitHub
parent
c39f44e190
commit
d4144ddf9b
@@ -88,7 +88,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
),
|
||||
CommandSpec::new("select.set", "Select", select_set).icon("select")
|
||||
.noundo()
|
||||
.params("items: [{type: body|edge|face|feature|sketch_curve|sketch_point|profile, …}], add?: bool"),
|
||||
.params("items: [{type: body|edge|face|feature|sketch_curve|sketch_point|profile, …}], add?: bool; face/edge: body plus point [x,y,z] in world mm or index (both also accepted); a point uses Measure's pick tolerance, an index supplies a point on the item; invalid partial picks leave the selection unchanged"),
|
||||
CommandSpec::new("select.filter", "Selection filter", select_filter).icon("select").noundo().doc_only()
|
||||
.params("mode: body|face|component → validated UI pick granularity; headless echo only, no document edits or undo"),
|
||||
CommandSpec::new("select.clear", "Clear Selection", select_clear).icon("select").noundo(),
|
||||
@@ -1225,6 +1225,65 @@ fn edit_feature(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"feature": id, "recomputed": s.model.last_recomputed, "restored": s.model.last_restored}))
|
||||
}
|
||||
|
||||
/// Complete a partial script pick without changing the stored selection type or reinterpreting
|
||||
/// an existing index-and-point pair. Unlike Measure, selection keeps the exact face/edge index:
|
||||
/// it must not merge a cylinder's patches or a circular rim's arcs into a different item.
|
||||
fn selection_item(s: &Session, value: Value) -> Result<Sel> {
|
||||
use solidcraft_geom::Vec3;
|
||||
|
||||
let kind = value.get("type").and_then(Value::as_str);
|
||||
if !matches!(kind, Some("face" | "edge")) || (value.get("index").is_some() && value.get("point").is_some()) {
|
||||
return serde_json::from_value(value).map_err(|e| bad("select.set", e.to_string()));
|
||||
}
|
||||
let face = kind == Some("face");
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Pick {
|
||||
body: String,
|
||||
index: Option<usize>,
|
||||
point: Option<Vec3>,
|
||||
}
|
||||
let Pick { body, index, point } = serde_json::from_value(value).map_err(|e| bad("select.set", e.to_string()))?;
|
||||
let state = s.world_state();
|
||||
let b = state.body(&body).ok_or_else(|| bad("select.set", format!("no body `{body}`")))?;
|
||||
let missing = || bad("select.set", format!("no {} of `{body}` at the given index or point", if face { "face" } else { "edge" }));
|
||||
let (index, point) = match (index, point) {
|
||||
(Some(index), None) => {
|
||||
let point = if face {
|
||||
solidcraft_doc::naming::face_spot(b, index).map(|spot| spot.point)
|
||||
} else {
|
||||
let mesh = b.mesh();
|
||||
mesh.edges.get(index).and_then(|edge| match edge.as_slice() {
|
||||
[a, b] => Some(a.lerp(*b, 0.5)),
|
||||
// Use a sample on the curve, not the midpoint of a chord inside it.
|
||||
points if points.len() > 2 => points.get(points.len() / 2).copied(),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
(index, point.filter(|p| p.is_finite()).ok_or_else(missing)?)
|
||||
}
|
||||
(None, Some(point)) if point.is_finite() => {
|
||||
let index = if face {
|
||||
solidcraft_doc::appearance::face_index_at(b, point)
|
||||
} else {
|
||||
let mesh = b.mesh();
|
||||
mesh.edges
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, edge)| {
|
||||
let distance =
|
||||
edge.windows(2).filter_map(|w| Some(point.dist_to_segment(*w.first()?, *w.get(1)?))).fold(f64::INFINITY, f64::min);
|
||||
(distance <= solidcraft_doc::appearance::pick_tolerance(b)).then_some((index, distance))
|
||||
})
|
||||
.min_by(|(_, a), (_, b)| a.total_cmp(b))
|
||||
.map(|(index, _)| index)
|
||||
};
|
||||
(index.ok_or_else(missing)?, point)
|
||||
}
|
||||
_ => return Err(bad("select.set", "a face or edge needs an index or a finite point")),
|
||||
};
|
||||
Ok(if face { Sel::Face { body, index, point } } else { Sel::Edge { body, index, point } })
|
||||
}
|
||||
|
||||
fn select_set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let items = p.get("items").and_then(Value::as_array).cloned().unwrap_or_default();
|
||||
if items.len() > 10_000 {
|
||||
@@ -1232,7 +1291,7 @@ fn select_set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let mut sel: Vec<Sel> = Vec::new();
|
||||
for it in items {
|
||||
let one: Sel = serde_json::from_value(it).map_err(|e| bad("select.set", e.to_string()))?;
|
||||
let one = selection_item(s, it)?;
|
||||
match &one {
|
||||
Sel::Occurrence { path } => {
|
||||
s.occurrence_bodies(path, true).map_err(|e| bad("select.set", e.to_string()))?;
|
||||
|
||||
@@ -8,6 +8,107 @@ fn run(s: &mut Session, id: &str, params: Value) -> Value {
|
||||
s.execute(id, ¶ms).unwrap_or_else(|e| panic!("{id} {params}: {e}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_geometry_resolves_points_and_indices_without_changing_history() {
|
||||
use solidcraft_geom::Vec3;
|
||||
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"body_name":"Block", "corner":[20,30,40], "length":10, "width":10, "height":10}));
|
||||
let before = (s.doc.to_json(), s.undo.len());
|
||||
for (kind, point) in [("face", Vec3::new(25.0, 35.0, 50.0)), ("edge", Vec3::new(25.0, 30.0, 50.0))] {
|
||||
assert_eq!(run(&mut s, "select.set", json!({"items":[{"type":kind,"body":"Block","point":point}]}))["selected"], 1);
|
||||
let index = match &s.selection[0] {
|
||||
Sel::Face { index, point: actual, .. } | Sel::Edge { index, point: actual, .. } => {
|
||||
assert_eq!(*actual, point);
|
||||
*index
|
||||
}
|
||||
other => panic!("unexpected selection: {other:?}"),
|
||||
};
|
||||
let measured = s.measure_items(&s.selection);
|
||||
let (field, expected) = if kind == "face" { ("area_mm2", 100.0) } else { ("length_mm", 10.0) };
|
||||
assert!((measured["items"][0][field].as_f64().unwrap() - expected).abs() < 1e-9, "{measured}");
|
||||
run(&mut s, "select.set", json!({"items":[{"type":kind,"body":"Block","index":index}]}));
|
||||
let complete = serde_json::to_value(&s.selection[0]).unwrap();
|
||||
assert_eq!(complete["index"], index);
|
||||
let mut point_only = complete.clone();
|
||||
point_only.as_object_mut().unwrap().remove("index");
|
||||
run(&mut s, "select.set", json!({"items":[point_only]}));
|
||||
assert_eq!(serde_json::to_value(&s.selection[0]).unwrap(), complete);
|
||||
}
|
||||
assert_eq!((s.doc.to_json(), s.undo.len()), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_geometry_curved_picks_round_trip_and_add_without_duplicates() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.cylinder", json!({"body_name":"Round", "radius":5, "height":10}));
|
||||
for (kind, point) in [("face", [3, 4, 5]), ("edge", [3, 4, 10])] {
|
||||
run(&mut s, "select.set", json!({"items":[{"type":kind,"body":"Round","point":point}]}));
|
||||
let index = serde_json::to_value(&s.selection[0]).unwrap()["index"].clone();
|
||||
run(&mut s, "select.set", json!({"items":[{"type":kind,"body":"Round","index":index}]}));
|
||||
let complete = serde_json::to_value(&s.selection[0]).unwrap();
|
||||
let pick: solidcraft_geom::Vec3 = serde_json::from_value(complete["point"].clone()).unwrap();
|
||||
assert!((pick.x.hypot(pick.y) - 5.0).abs() < 1e-6, "the inferred point lies on the cylinder: {complete}");
|
||||
let mut partial = complete.clone();
|
||||
partial.as_object_mut().unwrap().remove("index");
|
||||
run(&mut s, "select.set", json!({"items":[partial, complete],"add":true}));
|
||||
assert_eq!(s.selection.len(), 1);
|
||||
assert_eq!(serde_json::to_value(&s.selection[0]).unwrap(), complete);
|
||||
let measured = s.measure_items(&s.selection);
|
||||
assert!((measured["items"][0]["diameter"].as_f64().unwrap() - 10.0).abs() < 1e-6, "{measured}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_geometry_uses_the_placed_body_in_world_coordinates() {
|
||||
let mut s = Session::default();
|
||||
let created = run(&mut s, "component.create", json!({"name":"Part"}));
|
||||
run(&mut s, "solid.box", json!({"body_name":"Block", "length":2, "width":3, "height":4}));
|
||||
let second = run(&mut s, "occurrence.copy", json!({"occurrence":created["occurrence"]}))["occurrence"].as_u64().unwrap();
|
||||
run(&mut s, "occurrence.move", json!({"occurrence":second,"translate":[10,20,0],"axis":[0,0,1],"angle":"90 deg","capture":false}));
|
||||
let name = s.occurrence_placement(&[second]).unwrap().bodies[0].world.clone();
|
||||
for (kind, point) in [("face", [8.5, 21.0, 4.0]), ("edge", [8.5, 20.0, 4.0])] {
|
||||
run(&mut s, "select.set", json!({"items":[{"type":kind,"body":name,"point":point}]}));
|
||||
let complete = serde_json::to_value(&s.selection[0]).unwrap();
|
||||
assert_eq!(complete["body"], name);
|
||||
run(&mut s, "select.set", json!({"items":[{"type":kind,"body":name,"index":complete["index"]}]}));
|
||||
let inferred = serde_json::to_value(&s.selection[0]).unwrap();
|
||||
let pick: solidcraft_geom::Vec3 = serde_json::from_value(inferred["point"].clone()).unwrap();
|
||||
assert!((pick.z - 4.0).abs() < 1e-9 && (7.0..=10.0).contains(&pick.x) && (20.0..=22.0).contains(&pick.y), "{inferred}");
|
||||
assert!(s.execute("select.set", &json!({"items":[{"type":kind,"body":name,"point":[1,1,4]}]})).is_err());
|
||||
assert_eq!(serde_json::to_value(&s.selection[0]).unwrap(), inferred);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_geometry_invalid_partial_picks_leave_selection_and_revision_unchanged() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"body_name":"Block", "length":10, "width":10, "height":10}));
|
||||
run(&mut s, "select.set", json!({"items":[{"type":"body","name":"Block"}]}));
|
||||
let before = (s.selection.clone(), s.revision, s.undo.len());
|
||||
for kind in ["face", "edge"] {
|
||||
for fields in [
|
||||
json!({}),
|
||||
json!({"index":999999}),
|
||||
json!({"index":-1}),
|
||||
json!({"index":"0"}),
|
||||
json!({"point":[500,500,500]}),
|
||||
json!({"point":[1,2]}),
|
||||
json!({"point":[1e308,1e308,1e308]}),
|
||||
] {
|
||||
let mut item = json!({"type":kind,"body":"Block"});
|
||||
item.as_object_mut().unwrap().extend(fields.as_object().unwrap().clone());
|
||||
for add in [false, true] {
|
||||
let result = s.execute("select.set", &json!({"items":[{"type":"face","body":"Block","point":[5,5,10]},item],"add":add}));
|
||||
assert!(result.is_err(), "accepted {item}");
|
||||
assert_eq!((s.selection.clone(), s.revision, s.undo.len()), before, "{item}");
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(s.execute("select.set", &json!({"items":[{"type":"face","body":"Missing","point":[5,5,10]}]})).is_err());
|
||||
assert_eq!((s.selection, s.revision, s.undo.len()), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_playback_is_a_typed_display_request_without_history_edits() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -343,6 +343,15 @@ the top level. With two items:
|
||||
|
||||
Earlier fields are unchanged, so older scripts keep working.
|
||||
|
||||
To select geometry in the view, use `execute` `select.set` with
|
||||
`{"items":[{"type":"face","body":"Body1","point":[5,5,10]}]}` (or `"type":"edge"`).
|
||||
The point is in world millimetres and uses the same proximity tolerance as Measure. An
|
||||
`index` can replace the point; the command then supplies a point on that face or edge,
|
||||
including curved faces and placed components. Existing selections with both fields keep
|
||||
their index and point. Missing geometry or an invalid partial pick leaves the entire
|
||||
selection unchanged, including with `"add":true`. `inspect_design` reports the completed
|
||||
selections with both fields.
|
||||
|
||||
To change a feature after it is made, read its definition with `execute` `timeline.inspect`
|
||||
(`{feature}`: id or name) and change fields with `timeline.edit` (`{feature, set}`; e.g.
|
||||
`{"extent": {"distance": "30"}}`). The fields per feature type are listed in the book's
|
||||
|
||||
Reference in New Issue
Block a user