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 <35337584+0xlillith@users.noreply.github.com>
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

@@ -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;