Construction axes, points and planes from model geometry (SOLID ▸ CONSTRUCT): axes through a cylinder/cone/torus, perpendicular to a face at a point, through two planes, two points or an edge; points at a vertex, through two edges, three planes, at the centre of a circle/sphere/torus, at an edge and a plane, along a path; midplane, planes through two edges or three points, tangent to a face, along a path. References (doc::construct::GeoRef) keep picked faces and edges by persistent name and resolve on each evaluation (they follow the model); results live in ModelState.construct, in the component's frame; view::construction_axes/points/planes for the viewport; kernel::face_surface for a face's analytic surface
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
68271137a5
commit
846bb112e2
21 files changed
+1400
-46
No files matched your search
@@ -358,7 +358,7 @@ fn redefine_sketch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let plane = super::sketch::plane_ref(s, p, cmd)?;
|
||||
let (vals, _) = s.doc.param_values();
|
||||
s.doc.resolve_plane(&vals, &plane, 0)?;
|
||||
s.doc.resolve_plane_in(&vals, &plane, 0, Some(&s.model.state()))?;
|
||||
if let Some(FeatureKind::Sketch { plane: slot, .. }) = s.doc_mut().feature_mut(id).map(|f| &mut f.kind) {
|
||||
*slot = plane;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
//! SOLID ▸ CONSTRUCT: construction axes, points and the planes built from model geometry.
|
||||
//!
|
||||
//! References are given as `{face: [x,y,z]}`, `{edge: [x,y,z]}`, `{vertex: [x,y,z]}`,
|
||||
//! `{point: [x,y,z]}`, `{plane: XY|name|{origin, normal}}`, `{axis: X|Y|Z|name}`,
|
||||
//! `{construct_point: name}`, `{sketch, point}` or `{sketch, curve}`, or as a tagged
|
||||
//! `GeoRef` (`{"type": "edge", "at": …}`). Picked faces and edges are stored with their
|
||||
//! persistent names.
|
||||
|
||||
use serde_json::Value;
|
||||
use solvecraft_doc::construct::{AxisDef, GeoRef, PointDef};
|
||||
use solvecraft_doc::{FeatureKind, PlaneRef};
|
||||
|
||||
use super::CommandSpec;
|
||||
use super::features::{add_feature, check_expr, plane_param, sketch_id};
|
||||
use crate::params::{bad, req_expr, str_, vec3};
|
||||
use crate::{Result, Session};
|
||||
|
||||
const REF: &str =
|
||||
"{face|edge|vertex|point: [x,y,z]} | {plane: XY|name|{origin, normal}} | {axis: X|Y|Z|name} | {construct_point: name} | {sketch, point|curve}";
|
||||
|
||||
pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("construct.axis.through_cylinder", "Axis Through Cylinder/Cone/Torus", axis_face)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("axis")
|
||||
.params("face: ref (a cylinder, cone or torus face, or a circular edge); name?"),
|
||||
CommandSpec::new("construct.axis.normal_to_face", "Axis Perpendicular To Face", axis_normal)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("axis")
|
||||
.params("face: ref; point: ref (the axis goes through it, square to the face); name?"),
|
||||
CommandSpec::new("construct.axis.two_planes", "Axis Through Two Planes", axis_planes)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("axis")
|
||||
.params("a, b: plane or planar face refs; name?"),
|
||||
CommandSpec::new("construct.axis.two_points", "Axis Through Two Points", axis_points)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("axis")
|
||||
.params("a, b: point refs (vertices, sketch points, points); name?"),
|
||||
CommandSpec::new("construct.axis.edge", "Axis Through Edge", axis_edge)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("axis")
|
||||
.params("edge: ref (a straight edge or sketch line); name?"),
|
||||
CommandSpec::new("construct.point.vertex", "Point At Vertex", point_at)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("point")
|
||||
.params("point: ref (vertex, sketch point, point); name?"),
|
||||
CommandSpec::new("construct.point.two_edges", "Point Through Two Edges", point_edges)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("point")
|
||||
.params("a, b: edge or line refs (where they meet, or come closest); name?"),
|
||||
CommandSpec::new("construct.point.three_planes", "Point Through Three Planes", point_planes)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("point")
|
||||
.params("a, b, c: plane or planar face refs; name?"),
|
||||
CommandSpec::new("construct.point.center", "Point At Center Of Circle/Sphere/Torus", point_center)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("point")
|
||||
.params("of: ref (circular edge, sketch circle or arc, sphere or torus face); name?"),
|
||||
CommandSpec::new("construct.point.edge_plane", "Point At Edge And Plane", point_edge_plane)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("point")
|
||||
.params("edge: edge or line ref; plane: plane or planar face ref; name?"),
|
||||
CommandSpec::new("construct.point.along_path", "Point Along Path", point_along)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("point")
|
||||
.params("path: edge or sketch curve ref; t: expr, 0…1 along it (default 0.5); name?"),
|
||||
CommandSpec::new("construct.plane.midplane", "Midplane", plane_mid)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("plane")
|
||||
.params("a, b: plane or planar face refs (halfway between; bisecting when they meet at an angle); name?"),
|
||||
CommandSpec::new("construct.plane.two_edges", "Plane Through Two Edges", plane_edges)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("plane")
|
||||
.params("a, b: straight edge or line refs in one plane; name?"),
|
||||
CommandSpec::new("construct.plane.three_points", "Plane Through Three Points", plane_points)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("plane")
|
||||
.params("a, b, c: point refs; name?"),
|
||||
CommandSpec::new("construct.plane.tangent", "Tangent Plane", plane_tangent)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("plane")
|
||||
.params("face: ref (cylinder, cone, sphere or torus); at?: [x,y,z] where it touches (default: the face pick); name?"),
|
||||
CommandSpec::new("construct.plane.along_path", "Plane Along Path", plane_along)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("plane")
|
||||
.params("path: edge or sketch curve ref; t: expr, 0…1 along it (default 0.5); name? The plane is square to the path there"),
|
||||
];
|
||||
|
||||
/// A reference from its JSON form.
|
||||
fn geo(s: &Session, v: Option<&Value>, cmd: &str, what: &str) -> Result<GeoRef> {
|
||||
let v = v.ok_or_else(|| bad(cmd, format!("`{what}` is required: {REF}")))?;
|
||||
let p3 = |k: &str| v.get(k).and_then(vec3);
|
||||
let mut g = if v.get("type").is_some() {
|
||||
serde_json::from_value::<GeoRef>(v.clone()).map_err(|e| bad(cmd, format!("`{what}`: {e}")))?
|
||||
} else if let Some(at) = p3("face") {
|
||||
GeoRef::Face { at, name: None }
|
||||
} else if let Some(at) = p3("edge") {
|
||||
GeoRef::Edge { at, name: None }
|
||||
} else if let Some(at) = p3("vertex") {
|
||||
GeoRef::Vertex { at }
|
||||
} else if let Some(sk) = v.get("sketch") {
|
||||
let sketch = sketch_id(s, Some(sk), cmd, what)?;
|
||||
match (str_(v, "point"), str_(v, "curve")) {
|
||||
(Some(point), _) => GeoRef::SketchPoint { sketch, point: point.to_string() },
|
||||
(None, Some(curve)) => GeoRef::SketchCurve { sketch, curve: curve.to_string() },
|
||||
_ => return Err(bad(cmd, format!("`{what}`: a sketch reference needs `point` or `curve`"))),
|
||||
}
|
||||
} else if let Some(p) = p3("point") {
|
||||
GeoRef::Point { p }
|
||||
} else if v.get("plane").is_some() {
|
||||
GeoRef::Plane { plane: plane_param(s, v.get("plane"), cmd)? }
|
||||
} else if let Some(a) = str_(v, "axis") {
|
||||
if ["x", "y", "z"].contains(&a.to_ascii_lowercase().as_str()) {
|
||||
GeoRef::World { axis: a.to_ascii_uppercase() }
|
||||
} else {
|
||||
match s.doc.find_feature(a).map(|f| &f.kind) {
|
||||
Some(FeatureKind::ConstructionAxis { .. }) => GeoRef::Axis { name: a.to_string() },
|
||||
_ => return Err(bad(cmd, format!("no axis `{a}`"))),
|
||||
}
|
||||
}
|
||||
} else if let Some(n) = str_(v, "construct_point") {
|
||||
match s.doc.find_feature(n).map(|f| &f.kind) {
|
||||
Some(FeatureKind::ConstructionPoint { .. }) => GeoRef::ConstructPoint { name: n.to_string() },
|
||||
_ => return Err(bad(cmd, format!("no construction point `{n}`"))),
|
||||
}
|
||||
} else if v.as_str().is_some() {
|
||||
// A bare string: a plane.
|
||||
GeoRef::Plane { plane: plane_param(s, Some(v), cmd)? }
|
||||
} else {
|
||||
return Err(bad(cmd, format!("`{what}` must be {REF}")));
|
||||
};
|
||||
g.name_picks(&s.model.state());
|
||||
Ok(g)
|
||||
}
|
||||
|
||||
fn t_param(s: &Session, p: &Value, cmd: &str) -> Result<String> {
|
||||
let t = if p.get("t").is_some() { req_expr(cmd, p, "t")? } else { "0.5".to_string() };
|
||||
check_expr(s, &t, solvecraft_doc::expr::Kind::Unitless, cmd, "t")?;
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
fn axis(s: &mut Session, p: &Value, def: AxisDef) -> Result<Value> {
|
||||
add_feature(s, p, FeatureKind::ConstructionAxis { def })
|
||||
}
|
||||
|
||||
fn point(s: &mut Session, p: &Value, def: PointDef) -> Result<Value> {
|
||||
add_feature(s, p, FeatureKind::ConstructionPoint { def })
|
||||
}
|
||||
|
||||
fn plane(s: &mut Session, p: &Value, plane: PlaneRef) -> Result<Value> {
|
||||
add_feature(s, p, FeatureKind::ConstructionPlane { plane })
|
||||
}
|
||||
|
||||
fn axis_face(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let face = geo(s, p.get("face"), "construct.axis.through_cylinder", "face")?;
|
||||
axis(s, p, AxisDef::ThroughFace { face })
|
||||
}
|
||||
|
||||
fn axis_normal(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "construct.axis.normal_to_face";
|
||||
let face = geo(s, p.get("face"), cmd, "face")?;
|
||||
let point = geo(s, p.get("point"), cmd, "point")?;
|
||||
axis(s, p, AxisDef::NormalToFace { face, point })
|
||||
}
|
||||
|
||||
fn axis_planes(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "construct.axis.two_planes";
|
||||
let (a, b) = (geo(s, p.get("a"), cmd, "a")?, geo(s, p.get("b"), cmd, "b")?);
|
||||
axis(s, p, AxisDef::TwoPlanes { a, b })
|
||||
}
|
||||
|
||||
fn axis_points(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "construct.axis.two_points";
|
||||
let (a, b) = (geo(s, p.get("a"), cmd, "a")?, geo(s, p.get("b"), cmd, "b")?);
|
||||
axis(s, p, AxisDef::TwoPoints { a, b })
|
||||
}
|
||||
|
||||
fn axis_edge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let edge = geo(s, p.get("edge"), "construct.axis.edge", "edge")?;
|
||||
axis(s, p, AxisDef::Edge { edge })
|
||||
}
|
||||
|
||||
fn point_at(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let at = geo(s, p.get("point"), "construct.point.vertex", "point")?;
|
||||
point(s, p, PointDef::At { point: at })
|
||||
}
|
||||
|
||||
fn point_edges(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "construct.point.two_edges";
|
||||
let (a, b) = (geo(s, p.get("a"), cmd, "a")?, geo(s, p.get("b"), cmd, "b")?);
|
||||
point(s, p, PointDef::TwoEdges { a, b })
|
||||
}
|
||||
|
||||
fn point_planes(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "construct.point.three_planes";
|
||||
let (a, b, c) = (geo(s, p.get("a"), cmd, "a")?, geo(s, p.get("b"), cmd, "b")?, geo(s, p.get("c"), cmd, "c")?);
|
||||
point(s, p, PointDef::ThreePlanes { a, b, c })
|
||||
}
|
||||
|
||||
fn point_center(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let of = geo(s, p.get("of"), "construct.point.center", "of")?;
|
||||
point(s, p, PointDef::Center { of })
|
||||
}
|
||||
|
||||
fn point_edge_plane(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "construct.point.edge_plane";
|
||||
let (edge, pl) = (geo(s, p.get("edge"), cmd, "edge")?, geo(s, p.get("plane"), cmd, "plane")?);
|
||||
point(s, p, PointDef::EdgeAndPlane { edge, plane: pl })
|
||||
}
|
||||
|
||||
fn point_along(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "construct.point.along_path";
|
||||
let path = geo(s, p.get("path"), cmd, "path")?;
|
||||
let t = t_param(s, p, cmd)?;
|
||||
point(s, p, PointDef::AlongPath { path, t })
|
||||
}
|
||||
|
||||
fn plane_mid(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "construct.plane.midplane";
|
||||
let (a, b) = (geo(s, p.get("a"), cmd, "a")?, geo(s, p.get("b"), cmd, "b")?);
|
||||
plane(s, p, PlaneRef::Midplane { a: Box::new(a), b: Box::new(b) })
|
||||
}
|
||||
|
||||
fn plane_edges(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "construct.plane.two_edges";
|
||||
let (a, b) = (geo(s, p.get("a"), cmd, "a")?, geo(s, p.get("b"), cmd, "b")?);
|
||||
plane(s, p, PlaneRef::TwoEdges { a: Box::new(a), b: Box::new(b) })
|
||||
}
|
||||
|
||||
fn plane_points(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "construct.plane.three_points";
|
||||
let (a, b, c) = (geo(s, p.get("a"), cmd, "a")?, geo(s, p.get("b"), cmd, "b")?, geo(s, p.get("c"), cmd, "c")?);
|
||||
plane(s, p, PlaneRef::ThreePoints { a: Box::new(a), b: Box::new(b), c: Box::new(c) })
|
||||
}
|
||||
|
||||
fn plane_tangent(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "construct.plane.tangent";
|
||||
let face = geo(s, p.get("face"), cmd, "face")?;
|
||||
let at = match (p.get("at").and_then(vec3), &face) {
|
||||
(Some(a), _) => a,
|
||||
(None, GeoRef::Face { at, .. }) => *at,
|
||||
_ => return Err(bad(cmd, "`at` is required: where the plane touches the face")),
|
||||
};
|
||||
plane(s, p, PlaneRef::Tangent { face: Box::new(face), at })
|
||||
}
|
||||
|
||||
fn plane_along(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "construct.plane.along_path";
|
||||
let path = geo(s, p.get("path"), cmd, "path")?;
|
||||
let t = t_param(s, p, cmd)?;
|
||||
plane(s, p, PlaneRef::AlongPath { path: Box::new(path), t })
|
||||
}
|
||||
@@ -797,7 +797,7 @@ fn mirror(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
/// A plane reference from a parameter: an origin plane, a construction plane name, or an
|
||||
/// explicit plane `{origin, x_dir, y_dir}` / `{origin, normal}`.
|
||||
pub fn plane_param(s: &Session, v: Option<&Value>, cmd: &str) -> Result<solvecraft_doc::PlaneRef> {
|
||||
pub(super) fn plane_param(s: &Session, v: Option<&Value>, cmd: &str) -> Result<solvecraft_doc::PlaneRef> {
|
||||
use solvecraft_doc::PlaneRef;
|
||||
match v {
|
||||
Some(Value::String(n)) if solvecraft_geom::Plane::named(n).is_some() => Ok(PlaneRef::Origin { name: n.to_ascii_uppercase() }),
|
||||
@@ -1080,7 +1080,7 @@ fn draft(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
check_expr(s, &angle, Kind::Angle, cmd, "angle")?;
|
||||
let neutral = plane_param(s, p.get("neutral"), cmd)?;
|
||||
let (vals, _) = s.doc.param_values();
|
||||
let pl = s.doc.resolve_plane(&vals, &neutral, 0)?;
|
||||
let pl = s.doc.resolve_plane_in(&vals, &neutral, 0, Some(&s.model.state()))?;
|
||||
let pull = p.get("pull").and_then(vec3).unwrap_or_else(|| pl.normal());
|
||||
add_feature(s, p, FeatureKind::Draft { faces, angle, neutral, pull, body: str_(p, "body").map(str::to_string) })
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
mod browser;
|
||||
mod clipboard;
|
||||
mod component;
|
||||
mod construct;
|
||||
mod delete;
|
||||
mod edit;
|
||||
mod face;
|
||||
@@ -145,6 +146,7 @@ pub fn command_specs() -> Vec<&'static CommandSpec> {
|
||||
v.extend(sketch_project::COMMANDS.iter());
|
||||
v.extend(features::COMMANDS.iter());
|
||||
v.extend(features_more::COMMANDS.iter());
|
||||
v.extend(construct::COMMANDS.iter());
|
||||
v.extend(sheet::COMMANDS.iter());
|
||||
v.extend(plastic::COMMANDS.iter());
|
||||
v.extend(component::COMMANDS.iter());
|
||||
|
||||
@@ -367,7 +367,7 @@ fn create_sketch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let plane = plane_ref(s, p, cmd)?;
|
||||
// Validate the plane resolves.
|
||||
let (vals, _) = s.doc.param_values();
|
||||
s.doc.resolve_plane(&vals, &plane, 0)?;
|
||||
s.doc.resolve_plane_in(&vals, &plane, 0, Some(&s.model.state()))?;
|
||||
let name = str_(p, "name");
|
||||
let mut kind = FeatureKind::Sketch { plane, sketch: Sketch::new() };
|
||||
super::component::to_active_frame(s, &mut kind);
|
||||
|
||||
@@ -48,7 +48,7 @@ fn make(s: &mut Session, p: &Value, cmd: &str, decal: bool) -> Result<Value> {
|
||||
let (format, pixels) = image_info(&bytes).ok_or_else(|| bad(cmd, "only PNG and JPEG images can be inserted"))?;
|
||||
let plane = super::sketch::plane_ref(s, p, cmd)?;
|
||||
let (vals, _) = s.doc.param_values();
|
||||
let pl = s.doc.resolve_plane(&vals, &plane, 0)?;
|
||||
let pl = s.doc.resolve_plane_in(&vals, &plane, 0, Some(&s.model.state()))?;
|
||||
// A decal sits where the face was picked.
|
||||
let at = p.get("at").or_else(|| p.get("plane").and_then(|v| v.get("face"))).and_then(crate::params::vec3).map(|q| pl.to_local(q));
|
||||
let center = p.get("center").and_then(vec2).or(at).unwrap_or(Vec2::ZERO);
|
||||
|
||||
Reference in new issue
Block a user