Merge pull request #123 from CypherNaught-0x/feat/sketch-tools-return-ids

Sketch commands return the ids of the geometry they create
This commit is contained in:
bherbruck authored and GitHub committed 2026-10-11 08:17:05 -04:00
commit b02c5c01f9
12 files changed
+542 -122

No files matched your search

+8
View File
@@ -2,6 +2,13 @@
//! `sketch.rectangle.two_point`); the toolbar placement (`tab`, `panel`) follows the familiar
//! Design workspace layout. Ids used before the rename still resolve (`legacy_ids`).
/// The params-doc note of every command that adds sketch geometry (see `sketch::created`).
macro_rules! created_doc {
() => {
". Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)"
};
}
mod browser;
mod clipboard;
mod component;
@@ -30,6 +37,7 @@ mod plastic;
mod sheet;
mod sketch;
pub(crate) use sketch::author_path_for_rebind;
pub(crate) use sketch::created as sketch_created;
mod sketch_3d;
mod sketch_canvas;
mod sketch_constraints;
+51 -16
View File
@@ -34,74 +34,74 @@ pub static COMMANDS: &[CommandSpec] = &[
.icon("line")
.key("L")
.enabled(in_sketch)
.params(concat!("points: [[x,y] | \"l1.end\" | {at, ref}…], closed?: bool, construction?: bool, ids?: [..], infer?: bool (add horizontal/vertical)", connect_doc!())),
.params(concat!("points: [[x,y] | \"l1.end\" | {at, ref}…], closed?: bool, construction?: bool, ids?: [..], infer?: bool (add horizontal/vertical)", connect_doc!(), created_doc!())),
CommandSpec::new("sketch.draw.dimensioned", "Draw Dimensioned Sketch Geometry", draw_dimensioned)
.enabled(in_sketch)
.params("command: sketch creation command, params: object, dimensions: object of role to numeric or expression value; creates geometry and all driving dimensions atomically in one undo step"),
.params(concat!("command: sketch creation command, params: object, dimensions: object of role to numeric or expression value; creates geometry and all driving dimensions atomically in one undo step", created_doc!())),
CommandSpec::new("sketch.rectangle.two_point", "2-Point Rectangle", rect_two_point)
.at("SKETCH", "CREATE")
.icon("rect")
.key("R")
.enabled(in_sketch)
.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!())),
.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!(), created_doc!())),
CommandSpec::new("sketch.rectangle.three_point", "3-Point Rectangle", rect_three_point)
.at("SKETCH", "CREATE")
.icon("rect3")
.enabled(in_sketch)
.params(concat!("p0, p1: first edge; p2: point on the opposite edge", connect_doc!())),
.params(concat!("p0, p1: first edge; p2: point on the opposite edge", connect_doc!(), created_doc!())),
CommandSpec::new("sketch.rectangle.center", "Center Rectangle", rect_center)
.at("SKETCH", "CREATE")
.icon("rect_center")
.enabled(in_sketch)
.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!())),
.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!(), created_doc!())),
CommandSpec::new("sketch.circle.center", "Center Diameter Circle", circle_center)
.at("SKETCH", "CREATE")
.icon("circle")
.key("C")
.enabled(in_sketch)
.params(concat!("center: [x,y] or point ref, radius | diameter: number", connect_doc!())),
.params(concat!("center: [x,y] or point ref, radius | diameter: number", connect_doc!(), created_doc!())),
CommandSpec::new("sketch.circle.two_point", "2-Point Circle", circle_two)
.at("SKETCH", "CREATE")
.icon("circle2")
.enabled(in_sketch)
.params(concat!("p0, p1: diameter ends", connect_doc!())),
.params(concat!("p0, p1: diameter ends", connect_doc!(), created_doc!())),
CommandSpec::new("sketch.circle.three_point", "3-Point Circle", circle_three)
.at("SKETCH", "CREATE")
.icon("circle3")
.enabled(in_sketch)
.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(concat!("start, end, through", connect_doc!())),
.params(concat!("p0, p1, p2", connect_doc!(), created_doc!())),
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!(), created_doc!())),
CommandSpec::new("sketch.arc.center_point", "Center Point Arc", arc_center)
.at("SKETCH", "CREATE")
.icon("arc_center")
.enabled(in_sketch)
.params(concat!("center, start, end (counter-clockwise from start) | center, start, sweep: signed degrees", connect_doc!())),
.params(concat!("center, start, end (counter-clockwise from start) | center, start, sweep: signed degrees", connect_doc!(), created_doc!())),
CommandSpec::new("sketch.polygon.inscribed", "Inscribed Polygon", polygon_inscribed)
.at("SKETCH", "CREATE")
.icon("polygon")
.enabled(in_sketch)
.params(concat!("center, radius (to vertices), sides, angle?: deg", connect_doc!())),
.params(concat!("center, radius (to vertices), sides, angle?: deg", connect_doc!(), created_doc!())),
CommandSpec::new("sketch.polygon.circumscribed", "Circumscribed Polygon", polygon_circumscribed)
.at("SKETCH", "CREATE")
.icon("polygon")
.enabled(in_sketch)
.params(concat!("center, radius (to edge midpoints), sides, angle?: deg", connect_doc!())),
.params(concat!("center, radius (to edge midpoints), sides, angle?: deg", connect_doc!(), created_doc!())),
CommandSpec::new("sketch.polygon.edge", "Edge Polygon", polygon_edge)
.at("SKETCH", "CREATE")
.icon("polygon")
.enabled(in_sketch)
.params(concat!("p0, p1: one edge, sides", connect_doc!())),
.params(concat!("p0, p1: one edge, sides", connect_doc!(), created_doc!())),
CommandSpec::new("sketch.slot.center_to_center", "Center to Center Slot", slot_c2c)
.at("SKETCH", "CREATE")
.icon("slot")
.enabled(in_sketch)
.params(concat!("p0, p1: arc centres, width", connect_doc!())),
.params(concat!("p0, p1: arc centres, width", connect_doc!(), created_doc!())),
CommandSpec::new("sketch.slot.overall", "Overall Slot", slot_overall)
.at("SKETCH", "CREATE")
.icon("slot")
.enabled(in_sketch)
.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(concat!("point: [x,y], id?", connect_doc!())),
.params(concat!("p0, p1: overall ends, width", connect_doc!(), created_doc!())),
CommandSpec::new("sketch.point", "Point", draw_point).at("SKETCH", "CREATE").icon("point").enabled(in_sketch).params(concat!("point: [x,y], id?", connect_doc!(), created_doc!())),
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); text_at?: [x, y]. Two lines: the angle between them (acute, or 180° − it when the value is nearer that) unless text_at picks a sector. Returns measured in unit (mm or deg)",
),
@@ -353,6 +353,37 @@ pub(super) fn mark_construction(sk: &mut Sketch, curves: &[usize], p: &Value) {
}
}
/// What a command added to a sketch: the ids of the points, curves, constraints and dimensions
/// in `after` that `before` does not have (`None`: a new sketch), in creation order, plus the
/// 3D curves (`wires`) when there are any. Ids are stable and never reused in a sketch, so they
/// can be used in later commands (`sketch.dimension`, `sketch.constraint.*`, `sketch.delete`…).
pub(crate) fn created(before: Option<&Sketch>, after: &Sketch) -> Value {
use std::collections::HashSet;
let empty = Sketch::new();
let before = before.unwrap_or(&empty);
let old: HashSet<&str> = before
.points
.iter()
.map(|x| x.id.as_str())
.chain(before.curves.iter().map(|x| x.id.as_str()))
.chain(before.constraints.iter().map(|x| x.id.as_str()))
.chain(before.wires.iter().map(|x| x.id.as_str()))
.collect();
let new = |id: &&str| !old.contains(*id);
let points: Vec<&str> = after.points.iter().map(|x| x.id.as_str()).filter(new).collect();
let curves: Vec<&str> = after.curves.iter().map(|x| x.id.as_str()).filter(new).collect();
let (dims, cons): (Vec<_>, Vec<_>) = after.constraints.iter().filter(|c| new(&c.id.as_str())).partition(|c| c.kind.is_dimension());
let ids = |v: Vec<&solvecraft_sketch::Constraint>| v.into_iter().map(|c| c.id.clone()).collect::<Vec<_>>();
let mut out = json!({"points": points, "curves": curves, "constraints": ids(cons), "dimensions": ids(dims)});
let wires: Vec<&str> = after.wires.iter().map(|x| x.id.as_str()).filter(new).collect();
if !wires.is_empty()
&& let Some(o) = out.as_object_mut()
{
o.insert("wires".into(), json!(wires));
}
out
}
pub(super) fn result(sk_out: (Vec<String>, Vec<String>), info: Value) -> Value {
json!({"curves": sk_out.0, "constraints": sk_out.1, "sketch": info})
}
@@ -1909,3 +1940,7 @@ mod rect_tests;
#[cfg(test)]
#[path = "sketch_ux_tests.rs"]
mod ux_tests;
#[cfg(test)]
#[path = "sketch_ids_tests.rs"]
mod ids_tests;
+8 -5
View File
@@ -12,13 +12,16 @@ use crate::params::{bad, req_vec3, vec3};
use crate::{Result, Session};
pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("sketch.line3d", "3D Line", line3d)
.enabled(in_sketch)
.params("points: [[x,y,z]…] world points (2 or more): one 3D line per segment, joined end to end (needs 3D Sketch on)"),
CommandSpec::new("sketch.line3d", "3D Line", line3d).enabled(in_sketch).params(concat!(
"points: [[x,y,z]…] world points (2 or more): one 3D line per segment, joined end to end (needs 3D Sketch on)",
created_doc!()
)),
CommandSpec::new("sketch.spline3d", "3D Spline", spline3d)
.enabled(in_sketch)
.params("points: [[x,y,z]…] world fit points (2 or more): a smooth 3D curve through them (needs 3D Sketch on)"),
CommandSpec::new("sketch.point3d", "3D Point", point3d).enabled(in_sketch).params("point: [x,y,z] world point (needs 3D Sketch on)"),
.params(concat!("points: [[x,y,z]…] world fit points (2 or more): a smooth 3D curve through them (needs 3D Sketch on)", created_doc!())),
CommandSpec::new("sketch.point3d", "3D Point", point3d)
.enabled(in_sketch)
.params(concat!("point: [x,y,z] world point (needs 3D Sketch on)", created_doc!())),
CommandSpec::new("sketch.move3d", "Move 3D Point", move3d)
.enabled(in_sketch)
.params("wire: drawn 3D curve or point id, index?: fit point (default 0), to: [x,y,z] | by: [dx,dy,dz]; points joined to it move too"),
+75 -77
View File
@@ -10,83 +10,81 @@ 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("sketch.line.midpoint", "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("sketch.arc.tangent", "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("sketch.circle.two_tangent", "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("sketch.circle.three_tangent", "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("sketch.slot.center_point", "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("sketch.slot.arc_three_point", "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("sketch.slot.arc_center", "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"),
CommandSpec::new("sketch.ellipse", "Ellipse", ellipse)
.at("SKETCH", "CREATE")
.icon("ellipse")
.enabled(in_sketch)
.params("center, major: end of the major axis [x,y], minor: a point the ellipse passes through on the minor side [x,y] | minor_radius"),
CommandSpec::new("sketch.spline.fit_point", "Fit Point Spline", spline_fit)
.at("SKETCH", "CREATE")
.icon("spline")
.enabled(in_sketch)
.params("points: [[x,y] or point refs…] (2…500), periodic?: boolean (default false); periodic cubic fit closes smoothly through at least 3 distinct samples"),
CommandSpec::new("sketch.spline.control_point", "Control Point Spline", spline_cv3)
.at("SKETCH", "CREATE")
.icon("spline_cv")
.enabled(in_sketch)
.params("points: control points (2…500), cubic"),
CommandSpec::new("sketch.spline.control_point_5", "Control Point Spline (Degree 5)", spline_cv5)
.at("SKETCH", "CREATE")
.icon("spline_cv")
.enabled(in_sketch)
.params("points: control points (2…500), degree 5"),
CommandSpec::new("sketch.conic", "Conic Curve", conic)
.at("SKETCH", "CREATE")
.icon("conic")
.enabled(in_sketch)
.params("start, end, apex: [x,y] or point refs; rho?: 0…1 (default 0.5: parabola)"),
CommandSpec::new("sketch.blend_curve", "Blend Curve", blend)
.at("SKETCH", "MODIFY")
.icon("blend")
.enabled(in_sketch)
.params("a, b: end points of two curves (\"l1.end\", \"a2.start\"): a smooth (tangent) spline joining them"),
CommandSpec::new("sketch.centerline", "Centerline", centerline)
.enabled(in_sketch)
.params("curves: [line ids], value?: bool (default toggles): centerline line type (not in profiles; an axis)"),
CommandSpec::new("sketch.text", "Text", text)
.at("SKETCH", "CREATE")
.icon("text")
.enabled(in_sketch)
.params("text: string (\\n breaks lines), at: baseline start [x,y], height?: capital height mm (default 5), angle?: deg"),
CommandSpec::new("sketch.edit_text", "Edit Text", edit_text)
.enabled(in_sketch)
.params("link: the text's link id (or entity: one of its curves), text?, at?, height?, angle?: deg"),
];
pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("sketch.line.midpoint", "Midpoint Line", midpoint_line)
.at("SKETCH", "CREATE")
.icon("line_mid")
.enabled(in_sketch)
.params(concat!("mid: [x,y] or point ref, end: [x,y]; construction?", created_doc!())),
CommandSpec::new("sketch.arc.tangent", "Tangent Arc", arc_tangent).at("SKETCH", "CREATE").icon("arc_tangent").enabled(in_sketch).params(concat!(
"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)",
created_doc!()
)),
CommandSpec::new("sketch.circle.two_tangent", "2-Tangent Circle", circle_tan_tan)
.at("SKETCH", "CREATE")
.icon("circle_tt")
.enabled(in_sketch)
.params(concat!("curves: [two lines/circles/arcs], radius | diameter: expr, near: [x,y] (which of the solutions)", created_doc!())),
CommandSpec::new("sketch.circle.three_tangent", "3-Tangent Circle", circle_three_tan)
.at("SKETCH", "CREATE")
.icon("circle_ttt")
.enabled(in_sketch)
.params(concat!("curves: [three lines/circles/arcs], near: [x,y] (which of the solutions)", created_doc!())),
CommandSpec::new("sketch.slot.center_point", "Center Point Slot", slot_center_point)
.at("SKETCH", "CREATE")
.icon("slot")
.enabled(in_sketch)
.params(concat!("center: slot centre, end: one arc centre, width", created_doc!())),
CommandSpec::new("sketch.slot.arc_three_point", "Three Point Arc Slot", arc_slot_three)
.at("SKETCH", "CREATE")
.icon("arc_slot")
.enabled(in_sketch)
.params(concat!("start, through, end: points on the centre arc, width", created_doc!())),
CommandSpec::new("sketch.slot.arc_center", "Center Point Arc Slot", arc_slot_center)
.at("SKETCH", "CREATE")
.icon("arc_slot")
.enabled(in_sketch)
.params(concat!("center, start: centre arc start, end: direction of the centre arc end (counter-clockwise), width", created_doc!())),
CommandSpec::new("sketch.ellipse", "Ellipse", ellipse).at("SKETCH", "CREATE").icon("ellipse").enabled(in_sketch).params(concat!(
"center, major: end of the major axis [x,y], minor: a point the ellipse passes through on the minor side [x,y] | minor_radius",
created_doc!()
)),
CommandSpec::new("sketch.spline.fit_point", "Fit Point Spline", spline_fit)
.at("SKETCH", "CREATE")
.icon("spline")
.enabled(in_sketch)
.params(concat!("points: [[x,y] or point refs…] (2…500): the spline passes through them, periodic?: boolean (default false); periodic cubic fit closes smoothly through at least 3 distinct samples", created_doc!())),
CommandSpec::new("sketch.spline.control_point", "Control Point Spline", spline_cv3)
.at("SKETCH", "CREATE")
.icon("spline_cv")
.enabled(in_sketch)
.params(concat!("points: control points (2…500), cubic", created_doc!())),
CommandSpec::new("sketch.spline.control_point_5", "Control Point Spline (Degree 5)", spline_cv5)
.at("SKETCH", "CREATE")
.icon("spline_cv")
.enabled(in_sketch)
.params(concat!("points: control points (2…500), degree 5", created_doc!())),
CommandSpec::new("sketch.conic", "Conic Curve", conic)
.at("SKETCH", "CREATE")
.icon("conic")
.enabled(in_sketch)
.params(concat!("start, end, apex: [x,y] or point refs; rho?: 0…1 (default 0.5: parabola)", created_doc!())),
CommandSpec::new("sketch.blend_curve", "Blend Curve", blend)
.at("SKETCH", "MODIFY")
.icon("blend")
.enabled(in_sketch)
.params(concat!("a, b: end points of two curves (\"l1.end\", \"a2.start\"): a smooth (tangent) spline joining them", created_doc!())),
CommandSpec::new("sketch.centerline", "Centerline", centerline)
.enabled(in_sketch)
.params("curves: [line ids], value?: bool (default toggles): centerline line type (not in profiles; an axis)"),
CommandSpec::new("sketch.text", "Text", text).at("SKETCH", "CREATE").icon("text").enabled(in_sketch).params(concat!(
"text: string (\\n breaks lines), at: baseline start [x,y], height?: capital height mm (default 5), angle?: deg",
created_doc!()
)),
CommandSpec::new("sketch.edit_text", "Edit Text", edit_text)
.enabled(in_sketch)
.params(concat!("link: the text's link id (or entity: one of its curves), text?, at?, height?, angle?: deg", created_doc!())),
];
fn text_source(p: &Value, cmd: &str, old: Option<(String, Vec2, f64, f64)>) -> Result<LinkSource> {
let (t0, at0, h0, a0) = old.unwrap_or((String::new(), Vec2::ZERO, 5.0, 0.0));
+330
View File
@@ -0,0 +1,330 @@
//! Every command that adds sketch geometry returns `created: {points, curves, constraints,
//! dimensions}` with the real ids of what it added, so a script can dimension and constrain the
//! new geometry right away; ids are never reused after a delete.
use serde_json::{Value, json};
use solvecraft_sketch::{CurveKind, Sketch};
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 strs(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) -> Sketch {
s.doc.sketch(s.active_sketch.unwrap()).unwrap().clone()
}
fn new_sketch() -> Session {
let mut s = Session::default();
run(&mut s, "sketch.create", json!({"plane": "XY"}));
s
}
/// The ids in `r.created` exist in the active sketch, and each curve and point takes a
/// dimension (driven, so it never conflicts) and a fix right away. `before`: the sketch before
/// the command. Returns `created`.
fn check(s: &mut Session, cmd: &str, r: &Value, before: &Sketch) -> Value {
let c = r.get("created").cloned().unwrap_or_else(|| panic!("{cmd}: no `created` in {r}"));
for k in ["points", "curves", "constraints", "dimensions"] {
assert!(c[k].is_array(), "{cmd}: created.{k} in {c}");
}
let sk = sketch(s);
let curves = strs(&c["curves"]);
let points = strs(&c["points"]);
assert!(!curves.is_empty() || !points.is_empty() || c.get("wires").is_some(), "{cmd}: nothing created: {c}");
for id in &points {
assert!(sk.point_index(id).is_some(), "{cmd}: point {id} not in the sketch");
assert_ne!(id, "origin", "{cmd}");
}
for id in strs(&c["constraints"]).iter().chain(&strs(&c["dimensions"])) {
assert!(sk.constraints.iter().any(|k| &k.id == id), "{cmd}: constraint {id} not in the sketch");
}
for id in strs(&c["dimensions"]) {
assert!(sk.constraints.iter().any(|k| k.id == id && k.kind.is_dimension()), "{cmd}: {id} is not a dimension");
}
for id in strs(&c.get("wires").cloned().unwrap_or_default()) {
assert!(sk.wire_index(&id).is_some(), "{cmd}: wire {id} not in the sketch");
}
// The new curves in the command's own list (where it has one) are in `created`.
let wires = strs(&c.get("wires").cloned().unwrap_or_default());
for id in strs(&r["curves"]) {
let old = before.curve_index(&id).is_some() || before.wire_index(&id).is_some();
assert!(old || curves.contains(&id) || wires.contains(&id), "{cmd}: returned curve {id} missing from created {c}");
}
for id in curves.iter().chain(&points) {
assert!(before.curve_index(id).is_none() && before.point_index(id).is_none(), "{cmd}: {id} is not new");
}
for id in &curves {
let i = sk.curve_index(id).unwrap_or_else(|| panic!("{cmd}: curve {id} not in the sketch"));
let measurable = matches!(sk.curves[i].kind, CurveKind::Line { .. } | CurveKind::Circle { .. } | CurveKind::Arc { .. });
if measurable {
run(s, "sketch.dimension", json!({"entities": [id], "driven": true}));
}
run(s, "sketch.constraint.fix", json!({"entity": id, "fixed": true}));
run(s, "edit.undo", json!({}));
if measurable {
run(s, "edit.undo", json!({}));
}
}
for id in &points {
run(s, "sketch.dimension", json!({"entities": [id, "origin"], "driven": true}));
run(s, "edit.undo", json!({}));
}
c
}
/// Run `cmd` in `s` and check what it reports.
fn made(s: &mut Session, cmd: &str, p: Value) -> Value {
let before = sketch(s);
let r = run(s, cmd, p);
check(s, cmd, &r, &before)
}
#[test]
fn drawing_commands_return_the_ids_they_create() {
let cases: Vec<(&str, Value)> = vec![
("sketch.line", json!({"points": [[0, 0], [10, 0], [10, 5]]})),
("sketch.rectangle.two_point", json!({"p0": [1, 1], "p1": [21, 15]})),
("sketch.rectangle.three_point", json!({"p0": [0, 0], "p1": [20, 5], "p2": [18, 15]})),
("sketch.rectangle.center", json!({"center": [3, 2], "corner": [13, 9]})),
("sketch.circle.center", json!({"center": [5, 5], "diameter": 8})),
("sketch.circle.two_point", json!({"p0": [0, 0], "p1": [10, 0]})),
("sketch.circle.three_point", json!({"p0": [0, 0], "p1": [10, 0], "p2": [5, 6]})),
("sketch.arc.three_point", json!({"start": [0, 0], "end": [10, 0], "through": [5, 4]})),
("sketch.arc.center_point", json!({"center": [0, 0], "start": [10, 0], "end": [0, 10]})),
("sketch.polygon.inscribed", json!({"center": [0, 0], "radius": 10, "sides": 6})),
("sketch.polygon.circumscribed", json!({"center": [0, 0], "radius": 10, "sides": 5})),
("sketch.polygon.edge", json!({"p0": [0, 0], "p1": [10, 0], "sides": 4})),
("sketch.slot.center_to_center", json!({"p0": [0, 0], "p1": [20, 0], "width": 6})),
("sketch.slot.overall", json!({"p0": [0, 0], "p1": [20, 0], "width": 6})),
("sketch.slot.center_point", json!({"center": [0, 0], "end": [10, 0], "width": 4})),
("sketch.slot.arc_three_point", json!({"start": [0, 0], "through": [10, 5], "end": [20, 0], "width": 3})),
("sketch.slot.arc_center", json!({"center": [0, 0], "start": [10, 0], "end": [0, 10], "width": 3})),
("sketch.point", json!({"point": [3, 4]})),
("sketch.line.midpoint", json!({"mid": [0, 0], "end": [10, 5]})),
("sketch.ellipse", json!({"center": [0, 0], "major": [10, 0], "minor_radius": 4})),
("sketch.spline.fit_point", json!({"points": [[0, 0], [5, 5], [10, 0]]})),
("sketch.spline.fit_point", json!({"points": [[0, 0], [5, 5], [10, 0]], "periodic": true})),
("sketch.spline.control_point", json!({"points": [[0, 0], [5, 5], [10, 0], [15, 5]]})),
("sketch.spline.control_point_5", json!({"points": [[0, 0], [3, 5], [6, 0], [9, 5], [12, 0], [15, 5]]})),
("sketch.conic", json!({"start": [0, 0], "end": [10, 0], "apex": [5, 6]})),
("sketch.text", json!({"text": "IT", "at": [0, 0], "height": 10})),
];
for (cmd, p) in cases {
let mut s = new_sketch();
made(&mut s, cmd, p);
}
}
#[test]
fn drawn_ids_take_driving_dimensions_and_constraints() {
let mut s = new_sketch();
let c = made(&mut s, "sketch.line", json!({"points": [[0, 0], [10, 0]], "connect": false}));
let l = strs(&c["curves"]);
let pts = strs(&c["points"]);
assert_eq!((l.len(), pts.len()), (1, 2), "{c}");
run(&mut s, "sketch.constraint.coincident", json!({"a": pts[0], "b": "origin"}));
run(&mut s, "sketch.constraint.horizontal_vertical", json!({"line": l[0]}));
let d = run(&mut s, "sketch.dimension", json!({"entities": [l[0]], "value": 30}));
assert_eq!(strs(&d["created"]["dimensions"]).len(), 1, "{d}");
let sk = sketch(&s);
let end = sk.point(sk.point_index(&pts[1]).unwrap()).unwrap();
assert!((end.x - 30.0).abs() < 1e-6 && end.y.abs() < 1e-6, "{end:?}");
let c = made(&mut s, "sketch.circle.center", json!({"center": [50, 0], "radius": 4}));
let circle = strs(&c["curves"]);
run(&mut s, "sketch.dimension", json!({"entities": [circle[0]], "type": "diameter", "value": 12}));
let sk = sketch(&s);
let i = sk.curve_index(&circle[0]).unwrap();
assert!(matches!(sk.curves[i].kind, CurveKind::Circle { r, .. } if (r - 6.0).abs() < 1e-6));
}
#[test]
fn modify_commands_return_the_ids_they_create() {
// Corner tools: an open L of two lines.
for (cmd, extra) in [
("sketch.fillet", json!({"radius": 2})),
("sketch.chamfer.equal_distance", json!({"distance": 2})),
("sketch.chamfer.distance_angle", json!({"distance": 2, "angle": 40})),
("sketch.chamfer.two_distance", json!({"distance": 2, "distance2": 3})),
] {
let mut s = new_sketch();
let c = made(&mut s, "sketch.line", json!({"points": [[0, 10], [0, 0], [10, 0]]}));
let l = strs(&c["curves"]);
let mut p = extra;
p["point"] = json!(format!("{}.end", l[0]));
let c = made(&mut s, cmd, p);
assert!(!strs(&c["curves"]).is_empty(), "{cmd}: {c}");
}
let mut s = new_sketch();
let rect = strs(&made(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [20, 10]}))["curves"]);
let c = made(&mut s, "sketch.offset", json!({"curves": [rect[0]], "distance": 2, "side": [10, 5]}));
assert_eq!(strs(&c["curves"]).len(), 4, "{c}");
made(&mut s, "sketch.move", json!({"entities": [rect[0]], "translate": [0, -20], "copy": true}));
made(&mut s, "sketch.pattern.rectangular", json!({"entities": [rect[2]], "count": 3, "spacing": 30}));
let circle = strs(&made(&mut s, "sketch.circle.center", json!({"center": [100, 0], "radius": 2}))["curves"]);
made(&mut s, "sketch.pattern.circular", json!({"entities": [circle[0]], "center": [90, 0], "count": 4}));
let axis = strs(&made(&mut s, "sketch.line", json!({"points": [[200, -10], [200, 10]]}))["curves"]);
let sm = strs(&made(&mut s, "sketch.circle.center", json!({"center": [190, 0], "radius": 2}))["curves"]);
made(&mut s, "sketch.mirror", json!({"entities": [sm[0]], "line": axis[0]}));
// Break and trim split a curve where another crosses it.
let mut s = new_sketch();
let h = strs(&made(&mut s, "sketch.line", json!({"points": [[0, 0], [20, 0]]}))["curves"]);
made(&mut s, "sketch.line", json!({"points": [[10, -5], [10, 5]]}));
made(&mut s, "sketch.break", json!({"curve": h[0], "at": [5, 0]}));
let mut s = new_sketch();
let h = strs(&made(&mut s, "sketch.line", json!({"points": [[0, 0], [30, 0]]}))["curves"]);
made(&mut s, "sketch.line", json!({"points": [[10, -5], [10, 5]]}));
made(&mut s, "sketch.line", json!({"points": [[20, -5], [20, 5]]}));
made(&mut s, "sketch.trim", json!({"curve": h[0], "at": [15, 0]}));
// Tangent tools and blends start from existing curves.
let mut s = new_sketch();
let l = strs(&made(&mut s, "sketch.line", json!({"points": [[0, 0], [10, 0]]}))["curves"]);
made(&mut s, "sketch.arc.tangent", json!({"start": format!("{}.end", l[0]), "end": [20, 10]}));
let a = strs(&made(&mut s, "sketch.line", json!({"points": [[0, 30], [10, 30]]}))["curves"]);
let b = strs(&made(&mut s, "sketch.line", json!({"points": [[20, 40], [30, 40]]}))["curves"]);
made(&mut s, "sketch.blend_curve", json!({"a": format!("{}.end", a[0]), "b": format!("{}.start", b[0])}));
let x = strs(&made(&mut s, "sketch.line", json!({"points": [[100, 0], [140, 0]], "connect": false}))["curves"]);
let y = strs(&made(&mut s, "sketch.line", json!({"points": [[100, 0], [100, 40]], "connect": false}))["curves"]);
made(&mut s, "sketch.circle.two_tangent", json!({"curves": [x[0], y[0]], "radius": 5, "near": [106, 4]}));
let z = strs(&made(&mut s, "sketch.line", json!({"points": [[100, 40], [140, 40]], "connect": false}))["curves"]);
made(&mut s, "sketch.circle.three_tangent", json!({"curves": [x[0], y[0], z[0]], "near": [120, 20]}));
}
#[test]
fn dimensioned_drawing_reports_its_geometry_and_driving_dimensions() {
// The creator runs nested inside sketch.draw.dimensioned; `created` must still cover the
// dimensions the wrapper adds afterwards, not just the nested creator's geometry.
for (cmd, p, dims) in [
("sketch.line", json!({"points": [[0, 0], [5, 5]]}), json!({"length": 10, "angle": 45})),
("sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [20, 10]}), json!({"width": 30, "height": 15})),
("sketch.circle.center", json!({"center": [0, 0], "radius": 3}), json!({"diameter": 12})),
("sketch.polygon.inscribed", json!({"center": [0, 0], "radius": 10, "sides": 6}), json!({"radius": 8})),
] {
let mut s = new_sketch();
let c = made(&mut s, "sketch.draw.dimensioned", json!({"command": cmd, "params": p, "dimensions": dims}));
let n = dims.as_object().map_or(0, |d| d.len());
assert_eq!(strs(&c["dimensions"]).len(), n, "{cmd}: {c}");
assert!(!strs(&c["curves"]).is_empty(), "{cmd}: {c}");
}
}
#[test]
fn breaking_an_offset_association_creates_nothing() {
let mut s = new_sketch();
let e = strs(&made(&mut s, "sketch.ellipse", json!({"center": [0, 0], "major": [10, 0], "minor_radius": 5, "connect": false}))["curves"]);
let c = made(&mut s, "sketch.offset", json!({"curves": [e[0]], "distance": -1, "chain": false}));
let out = strs(&c["curves"]);
let r = run(&mut s, "sketch.offset.break", json!({"curve": out[0]}));
let c = &r["created"];
for k in ["points", "curves", "constraints", "dimensions"] {
assert_eq!(c[k], json!([]), "{r}");
}
}
#[test]
fn text_edits_and_imports_return_the_ids_they_create() {
let mut s = new_sketch();
let r = run(&mut s, "sketch.text", json!({"text": "I", "at": [0, 0], "height": 10}));
check(&mut s, "sketch.text", &r, &Sketch::new());
let before = sketch(&s);
let link = r["link"].as_str().map(str::to_string).unwrap_or_else(|| panic!("{r}"));
let r = run(&mut s, "sketch.edit_text", json!({"link": link, "text": "II"}));
check(&mut s, "sketch.edit_text", &r, &before);
let svg = r#"<svg width="40mm" height="40mm" viewBox="0 0 40 40"><path d="M0 0 H40 V40 H0 Z"/></svg>"#;
let mut s = new_sketch();
let c = made(&mut s, "sketch.insert_svg", json!({"text": svg}));
assert_eq!(strs(&c["curves"]).len(), 4, "{c}");
let dxf = run(&mut s, "sketch.export_dxf", json!({}))["dxf"].as_str().unwrap_or_default().to_string();
let mut s = new_sketch();
let c = made(&mut s, "sketch.insert_dxf", json!({"text": dxf}));
assert_eq!(strs(&c["curves"]).len(), 4, "{c}");
}
#[test]
fn projections_and_3d_curves_return_the_ids_they_create() {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"corner": [0, 0, 0], "length": 20, "width": 20, "height": 10}));
let body = s.model.state().bodies.first().map(|b| b.name.clone()).unwrap_or_default();
run(&mut s, "sketch.create", json!({"plane": "XY", "offset": 5}));
made(&mut s, "sketch.project", json!({"refs": [{"edge": [10, 0, 10]}]}));
made(&mut s, "sketch.intersect", json!({"refs": [{"body": body}]}));
made(&mut s, "sketch.include_3d", json!({"refs": [{"edge": [20, 10, 10]}]}));
made(&mut s, "sketch.fit_curves_to_section", json!({"body": body}));
made(&mut s, "sketch.isoparametric_curve", json!({"face": [10, 10, 10]}));
let mut s = new_sketch();
run(&mut s, "sketch.options", json!({"sketch_3d": true}));
let c = made(&mut s, "sketch.line3d", json!({"points": [[0, 0, 0], [0, 0, 10], [10, 0, 10]]}));
assert_eq!(strs(&c["wires"]).len(), 2, "{c}");
made(&mut s, "sketch.spline3d", json!({"points": [[0, 0, 0], [5, 5, 5], [10, 0, 10]]}));
made(&mut s, "sketch.point3d", json!({"point": [5, 5, 5]}));
}
#[test]
fn a_new_sketch_reports_its_projected_face_edges() {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"corner": [0, 0, 0], "length": 20, "width": 20, "height": 10}));
let r = run(&mut s, "sketch.create", json!({"plane": {"face": [10, 10, 10]}}));
let c = check(&mut s, "sketch.create", &r, &Sketch::new());
assert_eq!(strs(&c["curves"]).len(), 4, "{c}");
}
#[test]
fn ids_are_never_reused_after_a_delete() {
let mut s = new_sketch();
let first = made(&mut s, "sketch.line", json!({"points": [[0, 0], [10, 0]]}));
let circle = made(&mut s, "sketch.circle.center", json!({"center": [30, 0], "radius": 3}));
let dim = run(&mut s, "sketch.dimension", json!({"entities": [strs(&circle["curves"])[0]], "value": 8}));
let mut gone: Vec<String> = ["points", "curves"].iter().flat_map(|k| strs(&first[k]).into_iter().chain(strs(&circle[k]))).collect();
gone.extend(strs(&dim["created"]["dimensions"]));
// Deleting the curves takes their own end and centre points (and the dimension) with them.
run(&mut s, "sketch.delete", json!({"entities": strs(&first["curves"]).into_iter().chain(strs(&circle["curves"])).collect::<Vec<_>>()}));
let sk = sketch(&s);
assert!(sk.curves.is_empty() && sk.points.len() == 1 && sk.constraints.is_empty(), "{:?}", sk.curves);
// The same commands again: every id is new.
let again = made(&mut s, "sketch.line", json!({"points": [[0, 0], [10, 0]]}));
let circle2 = made(&mut s, "sketch.circle.center", json!({"center": [30, 0], "radius": 3}));
let dim2 = run(&mut s, "sketch.dimension", json!({"entities": [strs(&circle2["curves"])[0]], "value": 8}));
let mut fresh: Vec<String> = ["points", "curves"].iter().flat_map(|k| strs(&again[k]).into_iter().chain(strs(&circle2[k]))).collect();
fresh.extend(strs(&dim2["created"]["dimensions"]));
assert_eq!(fresh.len(), gone.len(), "{fresh:?} {gone:?}");
for id in &fresh {
assert!(!gone.contains(id), "id {id} reused: {gone:?} -> {fresh:?}");
}
// Undo of a delete brings the old ids back unchanged (they still name the same geometry).
let line = strs(&again["curves"])[0].clone();
run(&mut s, "sketch.delete", json!({"entities": [line]}));
run(&mut s, "edit.undo", json!({}));
assert!(sketch(&s).curve_index(&line).is_some());
}
/// The example in docs/control-protocol.md.
#[test]
fn documented_example_holds() {
let mut s = new_sketch();
let r = run(&mut s, "sketch.line", json!({"points": [[0, 0], [40, 0]]}));
assert_eq!(r["curves"], json!(["l1"]));
assert_eq!(r["created"], json!({"points": ["l1.start", "l1.end"], "curves": ["l1"], "constraints": ["k1"], "dimensions": []}));
run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "value": 50}));
}
#[test]
fn commands_that_add_nothing_report_empty_lists() {
let mut s = new_sketch();
let c = made(&mut s, "sketch.line", json!({"points": [[0, 0], [10, 5]]}));
let r = run(&mut s, "sketch.move_point", json!({"point": strs(&c["points"])[1], "to": [12, 6]}));
assert_eq!(r["created"], json!({"points": [], "curves": [], "constraints": [], "dimensions": []}), "{r}");
}
+2 -2
View File
@@ -16,11 +16,11 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("sketch.insert_dxf", "Insert DXF", insert_dxf)
.at("SKETCH", "INSERT")
.icon("dxf")
.params("path | text: ASCII DXF (blocks expanded); at?: [x,y] offset, scale?, tolerance?: mm within which ends join (default 0.0001); plane?: (when no sketch is active, a new sketch on it; default XY)"),
.params(concat!("path | text: ASCII DXF (blocks expanded); at?: [x,y] offset, scale?, tolerance?: mm within which ends join (default 0.0001); plane?: (when no sketch is active, a new sketch on it; default XY)", created_doc!())),
CommandSpec::new("sketch.insert_svg", "Insert SVG", insert_svg)
.at("SKETCH", "INSERT")
.icon("svg")
.params("path | text: SVG; at?: [x,y] offset, scale?, tolerance?: mm within which ends join (default 0.0001); plane?: (when no sketch is active, a new sketch on it; default XY)"),
.params(concat!("path | text: SVG; at?: [x,y] offset, scale?, tolerance?: mm within which ends join (default 0.0001); plane?: (when no sketch is active, a new sketch on it; default XY)", created_doc!())),
CommandSpec::new("sketch.export_dxf", "Save As DXF", export_dxf)
.noundo()
.params("sketch?: id|name (default active); path?: file to write (else the DXF text is returned)"),
+11 -11
View File
@@ -20,28 +20,28 @@ pub static COMMANDS: &[CommandSpec] = &[
.at("SKETCH", "MODIFY")
.icon("sketch_fillet")
.enabled(in_sketch)
.params("point: corner shared by two curves (\"l1.end\") | points: [corners] (all rounded alike: one radius, the rest equal) | a, b: two lines or arcs meeting at a corner; radius: expr. A corner that dimensions or constraints measure to stays as a virtual sharp corner on both curves' extensions (`virtual_corners`; the moved ones in `moved_to_virtual_corner`)"),
.params(concat!("point: corner shared by two curves (\"l1.end\") | points: [corners] (all rounded alike: one radius, the rest equal) | a, b: two lines or arcs meeting at a corner; radius: expr. A corner that dimensions or constraints measure to stays as a virtual sharp corner on both curves' extensions (`virtual_corners`; the moved ones in `moved_to_virtual_corner`)", created_doc!())),
CommandSpec::new("sketch.chamfer.equal_distance", "Equal Distance Chamfer", chamfer_equal)
.at("SKETCH", "MODIFY")
.icon("sketch_chamfer")
.enabled(in_sketch)
.params("point | a, b: a corner of two lines; distance"),
.params(concat!("point | a, b: a corner of two lines; distance", created_doc!())),
CommandSpec::new("sketch.chamfer.distance_angle", "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"),
.params(concat!("point | a, b: a corner of two lines; distance: along a, angle: deg from a", created_doc!())),
CommandSpec::new("sketch.chamfer.two_distance", "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"),
.params(concat!("point | a, b: a corner of two lines; distance: along a, distance2: along b", created_doc!())),
CommandSpec::new("sketch.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)"),
.params(concat!("curves: [ids], distance: number (positive = left of the chain, or toward `side`), side?: [x,y], chain?: bool (default true: the connected chain)", created_doc!())),
CommandSpec::new("sketch.offset.break", "Break Offset Association", break_offset)
.enabled(in_sketch)
.params("curve: derived offset curve id (retains geometry and makes its points editable)"),
@@ -50,7 +50,7 @@ pub static COMMANDS: &[CommandSpec] = &[
.icon("trim")
.key("T")
.enabled(in_sketch)
.params("curve: id, at: [x,y] (the piece to remove: between the nearest intersections around it)"),
.params(concat!("curve: id, at: [x,y] (the piece to remove: between the nearest intersections around it)", created_doc!())),
CommandSpec::new("sketch.extend", "Extend", extend)
.at("SKETCH", "MODIFY")
.icon("extend")
@@ -60,7 +60,7 @@ pub static COMMANDS: &[CommandSpec] = &[
.at("SKETCH", "MODIFY")
.icon("break")
.enabled(in_sketch)
.params("curve: id, at: [x,y] (split at intersections) | points: [[x,y]…] (1…1000 positions projected onto the curve; duplicates are merged)"),
.params(concat!("curve: id, at: [x,y] (split at intersections) | points: [[x,y]…] (1…1000 positions projected onto the curve; duplicates are merged)", created_doc!())),
CommandSpec::new("sketch.scale", "Sketch Scale", scale)
.at("SKETCH", "MODIFY")
.icon("scale")
@@ -68,22 +68,22 @@ pub static COMMANDS: &[CommandSpec] = &[
.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"),
.params(concat!("entities: [ids], translate?: [dx,dy], angle?: deg, center?: [x,y], copy?: bool", created_doc!())),
CommandSpec::new("sketch.mirror", "Mirror", mirror)
.at("SKETCH", "CREATE")
.icon("sketch_mirror")
.enabled(in_sketch)
.params("entities: [curve/point ids], line: mirror line id (symmetry constraints are added)"),
.params(concat!("entities: [curve/point ids], line: mirror line id (symmetry constraints are added)", created_doc!())),
CommandSpec::new("sketch.pattern.circular", "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)"),
.params(concat!("entities: [ids], center: [x,y] or point ref, count: 2…500, angle?: total deg (default 360)", created_doc!())),
CommandSpec::new("sketch.pattern.rectangular", "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)"),
.params(concat!("entities: [ids], dir?: [x,y] (default x), count, spacing; count2?, spacing2? (perpendicular direction)", created_doc!())),
];
const TOL: f64 = 1e-7;
+14 -10
View File
@@ -13,41 +13,47 @@ use super::{CommandSpec, in_sketch};
use crate::params::{bad, bool_, str_, vec3};
use crate::{EngineError, Result, Session};
macro_rules! refs_doc {
() => {
"refs: [{edge: [x,y,z], body?} | {face: [x,y,z], body?} (its edge loops) | {body: name} (silhouette) | {vertex: [x,y,z], body?} | {sketch, curve} | {sketch, point} | \"origin\" | {axis: X|Y|Z} | {plane: XY|XZ|YZ|name}]"
};
}
pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("sketch.project", "Project", project_cmd).at("SKETCH", "CREATE").icon("project").key("P").enabled(in_sketch).params(REFS_DOC),
CommandSpec::new("sketch.project", "Project", project_cmd).at("SKETCH", "CREATE").icon("project").key("P").enabled(in_sketch).params(concat!(refs_doc!(), created_doc!())),
CommandSpec::new("sketch.intersect", "Intersect", intersect_cmd).at("SKETCH", "CREATE").icon("intersect").enabled(in_sketch).params(
"refs: [{body}, {face: [x,y,z], body?}, {edge: [x,y,z], body?}, {sketch, curve}]: section curves (or points) with the sketch plane, linked",
concat!("refs: [{body}, {face: [x,y,z], body?}, {edge: [x,y,z], body?}, {sketch, curve}]: section curves (or points) with the sketch plane, linked", created_doc!()),
),
CommandSpec::new("sketch.include_3d", "Include 3D Geometry", include_cmd)
.at("SKETCH", "CREATE")
.icon("include")
.enabled(in_sketch)
.params("refs: like Project (edges, vertices, sketch curves and points); curves out of the sketch plane are flattened onto it"),
.params(concat!("refs: like Project (edges, vertices, sketch curves and points); curves out of the sketch plane are flattened onto it", created_doc!())),
CommandSpec::new("sketch.project_to_surface", "Project To Surface", project_to_surface)
.at("SKETCH", "CREATE")
.icon("project_surface")
.enabled(in_sketch)
.params("curves: [{sketch, curve}…] (curves of other sketches), face: [x,y,z] point on the target face, body?: projected along the active sketch's normal onto the face, as linked 3D curves"),
.params(concat!("curves: [{sketch, curve}…] (curves of other sketches), face: [x,y,z] point on the target face, body?: projected along the active sketch's normal onto the face, as linked 3D curves", created_doc!())),
CommandSpec::new("sketch.intersection_curve", "Intersection Curve", intersection_curve)
.at("SKETCH", "CREATE")
.icon("intersection_curve")
.enabled(in_sketch)
.params("a, b: {body} | {face: [x,y,z], body?}: where they meet, as linked 3D curves"),
.params(concat!("a, b: {body} | {face: [x,y,z], body?}: where they meet, as linked 3D curves", created_doc!())),
CommandSpec::new("sketch.spun_profile", "Spun Profile", spun_profile)
.at("SKETCH", "CREATE")
.icon("spun")
.enabled(in_sketch)
.params("body: name, axis: a line of the active sketch (curve id) | X|Y|Z: the body's outline revolved about the axis, in the sketch plane"),
.params(concat!("body: name, axis: a line of the active sketch (curve id) | X|Y|Z: the body's outline revolved about the axis, in the sketch plane", created_doc!())),
CommandSpec::new("sketch.isoparametric_curve", "Isoparametric Curve", iso_curve)
.at("SKETCH", "CREATE")
.icon("iso_curve")
.enabled(in_sketch)
.params("face: [x,y,z] point on the face (the curve passes through it), body?, direction?: u|v|[x,y,z] (default u): linked 3D curve"),
.params(concat!("face: [x,y,z] point on the face (the curve passes through it), body?, direction?: u|v|[x,y,z] (default u): linked 3D curve", created_doc!())),
CommandSpec::new("sketch.fit_curves_to_section", "Fit Curves to Mesh Section", fit_section)
.at("SKETCH", "CREATE")
.icon("fit_section")
.enabled(in_sketch)
.params("body: name (a mesh or solid body): its section with the sketch plane, fitted with lines and arcs, as normal (unlinked) geometry"),
.params(concat!("body: name (a mesh or solid body): its section with the sketch plane, fitted with lines and arcs, as normal (unlinked) geometry", created_doc!())),
CommandSpec::new("sketch.break_link", "Break Link", break_link)
.enabled(in_sketch)
.params("link?: link id (j1) | entities?: [curve or point ids]: linked geometry becomes normal sketch geometry"),
@@ -56,8 +62,6 @@ pub static COMMANDS: &[CommandSpec] = &[
.params("value?: bool (default toggles): project a face's edges when a sketch is created on it"),
];
const REFS_DOC: &str = "refs: [{edge: [x,y,z], body?} | {face: [x,y,z], body?} (its edge loops) | {body: name} (silhouette) | {vertex: [x,y,z], body?} | {sketch, curve} | {sketch, point} | \"origin\" | {axis: X|Y|Z} | {plane: XY|XZ|YZ|name}]";
/// Parse one reference.
fn source(s: &Session, v: &Value, cmd: &str) -> Result<LinkSource> {
if let Some(name) = v.as_str() {