Skip to content

Commit a7d525c

Browse files
committed
draw: Function for finding closest point
1 parent cbbe1b7 commit a7d525c

File tree

4 files changed

+114
-1
lines changed

4 files changed

+114
-1
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ package called `cetz-plot`.
2222

2323
## Draw
2424
- Added `floating` function for drawing elements without affecting bounding boxes.
25+
- Added `closest-point` for creating an anchor at the closest point between a
26+
reference point and one or more elements.
2527

2628
## Marks
2729
- Added support for mark `anchor` style key, to adjust mark placement and

src/bezier.typ

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,3 +586,30 @@
586586
}
587587
return pts
588588
}
589+
590+
/// Find the closest point on a bezier to a given point
591+
/// by using a binary search along the curve.
592+
#let cubic-closest-point(pt, s, e, c1, c2, max-recursion: 1) = {
593+
let probe(low, high, depth) = {
594+
let min = calc.inf
595+
let min-t = 0
596+
597+
for t in range(0, 11) {
598+
t = low + t / 10 * (high - low)
599+
let d = vector.dist(pt, cubic-point(s, e, c1, c2, t))
600+
if d < min {
601+
min = d
602+
min-t = t
603+
}
604+
}
605+
606+
if depth < max-recursion {
607+
let step = (high - low) / 10
608+
return probe(calc.max(0, min-t - step), calc.min(min-t + step, 1), depth + 1)
609+
}
610+
611+
return cubic-point(s, e, c1, c2, min-t)
612+
}
613+
614+
return probe(0, 1, 0)
615+
}

src/draw.typ

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#import "draw/grouping.typ": intersections, group, anchor, copy-anchors, place-anchors, set-ctx, get-ctx, for-each-anchor, on-layer, place-marks, hide, floating
1+
#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
22
#import "draw/transformations.typ": set-transform, rotate, translate, scale, set-origin, move-to, set-viewport
33
#import "draw/styling.typ": set-style, fill, stroke
44
#import "draw/shapes.typ": circle, circle-through, arc, arc-through, mark, line, grid, content, rect, bezier, bezier-through, catmull, hobby, merge-path

src/draw/grouping.typ

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,90 @@
198198
},)
199199
}
200200

201+
/// Finds the closest point on one or more elements to a coordinate and
202+
/// creates an anchor. Transformations insides the body are scoped and do
203+
/// not get applied outsides.
204+
///
205+
/// - name (string): Anchor name.
206+
/// - reference-point (coordinate): Coordinate to find the closest point to.
207+
/// - body (element): One or more elements to consider. A least one is required. A function that accepts `ctx` and returns elements is also accepted.
208+
#let closest-point(name, reference-point, body) = {
209+
import "/src/bezier.typ": cubic-closest-point
210+
211+
assert(type(name) == str,
212+
message: "Anchor name must be of type string, got " + repr(name))
213+
coordinate.resolve-system(reference-point)
214+
215+
return (ctx => {
216+
let (_, pt) = coordinate.resolve(ctx, reference-point)
217+
pt = util.apply-transform(ctx.transform, pt)
218+
219+
let group-ctx = ctx
220+
group-ctx.groups.push(())
221+
let (ctx: group-ctx, drawables, bounds) = process.many(group-ctx, util.resolve-body(ctx, body))
222+
ctx.nodes += group-ctx.nodes
223+
224+
let min = calc.inf
225+
let min-pt = none
226+
227+
// Compute the closest point on line a-b to point pt
228+
let line-closest-pt(pt, a, b) = {
229+
let n = vector.sub(b, a)
230+
let d = vector.dot(n, pt)
231+
d -= vector.dot(a, n)
232+
233+
let f = d / vector.dot(n, n)
234+
return if f < 0 {
235+
a
236+
} else if f > 1 {
237+
b
238+
} else {
239+
vector.add(a, vector.scale(n, f))
240+
}
241+
}
242+
243+
for d in drawables {
244+
if not "segments" in d { continue }
245+
246+
for ((kind, ..pts)) in d.segments {
247+
if kind == "cubic" {
248+
let tmp-pt = cubic-closest-point(pt, ..pts)
249+
let tmp-min = vector.dist(tmp-pt, pt)
250+
if tmp-min < min {
251+
min-pt = tmp-pt
252+
min = tmp-min
253+
}
254+
} else {
255+
for i in range(1, pts.len()) {
256+
let tmp-pt = line-closest-pt(pt, pts.at(i - 1), pts.at(i))
257+
let tmp-min = vector.dist(tmp-pt, pt)
258+
if tmp-min < min {
259+
min-pt = tmp-pt
260+
min = tmp-min
261+
}
262+
}
263+
}
264+
}
265+
}
266+
267+
let (transform, anchors) = anchor_.setup(
268+
anchor => min-pt,
269+
("default",),
270+
default: "default",
271+
name: name,
272+
transform: none
273+
)
274+
275+
return (
276+
ctx: ctx,
277+
name: name,
278+
anchors: anchors,
279+
drawables: drawables,
280+
bounds: bounds
281+
)
282+
},)
283+
}
284+
201285
/// 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.
202286
///
203287
/// #example(```

0 commit comments

Comments
 (0)