diff --git a/crates/sketch/src/dims.rs b/crates/sketch/src/dims.rs index 91b73bcd..9d9d9eca 100644 --- a/crates/sketch/src/dims.rs +++ b/crates/sketch/src/dims.rs @@ -178,10 +178,51 @@ fn arc_pts(c: Vec2, r: f64, a0: f64, sweep: f64) -> Vec { (0..=n).map(|i| c + Vec2::from_angle(a0 + sweep * i as f64 / n as f64) * r).collect() } +/// The lines a linear dimension's two extension lines may start from (#1361), for its `p0` and +/// `p1` sides: a dimension to a line reaches the nearest part of that line, not the foot of the +/// measured point, and a dimension between two parallel lines reaches the nearest part of the +/// line whose end it measures from. `None` for a side that measures a point. +pub fn dim_reach(sk: &Sketch, k: &ConstraintKind) -> [Option<(Vec2, Vec2)>; 2] { + use ConstraintKind::*; + let (p, l, to_line) = match *k { + PointLineDistance { p, l, .. } => (p, l, true), + LinearDiameter { p, l, .. } => (p, l, false), + _ => return [None, None], + }; + let Some((a, b)) = line_ends(sk, l) else { return [None, None] }; + let d = b - a; + // A line ending at `p` that runs parallel to `l`. + let parallel = sk.curves.iter().enumerate().find_map(|(i, c)| match c.kind { + CurveKind::Line { a: pa, b: pb } if i != l && (pa == p || pb == p) => { + let (u, v) = line_ends(sk, i)?; + let e = v - u; + (e.len() > 1e-12 && d.cross(e).abs() <= 1e-9 * d.len() * e.len()).then_some((u, v)) + } + _ => None, + }); + [to_line.then_some((a, b)), parallel] +} + +/// The point of segment `s` nearest `q`. +fn nearest_on(s: (Vec2, Vec2), q: Vec2) -> Vec2 { + let d = s.1 - s.0; + let l2 = d.dot(d); + if !(l2 > 1e-24) { + return s.0; + } + s.0 + d * ((q - s.0).dot(d) / l2).clamp(0.0, 1.0) +} + /// Lay a dimension out. `text` is the stored text position (None: the default place), `px` /// the sketch length of one screen pixel and `half` the half size of the text box (sketch /// units), which the dimension line leaves a gap for. pub fn dim_layout(f: &DimFrame, text: Option, px: f64, half: Vec2) -> DimLayout { + dim_layout_reaching(f, text, px, half, [None, None]) +} + +/// [`dim_layout`] with the extension lines of a linear dimension starting from the nearest +/// point of the geometry in `reach` ([`dim_reach`]) instead of the measured points. +pub fn dim_layout_reaching(f: &DimFrame, text: Option, px: f64, half: Vec2, reach: [Option<(Vec2, Vec2)>; 2]) -> DimLayout { let px = if px.is_finite() && px > 0.0 { px } else { 1.0 }; let arrow = 9.0 * px; let gap = 3.0 * px; @@ -196,7 +237,8 @@ pub fn dim_layout(f: &DimFrame, text: Option, px: f64, half: Vec2) -> DimL // The dimension line sits `h` across from the middle of the measured points. let q0 = p0 + n * (h - (p0 - m).dot(n)); let q1 = p1 + n * (h - (p1 - m).dot(n)); - for (p, q) in [(p0, q0), (p1, q1)] { + for ((p, q), r) in [(p0, q0), (p1, q1)].into_iter().zip(reach) { + let p = r.map_or(p, |r| nearest_on(r, q)); let v = q - p; if let Some(u) = v.normalized() { out.lines.push(vec![p + u * gap.min(v.len()), q + u * over]); @@ -375,4 +417,35 @@ mod tests { assert_eq!(l.arrows.len(), 2); assert!(l.text.x > 0.0 && l.text.y > 0.0); } + + #[test] + fn extension_lines_reach_the_nearest_part_of_the_lines_measured() { + // #1361: two parallel lines dimensioned from the low end of one; the dimension placed + // across their middles needs no extension lines, one placed above them reaches their + // top ends, not the low end. + let mut sk = Sketch::new(); + let l = sk.add_line(Vec2::new(0.0, -100.0), Vec2::new(0.0, 100.0), None, None, None).unwrap_or(0); + let m = sk.add_line(Vec2::new(30.0, -80.0), Vec2::new(30.0, 90.0), None, None, None).unwrap_or(0); + let p = sk.curves[m].kind.point_ids()[0]; + let k = ConstraintKind::PointLineDistance { p, l, value: 30.0 }; + let f = dim_frame(&sk, &k).unwrap_or(DimFrame::Radial { center: Vec2::ZERO, radius: 1.0, diameter: false }); + let reach = dim_reach(&sk, &k); + assert!(reach[0].is_some() && reach[1].is_some()); + let extension_ys = |at: Vec2| -> Vec<(f64, f64)> { + let lay = dim_layout_reaching(&f, Some(encode_text(&f, at)), 0.1, Vec2::new(2.0, 1.0), reach); + lay.lines.iter().filter(|line| line.len() == 2 && (line[0].x - line[1].x).abs() < 1e-9).map(|line| (line[0].y, line[1].y)).collect() + }; + assert!(extension_ys(Vec2::new(15.0, 50.0)).is_empty(), "{:?}", extension_ys(Vec2::new(15.0, 50.0))); + let above = extension_ys(Vec2::new(15.0, 120.0)); + assert_eq!(above.len(), 2); + assert!((above[0].0 - 100.3).abs() < 1e-9 && (above[1].0 - 90.3).abs() < 1e-9, "{above:?}"); + // Without the lines' reach the extension lines start at the measured point's height. + let lay = dim_layout(&f, Some(encode_text(&f, Vec2::new(15.0, 120.0))), 0.1, Vec2::new(2.0, 1.0)); + assert!(lay.lines.iter().any(|line| (line[0].y + 79.7).abs() < 1e-9)); + // A point that is not the end of a parallel line keeps its own extension line. + let mut sk = Sketch::new(); + let l = sk.add_line(Vec2::new(0.0, -100.0), Vec2::new(0.0, 100.0), None, None, None).unwrap_or(0); + let q = sk.add_point(Vec2::new(30.0, -80.0), None).unwrap_or(0); + assert!(dim_reach(&sk, &ConstraintKind::PointLineDistance { p: q, l, value: 30.0 })[1].is_none()); + } } diff --git a/crates/sketch/src/lib.rs b/crates/sketch/src/lib.rs index 3922472e..60690953 100644 --- a/crates/sketch/src/lib.rs +++ b/crates/sketch/src/lib.rs @@ -43,7 +43,7 @@ pub use curves::{ spline_end_tangent, spline_end_tangent_mode, spline_point, spline_point_mode, spline_polyline, spline_polyline_mode, }; pub use decompose::Budget; -pub use dims::{DimFrame, DimLayout, chain_centres, default_text, dim_frame, dim_layout, encode_text}; +pub use dims::{DimFrame, DimLayout, chain_centres, default_text, dim_frame, dim_layout, dim_layout_reaching, dim_reach, encode_text}; pub use fonts::{DEFAULT_FONT_FAMILY, FontData, families_in, find_style_in, font_data, font_data_styled, installed_families, set_font_file_check}; pub use freeform_offset::{FreeformOffset, OFFSET_TOLERANCE}; pub use link::{Link, LinkGeom, LinkKind, LinkSource, MAX_LINK_ENTITIES, SKETCH_AXIS_HALF, sketch_axis_geom}; diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index bc76b565..6c396c19 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -1818,7 +1818,7 @@ fn triad(app: &mut SolveApp, ui: &mut egui::Ui, m: &mut OccMove) { /// viewport like the sketch being edited draws its own (`crate::dim_view`), but not editable. pub fn dims_overlay(app: &SolveApp, ctx: &egui::Context) { use solidcraft_engine::geom::Vec2; - use solidcraft_engine::sketch::{DimFrame, chain_centres, default_text, dim_frame, dim_layout}; + use solidcraft_engine::sketch::{DimFrame, chain_centres, default_text, dim_frame, dim_layout_reaching, dim_reach}; let Some(rect) = app.viewport.rect else { return }; if app.ui.shown_dims.is_empty() { return; @@ -1856,7 +1856,7 @@ pub fn dims_overlay(app: &SolveApp, ctx: &egui::Context) { ); let size = galley.size() + vec2(6.0, 2.0); let half = Vec2::new(f64::from(size.x) * 0.5 * px, f64::from(size.y) * 0.5 * px); - let lay = dim_layout(&frame, c.text.or_else(|| default_text(&frame, middle, px)), px, half); + let lay = dim_layout_reaching(&frame, c.text.or_else(|| default_text(&frame, middle, px)), px, half, dim_reach(sk, &c.kind)); for line in &lay.lines { painter.add(egui::Shape::line(line.iter().filter_map(|q| to(*q)).collect(), Stroke::new(1.0, col))); } diff --git a/crates/ui-egui/src/dim_view.rs b/crates/ui-egui/src/dim_view.rs index a596b081..11bcf39b 100644 --- a/crates/ui-egui/src/dim_view.rs +++ b/crates/ui-egui/src/dim_view.rs @@ -16,7 +16,7 @@ use egui::{FontId, Pos2, Rect, Shape, Stroke, vec2}; use serde_json::json; use solidcraft_engine::Sel; use solidcraft_engine::geom::Vec2; -use solidcraft_engine::sketch::{ConstraintKind, DimFrame, chain_centres, default_text, dim_frame, dim_layout}; +use solidcraft_engine::sketch::{ConstraintKind, DimFrame, chain_centres, default_text, dim_frame, dim_layout_reaching, dim_reach}; use crate::SolveApp; use crate::theme::Tokens; @@ -516,7 +516,7 @@ pub fn show(app: &mut SolveApp, ui: &egui::Ui, painter: &egui::Painter, proj: &P let half = Vec2::new(f64::from(size.x) * 0.5 * px, f64::from(size.y) * 0.5 * px); // Unplaced linear dimensions sit outside the shape they measure. let away = first_point(sk, &c.kind).and_then(|p| centres.get(p).copied()).unwrap_or(anchor); - let lay = dim_layout(&frame, c.text.or_else(|| default_text(&frame, away, px)), px, half); + let lay = dim_layout_reaching(&frame, c.text.or_else(|| default_text(&frame, away, px)), px, half, dim_reach(sk, &c.kind)); let stroke = Stroke::new(1.0, col); let mut segs = Vec::new(); for line in &lay.lines {