Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

draw: Function for finding closest point #668

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ package called `cetz-plot`.
depth ordering and face culling of drawables. Ordering is enabled by default.
- Closed `line` and `merge-path` elements now have a `"centroid"` anchor that
is the calculated centroid of the (non self-intersecting!) shape.
- Added `find-closest-point` for creating an anchor at the closest point between a
reference point and one or more elements.

## Marks
- Added support for mark `anchor` style key, to adjust mark placement and
Expand Down
33 changes: 33 additions & 0 deletions src/bezier.typ
Original file line number Diff line number Diff line change
Expand Up @@ -585,3 +585,36 @@
}
return pts
}

/// Find the closest point on a bezier to a given point.
///
/// - pt (vector): Reference point to find the closest point to
/// - s (vector): Bezier start
/// - e (vector): Bezier end
/// - c1 (vector): Bezier control point 1
/// - c2 (vector): Bezier control point 2
/// - max-recursion (int): Max recursion depth
#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, scope, anchor, copy-anchors, set-ctx, get-ctx, for-each-anchor, on-layer, hide, floating
#import "draw/grouping.typ": intersections, group, scope, anchor, copy-anchors, set-ctx, get-ctx, for-each-anchor, on-layer, hide, floating, find-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, register-mark
#import "draw/shapes.typ": circle, circle-through, arc, arc-through, mark, line, grid, content, rect, bezier, bezier-through, catmull, hobby, merge-path
Expand Down
92 changes: 92 additions & 0 deletions src/draw/grouping.typ
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,98 @@
},)
}

/// 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 (str): Anchor name.
/// - reference-point (coordinate): Coordinate to find the closest point to.
/// - body (element,str): One or more elements to consider. A least one is required. A function that accepts `ctx` and returns elements is also accepted. If a string is passed, the existing named element is used.
#let find-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))
assert(type(body) in (array, function, str),
message: "Expected body to be a list of elements, a callback or an elements name")
coordinate.resolve-system(reference-point)

return (ctx => {
let (_, pt) = coordinate.resolve(ctx, reference-point)
pt = util.apply-transform(ctx.transform, pt)

let (sub-ctx, drawables, output-drawables) = if type(body) == str {
let node = ctx.nodes.at(body)
(ctx, node.drawables, false)
} else {
let group-ctx = ctx
group-ctx.groups.push(())
let node = process.many(group-ctx, util.resolve-body(ctx, body))
(node.ctx, node.drawables, true)
}

ctx.nodes += sub-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: if output-drawables { drawables } else { () },
)
},)
}

/// 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.
///
/// ```typc example
Expand Down
Binary file added tests/closest-point/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions tests/closest-point/test.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#set page(width: auto, height: auto)
#import "/tests/helper.typ": *

#test-case({
import cetz.draw: *

group(name: "g", {
rotate(10deg)
rect((-1, -1), (1, 1), radius: .45)
})

for i in range(0, 360, step: 10) {
let pt = (i * 1deg, 2)

find-closest-point("test", pt, {
rotate(10deg)
hide(rect((-1, -1), (1, 1), radius: .45))
})

line(pt, "test")
circle(pt, radius: .1, fill: blue)
}
})

#test-case({
import cetz.draw: *

group(name: "g", {
rotate(10deg)
rect((-1, -1), (1, 1), radius: .45)
})

let pt = (2, 2)
find-closest-point("test", pt, "g")
line("test", pt)
})