fix(ui): preview actual polygon outlines while drawing (#583)
Inscribed, circumscribed and edge polygon tools preview their actual outline (side count and orientation) while drawing, with the current line type, instead of only the support circle. Preview work is bounded. Fixes #520. Co-authored-by: lillith <[email protected]>
This commit is contained in:
1 parent
dc060f8b86
commit
5ad8b3966f
5 files changed
+102
-1
No files matched your search
@@ -61,6 +61,8 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
|
||||
|
||||
## Dimensioning and error reporting
|
||||
|
||||
- Inscribed, circumscribed and edge polygon tools preview their actual outline before
|
||||
placement, including orientation and the selected side count.
|
||||
- Construction (X) preserves the active sketch drawing tool, picked anchors and inline
|
||||
dimension values, so line type can change while a shape is in progress.
|
||||
- Planar sketch construction previews and hover feedback keep the same dashed pattern as
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{Tool, preview_curve};
|
||||
use super::{Tool, polygon_outline, preview_curve};
|
||||
use crate::scenario::Harness;
|
||||
use crate::viewport::Proj;
|
||||
use serde_json::{Value, json};
|
||||
@@ -120,3 +120,33 @@ fn pixel_preview_rejects_degenerate_and_unbounded_input_without_changing_documen
|
||||
let points = vec![[0., 0.]; 500];
|
||||
assert!(preview_curve(&tool("sketch.spline.fit_point", &points), Vec2::X, None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polygon_preview_squares_have_the_expected_rotation_and_size() {
|
||||
let cases = [
|
||||
("sketch.polygon.inscribed", [10.0, 0.0], [[10.0, 0.0], [0.0, 10.0], [-10.0, 0.0], [0.0, -10.0], [10.0, 0.0]]),
|
||||
("sketch.polygon.circumscribed", [10.0, 0.0], [[10.0, 10.0], [-10.0, 10.0], [-10.0, -10.0], [10.0, -10.0], [10.0, 10.0]]),
|
||||
("sketch.polygon.edge", [20.0, 0.0], [[0.0, 0.0], [20.0, 0.0], [20.0, 20.0], [0.0, 20.0], [0.0, 0.0]]),
|
||||
];
|
||||
for (command, hover, expected) in cases {
|
||||
let t = tool(command, &[[0.0, 0.0]]);
|
||||
let outline = polygon_outline(&t, Vec2::new(hover[0], hover[1]), 4.0).unwrap();
|
||||
assert_eq!(outline.len(), expected.len());
|
||||
for (actual, expected) in outline.iter().zip(expected) {
|
||||
assert!(actual.dist(Vec2::new(expected[0], expected[1])) < 1e-9, "{command}: {actual:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polygon_preview_rejects_invalid_or_unbounded_input() {
|
||||
let t = tool("sketch.polygon.inscribed", &[[0.0, 0.0]]);
|
||||
for sides in [0.0, 2.0, 3.5, 201.0, f64::INFINITY, f64::NAN] {
|
||||
assert!(polygon_outline(&t, Vec2::X, sides).is_none());
|
||||
}
|
||||
assert!(polygon_outline(&t, Vec2::ZERO, 6.0).is_none());
|
||||
assert!(polygon_outline(&t, Vec2::new(f64::NAN, 0.0), 6.0).is_none());
|
||||
assert_eq!(polygon_outline(&t, Vec2::X, 200.0).unwrap().len(), 201);
|
||||
let t = tool("sketch.polygon.inscribed", &[[f64::INFINITY, 0.0]]);
|
||||
assert!(polygon_outline(&t, Vec2::X, 6.0).is_none());
|
||||
}
|
||||
@@ -580,6 +580,37 @@ pub(crate) fn preview_curve(t: &Tool, hover: Vec2, sweep: Option<f64>) -> Option
|
||||
Some(sk)
|
||||
}
|
||||
|
||||
/// The polygon's actual straight edges, in the same orientation as the eventual command.
|
||||
/// Keep preview work bounded even while a side-count expression is being edited.
|
||||
pub(crate) fn polygon_outline(t: &Tool, hover: Vec2, sides: f64) -> Option<Vec<Vec2>> {
|
||||
if !hover.is_finite() || !(3.0..=200.0).contains(&sides) || sides.fract() != 0.0 || t.pts.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
let a = t.pts.first()?.0;
|
||||
let n = sides as usize;
|
||||
let half = std::f64::consts::PI / sides;
|
||||
let distance = a.dist(hover);
|
||||
if !a.is_finite() || !distance.is_finite() || distance <= 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let (centre, radius, start) = match t.cmd {
|
||||
"sketch.polygon.inscribed" => (a, distance, (hover - a).angle()),
|
||||
"sketch.polygon.circumscribed" => (a, distance / half.cos(), (hover - a).angle() + half),
|
||||
"sketch.polygon.edge" => {
|
||||
let radius = distance / (2.0 * half.sin());
|
||||
let centre = (a + hover) * 0.5 + (hover - a).normalized()?.perp() * (radius * half.cos());
|
||||
(centre, radius, (a - centre).angle())
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let mut corners: Vec<_> = (0..n).map(|i| centre + Vec2::from_angle(start + std::f64::consts::TAU * i as f64 / sides) * radius).collect();
|
||||
if corners.iter().any(|p| !p.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
corners.push(*corners.first()?);
|
||||
Some(corners)
|
||||
}
|
||||
|
||||
pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) {
|
||||
if crate::sketch3d::preview(app, t, painter, proj) {
|
||||
return;
|
||||
@@ -607,6 +638,15 @@ 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(corners) = polygon_outline(t, h, crate::sketch_dims::locked_value(app, t, "Sides").unwrap_or(6.0)) {
|
||||
let outline: Vec<_> = corners.iter().map(|c| ss.plane.to_world(*c)).collect();
|
||||
paint_sketch_path(&outline, crate::sketch_palette::mode(app), painter, proj, stroke);
|
||||
crate::scenario::publish_count("polygon_preview_edges", (corners.len() - 1) as f64);
|
||||
if t.cmd == "sketch.polygon.edge" {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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, crate::sketch_palette::mode(app));
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[
|
||||
{"note":"#520: all polygon drawing variants show their real edge outline before committing, including a typed side count."},
|
||||
{"call":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}},
|
||||
{"call":"ui.view","params":{"view":"top"}},
|
||||
{"start":"sketch.polygon.inscribed"},
|
||||
{"click":{"sketch":[0,0]}},
|
||||
{"move":{"sketch":[20,8]}},
|
||||
{"expect":{"count":{"name":"polygon_preview_edges","value":6},"errors":0}},
|
||||
{"text":"10"},
|
||||
{"key":"Tab"},
|
||||
{"text":"4"},
|
||||
{"move":{"sketch":[20,10]}},
|
||||
{"expect":{"count":{"name":"polygon_preview_edges","value":4},"errors":0}},
|
||||
{"key":"Escape"},
|
||||
{"start":"sketch.polygon.circumscribed"},
|
||||
{"click":{"sketch":[0,0]}},
|
||||
{"move":{"sketch":[20,8]}},
|
||||
{"expect":{"count":{"name":"polygon_preview_edges","value":6},"errors":0}},
|
||||
{"key":"Escape"},
|
||||
{"start":"sketch.polygon.edge"},
|
||||
{"click":{"sketch":[0,0]}},
|
||||
{"move":{"sketch":[20,8]}},
|
||||
{"expect":{"count":{"name":"polygon_preview_edges","value":6},"errors":0}},
|
||||
{"key":"Escape"}
|
||||
]
|
||||
@@ -31,6 +31,10 @@ fit-point and control-point splines, conics, points and text. **Construction** (
|
||||
into reference geometry that doesn't form profiles. Modify tools include trim (T), extend, break,
|
||||
fillet, chamfer, offset (O), mirror, patterns, move/copy and scale.
|
||||
|
||||
Polygon tools show their edge outline while positioning the second point, so the orientation
|
||||
can be checked before placement. Inscribed and circumscribed polygons retain their support
|
||||
circle and update the outline when the inline **Sides** value changes.
|
||||
|
||||
**Text** is set in the built-in font (Liberation Sans) unless it is given another: `sketch.text` and
|
||||
`sketch.edit_text` take a `font`, the family or full name of an installed font ("DejaVu Sans") or the path
|
||||
of a `.ttf` / `.otf` file, and the **Edit Text** dialog has a **Font** field for it. An unknown font is an
|
||||
|
||||
Reference in new issue
Block a user