feat: preserve adaptive sketch profiles and curved modeling workflows

This commit is contained in:
bherbruck committed 2026-10-11 09:06:25 -04:00
1 parent 4c4a0449a4
commit 4e595deff2
62 files changed
+7114 -362

No files matched your search

+120 -7
View File
@@ -1,13 +1,17 @@
//! Interactive sketch tools: click points on the sketch plane (or pick sketch entities) and the
//! tool runs the matching command. Tools are UI state only; the work is done by commands.
use egui::{Pos2, Shape, Stroke};
use egui::{Pos2, Stroke};
use serde_json::{Value, json};
use solvecraft_engine::geom::Vec2;
use crate::SolveApp;
use crate::viewport::{Hit, Proj, pick, sketch_point_at};
#[cfg(test)]
#[path = "sketch_display_preview_tests.rs"]
mod display_preview_tests;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
/// Click points; `n` points complete the shape (0 = chain until finished).
@@ -350,6 +354,117 @@ pub fn finish(app: &mut SolveApp) {
}
/// Rubber-band preview of the shape being drawn.
pub(crate) fn paint_preview_curve(
sketch: &solvecraft_engine::sketch::Sketch,
plane: &solvecraft_engine::geom::Plane,
painter: &egui::Painter,
proj: &Proj,
stroke: Stroke,
) {
paint_curve(sketch, 0, plane, painter, proj, stroke, &mut solvecraft_engine::render::SamplingBudget::new(4096, 65_536));
}
pub(crate) fn paint_curve(
sketch: &solvecraft_engine::sketch::Sketch,
index: usize,
plane: &solvecraft_engine::geom::Plane,
painter: &egui::Painter,
proj: &Proj,
stroke: Stroke,
budget: &mut solvecraft_engine::render::SamplingBudget,
) {
for part in crate::sketch_display::curve_with_budget(sketch, index, plane, proj, painter.ctx().pixels_per_point(), budget).parts {
let screen: Vec<_> = part.into_iter().filter_map(|point| proj.to_screen(point)).collect();
if screen.len() >= 2 {
painter.add(egui::Shape::line(screen, stroke));
}
}
}
/// Temporary analytic geometry uses the same exact span extraction as committed curves.
/// It never solves constraints or modifies the document.
pub(crate) fn preview_curve(t: &Tool, hover: Vec2, sweep: Option<f64>) -> Option<solvecraft_engine::sketch::Sketch> {
use solvecraft_engine::sketch::{CurveKind, Sketch};
if !hover.is_finite() || t.pts.len() >= solvecraft_engine::sketch::MAX_SPLINE_POINTS || t.pts.iter().any(|p| !p.0.is_finite()) {
return None;
}
let mut sk = Sketch::new();
let pts: Vec<_> = t.pts.iter().map(|p| p.0).collect();
match (t.cmd, pts.as_slice()) {
("sketch.circle.center" | "sketch.polygon.inscribed" | "sketch.polygon.circumscribed", [c]) => {
sk.add_circle(*c, c.dist(hover), None, None).ok()?;
}
("sketch.circle.two_point", [a]) => {
sk.add_circle((*a + hover) * 0.5, a.dist(hover) * 0.5, None, None).ok()?;
}
("sketch.circle.three_point" | "sketch.arc.three_point", [a, b]) => {
let ab = *b - *a;
let ah = hover - *a;
let d = 2.0 * ab.cross(ah);
if !d.is_finite() || d.abs() < 1e-12 {
return None;
}
let centre = *a + Vec2::new(ah.y * ab.len2() - ab.y * ah.len2(), ab.x * ah.len2() - ah.x * ab.len2()) / d;
if t.cmd == "sketch.circle.three_point" {
sk.add_circle(centre, centre.dist(*a), None, None).ok()?;
} else {
let angle = |p: Vec2| (p - centre).angle();
let span = |p: Vec2| (angle(p) - angle(*a)).rem_euclid(std::f64::consts::TAU);
let ccw = span(hover) < span(*b);
sk.add_arc(centre, if ccw { *a } else { *b }, if ccw { *b } else { *a }, [None; 3], None).ok()?;
if let Some(curve) = sk.curves.first_mut() {
curve.reversed = !ccw;
}
}
}
("sketch.arc.center_point", [c, a]) => {
let start = (*a - *c).angle();
let sweep = sweep.unwrap_or_else(|| ((hover - *c).angle() - start).rem_euclid(std::f64::consts::TAU));
if !sweep.is_finite() || sweep.abs() <= 1e-9 || sweep.abs() >= std::f64::consts::TAU {
return None;
}
let b = *c + Vec2::from_angle(start + sweep) * c.dist(*a);
sk.add_arc(*c, if sweep > 0.0 { *a } else { b }, if sweep > 0.0 { b } else { *a }, [None; 3], None).ok()?;
if let Some(curve) = sk.curves.first_mut() {
curve.reversed = sweep < 0.0;
}
}
("sketch.ellipse", [c, m]) => {
let u = (*m - *c).normalized()?;
let d = hover - *c;
let k = 1.0 - (d.dot(u) / c.dist(*m)).powi(2);
if !k.is_finite() || k <= 1e-12 {
return None;
}
let r = d.cross(u).abs() / k.sqrt();
let c = sk.add_point(*c, None).ok()?;
let m = sk.add_point(*m, None).ok()?;
sk.add_curve(CurveKind::Ellipse { c, m, r }, None).ok()?;
}
("sketch.spline.fit_point" | "sketch.spline.control_point" | "sketch.spline.control_point_5", points) if !points.is_empty() => {
let periodic = t.cmd == "sketch.spline.fit_point" && points.len() >= 3 && points.first() == Some(&hover);
let mut indices = points.iter().map(|p| sk.add_point(*p, None).ok()).collect::<Option<Vec<_>>>()?;
if periodic {
indices.push(*indices.first()?);
} else if points.last() != Some(&hover) {
indices.push(sk.add_point(hover, None).ok()?);
}
sk.add_curve(
CurveKind::Spline {
pts: indices,
control: t.cmd != "sketch.spline.fit_point",
degree: if t.cmd == "sketch.spline.control_point_5" { 5 } else { 3 },
periodic,
},
None,
)
.ok()?;
}
_ => return None,
}
Some(sk)
}
pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) {
if crate::sketch3d::preview(app, t, painter, proj) {
return;
@@ -377,16 +492,16 @@ pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) {
}
let Some((h, _)) = t.hover.clone() else { return };
let h = crate::sketch_dims::effective(app, t, h);
if let Some(curve) = preview_curve(t, h, crate::sketch_dims::locked_value(app, t, "Sweep")) {
paint_preview_curve(&curve, &ss.plane, painter, proj, stroke);
return;
}
let pts: Vec<Vec2> = t.pts.iter().map(|p| p.0).collect();
let line = |a: Vec2, b: Vec2| {
if let (Some(x), Some(y)) = (to(a), to(b)) {
painter.line_segment([x, y], stroke);
}
};
let circle = |c: Vec2, r: f64| {
let v: Vec<Pos2> = (0..=64).filter_map(|i| to(c + Vec2::from_angle(i as f64 / 64.0 * std::f64::consts::TAU) * r)).collect();
painter.add(Shape::line(v, stroke));
};
match (t.cmd, pts.as_slice()) {
("sketch.line", [.., last]) => {
line(*last, h);
@@ -419,8 +534,6 @@ pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) {
line(k[i], k[(i + 1) % 4]);
}
}
("sketch.circle.center" | "sketch.polygon.inscribed" | "sketch.polygon.circumscribed", [c]) => circle(*c, c.dist(h)),
("sketch.circle.two_point", [a]) => circle((*a + h) * 0.5, a.dist(h) * 0.5),
(_, [a]) => line(*a, h),
(_, [a, b]) => {
line(*a, *b);