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() {
+22
View File
@@ -395,6 +395,7 @@ impl Session {
let changed = !Arc::ptr_eq(&self.doc, &before.doc);
match r {
Ok(v) => {
let v = if changed { self.with_created(spec.id, params, &before, v) } else { v };
if changed {
if spec.undoable {
self.undo.push(before);
@@ -419,6 +420,27 @@ impl Session {
}
}
/// A sketch command's result gets `created`: the ids of the points, curves, constraints and
/// dimensions it added to its sketch (the `sketch` param, else the active sketch), so every
/// drawing and modify command reports them the same way (see `cmd::sketch_created`). A command
/// that runs another one (`sketch.draw.dimensioned`) gets the whole diff, replacing the nested
/// command's narrower `created`.
fn with_created(&self, id: &str, params: &Value, before: &Snapshot, mut v: Value) -> Value {
if !id.starts_with("sketch.") {
return v;
}
let Some(out) = v.as_object_mut() else { return v };
let named = match params.get("sketch") {
Some(Value::Number(n)) => self.doc.find_feature(&n.to_string()).map(|f| f.id),
Some(Value::String(x)) => self.doc.find_feature(x).map(|f| f.id),
_ => None,
};
let Some(sk) = named.or(self.active_sketch).or(before.active_sketch) else { return v };
let Ok(after) = self.doc.sketch(sk) else { return v };
out.insert("created".into(), cmd::sketch_created(before.doc.sketch(sk).ok(), after));
v
}
/// Remember the design, active sketch, pending positions and undo history; returns the checkpoint's id.
pub fn checkpoint(&mut self) -> u64 {
let id = self.next_checkpoint;
+4 -1
View File
@@ -58,7 +58,10 @@ pub fn tool_definitions() -> Vec<Value> {
{center: [20,15], radius: 5}; sketch.dimension {entities: [curve id], value: \"width\"}; sketch.finish {}; solid.extrude \
{distance: 20, operation: \"cut\"}; solid.fillet {edges: [[0,0,10]], radius: 3}; solid.box {length, \
width, height}. Lengths are mm; expressions may use parameters and units (\"width / 2\", \"30 deg\"). Returns \
the command's result; failures leave the design unchanged.",
the command's result; failures leave the design unchanged. Sketch commands that add geometry return \
created: {points, curves, constraints, dimensions}: the ids of what they added, stable and not reused \
after a delete (undo rewinds the counters with the design), usable right away \
in sketch.dimension, sketch.constraint.* and sketch.delete.",
obj(
json!({
"command": string("Command id (see list_commands)"),
+5
View File
@@ -15,6 +15,11 @@ fillet, chamfer, offset (O), mirror, patterns, move/copy and scale.
**Project** (P) and **Intersect** bring model edges into the sketch as linked geometry that
follows later changes. Sketching on a face projects its edges automatically.
Run from a script or the control channel, every command that adds sketch geometry returns the
ids of what it added (`created: {points, curves, constraints, dimensions}`). Ids are stable and
never reused within a sketch, even after a delete, so a script can dimension or constrain the new
geometry right away.
Extend finds intersections with exact ellipses, rational conics and spline spans. It keeps the
original line or circular support and extends only beyond the selected endpoint; conflicting
constraints or an ambiguous overlap leave the sketch unchanged.
+12
View File
@@ -17,6 +17,18 @@ events, one pointer event per frame.
Commands never open dialogs when run this way. `solvecraft-cli commands` prints the same registry.
Every `sketch.*` command that changes a sketch adds `created: {points, curves, constraints,
dimensions}` to its result (and `wires` for 3D curves): the ids of what it added, in creation
order, next to whatever else the command returns (`curves`, `sides`, `corners`…). Sketch ids are
stable and never reused, even after a delete (`l1`, `l2`, then `l3` once `l2` is gone; only undo
rolls the numbering back with the design), so they can go straight into later commands:
```text
{"id":7,"method":"engine.execute","params":{"command":"sketch.line","params":{"points":[[0,0],[40,0]]}}}
→ {"curves":["l1"],"created":{"points":["l1.start","l1.end"],"curves":["l1"],"constraints":["k1"],"dimensions":[]},…}
{"id":8,"method":"engine.execute","params":{"command":"sketch.dimension","params":{"entities":["l1"],"value":50}}}
```
## UI
| Method | Params | Notes |