diff --git a/src/draw/grouping.typ b/src/draw/grouping.typ index d3dc791f..ed6a0460 100644 --- a/src/draw/grouping.typ +++ b/src/draw/grouping.typ @@ -115,7 +115,13 @@ /// - name (str): Name to prepend to the generated anchors. (Not to be confused with other `name` arguments that allow the use of anchor coordinates.) /// - ..elements (elements,str): Elements and/or element names to calculate intersections with. Elements referred to by name are (unlike elements passed) not drawn by the intersections function! /// - samples (int): Number of samples to use for non-linear path segments. A higher sample count can give more precise results but worse performance. -#let intersections(name, ..elements, samples: 10) = { +/// - sort (none,function): A function of the form `(context, array) -> array` +/// that gets called with the list of intersection points. +/// +/// CeTZ provides the following sorting functions: +/// - sorting.points-by-distace(points, reference: (0, 0, 0)) +/// - sorting.points-by-angle(points, reference: (0, 0, 0)) +#let intersections(name, ..elements, samples: 10, sort: none) = { samples = calc.clamp(samples, 2, 2500) assert(type(name) == str and name != "", @@ -169,6 +175,11 @@ } } } + + if sort != none { + pts = (sort)(ctx, pts) + } + let anchors = (:) for (i, pt) in pts.enumerate() { anchors.insert(str(i), pt) diff --git a/src/lib.typ b/src/lib.typ index a471dcd3..7bd90724 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -15,6 +15,7 @@ #import "path-util.typ" #import "mark.typ" #import "mark-shapes.typ" +#import "sorting.typ" // Libraries #import "lib/palette.typ" diff --git a/src/sorting.typ b/src/sorting.typ new file mode 100644 index 00000000..3778b8c4 --- /dev/null +++ b/src/sorting.typ @@ -0,0 +1,29 @@ +#import "/src/vector.typ" +#import "/src/util.typ" + +/// Sort list of points by distance to a +/// reference point. +/// +/// - points (array): List of points to sort +/// - reference (vec): Reference point +/// -> List of points +#let points-by-distance(ctx, points, reference: (0, 0, 0)) = { + let reference = util.apply-transform(ctx.transform, reference) + return points.sorted(key: pt => { + vector.dist(pt, reference) + }) +} + +/// Sort list of 2D points by angle to a +/// reference 2D point in CCW order. +/// Z component is ignored. +/// +/// - points (array): List of points to sort +/// - reference (vec): Reference point +/// -> List of points +#let points-by-angle(ctx, points, reference: (0, 0, 0)) = { + let (rx, ry, ..) = util.apply-transform(ctx.transform, reference) + return points.sorted(key: ((px, py, ..)) => { + 360deg - calc.atan2(rx - px, ry - py) + }) +} diff --git a/tests/intersection/ref/1.png b/tests/intersection/ref/1.png index 07d005fa..18157bd0 100644 Binary files a/tests/intersection/ref/1.png and b/tests/intersection/ref/1.png differ diff --git a/tests/intersection/test.typ b/tests/intersection/test.typ index 2744ba81..1d36007d 100644 --- a/tests/intersection/test.typ +++ b/tests/intersection/test.typ @@ -1,5 +1,6 @@ #set page(width: auto, height: auto) #import "/src/lib.typ": * +#import "/tests/helper.typ": * #let test(body) = canvas(length: 1cm, { import draw: * @@ -66,7 +67,7 @@ }) }) -#box(stroke: 2pt + red, canvas({ +#test-case({ import draw: * intersections("i", { @@ -76,9 +77,9 @@ line("a.default", "b.default", stroke: none) }) line("i.0", "i.1", mark: (end: ">")) -})) +}) -#box(stroke: 2pt + red, canvas({ +#test-case({ import draw: * circle((0,0), name: "a") @@ -89,4 +90,20 @@ for-each-anchor("i", (name) => { circle("i."+name, radius: .1, fill: red) }) -})) +}) + +#test-case(fn => { + import draw: * + + let c = circle((1,1), name: "a", radius: 1.25) + let r = rect((0,0), (2,2), name: "b") + intersections("i", r, c, sort: fn) + + for-each-anchor("i", (name) => { + content((), [#name], frame: "circle", fill: white) + }) +}, args: ( + none, + sorting.points-by-angle.with(reference: (1, 1)), + sorting.points-by-distance.with(reference: (0, 0.1)), +))