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:
bherbruckandClaude Opus 5.5 committed 2026-10-08 08:39:22 +00:00
1 parent d2068e07a3
commit d40ef5bbdc
14 files changed
+755 -10

No files matched your search

+1 -1
View File
@@ -29,7 +29,7 @@ pub use profiles::{Profile, find_drawn_profiles, find_profiles, merge_regions};
pub use shape::{Shape, intersections};
pub use solver::{Hold, SolveReport, SolveStatus, solve, solve_holding};
pub use text::{MAX_TEXT_CHARS, text_geometry};
pub use wire::{MAX_WIRE_POINTS, Wire};
pub use wire::{MAX_FIT_POINTS, MAX_WIRE_POINTS, Wire, fit_polyline};
#[cfg(test)]
mod tests;
+87 -1
View File
@@ -18,6 +18,53 @@ pub struct Wire {
pub pts: Vec<Vec3>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub link: Option<String>,
/// Drawn in a 3D sketch: the points the curve was drawn through (one: a point; two: a
/// line; more: a smooth spline). `pts` is made from them.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fit: Vec<Vec3>,
}
/// Most fit points of a drawn 3D curve.
pub const MAX_FIT_POINTS: usize = 500;
/// Samples per span of a drawn 3D spline.
const SPAN_SAMPLES: usize = 24;
/// The polyline of a drawn 3D curve through `fit`: the point itself, the line, or a centripetal
/// Catmull–Rom spline through the points (smooth, no overshoot loops).
pub fn fit_polyline(fit: &[Vec3]) -> Vec<Vec3> {
if fit.len() <= 2 {
return fit.to_vec();
}
let n = fit.len();
let at = |i: isize| -> Vec3 {
match usize::try_from(i) {
Ok(i) if i < n => fit[i],
// Mirrored end tangents.
Ok(_) => fit[n - 1] * 2.0 - fit[n - 2],
Err(_) => fit[0] * 2.0 - fit[1],
}
};
let mut out = vec![fit[0]];
for k in 0..n - 1 {
let k = k as isize;
let (p0, p1, p2, p3) = (at(k - 1), at(k), at(k + 1), at(k + 2));
let tj = |a: Vec3, b: Vec3| a.dist(b).sqrt().max(1e-9);
let t0 = 0.0;
let t1 = t0 + tj(p0, p1);
let t2 = t1 + tj(p1, p2);
let t3 = t2 + tj(p2, p3);
for s in 1..=SPAN_SAMPLES {
let t = t1 + (t2 - t1) * s as f64 / SPAN_SAMPLES as f64;
let a1 = p0 * ((t1 - t) / (t1 - t0)) + p1 * ((t - t0) / (t1 - t0));
let a2 = p1 * ((t2 - t) / (t2 - t1)) + p2 * ((t - t1) / (t2 - t1));
let a3 = p2 * ((t3 - t) / (t3 - t2)) + p3 * ((t - t2) / (t3 - t2));
let b1 = a1 * ((t2 - t) / (t2 - t0)) + a2 * ((t - t0) / (t2 - t0));
let b2 = a2 * ((t3 - t) / (t3 - t1)) + a3 * ((t - t1) / (t3 - t1));
out.push(b1 * ((t2 - t) / (t2 - t1)) + b2 * ((t - t1) / (t2 - t1)));
}
}
out
}
fn clean(w: &[Vec3]) -> Option<Vec<Vec3>> {
@@ -73,11 +120,50 @@ impl Sketch {
Some(o) => o.clone(),
None => self.fresh("w"),
};
self.wires.push(Wire { id: wid, pts, link: Some(id.to_string()) });
self.wires.push(Wire { id: wid, pts, link: Some(id.to_string()), fit: Vec::new() });
}
Ok(())
}
/// Add a curve drawn in a 3D sketch through `fit` (see [`Wire::fit`]).
pub fn add_drawn_wire(&mut self, fit: &[Vec3]) -> Result<String> {
if fit.is_empty() || fit.len() > MAX_FIT_POINTS {
return Err(SketchError::Invalid(format!("a 3D curve needs 1 to {MAX_FIT_POINTS} points")));
}
if fit.iter().any(|p| !(p.is_finite() && p.x.abs() < 1e9 && p.y.abs() < 1e9 && p.z.abs() < 1e9)) {
return Err(SketchError::Invalid("3D point out of range".into()));
}
if fit.windows(2).any(|w| w[0].dist(w[1]) < 1e-9) {
return Err(SketchError::Invalid("the points of a 3D curve must differ".into()));
}
let id = self.fresh(if fit.len() == 1 { "q" } else { "w" });
self.wires.push(Wire { id: id.clone(), pts: fit_polyline(fit), link: None, fit: fit.to_vec() });
Ok(id)
}
/// Move a fit point of a drawn 3D curve, and every other drawn point at the same place
/// (curves drawn end to end stay joined). Returns how many points moved.
pub fn move_drawn_point(&mut self, wire: &str, index: usize, to: Vec3) -> Result<usize> {
if !(to.is_finite() && to.x.abs() < 1e9 && to.y.abs() < 1e9 && to.z.abs() < 1e9) {
return Err(SketchError::Invalid("3D point out of range".into()));
}
let w = self.wire_index(wire).ok_or_else(|| SketchError::Unknown(wire.into()))?;
let from = self.wires.get(w).and_then(|w| w.fit.get(index)).copied().ok_or_else(|| SketchError::Unknown(format!("{wire} point {index}")))?;
let mut n = 0;
for w in self.wires.iter_mut().filter(|w| !w.fit.is_empty()) {
let mut changed = false;
for p in w.fit.iter_mut().filter(|p| p.dist(from) < 1e-9) {
*p = to;
changed = true;
n += 1;
}
if changed {
w.pts = fit_polyline(&w.fit);
}
}
Ok(n)
}
/// Remove wires by id.
pub fn remove_wires(&mut self, ids: &[String]) {
self.wires.retain(|w| !ids.contains(&w.id));