Construction toggle on every curve type: the selection switches as a whole (slots, polygons; to construction unless all already are), projected curves too; construction curves are drawn dashed and centerlines dash-dot; profiles update at once
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
00ddaa25f5
commit
7757e6d9c7
3 files changed
+109
-9
No files matched your search
@@ -149,7 +149,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.icon("construction")
|
||||
.key("X")
|
||||
.enabled(in_sketch)
|
||||
.params("curves: [ids], value?: bool (default toggles)"),
|
||||
.params("curves: [ids], value?: bool (default: construction unless all of them already are)"),
|
||||
CommandSpec::new("sketch.delete", "Delete Sketch Entities", sketch_delete)
|
||||
.enabled(in_sketch)
|
||||
.params("entities: [curve, point or constraint ids]"),
|
||||
@@ -1283,17 +1283,23 @@ fn construction(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let want = bool_(p, "value");
|
||||
let (n, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let cs: Vec<usize> =
|
||||
ids.iter().map(|id| sk.curve_index(id).ok_or_else(|| bad(cmd, format!("unknown curve `{id}`")))).collect::<Result<_>>()?;
|
||||
// The selection switches together (a slot or polygon as a whole): to construction
|
||||
// unless all of it already is. Projected curves may switch too.
|
||||
let to = want.unwrap_or_else(|| !cs.iter().all(|c| sk.curves.get(*c).is_some_and(|c| c.construction)));
|
||||
let mut n = 0;
|
||||
for id in &ids {
|
||||
let c = sk.curve_index(id).ok_or_else(|| bad(cmd, format!("unknown curve `{id}`")))?;
|
||||
if let Some(cu) = sk.curves.get_mut(c) {
|
||||
cu.construction = want.unwrap_or(!cu.construction);
|
||||
for c in cs {
|
||||
if let Some(cu) = sk.curves.get_mut(c)
|
||||
&& cu.construction != to
|
||||
{
|
||||
cu.construction = to;
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
Ok(n)
|
||||
Ok((n, to))
|
||||
})?;
|
||||
Ok(json!({"changed": n, "sketch": info}))
|
||||
Ok(json!({"changed": n.0, "construction": n.1, "sketch": info}))
|
||||
}
|
||||
|
||||
fn sketch_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
@@ -198,3 +198,57 @@ fn projected_geometry_counts_as_fixed() {
|
||||
assert_eq!(r["result"]["changed"], 0);
|
||||
assert!(sketch(&s).curve_locked(ci));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Construction toggle
|
||||
|
||||
fn profiles(s: &mut Session) -> i64 {
|
||||
run(s, "sketch.solve", json!({}))["profiles"].as_i64().unwrap_or(-1)
|
||||
}
|
||||
|
||||
fn curve_ids(v: &Value) -> Vec<String> {
|
||||
v["curves"].as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construction_toggle_takes_shapes_out_of_profiles_and_back() {
|
||||
let mut s = new_sketch();
|
||||
let rect = curve_ids(&run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [20, 10]})));
|
||||
let slot = curve_ids(&run(&mut s, "ShapeSlotCenterToCenter", json!({"p0": [40, 5], "p1": [60, 5], "width": 6})));
|
||||
let poly = curve_ids(&run(&mut s, "ShapePolygonInscribed", json!({"center": [0, 40], "radius": 8, "sides": 6})));
|
||||
let circ = curve_ids(&run(&mut s, "CircleCenterRadius", json!({"center": [40, 40], "radius": 5})));
|
||||
let ell = curve_ids(&run(&mut s, "CircleElipse", json!({"center": [70, 40], "major": [80, 40], "minor_radius": 4})));
|
||||
assert_eq!(profiles(&mut s), 5);
|
||||
for (shape, left) in [(&rect, 4), (&slot, 3), (&poly, 2), (&circ, 1), (&ell, 0)] {
|
||||
let r = run(&mut s, "sketch.construction", json!({ "curves": shape }));
|
||||
assert_eq!(r["construction"], true);
|
||||
assert_eq!(profiles(&mut s), left, "{shape:?}");
|
||||
}
|
||||
// Back again in one go.
|
||||
let all: Vec<String> = [rect, slot, poly, circ, ell].concat();
|
||||
let r = run(&mut s, "sketch.construction", json!({ "curves": all }));
|
||||
assert_eq!(r["construction"], false);
|
||||
assert_eq!(profiles(&mut s), 5);
|
||||
// A mixed selection becomes construction as a whole.
|
||||
run(&mut s, "sketch.construction", json!({"curves": ["l1"]}));
|
||||
let r = run(&mut s, "sketch.construction", json!({"curves": ["l1", "l2", "l3", "l4"]}));
|
||||
assert_eq!(r["construction"], true);
|
||||
assert!(sketch(&s).curves.iter().filter(|c| ["l1", "l2", "l3", "l4"].contains(&c.id.as_str())).all(|c| c.construction));
|
||||
assert!(s.execute("sketch.construction", &json!({"curves": ["nope"]})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projected_geometry_can_become_construction() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "SketchCreate", json!({"plane": "XY"}));
|
||||
run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [20, 10]}));
|
||||
run(&mut s, "SketchStop", json!({}));
|
||||
run(&mut s, "Extrude", json!({"distance": 5}));
|
||||
run(&mut s, "SketchCreate", json!({"plane": "XY"}));
|
||||
run(&mut s, "ProjectNewCmd", json!({"refs": [{"edge": [10, 0, 5]}]}));
|
||||
let id = sketch(&s).curves.iter().find(|c| c.link.is_some()).map(|c| c.id.clone()).expect("projected");
|
||||
let r = run(&mut s, "sketch.construction", json!({ "curves": [id.clone()] }));
|
||||
assert_eq!(r["changed"], 1);
|
||||
let sk = sketch(&s);
|
||||
assert!(sk.curves.iter().any(|c| c.id == id && c.construction && c.link.is_some()));
|
||||
}
|
||||
@@ -254,15 +254,55 @@ fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Construction curves are dashed, centerlines dash-dot (lengths follow the grid).
|
||||
let unit = grid_step(app.cam.half_height()).0;
|
||||
for (pts, col, cons) in sketch_lines(&ss.sketch, &ss.plane, active, &ss.report.curve_determined) {
|
||||
for w in pts.windows(2) {
|
||||
sc.line(w[0].to_f32(), w[1].to_f32(), sketch_color(&tk, col), if cons { 1.2 } else { 2.0 }, active);
|
||||
let segs = if !cons {
|
||||
pts.windows(2).map(|w| (w[0], w[1])).collect()
|
||||
} else if col == colors::SKETCH_CENTERLINE {
|
||||
dashes(&pts, &[0.6 * unit, 0.15 * unit, 0.05 * unit, 0.15 * unit])
|
||||
} else {
|
||||
dashes(&pts, &[0.3 * unit, 0.2 * unit])
|
||||
};
|
||||
for (a, b) in segs {
|
||||
sc.line(a.to_f32(), b.to_f32(), sketch_color(&tk, col), if cons { 1.2 } else { 2.0 }, active);
|
||||
}
|
||||
}
|
||||
}
|
||||
sc
|
||||
}
|
||||
|
||||
/// The drawn pieces of a polyline in a dash pattern (on, off, on, off… lengths).
|
||||
pub(crate) fn dashes(pts: &[Vec3], pattern: &[f64]) -> Vec<(Vec3, Vec3)> {
|
||||
let mut out = Vec::new();
|
||||
if pattern.is_empty() || pattern.iter().any(|l| !(l.is_finite() && *l > 0.0)) {
|
||||
return pts.windows(2).map(|w| (w[0], w[1])).collect();
|
||||
}
|
||||
let (mut k, mut left) = (0usize, pattern[0]);
|
||||
for w in pts.windows(2) {
|
||||
let (mut a, b) = (w[0], w[1]);
|
||||
let mut len = (b - a).len();
|
||||
// Cap the pieces per segment so a huge zoom-out can't explode the scene.
|
||||
let mut guard = 0;
|
||||
while len > 1e-12 && guard < 10_000 {
|
||||
guard += 1;
|
||||
let step = left.min(len);
|
||||
let q = a + (b - a) * (step / len);
|
||||
if k % 2 == 0 {
|
||||
out.push((a, q));
|
||||
}
|
||||
left -= step;
|
||||
len -= step;
|
||||
a = q;
|
||||
if left <= 1e-12 {
|
||||
k = (k + 1) % pattern.len();
|
||||
left = pattern.get(k).copied().unwrap_or(1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Colour as straight (unpremultiplied) sRGBA bytes for the GPU.
|
||||
fn c4(c: Color32) -> [u8; 4] {
|
||||
c.to_srgba_unmultiplied()
|
||||
|
||||
Reference in new issue
Block a user