3D sketch: with 3D Sketch on, the Line, Spline and Point tools take points off the sketch plane, snapping to model vertices, edges and faces and to drawn 3D points (segments that stay on the plane are ordinary sketch lines); drawn 3D curves are wires with fit points (sketch.line3d, sketch.spline3d: centripetal Catmull-Rom, sketch.point3d), a clicked 3D point gets a move triad (drag along X/Y/Z or parallel to the sketch plane: sketch.move3d, joined ends move together), Delete removes it; sweep and pipe paths may mix 3D curves with sketch curves; control: ui.triad, ui.worldToScreen
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
d2068e07a3
commit
d40ef5bbdc
14 files changed
+755
-10
No files matched your search
@@ -21,6 +21,7 @@ mod parameters;
|
||||
mod plastic;
|
||||
mod sheet;
|
||||
mod sketch;
|
||||
mod sketch_3d;
|
||||
mod sketch_canvas;
|
||||
mod sketch_constraints;
|
||||
mod sketch_create;
|
||||
@@ -130,6 +131,7 @@ pub fn command_specs() -> Vec<&'static CommandSpec> {
|
||||
v.extend(sketch_import::COMMANDS.iter());
|
||||
v.extend(sketch_inspect::COMMANDS.iter());
|
||||
v.extend(sketch_canvas::COMMANDS.iter());
|
||||
v.extend(sketch_3d::COMMANDS.iter());
|
||||
v.extend(sketch_modify::COMMANDS.iter());
|
||||
v.extend(sketch_project::COMMANDS.iter());
|
||||
v.extend(features::COMMANDS.iter());
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
//! 3D sketch drawing: with the sketch's 3D Sketch option on (Sketch Palette), lines, splines and
|
||||
//! points may leave the sketch plane. They are kept as drawn 3D curves (wires with fit points):
|
||||
//! drawn and snapped to, movable point by point, and usable as sweep and pipe paths.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_geom::Vec3;
|
||||
|
||||
use super::sketch::edit;
|
||||
use super::{CommandSpec, in_sketch};
|
||||
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.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)"),
|
||||
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"),
|
||||
];
|
||||
|
||||
fn points(cmd: &str, p: &Value, min: usize) -> Result<Vec<Vec3>> {
|
||||
let a = p.get("points").and_then(Value::as_array).ok_or_else(|| bad(cmd, "`points` must list [x,y,z] points"))?;
|
||||
if a.len() < min || a.len() > solvecraft_sketch::MAX_FIT_POINTS {
|
||||
return Err(bad(cmd, format!("`points` needs {min} to {} points", solvecraft_sketch::MAX_FIT_POINTS)));
|
||||
}
|
||||
a.iter().map(|v| vec3(v).ok_or_else(|| bad(cmd, "a point must be [x, y, z]"))).collect()
|
||||
}
|
||||
|
||||
fn needs_3d(sk: &solvecraft_sketch::Sketch, cmd: &str) -> Result<()> {
|
||||
if sk.view.three_d { Ok(()) } else { Err(bad(cmd, "turn on 3D Sketch (sketch.options sketch_3d: true) to draw off the sketch plane")) }
|
||||
}
|
||||
|
||||
fn line3d(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.line3d";
|
||||
let pts = points(cmd, p, 2)?;
|
||||
let (ids, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
needs_3d(sk, cmd)?;
|
||||
pts.windows(2).map(|w| sk.add_drawn_wire(w).map_err(Into::into)).collect::<Result<Vec<String>>>()
|
||||
})?;
|
||||
Ok(json!({"curves": ids, "sketch": info}))
|
||||
}
|
||||
|
||||
fn spline3d(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.spline3d";
|
||||
let pts = points(cmd, p, 2)?;
|
||||
let (id, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
needs_3d(sk, cmd)?;
|
||||
Ok(sk.add_drawn_wire(&pts)?)
|
||||
})?;
|
||||
Ok(json!({"curves": [id], "sketch": info}))
|
||||
}
|
||||
|
||||
fn point3d(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.point3d";
|
||||
let at = req_vec3(cmd, p, "point")?;
|
||||
let (id, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
needs_3d(sk, cmd)?;
|
||||
Ok(sk.add_drawn_wire(&[at])?)
|
||||
})?;
|
||||
Ok(json!({"point": id, "sketch": info}))
|
||||
}
|
||||
|
||||
fn move3d(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.move3d";
|
||||
let wire = p.get("wire").and_then(Value::as_str).ok_or_else(|| bad(cmd, "`wire` must be a 3D curve or point id"))?.to_string();
|
||||
let index = p.get("index").and_then(Value::as_u64).unwrap_or(0) as usize;
|
||||
let to = p.get("to").and_then(vec3);
|
||||
let by = p.get("by").and_then(vec3);
|
||||
let (n, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let from = sk
|
||||
.wire_index(&wire)
|
||||
.and_then(|w| sk.wires.get(w))
|
||||
.filter(|w| !w.fit.is_empty())
|
||||
.and_then(|w| w.fit.get(index))
|
||||
.copied()
|
||||
.ok_or_else(|| bad(cmd, format!("`{wire}` has no drawn point {index}")))?;
|
||||
let to = match (to, by) {
|
||||
(Some(t), _) => t,
|
||||
(None, Some(d)) => from + d,
|
||||
_ => return Err(bad(cmd, "give `to` or `by`")),
|
||||
};
|
||||
Ok(sk.move_drawn_point(&wire, index, to)?)
|
||||
})?;
|
||||
Ok(json!({"moved": n, "sketch": info}))
|
||||
}
|
||||
@@ -564,3 +564,58 @@ fn constraints_and_dimensions_can_be_selected_and_deleted_together() {
|
||||
run(&mut s, "selection.delete", json!({"items": items}));
|
||||
assert!(sketch(&s).constraints.is_empty());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// 3D sketch
|
||||
|
||||
#[test]
|
||||
fn three_d_curves_need_the_option_and_move_joined() {
|
||||
let mut s = new_sketch();
|
||||
assert!(s.execute("sketch.line3d", &json!({"points": [[0, 0, 0], [0, 0, 10]]})).is_err());
|
||||
run(&mut s, "sketch.options", json!({"sketch_3d": true}));
|
||||
let r = run(&mut s, "sketch.line3d", json!({"points": [[0, 0, 0], [0, 0, 10], [10, 0, 10]]}));
|
||||
let ids = curve_ids(&r);
|
||||
assert_eq!(ids.len(), 2);
|
||||
run(&mut s, "sketch.point3d", json!({"point": [5, 5, 5]}));
|
||||
let sp = run(&mut s, "sketch.spline3d", json!({"points": [[10, 0, 10], [15, 5, 12], [20, 0, 20]]}));
|
||||
let sid = curve_ids(&sp)[0].clone();
|
||||
let sk = sketch(&s);
|
||||
let w = &sk.wires[sk.wire_index(&sid).expect("spline")];
|
||||
assert!(w.pts.len() > 10);
|
||||
assert!(w.pts.first().expect("p").dist(solvecraft_geom::Vec3::new(10.0, 0.0, 10.0)) < 1e-12);
|
||||
assert!(w.pts.last().expect("p").dist(solvecraft_geom::Vec3::new(20.0, 0.0, 20.0)) < 1e-12);
|
||||
// The shared corner moves for both lines (and the spline that starts there).
|
||||
let r = run(&mut s, "sketch.move3d", json!({"wire": ids[1], "index": 1, "by": [0, 0, 5]}));
|
||||
assert_eq!(r["moved"], 2);
|
||||
let r = run(&mut s, "sketch.move3d", json!({"wire": ids[0], "index": 1, "to": [0, 3, 12]}));
|
||||
assert_eq!(r["moved"], 2);
|
||||
let sk = sketch(&s);
|
||||
let fit = |id: &str| sk.wires[sk.wire_index(id).expect("w")].fit.clone();
|
||||
assert_eq!(fit(&ids[0])[1], fit(&ids[1])[0]);
|
||||
assert!(s.execute("sketch.move3d", &json!({"wire": "nope", "to": [0, 0, 0]})).is_err());
|
||||
assert!(s.execute("sketch.line3d", &json!({"points": [[0, 0, 0], [0, 0, 0]]})).is_err());
|
||||
assert!(s.execute("sketch.line3d", &json!({"points": [[0, 0, 0], [f64::NAN, 0, 0]]})).is_err());
|
||||
// Deleting works like any sketch entity.
|
||||
run(&mut s, "sketch.delete", json!({"entities": [sid]}));
|
||||
assert!(sketch(&s).wire_index(&curve_ids(&sp)[0]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipe_and_sweep_follow_3d_sketch_curves() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Path"}));
|
||||
run(&mut s, "sketch.options", json!({"sketch_3d": true}));
|
||||
let r = run(&mut s, "sketch.line3d", json!({"points": [[0, 0, 0], [0, 0, 30], [20, 0, 30]]}));
|
||||
let mut path = curve_ids(&r);
|
||||
let sp = run(&mut s, "sketch.spline3d", json!({"points": [[20, 0, 30], [35, 10, 35], [50, 0, 50]]}));
|
||||
path.extend(curve_ids(&sp));
|
||||
run(&mut s, "SketchStop", json!({}));
|
||||
let p = run(&mut s, "PrimitivePipe", json!({"path_sketch": "Path", "path": path, "diameter": 4}));
|
||||
let si = run(&mut s, "document.inspect", json!({"measure": true}));
|
||||
let body = si["bodies"].as_array().and_then(|b| b.first()).cloned().unwrap_or_default();
|
||||
// The tube runs from the first line's start to the spline's end (radius 2 around them).
|
||||
let (lo, hi) = (&body["bbox"]["min"], &body["bbox"]["max"]);
|
||||
let near = |v: &Value, x: f64| (v.as_f64().unwrap_or(f64::NAN) - x).abs() < 0.5;
|
||||
assert!(near(&lo[0], -2.0) && near(&lo[2], 0.0) && near(&hi[0], 52.0) && near(&hi[2], 50.0), "{p} {body}");
|
||||
assert!(body["volume_mm3"].as_f64().unwrap_or(0.0) > 4.0 * std::f64::consts::PI * 50.0 * 0.95, "{p} {body}");
|
||||
}
|
||||
Reference in new issue
Block a user