From b48e998c6ae0417c30bf32fbd073018873c0c8f5 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Wed, 7 Aug 2024 02:03:18 +0200 Subject: [PATCH] draw: Function for finding closest point --- src/bezier.typ | 27 ++++++++++++++ src/draw.typ | 2 +- src/draw/grouping.typ | 84 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/bezier.typ b/src/bezier.typ index daa74e03..3b3e2f44 100644 --- a/src/bezier.typ +++ b/src/bezier.typ @@ -586,3 +586,30 @@ } return pts } + +/// Find the closest point on a bezier to a given point +/// by using a binary search along the curve. +#let cubic-closest-point(pt, s, e, c1, c2, max-recursion: 1) = { + let probe(low, high, depth) = { + let min = calc.inf + let min-t = 0 + + for t in range(0, 11) { + t = low + t / 10 * (high - low) + let d = vector.dist(pt, cubic-point(s, e, c1, c2, t)) + if d < min { + min = d + min-t = t + } + } + + if depth < max-recursion { + let step = (high - low) / 10 + return probe(calc.max(0, min-t - step), calc.min(min-t + step, 1), depth + 1) + } + + return cubic-point(s, e, c1, c2, min-t) + } + + return probe(0, 1, 0) +} diff --git a/src/draw.typ b/src/draw.typ index 53d84ba4..321470bd 100644 --- a/src/draw.typ +++ b/src/draw.typ @@ -1,4 +1,4 @@ -#import "draw/grouping.typ": intersections, group, anchor, copy-anchors, place-anchors, set-ctx, get-ctx, for-each-anchor, on-layer, place-marks, hide, floating +#import "draw/grouping.typ": intersections, group, anchor, copy-anchors, place-anchors, set-ctx, get-ctx, for-each-anchor, on-layer, place-marks, hide, floating, closest-point #import "draw/transformations.typ": set-transform, rotate, translate, scale, set-origin, move-to, set-viewport #import "draw/styling.typ": set-style, fill, stroke #import "draw/shapes.typ": circle, circle-through, arc, arc-through, mark, line, grid, content, rect, bezier, bezier-through, catmull, hobby, merge-path diff --git a/src/draw/grouping.typ b/src/draw/grouping.typ index 1c4cab18..629c0ca3 100644 --- a/src/draw/grouping.typ +++ b/src/draw/grouping.typ @@ -198,6 +198,90 @@ },) } +/// Finds the closest point on one or more elements to a coordinate and +/// creates an anchor. Transformations insides the body are scoped and do +/// not get applied outsides. +/// +/// - name (string): Anchor name. +/// - reference-point (coordinate): Coordinate to find the closest point to. +/// - body (element): One or more elements to consider. A least one is required. A function that accepts `ctx` and returns elements is also accepted. +#let closest-point(name, reference-point, body) = { + import "/src/bezier.typ": cubic-closest-point + + assert(type(name) == str, + message: "Anchor name must be of type string, got " + repr(name)) + coordinate.resolve-system(reference-point) + + return (ctx => { + let (_, pt) = coordinate.resolve(ctx, reference-point) + pt = util.apply-transform(ctx.transform, pt) + + let group-ctx = ctx + group-ctx.groups.push(()) + let (ctx: group-ctx, drawables, bounds) = process.many(group-ctx, util.resolve-body(ctx, body)) + ctx.nodes += group-ctx.nodes + + let min = calc.inf + let min-pt = none + + // Compute the closest point on line a-b to point pt + let line-closest-pt(pt, a, b) = { + let n = vector.sub(b, a) + let d = vector.dot(n, pt) + d -= vector.dot(a, n) + + let f = d / vector.dot(n, n) + return if f < 0 { + a + } else if f > 1 { + b + } else { + vector.add(a, vector.scale(n, f)) + } + } + + for d in drawables { + if not "segments" in d { continue } + + for ((kind, ..pts)) in d.segments { + if kind == "cubic" { + let tmp-pt = cubic-closest-point(pt, ..pts) + let tmp-min = vector.dist(tmp-pt, pt) + if tmp-min < min { + min-pt = tmp-pt + min = tmp-min + } + } else { + for i in range(1, pts.len()) { + let tmp-pt = line-closest-pt(pt, pts.at(i - 1), pts.at(i)) + let tmp-min = vector.dist(tmp-pt, pt) + if tmp-min < min { + min-pt = tmp-pt + min = tmp-min + } + } + } + } + } + + let (transform, anchors) = anchor_.setup( + anchor => min-pt, + ("default",), + default: "default", + name: name, + transform: none + ) + + return ( + ctx: ctx, + name: name, + anchors: anchors, + drawables: drawables, + bounds: bounds + ) + },) +} + /// Groups one or more elements together. This element acts as a scope, all state changes such as transformations and styling only affect the elements in the group. Elements after the group are not affected by the changes inside the group. /// /// #example(```