diff --git a/src/anchor.typ b/src/anchor.typ index 15fb51658..9476336b1 100644 --- a/src/anchor.typ +++ b/src/anchor.typ @@ -33,66 +33,6 @@ #let closed-shape-names = compass-directions-with-center + path-distance-names -/// Setup an anchor calculation and handling function for an element. Unifies anchor error checking and calculation of the offset transform. -/// -/// A tuple of a transformation matrix and function will be returned. -/// The transform is calculated by translating the given transform by the distance between the position of `offset-anchor` and `default`. It can then be used to correctly transform an element's drawables. If both either are none the calculation won't happen but the transform will still be returned. -/// The function can be used to get the transformed anchors of an element by passing it a string. An empty array can be passed to get the list of valid anchors. -/// -/// - callback (function): The function to call to get an anchor's position. The anchor's name will be passed and it should return a vector (str => vector). -/// - anchor-names (array): A list of valid anchor names. This list will be used to validate an anchor exists before `callback` is used. -/// - default (str): The name of the default anchor. -/// - transform (matrix): The current transformation matrix to apply to an anchor's position before returning it. If `offset-anchor` and `default` is set, it will be first translated by the distance between them. -/// - name (str): The name of the element, this is only used in the error message in the event an anchor is invalid. -/// - offset-anchor: The name of an anchor to offset the transform by. -/// -> (matrix, function) -#let setup(callback, anchor-names, default: none, transform: none, name: none, offset-anchor: none) = { - if default != none and offset-anchor != none { - assert( - offset-anchor in anchor-names, - message: strfmt("Anchor '{}' not in anchors {} for element '{}'", offset-anchor, repr(anchor-names), name) - ) - let offset = matrix.transform-translate( - ..vector.sub(callback(default), callback(offset-anchor)).slice(0, 3) - ) - transform = if transform != none { - matrix.mul-mat( - transform, - offset - ) - } else { - offset - } - } - - let calculate-anchor(anchor) = { - if anchor == () { - return anchor-names - } - if anchor == "default" { - assert.ne(default, none, message: strfmt("Element '{}' does not have a default anchor!", name)) - anchor = default - } - - let out = callback(anchor) - assert( - out != none, - message: strfmt("Anchor '{}' not in anchors {} for element '{}'", anchor, repr(anchor-names), name) - ) - - return if transform != none { - util.apply-transform( - transform, - out - ) - } else { - out - } - } - return (if transform == none { matrix.ident() } else { transform }, calculate-anchor) -} - - /// Calculates a border anchor at the given angle by testing for an intersection between a line and the given drawables. /// /// This function is not ready to be used widely in its current state. It is only to be used to calculate the cardinal anchors of the arc element until properly updated. It will panic if no intersections have been found. @@ -137,9 +77,8 @@ } /// Handle path distance anchor -#let resolve-distance(ctx, anchor, drawable) = { - if type(anchor) in (int, float, length, ratio) { - anchor = util.resolve-number(ctx, anchor) +#let resolve-distance(anchor, drawable) = { + if type(anchor) in (int, float, ratio) { return path-util.point-on-path(drawable.segments, anchor) } } @@ -162,9 +101,10 @@ // Handle anchor for a line shape // -// Line shapes have: +// Path anchors are: // - Distance anchors -#let resolve-line-shape(ctx, anchor, drawable) = { +// - Ratio anchors +#let calculate-path-anchor(anchor, drawable) = { if type(drawable) == array { assert(drawable.len() == 1, message: "Expected a single path, got " + repr(drawable)) @@ -175,31 +115,137 @@ anchor = path-distances.at(anchor) } - return resolve-distance(ctx, anchor, drawable) + return resolve-distance(anchor, drawable) } // Handle anchor for a closed shape // -// Closed shapes have: +// Border anchors are: // - Compass direction anchors -// - Distance anchors // - Angle anchors -#let resolve-closed-shape(ctx, anchor, center, rx, ry, drawable) = { +#let calculate-border-anchor(anchor, center, rx, ry, drawable) = { if type(drawable) == array { assert(drawable.len() == 1, message: "Expected a single path, got " + repr(drawable)) drawable = drawable.first() } - if type(anchor) == str and anchor in path-distance-names { - anchor = path-distances.at(anchor) - } - if type(anchor) == str { return resolve-compass-dir(anchor, center, rx, ry, drawable) } else if type(anchor) == angle { return resolve-border-angle(anchor, center, rx, ry, drawable) - } else { - return resolve-distance(ctx, anchor, drawable) } } + + +/// Setup an anchor calculation and handling function for an element. Unifies anchor error checking and calculation of the offset transform. +/// +/// A tuple of a transformation matrix and function will be returned. +/// The transform is calculated by translating the given transform by the distance between the position of `offset-anchor` and `default`. It can then be used to correctly transform an element's drawables. If both either are none the calculation won't happen but the transform will still be returned. +/// The function can be used to get the transformed anchors of an element by passing it a string. An empty array can be passed to get the list of valid anchors. +/// +/// - callback (function): The function to call to get an anchor's position. The anchor's name will be passed and it should return a vector (str => vector). +/// - anchor-names (array): A list of valid anchor names. This list will be used to validate an anchor exists before `callback` is used. +/// - default (str): The name of the default anchor. +/// - transform (matrix): The current transformation matrix to apply to an anchor's position before returning it. If `offset-anchor` and `default` is set, it will be first translated by the distance between them. +/// - name (str): The name of the element, this is only used in the error message in the event an anchor is invalid. +/// - offset-anchor: The name of an anchor to offset the transform by. +/// - border-anchors (bool): If true, add border anchors (compass and angle anchors) +/// - path-anchors (bool): If true, add path anchors (distance anchors) +/// - center (none,vector): Center of the path `path`, used for border anchor calculation +/// - radii (none,tuple): Radius tuple used for border anchor calculation +/// - path (none,drawable): Path used for path and border anchor calculation +/// -> (matrix, function) +#let setup(callback, + anchor-names, + default: none, + transform: none, + name: none, + offset-anchor: none, + border-anchors: false, + path-anchors: false, + center: none, + radii: none, + path: none) = { + // Passing no callback is valid! + if callback == auto { + callback = (anchor) => {} + } + + // Add enabled anchor names + if border-anchors { + assert(center != none and radii != none and path != none, + message: "Border anchors need center point, radii and the path set!") + anchor-names += compass-directions-with-center + } + if path-anchors { + assert(path != none, + message: "Path anchors need the path set!") + anchor-names += path-distance-names + } + + // Populate callback with auto added + // anchor functions + if border-anchors or path-anchors { + callback = (anchor) => { + let pt = callback(anchor) + if pt == none and border-anchors { + pt = calculate-border-anchor( + anchor, center, ..radii, path) + } + if pt == none and path-anchors { + pt = calculate-path-anchor( + anchor, path) + } + return pt + } + } + + if default != none and offset-anchor != none { + assert( + offset-anchor in anchor-names, + message: strfmt("Anchor '{}' not in anchors {} for element '{}'", offset-anchor, repr(anchor-names), name) + ) + let offset = matrix.transform-translate( + ..vector.sub(callback(default), callback(offset-anchor)).slice(0, 3) + ) + transform = if transform != none { + matrix.mul-mat( + transform, + offset + ) + } else { + offset + } + } + + // Anchor callback + let calculate-anchor(anchor) = { + if anchor == () { + return anchor-names + } + if anchor == "default" { + assert.ne(default, none, message: strfmt("Element '{}' does not have a default anchor!", name)) + anchor = default + } + + let out = callback(anchor) + assert( + out != none, + message: strfmt("Anchor '{}' not in anchors {} for element '{}'", + anchor, repr(anchor-names), name) + ) + + return if transform != none { + util.apply-transform( + transform, + out + ) + } else { + out + } + } + return (if transform == none { matrix.ident() } else { transform }, calculate-anchor) +} + + diff --git a/src/coordinate.typ b/src/coordinate.typ index fb7b17e34..493d4e115 100644 --- a/src/coordinate.typ +++ b/src/coordinate.typ @@ -71,6 +71,11 @@ assert(name in ctx.nodes, message: "Unknown element '" + name + "' in elements " + repr(ctx.nodes.keys())) + // Resolve length anchors + if type(anchor) == length { + anchor = util.resolve-number(ctx, anchor) + } + // Check if anchor is known let node = ctx.nodes.at(name) let pos = (node.anchors)(anchor) diff --git a/src/draw/grouping.typ b/src/draw/grouping.typ index 2298227f8..44776ad14 100644 --- a/src/draw/grouping.typ +++ b/src/draw/grouping.typ @@ -171,34 +171,38 @@ aabb.padded(bounds, padding) } + // Calculate a bounding box path used for border + // anchor calculation. + let (center, width, height, path) = if bounds != none { + (bounds.low.at(1), bounds.high.at(1)) = (bounds.high.at(1), bounds.low.at(1)) + let center = aabb.mid(bounds) + let (width, height, _) = aabb.size(bounds) + let path = drawable.path( + path-util.line-segment(( + (bounds.low.at(0), bounds.high.at(1)), + bounds.high, + (bounds.high.at(0), bounds.low.at(1)), + bounds.low, + )), close: true) + (center, width, height, path) + } else { (none, none, none, none) } + let (transform, anchors) = anchor_.setup( anchor => { let anchors = group-ctx.groups.last().anchors if type(anchor) == str and anchor in anchors { return anchors.at(anchor) } - - if bounds != none { - let bounds = bounds - (bounds.low.at(1), bounds.high.at(1)) = (bounds.high.at(1), bounds.low.at(1)) - let center = aabb.mid(bounds) - let (width, height, _) = aabb.size(bounds) - - return anchor_.resolve-closed-shape( - ctx, anchor, center, width, height, drawable.path( - path-util.line-segment(( - (bounds.low.at(0), bounds.high.at(1)), - bounds.high, - (bounds.high.at(0), bounds.low.at(1)), - bounds.low, - )), - close: true)) - } }, - group-ctx.groups.last().anchors.keys() + if bounds != none { anchor_.closed-shape-names }, + group-ctx.groups.last().anchors.keys(), name: name, default: if bounds != none { "center" } else { none }, - offset-anchor: anchor + offset-anchor: anchor, + path-anchors: bounds != none, + border-anchors: bounds != none, + center: center, + radii: (width, height), + path: path, ) return ( ctx: ctx, diff --git a/src/draw/shapes.typ b/src/draw/shapes.typ index d9d299fa1..71cd08b71 100644 --- a/src/draw/shapes.typ +++ b/src/draw/shapes.typ @@ -63,15 +63,17 @@ stroke: style.stroke) let (transform, anchors) = anchor_.setup( - (anchor) => { - anchor_.resolve-closed-shape( - ctx, anchor, (cx, cy, cz), rx, ry, path) - }, - anchor_.closed-shape-names, + auto, + (), default: "center", name: name, offset-anchor: anchor, - transform: ctx.transform + transform: ctx.transform, + border-anchors: true, + path-anchors: true, + center: (cx, cy, cz), + radii: (rx, ry), + path: path, ) return ( @@ -135,15 +137,17 @@ stroke: style.stroke) let (transform, anchors) = anchor_.setup( - anchor => { - anchor_.resolve-closed-shape( - ctx, anchor, center, r, r, path) - }, - anchor_.closed-shape-names, + auto, + (), default: "center", name: name, offset-anchor: anchor, - transform: ctx.transform + transform: ctx.transform, + border-anchors: true, + path-anchors: true, + center: center, + radii: (r, r), + path: path, ) return ( @@ -311,15 +315,18 @@ (path-util.segment-start(path.segments.first()), sector-center, path-util.segment-end(path.segments.last())))) - return anchor_.resolve-closed-shape( - ctx, anchor, center, 2 * rx, 2 * ry, path) + return anchor_.calculate-border-anchor( + anchor, center, 2 * rx, 2 * ry, path) } else { - return anchor_.resolve-line-shape( - ctx, anchor, path) + return anchor_.calculate-path-anchor( + anchor, path) } } else { - return anchor_.resolve-closed-shape( - ctx, anchor, center, 2 * rx, 2 * ry, path) + let pt = anchor_.calculate-border-anchor( + anchor, center, 2 * rx, 2 * ry, path) + if pt != none { return pt } + return anchor_.calculate-path-anchor( + anchor, path) } }, ("arc-center", "chord-center", "origin", "arc-start", "arc-end") + anchor_.closed-shape-names, @@ -602,12 +609,12 @@ // Get bounds let (transform, anchors) = anchor_.setup( - (anchor) => { - return anchor_.resolve-line-shape(ctx, anchor, path) - }, - anchor_.path-distance-names, + auto, + (), name: name, transform: ctx.transform, + path-anchors: true, + path: path ) let drawables = (path,) @@ -1140,15 +1147,17 @@ let center = vector.scale(vector.add(a, b), .5) let (width, height, ..) = size let (transform, anchors) = anchor_.setup( - (anchor) => { - return anchor_.resolve-closed-shape( - ctx, anchor, center, width, height, drawables) - }, - anchor_.closed-shape-names, + auto, + (), default: "center", name: name, offset-anchor: anchor, - transform: ctx.transform + transform: ctx.transform, + border-anchors: true, + path-anchors: true, + center: center, + radii: (width, height), + path: drawables, ) return ( @@ -1235,13 +1244,13 @@ } else if anchor == "ctrl-1" { return ctrl.at(1) } - return anchor_.resolve-line-shape( - ctx, anchor, path) }, - ("ctrl-0", "ctrl-1") + anchor_.path-distance-names, + ("ctrl-0", "ctrl-1"), default: "start", name: name, - transform: ctx.transform + transform: ctx.transform, + path-anchors: true, + path: path, ) if marks != none { @@ -1352,13 +1361,13 @@ if type(anchor) == str and anchor in a { return a.at(anchor) } - return anchor_.resolve-line-shape( - ctx, anchor, path) }, - a.keys() + anchor_.path-distance-names, + a.keys(), name: name, default: "start", - transform: ctx.transform + transform: ctx.transform, + path-anchors: true, + path: path, ) } @@ -1446,13 +1455,13 @@ if type(anchor) == str and anchor in a { return a.at(anchor) } - return anchor_.resolve-line-shape( - ctx, anchor, path) }, - a.keys() + anchor_.path-distance-names, + a.keys(), name: name, default: "start", - transform: ctx.transform + transform: ctx.transform, + path-anchors: true, + path: path, ) } @@ -1528,13 +1537,12 @@ let path = drawable.path(fill: style.fill, stroke: style.stroke, close: close, segments) let (transform, anchors) = anchor_.setup( - anchor => { - return anchor_.resolve-line-shape( - ctx, anchor, path) - }, - anchor_.path-distance-names, + auto, + (), name: name, transform: ctx.transform, + path-anchors: true, + path: path, ) return ( diff --git a/tests/anchor/element-anchors/ref.png b/tests/anchor/element-anchors/ref.png index 79f3dc0a4..505abfd1d 100644 Binary files a/tests/anchor/element-anchors/ref.png and b/tests/anchor/element-anchors/ref.png differ diff --git a/tests/chart/piechart/ref.png b/tests/chart/piechart/ref.png index 582305391..bdfd79bdf 100644 Binary files a/tests/chart/piechart/ref.png and b/tests/chart/piechart/ref.png differ diff --git a/tests/group/transform/ref.png b/tests/group/transform/ref.png index 05a2d940b..ab9173afe 100644 Binary files a/tests/group/transform/ref.png and b/tests/group/transform/ref.png differ diff --git a/tests/group/translate/ref.png b/tests/group/translate/ref.png index d956aeefb..cd4e32184 100644 Binary files a/tests/group/translate/ref.png and b/tests/group/translate/ref.png differ diff --git a/tests/plot/legend/ref.png b/tests/plot/legend/ref.png index 17548b19f..53680e563 100644 Binary files a/tests/plot/legend/ref.png and b/tests/plot/legend/ref.png differ diff --git a/tests/rect/rounded/ref.png b/tests/rect/rounded/ref.png index 739d3e1f3..acad7f08a 100644 Binary files a/tests/rect/rounded/ref.png and b/tests/rect/rounded/ref.png differ