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);
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
//! Construction axes, points and planes (SOLID ▸ CONSTRUCT), one test per type, against a
|
||||
//! 40 × 30 × 20 box at the origin, a cylinder, a sphere and a torus.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_geom::{Plane, Vec3};
|
||||
|
||||
use crate::Session;
|
||||
use crate::view::{construction_axes, construction_planes, construction_points};
|
||||
|
||||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||||
match s.execute(id, &p) {
|
||||
Ok(v) => v,
|
||||
Err(e) => panic!("{id} {p}: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn part() -> Session {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20, "body_name": "Box"}));
|
||||
run(&mut s, "solid.cylinder", json!({"base": [60, 0, 0], "radius": 5, "height": 10, "body_name": "Cyl"}));
|
||||
run(&mut s, "solid.sphere", json!({"center": [0, 0, 60], "radius": 5, "body_name": "Ball"}));
|
||||
run(&mut s, "solid.torus", json!({"center": [0, 60, 0], "major": 10, "minor": 2, "body_name": "Ring"}));
|
||||
s
|
||||
}
|
||||
|
||||
fn near(a: Vec3, b: Vec3) -> bool {
|
||||
a.dist(b) < 1e-3
|
||||
}
|
||||
|
||||
fn parallel(a: Vec3, b: Vec3) -> bool {
|
||||
a.normalized().zip(b.normalized()).is_some_and(|(a, b)| a.cross(b).len() < 1e-6)
|
||||
}
|
||||
|
||||
fn axis(s: &Session, name: &str) -> (Vec3, Vec3) {
|
||||
construction_axes(s).into_iter().find(|a| a.1 == name).map(|a| (a.2, a.3)).unwrap_or_else(|| panic!("no axis {name}"))
|
||||
}
|
||||
|
||||
fn point(s: &Session, name: &str) -> Vec3 {
|
||||
construction_points(s).into_iter().find(|p| p.1 == name).map(|p| p.2).unwrap_or_else(|| panic!("no point {name}"))
|
||||
}
|
||||
|
||||
fn plane(s: &Session, name: &str) -> Plane {
|
||||
construction_planes(s).into_iter().find(|p| p.1 == name).map(|p| p.2).unwrap_or_else(|| panic!("no plane {name}"))
|
||||
}
|
||||
|
||||
/// The line through `o` along `d` contains `p`.
|
||||
fn on_line(o: Vec3, d: Vec3, p: Vec3) -> bool {
|
||||
let d = d.normalized().unwrap_or(Vec3::Z);
|
||||
(p - o - d * (p - o).dot(d)).len() < 1e-3
|
||||
}
|
||||
|
||||
fn on_plane(pl: &Plane, p: Vec3) -> bool {
|
||||
(p - pl.origin).dot(pl.normal()).abs() < 1e-3
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axis_through_cylinder_cone_or_torus() {
|
||||
let mut s = part();
|
||||
run(&mut s, "construct.axis.through_cylinder", json!({"face": {"face": [65, 0, 5]}, "name": "A"}));
|
||||
let (o, d) = axis(&s, "A");
|
||||
assert!(parallel(d, Vec3::Z) && on_line(o, d, Vec3::new(60.0, 0.0, 0.0)), "{o:?} {d:?}");
|
||||
run(&mut s, "construct.axis.through_cylinder", json!({"face": {"face": [12, 60, 0]}, "name": "T"}));
|
||||
let (o, d) = axis(&s, "T");
|
||||
assert!(parallel(d, Vec3::Z) && on_line(o, d, Vec3::new(0.0, 60.0, 0.0)), "{o:?} {d:?}");
|
||||
// A flat face has no axis.
|
||||
assert!(s.execute("construct.axis.through_cylinder", &json!({"face": {"face": [20, 15, 20]}})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axis_perpendicular_to_a_face_at_a_point() {
|
||||
let mut s = part();
|
||||
run(&mut s, "construct.axis.normal_to_face", json!({"face": {"face": [10, 10, 20]}, "point": {"point": [10, 10, 25]}, "name": "N"}));
|
||||
let (o, d) = axis(&s, "N");
|
||||
assert!(near(o, Vec3::new(10.0, 10.0, 20.0)) && parallel(d, Vec3::Z), "{o:?} {d:?}");
|
||||
// On the cylinder: radial through the point.
|
||||
run(&mut s, "construct.axis.normal_to_face", json!({"face": {"face": [65, 0, 5]}, "point": {"point": [70, 0, 5]}, "name": "R"}));
|
||||
let (o, d) = axis(&s, "R");
|
||||
assert!(near(o, Vec3::new(65.0, 0.0, 5.0)) && parallel(d, Vec3::X), "{o:?} {d:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axis_through_two_planes() {
|
||||
let mut s = part();
|
||||
run(&mut s, "construct.axis.two_planes", json!({"a": {"plane": "XZ"}, "b": {"face": [40, 15, 10]}, "name": "L"}));
|
||||
let (o, d) = axis(&s, "L");
|
||||
assert!(parallel(d, Vec3::Z) && on_line(o, d, Vec3::new(40.0, 0.0, 0.0)), "{o:?} {d:?}");
|
||||
assert!(s.execute("construct.axis.two_planes", &json!({"a": "XY", "b": {"face": [20, 15, 20]}})).is_err(), "parallel");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axis_through_two_points() {
|
||||
let mut s = part();
|
||||
run(&mut s, "construct.axis.two_points", json!({"a": {"vertex": [0.5, 0.5, 0.5]}, "b": {"vertex": [39, 29, 19]}, "name": "D"}));
|
||||
let (o, d) = axis(&s, "D");
|
||||
assert!(on_line(o, d, Vec3::ZERO) && parallel(d, Vec3::new(40.0, 30.0, 20.0)), "{o:?} {d:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axis_through_an_edge() {
|
||||
let mut s = part();
|
||||
run(&mut s, "construct.axis.edge", json!({"edge": {"edge": [20, 0, 0]}, "name": "E"}));
|
||||
let (o, d) = axis(&s, "E");
|
||||
assert!(parallel(d, Vec3::X) && on_line(o, d, Vec3::ZERO), "{o:?} {d:?}");
|
||||
// A circular edge is not straight.
|
||||
assert!(s.execute("construct.axis.edge", &json!({"edge": {"edge": [65, 0, 10]}})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_at_a_vertex() {
|
||||
let mut s = part();
|
||||
run(&mut s, "construct.point.vertex", json!({"point": {"vertex": [39, 29, 19]}, "name": "P"}));
|
||||
assert!(near(point(&s, "P"), Vec3::new(40.0, 30.0, 20.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_through_two_edges() {
|
||||
let mut s = part();
|
||||
run(&mut s, "construct.point.two_edges", json!({"a": {"edge": [20, 0, 0]}, "b": {"edge": [40, 15, 0]}, "name": "P"}));
|
||||
assert!(near(point(&s, "P"), Vec3::new(40.0, 0.0, 0.0)), "{:?}", point(&s, "P"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_through_three_planes() {
|
||||
let mut s = part();
|
||||
run(
|
||||
&mut s,
|
||||
"construct.point.three_planes",
|
||||
json!({"a": {"face": [40, 15, 10]}, "b": {"face": [20, 30, 10]}, "c": {"face": [20, 15, 20]}, "name": "P"}),
|
||||
);
|
||||
assert!(near(point(&s, "P"), Vec3::new(40.0, 30.0, 20.0)), "{:?}", point(&s, "P"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_at_the_centre_of_a_circle_sphere_or_torus() {
|
||||
let mut s = part();
|
||||
run(&mut s, "construct.point.center", json!({"of": {"edge": [65, 0, 10]}, "name": "C"}));
|
||||
assert!(near(point(&s, "C"), Vec3::new(60.0, 0.0, 10.0)), "{:?}", point(&s, "C"));
|
||||
run(&mut s, "construct.point.center", json!({"of": {"face": [5, 0, 60]}, "name": "S"}));
|
||||
assert!(near(point(&s, "S"), Vec3::new(0.0, 0.0, 60.0)), "{:?}", point(&s, "S"));
|
||||
run(&mut s, "construct.point.center", json!({"of": {"face": [12, 60, 0]}, "name": "T"}));
|
||||
assert!(near(point(&s, "T"), Vec3::new(0.0, 60.0, 0.0)), "{:?}", point(&s, "T"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_at_edge_and_plane() {
|
||||
let mut s = part();
|
||||
run(
|
||||
&mut s,
|
||||
"construct.point.edge_plane",
|
||||
json!({"edge": {"edge": [20, 0, 0]}, "plane": {"plane": {"origin": [12, 0, 0], "normal": [1, 0, 0]}}, "name": "P"}),
|
||||
);
|
||||
assert!(near(point(&s, "P"), Vec3::new(12.0, 0.0, 0.0)), "{:?}", point(&s, "P"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_along_a_path() {
|
||||
let mut s = part();
|
||||
run(&mut s, "construct.point.along_path", json!({"path": {"edge": [20, 0, 0]}, "t": 0.25, "name": "P"}));
|
||||
let p = point(&s, "P");
|
||||
assert!(near(p, Vec3::new(10.0, 0.0, 0.0)) || near(p, Vec3::new(30.0, 0.0, 0.0)), "{p:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn midplane_between_parallel_and_meeting_faces() {
|
||||
let mut s = part();
|
||||
run(&mut s, "construct.plane.midplane", json!({"a": "XY", "b": {"face": [20, 15, 20]}, "name": "M"}));
|
||||
let m = plane(&s, "M");
|
||||
assert!(parallel(m.normal(), Vec3::Z) && on_plane(&m, Vec3::new(5.0, 5.0, 10.0)), "{m:?}");
|
||||
// Two sides meeting at the corner (40, 30): the diagonal plane through it.
|
||||
run(&mut s, "construct.plane.midplane", json!({"a": {"face": [40, 15, 10]}, "b": {"face": [20, 30, 10]}, "name": "Q"}));
|
||||
let q = plane(&s, "Q");
|
||||
assert!(on_plane(&q, Vec3::new(40.0, 30.0, 0.0)) && on_plane(&q, Vec3::new(41.0, 31.0, 5.0)), "{q:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plane_through_two_edges() {
|
||||
let mut s = part();
|
||||
run(&mut s, "construct.plane.two_edges", json!({"a": {"edge": [20, 0, 0]}, "b": {"edge": [20, 30, 20]}, "name": "E"}));
|
||||
let e = plane(&s, "E");
|
||||
// The front-bottom and back-top edges: the slanted plane through both.
|
||||
assert!(on_plane(&e, Vec3::new(5.0, 0.0, 0.0)) && on_plane(&e, Vec3::new(5.0, 30.0, 20.0)), "{e:?}");
|
||||
assert!(s.execute("construct.plane.two_edges", &json!({"a": {"edge": [20, 0, 0]}, "b": {"edge": [40, 15, 20]}})).is_err(), "skew");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plane_through_three_points() {
|
||||
let mut s = part();
|
||||
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"}),
|
||||
);
|
||||
let t = plane(&s, "T");
|
||||
assert!(on_plane(&t, Vec3::new(0.0, 0.0, 20.0)) && on_plane(&t, Vec3::new(40.0, 30.0, 0.0)) && on_plane(&t, Vec3::new(0.0, 30.0, 0.0)), "{t:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plane_tangent_to_a_cylinder() {
|
||||
let mut s = part();
|
||||
run(&mut s, "construct.plane.tangent", json!({"face": {"face": [65, 0, 5]}, "name": "T"}));
|
||||
let t = plane(&s, "T");
|
||||
assert!(parallel(t.normal(), Vec3::X) && on_plane(&t, Vec3::new(65.0, 3.0, 9.0)), "{t:?}");
|
||||
// And to the sphere, where the pick lies over it.
|
||||
run(&mut s, "construct.plane.tangent", json!({"face": {"face": [0, 0, 65]}, "name": "U"}));
|
||||
let u = plane(&s, "U");
|
||||
assert!(parallel(u.normal(), Vec3::Z) && on_plane(&u, Vec3::new(3.0, 3.0, 65.0)), "{u:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plane_along_a_path() {
|
||||
let mut s = part();
|
||||
run(&mut s, "construct.plane.along_path", json!({"path": {"edge": [20, 0, 0]}, "t": 0.5, "name": "P"}));
|
||||
let p = plane(&s, "P");
|
||||
assert!(parallel(p.normal(), Vec3::X) && on_plane(&p, Vec3::new(20.0, 7.0, 3.0)), "{p:?}");
|
||||
// A sketch can be placed on it.
|
||||
run(&mut s, "sketch.create", json!({"plane": "P", "name": "OnPath"}));
|
||||
}
|
||||
|
||||
/// Built from model geometry, they follow when the model changes: the box gets taller.
|
||||
#[test]
|
||||
fn construction_follows_the_model() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.add", json!({"name": "h", "expression": "20 mm"}));
|
||||
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": "h", "body_name": "Box"}));
|
||||
run(
|
||||
&mut s,
|
||||
"construct.point.three_planes",
|
||||
json!({"a": {"face": [40, 15, 10]}, "b": {"face": [20, 30, 10]}, "c": {"face": [20, 15, 20]}, "name": "Corner"}),
|
||||
);
|
||||
run(&mut s, "construct.axis.edge", json!({"edge": {"edge": [20, 30, 20]}, "name": "TopBack"}));
|
||||
run(&mut s, "construct.plane.midplane", json!({"a": "XY", "b": {"face": [20, 15, 20]}, "name": "Mid"}));
|
||||
run(&mut s, "parameters.change", json!({"name": "h", "expression": "50 mm"}));
|
||||
assert!(near(point(&s, "Corner"), Vec3::new(40.0, 30.0, 50.0)), "{:?}", point(&s, "Corner"));
|
||||
let (o, d) = axis(&s, "TopBack");
|
||||
assert!(parallel(d, Vec3::X) && on_line(o, d, Vec3::new(0.0, 30.0, 50.0)), "{o:?} {d:?}");
|
||||
assert!(on_plane(&plane(&s, "Mid"), Vec3::new(1.0, 1.0, 25.0)));
|
||||
// Construction from construction: an axis through two construction points.
|
||||
run(&mut s, "construct.point.vertex", json!({"point": {"vertex": [0, 0, 0]}, "name": "Origin"}));
|
||||
run(&mut s, "construct.axis.two_points", json!({"a": {"construct_point": "Origin"}, "b": {"construct_point": "Corner"}, "name": "Diag"}));
|
||||
let (o, d) = axis(&s, "Diag");
|
||||
assert!(on_line(o, d, Vec3::new(40.0, 30.0, 50.0)) && on_line(o, d, Vec3::ZERO));
|
||||
}
|
||||
|
||||
/// 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]
|
||||
fn construction_lives_in_the_component_frame() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "component.create", json!({"name": "Part"}));
|
||||
run(&mut s, "solid.cylinder", json!({"radius": 5, "height": 10, "body_name": "Pin"}));
|
||||
run(&mut s, "occurrence.move", json!({"occurrence": "Part:1", "translate": [50, 0, 0]}));
|
||||
run(&mut s, "construct.axis.through_cylinder", json!({"face": {"face": [55, 0, 5]}, "name": "PinAxis"}));
|
||||
run(&mut s, "construct.point.center", json!({"of": {"edge": [55, 0, 10]}, "name": "PinTop"}));
|
||||
let (o, d) = axis(&s, "PinAxis");
|
||||
assert!(parallel(d, Vec3::Z) && on_line(o, d, Vec3::new(50.0, 0.0, 0.0)), "{o:?} {d:?}");
|
||||
assert!(near(point(&s, "PinTop"), Vec3::new(50.0, 0.0, 10.0)), "{:?}", point(&s, "PinTop"));
|
||||
run(&mut s, "occurrence.move", json!({"occurrence": "Part:1", "translate": [0, 20, 0]}));
|
||||
assert!(near(point(&s, "PinTop"), Vec3::new(50.0, 20.0, 10.0)), "{:?}", point(&s, "PinTop"));
|
||||
}
|
||||
@@ -425,6 +425,8 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod construct_tests;
|
||||
#[cfg(test)]
|
||||
mod delete_face_tests;
|
||||
#[cfg(test)]
|
||||
|
||||
+50
-22
@@ -181,30 +181,58 @@ pub fn home_camera(s: &Session) -> Camera {
|
||||
c
|
||||
}
|
||||
|
||||
/// Construction planes of the timeline: (feature id, name, plane).
|
||||
/// Construction planes of the timeline, as evaluated: (feature id, name, plane).
|
||||
pub fn construction_planes(s: &Session) -> Vec<(u64, String, Plane)> {
|
||||
use solvecraft_doc::FeatureKind;
|
||||
let (vals, _) = s.doc.param_values();
|
||||
s.doc
|
||||
.features
|
||||
.iter()
|
||||
.filter_map(|f| match &f.kind {
|
||||
FeatureKind::ConstructionPlane { plane } if !f.suppressed => s.doc.resolve_plane(&vals, plane, 0).ok().map(|p| {
|
||||
// Shown where its component is placed.
|
||||
let m = s.doc.component_transform(f.component);
|
||||
let p = if solvecraft_doc::is_identity(&m) {
|
||||
p
|
||||
} else {
|
||||
Plane::new(
|
||||
solvecraft_doc::apply_point(&m, p.origin),
|
||||
solvecraft_doc::apply_vector(&m, p.x),
|
||||
solvecraft_doc::apply_vector(&m, p.y),
|
||||
)
|
||||
.unwrap_or(p)
|
||||
};
|
||||
(f.id, f.name.clone(), p)
|
||||
}),
|
||||
constructs(s)
|
||||
.into_iter()
|
||||
.filter_map(|(id, name, g)| match g {
|
||||
solvecraft_doc::construct::ConstructGeom::Plane(p) => Some((id, name, p)),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Construction axes, as evaluated: (feature id, name, origin, unit direction).
|
||||
pub fn construction_axes(s: &Session) -> Vec<(u64, String, Vec3, Vec3)> {
|
||||
constructs(s)
|
||||
.into_iter()
|
||||
.filter_map(|(id, name, g)| match g {
|
||||
solvecraft_doc::construct::ConstructGeom::Axis { origin, dir } => Some((id, name, origin, dir)),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Construction points, as evaluated: (feature id, name, position).
|
||||
pub fn construction_points(s: &Session) -> Vec<(u64, String, Vec3)> {
|
||||
constructs(s)
|
||||
.into_iter()
|
||||
.filter_map(|(id, name, g)| match g {
|
||||
solvecraft_doc::construct::ConstructGeom::Point(p) => Some((id, name, p)),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Construction geometry of unsuppressed features, shown where its component is placed.
|
||||
fn constructs(s: &Session) -> Vec<(u64, String, solvecraft_doc::construct::ConstructGeom)> {
|
||||
use solvecraft_doc::construct::ConstructGeom;
|
||||
let st = s.model.state();
|
||||
st.construct
|
||||
.iter()
|
||||
.filter_map(|c| {
|
||||
let f = s.doc.feature(c.feature).filter(|f| !f.suppressed)?;
|
||||
let m = s.doc.component_transform(f.component);
|
||||
if solvecraft_doc::is_identity(&m) {
|
||||
return Some((c.feature, c.name.clone(), c.geom.clone()));
|
||||
}
|
||||
let (pt, v) = (|p: Vec3| solvecraft_doc::apply_point(&m, p), |d: Vec3| solvecraft_doc::apply_vector(&m, d));
|
||||
let g = match &c.geom {
|
||||
ConstructGeom::Plane(p) => ConstructGeom::Plane(Plane::new(pt(p.origin), v(p.x), v(p.y)).unwrap_or(*p)),
|
||||
ConstructGeom::Axis { origin, dir } => ConstructGeom::Axis { origin: pt(*origin), dir: v(*dir) },
|
||||
ConstructGeom::Point(p) => ConstructGeom::Point(pt(*p)),
|
||||
};
|
||||
Some((c.feature, c.name.clone(), g))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
Reference in new issue
Block a user