Sketch tools: Midpoint Line, Tangent Arc, 2-Tangent and 3-Tangent Circle, Center Point Slot, three-point and center-point Arc Slots; Trim, Extend, Break, sketch Fillet (radius dimension), equal/distance-angle/two-distance Chamfers, Offset (chains, side point), Mirror (symmetry constraints), Circular and Rectangular sketch patterns, Sketch Scale, sketch.move (move/rotate/copy)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
51f5f8a0e5
commit
73f68f3f50
10 files changed
+1723
-135
No files matched your search
@@ -9,6 +9,8 @@ pub use features::auto_operation;
|
||||
mod file;
|
||||
mod inspect;
|
||||
mod sketch;
|
||||
mod sketch_create;
|
||||
mod sketch_modify;
|
||||
mod sketch_project;
|
||||
|
||||
use serde::Serialize;
|
||||
@@ -106,6 +108,8 @@ pub fn in_sketch(s: &Session) -> std::result::Result<(), String> {
|
||||
pub fn command_specs() -> Vec<&'static CommandSpec> {
|
||||
let mut v: Vec<&'static CommandSpec> = Vec::new();
|
||||
v.extend(sketch::COMMANDS.iter());
|
||||
v.extend(sketch_create::COMMANDS.iter());
|
||||
v.extend(sketch_modify::COMMANDS.iter());
|
||||
v.extend(sketch_project::COMMANDS.iter());
|
||||
v.extend(features::COMMANDS.iter());
|
||||
v.extend(edit::COMMANDS.iter());
|
||||
|
||||
@@ -164,7 +164,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
// Helpers
|
||||
|
||||
/// Resolve the sketch a command acts on: `sketch` param (id or name) or the active sketch.
|
||||
fn target_sketch(s: &Session, p: &Value, cmd: &str) -> Result<u64> {
|
||||
pub(super) fn target_sketch(s: &Session, p: &Value, cmd: &str) -> Result<u64> {
|
||||
if let Some(v) = p.get("sketch") {
|
||||
let key = match v {
|
||||
Value::Number(n) => n.to_string(),
|
||||
@@ -182,7 +182,7 @@ fn target_sketch(s: &Session, p: &Value, cmd: &str) -> Result<u64> {
|
||||
|
||||
/// Edit a sketch: clone it, apply `f`, solve with the document's dimension values and store
|
||||
/// it. With `strict`, an edit that makes the constraints unsolvable is rejected.
|
||||
fn edit<T>(
|
||||
pub(super) fn edit<T>(
|
||||
s: &mut Session,
|
||||
p: &Value,
|
||||
cmd: &str,
|
||||
@@ -207,18 +207,18 @@ fn edit<T>(
|
||||
|
||||
/// A point argument: `[x, y]` (new point), a point reference string, or `{at, ref}`.
|
||||
#[derive(Clone)]
|
||||
enum PArg {
|
||||
pub(super) enum PArg {
|
||||
At(Vec2),
|
||||
Ref(usize, Vec2),
|
||||
}
|
||||
|
||||
impl PArg {
|
||||
fn pos(&self) -> Vec2 {
|
||||
pub(super) fn pos(&self) -> Vec2 {
|
||||
match self {
|
||||
PArg::At(p) | PArg::Ref(_, p) => *p,
|
||||
}
|
||||
}
|
||||
fn idx(&self) -> Option<usize> {
|
||||
pub(super) fn idx(&self) -> Option<usize> {
|
||||
match self {
|
||||
PArg::Ref(i, _) => Some(*i),
|
||||
PArg::At(_) => None,
|
||||
@@ -226,7 +226,7 @@ impl PArg {
|
||||
}
|
||||
}
|
||||
|
||||
fn parg(sk: &Sketch, v: &Value, cmd: &str) -> Result<PArg> {
|
||||
pub(super) fn parg(sk: &Sketch, v: &Value, cmd: &str) -> Result<PArg> {
|
||||
if let Some(p) = vec2(v) {
|
||||
return Ok(PArg::At(p));
|
||||
}
|
||||
@@ -245,29 +245,29 @@ fn parg(sk: &Sketch, v: &Value, cmd: &str) -> Result<PArg> {
|
||||
Err(bad(cmd, "a point must be [x, y], a point id like \"l1.end\", or {at, ref}"))
|
||||
}
|
||||
|
||||
fn req_parg(sk: &Sketch, p: &Value, k: &str, cmd: &str) -> Result<PArg> {
|
||||
pub(super) fn req_parg(sk: &Sketch, p: &Value, k: &str, cmd: &str) -> Result<PArg> {
|
||||
parg(sk, p.get(k).ok_or_else(|| bad(cmd, format!("missing `{k}`")))?, cmd)
|
||||
}
|
||||
|
||||
fn curve_ref(sk: &Sketch, v: Option<&Value>, cmd: &str, what: &str) -> Result<usize> {
|
||||
pub(super) fn curve_ref(sk: &Sketch, v: Option<&Value>, cmd: &str, what: &str) -> Result<usize> {
|
||||
let id = v.and_then(Value::as_str).ok_or_else(|| bad(cmd, format!("`{what}` must be a curve id")))?;
|
||||
sk.curve_index(id).ok_or_else(|| bad(cmd, format!("unknown sketch curve `{id}`")))
|
||||
}
|
||||
|
||||
fn point_ref(sk: &Sketch, v: Option<&Value>, cmd: &str, what: &str) -> Result<usize> {
|
||||
pub(super) fn point_ref(sk: &Sketch, v: Option<&Value>, cmd: &str, what: &str) -> Result<usize> {
|
||||
let id = v.and_then(Value::as_str).ok_or_else(|| bad(cmd, format!("`{what}` must be a point reference")))?;
|
||||
sk.resolve_point(id).ok_or_else(|| bad(cmd, format!("unknown sketch point `{id}`")))
|
||||
}
|
||||
|
||||
fn ids_of(sk: &Sketch, curves: &[usize]) -> Vec<String> {
|
||||
pub(super) fn ids_of(sk: &Sketch, curves: &[usize]) -> Vec<String> {
|
||||
curves.iter().filter_map(|c| sk.curves.get(*c).map(|c| c.id.clone())).collect()
|
||||
}
|
||||
|
||||
fn add_c(sk: &mut Sketch, k: ConstraintKind) -> Result<String> {
|
||||
pub(super) fn add_c(sk: &mut Sketch, k: ConstraintKind) -> Result<String> {
|
||||
Ok(sk.add_constraint(k, None)?)
|
||||
}
|
||||
|
||||
fn mark_construction(sk: &mut Sketch, curves: &[usize], p: &Value) {
|
||||
pub(super) fn mark_construction(sk: &mut Sketch, curves: &[usize], p: &Value) {
|
||||
if bool_(p, "construction").unwrap_or(false) {
|
||||
for c in curves {
|
||||
if let Some(cu) = sk.curves.get_mut(*c) {
|
||||
@@ -277,7 +277,7 @@ fn mark_construction(sk: &mut Sketch, curves: &[usize], p: &Value) {
|
||||
}
|
||||
}
|
||||
|
||||
fn result(sk_out: (Vec<String>, Vec<String>), info: Value) -> Value {
|
||||
pub(super) fn result(sk_out: (Vec<String>, Vec<String>), info: Value) -> Value {
|
||||
json!({"curves": sk_out.0, "constraints": sk_out.1, "sketch": info})
|
||||
}
|
||||
|
||||
@@ -529,7 +529,7 @@ fn rect_three_point(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(result(out, info))
|
||||
}
|
||||
|
||||
fn radius_arg(p: &Value, cmd: &str) -> Result<f64> {
|
||||
pub(super) fn radius_arg(p: &Value, cmd: &str) -> Result<f64> {
|
||||
let r = match (num(p, "radius"), num(p, "diameter")) {
|
||||
(Some(r), _) => r,
|
||||
(None, Some(d)) => d / 2.0,
|
||||
@@ -567,7 +567,7 @@ fn circle_two(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
/// Circle through three points.
|
||||
fn circumcircle(a: Vec2, b: Vec2, c: Vec2) -> Option<(Vec2, f64)> {
|
||||
pub(super) fn circumcircle(a: Vec2, b: Vec2, c: Vec2) -> Option<(Vec2, f64)> {
|
||||
let d = 2.0 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
|
||||
if d.abs() < 1e-12 {
|
||||
return None;
|
||||
@@ -727,7 +727,7 @@ fn polygon_edge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
polygon(s, p, cmd, center, rv, start, n, true)
|
||||
}
|
||||
|
||||
fn slot(s: &mut Session, p: &Value, cmd: &str, c0: Vec2, c1: Vec2, w: f64) -> Result<Value> {
|
||||
pub(super) fn slot(s: &mut Session, p: &Value, cmd: &str, c0: Vec2, c1: Vec2, w: f64) -> Result<Value> {
|
||||
let h = w / 2.0;
|
||||
let d = (c1 - c0).normalized().ok_or_else(|| bad(cmd, "the slot ends must differ"))?;
|
||||
let n = d.perp();
|
||||
@@ -941,7 +941,7 @@ fn c_fix(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Dimensions
|
||||
|
||||
fn line_pts(sk: &Sketch, l: usize) -> Option<(Vec2, Vec2)> {
|
||||
pub(super) fn line_pts(sk: &Sketch, l: usize) -> Option<(Vec2, Vec2)> {
|
||||
match sk.curves.get(l)?.kind {
|
||||
CurveKind::Line { a, b } => Some((sk.point(a)?, sk.point(b)?)),
|
||||
_ => None,
|
||||
@@ -1084,7 +1084,7 @@ fn angle_by_sector(a: usize, b: usize, (a0, a1): (Vec2, Vec2), (b0, b1): (Vec2,
|
||||
None
|
||||
}
|
||||
|
||||
fn round6(x: f64) -> f64 {
|
||||
pub(super) fn round6(x: f64) -> f64 {
|
||||
(x * 1e6).round() / 1e6
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
//! More sketch creation tools: midpoint line, tangent arc, tangent circles, center-point and
|
||||
//! arc slots.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_geom::Vec2;
|
||||
use solvecraft_sketch::{ConstraintKind, CurveKind, Sketch};
|
||||
|
||||
use super::sketch::{add_c, circumcircle, edit, ids_of, mark_construction, req_parg, result, slot};
|
||||
use super::{CommandSpec, in_sketch};
|
||||
use crate::params::{bad, num, req_vec2, str_, string_list};
|
||||
use crate::{Result, Session};
|
||||
|
||||
pub static COMMANDS: &[CommandSpec] =
|
||||
&[
|
||||
CommandSpec::new("SketchMidpointLine", "Midpoint Line", midpoint_line)
|
||||
.at("SKETCH", "CREATE")
|
||||
.icon("line_mid")
|
||||
.enabled(in_sketch)
|
||||
.params("mid: [x,y] or point ref, end: [x,y]; construction?"),
|
||||
CommandSpec::new("ArcTangent", "Tangent Arc", arc_tangent).at("SKETCH", "CREATE").icon("arc_tangent").enabled(in_sketch).params(
|
||||
"start: end point of a line or arc (\"l1.end\"), end: [x,y]; curve?: the curve to be tangent to (default: the one ending at start)",
|
||||
),
|
||||
CommandSpec::new("CircleTanTanRadius", "2-Tangent Circle", circle_tan_tan)
|
||||
.at("SKETCH", "CREATE")
|
||||
.icon("circle_tt")
|
||||
.enabled(in_sketch)
|
||||
.params("curves: [two lines/circles/arcs], radius | diameter: expr, near: [x,y] (which of the solutions)"),
|
||||
CommandSpec::new("CircleThreeTangent", "3-Tangent Circle", circle_three_tan)
|
||||
.at("SKETCH", "CREATE")
|
||||
.icon("circle_ttt")
|
||||
.enabled(in_sketch)
|
||||
.params("curves: [three lines/circles/arcs], near: [x,y] (which of the solutions)"),
|
||||
CommandSpec::new("ShapeSlotCenterPoint", "Center Point Slot", slot_center_point)
|
||||
.at("SKETCH", "CREATE")
|
||||
.icon("slot")
|
||||
.enabled(in_sketch)
|
||||
.params("center: slot centre, end: one arc centre, width"),
|
||||
CommandSpec::new("ShapeArcSlotThreePoint", "Three Point Arc Slot", arc_slot_three)
|
||||
.at("SKETCH", "CREATE")
|
||||
.icon("arc_slot")
|
||||
.enabled(in_sketch)
|
||||
.params("start, through, end: points on the centre arc, width"),
|
||||
CommandSpec::new("ShapeArcSlotCenterTwoPoint", "Center Point Arc Slot", arc_slot_center)
|
||||
.at("SKETCH", "CREATE")
|
||||
.icon("arc_slot")
|
||||
.enabled(in_sketch)
|
||||
.params("center, start: centre arc start, end: direction of the centre arc end (counter-clockwise), width"),
|
||||
];
|
||||
|
||||
fn midpoint_line(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "SketchMidpointLine";
|
||||
let end = req_vec2(cmd, p, "end")?;
|
||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let m = req_parg(sk, p, "mid", cmd)?;
|
||||
let start = m.pos() * 2.0 - end;
|
||||
if start.dist(end) < 1e-9 {
|
||||
return Err(bad(cmd, "the end must differ from the midpoint"));
|
||||
}
|
||||
let l = sk.add_line(start, end, None, None, None)?;
|
||||
let mp = match m.idx() {
|
||||
Some(i) => i,
|
||||
None => sk.add_point(m.pos(), None)?,
|
||||
};
|
||||
let k = add_c(sk, ConstraintKind::Midpoint { p: mp, l })?;
|
||||
mark_construction(sk, &[l], p);
|
||||
Ok((ids_of(sk, &[l]), vec![k]))
|
||||
})?;
|
||||
Ok(result(out, info))
|
||||
}
|
||||
|
||||
/// Direction leaving curve `ci` at its end point `pi` (continuing the curve past that end).
|
||||
fn leaving_dir(sk: &Sketch, ci: usize, pi: usize) -> Option<Vec2> {
|
||||
match sk.curves.get(ci)?.kind {
|
||||
CurveKind::Line { a, b } => {
|
||||
let (pa, pb) = (sk.point(a)?, sk.point(b)?);
|
||||
if pi == b {
|
||||
(pb - pa).normalized()
|
||||
} else if pi == a {
|
||||
(pa - pb).normalized()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
CurveKind::Arc { c, a, b } => {
|
||||
let (pc, q) = (sk.point(c)?, sk.point(pi)?);
|
||||
let ccw = (q - pc).perp().normalized()?;
|
||||
if pi == b {
|
||||
Some(ccw)
|
||||
} else if pi == a {
|
||||
Some(-ccw)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
CurveKind::Circle { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn arc_tangent(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "ArcTangent";
|
||||
let end = req_vec2(cmd, p, "end")?;
|
||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let st = req_parg(sk, p, "start", cmd)?;
|
||||
let pi = st.idx().ok_or_else(|| bad(cmd, "`start` must be the end point of a line or arc"))?;
|
||||
let ci = match str_(p, "curve") {
|
||||
Some(id) => sk.curve_index(id).ok_or_else(|| bad(cmd, format!("unknown curve `{id}`")))?,
|
||||
None => sk
|
||||
.curves
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(i, c)| !c.construction && leaving_dir(sk, *i, pi).is_some())
|
||||
.map(|(i, _)| i)
|
||||
.ok_or_else(|| bad(cmd, "no line or arc ends at `start`"))?,
|
||||
};
|
||||
let t = leaving_dir(sk, ci, pi).ok_or_else(|| bad(cmd, "`start` is not an end of that curve"))?;
|
||||
let s0 = st.pos();
|
||||
let n = t.perp();
|
||||
let d = end - s0;
|
||||
let den = 2.0 * n.dot(d);
|
||||
if den.abs() < 1e-9 * d.len().max(1e-9) || d.len() < 1e-9 {
|
||||
return Err(bad(cmd, "the end is straight ahead: draw a line instead"));
|
||||
}
|
||||
let center = s0 + n * (d.len2() / den);
|
||||
let a = if den > 0.0 {
|
||||
sk.add_arc(center, s0, end, [None, Some(pi), None], None)?
|
||||
} else {
|
||||
let a = sk.add_arc(center, end, s0, [None, None, Some(pi)], None)?;
|
||||
if let Some(c) = sk.curves.get_mut(a) {
|
||||
c.reversed = true;
|
||||
}
|
||||
a
|
||||
};
|
||||
let k = add_c(sk, ConstraintKind::Tangent { a: ci, b: a })?;
|
||||
Ok((ids_of(sk, &[a]), vec![k]))
|
||||
})?;
|
||||
Ok(result(out, info))
|
||||
}
|
||||
|
||||
/// Initial radius for a circle near `at` touching the curves: the mean distance to them.
|
||||
fn guess_radius(sk: &Sketch, curves: &[usize], at: Vec2) -> f64 {
|
||||
let ds: Vec<f64> = curves.iter().filter_map(|c| sk.shape(*c)).map(|sh| sh.project(at).dist(at)).collect();
|
||||
let r = ds.iter().sum::<f64>() / ds.len().max(1) as f64;
|
||||
if r.is_finite() && r > 1e-6 { r } else { 1.0 }
|
||||
}
|
||||
|
||||
fn tangent_curves(sk: &Sketch, p: &Value, cmd: &str, n: usize) -> Result<Vec<usize>> {
|
||||
let ids = string_list(p, "curves");
|
||||
if ids.len() != n {
|
||||
return Err(bad(cmd, format!("`curves` must list {n} curves")));
|
||||
}
|
||||
ids.iter().map(|id| sk.curve_index(id).ok_or_else(|| bad(cmd, format!("unknown curve `{id}`")))).collect()
|
||||
}
|
||||
|
||||
/// A circle tangent to `curves`, solved from a start near `near`.
|
||||
fn tangent_circle(s: &mut Session, p: &Value, cmd: &str, n: usize, radius: Option<String>) -> Result<Value> {
|
||||
let near = req_vec2(cmd, p, "near")?;
|
||||
let (out, info) = edit(s, p, cmd, true, |sk, doc| {
|
||||
let cs = tangent_curves(sk, p, cmd, n)?;
|
||||
let r0 = match &radius {
|
||||
Some(e) => doc.eval(e, solvecraft_doc::expr::Kind::Length).map_err(|e| bad(cmd, format!("radius: {e}")))?,
|
||||
None => guess_radius(sk, &cs, near),
|
||||
};
|
||||
if !(r0 > 1e-9 && r0 < 1e8) {
|
||||
return Err(bad(cmd, "radius must be positive"));
|
||||
}
|
||||
// Start where the circle already nearly touches: move the centre so the guessed circle
|
||||
// sits between the curves.
|
||||
let first_new = sk.points.len();
|
||||
let c = sk.add_circle(near, r0, None, None)?;
|
||||
let mut cons = Vec::new();
|
||||
for k in &cs {
|
||||
cons.push(add_c(sk, ConstraintKind::Tangent { a: *k, b: c })?);
|
||||
}
|
||||
if let Some(e) = &radius {
|
||||
let pname = doc.new_model_param(e, "mm");
|
||||
let k = sk.add_constraint(ConstraintKind::Radius { c, value: r0 }, Some(pname))?;
|
||||
cons.push(k);
|
||||
}
|
||||
solve_new_only(sk, first_new);
|
||||
Ok((ids_of(sk, &[c]), cons))
|
||||
})?;
|
||||
if info["solved"] != json!(true) {
|
||||
return Err(bad(cmd, "no tangent circle there"));
|
||||
}
|
||||
Ok(result(out, info))
|
||||
}
|
||||
|
||||
fn circle_tan_tan(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "CircleTanTanRadius";
|
||||
let r = match (crate::params::expr(p, "radius"), num(p, "diameter")) {
|
||||
(Some(r), _) => r,
|
||||
(None, Some(d)) => format!("{}", d / 2.0),
|
||||
_ => return Err(bad(cmd, "needs `radius` or `diameter`")),
|
||||
};
|
||||
tangent_circle(s, p, cmd, 2, Some(r))
|
||||
}
|
||||
|
||||
fn circle_three_tan(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
tangent_circle(s, p, "CircleThreeTangent", 3, None)
|
||||
}
|
||||
|
||||
fn width(p: &Value, cmd: &str) -> Result<f64> {
|
||||
num(p, "width").filter(|w| *w > 1e-9).ok_or_else(|| bad(cmd, "`width` must be positive"))
|
||||
}
|
||||
|
||||
fn slot_center_point(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "ShapeSlotCenterPoint";
|
||||
let (c, e) = (req_vec2(cmd, p, "center")?, req_vec2(cmd, p, "end")?);
|
||||
let w = width(p, cmd)?;
|
||||
slot(s, p, cmd, c * 2.0 - e, e, w)
|
||||
}
|
||||
|
||||
/// Arc slot around `o` (centre-line radius `r`) from angle `t0` sweeping `sweep` (> 0).
|
||||
fn arc_slot(s: &mut Session, p: &Value, cmd: &str, o: Vec2, r: f64, t0: f64, sweep: f64, w: f64) -> Result<Value> {
|
||||
let h = w / 2.0;
|
||||
if h >= r {
|
||||
return Err(bad(cmd, "the slot is wider than its centre arc's diameter"));
|
||||
}
|
||||
if !(sweep > 1e-6 && sweep < std::f64::consts::TAU - 1e-6) {
|
||||
return Err(bad(cmd, "the centre arc must have a length"));
|
||||
}
|
||||
let t1 = t0 + sweep;
|
||||
let at = |t: f64, rr: f64| o + Vec2::from_angle(t) * rr;
|
||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let oc = sk.add_point(o, None)?;
|
||||
let ps = sk.add_point(at(t0, r), None)?;
|
||||
let pe = sk.add_point(at(t1, r), None)?;
|
||||
let (os, is) = (sk.add_point(at(t0, r + h), None)?, sk.add_point(at(t0, r - h), None)?);
|
||||
let (oe, ie) = (sk.add_point(at(t1, r + h), None)?, sk.add_point(at(t1, r - h), None)?);
|
||||
let outer = sk.add_arc(o, at(t0, r + h), at(t1, r + h), [Some(oc), Some(os), Some(oe)], None)?;
|
||||
let inner = sk.add_arc(o, at(t0, r - h), at(t1, r - h), [Some(oc), Some(is), Some(ie)], None)?;
|
||||
let cap_e = sk.add_arc(at(t1, r), at(t1, r + h), at(t1, r - h), [Some(pe), Some(oe), Some(ie)], None)?;
|
||||
let cap_s = sk.add_arc(at(t0, r), at(t0, r - h), at(t0, r + h), [Some(ps), Some(is), Some(os)], None)?;
|
||||
let centre = sk.add_arc(o, at(t0, r), at(t1, r), [Some(oc), Some(ps), Some(pe)], None)?;
|
||||
if let Some(c) = sk.curves.get_mut(centre) {
|
||||
c.construction = true;
|
||||
}
|
||||
let mut cons = Vec::new();
|
||||
for (a, b) in [(outer, cap_e), (inner, cap_e), (outer, cap_s), (inner, cap_s)] {
|
||||
cons.push(add_c(sk, ConstraintKind::Tangent { a, b })?);
|
||||
}
|
||||
cons.push(add_c(sk, ConstraintKind::Equal { a: cap_s, b: cap_e })?);
|
||||
Ok((ids_of(sk, &[outer, cap_e, inner, cap_s]), cons))
|
||||
})?;
|
||||
Ok(result(out, info))
|
||||
}
|
||||
|
||||
fn arc_slot_three(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "ShapeArcSlotThreePoint";
|
||||
let (a, t, b) = (req_vec2(cmd, p, "start")?, req_vec2(cmd, p, "through")?, req_vec2(cmd, p, "end")?);
|
||||
let w = width(p, cmd)?;
|
||||
let (o, r) = circumcircle(a, t, b).ok_or_else(|| bad(cmd, "the points are collinear"))?;
|
||||
let ang = |q: Vec2| (q - o).angle();
|
||||
let span = |from: f64, to: f64| (to - from).rem_euclid(std::f64::consts::TAU);
|
||||
// Counter-clockwise from start must pass the through point; otherwise go from the end.
|
||||
let (t0, sweep) = if span(ang(a), ang(t)) < span(ang(a), ang(b)) { (ang(a), span(ang(a), ang(b))) } else { (ang(b), span(ang(b), ang(a))) };
|
||||
arc_slot(s, p, cmd, o, r, t0, sweep, w)
|
||||
}
|
||||
|
||||
fn arc_slot_center(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "ShapeArcSlotCenterTwoPoint";
|
||||
let (o, a, b) = (req_vec2(cmd, p, "center")?, req_vec2(cmd, p, "start")?, req_vec2(cmd, p, "end")?);
|
||||
let w = width(p, cmd)?;
|
||||
let r = o.dist(a);
|
||||
if r < 1e-9 || o.dist(b) < 1e-9 {
|
||||
return Err(bad(cmd, "the points must differ from the centre"));
|
||||
}
|
||||
let t0 = (a - o).angle();
|
||||
let sweep = ((b - o).angle() - t0).rem_euclid(std::f64::consts::TAU);
|
||||
arc_slot(s, p, cmd, o, r, t0, sweep, w)
|
||||
}
|
||||
|
||||
/// Solve moving only the points from `first_new` on (the geometry a tool just made), so the
|
||||
/// existing drawing stays put when the new entities alone can satisfy the constraints. Falls
|
||||
/// back to leaving everything to the normal solve.
|
||||
pub(super) fn solve_new_only(sk: &mut Sketch, first_new: usize) {
|
||||
let mut trial = sk.clone();
|
||||
for (i, p) in trial.points.iter_mut().enumerate() {
|
||||
if i < first_new {
|
||||
p.fixed = true;
|
||||
}
|
||||
}
|
||||
if solvecraft_sketch::solve(&mut trial).ok() {
|
||||
for (p, t) in sk.points.iter_mut().zip(&trial.points).skip(first_new) {
|
||||
p.pos = t.pos;
|
||||
}
|
||||
for (c, t) in sk.curves.iter_mut().zip(&trial.curves) {
|
||||
if let (CurveKind::Circle { r, .. }, CurveKind::Circle { r: tr, .. }) = (&mut c.kind, &t.kind) {
|
||||
*r = *tr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,993 @@
|
||||
//! Sketch modify tools: trim, extend, break, fillet, chamfer, offset, mirror, circular and
|
||||
//! rectangular patterns, move/copy and scale.
|
||||
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_geom::Vec2;
|
||||
use solvecraft_sketch::{ConstraintKind, Curve, CurveKind, Shape, Sketch, intersections};
|
||||
|
||||
use super::sketch::{add_c, edit, ids_of};
|
||||
use super::{CommandSpec, in_sketch};
|
||||
use crate::params::{bad, bool_, expr, num, req_vec2, str_, string_list, vec2};
|
||||
use crate::{Result, Session};
|
||||
|
||||
pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("FilletSketchCmd", "Fillet", fillet)
|
||||
.at("SKETCH", "MODIFY")
|
||||
.icon("sketch_fillet")
|
||||
.enabled(in_sketch)
|
||||
.params("point: corner shared by two lines (\"l1.end\") | a, b: two lines meeting at a corner; radius: expr"),
|
||||
CommandSpec::new("ChamferSketchEqualDistance", "Equal Distance Chamfer", chamfer_equal)
|
||||
.at("SKETCH", "MODIFY")
|
||||
.icon("sketch_chamfer")
|
||||
.enabled(in_sketch)
|
||||
.params("point | a, b: a corner of two lines; distance"),
|
||||
CommandSpec::new("ChamferSketchDistanceAngle", "Distance and Angle Chamfer", chamfer_dist_angle)
|
||||
.at("SKETCH", "MODIFY")
|
||||
.icon("sketch_chamfer")
|
||||
.enabled(in_sketch)
|
||||
.params("point | a, b: a corner of two lines; distance: along a, angle: deg from a"),
|
||||
CommandSpec::new("ChamferSketchDistanceDistance", "Two Distance Chamfer", chamfer_two)
|
||||
.at("SKETCH", "MODIFY")
|
||||
.icon("sketch_chamfer")
|
||||
.enabled(in_sketch)
|
||||
.params("point | a, b: a corner of two lines; distance: along a, distance2: along b"),
|
||||
CommandSpec::new("Offset", "Offset", offset)
|
||||
.at("SKETCH", "MODIFY")
|
||||
.icon("offset")
|
||||
.key("O")
|
||||
.enabled(in_sketch)
|
||||
.params("curves: [ids], distance: number (positive = left of the chain, or toward `side`), side?: [x,y], chain?: bool (default true: the connected chain)"),
|
||||
CommandSpec::new("TrimSketchCmd", "Trim", trim)
|
||||
.at("SKETCH", "MODIFY")
|
||||
.icon("trim")
|
||||
.key("T")
|
||||
.enabled(in_sketch)
|
||||
.params("curve: id, at: [x,y] (the piece to remove: between the nearest intersections around it)"),
|
||||
CommandSpec::new("ExtendSketchCmd", "Extend", extend)
|
||||
.at("SKETCH", "MODIFY")
|
||||
.icon("extend")
|
||||
.enabled(in_sketch)
|
||||
.params("curve: id, at: [x,y] near the end to extend (to the nearest curve)"),
|
||||
CommandSpec::new("BreakSketchCmd", "Break", break_cmd)
|
||||
.at("SKETCH", "MODIFY")
|
||||
.icon("break")
|
||||
.enabled(in_sketch)
|
||||
.params("curve: id, at: [x,y] (split at the nearest intersections around it) | params: split at points: [[x,y]…]"),
|
||||
CommandSpec::new("SketchScaleCmd", "Sketch Scale", scale)
|
||||
.at("SKETCH", "MODIFY")
|
||||
.icon("scale")
|
||||
.enabled(in_sketch)
|
||||
.params("entities: [curve/point ids], base: [x,y] or point ref, factor: number"),
|
||||
CommandSpec::new("sketch.move", "Move/Copy Sketch Entities", move_copy)
|
||||
.enabled(in_sketch)
|
||||
.params("entities: [ids], translate?: [dx,dy], angle?: deg, center?: [x,y], copy?: bool"),
|
||||
CommandSpec::new("MirrorSketchCommand", "Mirror", mirror)
|
||||
.at("SKETCH", "CREATE")
|
||||
.icon("sketch_mirror")
|
||||
.enabled(in_sketch)
|
||||
.params("entities: [curve/point ids], line: mirror line id (symmetry constraints are added)"),
|
||||
CommandSpec::new("CircularSketchPatternCommand", "Circular Pattern", pattern_circular)
|
||||
.at("SKETCH", "CREATE")
|
||||
.icon("sketch_circ_pattern")
|
||||
.enabled(in_sketch)
|
||||
.params("entities: [ids], center: [x,y] or point ref, count: 2…500, angle?: total deg (default 360)"),
|
||||
CommandSpec::new("RectangularSketchPatternCommand", "Rectangular Pattern", pattern_rect)
|
||||
.at("SKETCH", "CREATE")
|
||||
.icon("sketch_rect_pattern")
|
||||
.enabled(in_sketch)
|
||||
.params("entities: [ids], dir?: [x,y] (default x), count, spacing; count2?, spacing2? (perpendicular direction)"),
|
||||
];
|
||||
|
||||
const TOL: f64 = 1e-7;
|
||||
const MAX_COPIES: usize = 500;
|
||||
|
||||
fn curve_arg(sk: &Sketch, p: &Value, cmd: &str) -> Result<usize> {
|
||||
let id = str_(p, "curve").ok_or_else(|| bad(cmd, "`curve` must be a curve id"))?;
|
||||
let c = sk.curve_index(id).ok_or_else(|| bad(cmd, format!("unknown curve `{id}`")))?;
|
||||
if sk.is_linked_curve(c) {
|
||||
return Err(bad(cmd, format!("`{id}` is projected geometry: break its link first")));
|
||||
}
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
/// Parameter of `p` on the bounded shape, ends included.
|
||||
fn on_bounded(sh: &Shape, p: Vec2) -> Option<f64> {
|
||||
if sh.dist(p) > TOL * 10.0 {
|
||||
return None;
|
||||
}
|
||||
let t = sh.param(p);
|
||||
let (t0, t1) = sh.range();
|
||||
let slack = match *sh {
|
||||
Shape::Line { a, b } => TOL * 10.0 / a.dist(b).max(1e-12),
|
||||
Shape::Round { r, .. } => TOL * 10.0 / r.max(1e-12),
|
||||
};
|
||||
if sh.is_full() || (t >= t0 - slack && t <= t1 + slack) { Some(t) } else { None }
|
||||
}
|
||||
|
||||
/// Points where other curves meet curve `ci` (crossings and end points touching it), with the
|
||||
/// curve that makes each one and the parameter on `ci`.
|
||||
fn cut_points(sk: &Sketch, ci: usize) -> Vec<(f64, Vec2, usize)> {
|
||||
let Some(sh) = sk.shape(ci) else { return Vec::new() };
|
||||
let mut out: Vec<(f64, Vec2, usize)> = Vec::new();
|
||||
for (oi, _) in sk.curves.iter().enumerate() {
|
||||
if oi == ci {
|
||||
continue;
|
||||
}
|
||||
let Some(other) = sk.shape(oi) else { continue };
|
||||
let mut pts = intersections(&sh, &other);
|
||||
// Overlapping or touching ends that the analytic intersection misses (parallel lines).
|
||||
if let Shape::Line { a, b } = other {
|
||||
pts.extend([a, b]);
|
||||
}
|
||||
if let Shape::Round { sweep, .. } = other
|
||||
&& sweep < TAU - 1e-12
|
||||
{
|
||||
pts.extend([other.point(0.0), other.point(sweep)]);
|
||||
}
|
||||
for q in pts {
|
||||
if on_bounded(&other, q).is_some()
|
||||
&& let Some(t) = on_bounded(&sh, q)
|
||||
&& !out.iter().any(|(_, x, _)| x.dist(q) < TOL * 10.0)
|
||||
{
|
||||
out.push((t, q, oi));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Constrain a new end point onto the curve that cut there.
|
||||
fn pin(sk: &mut Sketch, p: usize, cutter: usize) -> Result<()> {
|
||||
let at = sk.point(p).unwrap_or_default();
|
||||
// An end point of the cutter there: coincident with it; otherwise on the curve.
|
||||
let ends: Vec<usize> = match sk.curves.get(cutter).map(|c| c.kind.clone()) {
|
||||
Some(CurveKind::Line { a, b }) | Some(CurveKind::Arc { a, b, .. }) => vec![a, b],
|
||||
_ => Vec::new(),
|
||||
};
|
||||
if let Some(e) = ends.into_iter().find(|e| sk.point(*e).is_some_and(|q| q.dist(at) < TOL * 10.0)) {
|
||||
if e != p {
|
||||
add_c(sk, ConstraintKind::Coincident { p, q: e })?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
add_c(sk, ConstraintKind::PointOnCurve { p, c: cutter })?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Split curve `ci` at interior parameters `ts` (sorted). Returns the pieces in parameter
|
||||
/// order (the first keeps the original index) and the new points.
|
||||
fn split_at(sk: &mut Sketch, ci: usize, ts: &[f64]) -> Result<(Vec<usize>, Vec<usize>)> {
|
||||
let sh = sk.shape(ci).ok_or_else(|| bad("split", "curve"))?;
|
||||
let kind = sk.curves.get(ci).map(|c| c.kind.clone()).ok_or_else(|| bad("split", "curve"))?;
|
||||
let construction = sk.curves.get(ci).is_some_and(|c| c.construction);
|
||||
let mut pts = Vec::new();
|
||||
for t in ts {
|
||||
pts.push(sk.add_point(sh.point(*t), None)?);
|
||||
}
|
||||
let mut pieces = vec![ci];
|
||||
let mut push = |sk: &mut Sketch, kind: CurveKind, prefix: &str| -> Result<()> {
|
||||
let id = sk.fresh(prefix);
|
||||
sk.curves.push(Curve { id, kind, construction, reversed: false, link: None });
|
||||
pieces.push(sk.curves.len() - 1);
|
||||
Ok(())
|
||||
};
|
||||
match kind {
|
||||
CurveKind::Line { a, b } => {
|
||||
let mut chain = vec![a];
|
||||
chain.extend(&pts);
|
||||
chain.push(b);
|
||||
if let Some(c) = sk.curves.get_mut(ci) {
|
||||
c.kind = CurveKind::Line { a, b: chain.get(1).copied().unwrap_or(b) };
|
||||
}
|
||||
for w in chain.windows(2).skip(1) {
|
||||
push(sk, CurveKind::Line { a: w[0], b: w[1] }, "l")?;
|
||||
}
|
||||
}
|
||||
CurveKind::Arc { c, a, b } => {
|
||||
let mut chain = vec![a];
|
||||
chain.extend(&pts);
|
||||
chain.push(b);
|
||||
if let Some(cu) = sk.curves.get_mut(ci) {
|
||||
cu.kind = CurveKind::Arc { c, a, b: chain.get(1).copied().unwrap_or(b) };
|
||||
}
|
||||
for w in chain.windows(2).skip(1) {
|
||||
push(sk, CurveKind::Arc { c, a: w[0], b: w[1] }, "a")?;
|
||||
}
|
||||
}
|
||||
CurveKind::Circle { c, .. } => {
|
||||
if pts.len() < 2 {
|
||||
return Err(bad("split", "a circle needs two points to split at"));
|
||||
}
|
||||
let mut chain = pts.clone();
|
||||
if let Some(f) = pts.first() {
|
||||
chain.push(*f);
|
||||
}
|
||||
if let Some(cu) = sk.curves.get_mut(ci) {
|
||||
cu.kind = CurveKind::Arc { c, a: chain[0], b: chain[1] };
|
||||
cu.reversed = false;
|
||||
}
|
||||
for w in chain.windows(2).skip(1) {
|
||||
push(sk, CurveKind::Arc { c, a: w[0], b: w[1] }, "a")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((pieces, pts))
|
||||
}
|
||||
|
||||
/// Keep the pieces of a split tied together: lines collinear, arcs on one circle.
|
||||
fn tie(sk: &mut Sketch, pieces: &[usize]) -> Result<()> {
|
||||
for w in pieces.windows(2) {
|
||||
let (a, b) = (w[0], w[1]);
|
||||
let line = matches!(sk.curves.get(a).map(|c| &c.kind), Some(CurveKind::Line { .. }));
|
||||
add_c(sk, if line { ConstraintKind::Collinear { a, b } } else { ConstraintKind::Equal { a, b } })?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The cut parameters bracketing `tq` on curve `ci`: (before, after) with their cutter curves.
|
||||
fn bracket(sk: &Sketch, ci: usize, tq: f64) -> (Vec<(f64, Vec2, usize)>, bool) {
|
||||
let Some(sh) = sk.shape(ci) else { return (Vec::new(), false) };
|
||||
let (t0, t1) = sh.range();
|
||||
let full = sh.is_full();
|
||||
let span = t1 - t0;
|
||||
let mut cuts: Vec<(f64, Vec2, usize)> =
|
||||
cut_points(sk, ci).into_iter().filter(|(t, _, _)| full || (*t > t0 + 1e-9 * span && *t < t1 - 1e-9 * span)).collect();
|
||||
cuts.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
if full {
|
||||
if cuts.len() < 2 {
|
||||
return (Vec::new(), true);
|
||||
}
|
||||
// Cuts around tq on the circle (wrapping).
|
||||
let after = cuts.iter().position(|c| c.0 > tq).unwrap_or(0);
|
||||
let before = (after + cuts.len() - 1) % cuts.len();
|
||||
let mut two = vec![cuts[before], cuts[after]];
|
||||
two.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
return (two, true);
|
||||
}
|
||||
let before = cuts.iter().rev().find(|c| c.0 < tq).copied();
|
||||
let after = cuts.iter().find(|c| c.0 > tq).copied();
|
||||
(before.into_iter().chain(after).collect(), false)
|
||||
}
|
||||
|
||||
fn trim(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "TrimSketchCmd";
|
||||
let at = req_vec2(cmd, p, "at")?;
|
||||
let (out, info) = edit(s, p, cmd, false, |sk, doc| {
|
||||
let ci = curve_arg(sk, p, cmd)?;
|
||||
let id = sk.curves.get(ci).map(|c| c.id.clone()).unwrap_or_default();
|
||||
let sh = sk.shape(ci).ok_or_else(|| bad(cmd, "curve"))?;
|
||||
let tq = sh.param(sh.project(at));
|
||||
let (cuts, full) = bracket(sk, ci, tq);
|
||||
if cuts.is_empty() {
|
||||
// Nothing crosses it: the whole curve goes.
|
||||
sk.remove_curves(&[ci]);
|
||||
doc.prune_model_params();
|
||||
return Ok(json!({"deleted": id}));
|
||||
}
|
||||
let ts: Vec<f64> = cuts.iter().map(|c| c.0).collect();
|
||||
let (pieces, pts) = split_at(sk, ci, &ts)?;
|
||||
// The piece containing tq.
|
||||
let k = if full {
|
||||
let (a, b) = (ts[0], ts[1]);
|
||||
if tq > a && tq < b { 0 } else { 1 }
|
||||
} else {
|
||||
ts.iter().filter(|t| **t < tq).count()
|
||||
};
|
||||
for (pi, c) in pts.iter().zip(&cuts) {
|
||||
pin(sk, *pi, c.2)?;
|
||||
}
|
||||
let gone = pieces.get(k).copied().ok_or_else(|| bad(cmd, "piece"))?;
|
||||
let keep: Vec<usize> = pieces.iter().copied().filter(|x| *x != gone).collect();
|
||||
// The original curve keeps its id (and constraints): swap geometry into it if needed.
|
||||
let gone = match keep.first() {
|
||||
Some(&other) if gone == ci => {
|
||||
let ka = sk.curves.get(ci).map(|c| c.kind.clone());
|
||||
let kb = sk.curves.get(other).map(|c| c.kind.clone());
|
||||
if let (Some(ka), Some(kb)) = (ka, kb) {
|
||||
if let Some(c) = sk.curves.get_mut(ci) {
|
||||
c.kind = kb;
|
||||
}
|
||||
if let Some(c) = sk.curves.get_mut(other) {
|
||||
c.kind = ka;
|
||||
}
|
||||
}
|
||||
other
|
||||
}
|
||||
_ => gone,
|
||||
};
|
||||
let keep: Vec<usize> = pieces.iter().copied().filter(|x| *x != gone).collect();
|
||||
tie(sk, &keep)?;
|
||||
sk.remove_curves(&[gone]);
|
||||
doc.prune_model_params();
|
||||
Ok(json!({"trimmed": id}))
|
||||
})?;
|
||||
Ok(json!({"result": out, "sketch": info}))
|
||||
}
|
||||
|
||||
fn break_cmd(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "BreakSketchCmd";
|
||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let ci = curve_arg(sk, p, cmd)?;
|
||||
let sh = sk.shape(ci).ok_or_else(|| bad(cmd, "curve"))?;
|
||||
let (ts, cutters): (Vec<f64>, Vec<Option<usize>>) = if let Some(list) = p.get("points").and_then(Value::as_array) {
|
||||
let mut v: Vec<f64> = list.iter().take(1000).filter_map(vec2).map(|q| sh.param(sh.project(q))).collect();
|
||||
v.sort_by(|a, b| a.total_cmp(b));
|
||||
let n = v.len();
|
||||
(v, vec![None; n])
|
||||
} else {
|
||||
let at = req_vec2(cmd, p, "at")?;
|
||||
let (cuts, _) = bracket(sk, ci, sh.param(sh.project(at)));
|
||||
(cuts.iter().map(|c| c.0).collect(), cuts.iter().map(|c| Some(c.2)).collect())
|
||||
};
|
||||
let (t0, t1) = sh.range();
|
||||
if ts.is_empty() || (!sh.is_full() && ts.iter().any(|t| *t <= t0 || *t >= t1)) {
|
||||
return Err(bad(cmd, "nothing to break at there"));
|
||||
}
|
||||
let (pieces, pts) = split_at(sk, ci, &ts)?;
|
||||
for (pi, c) in pts.iter().zip(&cutters) {
|
||||
if let Some(c) = c {
|
||||
pin(sk, *pi, *c)?;
|
||||
}
|
||||
}
|
||||
tie(sk, &pieces)?;
|
||||
Ok(ids_of(sk, &pieces))
|
||||
})?;
|
||||
Ok(json!({"curves": out, "sketch": info}))
|
||||
}
|
||||
|
||||
fn extend(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "ExtendSketchCmd";
|
||||
let at = req_vec2(cmd, p, "at")?;
|
||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let ci = curve_arg(sk, p, cmd)?;
|
||||
let sh = sk.shape(ci).ok_or_else(|| bad(cmd, "curve"))?;
|
||||
let (pa, pb) = match sk.curves.get(ci).map(|c| c.kind.clone()) {
|
||||
Some(CurveKind::Line { a, b }) | Some(CurveKind::Arc { a, b, .. }) => (a, b),
|
||||
_ => return Err(bad(cmd, "only lines and arcs can be extended")),
|
||||
};
|
||||
let (qa, qb) = (sk.point(pa).unwrap_or_default(), sk.point(pb).unwrap_or_default());
|
||||
let at_b = at.dist(qb) <= at.dist(qa);
|
||||
let end = if at_b { pb } else { pa };
|
||||
let shared = sk.curves.iter().enumerate().filter(|(i, c)| *i != ci && uses(c, end)).count();
|
||||
if shared > 0 || sk.points.get(end).is_some_and(|q| q.fixed) {
|
||||
return Err(bad(cmd, "that end is connected to other geometry"));
|
||||
}
|
||||
// Candidate hits on the unbounded curve beyond the chosen end.
|
||||
let unbounded = match sh {
|
||||
Shape::Line { .. } => sh,
|
||||
Shape::Round { c, r, start, .. } => Shape::Round { c, r, start, sweep: TAU },
|
||||
};
|
||||
let (_, t1) = sh.range();
|
||||
let mut best: Option<(f64, Vec2, usize)> = None;
|
||||
for (oi, _) in sk.curves.iter().enumerate() {
|
||||
if oi == ci {
|
||||
continue;
|
||||
}
|
||||
let Some(other) = sk.shape(oi) else { continue };
|
||||
for q in intersections(&unbounded, &other) {
|
||||
if on_bounded(&other, q).is_none() {
|
||||
continue;
|
||||
}
|
||||
let t = sh.param(q);
|
||||
let gap = match sh {
|
||||
Shape::Line { a, b } => {
|
||||
let l = a.dist(b);
|
||||
if at_b { (t - 1.0) * l } else { -t * l }
|
||||
}
|
||||
Shape::Round { r, .. } => {
|
||||
if at_b {
|
||||
(t - t1) * r
|
||||
} else {
|
||||
(TAU - t) * r
|
||||
}
|
||||
}
|
||||
};
|
||||
if gap > TOL * 10.0 && best.is_none_or(|b| gap < b.0) {
|
||||
best = Some((gap, q, oi));
|
||||
}
|
||||
}
|
||||
}
|
||||
let (_, q, oi) = best.ok_or_else(|| bad(cmd, "nothing to extend to"))?;
|
||||
if let Some(pt) = sk.points.get_mut(end) {
|
||||
pt.pos = q;
|
||||
}
|
||||
pin(sk, end, oi)?;
|
||||
Ok(sk.points.get(end).map(|x| x.id.clone()).unwrap_or_default())
|
||||
})?;
|
||||
Ok(json!({"moved": out, "sketch": info}))
|
||||
}
|
||||
|
||||
fn uses(c: &Curve, p: usize) -> bool {
|
||||
match c.kind {
|
||||
CurveKind::Line { a, b } => a == p || b == p,
|
||||
CurveKind::Circle { c, .. } => c == p,
|
||||
CurveKind::Arc { c, a, b } => c == p || a == p || b == p,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Corners: fillet and chamfer
|
||||
|
||||
/// A corner of two lines: (line a, line b, corner point, far end of a, far end of b).
|
||||
fn corner(sk: &Sketch, p: &Value, cmd: &str) -> Result<(usize, usize, usize, usize, usize)> {
|
||||
let ends = |l: usize| match sk.curves.get(l).map(|c| &c.kind) {
|
||||
Some(CurveKind::Line { a, b }) => Some((*a, *b)),
|
||||
_ => None,
|
||||
};
|
||||
let (la, lb) = if let Some(r) = str_(p, "point") {
|
||||
let q = sk.resolve_point(r).ok_or_else(|| bad(cmd, format!("unknown point `{r}`")))?;
|
||||
let lines: Vec<usize> = (0..sk.curves.len()).filter(|i| ends(*i).is_some_and(|(a, b)| a == q || b == q) && !sk.is_linked_curve(*i)).collect();
|
||||
match lines[..] {
|
||||
[a, b] => (a, b),
|
||||
_ => return Err(bad(cmd, "the corner must join exactly two lines")),
|
||||
}
|
||||
} else {
|
||||
let a = str_(p, "a").and_then(|i| sk.curve_index(i)).ok_or_else(|| bad(cmd, "`a` must be a line"))?;
|
||||
let b = str_(p, "b").and_then(|i| sk.curve_index(i)).ok_or_else(|| bad(cmd, "`b` must be a line"))?;
|
||||
(a, b)
|
||||
};
|
||||
let (a0, a1) = ends(la).ok_or_else(|| bad(cmd, "fillets and chamfers work on two lines"))?;
|
||||
let (b0, b1) = ends(lb).ok_or_else(|| bad(cmd, "fillets and chamfers work on two lines"))?;
|
||||
let shared = [a0, a1].into_iter().find(|x| *x == b0 || *x == b1).ok_or_else(|| bad(cmd, "the lines must share a corner point"))?;
|
||||
let fa = if a0 == shared { a1 } else { a0 };
|
||||
let fb = if b0 == shared { b1 } else { b0 };
|
||||
Ok((la, lb, shared, fa, fb))
|
||||
}
|
||||
|
||||
/// Replace the corner end of line `l` with point `np`.
|
||||
fn reattach(sk: &mut Sketch, l: usize, corner: usize, np: usize) {
|
||||
if let Some(Curve { kind: CurveKind::Line { a, b }, .. }) = sk.curves.get_mut(l) {
|
||||
if *a == corner {
|
||||
*a = np;
|
||||
} else if *b == corner {
|
||||
*b = np;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Points at distances `da` along a and `db` along b from the corner; lines shortened to them.
|
||||
fn cut_corner(sk: &mut Sketch, la: usize, lb: usize, c: usize, fa: usize, fb: usize, da: f64, db: f64, cmd: &str) -> Result<(usize, usize, Vec2)> {
|
||||
let (pc, pa, pb) = (sk.point(c).unwrap_or_default(), sk.point(fa).unwrap_or_default(), sk.point(fb).unwrap_or_default());
|
||||
if !(da > 1e-9 && db > 1e-9 && da < pc.dist(pa) - 1e-9 && db < pc.dist(pb) - 1e-9) {
|
||||
return Err(bad(cmd, "too large for these lines"));
|
||||
}
|
||||
let u = (pa - pc).normalized().ok_or_else(|| bad(cmd, "degenerate line"))?;
|
||||
let v = (pb - pc).normalized().ok_or_else(|| bad(cmd, "degenerate line"))?;
|
||||
let ta = sk.add_point(pc + u * da, None)?;
|
||||
let tb = sk.add_point(pc + v * db, None)?;
|
||||
reattach(sk, la, c, ta);
|
||||
reattach(sk, lb, c, tb);
|
||||
let used = sk.curves.iter().any(|x| uses(x, c));
|
||||
if !used && c != 0 {
|
||||
sk.remove_points(&[c]);
|
||||
}
|
||||
// Indices may have shifted when the corner point was removed.
|
||||
let fix = |i: usize| if !used && c != 0 && i > c { i - 1 } else { i };
|
||||
Ok((fix(ta), fix(tb), pc))
|
||||
}
|
||||
|
||||
fn fillet(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "FilletSketchCmd";
|
||||
let r_expr = expr(p, "radius").ok_or_else(|| bad(cmd, "`radius` must be a number or an expression"))?;
|
||||
let (out, info) = edit(s, p, cmd, false, |sk, doc| {
|
||||
let r = doc.eval(&r_expr, solvecraft_doc::expr::Kind::Length).map_err(|e| bad(cmd, format!("radius: {e}")))?;
|
||||
if !(r > 1e-9 && r < 1e8) {
|
||||
return Err(bad(cmd, "radius must be positive"));
|
||||
}
|
||||
let (la, lb, c, fa, fb) = corner(sk, p, cmd)?;
|
||||
let (pc, pa, pb) = (sk.point(c).unwrap_or_default(), sk.point(fa).unwrap_or_default(), sk.point(fb).unwrap_or_default());
|
||||
let u = (pa - pc).normalized().ok_or_else(|| bad(cmd, "degenerate line"))?;
|
||||
let v = (pb - pc).normalized().ok_or_else(|| bad(cmd, "degenerate line"))?;
|
||||
let half = 0.5 * u.cross(v).atan2(u.dot(v)).abs();
|
||||
if half < 1e-6 || half > std::f64::consts::FRAC_PI_2 - 1e-9 {
|
||||
return Err(bad(cmd, "the lines are parallel"));
|
||||
}
|
||||
let d = r / half.tan();
|
||||
let bis = (u + v).normalized().ok_or_else(|| bad(cmd, "the lines are parallel"))?;
|
||||
let center = pc + bis * (r / half.sin());
|
||||
let (ta, tb, _) = cut_corner(sk, la, lb, c, fa, fb, d, d, cmd)?;
|
||||
let (qa, qb) = (sk.point(ta).unwrap_or_default(), sk.point(tb).unwrap_or_default());
|
||||
let ccw = (qa - center).cross(qb - center) > 0.0;
|
||||
let arc = if ccw {
|
||||
sk.add_arc(center, qa, qb, [None, Some(ta), Some(tb)], None)?
|
||||
} else {
|
||||
sk.add_arc(center, qb, qa, [None, Some(tb), Some(ta)], None)?
|
||||
};
|
||||
let mut cons = vec![add_c(sk, ConstraintKind::Tangent { a: la, b: arc })?, add_c(sk, ConstraintKind::Tangent { a: lb, b: arc })?];
|
||||
let pname = doc.new_model_param(&r_expr, "mm");
|
||||
cons.push(sk.add_constraint(ConstraintKind::Radius { c: arc, value: r }, Some(pname.clone()))?);
|
||||
Ok((ids_of(sk, &[arc]), cons, pname))
|
||||
})?;
|
||||
Ok(json!({"curves": out.0, "constraints": out.1, "param": out.2, "sketch": info}))
|
||||
}
|
||||
|
||||
fn chamfer(s: &mut Session, p: &Value, cmd: &str, dists: impl FnOnce(f64) -> Result<(f64, f64)>) -> Result<Value> {
|
||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let (la, lb, c, fa, fb) = corner(sk, p, cmd)?;
|
||||
let (pc, pa, pb) = (sk.point(c).unwrap_or_default(), sk.point(fa).unwrap_or_default(), sk.point(fb).unwrap_or_default());
|
||||
let (u, v) = ((pa - pc).normalized().unwrap_or(Vec2::X), (pb - pc).normalized().unwrap_or(Vec2::Y));
|
||||
let corner_angle = u.cross(v).atan2(u.dot(v)).abs();
|
||||
let (da, db) = dists(corner_angle)?;
|
||||
let (ta, tb, _) = cut_corner(sk, la, lb, c, fa, fb, da, db, cmd)?;
|
||||
let l = sk.add_line_pts(ta, tb, None)?;
|
||||
Ok(ids_of(sk, &[l]))
|
||||
})?;
|
||||
Ok(json!({"curves": out, "sketch": info}))
|
||||
}
|
||||
|
||||
fn chamfer_equal(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "ChamferSketchEqualDistance";
|
||||
let d = num(p, "distance").ok_or_else(|| bad(cmd, "`distance` must be a number"))?;
|
||||
chamfer(s, p, cmd, |_| Ok((d, d)))
|
||||
}
|
||||
|
||||
fn chamfer_two(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "ChamferSketchDistanceDistance";
|
||||
let d = num(p, "distance").ok_or_else(|| bad(cmd, "`distance` must be a number"))?;
|
||||
let d2 = num(p, "distance2").ok_or_else(|| bad(cmd, "`distance2` must be a number"))?;
|
||||
chamfer(s, p, cmd, |_| Ok((d, d2)))
|
||||
}
|
||||
|
||||
fn chamfer_dist_angle(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "ChamferSketchDistanceAngle";
|
||||
let d = num(p, "distance").ok_or_else(|| bad(cmd, "`distance` must be a number"))?;
|
||||
let a = num(p, "angle").ok_or_else(|| bad(cmd, "`angle` must be a number (degrees)"))?.to_radians();
|
||||
chamfer(s, p, cmd, |phi| {
|
||||
// Triangle corner–Ta–Tb: angle phi at the corner, `a` at Ta.
|
||||
let opp = std::f64::consts::PI - a - phi;
|
||||
if !(a > 1e-6 && opp > 1e-6) {
|
||||
return Err(bad(cmd, "that angle does not meet the other line"));
|
||||
}
|
||||
Ok((d, d * a.sin() / opp.sin()))
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Offset
|
||||
|
||||
/// Curves of the connected chain through `start` (joined where exactly two curves meet), in
|
||||
/// order, with whether each is walked from its stored start (`a`) to its end (`b`).
|
||||
fn chain(sk: &Sketch, start: usize, pool: &[usize]) -> (Vec<(usize, bool)>, bool) {
|
||||
let ends = |c: usize| match sk.curves.get(c).map(|c| &c.kind) {
|
||||
Some(CurveKind::Line { a, b }) | Some(CurveKind::Arc { a, b, .. }) => Some((*a, *b)),
|
||||
_ => None,
|
||||
};
|
||||
let Some((s0, s1)) = ends(start) else { return (vec![(start, true)], true) };
|
||||
let next = |from: usize, at: usize, used: &[usize]| -> Option<usize> {
|
||||
let users: Vec<usize> = pool.iter().copied().filter(|c| ends(*c).is_some_and(|(a, b)| a == at || b == at)).collect();
|
||||
if users.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
users.into_iter().find(|c| *c != from && !used.contains(c))
|
||||
};
|
||||
let mut fwd = vec![(start, true)];
|
||||
let mut used = vec![start];
|
||||
let (mut cur, mut at) = (start, s1);
|
||||
let mut closed = false;
|
||||
while let Some(n) = next(cur, at, &used) {
|
||||
let Some((a, b)) = ends(n) else { break };
|
||||
let forward = a == at;
|
||||
fwd.push((n, forward));
|
||||
used.push(n);
|
||||
cur = n;
|
||||
at = if forward { b } else { a };
|
||||
if used.len() > 10_000 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if at == s0 && fwd.len() > 1 {
|
||||
closed = true;
|
||||
} else {
|
||||
// Walk backwards from the start.
|
||||
let (mut cur, mut at) = (start, s0);
|
||||
let mut back = Vec::new();
|
||||
while let Some(n) = next(cur, at, &used) {
|
||||
let Some((a, b)) = ends(n) else { break };
|
||||
let forward = b == at;
|
||||
back.push((n, forward));
|
||||
used.push(n);
|
||||
cur = n;
|
||||
at = if forward { a } else { b };
|
||||
if used.len() > 10_000 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
back.reverse();
|
||||
back.extend(fwd);
|
||||
fwd = back;
|
||||
}
|
||||
(fwd, closed)
|
||||
}
|
||||
|
||||
/// An offset piece: shape walked in chain direction, offset by `d` to the left.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Piece {
|
||||
Line(Vec2, Vec2),
|
||||
/// Centre point index, centre, radius, start point, end point (walk direction), ccw walk.
|
||||
Arc(usize, Vec2, f64, Vec2, Vec2, bool),
|
||||
}
|
||||
|
||||
fn offset_piece(sk: &Sketch, c: usize, forward: bool, d: f64) -> Option<Piece> {
|
||||
match sk.curves.get(c)?.kind {
|
||||
CurveKind::Line { a, b } => {
|
||||
let (mut p, mut q) = (sk.point(a)?, sk.point(b)?);
|
||||
if !forward {
|
||||
std::mem::swap(&mut p, &mut q);
|
||||
}
|
||||
let n = (q - p).normalized()?.perp();
|
||||
Some(Piece::Line(p + n * d, q + n * d))
|
||||
}
|
||||
CurveKind::Arc { c: ci, a, b } => {
|
||||
let (cc, pa, pb) = (sk.point(ci)?, sk.point(a)?, sk.point(b)?);
|
||||
let r = cc.dist(pa);
|
||||
// Walking counter-clockwise, left is toward the centre.
|
||||
let nr = if forward { r - d } else { r + d };
|
||||
if nr <= 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let at = |q: Vec2| cc + (q - cc).normalized().unwrap_or(Vec2::X) * nr;
|
||||
let (s, e) = if forward { (at(pa), at(pb)) } else { (at(pb), at(pa)) };
|
||||
Some(Piece::Arc(ci, cc, nr, s, e, forward))
|
||||
}
|
||||
CurveKind::Circle { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn piece_shape(p: &Piece) -> Shape {
|
||||
match *p {
|
||||
Piece::Line(a, b) => Shape::Line { a, b },
|
||||
Piece::Arc(_, c, r, ..) => Shape::Round { c, r, start: 0.0, sweep: TAU },
|
||||
}
|
||||
}
|
||||
|
||||
fn offset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "Offset";
|
||||
let ids = string_list(p, "curves");
|
||||
if ids.is_empty() {
|
||||
return Err(bad(cmd, "`curves` must list curves"));
|
||||
}
|
||||
let d0 = num(p, "distance").filter(|d| d.abs() > 1e-9).ok_or_else(|| bad(cmd, "`distance` must be a non-zero number"))?;
|
||||
let side = p.get("side").and_then(vec2);
|
||||
let use_chain = bool_(p, "chain").unwrap_or(true);
|
||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let sel: Vec<usize> = ids.iter().map(|i| sk.curve_index(i).ok_or_else(|| bad(cmd, format!("unknown curve `{i}`")))).collect::<Result<_>>()?;
|
||||
let first = *sel.first().ok_or_else(|| bad(cmd, "no curves"))?;
|
||||
let mut made = Vec::new();
|
||||
let mut cons = Vec::new();
|
||||
// Circles offset on their own.
|
||||
if let Some(CurveKind::Circle { c, r }) = sk.curves.get(first).map(|c| c.kind.clone()) {
|
||||
let cc = sk.point(c).unwrap_or_default();
|
||||
let grow = match side {
|
||||
Some(q) => q.dist(cc) > r,
|
||||
None => d0 < 0.0,
|
||||
};
|
||||
let nr = if grow { r + d0.abs() } else { r - d0.abs() };
|
||||
if nr <= 1e-9 {
|
||||
return Err(bad(cmd, "the offset circle would have no size"));
|
||||
}
|
||||
let ci = sk.add_circle(cc, nr, Some(c), None)?;
|
||||
made.push(ci);
|
||||
return Ok((ids_of(sk, &made), cons));
|
||||
}
|
||||
let pool: Vec<usize> = if use_chain {
|
||||
(0..sk.curves.len())
|
||||
.filter(|i| sk.curves.get(*i).is_some_and(|c| c.construction == sk.curves.get(first).is_some_and(|f| f.construction)))
|
||||
.collect()
|
||||
} else {
|
||||
sel.clone()
|
||||
};
|
||||
let (order, closed) = chain(sk, first, &pool);
|
||||
// Side: toward `side` (compare both offsets' distance to it), else the sign.
|
||||
let d = match side {
|
||||
Some(q) => {
|
||||
let dist = |dd: f64| {
|
||||
offset_piece(sk, first, order.iter().find(|x| x.0 == first).is_none_or(|x| x.1), dd)
|
||||
.map(|pc| piece_shape(&pc).dist(q))
|
||||
.unwrap_or(f64::MAX)
|
||||
};
|
||||
if dist(d0.abs()) <= dist(-d0.abs()) { d0.abs() } else { -d0.abs() }
|
||||
}
|
||||
None => d0,
|
||||
};
|
||||
let pieces: Vec<Piece> =
|
||||
order.iter().map(|(c, f)| offset_piece(sk, *c, *f, d)).collect::<Option<_>>().ok_or_else(|| bad(cmd, "the offset collapses an arc"))?;
|
||||
let n = pieces.len();
|
||||
let start_of = |p: &Piece| match *p {
|
||||
Piece::Line(a, _) => a,
|
||||
Piece::Arc(_, _, _, s, _, _) => s,
|
||||
};
|
||||
let end_of = |p: &Piece| match *p {
|
||||
Piece::Line(_, b) => b,
|
||||
Piece::Arc(_, _, _, _, e, _) => e,
|
||||
};
|
||||
// Junction points between consecutive pieces.
|
||||
let joint = |x: &Piece, y: &Piece| -> Vec2 {
|
||||
let (e, s0) = (end_of(x), start_of(y));
|
||||
if e.dist(s0) < TOL * 10.0 {
|
||||
return e;
|
||||
}
|
||||
let guess = (e + s0) * 0.5;
|
||||
intersections(&piece_shape(x), &piece_shape(y)).into_iter().min_by(|a, b| a.dist(guess).total_cmp(&b.dist(guess))).unwrap_or(guess)
|
||||
};
|
||||
let mut joints: Vec<Vec2> = Vec::new();
|
||||
joints.push(if closed { joint(&pieces[n - 1], &pieces[0]) } else { start_of(&pieces[0]) });
|
||||
for k in 0..n.saturating_sub(1) {
|
||||
joints.push(joint(&pieces[k], &pieces[k + 1]));
|
||||
}
|
||||
if !closed {
|
||||
joints.push(end_of(&pieces[n - 1]));
|
||||
}
|
||||
let pts: Vec<usize> = joints.iter().map(|q| sk.add_point(*q, None)).collect::<std::result::Result<_, _>>()?;
|
||||
for (k, pc) in pieces.iter().enumerate() {
|
||||
let (pa, pb) = (pts[k], if closed { pts[(k + 1) % n] } else { pts[k + 1] });
|
||||
let (orig, _) = order[k];
|
||||
match *pc {
|
||||
Piece::Line(..) => {
|
||||
let l = sk.add_line_pts(pa, pb, None)?;
|
||||
cons.push(add_c(sk, ConstraintKind::Parallel { a: orig, b: l })?);
|
||||
made.push(l);
|
||||
}
|
||||
Piece::Arc(ci, cc, _, _, _, ccw) => {
|
||||
let (qa, qb) = (sk.point(pa).unwrap_or_default(), sk.point(pb).unwrap_or_default());
|
||||
let a = if ccw {
|
||||
sk.add_arc(cc, qa, qb, [Some(ci), Some(pa), Some(pb)], None)?
|
||||
} else {
|
||||
sk.add_arc(cc, qb, qa, [Some(ci), Some(pb), Some(pa)], None)?
|
||||
};
|
||||
made.push(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((ids_of(sk, &made), cons))
|
||||
})?;
|
||||
Ok(json!({"curves": out.0, "constraints": out.1, "sketch": info}))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Copies: mirror, patterns, move, scale
|
||||
|
||||
/// Curves and lone points named in `entities`.
|
||||
fn entities(sk: &Sketch, p: &Value, cmd: &str) -> Result<(Vec<usize>, Vec<usize>)> {
|
||||
let ids = string_list(p, "entities");
|
||||
if ids.is_empty() {
|
||||
return Err(bad(cmd, "`entities` must list sketch curves or points"));
|
||||
}
|
||||
let (mut cs, mut ps) = (Vec::new(), Vec::new());
|
||||
for id in &ids {
|
||||
if let Some(c) = sk.curve_index(id) {
|
||||
cs.push(c);
|
||||
} else if let Some(q) = sk.resolve_point(id) {
|
||||
ps.push(q);
|
||||
} else {
|
||||
return Err(bad(cmd, format!("unknown sketch entity `{id}`")));
|
||||
}
|
||||
}
|
||||
Ok((cs, ps))
|
||||
}
|
||||
|
||||
/// Points used by curves plus lone points, in a stable order.
|
||||
fn involved_points(sk: &Sketch, cs: &[usize], ps: &[usize]) -> Vec<usize> {
|
||||
let mut v: Vec<usize> = Vec::new();
|
||||
for c in cs {
|
||||
match sk.curves.get(*c).map(|c| &c.kind) {
|
||||
Some(CurveKind::Line { a, b }) => v.extend([*a, *b]),
|
||||
Some(CurveKind::Circle { c, .. }) => v.push(*c),
|
||||
Some(CurveKind::Arc { c, a, b }) => v.extend([*c, *a, *b]),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
v.extend(ps);
|
||||
let mut out = Vec::new();
|
||||
for q in v {
|
||||
if !out.contains(&q) {
|
||||
out.push(q);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Copy curves and points through `f`. `flip` reverses arcs (mirroring). `keep(p)`: reuse the
|
||||
/// original point instead of copying it (points on a mirror line). Returns the new curves and
|
||||
/// the point map (original → copy).
|
||||
fn copy_through(
|
||||
sk: &mut Sketch,
|
||||
cs: &[usize],
|
||||
ps: &[usize],
|
||||
f: &dyn Fn(Vec2) -> Vec2,
|
||||
flip: bool,
|
||||
keep: &dyn Fn(Vec2) -> bool,
|
||||
scale_r: f64,
|
||||
) -> Result<(Vec<usize>, Vec<(usize, usize)>)> {
|
||||
let pts = involved_points(sk, cs, ps);
|
||||
let mut map: Vec<(usize, usize)> = Vec::new();
|
||||
for q in &pts {
|
||||
let pos = sk.point(*q).unwrap_or_default();
|
||||
let n = if keep(pos) { *q } else { sk.add_point(f(pos), None)? };
|
||||
map.push((*q, n));
|
||||
}
|
||||
let m = |q: usize| map.iter().find(|x| x.0 == q).map(|x| x.1).unwrap_or(q);
|
||||
let mut made = Vec::new();
|
||||
for c in cs {
|
||||
let Some(cu) = sk.curves.get(*c).cloned() else { continue };
|
||||
let (kind, prefix) = match cu.kind {
|
||||
CurveKind::Line { a, b } => (CurveKind::Line { a: m(a), b: m(b) }, "l"),
|
||||
CurveKind::Circle { c, r } => (CurveKind::Circle { c: m(c), r: r * scale_r }, "c"),
|
||||
CurveKind::Arc { c, a, b } => {
|
||||
if flip {
|
||||
(CurveKind::Arc { c: m(c), a: m(b), b: m(a) }, "a")
|
||||
} else {
|
||||
(CurveKind::Arc { c: m(c), a: m(a), b: m(b) }, "a")
|
||||
}
|
||||
}
|
||||
};
|
||||
let id = sk.fresh(prefix);
|
||||
sk.curves.push(Curve { id, kind, construction: cu.construction, reversed: if flip { !cu.reversed } else { cu.reversed }, link: None });
|
||||
made.push(sk.curves.len() - 1);
|
||||
}
|
||||
Ok((made, map))
|
||||
}
|
||||
|
||||
fn mirror(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "MirrorSketchCommand";
|
||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let (cs, ps) = entities(sk, p, cmd)?;
|
||||
let lid = str_(p, "line").ok_or_else(|| bad(cmd, "`line` must be the mirror line"))?;
|
||||
let l = sk.curve_index(lid).ok_or_else(|| bad(cmd, format!("unknown curve `{lid}`")))?;
|
||||
let Some(Shape::Line { a, b }) = sk.shape(l) else { return Err(bad(cmd, "the mirror must be a line")) };
|
||||
let u = (b - a).normalized().ok_or_else(|| bad(cmd, "degenerate mirror line"))?;
|
||||
let refl = move |q: Vec2| {
|
||||
let foot = a + u * (q - a).dot(u);
|
||||
foot * 2.0 - q
|
||||
};
|
||||
let on_line = move |q: Vec2| u.cross(q - a).abs() < TOL * 10.0;
|
||||
let cs: Vec<usize> = cs.into_iter().filter(|c| *c != l).collect();
|
||||
let (made, map) = copy_through(sk, &cs, &ps, &refl, true, &on_line, 1.0)?;
|
||||
let mut cons = Vec::new();
|
||||
for (o, n) in &map {
|
||||
if o != n {
|
||||
cons.push(add_c(sk, ConstraintKind::Symmetric { p: *o, q: *n, l })?);
|
||||
}
|
||||
}
|
||||
for (o, n) in cs.iter().zip(&made) {
|
||||
if matches!(sk.curves.get(*o).map(|c| &c.kind), Some(CurveKind::Circle { .. })) {
|
||||
cons.push(add_c(sk, ConstraintKind::Equal { a: *o, b: *n })?);
|
||||
}
|
||||
}
|
||||
Ok((ids_of(sk, &made), cons))
|
||||
})?;
|
||||
Ok(json!({"curves": out.0, "constraints": out.1, "sketch": info}))
|
||||
}
|
||||
|
||||
fn count(p: &Value, k: &str, cmd: &str) -> Result<usize> {
|
||||
let n = num(p, k).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?;
|
||||
if !(1.0..=MAX_COPIES as f64).contains(&n) {
|
||||
return Err(bad(cmd, format!("`{k}` must be 1…{MAX_COPIES}")));
|
||||
}
|
||||
Ok(n as usize)
|
||||
}
|
||||
|
||||
fn point_or_ref(sk: &Sketch, p: &Value, k: &str, cmd: &str) -> Result<Vec2> {
|
||||
match p.get(k) {
|
||||
Some(Value::String(r)) => sk.resolve_point(r).and_then(|i| sk.point(i)).ok_or_else(|| bad(cmd, format!("unknown point `{r}`"))),
|
||||
Some(v) => vec2(v).ok_or_else(|| bad(cmd, format!("`{k}` must be [x, y] or a point id"))),
|
||||
None => Err(bad(cmd, format!("missing `{k}`"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn rotate(c: Vec2, ang: f64) -> impl Fn(Vec2) -> Vec2 {
|
||||
move |q: Vec2| {
|
||||
let d = q - c;
|
||||
let (s, co) = ang.sin_cos();
|
||||
c + Vec2::new(d.x * co - d.y * s, d.x * s + d.y * co)
|
||||
}
|
||||
}
|
||||
|
||||
fn pattern_circular(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "CircularSketchPatternCommand";
|
||||
let n = count(p, "count", cmd)?;
|
||||
let total = num(p, "angle").unwrap_or(360.0);
|
||||
if !(total.abs() > 1e-9 && total.abs() <= 360.0) {
|
||||
return Err(bad(cmd, "`angle` must be in (0, 360]"));
|
||||
}
|
||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let (cs, ps) = entities(sk, p, cmd)?;
|
||||
let c = point_or_ref(sk, p, "center", cmd)?;
|
||||
let full = (total.abs() - 360.0).abs() < 1e-9;
|
||||
let step = if full { total / n as f64 } else { total / (n.max(2) - 1) as f64 };
|
||||
let mut made = Vec::new();
|
||||
for k in 1..n {
|
||||
let f = rotate(c, (step * k as f64).to_radians());
|
||||
let (m, _) = copy_through(sk, &cs, &ps, &f, false, &|_| false, 1.0)?;
|
||||
made.extend(m);
|
||||
}
|
||||
Ok(ids_of(sk, &made))
|
||||
})?;
|
||||
Ok(json!({"curves": out, "sketch": info}))
|
||||
}
|
||||
|
||||
fn pattern_rect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "RectangularSketchPatternCommand";
|
||||
let n1 = count(p, "count", cmd)?;
|
||||
let s1 = num(p, "spacing").ok_or_else(|| bad(cmd, "`spacing` must be a number"))?;
|
||||
let n2 = if p.get("count2").is_some() { count(p, "count2", cmd)? } else { 1 };
|
||||
let s2 = num(p, "spacing2").unwrap_or(s1);
|
||||
if n1 * n2 > MAX_COPIES {
|
||||
return Err(bad(cmd, "too many copies"));
|
||||
}
|
||||
let d1 = p.get("dir").and_then(vec2).and_then(Vec2::normalized).unwrap_or(Vec2::X);
|
||||
let d2 = d1.perp();
|
||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let (cs, ps) = entities(sk, p, cmd)?;
|
||||
let mut made = Vec::new();
|
||||
for i in 0..n1 {
|
||||
for j in 0..n2 {
|
||||
if i == 0 && j == 0 {
|
||||
continue;
|
||||
}
|
||||
let off = d1 * (s1 * i as f64) + d2 * (s2 * j as f64);
|
||||
let (m, _) = copy_through(sk, &cs, &ps, &move |q| q + off, false, &|_| false, 1.0)?;
|
||||
made.extend(m);
|
||||
}
|
||||
}
|
||||
Ok(ids_of(sk, &made))
|
||||
})?;
|
||||
Ok(json!({"curves": out, "sketch": info}))
|
||||
}
|
||||
|
||||
/// Move the points of the entities (in place) through `f`; circle radii scale by `k`.
|
||||
fn transform_in_place(sk: &mut Sketch, cs: &[usize], ps: &[usize], f: &dyn Fn(Vec2) -> Vec2, k: f64, cmd: &str) -> Result<()> {
|
||||
let pts = involved_points(sk, cs, ps);
|
||||
if pts.iter().any(|q| sk.points.get(*q).is_some_and(|x| x.fixed || x.link.is_some())) {
|
||||
return Err(bad(cmd, "fixed or projected geometry cannot move"));
|
||||
}
|
||||
for q in pts {
|
||||
if let Some(x) = sk.points.get_mut(q) {
|
||||
x.pos = f(x.pos);
|
||||
}
|
||||
}
|
||||
for c in cs {
|
||||
if let Some(Curve { kind: CurveKind::Circle { r, .. }, .. }) = sk.curves.get_mut(*c) {
|
||||
*r *= k;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn move_copy(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.move";
|
||||
let t = p.get("translate").and_then(vec2).unwrap_or(Vec2::ZERO);
|
||||
let ang = num(p, "angle").unwrap_or(0.0).to_radians();
|
||||
let copy = bool_(p, "copy").unwrap_or(false);
|
||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let (cs, ps) = entities(sk, p, cmd)?;
|
||||
let c = if p.get("center").is_some() { point_or_ref(sk, p, "center", cmd)? } else { Vec2::ZERO };
|
||||
let rot = rotate(c, ang);
|
||||
let f = move |q: Vec2| rot(q) + t;
|
||||
if copy {
|
||||
let (m, _) = copy_through(sk, &cs, &ps, &f, false, &|_| false, 1.0)?;
|
||||
return Ok(ids_of(sk, &m));
|
||||
}
|
||||
transform_in_place(sk, &cs, &ps, &f, 1.0, cmd)?;
|
||||
Ok(ids_of(sk, &cs))
|
||||
})?;
|
||||
Ok(json!({"curves": out, "sketch": info}))
|
||||
}
|
||||
|
||||
fn scale(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "SketchScaleCmd";
|
||||
let k = num(p, "factor").filter(|k| *k > 1e-9 && *k < 1e6).ok_or_else(|| bad(cmd, "`factor` must be a positive number"))?;
|
||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let (cs, ps) = entities(sk, p, cmd)?;
|
||||
let b = if p.get("base").is_some() { point_or_ref(sk, p, "base", cmd)? } else { Vec2::ZERO };
|
||||
let f = move |q: Vec2| b + (q - b) * k;
|
||||
transform_in_place(sk, &cs, &ps, &f, k, cmd)?;
|
||||
Ok(ids_of(sk, &cs))
|
||||
})?;
|
||||
Ok(json!({"curves": out, "sketch": info}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sketch_modify_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,207 @@
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_geom::Vec2;
|
||||
use solvecraft_sketch::CurveKind;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
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 ids(v: &Value) -> Vec<String> {
|
||||
v.as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn sketch(s: &Session) -> solvecraft_sketch::Sketch {
|
||||
let id = s.active_sketch.unwrap();
|
||||
s.model.state().sketch(id).unwrap().sketch.clone()
|
||||
}
|
||||
|
||||
fn new_sketch() -> Session {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "SketchCreate", json!({"plane": "XY"}));
|
||||
s
|
||||
}
|
||||
|
||||
fn profiles(s: &Session) -> Vec<f64> {
|
||||
let id = s.active_sketch.unwrap();
|
||||
let mut a: Vec<f64> = s.model.state().sketch(id).unwrap().profiles.iter().map(|p| p.area).collect();
|
||||
a.sort_by(|x, y| x.total_cmp(y));
|
||||
a
|
||||
}
|
||||
|
||||
fn close(a: f64, b: f64) -> bool {
|
||||
(a - b).abs() < 1e-6
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_extend_break() {
|
||||
let mut s = new_sketch();
|
||||
// A cross: horizontal line crossed by a vertical one.
|
||||
let h = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [20, 0]]}))["curves"])[0].clone();
|
||||
let v = ids(&run(&mut s, "DrawPolyline", json!({"points": [[10, -5], [10, 5]]}))["curves"])[0].clone();
|
||||
// Trim the right half of the horizontal line.
|
||||
run(&mut s, "TrimSketchCmd", json!({"curve": h, "at": [15, 0]}));
|
||||
let sk = sketch(&s);
|
||||
let sh = sk.shape(sk.curve_index(&h).unwrap()).unwrap();
|
||||
assert!(
|
||||
matches!(sh, solvecraft_sketch::Shape::Line { a, b } if a.dist(Vec2::new(0.0, 0.0)) < 1e-9 && b.dist(Vec2::new(10.0, 0.0)) < 1e-9),
|
||||
"{sh:?}"
|
||||
);
|
||||
// Trim the lower half of the vertical line: the original id survives on the upper piece.
|
||||
run(&mut s, "TrimSketchCmd", json!({"curve": v, "at": [10, -3]}));
|
||||
let sk = sketch(&s);
|
||||
assert_eq!(sk.curves.len(), 2);
|
||||
// Extend: a short line reaching toward the vertical one.
|
||||
let e = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 3], [4, 3]]}))["curves"])[0].clone();
|
||||
run(&mut s, "ExtendSketchCmd", json!({"curve": e, "at": [4, 3]}));
|
||||
let sk = sketch(&s);
|
||||
let end = sk.resolve_point(&format!("{e}.end")).and_then(|i| sk.point(i)).unwrap();
|
||||
assert!(end.dist(Vec2::new(10.0, 3.0)) < 1e-7, "{end:?}");
|
||||
// Break the horizontal line where... nothing crosses it inside: error.
|
||||
assert!(s.execute("BreakSketchCmd", &json!({"curve": h, "at": [5, 0]})).is_err());
|
||||
// Break the vertical at the extended line: two pieces.
|
||||
let r = run(&mut s, "BreakSketchCmd", json!({"curve": v, "at": [10, 1]}));
|
||||
assert_eq!(ids(&r["curves"]).len(), 2, "{r}");
|
||||
// A lone curve trims away entirely.
|
||||
let lone = ids(&run(&mut s, "DrawPolyline", json!({"points": [[50, 50], [60, 50]]}))["curves"])[0].clone();
|
||||
run(&mut s, "TrimSketchCmd", json!({"curve": lone, "at": [55, 50]}));
|
||||
assert!(sketch(&s).curve_index(&lone).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_circle_makes_an_arc_and_a_profile() {
|
||||
let mut s = new_sketch();
|
||||
let c = ids(&run(&mut s, "CircleCenterRadius", json!({"center": [0, 0], "radius": 10}))["curves"])[0].clone();
|
||||
run(&mut s, "DrawPolyline", json!({"points": [[-20, 0], [20, 0]]}));
|
||||
run(&mut s, "TrimSketchCmd", json!({"curve": c, "at": [0, -10]}));
|
||||
let sk = sketch(&s);
|
||||
let ci = sk.curve_index(&c).unwrap();
|
||||
assert!(matches!(sk.curves[ci].kind, CurveKind::Arc { .. }));
|
||||
// Upper half disc closes against the line.
|
||||
let a = profiles(&s);
|
||||
assert_eq!(a.len(), 1, "{a:?}");
|
||||
assert!(close(a[0], std::f64::consts::PI * 50.0), "{a:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fillet_and_chamfers() {
|
||||
let mut s = new_sketch();
|
||||
let r = run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [40, 30]}));
|
||||
let l = ids(&r["curves"]);
|
||||
// Pin the rectangle so a dimension change only moves the fillet.
|
||||
run(&mut s, "ConstraintFix", json!({"entity": l[2]}));
|
||||
run(&mut s, "ConstraintFix", json!({"entity": l[3]}));
|
||||
let f = run(&mut s, "FilletSketchCmd", json!({"a": l[0], "b": l[1], "radius": 5}));
|
||||
assert!(f["param"].is_string());
|
||||
let a = profiles(&s);
|
||||
assert!(close(a[0], 1200.0 - (25.0 - std::f64::consts::PI * 25.0 / 4.0)), "{a:?}");
|
||||
// The fillet radius is a dimension: change it.
|
||||
let pname = f["param"].as_str().unwrap().to_string();
|
||||
run(&mut s, "ChangeParameterCommand", json!({"name": pname, "expression": "8 mm"}));
|
||||
let a = profiles(&s);
|
||||
assert!(close(a[0], 1200.0 - (64.0 - std::f64::consts::PI * 64.0 / 4.0)), "{a:?}");
|
||||
run(&mut s, "ChamferSketchEqualDistance", json!({"a": l[2], "b": l[3], "distance": 4}));
|
||||
let a2 = profiles(&s)[0];
|
||||
assert!(close(a[0] - a2, 8.0), "{a2}");
|
||||
run(&mut s, "ChamferSketchDistanceDistance", json!({"a": l[1], "b": l[2], "distance": 2, "distance2": 6}));
|
||||
let a3 = profiles(&s)[0];
|
||||
assert!(close(a2 - a3, 6.0), "{a3}");
|
||||
// Distance-angle at the last corner (origin, 90 degrees): 45 deg gives an isosceles cut.
|
||||
run(&mut s, "ChamferSketchDistanceAngle", json!({"a": l[3], "b": l[0], "distance": 3, "angle": 45}));
|
||||
let a4 = profiles(&s)[0];
|
||||
assert!(close(a3 - a4, 4.5), "{a4}");
|
||||
assert!(s.execute("FilletSketchCmd", &json!({"a": l[0], "b": l[2], "radius": 1})).is_err(), "no shared corner");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_chain_mirror_patterns_move_scale() {
|
||||
let mut s = new_sketch();
|
||||
let r = run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [40, 30]}));
|
||||
let l = ids(&r["curves"]);
|
||||
// Outward offset by 2 (toward a point outside).
|
||||
let o = run(&mut s, "Offset", json!({"curves": [l[0]], "distance": 2, "side": [20, -10]}));
|
||||
assert_eq!(ids(&o["curves"]).len(), 4, "{o}");
|
||||
let a = profiles(&s);
|
||||
assert!(close(a[1], 44.0 * 34.0 - 1200.0) || close(a.iter().sum::<f64>(), 44.0 * 34.0), "{a:?}");
|
||||
// Circle offset inward.
|
||||
let c = ids(&run(&mut s, "CircleCenterRadius", json!({"center": [100, 0], "radius": 10}))["curves"])[0].clone();
|
||||
run(&mut s, "Offset", json!({"curves": [c], "distance": 3, "side": [100, 0]}));
|
||||
// Mirror a circle about a vertical line.
|
||||
let m = ids(&run(&mut s, "DrawPolyline", json!({"points": [[120, -50], [120, 50]]}))["curves"])[0].clone();
|
||||
let r = run(&mut s, "MirrorSketchCommand", json!({"entities": [c], "line": m}));
|
||||
let mc = ids(&r["curves"])[0].clone();
|
||||
let sk = sketch(&s);
|
||||
assert!(sk.center(sk.curve_index(&mc).unwrap()).unwrap().dist(Vec2::new(140.0, 0.0)) < 1e-7);
|
||||
// Circular pattern of the mirrored circle: 4 around (120, 0).
|
||||
let r = run(&mut s, "CircularSketchPatternCommand", json!({"entities": [mc], "center": [120, 0], "count": 4}));
|
||||
assert_eq!(ids(&r["curves"]).len(), 3);
|
||||
let sk = sketch(&s);
|
||||
let c2 = sk.center(sk.curve_index(&ids(&r["curves"])[0]).unwrap()).unwrap();
|
||||
assert!(c2.dist(Vec2::new(120.0, 20.0)) < 1e-7, "{c2:?}");
|
||||
// Rectangular pattern 3 x 2.
|
||||
let small = ids(&run(&mut s, "CircleCenterRadius", json!({"center": [0, 100], "radius": 1}))["curves"])[0].clone();
|
||||
let r = run(&mut s, "RectangularSketchPatternCommand", json!({"entities": [small], "count": 3, "spacing": 5, "count2": 2, "spacing2": 4}));
|
||||
assert_eq!(ids(&r["curves"]).len(), 5);
|
||||
// Move and scale.
|
||||
run(&mut s, "sketch.move", json!({"entities": [small], "translate": [0, 10]}));
|
||||
run(&mut s, "SketchScaleCmd", json!({"entities": [small], "base": [0, 110], "factor": 3}));
|
||||
let sk = sketch(&s);
|
||||
let ci = sk.curve_index(&small).unwrap();
|
||||
assert!(sk.center(ci).unwrap().dist(Vec2::new(0.0, 110.0)) < 1e-7);
|
||||
assert!(close(sk.radius(ci).unwrap(), 3.0));
|
||||
let r = run(&mut s, "sketch.move", json!({"entities": [small], "angle": 90, "center": [0, 0], "copy": true}));
|
||||
let cp = ids(&r["curves"])[0].clone();
|
||||
let sk = sketch(&s);
|
||||
assert!(sk.center(sk.curve_index(&cp).unwrap()).unwrap().dist(Vec2::new(-110.0, 0.0)) < 1e-7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_creation_tools() {
|
||||
let mut s = new_sketch();
|
||||
let r = run(&mut s, "SketchMidpointLine", json!({"mid": [0, 0], "end": [10, 5]}));
|
||||
let l = ids(&r["curves"])[0].clone();
|
||||
let sk = sketch(&s);
|
||||
let start = sk.resolve_point(&format!("{l}.start")).and_then(|i| sk.point(i)).unwrap();
|
||||
assert!(start.dist(Vec2::new(-10.0, -5.0)) < 1e-9);
|
||||
// Tangent arc off the end of a horizontal line.
|
||||
let h = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 20], [10, 20]]}))["curves"])[0].clone();
|
||||
let r = run(&mut s, "ArcTangent", json!({"start": format!("{h}.end"), "end": [20, 30]}));
|
||||
let a = ids(&r["curves"])[0].clone();
|
||||
let sk = sketch(&s);
|
||||
let ai = sk.curve_index(&a).unwrap();
|
||||
assert!(sk.center(ai).unwrap().dist(Vec2::new(10.0, 30.0)) < 1e-7, "{:?}", sk.center(ai));
|
||||
assert!(close(sk.radius(ai).unwrap(), 10.0));
|
||||
// 2-tangent circle in the corner of two lines.
|
||||
let x = ids(&run(&mut s, "DrawPolyline", json!({"points": [[100, 0], [150, 0]]}))["curves"])[0].clone();
|
||||
let y = ids(&run(&mut s, "DrawPolyline", json!({"points": [[100, 0], [100, 50]]}))["curves"])[0].clone();
|
||||
let r = run(&mut s, "CircleTanTanRadius", json!({"curves": [x, y], "radius": 5, "near": [106, 104 - 100]}));
|
||||
let c = ids(&r["curves"])[0].clone();
|
||||
let sk = sketch(&s);
|
||||
let ci = sk.curve_index(&c).unwrap();
|
||||
assert!(sk.center(ci).unwrap().dist(Vec2::new(105.0, 5.0)) < 1e-6, "{:?}", sk.center(ci));
|
||||
// 3-tangent circle inside a triangle: the incircle of the 3-4-5 triangle (r = 1).
|
||||
let t = run(&mut s, "DrawPolyline", json!({"points": [[200, 0], [203, 0], [200, 4]], "closed": true}));
|
||||
let tl = ids(&t["curves"]);
|
||||
let r = run(&mut s, "CircleThreeTangent", json!({"curves": tl, "near": [201, 1.2]}));
|
||||
let c = ids(&r["curves"])[0].clone();
|
||||
let sk = sketch(&s);
|
||||
let ci = sk.curve_index(&c).unwrap();
|
||||
assert!(close(sk.radius(ci).unwrap(), 1.0), "{:?}", sk.radius(ci));
|
||||
assert!(sk.center(ci).unwrap().dist(Vec2::new(201.0, 1.0)) < 1e-6);
|
||||
// Slots.
|
||||
let before = profiles(&s).len();
|
||||
run(&mut s, "ShapeSlotCenterPoint", json!({"center": [0, -100], "end": [10, -100], "width": 4}));
|
||||
run(&mut s, "ShapeArcSlotThreePoint", json!({"start": [100, -100], "through": [110, -90], "end": [120, -100], "width": 2}));
|
||||
run(&mut s, "ShapeArcSlotCenterTwoPoint", json!({"center": [200, -100], "start": [210, -100], "end": [200, -90], "width": 2}));
|
||||
let a = profiles(&s);
|
||||
assert_eq!(a.len(), before + 3, "{a:?}");
|
||||
let pi = std::f64::consts::PI;
|
||||
assert!(a.iter().any(|x| close(*x, 20.0 * 4.0 + pi * 4.0)), "{a:?}");
|
||||
// Half-circle arc slot: centre arc length pi*10, width 2, plus a full cap circle.
|
||||
assert!(a.iter().any(|x| close(*x, pi * 10.0 * 2.0 + pi)), "{a:?}");
|
||||
assert!(a.iter().any(|x| close(*x, pi * 5.0 * 2.0 + pi)), "{a:?}");
|
||||
}
|
||||
Reference in new issue
Block a user