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:
selune authored and GitHub committed 2026-10-13 04:26:00 -04:00
1 parent dc060f8b86
commit 5ad8b3966f
5 files changed
+102 -1

No files matched your search

+2
View File
@@ -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());
}
+40
View File
@@ -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"}
]
+4
View File
@@ -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