From 297f77b1d1194a8092203ad9a1205654374bd70b Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Wed, 7 Aug 2024 02:03:18 +0200 Subject: [PATCH 1/2] draw: Function for finding closest point --- CHANGES.md | 2 ++ src/bezier.typ | 27 ++++++++++++++ src/draw/grouping.typ | 84 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index ca1cf1fb..de65fb25 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 `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 diff --git a/src/bezier.typ b/src/bezier.typ index 36231a50..6a17e737 100644 --- a/src/bezier.typ +++ b/src/bezier.typ @@ -585,3 +585,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/grouping.typ b/src/draw/grouping.typ index d3dc791f..b6a3ca64 100644 --- a/src/draw/grouping.typ +++ b/src/draw/grouping.typ @@ -190,6 +190,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. /// /// ```typc example From ceb577be021b20cc11f7ed59c74fd155b0ca069b Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Wed, 21 Aug 2024 01:00:34 +0200 Subject: [PATCH 2/2] Find closest point by name --- CHANGES.md | 2 +- src/bezier.typ | 10 ++++++++-- src/draw.typ | 2 +- src/draw/grouping.typ | 26 +++++++++++++++--------- tests/closest-point/ref/1.png | Bin 0 -> 10648 bytes tests/closest-point/test.typ | 36 ++++++++++++++++++++++++++++++++++ 6 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 tests/closest-point/ref/1.png create mode 100644 tests/closest-point/test.typ diff --git a/CHANGES.md b/CHANGES.md index de65fb25..f8fd6742 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -41,7 +41,7 @@ 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 `closest-point` for creating an anchor at the closest point between a +- Added `find-closest-point` for creating an anchor at the closest point between a reference point and one or more elements. ## Marks diff --git a/src/bezier.typ b/src/bezier.typ index 6a17e737..85237077 100644 --- a/src/bezier.typ +++ b/src/bezier.typ @@ -586,8 +586,14 @@ return pts } -/// Find the closest point on a bezier to a given point -/// by using a binary search along the curve. +/// 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 diff --git a/src/draw.typ b/src/draw.typ index e46b87de..0fe7a97e 100644 --- a/src/draw.typ +++ b/src/draw.typ @@ -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 diff --git a/src/draw/grouping.typ b/src/draw/grouping.typ index b6a3ca64..becb5892 100644 --- a/src/draw/grouping.typ +++ b/src/draw/grouping.typ @@ -194,24 +194,33 @@ /// creates an anchor. Transformations insides the body are scoped and do /// not get applied outsides. /// -/// - name (string): Anchor name. +/// - name (str): 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) = { +/// - 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 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 (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 @@ -268,8 +277,7 @@ ctx: ctx, name: name, anchors: anchors, - drawables: drawables, - bounds: bounds + drawables: if output-drawables { drawables } else { () }, ) },) } diff --git a/tests/closest-point/ref/1.png b/tests/closest-point/ref/1.png new file mode 100644 index 0000000000000000000000000000000000000000..0cd7d7c60c334efd96cb1920f7da3728aa8f703a GIT binary patch literal 10648 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�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 literal 0 HcmV?d00001 diff --git a/tests/closest-point/test.typ b/tests/closest-point/test.typ new file mode 100644 index 00000000..5fc1ea02 --- /dev/null +++ b/tests/closest-point/test.typ @@ -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) +})