Sketch inline dimensions while drawing (line length/angle, rectangle width/height, circle diameter, arc radius/sweep, polygon radius/sides, slot length/width): boxes sit on what they measure, typing goes into the first box and locks it, Tab moves on, Enter places the shape, typed values become dimension constraints (expressions and units); Press Pull (Q: faces extrude/cut by sign, edges fillet), X toggles construction on selected curves, Move dialog (M) with preview

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-07 21:40:14 +00:00
1 parent ea3dd0ed6e
commit eb22a149b1
11 files changed
+411 -25

No files matched your search

+61 -23
View File
@@ -26,6 +26,11 @@ pub struct Tool {
pub hover: Option<(Vec2, Option<String>)>,
/// Picked entity ids.
pub picks: Vec<String>,
/// Inline dimension boxes for the shape being drawn, the point count they were made for,
/// and whether the first box should take the keyboard.
pub dims: Vec<crate::sketch_dims::DimBox>,
pub dims_stage: usize,
pub dims_focus: bool,
}
impl Tool {
@@ -61,7 +66,7 @@ impl Tool {
"sketch.construction" => ("sketch.construction", Kind::Pick(1)),
_ => return None,
};
Some(Tool { cmd, kind, pts: Vec::new(), hover: None, picks: Vec::new() })
Some(Tool { cmd, kind, pts: Vec::new(), hover: None, picks: Vec::new(), dims: Vec::new(), dims_stage: usize::MAX, dims_focus: false })
}
}
@@ -102,24 +107,15 @@ 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 };
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"));
}
}
Kind::Draw(_) => {
if let Some(mut p) = sketch_point_at(app, proj, pos) {
// Typed values win over the cursor.
if tool.dims.iter().any(|b| b.locked) {
p = (crate::sketch_dims::effective(app, &tool, p.0), None);
}
} else if n > 0 && tool.pts.len() >= n {
run_shape(app, &tool);
tool.pts.clear();
app.tool = Some(tool);
place(app, p);
return;
}
}
Kind::Pick(n) => {
@@ -168,9 +164,47 @@ pub fn on_click(app: &mut SolveApp, proj: &Proj, pos: Pos2) {
app.tool = Some(tool);
}
fn run_shape(app: &mut SolveApp, t: &Tool) {
/// Put down the next point of the shape being drawn; the last point runs the command (with
/// the typed values as dimension constraints).
pub fn place(app: &mut SolveApp, p: (Vec2, Option<String>)) {
let Some(mut tool) = app.tool.take() else { return };
let Kind::Draw(n) = tool.kind else {
app.tool = Some(tool);
return;
};
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})) {
let curves: Vec<String> = v["curves"].as_array().into_iter().flatten().filter_map(|c| c.as_str().map(str::to_string)).collect();
dimension(app, &tool, &curves);
// Chain: the next segment starts at this one's end point.
if let Some(id) = curves.first() {
let last = tool.pts.len() - 1;
tool.pts[last].1 = Some(format!("{id}.end"));
}
}
}
} else if n > 0 && tool.pts.len() >= n {
let curves = run_shape(app, &tool);
dimension(app, &tool, &curves);
tool.pts.clear();
}
app.tool = Some(tool);
}
/// Typed values become dimension constraints on the new curves.
fn dimension(app: &mut SolveApp, t: &Tool, curves: &[String]) {
for (ents, ty, value) in crate::sketch_dims::constraints(t, curves) {
let _ = app.run("SketchDimension", crate::sketch_dims::dimension_params(&ents, ty, &value));
}
}
fn run_shape(app: &mut SolveApp, t: &Tool) -> Vec<String> {
let p = &t.pts;
let d = |a: usize, b: usize| p[a].0.dist(p[b].0);
let sides = crate::sketch_dims::locked_value(app, t, "Sides").map_or(6, |v| v.round().clamp(3.0, 200.0) as usize);
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])}),
@@ -182,18 +216,21 @@ fn run_shape(app: &mut SolveApp, t: &Tool) {
"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})
json!({"center": xy(&p[0]), "radius": d(0, 1), "sides": sides, "angle": a})
}
"ShapePolygonEdge" => json!({"p0": xy(&p[0]), "p1": xy(&p[1]), "sides": 6}),
"ShapePolygonEdge" => json!({"p0": xy(&p[0]), "p1": xy(&p[1]), "sides": sides}),
"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,
_ => return Vec::new(),
};
let _ = app.run(t.cmd, params);
app.run(t.cmd, params)
.ok()
.map(|v| v["curves"].as_array().into_iter().flatten().filter_map(|c| c.as_str().map(str::to_string)).collect())
.unwrap_or_default()
}
fn run_pick(app: &mut SolveApp, t: &Tool) {
@@ -242,6 +279,7 @@ 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);
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)) {