M0: desktop app: toolbar, browser, timeline, GPU viewport, view cube, sketch tools, dialogs, control channel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3714342737
commit
8a811f8eff
33 files changed
+7321
-32
No files matched your search
@@ -0,0 +1,281 @@
|
||||
//! 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::{Color32, Pos2, Shape, Stroke};
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_engine::geom::Vec2;
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::viewport::{Hit, Proj, pick, sketch_point_at};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Kind {
|
||||
/// Click points; `n` points complete the shape (0 = chain until finished).
|
||||
Draw(usize),
|
||||
/// Pick sketch entities; `n` picks run the command.
|
||||
Pick(usize),
|
||||
Dimension,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Tool {
|
||||
pub cmd: &'static str,
|
||||
pub kind: Kind,
|
||||
/// Clicked points (sketch coordinates) and the sketch point they snapped to.
|
||||
pub pts: Vec<(Vec2, Option<String>)>,
|
||||
pub hover: Option<(Vec2, Option<String>)>,
|
||||
/// Picked entity ids.
|
||||
pub picks: Vec<String>,
|
||||
}
|
||||
|
||||
impl Tool {
|
||||
pub fn for_command(id: &str) -> Option<Tool> {
|
||||
let (cmd, kind): (&'static str, Kind) = match id {
|
||||
"DrawPolyline" => ("DrawPolyline", Kind::Draw(0)),
|
||||
"ShapeRectangleTwoPoint" => ("ShapeRectangleTwoPoint", Kind::Draw(2)),
|
||||
"ShapeRectangleCenter" => ("ShapeRectangleCenter", Kind::Draw(2)),
|
||||
"ShapeRectangleThreePoint" => ("ShapeRectangleThreePoint", Kind::Draw(3)),
|
||||
"CircleCenterRadius" => ("CircleCenterRadius", Kind::Draw(2)),
|
||||
"CircleTwoPoint" => ("CircleTwoPoint", Kind::Draw(2)),
|
||||
"CircleThreePoint" => ("CircleThreePoint", Kind::Draw(3)),
|
||||
"ArcThreePoint" => ("ArcThreePoint", Kind::Draw(3)),
|
||||
"ArcCenterTwoPoint" => ("ArcCenterTwoPoint", Kind::Draw(3)),
|
||||
"ShapePolygonInscribed" => ("ShapePolygonInscribed", Kind::Draw(2)),
|
||||
"ShapePolygonCircumscribed" => ("ShapePolygonCircumscribed", Kind::Draw(2)),
|
||||
"ShapePolygonEdge" => ("ShapePolygonEdge", Kind::Draw(2)),
|
||||
"ShapeSlotCenterToCenter" => ("ShapeSlotCenterToCenter", Kind::Draw(3)),
|
||||
"ShapeSlotOverall" => ("ShapeSlotOverall", Kind::Draw(3)),
|
||||
"DrawPoint" => ("DrawPoint", Kind::Draw(1)),
|
||||
"SketchDimension" => ("SketchDimension", Kind::Dimension),
|
||||
"ConstraintHorizontalVertical" => ("ConstraintHorizontalVertical", Kind::Pick(1)),
|
||||
"ConstraintFix" => ("ConstraintFix", Kind::Pick(1)),
|
||||
"ConstraintCoincident" => ("ConstraintCoincident", Kind::Pick(2)),
|
||||
"ConstraintTangent" => ("ConstraintTangent", Kind::Pick(2)),
|
||||
"ConstraintEqual" => ("ConstraintEqual", Kind::Pick(2)),
|
||||
"ConstraintParallel" => ("ConstraintParallel", Kind::Pick(2)),
|
||||
"ConstraintPerpendicular" => ("ConstraintPerpendicular", Kind::Pick(2)),
|
||||
"ConstraintConcentric" => ("ConstraintConcentric", Kind::Pick(2)),
|
||||
"ConstraintCollinear" => ("ConstraintCollinear", Kind::Pick(2)),
|
||||
"ConstraintMidPoint" => ("ConstraintMidPoint", Kind::Pick(2)),
|
||||
"ConstraintSymmetry" => ("ConstraintSymmetry", Kind::Pick(3)),
|
||||
"sketch.construction" => ("sketch.construction", Kind::Pick(1)),
|
||||
_ => return None,
|
||||
};
|
||||
Some(Tool { cmd, kind, pts: Vec::new(), hover: None, picks: Vec::new() })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hint(id: &str) -> String {
|
||||
match id {
|
||||
"DrawPolyline" => "Line: click points; right-click or Esc to finish".into(),
|
||||
"ShapeRectangleTwoPoint" => "Rectangle: click two opposite corners".into(),
|
||||
"ShapeRectangleCenter" => "Center rectangle: click the centre, then a corner".into(),
|
||||
"ShapeRectangleThreePoint" => "3-point rectangle: click two points of one edge, then the width".into(),
|
||||
"CircleCenterRadius" => "Circle: click the centre, then a point on the circle".into(),
|
||||
"ArcThreePoint" => "Arc: click start, end, then a point on the arc".into(),
|
||||
"ArcCenterTwoPoint" => "Arc: click centre, start, then end (counter-clockwise)".into(),
|
||||
"SketchDimension" => "Dimension: pick a line, circle or arc (or two points/lines)".into(),
|
||||
"ShapeSlotCenterToCenter" | "ShapeSlotOverall" => "Slot: click both ends, then the width".into(),
|
||||
_ if id.starts_with("Constraint") => "Constraint: pick the sketch entities".into(),
|
||||
_ => "Click in the sketch".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn arg(p: &(Vec2, Option<String>)) -> Value {
|
||||
match &p.1 {
|
||||
Some(id) => json!(id),
|
||||
None => json!([p.0.x, p.0.y]),
|
||||
}
|
||||
}
|
||||
|
||||
fn xy(p: &(Vec2, Option<String>)) -> Value {
|
||||
json!([p.0.x, p.0.y])
|
||||
}
|
||||
|
||||
pub fn on_hover(app: &mut SolveApp, proj: &Proj, pos: Pos2) {
|
||||
let h = sketch_point_at(app, proj, pos);
|
||||
if let Some(t) = app.tool.as_mut() {
|
||||
t.hover = h;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_click(app: &mut SolveApp, proj: &Proj, pos: Pos2) {
|
||||
let Some(mut tool) = app.tool.take() else { return };
|
||||
match tool.kind {
|
||||
Kind::Draw(n) => {
|
||||
if let Some(p) = sketch_point_at(app, proj, pos) {
|
||||
tool.pts.push(p);
|
||||
}
|
||||
if tool.cmd == "DrawPolyline" {
|
||||
if tool.pts.len() >= 2 {
|
||||
let (a, b) = (tool.pts[tool.pts.len() - 2].clone(), tool.pts[tool.pts.len() - 1].clone());
|
||||
if let Ok(v) = app.run("DrawPolyline", json!({"points": [arg(&a), arg(&b)], "infer": true})) {
|
||||
// Chain: the next segment starts at this one's end point.
|
||||
if let Some(id) = v["curves"].get(0).and_then(Value::as_str) {
|
||||
let last = tool.pts.len() - 1;
|
||||
tool.pts[last].1 = Some(format!("{id}.end"));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if n > 0 && tool.pts.len() >= n {
|
||||
run_shape(app, &tool);
|
||||
tool.pts.clear();
|
||||
}
|
||||
}
|
||||
Kind::Pick(n) => {
|
||||
let hits = pick(app, proj, pos);
|
||||
if let Some(id) = hits.iter().find_map(|h| match h {
|
||||
Hit::SketchPoint { id, .. } | Hit::SketchCurve { id, .. } => Some(id.clone()),
|
||||
_ => None,
|
||||
}) {
|
||||
tool.picks.push(id);
|
||||
}
|
||||
if tool.picks.len() >= n {
|
||||
run_pick(app, &tool);
|
||||
tool.picks.clear();
|
||||
}
|
||||
}
|
||||
Kind::Dimension => {
|
||||
let hits = pick(app, proj, pos);
|
||||
let hit = hits.iter().find_map(|h| match h {
|
||||
Hit::SketchPoint { id, .. } => Some((id.clone(), true)),
|
||||
Hit::SketchCurve { id, .. } => Some((id.clone(), false)),
|
||||
_ => None,
|
||||
});
|
||||
if let Some((id, is_point)) = hit {
|
||||
tool.picks.push(id.clone());
|
||||
let st = app.session.model.state();
|
||||
let is_line = app
|
||||
.session
|
||||
.active_sketch
|
||||
.and_then(|s| st.sketch(s))
|
||||
.and_then(|ss| ss.sketch.curve_index(&id).and_then(|c| ss.sketch.curves.get(c).cloned()))
|
||||
.map(|c| matches!(c.kind, solvecraft_engine::sketch::CurveKind::Line { .. }));
|
||||
let ready = (tool.picks.len() == 1 && !is_point && is_line != Some(true)) || tool.picks.len() >= 2;
|
||||
let single_line = tool.picks.len() == 1 && is_line == Some(true);
|
||||
if ready || single_line {
|
||||
// A single line gets its length right away; a second pick (line or point) replaces it.
|
||||
if let Ok(v) = app.run("SketchDimension", json!({"entities": tool.picks}))
|
||||
&& let Some(p) = v["param"].as_str()
|
||||
{
|
||||
app.dialog = Some(crate::dialogs::Dialog::edit_param(&app.session, p));
|
||||
}
|
||||
tool.picks.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
app.tool = Some(tool);
|
||||
}
|
||||
|
||||
fn run_shape(app: &mut SolveApp, t: &Tool) {
|
||||
let p = &t.pts;
|
||||
let d = |a: usize, b: usize| p[a].0.dist(p[b].0);
|
||||
let params = match t.cmd {
|
||||
"ShapeRectangleTwoPoint" => json!({"p0": xy(&p[0]), "p1": xy(&p[1])}),
|
||||
"ShapeRectangleCenter" => json!({"center": xy(&p[0]), "corner": xy(&p[1])}),
|
||||
"ShapeRectangleThreePoint" => json!({"p0": xy(&p[0]), "p1": xy(&p[1]), "p2": xy(&p[2])}),
|
||||
"CircleCenterRadius" => json!({"center": arg(&p[0]), "radius": d(0, 1)}),
|
||||
"CircleTwoPoint" => json!({"p0": xy(&p[0]), "p1": xy(&p[1])}),
|
||||
"CircleThreePoint" => json!({"p0": xy(&p[0]), "p1": xy(&p[1]), "p2": xy(&p[2])}),
|
||||
"ArcThreePoint" => json!({"start": arg(&p[0]), "end": arg(&p[1]), "through": xy(&p[2])}),
|
||||
"ArcCenterTwoPoint" => json!({"center": arg(&p[0]), "start": arg(&p[1]), "end": xy(&p[2])}),
|
||||
"ShapePolygonInscribed" | "ShapePolygonCircumscribed" => {
|
||||
let a = (p[1].0 - p[0].0).angle().to_degrees();
|
||||
json!({"center": xy(&p[0]), "radius": d(0, 1), "sides": 6, "angle": a})
|
||||
}
|
||||
"ShapePolygonEdge" => json!({"p0": xy(&p[0]), "p1": xy(&p[1]), "sides": 6}),
|
||||
"ShapeSlotCenterToCenter" | "ShapeSlotOverall" => {
|
||||
let dir = (p[1].0 - p[0].0).normalized().unwrap_or(Vec2::X);
|
||||
let w = 2.0 * dir.cross(p[2].0 - p[0].0).abs();
|
||||
json!({"p0": xy(&p[0]), "p1": xy(&p[1]), "width": w.max(1e-3)})
|
||||
}
|
||||
"DrawPoint" => json!({"point": xy(&p[0])}),
|
||||
_ => return,
|
||||
};
|
||||
let _ = app.run(t.cmd, params);
|
||||
}
|
||||
|
||||
fn run_pick(app: &mut SolveApp, t: &Tool) {
|
||||
let k = &t.picks;
|
||||
let params = match t.cmd {
|
||||
"ConstraintHorizontalVertical" => json!({"line": k[0]}),
|
||||
"ConstraintFix" => json!({"entity": k[0]}),
|
||||
"sketch.construction" => json!({"curves": [k[0]]}),
|
||||
"ConstraintCoincident" => json!({"a": k[0], "b": k[1]}),
|
||||
"ConstraintMidPoint" => json!({"point": k[0], "line": k[1]}),
|
||||
"ConstraintSymmetry" => json!({"a": k[0], "b": k[1], "line": k[2]}),
|
||||
_ => json!({"a": k[0], "b": k[1]}),
|
||||
};
|
||||
let _ = app.run(t.cmd, params);
|
||||
}
|
||||
|
||||
/// Right click / Esc: end the current chain (line) or cancel the tool.
|
||||
pub fn finish(app: &mut SolveApp) {
|
||||
if let Some(t) = app.tool.as_mut()
|
||||
&& !t.pts.is_empty()
|
||||
{
|
||||
t.pts.clear();
|
||||
t.picks.clear();
|
||||
return;
|
||||
}
|
||||
app.tool = None;
|
||||
}
|
||||
|
||||
/// Rubber-band preview of the shape being drawn.
|
||||
pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) {
|
||||
let Some(sid) = app.session.active_sketch else { return };
|
||||
let st = app.session.model.state();
|
||||
let Some(ss) = st.sketch(sid) else { return };
|
||||
let to = |p: Vec2| proj.to_screen(ss.plane.to_world(p));
|
||||
let stroke = Stroke::new(1.5, Color32::from_rgb(30, 120, 230));
|
||||
if let Some((hp, snap)) = &t.hover
|
||||
&& let Some(sp) = to(*hp)
|
||||
{
|
||||
painter.circle_stroke(sp, if snap.is_some() { 6.0 } else { 3.0 }, stroke);
|
||||
painter.text(
|
||||
sp + egui::vec2(10.0, 10.0),
|
||||
egui::Align2::LEFT_TOP,
|
||||
format!("{:.2}, {:.2}", hp.x, hp.y),
|
||||
egui::FontId::proportional(11.0),
|
||||
Color32::from_rgb(60, 70, 90),
|
||||
);
|
||||
}
|
||||
let Some((h, _)) = t.hover.clone() else { 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()) {
|
||||
("DrawPolyline", [.., last]) => line(*last, h),
|
||||
("ShapeRectangleTwoPoint", [a]) => {
|
||||
let (b, c) = (Vec2::new(h.x, a.y), Vec2::new(a.x, h.y));
|
||||
line(*a, b);
|
||||
line(b, h);
|
||||
line(h, c);
|
||||
line(c, *a);
|
||||
}
|
||||
("ShapeRectangleCenter", [c]) => {
|
||||
let d = h - *c;
|
||||
let k =
|
||||
[Vec2::new(c.x - d.x, c.y - d.y), Vec2::new(c.x + d.x, c.y - d.y), Vec2::new(c.x + d.x, c.y + d.y), Vec2::new(c.x - d.x, c.y + d.y)];
|
||||
for i in 0..4 {
|
||||
line(k[i], k[(i + 1) % 4]);
|
||||
}
|
||||
}
|
||||
("CircleCenterRadius" | "ShapePolygonInscribed" | "ShapePolygonCircumscribed", [c]) => circle(*c, c.dist(h)),
|
||||
("CircleTwoPoint", [a]) => circle((*a + h) * 0.5, a.dist(h) * 0.5),
|
||||
(_, [a]) => line(*a, h),
|
||||
(_, [a, b]) => {
|
||||
line(*a, *b);
|
||||
line(*b, h);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user