fix: sketches on planes built from the model can project, intersect, include and hold text (#459)
Project, Intersect, Include 3D Geometry and Text resolved the active sketch's plane without the model, so on a construction plane that is built from geometry (perpendicular to a plane through a sketch line, through three points, midway between two faces, tangent, along a path) they stopped with "this plane is built from the model: evaluate first". Evaluation resolves the same plane against the model before the sketch, in its component's frame; the sketch commands now do the same, in one place, for adding a link, refreshing the links on an edit, snapping a point onto an edge and the internal face profiles. Tests: a sketch on a plane through a sloped sketch line projects a point and a line of that sketch, follows a parameter that tilts the plane (also after reopen and after another edit), holds text, intersects a body and includes an edge; the same on a three-point plane and a midplane. Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
1ae6e9beb4
commit
9ab25cfd4a
3 files changed
+183
-23
No files matched your search
@@ -0,0 +1,144 @@
|
|||||||
|
//! Sketches on construction planes built from the model (perpendicular to a plane through a
|
||||||
|
//! sketch line, through three points, a midplane…): Project, Intersect, Include 3D Geometry and
|
||||||
|
//! Text resolve the sketch's plane against the model before the sketch, as evaluation does, and
|
||||||
|
//! the links follow when a parameter moves the plane.
|
||||||
|
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
use solidcraft_geom::Vec3;
|
||||||
|
|
||||||
|
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}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sketch `A` on YZ with the line `s` from the origin to (run 100, rise `rise`), and the plane
|
||||||
|
/// `P` through that line, square to YZ. The plane's origin is the line's start and its x axis
|
||||||
|
/// runs along the line.
|
||||||
|
fn sloped_plane(s: &mut Session) {
|
||||||
|
run(s, "parameters.add", json!({"name": "rise", "expression": "60 mm"}));
|
||||||
|
run(s, "sketch.create", json!({"plane": "YZ", "name": "A"}));
|
||||||
|
run(s, "sketch.line", json!({"points": [[0, 0], [100, 60]], "ids": ["s"]}));
|
||||||
|
run(s, "sketch.dimension", json!({"entities": ["s.start", "s.end"], "type": "horizontal", "value": "100 mm"}));
|
||||||
|
run(s, "sketch.dimension", json!({"entities": ["s.start", "s.end"], "type": "vertical", "value": "rise"}));
|
||||||
|
run(s, "sketch.finish", json!({}));
|
||||||
|
run(s, "construct.plane.perpendicular", json!({"plane": "YZ", "line": {"sketch": "A", "curve": "s"}, "name": "P"}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// World position of point `point` of sketch `sketch`.
|
||||||
|
fn world_point(s: &Session, sketch: &str, point: &str) -> Vec3 {
|
||||||
|
let id = s.doc.find_feature(sketch).unwrap().id;
|
||||||
|
let st = s.model.state();
|
||||||
|
let ss = st.sketch(id).unwrap();
|
||||||
|
let i = ss.sketch.resolve_point(point).unwrap_or_else(|| panic!("no point {point} in {sketch}"));
|
||||||
|
ss.plane.to_world(ss.sketch.point(i).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn no_trouble(s: &Session) {
|
||||||
|
for r in &s.model.results {
|
||||||
|
assert!(r.error.is_none() && r.warning.is_none(), "{}: {:?} {:?}", r.name, r.error, r.warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn project_onto_a_plane_built_from_a_sketch_line_follows_the_parameter() {
|
||||||
|
let mut s = Session::default();
|
||||||
|
sloped_plane(&mut s);
|
||||||
|
run(&mut s, "sketch.create", json!({"plane": "P", "name": "B"}));
|
||||||
|
let r = run(&mut s, "sketch.project", json!({"refs": [{"sketch": "A", "point": "s.end"}, {"sketch": "A", "curve": "s"}]}));
|
||||||
|
let point = r["links"][0]["points"][0].as_str().unwrap().to_string();
|
||||||
|
let curve = r["links"][1]["curves"][0].as_str().unwrap().to_string();
|
||||||
|
// Geometry drawn against the projection keeps working: a circle on the projected point.
|
||||||
|
run(&mut s, "sketch.circle.center", json!({"center": point, "radius": 4}));
|
||||||
|
run(&mut s, "sketch.finish", json!({}));
|
||||||
|
no_trouble(&s);
|
||||||
|
let len = (100.0f64 * 100.0 + 60.0 * 60.0).sqrt();
|
||||||
|
let end = world_point(&s, "A", "s.end");
|
||||||
|
assert!(world_point(&s, "B", &point).dist(end) < 1e-9, "{:?} {end:?}", world_point(&s, "B", &point));
|
||||||
|
let ends = [world_point(&s, "B", &format!("{curve}.start")), world_point(&s, "B", &format!("{curve}.end"))];
|
||||||
|
assert!(ends.iter().any(|q| q.dist(Vec3::ZERO) < 1e-9) && ends.iter().any(|q| q.dist(end) < 1e-9), "{ends:?}");
|
||||||
|
{
|
||||||
|
let st = s.model.state();
|
||||||
|
let b = st.sketch(s.doc.find_feature("B").unwrap().id).unwrap();
|
||||||
|
let q = b.sketch.point(b.sketch.resolve_point(&point).unwrap()).unwrap();
|
||||||
|
assert!((q.x - len).abs() < 1e-9 && q.y.abs() < 1e-9, "the line's end lies on the plane's x axis: {q:?}");
|
||||||
|
}
|
||||||
|
// The parameter tilts the plane; the projection and the circle on it follow, in the file too.
|
||||||
|
run(&mut s, "parameters.change", json!({"name": "rise", "expression": "25 mm"}));
|
||||||
|
let check = |s: &Session| {
|
||||||
|
no_trouble(s);
|
||||||
|
let end = world_point(s, "A", "s.end");
|
||||||
|
assert!((end.z - 25.0).abs() < 1e-9 && (end.y - 100.0).abs() < 1e-9, "{end:?}");
|
||||||
|
assert!(world_point(s, "B", &point).dist(end) < 1e-9);
|
||||||
|
assert!(world_point(s, "B", "c1.center").dist(end) < 1e-9);
|
||||||
|
};
|
||||||
|
check(&s);
|
||||||
|
check(&Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap()));
|
||||||
|
// Editing the sketch again after the change refreshes its links on the moved plane.
|
||||||
|
run(&mut s, "sketch.edit", json!({"sketch": "B"}));
|
||||||
|
run(&mut s, "sketch.point", json!({"point": [10, 10]}));
|
||||||
|
run(&mut s, "sketch.finish", json!({}));
|
||||||
|
check(&s);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_on_a_plane_built_from_a_sketch_line() {
|
||||||
|
let mut s = Session::default();
|
||||||
|
sloped_plane(&mut s);
|
||||||
|
run(&mut s, "sketch.create", json!({"plane": "P", "name": "B"}));
|
||||||
|
let r = run(&mut s, "sketch.text", json!({"text": "AB", "at": [10, 10], "height": 5}));
|
||||||
|
assert!(!r["curves"].as_array().unwrap().is_empty(), "{r}");
|
||||||
|
run(&mut s, "sketch.finish", json!({}));
|
||||||
|
no_trouble(&s);
|
||||||
|
let st = s.model.state();
|
||||||
|
let b = st.sketch(s.doc.find_feature("B").unwrap().id).unwrap();
|
||||||
|
assert!(!b.profiles.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn intersect_and_include_on_planes_built_from_the_model() {
|
||||||
|
let mut s = Session::default();
|
||||||
|
sloped_plane(&mut s);
|
||||||
|
run(&mut s, "solid.box", json!({"corner": [-10, 20, -5], "length": 20, "width": 30, "height": 80, "body_name": "Post"}));
|
||||||
|
// The plane through three sketch points and the midplane of two faces are built from the model too.
|
||||||
|
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "T"}));
|
||||||
|
run(&mut s, "sketch.line", json!({"points": [[0, 0], [40, 0], [40, 30]], "ids": ["u", "v"]}));
|
||||||
|
run(&mut s, "sketch.finish", json!({}));
|
||||||
|
run(
|
||||||
|
&mut s,
|
||||||
|
"construct.plane.three_points",
|
||||||
|
json!({"a": {"sketch": "T", "point": "u.start"}, "b": {"sketch": "T", "point": "u.end"}, "c": {"sketch": "T", "point": "v.end"}, "name": "Three"}),
|
||||||
|
);
|
||||||
|
run(&mut s, "construct.plane.midplane", json!({"a": {"face": [0, 20, 30]}, "b": {"face": [0, 50, 30]}, "name": "Mid"}));
|
||||||
|
|
||||||
|
run(&mut s, "sketch.create", json!({"plane": "P", "name": "B"}));
|
||||||
|
// The slope cuts the post: a closed section of four lines.
|
||||||
|
let r = run(&mut s, "sketch.intersect", json!({"refs": [{"body": "Post"}]}));
|
||||||
|
assert_eq!(r["links"][0]["curves"].as_array().unwrap().len(), 4, "{r}");
|
||||||
|
// An edge of the post, brought onto the slope.
|
||||||
|
let r = run(&mut s, "sketch.include_3d", json!({"refs": [{"edge": [-10, 20, 30], "body": "Post"}]}));
|
||||||
|
assert!(r["links"][0]["link"].is_string(), "{r}");
|
||||||
|
run(&mut s, "sketch.finish", json!({}));
|
||||||
|
|
||||||
|
run(&mut s, "sketch.create", json!({"plane": "Three", "name": "C"}));
|
||||||
|
let r = run(&mut s, "sketch.project", json!({"refs": [{"sketch": "T", "point": "v.end"}]}));
|
||||||
|
let p = r["links"][0]["points"][0].as_str().unwrap().to_string();
|
||||||
|
run(&mut s, "sketch.finish", json!({}));
|
||||||
|
assert!(world_point(&s, "C", &p).dist(Vec3::new(40.0, 30.0, 0.0)) < 1e-9);
|
||||||
|
|
||||||
|
run(&mut s, "sketch.create", json!({"plane": "Mid", "name": "D"}));
|
||||||
|
let r = run(&mut s, "sketch.intersect", json!({"refs": [{"body": "Post"}]}));
|
||||||
|
assert_eq!(r["links"][0]["curves"].as_array().unwrap().len(), 4, "{r}");
|
||||||
|
run(&mut s, "sketch.finish", json!({}));
|
||||||
|
no_trouble(&s);
|
||||||
|
// The midplane sits half way between the post's two faces (y = 35).
|
||||||
|
let st = s.model.state();
|
||||||
|
let d = st.sketch(s.doc.find_feature("D").unwrap().id).unwrap();
|
||||||
|
assert!(d.sketch.points.iter().any(|q| q.link.is_some()));
|
||||||
|
for q in d.sketch.points.iter().filter(|q| q.link.is_some()) {
|
||||||
|
assert!((d.plane.to_world(q.pos).y - 35.0).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -118,15 +118,36 @@ fn source(s: &Session, v: &Value, cmd: &str) -> Result<LinkSource> {
|
|||||||
Err(bad(cmd, format!("cannot project {v}")))
|
Err(bad(cmd, format!("cannot project {v}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The plane of a sketch feature.
|
/// The plane of a sketch feature, resolved against `st`: the model before the sketch as its
|
||||||
fn sketch_plane(doc: &Document, id: u64) -> Result<Plane> {
|
/// component sees it, which is what evaluation resolves it against. A construction plane built
|
||||||
|
/// from the model (through a sketch line, three points, between two faces…) exists only there.
|
||||||
|
fn sketch_plane(doc: &Document, id: u64, st: &solidcraft_doc::ModelState) -> Result<Plane> {
|
||||||
let (vals, _) = doc.param_values();
|
let (vals, _) = doc.param_values();
|
||||||
match doc.feature(id).map(|f| &f.kind) {
|
match doc.feature(id).map(|f| &f.kind) {
|
||||||
Some(FeatureKind::Sketch { plane, .. }) => Ok(doc.resolve_plane(&vals, plane, 0)?),
|
Some(FeatureKind::Sketch { plane, .. }) => Ok(doc.resolve_plane_in(&vals, plane, 0, Some(st))?),
|
||||||
_ => Err(EngineError::Other(format!("feature {id} is not a sketch"))),
|
_ => Err(EngineError::Other(format!("feature {id} is not a sketch"))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The model before sketch `id` in the frame its links and its plane are resolved in: the
|
||||||
|
/// attached face's frame for a sketch on a face, else its component's.
|
||||||
|
fn state_for_sketch<'a>(
|
||||||
|
s: &Session,
|
||||||
|
doc: &Document,
|
||||||
|
id: u64,
|
||||||
|
st: &'a solidcraft_doc::ModelState,
|
||||||
|
) -> std::result::Result<std::borrow::Cow<'a, solidcraft_doc::ModelState>, solidcraft_doc::DocError> {
|
||||||
|
let comp = doc.feature(id).map(|f| f.component).unwrap_or(0);
|
||||||
|
let attachment = doc.feature(id).and_then(|feature| match &feature.kind {
|
||||||
|
solidcraft_doc::FeatureKind::Sketch { plane, .. } => super::sketch::face::anchor(plane),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
match attachment {
|
||||||
|
Some(anchor) => solidcraft_doc::state_in_face_anchor_frame(doc, st, anchor, comp),
|
||||||
|
None => Ok(solidcraft_doc::state_in_frame_at(doc, st, comp, owner_frame(s, doc, comp))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Keep candidate feature edits while overlaying uncommitted occurrence placements.
|
/// Keep candidate feature edits while overlaying uncommitted occurrence placements.
|
||||||
fn placed_candidate<'a>(s: &Session, doc: &'a Document) -> std::borrow::Cow<'a, Document> {
|
fn placed_candidate<'a>(s: &Session, doc: &'a Document) -> std::borrow::Cow<'a, Document> {
|
||||||
if s.pending_moves.is_empty() {
|
if s.pending_moves.is_empty() {
|
||||||
@@ -152,21 +173,16 @@ pub(super) fn refresh(s: &Session, doc: &Document, id: u64, sk: &mut Sketch) ->
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let doc = placed_candidate(s, doc);
|
let doc = placed_candidate(s, doc);
|
||||||
let Ok(plane) = sketch_plane(&doc, id) else { return Vec::new() };
|
|
||||||
let (vals, _) = doc.param_values();
|
let (vals, _) = doc.param_values();
|
||||||
let st = s.model.state_before(id);
|
let st = s.model.state_before(id);
|
||||||
let comp = doc.feature(id).map(|f| f.component).unwrap_or(0);
|
|
||||||
let attachment = doc.feature(id).and_then(|feature| match &feature.kind {
|
let attachment = doc.feature(id).and_then(|feature| match &feature.kind {
|
||||||
solidcraft_doc::FeatureKind::Sketch { plane, .. } => super::sketch::face::anchor(plane),
|
solidcraft_doc::FeatureKind::Sketch { plane, .. } => super::sketch::face::anchor(plane),
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
let st = match attachment {
|
let Ok(st) = state_for_sketch(s, &doc, id, &st) else {
|
||||||
Some(anchor) => solidcraft_doc::state_in_face_anchor_frame(&doc, &st, anchor, comp),
|
|
||||||
None => Ok(solidcraft_doc::state_in_frame_at(&doc, &st, comp, owner_frame(s, &doc, comp))),
|
|
||||||
};
|
|
||||||
let Ok(st) = st else {
|
|
||||||
return vec!["lost face attachment; reattach explicitly".into()];
|
return vec!["lost face attachment; reattach explicitly".into()];
|
||||||
};
|
};
|
||||||
|
let Ok(plane) = sketch_plane(&doc, id, &st) else { return Vec::new() };
|
||||||
match attachment {
|
match attachment {
|
||||||
Some(anchor) => solidcraft_doc::project::refresh_face_anchor_links(&doc, &vals, &st, &plane, sk, anchor),
|
Some(anchor) => solidcraft_doc::project::refresh_face_anchor_links(&doc, &vals, &st, &plane, sk, anchor),
|
||||||
None => solidcraft_doc::project::refresh_links(&doc, &vals, &st, &plane, sk),
|
None => solidcraft_doc::project::refresh_links(&doc, &vals, &st, &plane, sk),
|
||||||
@@ -176,20 +192,11 @@ pub(super) fn refresh(s: &Session, doc: &Document, id: u64, sk: &mut Sketch) ->
|
|||||||
/// Add a link for `src` to sketch `id` in `doc` (not stored in the session). Returns the link id.
|
/// Add a link for `src` to sketch `id` in `doc` (not stored in the session). Returns the link id.
|
||||||
pub(super) fn add_link(s: &Session, doc: &Document, id: u64, sk: &mut Sketch, kind: LinkKind, src: LinkSource) -> Result<String> {
|
pub(super) fn add_link(s: &Session, doc: &Document, id: u64, sk: &mut Sketch, kind: LinkKind, src: LinkSource) -> Result<String> {
|
||||||
let doc = placed_candidate(s, doc);
|
let doc = placed_candidate(s, doc);
|
||||||
let plane = sketch_plane(&doc, id)?;
|
|
||||||
let (vals, _) = doc.param_values();
|
let (vals, _) = doc.param_values();
|
||||||
let st = s.model.state_before(id);
|
let st = s.model.state_before(id);
|
||||||
// The model as the sketch's component sees it.
|
// The model as the sketch's component sees it.
|
||||||
let comp = doc.feature(id).map(|f| f.component).unwrap_or(0);
|
let st = state_for_sketch(s, &doc, id, &st)?;
|
||||||
let attachment = doc.feature(id).and_then(|feature| match &feature.kind {
|
let plane = sketch_plane(&doc, id, &st)?;
|
||||||
solidcraft_doc::FeatureKind::Sketch { plane, .. } => super::sketch::face::anchor(plane),
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
let st = match attachment {
|
|
||||||
Some(anchor) => solidcraft_doc::state_in_face_anchor_frame(&doc, &st, anchor, comp),
|
|
||||||
None => Ok(solidcraft_doc::state_in_frame_at(&doc, &st, comp, owner_frame(s, &doc, comp))),
|
|
||||||
};
|
|
||||||
let st = st?;
|
|
||||||
let r = resolve(&doc, &vals, &st, &plane, kind, &src)?;
|
let r = resolve(&doc, &vals, &st, &plane, kind, &src)?;
|
||||||
let name = if let LinkSource::Face { body, at } = &r.source {
|
let name = if let LinkSource::Face { body, at } = &r.source {
|
||||||
st.body(body).and_then(|body| {
|
st.body(body).and_then(|body| {
|
||||||
@@ -454,7 +461,8 @@ pub(super) fn snap_points(s: &mut Session, p: &Value, cmd: &str) -> Result<Value
|
|||||||
sk.points.get(pi).map(|q| q.id.clone()).unwrap_or_default()
|
sk.points.get(pi).map(|q| q.id.clone()).unwrap_or_default()
|
||||||
} else if let Some(at) = edge {
|
} else if let Some(at) = edge {
|
||||||
let l = add_link(s, d, id, sk, LinkKind::Project, LinkSource::Edge { body, at }).map_err(|e| bad(cmd, format!("edge: {e}")))?;
|
let l = add_link(s, d, id, sk, LinkKind::Project, LinkSource::Edge { body, at }).map_err(|e| bad(cmd, format!("edge: {e}")))?;
|
||||||
let plane = sketch_plane(d, id)?;
|
let before = s.model.state_before(id);
|
||||||
|
let plane = sketch_plane(d, id, &*state_for_sketch(s, d, id, &before)?)?;
|
||||||
let q = plane.to_local(at);
|
let q = plane.to_local(at);
|
||||||
let ci = sk.link_curves(&l).first().copied().ok_or_else(|| bad(cmd, "the edge projects to a point"))?;
|
let ci = sk.link_curves(&l).first().copied().ok_or_else(|| bad(cmd, "the edge projects to a point"))?;
|
||||||
let pi = sk.add_point(q, None)?;
|
let pi = sk.add_point(q, None)?;
|
||||||
@@ -490,10 +498,10 @@ pub(super) fn snap_points(s: &mut Session, p: &Value, cmd: &str) -> Result<Value
|
|||||||
/// edits, but cannot provide the construction certificate required by public attachments.
|
/// edits, but cannot provide the construction certificate required by public attachments.
|
||||||
pub(super) fn project_internal_face(s: &mut Session, id: u64, body: &str, index: usize) -> Result<String> {
|
pub(super) fn project_internal_face(s: &mut Session, id: u64, body: &str, index: usize) -> Result<String> {
|
||||||
let mut doc = (*s.doc).clone();
|
let mut doc = (*s.doc).clone();
|
||||||
let plane = sketch_plane(&doc, id)?;
|
|
||||||
let raw = s.model.state_before(id);
|
let raw = s.model.state_before(id);
|
||||||
let component = doc.feature(id).map(|feature| feature.component).unwrap_or(0);
|
let component = doc.feature(id).map(|feature| feature.component).unwrap_or(0);
|
||||||
let state = solidcraft_doc::state_in_frame_at(&doc, &raw, component, owner_frame(s, &doc, component));
|
let state = solidcraft_doc::state_in_frame_at(&doc, &raw, component, owner_frame(s, &doc, component));
|
||||||
|
let plane = sketch_plane(&doc, id, &state)?;
|
||||||
let resolved = solidcraft_doc::project::resolve_face_indices(&state, &plane, LinkKind::Project, body, &[index])?;
|
let resolved = solidcraft_doc::project::resolve_face_indices(&state, &plane, LinkKind::Project, body, &[index])?;
|
||||||
let face_name = state.body(body).and_then(|body| solidcraft_doc::naming::face_names(body).get(index).cloned());
|
let face_name = state.body(body).and_then(|body| solidcraft_doc::naming::face_names(body).get(index).cloned());
|
||||||
let mut sk = doc.sketch(id)?.clone();
|
let mut sk = doc.sketch(id)?.clone();
|
||||||
@@ -538,3 +546,7 @@ pub(super) fn auto_project_face(s: &mut Session, id: u64, _at: Vec3) -> Result<O
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "sketch_project_tests.rs"]
|
#[path = "sketch_project_tests.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "sketch_model_plane_tests.rs"]
|
||||||
|
mod model_plane_tests;
|
||||||
@@ -39,6 +39,10 @@ is projected in one undo step. A rejected projection keeps the selection for cor
|
|||||||
an empty or mixed selection starts the ordinary picking tool. External sketch selections
|
an empty or mixed selection starts the ordinary picking tool. External sketch selections
|
||||||
retain their source sketch, even when the active sketch uses the same point or curve names.
|
retain their source sketch, even when the active sketch uses the same point or curve names.
|
||||||
|
|
||||||
|
A sketch on a construction plane built from the model (through a sketch line, through three
|
||||||
|
points, midway between two faces…) projects, intersects, includes and holds text like a sketch
|
||||||
|
on any other plane, and its links follow when a parameter moves that plane.
|
||||||
|
|
||||||
Run from a script or the control channel, every command that adds sketch geometry returns the
|
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
|
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
|
never reused within a sketch, even after a delete, so a script can dimension or constrain the new
|
||||||
|
|||||||
Reference in new issue
Block a user