Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit bc95c80

Browse files
committedAug 20, 2024·
draw: Function for finding closest point
1 parent b334fef commit bc95c80

File tree

4 files changed

+114
-1
lines changed

4 files changed

+114
-1
lines changed
 

‎CHANGES.md

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ package called `cetz-plot`.
2929
depth ordering and face culling of drawables. Ordering is enabled by default.
3030
- Closed `line` and `merge-path` elements now have a `"centroid"` anchor that
3131
is the calculated centroid of the (non self-intersecting!) shape.
32+
- Added `closest-point` for creating an anchor at the closest point between a
33+
reference point and one or more elements.
3234

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

‎src/bezier.typ

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

‎src/draw.typ

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#import "draw/grouping.typ": intersections, group, scope, 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, scope, 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

+84
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,90 @@
191191
},)
192192
}
193193

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

0 commit comments

Comments
 (0)
Please sign in to comment.