Skip to content

Commit

Permalink
anchor: Add automatic path and border anchor setup
Browse files Browse the repository at this point in the history
  • Loading branch information
johannes-wolf committed Dec 21, 2023
1 parent 99c522e commit 3178db9
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 139 deletions.
196 changes: 121 additions & 75 deletions src/anchor.typ
Original file line number Diff line number Diff line change
Expand Up @@ -34,66 +34,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<str>): 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.
Expand Down Expand Up @@ -138,9 +78,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)
}
}
Expand All @@ -163,9 +102,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))
Expand All @@ -176,31 +116,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<str>): 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)
}


5 changes: 5 additions & 0 deletions src/coordinate.typ
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,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)
Expand Down
42 changes: 23 additions & 19 deletions src/draw/grouping.typ
Original file line number Diff line number Diff line change
Expand Up @@ -220,34 +220,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,
Expand Down
Loading

0 comments on commit 3178db9

Please sign in to comment.