mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 03:37:00 -06:00
refactor(tools): move guides.rs items to guides/{snap,targets}.rs
This commit is contained in:
+12
-1547
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,455 @@
|
||||
use super::targets::{Targets, span};
|
||||
use super::*;
|
||||
|
||||
/// Where curves `a` and `b` cross near `p`: a point projected from one onto the other and back
|
||||
/// settles where they meet (none when they don't nearby).
|
||||
pub(super) fn meeting(a: &PathSeg, b: &PathSeg, p: Point) -> Option<Point> {
|
||||
let mut q = p;
|
||||
for _ in 0..32 {
|
||||
let on_a = a.eval(a.nearest(q, 1e-9).t);
|
||||
q = b.eval(b.nearest(on_a, 1e-9).t);
|
||||
if on_a.distance(q) <= 1e-6 {
|
||||
return Some(q);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The direction of `seg` at `t` (not normalised; zero in a cusp).
|
||||
pub(super) fn seg_direction(seg: &PathSeg, t: f64) -> Vec2 {
|
||||
match seg {
|
||||
PathSeg::Line(l) => l.p1 - l.p0,
|
||||
PathSeg::Quad(q) => q.deriv().eval(t).to_vec2(),
|
||||
PathSeg::Cubic(c) => c.deriv().eval(t).to_vec2(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The parameters in (0, 1) where the line from `at` to `seg` is square to it (`perpendicular`)
|
||||
/// or grazes it, found by sampling the curve for a change of sign and bisecting.
|
||||
pub(super) fn touch_params(seg: &PathSeg, at: Point, perpendicular: bool) -> Vec<f64> {
|
||||
const STEPS: usize = 24;
|
||||
let f = |t: f64| {
|
||||
let (v, d) = (seg.eval(t) - at, seg_direction(seg, t));
|
||||
if perpendicular { v.dot(d) } else { v.cross(d) }
|
||||
};
|
||||
let mut out = vec![];
|
||||
let mut prev = (0.0, f(0.0));
|
||||
for i in 1..=STEPS {
|
||||
let t = i as f64 / STEPS as f64;
|
||||
let cur = (t, f(t));
|
||||
if prev.1.is_finite() && cur.1.is_finite() && prev.1 * cur.1 < 0.0 {
|
||||
let (mut lo, mut hi) = (prev, cur);
|
||||
for _ in 0..40 {
|
||||
let mid = 0.5 * (lo.0 + hi.0);
|
||||
let m = (mid, f(mid));
|
||||
if lo.1 * m.1 <= 0.0 {
|
||||
hi = m;
|
||||
} else {
|
||||
lo = m;
|
||||
}
|
||||
}
|
||||
let t = 0.5 * (lo.0 + hi.0);
|
||||
// A jump through a cusp changes sign without crossing zero: the line must really
|
||||
// run along (or square to) the curve there.
|
||||
let (v, d) = (seg.eval(t) - at, seg_direction(seg, t));
|
||||
let scale = v.hypot() * d.hypot();
|
||||
if scale > 1e-12 && f(t).abs() <= 1e-3 * scale {
|
||||
out.push(t);
|
||||
}
|
||||
}
|
||||
prev = cur;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Where a press at `p` takes hold of `path` by its outline: the point of the path nearest `p`,
|
||||
/// when it is within `reach`, and on a straight segment exactly on its line. A path dragged by a
|
||||
/// press on its stroke or beside its edge snaps by that point of the path, so it is the path that
|
||||
/// lands on a ruler guide or lines up, not the pressed spot a few pixels beside it (#3701).
|
||||
pub(crate) fn hold_on_path(path: &vectorcraft_geom::PathData, p: Point, reach: f64) -> Option<Point> {
|
||||
let (si, seg, _, q, d) = path.nearest(p)?;
|
||||
if d > reach {
|
||||
return None;
|
||||
}
|
||||
let sp = path.subpaths.get(si)?;
|
||||
if !sp.segment_is_line(seg) {
|
||||
return Some(q);
|
||||
}
|
||||
let c = sp.segment(seg);
|
||||
let line = Line::new(c.p0, c.p3);
|
||||
Some(line.eval(line.nearest(p, 0.0).t))
|
||||
}
|
||||
|
||||
/// Snap `p` for a drawing tool when smart guides (or grid snapping) are on.
|
||||
pub fn snap_draw(cx: &ToolContext, p: Point, exclude: &[NodeId]) -> (Point, Vec<Overlay>) {
|
||||
snap_with(cx, p, || Targets::collect(cx.doc, exclude, None))
|
||||
}
|
||||
|
||||
/// Snap `p` to pixels or the grid when they are on, else to the smart guide `targets`, else (Snap
|
||||
/// to Point) to their anchors and the ruler guides within the Snap to Point distance.
|
||||
fn snap_with(cx: &ToolContext, p: Point, targets: impl FnOnce() -> Targets) -> (Point, Vec<Overlay>) {
|
||||
PointSnap::new(cx, targets).snap(cx, p)
|
||||
}
|
||||
|
||||
/// Snapping for a dragged point, its targets gathered once (when the drag begins): what
|
||||
/// [`snap_draw`] does for a point at a time.
|
||||
pub struct PointSnap {
|
||||
/// The smart guide targets, or with Smart Guides off those of Snap to Point, and how near (in
|
||||
/// screen pixels) they pull; none with pixels or the grid snapping instead, or nothing on.
|
||||
targets: Option<(Targets, f64)>,
|
||||
}
|
||||
|
||||
impl PointSnap {
|
||||
pub fn new(cx: &ToolContext, targets: impl FnOnce() -> Targets) -> Self {
|
||||
let targets = if cx.snap_to_pixel || cx.snap_to_grid {
|
||||
None
|
||||
} else if cx.smart_guides {
|
||||
// Snap to Point keeps pulling points onto the ruler guides with Smart Guides on (#812).
|
||||
let mut t = targets().styled(cx);
|
||||
(t.tangent, t.perpendicular) = (cx.snap_tangent, cx.snap_perpendicular);
|
||||
Some((if cx.snap_to_point { t.with_rulers(cx) } else { t }, cx.snapping_tolerance))
|
||||
} else if cx.snap_to_point {
|
||||
Some((targets().for_snap_to_point(cx), cx.snap_tolerance))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self { targets }
|
||||
}
|
||||
|
||||
/// Where `p` goes, and the guides showing why.
|
||||
pub fn snap(&self, cx: &ToolContext, p: Point) -> (Point, Vec<Overlay>) {
|
||||
self.snap_from(cx, p, None)
|
||||
}
|
||||
|
||||
/// Where a drawn point `p` leaving `from` goes ([`Targets::snap_from`]), and the guides
|
||||
/// showing why. Pixels and the grid round it before Shift turns it onto its angle.
|
||||
pub fn snap_from(&self, cx: &ToolContext, p: Point, from: Option<&Leave>) -> (Point, Vec<Overlay>) {
|
||||
let keep = |q: Point| from.and_then(|f| f.constrained(q)).unwrap_or(q);
|
||||
if cx.snap_to_pixel {
|
||||
return (keep(Point::new(p.x.round(), p.y.round())), vec![]);
|
||||
}
|
||||
if cx.snap_to_grid {
|
||||
return (keep(vectorcraft_geom::snap::snap_point_to_grid(p, cx.grid_step())), vec![]);
|
||||
}
|
||||
match &self.targets {
|
||||
Some((t, tol)) => t.snap_from(p, from, cx.tol(*tol)),
|
||||
None => (keep(p), vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Snap a bounding-box resize ([`Targets::snap_scale`]) as [`Self::snap`] snaps a point. With
|
||||
/// Snap to Pixel on, the corner or side the handle moves lines up with whole pixels, and with
|
||||
/// Snap to Grid on it lines up with the nearest gridline, however far that is; neither shows
|
||||
/// guides. Otherwise the handle snaps to the targets [`Self::new`] gathered.
|
||||
pub fn snap_scale(
|
||||
&self,
|
||||
cx: &ToolContext,
|
||||
bx: &OrientedBox,
|
||||
handle: Handle,
|
||||
a: Affine,
|
||||
proportional: bool,
|
||||
from_center: bool,
|
||||
) -> (Affine, Vec<Overlay>) {
|
||||
let step = if cx.snap_to_pixel {
|
||||
Some(1.0)
|
||||
} else if cx.snap_to_grid {
|
||||
Some(cx.grid_step()).filter(|s| s.is_finite() && *s > 0.0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(step) = step {
|
||||
let moved = Handle::ALL.map(|h| bx.to_doc() * (a * h.pos(bx.rect)));
|
||||
// Every point the handle moves is within half a step of a gridline, so a tolerance of
|
||||
// one step always finds one.
|
||||
return (Targets::grid_lines(moved, step).snap_scale(bx, handle, a, proportional, from_center, step).0, vec![]);
|
||||
}
|
||||
match &self.targets {
|
||||
Some((t, tol)) => t.snap_scale(bx, handle, a, proportional, from_center, cx.tol(*tol)),
|
||||
None => (a, vec![]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The Construction Guides angle set new preferences start with.
|
||||
pub const DEFAULT_CONSTRUCTION_ANGLES: &str = "90° & 45° Angles";
|
||||
|
||||
/// The angles (degrees) of the construction guides of a Smart Guides › Angles set: "90°" gives the
|
||||
/// horizontal and the vertical, "45°" the diagonals, "60°" and "30°" their multiples between those.
|
||||
pub fn construction_angles(set: &str) -> &'static [f64] {
|
||||
match set {
|
||||
"30° Angles" => &[30.0, 60.0, 120.0, 150.0],
|
||||
"45° Angles" => &[45.0, 135.0],
|
||||
"60° Angles" => &[60.0, 120.0],
|
||||
"90° Angles" => &[0.0, 90.0],
|
||||
"90° & 45° & 30° Angles" => &[0.0, 30.0, 45.0, 60.0, 90.0, 120.0, 135.0, 150.0],
|
||||
_ => &[0.0, 45.0, 90.0, 135.0],
|
||||
}
|
||||
}
|
||||
|
||||
/// How Shift keeps a drawn point to the point it leaves from.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Keep {
|
||||
/// At steps of `step` degrees counted from `base` (a segment).
|
||||
Angle { step: f64, base: f64 },
|
||||
/// On a diagonal, as far along as the pointer's farther axis (a square's corner).
|
||||
Square,
|
||||
}
|
||||
|
||||
/// The point a drawn point leaves from (the anchor before it, a shape's start) and how it keeps
|
||||
/// to it: with Shift held at fixed angles round it, and onto the construction guides through it
|
||||
/// (Smart Guides › Construction Guides).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Leave {
|
||||
pub at: Point,
|
||||
/// Shift held: how the point keeps to `at`.
|
||||
shift: Option<Keep>,
|
||||
/// The angles of the construction guides through `at` (none with them off).
|
||||
angles: &'static [f64],
|
||||
/// A drawn segment: it may land square to a segment or grazing a curve (a box does not).
|
||||
pub(super) contact: bool,
|
||||
}
|
||||
|
||||
impl Leave {
|
||||
/// A segment from `at` (Pen, Curvature, Line Segment): Shift keeps it at 45° steps from
|
||||
/// General › Constrain Angle; with Smart Guides on it lands on the construction guides.
|
||||
pub fn segment(cx: &ToolContext, at: Point, shift: bool) -> Self {
|
||||
Self {
|
||||
at,
|
||||
shift: shift.then_some(Keep::Angle { step: 45.0, base: cx.constrain_angle }),
|
||||
angles: if cx.smart_guides { cx.construction_angles } else { &[] },
|
||||
contact: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// A curved source cannot make a line-to-line perpendicular or line-to-curve tangent contact.
|
||||
pub fn without_contact(mut self) -> Self {
|
||||
self.contact = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// A box dragged from its corner or centre `at` (Rectangle, Ellipse, Arc, the grids, and
|
||||
/// Artboard, which passes no Shift): Shift keeps the dragged corner on a diagonal, the box a
|
||||
/// square.
|
||||
pub fn diagonal(at: Point, shift: bool) -> Self {
|
||||
Self { at, shift: shift.then_some(Keep::Square), angles: &[], contact: false }
|
||||
}
|
||||
|
||||
/// Where Shift puts `p`: none without it.
|
||||
pub(super) fn constrained(&self, p: Point) -> Option<Point> {
|
||||
let v = p - self.at;
|
||||
Some(match self.shift? {
|
||||
Keep::Angle { step, base } => self.at + constrain_angle_from(v, step, base),
|
||||
Keep::Square => self.at + square(v),
|
||||
})
|
||||
}
|
||||
|
||||
/// The construction guide through `at` nearest `p`, within `tol` of it (and `p` farther than
|
||||
/// that from `at`, where every guide meets): its direction and where `p` lands on it.
|
||||
pub(super) fn construction(&self, p: Point, tol: f64) -> Option<(Vec2, Point)> {
|
||||
let v = p - self.at;
|
||||
if v.hypot() <= tol {
|
||||
return None;
|
||||
}
|
||||
self.angles
|
||||
.iter()
|
||||
// Counter-clockwise as seen on the y-down page.
|
||||
.map(|a| Vec2::new(a.to_radians().cos(), -a.to_radians().sin()))
|
||||
.map(|d| (d, self.at + d * v.dot(d)))
|
||||
.map(|(d, q)| (d, q, q.distance(p)))
|
||||
.filter(|(.., dist)| *dist <= tol)
|
||||
.min_by(|a, b| a.2.total_cmp(&b.2))
|
||||
.map(|(d, q, _)| (d, q))
|
||||
}
|
||||
}
|
||||
|
||||
/// `d` made square: as long on both axes as on its longer one, each keeping its sign.
|
||||
pub fn square(d: Vec2) -> Vec2 {
|
||||
let s = d.x.abs().max(d.y.abs());
|
||||
Vec2::new(s * d.x.signum(), s * d.y.signum())
|
||||
}
|
||||
|
||||
/// Smart Guides for the points a drawing tool places: the Pen's and Curvature tool's anchors,
|
||||
/// the Pencil's ends, the shape tools' start and dragged corner, the Type tool's click and the
|
||||
/// Artboard tool's new artboard. Each such tool keeps one and asks it for every point, hovering
|
||||
/// too, so they all snap alike, and shows its [`Self::guides`].
|
||||
///
|
||||
/// The targets ([`Targets::for_drawing`], the art in the window) are gathered once per document
|
||||
/// state: hovering gathers them again only after the document, the selection, what is left out,
|
||||
/// the window or the snapping options changed; a press always gathers them afresh, and the drag
|
||||
/// it starts keeps them, the document then showing the drag's own previews.
|
||||
#[derive(Default)]
|
||||
pub struct DrawSnap {
|
||||
/// The targets, and what they were gathered for (none: for one drag only).
|
||||
targets: Option<(Option<SnapKey>, PointSnap)>,
|
||||
guides: Vec<Overlay>,
|
||||
}
|
||||
|
||||
/// What a [`DrawSnap`]'s targets were gathered for: the document state, the art left out, the
|
||||
/// window and every option [`PointSnap::new`] reads.
|
||||
#[derive(PartialEq)]
|
||||
struct SnapKey {
|
||||
revision: (u64, u64),
|
||||
exclude: Vec<NodeId>,
|
||||
screen: Option<ScreenFrame>,
|
||||
view: [bool; 7],
|
||||
snap_to_glyph: bool,
|
||||
style: GuideStyle,
|
||||
tolerances: [f64; 2],
|
||||
}
|
||||
|
||||
impl SnapKey {
|
||||
fn of(cx: &ToolContext, exclude: &[NodeId]) -> Self {
|
||||
Self {
|
||||
revision: cx.revision,
|
||||
exclude: exclude.to_vec(),
|
||||
screen: cx.screen,
|
||||
view: [cx.smart_guides, cx.snap_to_point, cx.snap_to_pixel, cx.snap_to_grid, cx.guides, cx.snap_tangent, cx.snap_perpendicular],
|
||||
snap_to_glyph: cx.snap_to_glyph && cx.alignment_guides,
|
||||
style: GuideStyle::of(cx),
|
||||
tolerances: [cx.snapping_tolerance, cx.snap_tolerance],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The part of the document the window shows (none headless: all of it).
|
||||
fn visible(cx: &ToolContext) -> Option<Rect> {
|
||||
let s = cx.screen?;
|
||||
let (w, h) = s.size;
|
||||
Some(Rect::from_points(s.at(0.0, 0.0), s.at(w, h)).union_pt(s.at(w, 0.0)).union_pt(s.at(0.0, h)))
|
||||
}
|
||||
|
||||
impl DrawSnap {
|
||||
/// Snap the pointer hovering at `p` (the art but `exclude` as targets, the anchors of
|
||||
/// `exclude` still pulling), reusing the targets while nothing they depend on changed.
|
||||
pub fn hover(&mut self, cx: &ToolContext, p: Point, exclude: &[NodeId], from: Option<&Leave>) -> Point {
|
||||
let key = SnapKey::of(cx, exclude);
|
||||
if self.targets.as_ref().is_none_or(|(k, _)| k.as_ref() != Some(&key)) {
|
||||
self.gather(cx, key);
|
||||
}
|
||||
self.snap(cx, p, from)
|
||||
}
|
||||
|
||||
/// Snap the point pressed at `p`, the targets gathered afresh: the drag that follows keeps
|
||||
/// them ([`Self::drag`]).
|
||||
pub fn press(&mut self, cx: &ToolContext, p: Point, exclude: &[NodeId], from: Option<&Leave>) -> Point {
|
||||
self.gather(cx, SnapKey::of(cx, exclude));
|
||||
self.snap(cx, p, from)
|
||||
}
|
||||
|
||||
/// Gather `targets` other than the art for the drag a press starts (a point being dragged
|
||||
/// leaves itself out); the next hover gathers its own again.
|
||||
pub fn hold(&mut self, cx: &ToolContext, targets: impl FnOnce() -> Targets) {
|
||||
self.targets = Some((None, PointSnap::new(cx, targets)));
|
||||
self.guides.clear();
|
||||
}
|
||||
|
||||
/// Snap `p`, dragged since the press, with the targets the press gathered.
|
||||
pub fn drag(&mut self, cx: &ToolContext, p: Point, from: Option<&Leave>) -> Point {
|
||||
if self.targets.is_none() {
|
||||
self.gather(cx, SnapKey::of(cx, &[]));
|
||||
}
|
||||
self.snap(cx, p, from)
|
||||
}
|
||||
|
||||
/// The guides of the last snap.
|
||||
pub fn guides(&self) -> &[Overlay] {
|
||||
&self.guides
|
||||
}
|
||||
|
||||
/// Hide the guides (the gesture ended).
|
||||
pub fn clear(&mut self) {
|
||||
self.guides.clear();
|
||||
}
|
||||
|
||||
fn gather(&mut self, cx: &ToolContext, key: SnapKey) {
|
||||
let snap = PointSnap::new(cx, || Targets::for_drawing(cx.doc, &key.exclude, visible(cx), key.snap_to_glyph));
|
||||
self.targets = Some((Some(key), snap));
|
||||
}
|
||||
|
||||
fn snap(&mut self, cx: &ToolContext, p: Point, from: Option<&Leave>) -> Point {
|
||||
let (q, guides) = match &self.targets {
|
||||
Some((_, t)) => t.snap_from(cx, p, from),
|
||||
None => (p, vec![]),
|
||||
};
|
||||
self.guides = guides;
|
||||
q
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapping for a dragged direction handle: Shift keeps it at 45° steps round its anchor, else it
|
||||
/// snaps as a drawn point does ([`DrawSnap`]). The targets ([`Targets::for_handle`]) are gathered
|
||||
/// on the first move that needs them and kept for the rest of the drag.
|
||||
#[derive(Default)]
|
||||
pub struct HandleSnap(pub(crate) DrawSnap);
|
||||
|
||||
impl HandleSnap {
|
||||
/// Where the handle of anchor `ai` of subpath `si` of path `id` goes with the pointer at `p`,
|
||||
/// and the guides showing why.
|
||||
pub fn snap(&mut self, cx: &ToolContext, (id, si, ai): (NodeId, usize, usize), p: Point, shift: bool) -> (Point, Vec<Overlay>) {
|
||||
let path = cx.doc.node(id).and_then(|n| n.path_data());
|
||||
if shift && let Some(a) = path.and_then(|pd| pd.subpaths.get(si)?.anchors.get(ai)) {
|
||||
return (a.p + constrain_angle_from(p - a.p, 45.0, cx.constrain_angle), vec![]);
|
||||
}
|
||||
if self.0.targets.is_none() {
|
||||
self.0.hold(cx, || Targets::for_handle(cx.doc, id));
|
||||
}
|
||||
let q = self.0.drag(cx, p, None);
|
||||
(q, self.0.guides().to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
/// Snap a picked point (a transform tool's reference point) to the nearest anchor or centre of the
|
||||
/// selection or of the object under the pointer, when Snap to Point or Smart Guides is on.
|
||||
pub fn snap_pick(cx: &ToolContext, p: Point) -> (Point, Vec<Overlay>) {
|
||||
if !(cx.snap_to_point || cx.smart_guides) {
|
||||
return (p, vec![]);
|
||||
}
|
||||
let tol = if cx.smart_guides { cx.snap_tol() } else { cx.tol(cx.snap_tolerance) };
|
||||
let mut roots = cx.selection.objects.clone();
|
||||
roots.extend(hit_test(cx.doc, p, HitOptions { tol, skip_guides: false, ..cx.hit_options() }).map(|h| h.leaf));
|
||||
let near = Rect::new(p.x - tol, p.y - tol, p.x + tol, p.y + tol);
|
||||
Targets::points_near(cx.doc, &roots, near).styled(cx).snap_point(p, tol)
|
||||
}
|
||||
|
||||
/// Distance Guides (Smart Guides → Invoke Distance Guides): how far the selection's bounds `from`
|
||||
/// lie from `to` along each axis, as measured lines ([`Overlay::Distance`]) labelled with their
|
||||
/// lengths in the General unit. Along an axis on which the two lie apart, one line spans the gap
|
||||
/// between their facing edges; along one on which they overlap, or one holds the other, one line
|
||||
/// runs between their left (top) edges and one between their right (bottom) edges. An artboard
|
||||
/// (`artboard`) is always measured edge to edge: the selection's distance from its left, right,
|
||||
/// top and bottom edges. Each line runs across the middle of where the two overlap on the other
|
||||
/// axis, else across the selection's middle, with a dashed line from `to`'s edge out to it when it
|
||||
/// passes beside `to`. Lines of no length are left out.
|
||||
pub fn distance_guides(cx: &ToolContext, from: Rect, to: Rect, artboard: bool) -> Vec<Overlay> {
|
||||
let finite = |r: &Rect| [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite());
|
||||
if !finite(&from) || !finite(&to) {
|
||||
return vec![];
|
||||
}
|
||||
let color = cx.smart_guide_color;
|
||||
let mut out = Vec::new();
|
||||
for x in [true, false] {
|
||||
let ((f0, f1), (t0, t1)) = (span(&from, x), span(&to, x));
|
||||
let ((g0, g1), (u0, u1)) = (span(&from, !x), span(&to, !x));
|
||||
let (lo, hi) = (g0.max(u0), g1.min(u1));
|
||||
let across = if lo <= hi { (lo + hi) / 2.0 } else { (g0 + g1) / 2.0 };
|
||||
let pt = |u: f64, c: f64| if x { Point::new(u, c) } else { Point::new(c, u) };
|
||||
// Each measured line: from the selection's edge to `to`'s.
|
||||
let lines: Vec<(f64, f64)> = match (artboard, f1 <= t0, t1 <= f0) {
|
||||
(false, true, _) => vec![(f1, t0)],
|
||||
(false, _, true) => vec![(f0, t1)],
|
||||
_ => vec![(f0, t0), (f1, t1)],
|
||||
};
|
||||
for (fu, tu) in lines {
|
||||
let length = (tu - fu).abs();
|
||||
if length <= 1e-9 {
|
||||
continue;
|
||||
}
|
||||
out.push(Overlay::Distance { a: pt(fu, across), b: pt(tu, across), text: cx.len(length), color });
|
||||
let on_edge = across.clamp(u0, u1);
|
||||
if on_edge != across {
|
||||
out.push(Overlay::Line { a: pt(tu, on_edge), b: pt(tu, across), color, dashed: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user