mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
* claim: #9324 * Sketch: Project takes a construction axis, not only X, Y and Z (#9324) sketch.project with {"axis": "Axis1"} upper-cased the name, and the axis link knew only the origin axes, so a construction axis failed with "unknown axis `AXIS1`". X, Y and Z (any case) are still the origin axes; any other name must be a construction axis, kept as its feature names it. The link finds the axis where the timeline before the sketch puts it (construction_axis_in, as Revolve does) and projects it as an origin axis: its line in the sketch plane, or a point when the axis stands square to it. Renaming the axis renames the link, as for a construction plane's projection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -176,6 +176,8 @@ pub(crate) fn remap_link(
|
||||
remap_link(b, ids, names, bodies, depth + 1);
|
||||
}
|
||||
LinkSource::Plane { name } => mapped_name(name, names),
|
||||
// A construction axis, by name (#9324); the origin axes are no feature's.
|
||||
LinkSource::Axis { name } if !matches!(name.as_str(), "X" | "Y" | "Z") => mapped_name(name, names),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,13 +238,14 @@ fn resolve_uncached(
|
||||
}
|
||||
LinkSource::Origin => vec![LinkGeom::Point(plane.to_local(Vec3::ZERO))],
|
||||
LinkSource::Axis { name } => {
|
||||
let d = match name.to_ascii_uppercase().as_str() {
|
||||
"X" => Vec3::X,
|
||||
"Y" => Vec3::Y,
|
||||
"Z" => Vec3::Z,
|
||||
_ => return Err(DocError::Unknown(format!("axis `{name}`"))),
|
||||
let (o, d) = match name.to_ascii_uppercase().as_str() {
|
||||
"X" => (Vec3::ZERO, Vec3::X),
|
||||
"Y" => (Vec3::ZERO, Vec3::Y),
|
||||
"Z" => (Vec3::ZERO, Vec3::Z),
|
||||
// A construction axis, by name (#9324), where the timeline so far puts it.
|
||||
_ => crate::eval::construction_axis_in(Some(doc), st, name, 0)?,
|
||||
};
|
||||
let o = plane.to_local(Vec3::ZERO);
|
||||
let o = plane.to_local(o);
|
||||
let d2 = Vec2::new(d.dot(plane.x), d.dot(plane.y));
|
||||
match d2.normalized() {
|
||||
Some(u) if d2.len() > 1e-9 => vec![LinkGeom::Line(o - u * AXIS_HALF, o + u * AXIS_HALF)],
|
||||
|
||||
@@ -16,7 +16,7 @@ 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}]"
|
||||
"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|name} | {plane: XY|XZ|YZ|name}]"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ fn source(s: &Session, v: &Value, cmd: &str) -> Result<LinkSource> {
|
||||
return Ok(LinkSource::Body { body: b.to_string() });
|
||||
}
|
||||
if let Some(a) = str_(v, "axis") {
|
||||
return Ok(LinkSource::Axis { name: a.to_ascii_uppercase() });
|
||||
return axis_source(s, a, cmd);
|
||||
}
|
||||
if let Some(p) = str_(v, "plane") {
|
||||
return Ok(LinkSource::Plane { name: p.to_string() });
|
||||
@@ -121,6 +121,17 @@ fn source(s: &Session, v: &Value, cmd: &str) -> Result<LinkSource> {
|
||||
Err(bad(cmd, format!("cannot project {v}")))
|
||||
}
|
||||
|
||||
/// An origin axis (`X`, `Y` or `Z`, in any case), or a construction axis by its name (#9324).
|
||||
fn axis_source(s: &Session, name: &str, cmd: &str) -> Result<LinkSource> {
|
||||
if ["X", "Y", "Z"].iter().any(|axis| name.eq_ignore_ascii_case(axis)) {
|
||||
return Ok(LinkSource::Axis { name: name.to_ascii_uppercase() });
|
||||
}
|
||||
match s.doc.find_feature(name) {
|
||||
Some(f) if matches!(f.kind, FeatureKind::ConstructionAxis { .. }) => Ok(LinkSource::Axis { name: f.name.clone() }),
|
||||
_ => Err(bad(cmd, format!("unknown axis `{name}`: X, Y, Z or the name of a construction axis"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// The plane of a sketch feature, resolved against `st`: the model before the sketch as its
|
||||
/// 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.
|
||||
|
||||
@@ -216,6 +216,53 @@ fn project_edges_bodies_sketches_and_origin() {
|
||||
assert!(s.execute("sketch.project", &json!({"refs": [{"plane": "XY"}]})).is_err(), "parallel plane projects to nothing");
|
||||
}
|
||||
|
||||
/// A construction axis projects as an origin axis does, as its line in the sketch plane, and stays
|
||||
/// linked to it: the projection follows when the axis moves and when it is renamed. The report's
|
||||
/// steps (#9324): the axis made after the sketch, then moved before it.
|
||||
#[test]
|
||||
fn a_construction_axis_projects_and_stays_linked() {
|
||||
let mut s = Session::default();
|
||||
let sketch = run(&mut s, "sketch.create", json!({"plane": "XY"}))["sketch"].as_u64().unwrap();
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let axis = |x: f64| json!({"a": {"point": [x, 0, 0]}, "b": {"point": [x, 30, 0]}, "name": "Axis1"});
|
||||
run(&mut s, "construct.axis.two_points", axis(10.0));
|
||||
run(&mut s, "timeline.reorder", json!({"feature": "Axis1", "position": 0}));
|
||||
run(&mut s, "sketch.edit", json!({"sketch": sketch}));
|
||||
let r = run(&mut s, "sketch.project", json!({"refs": [{"axis": "Axis1"}]}));
|
||||
let line = r["links"][0]["curves"][0].clone();
|
||||
// Where the projected line runs: its two ends' x, and its length along y.
|
||||
let at = |s: &mut Session| {
|
||||
run(s, "sketch.edit", json!({"sketch": sketch}));
|
||||
let si = inspect(s);
|
||||
let c = si["curves"].as_array().unwrap().iter().find(|c| c["id"] == line).cloned().unwrap();
|
||||
run(s, "sketch.finish", json!({}));
|
||||
let n = |v: &Value| v.as_f64().unwrap();
|
||||
(n(&c["start_at"][0]), n(&c["end_at"][0]), (n(&c["end_at"][1]) - n(&c["start_at"][1])).abs())
|
||||
};
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let (x0, x1, length) = at(&mut s);
|
||||
assert!((x0 - 10.0).abs() < 1e-9 && (x1 - 10.0).abs() < 1e-9 && length > 30.0, "{x0} {x1} {length}");
|
||||
run(&mut s, "timeline.redefine", json!({"feature": "Axis1", "command": "construct.axis.two_points", "params": axis(25.0)}));
|
||||
let (x0, x1, _) = at(&mut s);
|
||||
assert!((x0 - 25.0).abs() < 1e-9 && (x1 - 25.0).abs() < 1e-9, "the projection follows the axis: {x0} {x1}");
|
||||
run(&mut s, "timeline.rename", json!({"feature": "Axis1", "name": "Hinge axis"}));
|
||||
let state = s.model.state();
|
||||
let link = &state.sketch(sketch).unwrap().sketch.links[0];
|
||||
assert_eq!(link.source, solidcraft_sketch::LinkSource::Axis { name: "Hinge axis".into() });
|
||||
assert!(!link.lost, "renamed, the axis is still found");
|
||||
// A design with the axis after the sketch opens with the link lost, saying why.
|
||||
run(&mut s, "timeline.reorder", json!({"feature": "Hinge axis", "position": 1}));
|
||||
let opened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
|
||||
assert!(opened.model.state().sketch(sketch).unwrap().sketch.links[0].lost);
|
||||
let warnings = opened.model.result(sketch).and_then(|r| r.warning.clone()).unwrap_or_default();
|
||||
assert!(warnings.contains("it must come earlier in the timeline"), "{warnings}");
|
||||
run(&mut s, "timeline.reorder", json!({"feature": "Hinge axis", "position": 0}));
|
||||
// An axis that is no origin axis and no construction axis is refused, by its name.
|
||||
run(&mut s, "sketch.edit", json!({"sketch": sketch}));
|
||||
let error = s.execute("sketch.project", &json!({"refs": [{"axis": "Axis9"}]})).unwrap_err().to_string();
|
||||
assert!(error.contains("unknown axis `Axis9`"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_whole_body_can_create_unlinked_geometry() {
|
||||
let mut s = Session::default();
|
||||
|
||||
Reference in New Issue
Block a user