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
@@ -207,6 +207,30 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) ->
|
||||
"dimension_selected": crate::dim_view::selected(app),
|
||||
"dimension_editing": crate::dim_view::editing(),
|
||||
})),
|
||||
"ui.triad" => {
|
||||
// Screen points of the 3D move triad's handles (x, y, z arrows and the plane square).
|
||||
let Some(rect) = app.viewport.rect else { return err("no viewport") };
|
||||
let proj = crate::viewport::projection(app, rect);
|
||||
ok(json!(
|
||||
crate::sketch3d::handle_points(app, &proj).into_iter().map(|(n, p)| json!({"handle": n, "x": p.x, "y": p.y})).collect::<Vec<_>>()
|
||||
))
|
||||
}
|
||||
"ui.worldToScreen" => {
|
||||
let Some(rect) = app.viewport.rect else { return err("no viewport") };
|
||||
let proj = crate::viewport::projection(app, rect);
|
||||
let pts: Vec<Value> = p
|
||||
.get("points")
|
||||
.and_then(Value::as_array)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.take(1000)
|
||||
.map(|v| {
|
||||
let c = |i: usize| v.get(i).and_then(Value::as_f64).unwrap_or(0.0);
|
||||
proj.to_screen(solvecraft_engine::geom::Vec3::new(c(0), c(1), c(2))).map_or(Value::Null, |q| json!([q.x, q.y]))
|
||||
})
|
||||
.collect();
|
||||
ok(json!(pts))
|
||||
}
|
||||
"ui.sketchToScreen" => {
|
||||
// Screen points of active-sketch coordinates (for driving sketch interaction).
|
||||
let st = app.session.model.state();
|
||||
|
||||
@@ -31,7 +31,7 @@ fn is_empty(p: &Value) -> bool {
|
||||
/// Delete what is selected (the Delete key). A selected sketch dimension or constraint glyph
|
||||
/// goes first, as before.
|
||||
pub fn delete_selection(app: &mut SolveApp) {
|
||||
if crate::sketch_tools::delete_glyph(app) {
|
||||
if crate::sketch3d::delete_selected(app) || crate::sketch_tools::delete_glyph(app) {
|
||||
return;
|
||||
}
|
||||
request(app, selection_params(app));
|
||||
|
||||
@@ -29,6 +29,7 @@ mod preselect_tests;
|
||||
pub mod preview;
|
||||
pub mod ref_images;
|
||||
pub mod selection;
|
||||
pub mod sketch3d;
|
||||
pub mod sketch_dims;
|
||||
#[cfg(test)]
|
||||
mod sketch_edit_tests;
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
//! 3D sketching in the viewport (the Sketch Palette's 3D Sketch option): the Line, Spline and
|
||||
//! Point tools take points off the sketch plane by snapping to model vertices, edges and faces
|
||||
//! and to points already drawn in 3D; segments that stay on the plane are ordinary sketch lines.
|
||||
//! A drawn 3D point (or curve end) clicked without a tool gets a move triad: drag an arrow to
|
||||
//! move along X, Y or Z, or the square to move parallel to the sketch plane. Everything runs
|
||||
//! `sketch.line3d`, `sketch.spline3d`, `sketch.point3d`, `sketch.move3d` and `DrawPolyline`.
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
use egui::{Pos2, Rect, Shape, Stroke, vec2};
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_engine::geom::{Vec2, Vec3};
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::theme::Tokens;
|
||||
use crate::tools::Tool;
|
||||
use crate::viewport::{Hit, Proj, pick};
|
||||
|
||||
thread_local! {
|
||||
/// Points of the curve being drawn in 3D (with the tool they belong to).
|
||||
static CHAIN: RefCell<(Option<&'static str>, Vec<Vec3>)> = const { RefCell::new((None, Vec::new())) };
|
||||
/// The drawn point with the triad: wire id and fit index.
|
||||
static TRIAD: RefCell<Option<(String, usize)>> = const { RefCell::new(None) };
|
||||
/// A triad drag: axis (None: in the sketch plane), undo depth, grab offset.
|
||||
static DRAG: RefCell<Option<(Option<Vec3>, usize, Vec3)>> = const { RefCell::new(None) };
|
||||
/// Where the cursor would put a point (for the rubber band), and its snap label.
|
||||
static HOVER: RefCell<Option<(Vec3, &'static str)>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Triad arrow length (screen px).
|
||||
const ARROW: f32 = 60.0;
|
||||
|
||||
/// Is the sketch being edited a 3D sketch?
|
||||
pub fn active(app: &SolveApp) -> bool {
|
||||
let st = app.session.model.state();
|
||||
app.session.active_sketch.and_then(|s| st.sketch(s)).is_some_and(|ss| ss.sketch.view.three_d)
|
||||
}
|
||||
|
||||
fn takes(cmd: &str) -> bool {
|
||||
matches!(cmd, "DrawPolyline" | "DrawSpline" | "DrawPoint")
|
||||
}
|
||||
|
||||
/// Points drawn in 3D: (wire id, fit index, point).
|
||||
fn drawn_points(app: &SolveApp) -> Vec<(String, usize, Vec3)> {
|
||||
let st = app.session.model.state();
|
||||
let Some(ss) = app.session.active_sketch.and_then(|s| st.sketch(s)) else { return Vec::new() };
|
||||
ss.sketch.wires.iter().flat_map(|w| w.fit.iter().enumerate().map(|(i, p)| (w.id.clone(), i, *p))).collect()
|
||||
}
|
||||
|
||||
/// Closest point of segment a–b to the ray (o, d).
|
||||
fn seg_ray_closest(a: Vec3, b: Vec3, o: Vec3, d: Vec3) -> Vec3 {
|
||||
let u = b - a;
|
||||
let w0 = a - o;
|
||||
let (aa, bb, cc) = (u.dot(u), u.dot(d), d.dot(d));
|
||||
let (dd, ee) = (u.dot(w0), d.dot(w0));
|
||||
let den = aa * cc - bb * bb;
|
||||
let s = if den.abs() < 1e-12 { 0.0 } else { ((bb * ee - cc * dd) / den).clamp(0.0, 1.0) };
|
||||
a + u * s
|
||||
}
|
||||
|
||||
/// The 3D point under the cursor and what it snapped to: a drawn 3D point, a model vertex, a
|
||||
/// point on a model edge or face, a sketch point, or the sketch plane.
|
||||
pub fn point_at(app: &SolveApp, proj: &Proj, pos: Pos2) -> Option<(Vec3, &'static str)> {
|
||||
for (_, _, p) in drawn_points(app) {
|
||||
if proj.to_screen(p).is_some_and(|s| s.distance(pos) < 8.0) {
|
||||
return Some((p, "Pt"));
|
||||
}
|
||||
}
|
||||
let st = app.session.model.state();
|
||||
let ss = app.session.active_sketch.and_then(|s| st.sketch(s))?;
|
||||
let (o, d) = proj.ray(pos);
|
||||
for h in pick(app, proj, pos) {
|
||||
match h {
|
||||
Hit::SketchPoint { at, .. } => return Some((ss.plane.to_world(at), "Pt")),
|
||||
Hit::Vertex { point, .. } => return Some((point, "Vertex")),
|
||||
Hit::Edge { body, index, .. } => {
|
||||
let ws = app.session.world_state();
|
||||
let m = ws.body(&body)?.mesh();
|
||||
let e = m.edges.get(index)?;
|
||||
let best = e
|
||||
.windows(2)
|
||||
.map(|w| seg_ray_closest(w[0], w[1], o, d))
|
||||
.min_by(|p, q| (*p - o).cross(d).len().total_cmp(&(*q - o).cross(d).len()))?;
|
||||
return Some((best, "Edge"));
|
||||
}
|
||||
Hit::Face { point, .. } => return Some((point, "Face")),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
ss.plane.intersect_ray(o, d).map(|w| (w, "Plane"))
|
||||
}
|
||||
|
||||
/// The sketch-plane coordinates of `p` when it lies on the plane.
|
||||
fn on_plane(app: &SolveApp, p: Vec3) -> Option<Vec2> {
|
||||
let st = app.session.model.state();
|
||||
let ss = app.session.active_sketch.and_then(|s| st.sketch(s))?;
|
||||
let q = ss.plane.to_local(p);
|
||||
(ss.plane.to_world(q).dist(p) < 1e-6).then_some(q)
|
||||
}
|
||||
|
||||
fn chain() -> Vec<Vec3> {
|
||||
CHAIN.with(|c| c.borrow().1.clone())
|
||||
}
|
||||
|
||||
fn set_chain(cmd: Option<&'static str>, pts: Vec<Vec3>) {
|
||||
CHAIN.with(|c| *c.borrow_mut() = (cmd, pts));
|
||||
}
|
||||
|
||||
/// The pointer moved: remember where a click would put the point.
|
||||
pub fn on_hover(app: &SolveApp, proj: &Proj, pos: Pos2) {
|
||||
let h = if app.tool.as_ref().is_some_and(|t| takes(t.cmd)) && active(app) { point_at(app, proj, pos) } else { None };
|
||||
HOVER.with(|x| *x.borrow_mut() = h);
|
||||
}
|
||||
|
||||
/// A click while a drawing tool is active in a 3D sketch (true: handled here).
|
||||
pub fn on_click(app: &mut SolveApp, tool: &mut Tool, proj: &Proj, pos: Pos2) -> bool {
|
||||
if !takes(tool.cmd) || !active(app) {
|
||||
return false;
|
||||
}
|
||||
let Some((p, _)) = point_at(app, proj, pos) else { return true };
|
||||
if CHAIN.with(|c| c.borrow().0) != Some(tool.cmd) {
|
||||
set_chain(Some(tool.cmd), Vec::new());
|
||||
}
|
||||
match tool.cmd {
|
||||
"DrawPoint" => {
|
||||
let r = match on_plane(app, p) {
|
||||
Some(q) => app.run("DrawPoint", json!({"point": [q.x, q.y]})),
|
||||
None => app.run("sketch.point3d", json!({"point": [p.x, p.y, p.z]})),
|
||||
};
|
||||
report(app, r);
|
||||
}
|
||||
"DrawPolyline" => {
|
||||
if let Some(a) = chain().last().copied() {
|
||||
let r = match (on_plane(app, a), on_plane(app, p)) {
|
||||
(Some(qa), Some(qb)) => app.run("DrawPolyline", json!({"points": [[qa.x, qa.y], [qb.x, qb.y]], "infer": true})),
|
||||
_ => app.run("sketch.line3d", json!({"points": [[a.x, a.y, a.z], [p.x, p.y, p.z]]})),
|
||||
};
|
||||
report(app, r);
|
||||
}
|
||||
let mut c = chain();
|
||||
c.push(p);
|
||||
set_chain(Some(tool.cmd), c);
|
||||
}
|
||||
_ => {
|
||||
let mut c = chain();
|
||||
c.push(p);
|
||||
set_chain(Some(tool.cmd), c);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn report(app: &mut SolveApp, r: Result<Value, String>) {
|
||||
if let Err(e) = r {
|
||||
app.set_status(e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Right-click / Esc while drawing in 3D: a spline is made from its points, a line chain ends
|
||||
/// (true: handled here; the tool stays for the next curve).
|
||||
pub fn finish(app: &mut SolveApp) -> bool {
|
||||
let (cmd, pts) = CHAIN.with(|c| c.borrow().clone());
|
||||
let Some(cmd) = cmd.filter(|c| app.tool.as_ref().is_some_and(|t| t.cmd == *c)) else {
|
||||
set_chain(None, Vec::new());
|
||||
return false;
|
||||
};
|
||||
if pts.is_empty() {
|
||||
set_chain(None, Vec::new());
|
||||
return false;
|
||||
}
|
||||
if cmd == "DrawSpline" && pts.len() >= 2 {
|
||||
let r = if pts.iter().all(|p| on_plane(app, *p).is_some()) {
|
||||
let q: Vec<Value> = pts.iter().filter_map(|p| on_plane(app, *p)).map(|q| json!([q.x, q.y])).collect();
|
||||
app.run("DrawSpline", json!({ "points": q }))
|
||||
} else {
|
||||
let q: Vec<Value> = pts.iter().map(|p| json!([p.x, p.y, p.z])).collect();
|
||||
app.run("sketch.spline3d", json!({ "points": q }))
|
||||
};
|
||||
report(app, r);
|
||||
}
|
||||
set_chain(Some(cmd), Vec::new());
|
||||
true
|
||||
}
|
||||
|
||||
/// Rubber band and snap marker of the curve being drawn in 3D (true: drawn here).
|
||||
pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) -> bool {
|
||||
if !takes(t.cmd) || !active(app) {
|
||||
return false;
|
||||
}
|
||||
let tk = Tokens::get();
|
||||
let stroke = Stroke::new(1.5, tk.rubber_band);
|
||||
let pts = if CHAIN.with(|c| c.borrow().0) == Some(t.cmd) { chain() } else { Vec::new() };
|
||||
let hover = HOVER.with(|h| *h.borrow());
|
||||
let mut all: Vec<Pos2> = pts.iter().filter_map(|p| proj.to_screen(*p)).collect();
|
||||
if let Some((h, label)) = hover
|
||||
&& let Some(s) = proj.to_screen(h)
|
||||
{
|
||||
if t.cmd != "DrawPoint" && !pts.is_empty() {
|
||||
all.push(s);
|
||||
}
|
||||
painter.circle_stroke(s, if label == "Plane" { 3.0 } else { 6.0 }, stroke);
|
||||
if label != "Plane" {
|
||||
painter.text(s + vec2(10.0, -14.0), egui::Align2::LEFT_BOTTOM, label, egui::FontId::proportional(10.0), tk.text);
|
||||
}
|
||||
painter.text(
|
||||
s + vec2(10.0, 10.0),
|
||||
egui::Align2::LEFT_TOP,
|
||||
format!("{:.2}, {:.2}, {:.2}", h.x, h.y, h.z),
|
||||
egui::FontId::proportional(11.0),
|
||||
tk.text_dim,
|
||||
);
|
||||
}
|
||||
if t.cmd == "DrawSpline" && all.len() >= 2 {
|
||||
let mut w: Vec<Vec3> = pts.clone();
|
||||
if let Some((h, _)) = hover {
|
||||
w.push(h);
|
||||
}
|
||||
let poly: Vec<Pos2> = solvecraft_engine::sketch::fit_polyline(&w).into_iter().filter_map(|p| proj.to_screen(p)).collect();
|
||||
painter.add(Shape::line(poly, stroke));
|
||||
} else {
|
||||
painter.add(Shape::line(all, stroke));
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// The move triad
|
||||
|
||||
fn triad_point(app: &SolveApp) -> Option<(String, usize, Vec3)> {
|
||||
let (w, i) = TRIAD.with(|t| t.borrow().clone())?;
|
||||
drawn_points(app).into_iter().find(|(id, k, _)| *id == w && *k == i)
|
||||
}
|
||||
|
||||
fn unit(v: egui::Vec2) -> Option<egui::Vec2> {
|
||||
(v.length() > 1e-3).then(|| v / v.length())
|
||||
}
|
||||
|
||||
/// The triad's handles on screen: (axis or None for the plane square, tip, base).
|
||||
fn handles(app: &SolveApp, proj: &Proj) -> Vec<(Option<Vec3>, Pos2, Pos2)> {
|
||||
let Some((_, _, p)) = triad_point(app) else { return Vec::new() };
|
||||
let Some(base) = proj.to_screen(p) else { return Vec::new() };
|
||||
let mut out = Vec::new();
|
||||
for axis in [Vec3::X, Vec3::Y, Vec3::Z] {
|
||||
if let Some(t) = proj.to_screen(p + axis)
|
||||
&& let Some(dir) = unit(t - base)
|
||||
{
|
||||
out.push((Some(axis), base + dir * ARROW, base));
|
||||
}
|
||||
}
|
||||
out.push((None, base + vec2(14.0, -14.0), base));
|
||||
out
|
||||
}
|
||||
|
||||
/// A click without a tool: a drawn 3D point gets the triad (true: handled).
|
||||
pub fn click(app: &mut SolveApp, proj: &Proj, pos: Pos2) -> bool {
|
||||
if !active(app) {
|
||||
TRIAD.with(|t| *t.borrow_mut() = None);
|
||||
return false;
|
||||
}
|
||||
let hit = drawn_points(app).into_iter().find(|(_, _, p)| proj.to_screen(*p).is_some_and(|s| s.distance(pos) < 8.0));
|
||||
let on_handle = handles(app, proj).iter().any(|(_, tip, _)| tip.distance(pos) < 9.0);
|
||||
if on_handle {
|
||||
return true;
|
||||
}
|
||||
TRIAD.with(|t| *t.borrow_mut() = hit.as_ref().map(|(w, i, _)| (w.clone(), *i)));
|
||||
if let Some((w, _, p)) = hit {
|
||||
app.set_status(
|
||||
format!("{w} at {:.2}, {:.2}, {:.2}: drag an arrow (X, Y, Z) or the square (in the sketch plane); Delete removes it", p.x, p.y, p.z),
|
||||
false,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// The drawn curve or point with the triad, for Delete.
|
||||
pub fn delete_selected(app: &mut SolveApp) -> bool {
|
||||
let Some((w, _)) = TRIAD.with(|t| t.borrow_mut().take()) else { return false };
|
||||
let r = app.run("sketch.delete", json!({ "entities": [w] }));
|
||||
report(app, r);
|
||||
true
|
||||
}
|
||||
|
||||
/// A drag starting on a triad handle (true: the triad took it).
|
||||
pub fn drag_start(app: &SolveApp, proj: &Proj, press: Pos2) -> bool {
|
||||
let Some((axis, _, _)) = handles(app, proj).into_iter().find(|(_, tip, _)| tip.distance(press) < 9.0) else { return false };
|
||||
let Some((_, _, p)) = triad_point(app) else { return false };
|
||||
let grab = target(app, proj, press, axis, p).map(|g| p - g).unwrap_or(Vec3::ZERO);
|
||||
DRAG.with(|d| *d.borrow_mut() = Some((axis, app.session.undo.len(), grab)));
|
||||
true
|
||||
}
|
||||
|
||||
/// Where the pointer puts the point: along `axis` through `p`, or in the plane through `p`
|
||||
/// parallel to the sketch plane.
|
||||
fn target(app: &SolveApp, proj: &Proj, pos: Pos2, axis: Option<Vec3>, p: Vec3) -> Option<Vec3> {
|
||||
let (o, d) = proj.ray(pos);
|
||||
match axis {
|
||||
Some(u) => {
|
||||
let w0 = p - o;
|
||||
let (a, b, c) = (u.dot(u), u.dot(d), d.dot(d));
|
||||
let den = a * c - b * b;
|
||||
if den.abs() < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
Some(p + u * ((b * d.dot(w0) - c * u.dot(w0)) / den))
|
||||
}
|
||||
None => {
|
||||
let st = app.session.model.state();
|
||||
let n = app.session.active_sketch.and_then(|s| st.sketch(s))?.plane.normal();
|
||||
let den = d.dot(n);
|
||||
(den.abs() > 1e-12).then(|| o + d * ((p - o).dot(n) / den))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dragging() -> bool {
|
||||
DRAG.with(|d| d.borrow().is_some())
|
||||
}
|
||||
|
||||
pub fn drag_to(app: &mut SolveApp, proj: &Proj, pos: Pos2) {
|
||||
let Some((axis, depth, grab)) = DRAG.with(|d| *d.borrow()) else { return };
|
||||
let Some((w, i, p)) = triad_point(app) else { return };
|
||||
let Some(to) = target(app, proj, pos, axis, p).map(|t| t + grab) else { return };
|
||||
if app.session.execute("sketch.move3d", &json!({"wire": w, "index": i, "to": [to.x, to.y, to.z]})).is_ok() {
|
||||
app.session.undo.truncate(depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drag_end() {
|
||||
DRAG.with(|d| *d.borrow_mut() = None);
|
||||
}
|
||||
|
||||
/// Draw the drawn 3D points and the triad.
|
||||
pub fn show(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
if app.session.active_sketch.is_none() {
|
||||
return;
|
||||
}
|
||||
let tk = Tokens::get();
|
||||
let sel = TRIAD.with(|t| t.borrow().clone());
|
||||
for (w, i, p) in drawn_points(app) {
|
||||
if let Some(s) = proj.to_screen(p) {
|
||||
let on = sel.as_ref().is_some_and(|(sw, si)| *sw == w && *si == i);
|
||||
painter.rect_filled(Rect::from_center_size(s, vec2(6.0, 6.0)), 0.0, if on { tk.accent } else { tk.sketch_point_free });
|
||||
}
|
||||
}
|
||||
if !active(app) {
|
||||
return;
|
||||
}
|
||||
for (axis, tip, base) in handles(app, proj) {
|
||||
match axis {
|
||||
Some(a) => {
|
||||
let col = if a == Vec3::X {
|
||||
tk.axis_x
|
||||
} else if a == Vec3::Y {
|
||||
tk.axis_y
|
||||
} else {
|
||||
tk.axis_z
|
||||
};
|
||||
painter.line_segment([base, tip], Stroke::new(2.0, col));
|
||||
if let Some(dir) = unit(tip - base) {
|
||||
let n = vec2(-dir.y, dir.x);
|
||||
painter.add(Shape::convex_polygon(vec![tip + dir * 8.0, tip + n * 4.0, tip - n * 4.0], col, Stroke::NONE));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
painter.rect_stroke(Rect::from_center_size(tip, vec2(10.0, 10.0)), 0.0, Stroke::new(1.5, tk.accent), egui::StrokeKind::Inside);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test hook: screen positions of the triad handles (axis name or "plane", tip).
|
||||
pub fn handle_points(app: &SolveApp, proj: &Proj) -> Vec<(&'static str, Pos2)> {
|
||||
handles(app, proj)
|
||||
.into_iter()
|
||||
.map(|(a, tip, _)| {
|
||||
let name = match a {
|
||||
Some(v) if v == Vec3::X => "x",
|
||||
Some(v) if v == Vec3::Y => "y",
|
||||
Some(_) => "z",
|
||||
None => "plane",
|
||||
};
|
||||
(name, tip)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -100,6 +100,7 @@ fn xy(p: &(Vec2, Option<String>)) -> Value {
|
||||
}
|
||||
|
||||
pub fn on_hover(app: &mut SolveApp, proj: &Proj, pos: Pos2) {
|
||||
crate::sketch3d::on_hover(app, proj, pos);
|
||||
let h = sketch_point_at(app, proj, pos);
|
||||
if let Some(t) = app.tool.as_mut() {
|
||||
t.hover = h;
|
||||
@@ -108,6 +109,11 @@ pub fn on_hover(app: &mut SolveApp, proj: &Proj, pos: Pos2) {
|
||||
|
||||
pub fn on_click(app: &mut SolveApp, proj: &Proj, pos: Pos2) {
|
||||
let Some(mut tool) = app.tool.take() else { return };
|
||||
// In a 3D sketch the drawing tools take points off the plane.
|
||||
if crate::sketch3d::on_click(app, &mut tool, proj, pos) {
|
||||
app.tool = Some(tool);
|
||||
return;
|
||||
}
|
||||
match tool.kind {
|
||||
Kind::Draw(_) => {
|
||||
if let Some(mut p) = sketch_point_at(app, proj, pos) {
|
||||
@@ -252,6 +258,9 @@ fn run_pick(app: &mut SolveApp, t: &Tool) {
|
||||
|
||||
/// Right click / Esc: end the current chain (line) or cancel the tool.
|
||||
pub fn finish(app: &mut SolveApp) {
|
||||
if crate::sketch3d::finish(app) {
|
||||
return;
|
||||
}
|
||||
if let Some(mut t) = app.tool.take_if(|t| t.kind == Kind::Ext) {
|
||||
if crate::sketch_tools::finish(app, &mut t) {
|
||||
app.tool = Some(t);
|
||||
@@ -270,6 +279,9 @@ pub fn finish(app: &mut SolveApp) {
|
||||
|
||||
/// Rubber-band preview of the shape being drawn.
|
||||
pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) {
|
||||
if crate::sketch3d::preview(app, t, painter, proj) {
|
||||
return;
|
||||
}
|
||||
if t.kind == Kind::Ext {
|
||||
return crate::sketch_tools::preview(app, t, painter, proj);
|
||||
}
|
||||
|
||||
@@ -1138,9 +1138,11 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let sketching = app.session.active_sketch.is_some();
|
||||
let on_dim = crate::dim_view::hit(p).filter(|_| sketching && !off("dimensions"));
|
||||
let on_glyph = crate::sketch_tools::glyph_at(p).filter(|_| sketching && !off("constraints"));
|
||||
match on_dim.or(on_glyph) {
|
||||
Some(id) => select(app, Some(Sel::SketchConstraint { id }), add),
|
||||
None => select(app, cand.and_then(|c| c.1), add),
|
||||
if !crate::sketch3d::click(app, &proj, p) {
|
||||
match on_dim.or(on_glyph) {
|
||||
Some(id) => select(app, Some(Sel::SketchConstraint { id }), add),
|
||||
None => select(app, cand.and_then(|c| c.1), add),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1190,13 +1192,20 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let (o, d) = proj.ray(p);
|
||||
ss.plane.intersect_ray(o, d).map(|w| ss.plane.to_local(w))
|
||||
};
|
||||
// A dimension's text follows a drag that starts on it.
|
||||
// The 3D triad, then a dimension's text, follow a drag that starts on them.
|
||||
if resp.drag_started_by(egui::PointerButton::Primary)
|
||||
&& let Some(a) = ui.input(|i| i.pointer.press_origin())
|
||||
&& !crate::sketch3d::drag_start(app, &proj, a)
|
||||
&& let Some(q) = on_plane(app, a)
|
||||
{
|
||||
crate::dim_view::drag_start(app, a, q);
|
||||
}
|
||||
if crate::sketch3d::dragging()
|
||||
&& (resp.dragged_by(egui::PointerButton::Primary) || resp.drag_stopped())
|
||||
&& let Some(p) = hover
|
||||
{
|
||||
crate::sketch3d::drag_to(app, &proj, p);
|
||||
}
|
||||
if crate::dim_view::dragging()
|
||||
&& (resp.dragged_by(egui::PointerButton::Primary) || resp.drag_stopped())
|
||||
&& let Some(q) = hover.and_then(|p| on_plane(app, p))
|
||||
@@ -1206,6 +1215,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
// A point or a curve of the sketch being edited follows a drag that starts on it.
|
||||
if resp.drag_started_by(egui::PointerButton::Primary)
|
||||
&& !crate::dim_view::dragging()
|
||||
&& !crate::sketch3d::dragging()
|
||||
&& let Some(a) = ui.input(|i| i.pointer.press_origin())
|
||||
{
|
||||
let active = app.session.active_sketch;
|
||||
@@ -1232,10 +1242,16 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
if resp.drag_stopped() {
|
||||
app.viewport.point_drag = None;
|
||||
crate::dim_view::drag_end();
|
||||
crate::sketch3d::drag_end();
|
||||
}
|
||||
}
|
||||
// Box selection: a primary drag on the model when no navigation mode is on.
|
||||
if app.tool.is_none() && app.viewport.nav.is_none() && app.viewport.point_drag.is_none() && !crate::dim_view::dragging() {
|
||||
if app.tool.is_none()
|
||||
&& app.viewport.nav.is_none()
|
||||
&& app.viewport.point_drag.is_none()
|
||||
&& !crate::dim_view::dragging()
|
||||
&& !crate::sketch3d::dragging()
|
||||
{
|
||||
let origin = ui.input(|i| i.pointer.press_origin());
|
||||
if resp.dragged_by(egui::PointerButton::Primary)
|
||||
&& let (Some(a), Some(b)) = (origin, hover)
|
||||
@@ -1255,6 +1271,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
crate::ref_images::show(app, ui, &painter, &proj);
|
||||
overlays(app, &painter, &proj);
|
||||
crate::dim_view::show(app, ui, &painter, &proj);
|
||||
crate::sketch3d::show(app, &painter, &proj);
|
||||
hover_highlight(app, &painter, &proj);
|
||||
points_2d(app, &painter, &proj);
|
||||
if let Some(bx) = app.viewport.boxsel {
|
||||
|
||||
Reference in new issue
Block a user