Ignore repeated initial sketch points while drawing

This commit is contained in:
lillith authored and bherbruck committed 2026-10-12 09:30:18 -04:00
1 parent f0292b1606
commit 8c13914d6c
3 files changed
+58 -1

No files matched your search

+3
View File
@@ -63,6 +63,9 @@ headline counts in-scope tabs only and lists the deferred ones separately.
## Dimensioning and error reporting
- Repeating the initial point of a planar multi-point sketch drawing tool is neutral: the tool
waits for a distinct point without a degenerate-shape error or history entry. Typed input
and programmatic commands retain their validation.
- Single-line and rectangle-side dimensioning supports selecting a line or its two endpoints.
Type a value and press Enter to commit one undo step; rejected values remain editable and
cancellation leaves the sketch unchanged. Fit keeps geometry clear of the Sketch Palette
@@ -0,0 +1,44 @@
use crate::scenario::Harness;
use serde_json::json;
#[test]
fn repeated_first_point_is_neutral_and_drawing_can_continue() {
for (command, rest) in [
("sketch.rectangle.two_point", vec![[10, 5, 0]]),
("sketch.rectangle.center", vec![[10, 5, 0]]),
("sketch.rectangle.three_point", vec![[10, 0, 0], [10, 5, 0]]),
("sketch.circle.center", vec![[5, 0, 0]]),
("sketch.circle.three_point", vec![[10, 0, 0], [5, 5, 0]]),
("sketch.line", vec![[10, 5, 0]]),
("sketch.polygon.inscribed", vec![[10, 5, 0]]),
("sketch.slot.center_to_center", vec![[10, 0, 0], [10, 3, 0]]),
] {
let mut h = Harness::new();
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
h.app.cam.distance = 50.0;
h.step(&json!({"start":command})).unwrap();
let before = h.app.session.doc.to_json();
let undo = h.app.session.undo.len();
let p = h.call("ui.at", json!({"sketch":[0,0]}))["result"].clone();
h.call("ui.click", json!({"x":p[0],"y":p[1],"double":true}));
assert!(!h.app.status.as_ref().is_some_and(|s| s.2), "{command}: {:?}", h.app.status);
assert_eq!(h.app.tool.as_ref().unwrap().pts.len(), 1, "{command}");
assert_eq!(h.app.session.doc.to_json(), before);
assert_eq!(h.app.session.undo.len(), undo);
for point in rest {
h.step(&json!({"move":{"world":point}})).unwrap();
if command == "sketch.slot.center_to_center" {
// The live width editor is placed at the cursor; Enter accepts it.
h.step(&json!({"key":"Enter"})).unwrap();
} else {
h.step(&json!({"click":{"world":point}})).unwrap();
}
}
assert!(!h.app.status.as_ref().is_some_and(|s| s.2), "{command}: {:?}", h.app.status);
let state = h.app.session.world_state();
assert!(!state.sketches[0].sketch.curves.is_empty(), "{command}: {:?}; {:?}", h.app.tool, h.app.status);
assert_eq!(h.app.session.undo.len(), undo + 1, "{command}");
h.app.run("edit.undo", json!({})).unwrap();
assert_eq!(h.app.session.doc.to_json(), before, "{command}");
}
}
+11 -1
View File
@@ -12,6 +12,10 @@ use crate::viewport::{Hit, Proj, pick, sketch_point_at};
#[path = "sketch_display_preview_tests.rs"]
mod display_preview_tests;
#[cfg(test)]
#[path = "sketch_neutral_click_tests.rs"]
mod neutral_click_tests;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
/// Click points; `n` points complete the shape (0 = chain until finished).
@@ -144,12 +148,18 @@ pub fn on_click(app: &mut SolveApp, proj: &Proj, pos: Pos2) {
return;
}
match tool.kind {
Kind::Draw(_) => {
Kind::Draw(n) => {
if let Some(mut p) = sketch_point_at(app, proj, pos) {
// Typed values win over the cursor; lines snap to horizontal/vertical.
if tool.dims.iter().any(|b| b.locked) || (tool.cmd == "sketch.line" && p.1.is_none()) {
p = (crate::sketch_dims::effective(app, &tool, p.0), None);
}
// A repeated initial point supplies no extent. Keep waiting rather than
// attempting a degenerate shape; explicit typed values still validate.
if n != 1 && tool.pts.len() == 1 && tool.pts.first().is_some_and(|first| first.0 == p.0) && !tool.dims.iter().any(|b| b.locked) {
app.tool = Some(tool);
return;
}
app.tool = Some(tool);
place(app, p);
return;