Drawing connects to existing points: a new point landing exactly on an existing point (the origin too) gets a coincident constraint in the engine, for every drawing command and every caller (MCP, scripts, clicks), in the same undo step; connect: false opts out. Re-tying an existing coincident returns it ("existing": true) instead of failing; the UI's after-the-fact tie_snaps is gone
This commit is contained in:
1 parent
448ea769eb
commit
5d5ff44db2
8 files changed
+141
-66
No files matched your search
@@ -9,6 +9,13 @@ use super::{CommandSpec, in_sketch};
|
|||||||
use crate::params::{bad, bool_, expr, num, req_vec2, str_, string_list, vec2, vec3};
|
use crate::params::{bad, bool_, expr, num, req_vec2, str_, string_list, vec2, vec3};
|
||||||
use crate::{EngineError, Result, Session};
|
use crate::{EngineError, Result, Session};
|
||||||
|
|
||||||
|
/// The `connect` parameter every drawing command takes (see `connect_points`).
|
||||||
|
macro_rules! connect_doc {
|
||||||
|
() => {
|
||||||
|
"; connect?: bool (default true: a new point landing exactly on an existing point, the origin too, is made coincident with it)"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub static COMMANDS: &[CommandSpec] = &[
|
pub static COMMANDS: &[CommandSpec] = &[
|
||||||
CommandSpec::new("sketch.create", "Create Sketch", create_sketch)
|
CommandSpec::new("sketch.create", "Create Sketch", create_sketch)
|
||||||
.at("SOLID", "CREATE")
|
.at("SOLID", "CREATE")
|
||||||
@@ -21,71 +28,71 @@ pub static COMMANDS: &[CommandSpec] = &[
|
|||||||
.icon("line")
|
.icon("line")
|
||||||
.key("L")
|
.key("L")
|
||||||
.enabled(in_sketch)
|
.enabled(in_sketch)
|
||||||
.params("points: [[x,y] | \"l1.end\" | {at, ref}…], closed?: bool, construction?: bool, ids?: [..], infer?: bool (add horizontal/vertical)"),
|
.params(concat!("points: [[x,y] | \"l1.end\" | {at, ref}…], closed?: bool, construction?: bool, ids?: [..], infer?: bool (add horizontal/vertical)", connect_doc!())),
|
||||||
CommandSpec::new("sketch.rectangle.two_point", "2-Point Rectangle", rect_two_point)
|
CommandSpec::new("sketch.rectangle.two_point", "2-Point Rectangle", rect_two_point)
|
||||||
.at("SKETCH", "CREATE")
|
.at("SKETCH", "CREATE")
|
||||||
.icon("rect")
|
.icon("rect")
|
||||||
.key("R")
|
.key("R")
|
||||||
.enabled(in_sketch)
|
.enabled(in_sketch)
|
||||||
.params("p0, p1: opposite corners [x,y]; construction?. Returns sides {bottom, right, top, left} (line ids) and corners {bottom_left, bottom_right, top_right, top_left} (point ids)"),
|
.params(concat!("p0, p1: opposite corners [x,y]; construction?. Returns sides {bottom, right, top, left} (line ids) and corners {bottom_left, bottom_right, top_right, top_left} (point ids)", connect_doc!())),
|
||||||
CommandSpec::new("sketch.rectangle.three_point", "3-Point Rectangle", rect_three_point)
|
CommandSpec::new("sketch.rectangle.three_point", "3-Point Rectangle", rect_three_point)
|
||||||
.at("SKETCH", "CREATE")
|
.at("SKETCH", "CREATE")
|
||||||
.icon("rect3")
|
.icon("rect3")
|
||||||
.enabled(in_sketch)
|
.enabled(in_sketch)
|
||||||
.params("p0, p1: first edge; p2: point on the opposite edge"),
|
.params(concat!("p0, p1: first edge; p2: point on the opposite edge", connect_doc!())),
|
||||||
CommandSpec::new("sketch.rectangle.center", "Center Rectangle", rect_center)
|
CommandSpec::new("sketch.rectangle.center", "Center Rectangle", rect_center)
|
||||||
.at("SKETCH", "CREATE")
|
.at("SKETCH", "CREATE")
|
||||||
.icon("rect_center")
|
.icon("rect_center")
|
||||||
.enabled(in_sketch)
|
.enabled(in_sketch)
|
||||||
.params("center, corner. Returns sides {bottom, right, top, left} (line ids) and corners {bottom_left, bottom_right, top_right, top_left} (point ids)"),
|
.params(concat!("center, corner. Returns sides {bottom, right, top, left} (line ids) and corners {bottom_left, bottom_right, top_right, top_left} (point ids)", connect_doc!())),
|
||||||
CommandSpec::new("sketch.circle.center", "Center Diameter Circle", circle_center)
|
CommandSpec::new("sketch.circle.center", "Center Diameter Circle", circle_center)
|
||||||
.at("SKETCH", "CREATE")
|
.at("SKETCH", "CREATE")
|
||||||
.icon("circle")
|
.icon("circle")
|
||||||
.key("C")
|
.key("C")
|
||||||
.enabled(in_sketch)
|
.enabled(in_sketch)
|
||||||
.params("center: [x,y] or point ref, radius | diameter: number"),
|
.params(concat!("center: [x,y] or point ref, radius | diameter: number", connect_doc!())),
|
||||||
CommandSpec::new("sketch.circle.two_point", "2-Point Circle", circle_two)
|
CommandSpec::new("sketch.circle.two_point", "2-Point Circle", circle_two)
|
||||||
.at("SKETCH", "CREATE")
|
.at("SKETCH", "CREATE")
|
||||||
.icon("circle2")
|
.icon("circle2")
|
||||||
.enabled(in_sketch)
|
.enabled(in_sketch)
|
||||||
.params("p0, p1: diameter ends"),
|
.params(concat!("p0, p1: diameter ends", connect_doc!())),
|
||||||
CommandSpec::new("sketch.circle.three_point", "3-Point Circle", circle_three)
|
CommandSpec::new("sketch.circle.three_point", "3-Point Circle", circle_three)
|
||||||
.at("SKETCH", "CREATE")
|
.at("SKETCH", "CREATE")
|
||||||
.icon("circle3")
|
.icon("circle3")
|
||||||
.enabled(in_sketch)
|
.enabled(in_sketch)
|
||||||
.params("p0, p1, p2"),
|
.params(concat!("p0, p1, p2", connect_doc!())),
|
||||||
CommandSpec::new("sketch.arc.three_point", "3-Point Arc", arc_three).at("SKETCH", "CREATE").icon("arc3").enabled(in_sketch).params("start, end, through"),
|
CommandSpec::new("sketch.arc.three_point", "3-Point Arc", arc_three).at("SKETCH", "CREATE").icon("arc3").enabled(in_sketch).params(concat!("start, end, through", connect_doc!())),
|
||||||
CommandSpec::new("sketch.arc.center_point", "Center Point Arc", arc_center)
|
CommandSpec::new("sketch.arc.center_point", "Center Point Arc", arc_center)
|
||||||
.at("SKETCH", "CREATE")
|
.at("SKETCH", "CREATE")
|
||||||
.icon("arc_center")
|
.icon("arc_center")
|
||||||
.enabled(in_sketch)
|
.enabled(in_sketch)
|
||||||
.params("center, start, end (counter-clockwise from start) | center, start, sweep: signed degrees"),
|
.params(concat!("center, start, end (counter-clockwise from start) | center, start, sweep: signed degrees", connect_doc!())),
|
||||||
CommandSpec::new("sketch.polygon.inscribed", "Inscribed Polygon", polygon_inscribed)
|
CommandSpec::new("sketch.polygon.inscribed", "Inscribed Polygon", polygon_inscribed)
|
||||||
.at("SKETCH", "CREATE")
|
.at("SKETCH", "CREATE")
|
||||||
.icon("polygon")
|
.icon("polygon")
|
||||||
.enabled(in_sketch)
|
.enabled(in_sketch)
|
||||||
.params("center, radius (to vertices), sides, angle?: deg"),
|
.params(concat!("center, radius (to vertices), sides, angle?: deg", connect_doc!())),
|
||||||
CommandSpec::new("sketch.polygon.circumscribed", "Circumscribed Polygon", polygon_circumscribed)
|
CommandSpec::new("sketch.polygon.circumscribed", "Circumscribed Polygon", polygon_circumscribed)
|
||||||
.at("SKETCH", "CREATE")
|
.at("SKETCH", "CREATE")
|
||||||
.icon("polygon")
|
.icon("polygon")
|
||||||
.enabled(in_sketch)
|
.enabled(in_sketch)
|
||||||
.params("center, radius (to edge midpoints), sides, angle?: deg"),
|
.params(concat!("center, radius (to edge midpoints), sides, angle?: deg", connect_doc!())),
|
||||||
CommandSpec::new("sketch.polygon.edge", "Edge Polygon", polygon_edge)
|
CommandSpec::new("sketch.polygon.edge", "Edge Polygon", polygon_edge)
|
||||||
.at("SKETCH", "CREATE")
|
.at("SKETCH", "CREATE")
|
||||||
.icon("polygon")
|
.icon("polygon")
|
||||||
.enabled(in_sketch)
|
.enabled(in_sketch)
|
||||||
.params("p0, p1: one edge, sides"),
|
.params(concat!("p0, p1: one edge, sides", connect_doc!())),
|
||||||
CommandSpec::new("sketch.slot.center_to_center", "Center to Center Slot", slot_c2c)
|
CommandSpec::new("sketch.slot.center_to_center", "Center to Center Slot", slot_c2c)
|
||||||
.at("SKETCH", "CREATE")
|
.at("SKETCH", "CREATE")
|
||||||
.icon("slot")
|
.icon("slot")
|
||||||
.enabled(in_sketch)
|
.enabled(in_sketch)
|
||||||
.params("p0, p1: arc centres, width"),
|
.params(concat!("p0, p1: arc centres, width", connect_doc!())),
|
||||||
CommandSpec::new("sketch.slot.overall", "Overall Slot", slot_overall)
|
CommandSpec::new("sketch.slot.overall", "Overall Slot", slot_overall)
|
||||||
.at("SKETCH", "CREATE")
|
.at("SKETCH", "CREATE")
|
||||||
.icon("slot")
|
.icon("slot")
|
||||||
.enabled(in_sketch)
|
.enabled(in_sketch)
|
||||||
.params("p0, p1: overall ends, width"),
|
.params(concat!("p0, p1: overall ends, width", connect_doc!())),
|
||||||
CommandSpec::new("sketch.point", "Point", draw_point).at("SKETCH", "CREATE").icon("point").enabled(in_sketch).params("point: [x,y], id?"),
|
CommandSpec::new("sketch.point", "Point", draw_point).at("SKETCH", "CREATE").icon("point").enabled(in_sketch).params(concat!("point: [x,y], id?", connect_doc!())),
|
||||||
CommandSpec::new("sketch.dimension", "Sketch Dimension", dimension).at("SKETCH", "CREATE").icon("dimension").key("D").enabled(in_sketch).params(
|
CommandSpec::new("sketch.dimension", "Sketch Dimension", dimension).at("SKETCH", "CREATE").icon("dimension").key("D").enabled(in_sketch).params(
|
||||||
"entities: [refs], type?: auto|distance|horizontal|vertical|length|radius|diameter|angle, value?: number or expression (default: current)",
|
"entities: [refs], type?: auto|distance|horizontal|vertical|length|radius|diameter|angle, value?: number or expression (default: current)",
|
||||||
),
|
),
|
||||||
@@ -218,6 +225,47 @@ pub(super) fn edit<T>(
|
|||||||
Ok((out, json!({"solved": rep.ok(), "dof": rep.dof})))
|
Ok((out, json!({"solved": rep.ok(), "dof": rep.dof})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How close (mm) a new point must land to an existing one to be connected to it.
|
||||||
|
const CONNECT_TOL: f64 = 1e-6;
|
||||||
|
|
||||||
|
/// New points (index `first` on) that land on an existing point, the origin included, get a
|
||||||
|
/// coincident constraint to it, as when a click snaps there; `connect: false` leaves them free.
|
||||||
|
fn connect_points(sk: &mut Sketch, first: usize, p: &Value) -> Result<Vec<String>> {
|
||||||
|
if !bool_(p, "connect").unwrap_or(true) {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
// The existing points by x, for a range lookup per new point.
|
||||||
|
let mut old: Vec<(Vec2, usize)> = (0..first.min(sk.points.len())).filter_map(|i| Some((sk.point(i)?, i))).collect();
|
||||||
|
old.sort_by(|a, b| a.0.x.total_cmp(&b.0.x));
|
||||||
|
let mut ties = Vec::new();
|
||||||
|
for q in first..sk.points.len() {
|
||||||
|
let Some(at) = sk.point(q) else { continue };
|
||||||
|
let from = old.partition_point(|(o, _)| o.x < at.x - CONNECT_TOL);
|
||||||
|
let hit =
|
||||||
|
old.iter().skip(from).take_while(|(o, _)| o.x <= at.x + CONNECT_TOL).filter(|(o, _)| o.dist(at) < CONNECT_TOL).map(|(_, i)| *i).min();
|
||||||
|
if let Some(o) = hit {
|
||||||
|
ties.push((o, q));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ties.into_iter().map(|(o, q)| add_c(sk, ConstraintKind::Coincident { p: o, q })).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `edit` for drawing commands whose step returns (curve ids, constraint ids): the points it
|
||||||
|
/// adds are connected to the existing points they land on (see [`connect_points`]).
|
||||||
|
pub(super) fn draw(
|
||||||
|
s: &mut Session,
|
||||||
|
p: &Value,
|
||||||
|
cmd: &str,
|
||||||
|
f: impl FnOnce(&mut Sketch, &mut solvecraft_doc::Document) -> Result<(Vec<String>, Vec<String>)>,
|
||||||
|
) -> Result<((Vec<String>, Vec<String>), Value)> {
|
||||||
|
edit(s, p, cmd, false, |sk, doc| {
|
||||||
|
let first = sk.points.len();
|
||||||
|
let (curves, mut cons) = f(sk, doc)?;
|
||||||
|
cons.extend(connect_points(sk, first, p)?);
|
||||||
|
Ok((curves, cons))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// A point argument: `[x, y]` (new point), a point reference string, or `{at, ref}`.
|
/// A point argument: `[x, y]` (new point), a point reference string, or `{at, ref}`.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(super) enum PArg {
|
pub(super) enum PArg {
|
||||||
@@ -425,7 +473,7 @@ fn draw_line(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
let closed = bool_(p, "closed").unwrap_or(false);
|
let closed = bool_(p, "closed").unwrap_or(false);
|
||||||
let infer = bool_(p, "infer").unwrap_or(false);
|
let infer = bool_(p, "infer").unwrap_or(false);
|
||||||
let ids = string_list(p, "ids");
|
let ids = string_list(p, "ids");
|
||||||
let ((curves, cons), info) = edit(s, p, cmd, false, |sk, _| {
|
let ((curves, cons), info) = draw(s, p, cmd, |sk, _| {
|
||||||
let args: Vec<PArg> = list.iter().map(|v| parg(sk, v, cmd)).collect::<Result<_>>()?;
|
let args: Vec<PArg> = list.iter().map(|v| parg(sk, v, cmd)).collect::<Result<_>>()?;
|
||||||
let n = args.len();
|
let n = args.len();
|
||||||
let segs = if closed { n } else { n - 1 };
|
let segs = if closed { n } else { n - 1 };
|
||||||
@@ -555,9 +603,11 @@ fn rect_two_point(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
}
|
}
|
||||||
let (lo, hi) = (Vec2::new(a.x.min(b.x), a.y.min(b.y)), Vec2::new(a.x.max(b.x), a.y.max(b.y)));
|
let (lo, hi) = (Vec2::new(a.x.min(b.x), a.y.min(b.y)), Vec2::new(a.x.max(b.x), a.y.max(b.y)));
|
||||||
let ((out, names), info) = edit(s, p, cmd, false, |sk, _| {
|
let ((out, names), info) = edit(s, p, cmd, false, |sk, _| {
|
||||||
|
let first = sk.points.len();
|
||||||
let (l, pts) = polygon_lines(sk, &[lo, Vec2::new(hi.x, lo.y), hi, Vec2::new(lo.x, hi.y)])?;
|
let (l, pts) = polygon_lines(sk, &[lo, Vec2::new(hi.x, lo.y), hi, Vec2::new(lo.x, hi.y)])?;
|
||||||
let cons = rect_constraints(sk, &l)?;
|
let mut cons = rect_constraints(sk, &l)?;
|
||||||
mark_construction(sk, &l, p);
|
mark_construction(sk, &l, p);
|
||||||
|
cons.extend(connect_points(sk, first, p)?);
|
||||||
Ok(((ids_of(sk, &l), cons), rect_names(sk, &l, &pts)))
|
Ok(((ids_of(sk, &l), cons), rect_names(sk, &l, &pts)))
|
||||||
})?;
|
})?;
|
||||||
Ok(rect_result(out, names, info))
|
Ok(rect_result(out, names, info))
|
||||||
@@ -571,6 +621,7 @@ fn rect_center(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
return Err(bad(cmd, "the corner must differ from the centre in x and y"));
|
return Err(bad(cmd, "the corner must differ from the centre in x and y"));
|
||||||
}
|
}
|
||||||
let ((out, names), info) = edit(s, p, cmd, false, |sk, _| {
|
let ((out, names), info) = edit(s, p, cmd, false, |sk, _| {
|
||||||
|
let first = sk.points.len();
|
||||||
let (l, pts) = polygon_lines(sk, &[c - h, Vec2::new(c.x + h.x, c.y - h.y), c + h, Vec2::new(c.x - h.x, c.y + h.y)])?;
|
let (l, pts) = polygon_lines(sk, &[c - h, Vec2::new(c.x + h.x, c.y - h.y), c + h, Vec2::new(c.x - h.x, c.y + h.y)])?;
|
||||||
let mut cons = rect_constraints(sk, &l)?;
|
let mut cons = rect_constraints(sk, &l)?;
|
||||||
// Both diagonals (construction), the centre at their crossing. Only the first carries
|
// Both diagonals (construction), the centre at their crossing. Only the first carries
|
||||||
@@ -588,6 +639,7 @@ fn rect_center(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
cons.push(add_c(sk, ConstraintKind::Midpoint { p: cp, l: diag })?);
|
cons.push(add_c(sk, ConstraintKind::Midpoint { p: cp, l: diag })?);
|
||||||
}
|
}
|
||||||
mark_construction(sk, &l, p);
|
mark_construction(sk, &l, p);
|
||||||
|
cons.extend(connect_points(sk, first, p)?);
|
||||||
Ok(((ids_of(sk, &l), cons), rect_names(sk, &l, &pts)))
|
Ok(((ids_of(sk, &l), cons), rect_names(sk, &l, &pts)))
|
||||||
})?;
|
})?;
|
||||||
Ok(rect_result(out, names, info))
|
Ok(rect_result(out, names, info))
|
||||||
@@ -602,7 +654,7 @@ fn rect_three_point(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
if w.abs() < 1e-9 {
|
if w.abs() < 1e-9 {
|
||||||
return Err(bad(cmd, "p2 must not be on the first edge"));
|
return Err(bad(cmd, "p2 must not be on the first edge"));
|
||||||
}
|
}
|
||||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
let (out, info) = draw(s, p, cmd, |sk, _| {
|
||||||
let (l, _) = polygon_lines(sk, &[a, b, b + n * w, a + n * w])?;
|
let (l, _) = polygon_lines(sk, &[a, b, b + n * w, a + n * w])?;
|
||||||
let mut cons = Vec::new();
|
let mut cons = Vec::new();
|
||||||
if let [l0, l1, l2, l3] = l[..] {
|
if let [l0, l1, l2, l3] = l[..] {
|
||||||
@@ -630,7 +682,7 @@ fn circle_center(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
let p = &super::sketch_project::snap_points(s, p, cmd)?;
|
let p = &super::sketch_project::snap_points(s, p, cmd)?;
|
||||||
let r = radius_arg(p, cmd)?;
|
let r = radius_arg(p, cmd)?;
|
||||||
let id = str_(p, "id").map(str::to_string);
|
let id = str_(p, "id").map(str::to_string);
|
||||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
let (out, info) = draw(s, p, cmd, |sk, _| {
|
||||||
let c = req_parg(sk, p, "center", cmd)?;
|
let c = req_parg(sk, p, "center", cmd)?;
|
||||||
let ci = sk.add_circle(c.pos(), r, c.idx(), id.as_deref())?;
|
let ci = sk.add_circle(c.pos(), r, c.idx(), id.as_deref())?;
|
||||||
mark_construction(sk, &[ci], p);
|
mark_construction(sk, &[ci], p);
|
||||||
@@ -646,7 +698,7 @@ fn circle_two(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
if r < 1e-9 {
|
if r < 1e-9 {
|
||||||
return Err(bad(cmd, "the points must differ"));
|
return Err(bad(cmd, "the points must differ"));
|
||||||
}
|
}
|
||||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
let (out, info) = draw(s, p, cmd, |sk, _| {
|
||||||
let ci = sk.add_circle((a + b) * 0.5, r, None, None)?;
|
let ci = sk.add_circle((a + b) * 0.5, r, None, None)?;
|
||||||
Ok((ids_of(sk, &[ci]), Vec::new()))
|
Ok((ids_of(sk, &[ci]), Vec::new()))
|
||||||
})?;
|
})?;
|
||||||
@@ -671,7 +723,7 @@ fn circle_three(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
let cmd = "sketch.circle.three_point";
|
let cmd = "sketch.circle.three_point";
|
||||||
let (a, b, c) = (req_vec2(cmd, p, "p0")?, req_vec2(cmd, p, "p1")?, req_vec2(cmd, p, "p2")?);
|
let (a, b, c) = (req_vec2(cmd, p, "p0")?, req_vec2(cmd, p, "p1")?, req_vec2(cmd, p, "p2")?);
|
||||||
let (o, r) = circumcircle(a, b, c).ok_or_else(|| bad(cmd, "the points are collinear"))?;
|
let (o, r) = circumcircle(a, b, c).ok_or_else(|| bad(cmd, "the points are collinear"))?;
|
||||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
let (out, info) = draw(s, p, cmd, |sk, _| {
|
||||||
let ci = sk.add_circle(o, r, None, None)?;
|
let ci = sk.add_circle(o, r, None, None)?;
|
||||||
Ok((ids_of(sk, &[ci]), Vec::new()))
|
Ok((ids_of(sk, &[ci]), Vec::new()))
|
||||||
})?;
|
})?;
|
||||||
@@ -682,7 +734,7 @@ fn arc_three(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
let cmd = "sketch.arc.three_point";
|
let cmd = "sketch.arc.three_point";
|
||||||
let p = &super::sketch_project::snap_points(s, p, cmd)?;
|
let p = &super::sketch_project::snap_points(s, p, cmd)?;
|
||||||
let id = str_(p, "id").map(str::to_string);
|
let id = str_(p, "id").map(str::to_string);
|
||||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
let (out, info) = draw(s, p, cmd, |sk, _| {
|
||||||
let (a, b) = (req_parg(sk, p, "start", cmd)?, req_parg(sk, p, "end", cmd)?);
|
let (a, b) = (req_parg(sk, p, "start", cmd)?, req_parg(sk, p, "end", cmd)?);
|
||||||
let t = req_vec2(cmd, p, "through")?;
|
let t = req_vec2(cmd, p, "through")?;
|
||||||
let (o, _) = circumcircle(a.pos(), t, b.pos()).ok_or_else(|| bad(cmd, "the points are collinear"))?;
|
let (o, _) = circumcircle(a.pos(), t, b.pos()).ok_or_else(|| bad(cmd, "the points are collinear"))?;
|
||||||
@@ -705,7 +757,7 @@ fn arc_center(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
let p = &super::sketch_project::snap_points(s, p, cmd)?;
|
let p = &super::sketch_project::snap_points(s, p, cmd)?;
|
||||||
let id = str_(p, "id").map(str::to_string);
|
let id = str_(p, "id").map(str::to_string);
|
||||||
let sweep = num(p, "sweep");
|
let sweep = num(p, "sweep");
|
||||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
let (out, info) = draw(s, p, cmd, |sk, _| {
|
||||||
let c = req_parg(sk, p, "center", cmd)?;
|
let c = req_parg(sk, p, "center", cmd)?;
|
||||||
let a = req_parg(sk, p, "start", cmd)?;
|
let a = req_parg(sk, p, "start", cmd)?;
|
||||||
if let Some(sw) = sweep {
|
if let Some(sw) = sweep {
|
||||||
@@ -742,7 +794,7 @@ fn arc_center(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn polygon(s: &mut Session, p: &Value, cmd: &str, center: Vec2, r_vertex: f64, start: f64, n: usize, inscribed: bool) -> Result<Value> {
|
fn polygon(s: &mut Session, p: &Value, cmd: &str, center: Vec2, r_vertex: f64, start: f64, n: usize, inscribed: bool) -> Result<Value> {
|
||||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
let (out, info) = draw(s, p, cmd, |sk, _| {
|
||||||
let corners: Vec<Vec2> = (0..n).map(|i| center + Vec2::from_angle(start + std::f64::consts::TAU * i as f64 / n as f64) * r_vertex).collect();
|
let corners: Vec<Vec2> = (0..n).map(|i| center + Vec2::from_angle(start + std::f64::consts::TAU * i as f64 / n as f64) * r_vertex).collect();
|
||||||
let (l, pts) = polygon_lines(sk, &corners)?;
|
let (l, pts) = polygon_lines(sk, &corners)?;
|
||||||
let mut cons = Vec::new();
|
let mut cons = Vec::new();
|
||||||
@@ -818,7 +870,7 @@ pub(super) fn slot(s: &mut Session, p: &Value, cmd: &str, c0: Vec2, c1: Vec2, w:
|
|||||||
let h = w / 2.0;
|
let h = w / 2.0;
|
||||||
let d = (c1 - c0).normalized().ok_or_else(|| bad(cmd, "the slot ends must differ"))?;
|
let d = (c1 - c0).normalized().ok_or_else(|| bad(cmd, "the slot ends must differ"))?;
|
||||||
let n = d.perp();
|
let n = d.perp();
|
||||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
let (out, info) = draw(s, p, cmd, |sk, _| {
|
||||||
let pts: Vec<usize> =
|
let pts: Vec<usize> =
|
||||||
[c0 - n * h, c1 - n * h, c1 + n * h, c0 + n * h].iter().map(|q| sk.add_point(*q, None)).collect::<std::result::Result<_, _>>()?;
|
[c0 - n * h, c1 - n * h, c1 + n * h, c0 + n * h].iter().map(|q| sk.add_point(*q, None)).collect::<std::result::Result<_, _>>()?;
|
||||||
let (p0, p1, p2, p3) = (pts[0], pts[1], pts[2], pts[3]);
|
let (p0, p1, p2, p3) = (pts[0], pts[1], pts[2], pts[3]);
|
||||||
@@ -858,11 +910,12 @@ fn draw_point(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
let cmd = "sketch.point";
|
let cmd = "sketch.point";
|
||||||
let at = req_vec2(cmd, p, "point")?;
|
let at = req_vec2(cmd, p, "point")?;
|
||||||
let id = str_(p, "id").map(str::to_string);
|
let id = str_(p, "id").map(str::to_string);
|
||||||
let (pid, info) = edit(s, p, cmd, false, |sk, _| {
|
let ((pid, cons), info) = edit(s, p, cmd, false, |sk, _| {
|
||||||
let i = sk.add_point(at, id.as_deref())?;
|
let i = sk.add_point(at, id.as_deref())?;
|
||||||
Ok(sk.points.get(i).map(|q| q.id.clone()).unwrap_or_default())
|
let cons = connect_points(sk, i, p)?;
|
||||||
|
Ok((sk.points.get(i).map(|q| q.id.clone()).unwrap_or_default(), cons))
|
||||||
})?;
|
})?;
|
||||||
Ok(json!({"point": pid, "sketch": info}))
|
Ok(json!({"point": pid, "constraints": cons, "sketch": info}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------
|
||||||
@@ -1070,6 +1123,15 @@ fn c_horizontal_vertical(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
|
|
||||||
fn c_coincident(s: &mut Session, p: &Value) -> Result<Value> {
|
fn c_coincident(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let cmd = "sketch.constraint.coincident";
|
let cmd = "sketch.constraint.coincident";
|
||||||
|
// Already tied (e.g. connected when it was drawn there): nothing to add.
|
||||||
|
let sk = s.doc.sketch(target_sketch(s, p, cmd)?)?;
|
||||||
|
let pt = |k: &str| p.get(k).and_then(Value::as_str).and_then(|r| sk.resolve_point(r));
|
||||||
|
if let (Some(a), Some(b)) = (pt("a"), pt("b"))
|
||||||
|
&& let Some(c) = sk.constraints.iter().find(|c| matches!(c.kind, ConstraintKind::Coincident { p, q } if (p, q) == (a, b) || (p, q) == (b, a)))
|
||||||
|
{
|
||||||
|
let rep = solve(&mut sk.clone());
|
||||||
|
return Ok(json!({"constraint": c.id, "existing": true, "sketch": {"solved": rep.ok(), "dof": rep.dof}}));
|
||||||
|
}
|
||||||
constrain(s, p, cmd, |sk| {
|
constrain(s, p, cmd, |sk| {
|
||||||
let aref = p.get("a").and_then(Value::as_str).unwrap_or_default();
|
let aref = p.get("a").and_then(Value::as_str).unwrap_or_default();
|
||||||
let bref = p.get("b").and_then(Value::as_str).ok_or_else(|| bad(cmd, "`b` must be a point or curve"))?;
|
let bref = p.get("b").and_then(Value::as_str).ok_or_else(|| bad(cmd, "`b` must be a point or curve"))?;
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ fn curvature_polygon_arc_length_linear_diameter() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn glyphs_and_snaps() {
|
fn glyphs_and_snaps() {
|
||||||
let mut s = new_sketch();
|
let mut s = new_sketch();
|
||||||
let r = run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [20, 10]}));
|
let r = run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [20, 10], "connect": false}));
|
||||||
let l = ids(&r["curves"]);
|
let l = ids(&r["curves"]);
|
||||||
let g = run(&mut s, "sketch.glyphs", json!({}));
|
let g = run(&mut s, "sketch.glyphs", json!({}));
|
||||||
assert_eq!(g["glyphs"].as_array().unwrap().len(), 4);
|
assert_eq!(g["glyphs"].as_array().unwrap().len(), 4);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ fn rect(kind: &str) -> (Session, Vec<String>) {
|
|||||||
let r = match kind {
|
let r = match kind {
|
||||||
"center" => run(&mut s, "sketch.rectangle.center", json!({"center": [3, 2], "corner": [13, 9]})),
|
"center" => run(&mut s, "sketch.rectangle.center", json!({"center": [3, 2], "corner": [13, 9]})),
|
||||||
"two" => run(&mut s, "sketch.rectangle.two_point", json!({"p0": [1, 1], "p1": [21, 15]})),
|
"two" => run(&mut s, "sketch.rectangle.two_point", json!({"p0": [1, 1], "p1": [21, 15]})),
|
||||||
_ => run(&mut s, "sketch.rectangle.three_point", json!({"p0": [0, 0], "p1": [20, 5], "p2": [18, 15]})),
|
_ => run(&mut s, "sketch.rectangle.three_point", json!({"p0": [0, 0], "p1": [20, 5], "p2": [18, 15], "connect": false})),
|
||||||
};
|
};
|
||||||
let sides = ids(&r["curves"]);
|
let sides = ids(&r["curves"]);
|
||||||
assert_eq!(sides.len(), 4, "{r}");
|
assert_eq!(sides.len(), 4, "{r}");
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ fn every_dimension_kind_has_a_drawable_frame() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn dimension_text_is_stored_relative_to_the_dimension_and_follows_it() {
|
fn dimension_text_is_stored_relative_to_the_dimension_and_follows_it() {
|
||||||
let mut s = new_sketch();
|
let mut s = new_sketch();
|
||||||
run(&mut s, "sketch.line", json!({"points": [[0, 0], [40, 0]]}));
|
run(&mut s, "sketch.line", json!({"points": [[0, 0], [40, 0]], "connect": false}));
|
||||||
let d = run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "text_at": [20, -8]}));
|
let d = run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "text_at": [20, -8]}));
|
||||||
let param = d["param"].as_str().unwrap_or_default().to_string();
|
let param = d["param"].as_str().unwrap_or_default().to_string();
|
||||||
let text = |s: &Session| {
|
let text = |s: &Session| {
|
||||||
@@ -126,7 +126,7 @@ fn dof(s: &mut Session) -> i64 {
|
|||||||
#[test]
|
#[test]
|
||||||
fn fixed_points_survive_drags_and_solves_and_unfix_frees_them() {
|
fn fixed_points_survive_drags_and_solves_and_unfix_frees_them() {
|
||||||
let mut s = new_sketch();
|
let mut s = new_sketch();
|
||||||
run(&mut s, "sketch.line", json!({"points": [[0, 0], [30, 0], [30, 20]]}));
|
run(&mut s, "sketch.line", json!({"points": [[0, 0], [30, 0], [30, 20]], "connect": false}));
|
||||||
let free = dof(&mut s);
|
let free = dof(&mut s);
|
||||||
let r = run(&mut s, "sketch.constraint.fix", json!({"entity": "l1.end"}));
|
let r = run(&mut s, "sketch.constraint.fix", json!({"entity": "l1.end"}));
|
||||||
assert_eq!(r["result"]["fixed"], true);
|
assert_eq!(r["result"]["fixed"], true);
|
||||||
@@ -333,7 +333,7 @@ fn two_fully_constrained_entities_conflict_and_nothing_moves() {
|
|||||||
fn line_pairs_keep_the_first_line() {
|
fn line_pairs_keep_the_first_line() {
|
||||||
let lines = || {
|
let lines = || {
|
||||||
let mut s = new_sketch();
|
let mut s = new_sketch();
|
||||||
run(&mut s, "sketch.line", json!({"points": [[0, 0], [30, 2]]}));
|
run(&mut s, "sketch.line", json!({"points": [[0, 0], [30, 2]], "connect": false}));
|
||||||
run(&mut s, "sketch.line", json!({"points": [[5, 20], [25, 35]]}));
|
run(&mut s, "sketch.line", json!({"points": [[5, 20], [25, 35]]}));
|
||||||
s
|
s
|
||||||
};
|
};
|
||||||
@@ -360,7 +360,7 @@ fn line_pairs_keep_the_first_line() {
|
|||||||
fn circle_pairs_keep_the_first_circle() {
|
fn circle_pairs_keep_the_first_circle() {
|
||||||
let circles = || {
|
let circles = || {
|
||||||
let mut s = new_sketch();
|
let mut s = new_sketch();
|
||||||
run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 5}));
|
run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 5, "connect": false}));
|
||||||
run(&mut s, "sketch.circle.center", json!({"center": [30, 10], "radius": 8}));
|
run(&mut s, "sketch.circle.center", json!({"center": [30, 10], "radius": 8}));
|
||||||
s
|
s
|
||||||
};
|
};
|
||||||
@@ -471,7 +471,7 @@ fn dragging_a_rectangle_side_moves_that_side_only() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn dragging_circles_and_arcs() {
|
fn dragging_circles_and_arcs() {
|
||||||
let mut s = new_sketch();
|
let mut s = new_sketch();
|
||||||
run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 5}));
|
run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 5, "connect": false}));
|
||||||
// The edge changes the radius about a fixed centre; the centre moves the circle.
|
// The edge changes the radius about a fixed centre; the centre moves the circle.
|
||||||
drag(&mut s, "c1", [5.0, 0.0], [0.0, 8.0]);
|
drag(&mut s, "c1", [5.0, 0.0], [0.0, 8.0]);
|
||||||
assert!((snap(&s, "c1").1.unwrap_or(0.0) - 8.0).abs() < 1e-9);
|
assert!((snap(&s, "c1").1.unwrap_or(0.0) - 8.0).abs() < 1e-9);
|
||||||
@@ -553,7 +553,7 @@ fn chain_follows_shared_and_coincident_ends() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn constraints_and_dimensions_can_be_selected_and_deleted_together() {
|
fn constraints_and_dimensions_can_be_selected_and_deleted_together() {
|
||||||
let mut s = new_sketch();
|
let mut s = new_sketch();
|
||||||
run(&mut s, "sketch.line", json!({"points": [[0, 0], [30, 0]]}));
|
run(&mut s, "sketch.line", json!({"points": [[0, 0], [30, 0]], "connect": false}));
|
||||||
run(&mut s, "sketch.constraint.horizontal_vertical", json!({"line": "l1"}));
|
run(&mut s, "sketch.constraint.horizontal_vertical", json!({"line": "l1"}));
|
||||||
run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "value": 30}));
|
run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "value": 30}));
|
||||||
let ids: Vec<String> = sketch(&s).constraints.iter().map(|c| c.id.clone()).collect();
|
let ids: Vec<String> = sketch(&s).constraints.iter().map(|c| c.id.clone()).collect();
|
||||||
|
|||||||
@@ -1887,3 +1887,31 @@ fn rectangle_names_sides_and_corners() {
|
|||||||
assert!((p.x - 90.0).abs() < 1e-9 && (p.y + 5.0).abs() < 1e-9, "{c}");
|
assert!((p.x - 90.0).abs() < 1e-9 && (p.y + 5.0).abs() < 1e-9, "{c}");
|
||||||
assert_eq!(pos(&s, &json!(format!("{}.start", c["sides"]["top"].as_str().unwrap()))), pos(&s, &c["corners"]["top_right"]), "{c}");
|
assert_eq!(pos(&s, &json!(format!("{}.start", c["sides"]["top"].as_str().unwrap()))), pos(&s, &c["corners"]["top_right"]), "{c}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Points drawn exactly onto an existing point (the origin, a corner) are connected to it;
|
||||||
|
/// `connect: false` leaves them free, and tying them again by hand is a no-op, not a conflict.
|
||||||
|
#[test]
|
||||||
|
fn drawing_on_existing_points_connects_to_them() {
|
||||||
|
let mut s = Session::default();
|
||||||
|
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
||||||
|
let n = |v: &Value| v["constraints"].as_array().map_or(0, Vec::len);
|
||||||
|
// Horizontal/vertical x4, plus the corner on the origin.
|
||||||
|
let r = run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]}));
|
||||||
|
assert_eq!(n(&r), 5, "{r}");
|
||||||
|
run(&mut s, "sketch.dimension", json!({"entities": [r["curves"][0]], "value": 40}));
|
||||||
|
run(&mut s, "sketch.dimension", json!({"entities": [r["curves"][1]], "value": 30}));
|
||||||
|
let si = run(&mut s, "sketch.inspect", json!({}));
|
||||||
|
assert_eq!(si["dof"], 0, "fully constrained without a hand-made coincident: {si}");
|
||||||
|
let again = run(&mut s, "sketch.constraint.coincident", json!({"a": "origin", "b": "l1.start"}));
|
||||||
|
assert_eq!(again["existing"], true, "{again}");
|
||||||
|
|
||||||
|
// A line from a corner, a circle on a corner and a point on the origin connect too.
|
||||||
|
assert_eq!(n(&run(&mut s, "sketch.line", json!({"points": [[40, 30], [60, 30]]}))), 1);
|
||||||
|
assert_eq!(n(&run(&mut s, "sketch.circle.center", json!({"center": [0, 30], "radius": 3}))), 1);
|
||||||
|
assert_eq!(n(&run(&mut s, "sketch.point", json!({"point": [0, 0]}))), 1);
|
||||||
|
// Close is not on it, and `connect: false` opts out.
|
||||||
|
assert_eq!(n(&run(&mut s, "sketch.point", json!({"point": [40.001, 0]}))), 0);
|
||||||
|
assert_eq!(n(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [0, -10]], "connect": false}))), 0);
|
||||||
|
let si = run(&mut s, "sketch.inspect", json!({}));
|
||||||
|
assert_ne!(si["status"], "conflict", "{si}");
|
||||||
|
}
|
||||||
@@ -242,38 +242,9 @@ fn run_shape(app: &mut SolveApp, t: &Tool) -> Vec<String> {
|
|||||||
.ok()
|
.ok()
|
||||||
.map(|v| v["curves"].as_array().into_iter().flatten().filter_map(|c| c.as_str().map(str::to_string)).collect())
|
.map(|v| v["curves"].as_array().into_iter().flatten().filter_map(|c| c.as_str().map(str::to_string)).collect())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
tie_snaps(app, t, &curves);
|
|
||||||
curves
|
curves
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A click that snapped to an existing point (the origin, a corner) but went in as plain
|
|
||||||
/// coordinates: tie the new shape's point there to it, as Fusion does.
|
|
||||||
fn tie_snaps(app: &mut SolveApp, t: &Tool, curves: &[String]) {
|
|
||||||
let mut ties = Vec::new();
|
|
||||||
{
|
|
||||||
let st = app.session.world_state();
|
|
||||||
let Some(ss) = app.session.active_sketch.and_then(|s| st.sketch(s)) else { return };
|
|
||||||
let sk = &ss.sketch;
|
|
||||||
let new_pts: Vec<usize> =
|
|
||||||
curves.iter().filter_map(|c| sk.curve_index(c)).flat_map(|i| sk.curves.get(i).map(|c| c.kind.point_ids()).unwrap_or_default()).collect();
|
|
||||||
for (at, r) in &t.pts {
|
|
||||||
let Some(r) = r else { continue };
|
|
||||||
let Some(target) = sk.resolve_point(r) else { continue };
|
|
||||||
if new_pts.contains(&target) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Some(q) = new_pts.iter().copied().find(|q| sk.point(*q).is_some_and(|p| p.dist(*at) < 1e-6))
|
|
||||||
&& let Some(qid) = sk.points.get(q).map(|p| p.id.clone())
|
|
||||||
{
|
|
||||||
ties.push((r.clone(), qid));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (a, b) in ties {
|
|
||||||
let _ = app.run("sketch.constraint.coincident", json!({"a": a, "b": b}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_pick(app: &mut SolveApp, t: &Tool) {
|
fn run_pick(app: &mut SolveApp, t: &Tool) {
|
||||||
let k = &t.picks;
|
let k = &t.picks;
|
||||||
let params = match t.cmd {
|
let params = match t.cmd {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[
|
[
|
||||||
{"call": "engine.execute", "params": {"command": "sketch.create", "params": {"plane": "XY"}}},
|
{"call": "engine.execute", "params": {"command": "sketch.create", "params": {"plane": "XY"}}},
|
||||||
{"call": "engine.execute", "params": {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [40, 20]}}},
|
{"call": "engine.execute", "params": {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [40, 20], "connect": false}}},
|
||||||
{"call": "engine.execute", "params": {"command": "sketch.circle.center", "params": {"center": [60, 10], "radius": 5}}},
|
{"call": "engine.execute", "params": {"command": "sketch.circle.center", "params": {"center": [60, 10], "radius": 5}}},
|
||||||
{"call": "engine.execute", "params": {"command": "sketch.line", "params": {"points": [[45, -10], [75, -10]]}}},
|
{"call": "engine.execute", "params": {"command": "sketch.line", "params": {"points": [[45, -10], [75, -10]]}}},
|
||||||
{"call": "engine.execute", "params": {"command": "sketch.line", "params": {"points": [[45, 30], [75, 30]]}}},
|
{"call": "engine.execute", "params": {"command": "sketch.line", "params": {"points": [[45, 30], [75, 30]]}}},
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{"note": "A rectangle whose first corner is clicked on the origin is tied to it once (the engine connects it), so two dimensions fully constrain it."},
|
||||||
|
{"start": "sketch.create"},
|
||||||
|
{"click": {"plane": "XY"}},
|
||||||
|
{"start": "sketch.rectangle.two_point"},
|
||||||
|
{"click": {"sketch": [0, 0]}},
|
||||||
|
{"click": {"sketch": [30, 20]}},
|
||||||
|
{"key": "Escape"},
|
||||||
|
{"expect": {"query": {"command": "sketch.inspect", "path": ["dof"], "value": 2}}},
|
||||||
|
{"expect": {"query": {"command": "sketch.inspect", "path": ["constraints", 4, "type"], "value": "coincident"}}},
|
||||||
|
{"expect": {"query": {"command": "sketch.inspect", "path": ["constraints", 5], "value": null}}},
|
||||||
|
{"key": "Z", "cmd": true},
|
||||||
|
{"expect": {"query": {"command": "sketch.inspect", "path": ["curves", 0], "value": null}}}
|
||||||
|
]
|
||||||
Reference in new issue
Block a user