Skip to content

Commit

Permalink
draw: Function for finding closest point
Browse files Browse the repository at this point in the history
  • Loading branch information
johannes-wolf committed Aug 7, 2024
1 parent cbbe1b7 commit b48e998
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 1 deletion.
27 changes: 27 additions & 0 deletions src/bezier.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
2 changes: 1 addition & 1 deletion src/draw.typ
Original file line number Diff line number Diff line change
@@ -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
Expand Down
84 changes: 84 additions & 0 deletions src/draw/grouping.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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(```
Expand Down

0 comments on commit b48e998

Please sign in to comment.