Faces by name in the face commands; model.faces gives a point on each face (#1243)

This commit is contained in:
NOWN-DEV authored and GitHub committed 2026-10-14 20:18:13 -04:00
1 parent 2cab09f65b
commit 5c6f70cc00
17 files changed
+480 -29

No files matched your search

+17 -1
View File
@@ -6,7 +6,7 @@ use serde_json::{Value, json};
use solidcraft_doc::{EmbossFace, apply_point, mat_inverse, mat_mul};
use solidcraft_geom::{Mesh, Vec2, Vec3};
use crate::params::bad;
use crate::params::{bad, vec3};
use crate::{Result, Session};
fn point_triangle_dist(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> f64 {
@@ -57,6 +57,22 @@ pub(super) fn face_is_planar(s: &Session, p: Vec3) -> Option<bool> {
Some(ns.iter().all(|n| n.dot(n0) > 1.0 - 1e-6))
}
/// A face given to a command: a point `[x, y, z]` on it (or `{point}`), or its name as
/// `model.faces` lists it, looked up on `body` (any body without one). A name becomes a point on
/// that face, which the feature keeps like a picked one, so it also keeps the face's name.
/// The point is in the frame of the command's other picks: the component's own when
/// they were mapped into it, else the world's.
pub(super) fn face_ref(s: &Session, v: &Value, body: Option<&str>, cmd: &str) -> Result<Vec3> {
if let Some(name) = v.as_str() {
if name.is_empty() || name.len() > 512 {
return Err(bad(cmd, "a face name is 1…512 characters"));
}
let st = if crate::frames::mapped() { s.model.state() } else { s.world_state() };
return solidcraft_doc::naming::named_face_point(&st, body, name).map_err(|why| bad(cmd, why));
}
vec3(v).or_else(|| v.get("point").and_then(vec3)).ok_or_else(|| bad(cmd, "a face is a point [x, y, z] on it, or its name from model.faces"))
}
/// The body face under a point: (mesh, face index, distance, body).
fn face_at(s: &Session, p: Vec3) -> Option<(std::sync::Arc<Mesh>, u32, f64, String)> {
face_at_in(s, p, None).ok().flatten()
+13 -13
View File
@@ -67,11 +67,11 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("face.delete", "Delete Face", delete_face)
.at("SOLID", "MODIFY")
.icon("delete")
.params("faces: [[x,y,z] points on the faces]; body?; heal?: bool (default true: the neighbouring planes close the gap, as when a rounded edge, hole or boss is taken off; false: the gap stays open and the faces left are a surface body)"),
.params("faces: [[x,y,z] points on the faces, or face names from model.faces]; body?; heal?: bool (default true: the neighbouring planes close the gap, as when a rounded edge, hole or boss is taken off; false: the gap stays open and the faces left are a surface body)"),
CommandSpec::new("solid.offset_face", "Offset Face", offset_face)
.at("SOLID", "MODIFY")
.icon("offset_face")
.params("faces: [[x,y,z] points on planar faces]; distance: expr (positive: outward); body?"),
.params("faces: [[x,y,z] points on planar faces, or face names from model.faces]; distance: expr (positive: outward); body?"),
CommandSpec::new("solid.thread", "Thread", thread)
.at("SOLID", "CREATE")
.icon("thread")
@@ -79,11 +79,11 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("solid.shell", "Shell", shell)
.at("SOLID", "MODIFY")
.icon("shell")
.params("faces: [[x,y,z] points on the faces to remove]; thickness: expr; direction?: inside|outside|both; tangent_chain?: bool (also open faces joined smoothly to the picked ones); body?"),
.params("faces: [[x,y,z] points on the faces to remove, or face names from model.faces]; thickness: expr; direction?: inside|outside|both; tangent_chain?: bool (also open faces joined smoothly to the picked ones); body?"),
CommandSpec::new("solid.draft", "Draft", draft)
.at("SOLID", "MODIFY")
.icon("draft")
.params("faces: [[x,y,z]]; angle: expr; neutral: XY|XZ|YZ|plane name|{origin, normal}; pull?: [x,y,z] (default: neutral plane normal); body?"),
.params("faces: [[x,y,z] points, or face names from model.faces]; angle: expr; neutral: XY|XZ|YZ|plane name|{origin, normal}; pull?: [x,y,z] (default: neutral plane normal); body?"),
CommandSpec::new("construct.plane.offset", "Offset Plane", plane_offset)
.at("SOLID", "CONSTRUCT")
.icon("plane")
@@ -96,7 +96,7 @@ pub static COMMANDS: &[CommandSpec] = &[
.at("SOLID", "MODIFY")
.icon("split")
.params(
"faces: [[x,y,z] points on the faces to split]; body?; and the splitting tool: plane: XY|XZ|YZ|plane name|{origin, normal} — or tool: {body, point} (a face of a body, its surface extended) — or sketch: id or name (its curves, along the sketch's normal)",
"faces: [[x,y,z] points on the faces to split, or face names from model.faces]; body?; and the splitting tool: plane: XY|XZ|YZ|plane name|{origin, normal} — or tool: {body, point} (a face of a body, its surface extended) — or sketch: id or name (its curves, along the sketch's normal)",
),
CommandSpec::new("solid.split_body", "Split Body", split_body)
.at("SOLID", "MODIFY")
@@ -1323,7 +1323,7 @@ fn plane_angle(s: &mut Session, p: &Value) -> Result<Value> {
fn split_face(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "solid.split_face";
let faces = face_points(p, cmd)?;
let faces = face_points(s, p, cmd)?;
let body = str_(p, "body").map(str::to_string);
let xy = || solidcraft_doc::PlaneRef::Origin { name: "XY".into() };
let kind = if let Some(t) = p.get("tool").filter(|t| !t.is_null()) {
@@ -1542,17 +1542,17 @@ fn sketch_hole_points(s: &Session, sketch: u64) -> Vec<String> {
ss.sketch.points.iter().enumerate().filter(|(i, p)| p.id != "origin" && !ends.contains(i)).map(|(_, p)| p.id.clone()).collect()
}
fn face_points(p: &Value, cmd: &str) -> Result<Vec<Vec3>> {
let a = p.get("faces").and_then(Value::as_array).ok_or_else(|| bad(cmd, "`faces` must be a list of points"))?;
fn face_points(s: &Session, p: &Value, cmd: &str) -> Result<Vec<Vec3>> {
let a = p.get("faces").and_then(Value::as_array).ok_or_else(|| bad(cmd, "`faces` must be a list of points or face names"))?;
if a.is_empty() || a.len() > 1000 {
return Err(bad(cmd, "select 1…1000 faces"));
}
a.iter().map(|v| vec3(v).or_else(|| v.get("point").and_then(vec3)).ok_or_else(|| bad(cmd, "a face is a point [x, y, z] on it"))).collect()
a.iter().map(|v| super::face::face_ref(s, v, str_(p, "body"), cmd)).collect()
}
fn shell(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "solid.shell";
let faces = face_points(p, cmd)?;
let faces = face_points(s, p, cmd)?;
let thickness = req_expr(cmd, p, "thickness")?;
check_expr(s, &thickness, Kind::Length, cmd, "thickness")?;
let direction = str_(p, "direction").map(str::to_ascii_lowercase).unwrap_or_default();
@@ -1574,7 +1574,7 @@ fn shell(s: &mut Session, p: &Value) -> Result<Value> {
fn draft(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "solid.draft";
let faces = face_points(p, cmd)?;
let faces = face_points(s, p, cmd)?;
let angle = req_expr(cmd, p, "angle")?;
check_expr(s, &angle, Kind::Angle, cmd, "angle")?;
let neutral = plane_param(s, p.get("neutral"), cmd)?;
@@ -1715,14 +1715,14 @@ fn scale(s: &mut Session, p: &Value) -> Result<Value> {
}
fn delete_face(s: &mut Session, p: &Value) -> Result<Value> {
let faces = face_points(p, "face.delete")?;
let faces = face_points(s, p, "face.delete")?;
let heal = bool_(p, "heal").unwrap_or(true);
add_feature(s, p, FeatureKind::DeleteFace { faces, body: str_(p, "body").map(str::to_string), heal })
}
fn offset_face(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "solid.offset_face";
let faces = face_points(p, cmd)?;
let faces = face_points(s, p, cmd)?;
let distance = req_expr(cmd, p, "distance")?;
check_expr(s, &distance, Kind::Length, cmd, "distance")?;
add_feature(s, p, FeatureKind::OffsetFace { faces, distance, body: str_(p, "body").map(str::to_string) })
+5 -5
View File
@@ -35,7 +35,7 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("solid.replace_face", "Replace Face", replace_face)
.at("SOLID", "MODIFY")
.icon("offset_face")
.params("faces: [[x,y,z] points on planar faces]; target: plane (XY|XZ|YZ|construction plane|{origin, normal}) | target_face: [x,y,z] on a parallel planar face; body?"),
.params("faces: [[x,y,z] points on planar faces, or face names from model.faces]; target: plane (XY|XZ|YZ|construction plane|{origin, normal}) | target_face: [x,y,z] on a parallel planar face; body?"),
CommandSpec::new("solid.align", "Align", align)
.at("SOLID", "MODIFY")
.icon("move")
@@ -316,17 +316,17 @@ fn infer_emboss_selection(
}
Ok(chosen)
}
fn face_points(p: &Value, k: &str, cmd: &str) -> Result<Vec<Vec3>> {
let a = p.get(k).and_then(Value::as_array).ok_or_else(|| bad(cmd, format!("`{k}` must list points [x, y, z]")))?;
fn face_points(s: &Session, p: &Value, k: &str, cmd: &str) -> Result<Vec<Vec3>> {
let a = p.get(k).and_then(Value::as_array).ok_or_else(|| bad(cmd, format!("`{k}` must list points [x, y, z] or face names")))?;
if a.is_empty() || a.len() > 1000 {
return Err(bad(cmd, format!("`{k}` must list 1…1000 points")));
}
a.iter().map(|v| vec3(v).ok_or_else(|| bad(cmd, format!("`{k}` must contain [x, y, z] points")))).collect()
a.iter().map(|v| super::face::face_ref(s, v, str_(p, "body"), cmd)).collect()
}
fn replace_face(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "solid.replace_face";
let faces = face_points(p, "faces", cmd)?;
let faces = face_points(s, p, "faces", cmd)?;
for f in &faces {
if super::face::face_normal(s, *f).is_none() {
return Err(bad(cmd, "not supported yet: replacing a face that is not planar"));
+14 -3
View File
@@ -17,7 +17,10 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("document.inspect", "Inspect Design", inspect).noundo().params("measure?: bool (include body measurements)"),
CommandSpec::new("sketch.inspect", "Inspect Sketch", sketch_inspect).noundo().params("sketch?: id|name (default active)"),
CommandSpec::new("model.edges", "List Edges", model_edges).noundo().params("body: name"),
CommandSpec::new("model.faces", "List Faces", model_faces).noundo().params("body: name"),
CommandSpec::new("model.faces", "List Faces", model_faces).noundo().params(
"body: name → per face: index, name (what face.delete, sheet.convert `face`, solid.shell … take in place of a point), exact, area, centroid (not always on the face), \
plane_normal, point (a point on the face that picks it), cylinder? ({origin, axis, radius} of a cylindrical face)",
),
CommandSpec::new("model.threads", "List Threads", model_threads).noundo(),
CommandSpec::new("material.assign", "Physical Material", physical_material)
.at("SOLID", "MODIFY")
@@ -283,13 +286,21 @@ fn model_faces(s: &mut Session, p: &Value) -> Result<Value> {
let b = body_of(&st, p, "model.faces")?;
let names = solidcraft_doc::naming::face_names(b);
let tol = (b.body.size() * 2e-3).max(1e-3);
let spots = solidcraft_doc::naming::face_spots(b);
let faces: Vec<Value> = b
.body
.faces(tol)?
.iter()
.map(|f| {
json!({"index": f.index, "name": names.get(f.index), "exact": solidcraft_doc::naming::face_name_exact(b, f.index),
"area": f.area, "centroid": f.centroid, "plane_normal": f.plane_normal})
let spot = spots.get(f.index).copied().flatten();
let mut v = json!({"index": f.index, "name": names.get(f.index), "exact": solidcraft_doc::naming::face_name_exact(b, f.index),
"area": f.area, "centroid": f.centroid, "plane_normal": f.plane_normal, "point": spot.map(|x| x.point)});
if let (Some(solidcraft_kernel::FaceSurface::Cylinder { origin, axis, radius }), Some(o)) =
(spot.and_then(|x| x.surface), v.as_object_mut())
{
o.insert("cylinder".into(), json!({"origin": origin, "axis": axis, "radius": radius}));
}
v
})
.collect();
Ok(json!({"body": b.name, "faces": faces}))
+3 -2
View File
@@ -38,7 +38,7 @@ pub static COMMANDS: &[CommandSpec] = &[
.at("SHEET METAL", "CREATE")
.icon("convert_sheet")
.params(
"body; face: [x,y,z] on its large flat face (a plate of even thickness); rule?; \
"body; face: [x,y,z] on its large flat face (a plate of even thickness), or that face's name from model.faces; rule?; \
bends?: [[x,y,z] on sharp edges] (a solid or a shelled body: `face` is the fixed face and the flat faces reached from it \
across these edges become the sheet; a shell keeps its thickness, a solid takes the rule's inward; \
edges between two of those faces that are no bends are cut open with the rule's gap)",
@@ -297,7 +297,8 @@ fn convert(s: &mut Session, p: &Value) -> Result<Value> {
if s.model.state().body(&body).is_none() {
return Err(bad(cmd, format!("no body `{body}`")));
}
let face = p.get("face").and_then(vec3).ok_or_else(|| bad(cmd, "`face` must be a point [x, y, z] on the plate's face"))?;
let face = p.get("face").ok_or_else(|| bad(cmd, "`face` must be a point [x, y, z] on the plate's face, or its name from model.faces"))?;
let face = super::face::face_ref(s, face, Some(&body), cmd)?;
// With bend edges, a solid or shelled body; without, a plate.
let bends = match p.get("bends") {
None | Some(Value::Null) => Vec::new(),