Skip to content
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
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
- `rect-around` now uses the shape instead of all anchors to compute an elements bounding box (#1022)
- Fixed transformation of content frame paths along with the position in `drawable.apply-transform`
- `intersections` is now using a `scope` internally to not leak body transformations
- Added special border & path anchor calculation for rects and ellipses
- Added commented out code that prepares for tikz-like border anchors
(north-east ≠ 45°) - this will be a breaking change

# 0.5.0
- **BREAKING** The default matrix changed to id (#967)
Expand Down
265 changes: 265 additions & 0 deletions src/anchor-helper.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
#import "vector.typ"
#import "util.typ": float-epsilon

#let _sampled-quarter-samples = 16
#let _sampled-quarter = range(0, _sampled-quarter-samples + 1).map(t => {
t = t / _sampled-quarter-samples * 90deg
(calc.cos(t), calc.sin(t))
})

// Get the circumference of a sampled quarter.
//
// -> float
#let _sampled-quarter-circumference(x-radius, y-radius) = {
let len = _sampled-quarter-samples
let u = 0
for i in range(1, len + 1) {
let (p0x, p0y) = _sampled-quarter.at(i - 1)
p0x *= x-radius
p0y *= y-radius

let (p1x, p1y) = _sampled-quarter.at(i)
p1x *= x-radius
p1y *= y-radius

u += vector.dist((p0x, p0y), (p1x, p1y))
}
return u
}

// Lookup a sampled point on a quarter for distance s.
//
// - s (float): Distance on the quarter
// -> vector
#let _lookup-sampled-quarter-point(s, x-radius, y-radius) = {
let len = _sampled-quarter-samples
let t = 0
for i in range(1, len + 1) {
let (p0x, p0y) = _sampled-quarter.at(i - 1)
p0x *= x-radius
p0y *= y-radius

let (p1x, p1y) = _sampled-quarter.at(i)
p1x *= x-radius
p1y *= y-radius
let d = vector.dist((p0x, p0y), (p1x, p1y))

// We found our segement, lets interpolate
if t <= s and s <= t + d {
return vector.lerp((p0x, p0y), (p1x, p1y), (s - t) / d)
}

t += d
}

return (0, y-radius)
}

// Lookup a point on the sampled ellipse for distance s
//
// - s (ration, float, length): Distance on the ellipses border, must be normalized
// - x-radius (float): X radius
// - y-radius (float): Y radius
// - unit-length (length): Unit length
// -> vector
// -> none
#let _lookup-ellipse-point(s, x-radius, y-radius, unit-length) = {
let qcirc = _sampled-quarter-circumference(x-radius, y-radius)
let circ = 4 * qcirc

if type(s) == ratio {
s = s / 100% * circ
}
if type(s) == length {
s = s / unit-length
}

// Normalize the distance to [0, circ]
s = s - calc.floor(s / circ) * circ

// Find the quadrant we are in
let quadrant = calc.floor(s / qcirc)
let local = s - quadrant * qcirc

return if quadrant == 0 {
_lookup-sampled-quarter-point(local, x-radius, y-radius)
} else if quadrant == 1 {
let (x, y) = _lookup-sampled-quarter-point(qcirc - local, x-radius, y-radius)
(-x, y)
} else if quadrant == 2 {
let (x, y) = _lookup-sampled-quarter-point(local, x-radius, y-radius)
(-x, -y)
} else {
let (x, y) = _lookup-sampled-quarter-point(qcirc - local, x-radius, y-radius)
(x, -y)
}
}

// Compute a point on a circle for distance s
//
// - s (float): Distance on the circle border
// -> vector
#let _circle-point(s, radius, unit-length) = {
let circ = 2 * calc.pi * radius

if type(s) == ratio {
s = s / 100% * circ
}
if type(s) == length {
s = s / unit-length
}

// Normalize the distance to [0, u]
s = s - calc.floor(s / circ) * circ

let theta = s / circ * 360deg
return (calc.cos(theta) * radius,
calc.sin(theta) * radius)
}

/// Compute the border-anchor of a rect.
///
/// - center (vector): Rect center point
/// - angle (angle): Angle
/// - width (float): Width of the rect
/// - height (float): Height of the rect
/// -> vector
#let compute-rect-border(center, angle, width: 1, height: 1) = {
let eps = float-epsilon
let (cx, cy, cz) = center

// Normalize angle
angle = angle - calc.floor(angle / 360deg) * 360deg

// Special cases for degenerate rects
if width < eps {
return (cx, cy + calc.sin(angle) * height / 2, cz)
} else if height < eps {
return (cx + calc.cos(angle) * width / 2, cy, cz)
}

// Fast path for square rects
/* if calc.abs(width - height) < eps { */
let sx = calc.cos(angle)
let sy = calc.sin(angle)

if 45deg <= angle and angle <= 135deg { sy = 1 }
else if 225deg <= angle and angle <= 315deg { sy = -1 }

if 315deg <= angle or angle <= 45deg { sx = 1 }
else if 135deg <= angle and angle <= 225deg { sx = -1 }

return (cx + sx * width / 2,
cy + sy * height / 2,
cz)
/* } */

/* This is the correct code for tikz like border anchors on a circle
let radius = width * width + height * height

let p0 = (cx - width / 2, cy + height / 2, cz)
let p1 = (cx + width / 2, cy + height / 2, cz)
let p2 = (cx + width / 2, cy - height / 2, cz)
let p3 = (cx - width / 2, cy - height / 2, cz)

let scanline = (cx + calc.cos(angle) * radius,
cy + calc.sin(angle) * radius,
cz)

let pt
pt = intersection.line-line(p0, p1, center, scanline)
if (pt != none) { return pt }
pt = intersection.line-line(p1, p2, center, scanline)
if (pt != none) { return pt }
pt = intersection.line-line(p2, p3, center, scanline)
if (pt != none) { return pt }
pt = intersection.line-line(p3, p0, center, scanline)
if (pt != none) { return pt }

panic("Unreachable: rect-border", angle, center, scanline, width, height)
*/
}

/// Compute the path-anchor of a rect.
///
/// - center (vector): Rect center point
/// - anchor (float, ratio): Distance
/// - width (float): Width of the rect
/// - height (float): Height of the rect
/// - unit-length (length): Canvas unit length
/// -> vector
#let compute-rect-path(center, anchor, width: 1, height: 1, unit-length: 1cm) = {
let u = width * 2 + height * 2
if type(anchor) == ratio {
anchor = anchor / 100% * u
}
if type(anchor) == length {
anchor /= unit-length
}

// Normalize the distance to [0, u]
anchor = anchor - calc.floor(anchor / u) * u
let (cx, cy, cz) = center

// We start at east and go counter clockwise
let h2 = height / 2
let w2 = width / 2
if anchor <= h2 { return (cx + w2, cy + anchor, cz) }
anchor -= h2
if anchor <= width { return (cx + w2 - anchor, cy + h2, cz) }
anchor -= width
if anchor <= height { return (cx - w2, cy + h2 - anchor, cz) }
anchor -= height
if anchor <= width { return (cx - w2 + anchor, cy - h2, cz) }
anchor -= width
return (cx + w2, cy - h2 + anchor, cz)
}

/// Compute the border-anchor of an ellipse.
///
/// - center (vector): Rect center point
/// - angle (angle): Angle
/// - x-radius (float): X radius
/// - y-radius (float): Y radius
/// -> vector
#let compute-ellipse-border(center, angle, x-radius: 1, y-radius: 1) = {
let eps = float-epsilon

let (cx, cy, cz) = center

// TODO: See compute-rect-border
/* if calc.abs(x-radius - y-radius) < eps { */
return (cx + calc.cos(angle) * x-radius,
cy + calc.sin(angle) * y-radius,
cz)
/* } */

/*
let d = calc.sqrt(calc.pow(calc.cos(angle), 2)/(x-radius * x-radius) + calc.pow(calc.sin(angle), 2)/(y-radius * y-radius))
let bx = calc.cos(angle) / d
let by = calc.sin(angle) / d

return (cx + bx, cy + by, cz)
*/
}

/// Compute the path-anchor of an ellipse
///
/// - center (vector): Rect center point
/// - anchor (float, ratio, length): Distance
/// - x-radius (float): X radius
/// - y-radius (float): Y radius
/// - unit-length (length): Canvas unit length
/// -> vector
#let compute-ellipse-path(center, anchor, x-radius: 1, y-radius: 1, unit-length: 1cm) = {
let eps = 1e-6

let (ox, oy) = if calc.abs(x-radius - y-radius) < eps {
_circle-point(anchor, x-radius, unit-length)
} else {
_lookup-ellipse-point(anchor, x-radius, y-radius, unit-length)
}

let (cx, cy, cz) = center
return (cx + ox, cy + oy, cz)
}
44 changes: 24 additions & 20 deletions src/anchor.typ
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#import "path-util.typ"
#import "matrix.typ"
#import "vector.typ"
#import "anchor-helper.typ": compute-rect-border, compute-rect-path, compute-ellipse-border, compute-ellipse-path

// Compass direction to angle
#let named-border-anchors = (
Expand All @@ -27,37 +28,37 @@
end: 100%,
)

/// Calculates a border anchor at the given angle by testing for an intersection between a line and the given drawables. Returns `none` if no intersection is found for better error reporting.
/// Calculates a border anchor at the given angle by testing for an
/// intersection between a line and the given drawables. The scan-line
/// goes from `center` to the border of an ellipse with radius `x-radius`, `y-radiu`
/// at angle `angle`. If multiple intersection points are found, the
/// one with the largest distance from `center` is returned.
///
/// Returns `none` if no intersection is found for better error reporting.
///
/// - center (vector): The position from which to start the test line.
/// - x-dist (number): The furthest distance the test line should go in the x direction.
/// - y-dist (number): The furthest distance the test line should go in the y direction.
/// - drawables (drawables): Drawables to test for an intersection against. Ideally should be of type path but all others are ignored.
/// - x-radius (number): The furthest distance the test line should go in the x direction.
/// - y-radius (number): The furthest distance the test line should go in the y direction.
/// - angle (angle): The angle to check for a border anchor at.
/// - drawables (drawables): Drawables to test for an intersection against. Ideally should be of type path but all others are ignored.
/// -> none
/// -> vector
#let shape-border(center, x-dist, y-dist, drawables, angle) = {
x-dist += util.float-epsilon
y-dist += util.float-epsilon

#let _shape-border(center, x-dist, y-dist, angle, drawables) = {
let eps = util.float-epsilon
if type(drawables) == dictionary {
drawables = (drawables,)
}

let (cx, cy, cz) = center
let test-line = (
center,
(
center.at(0) + calc.abs(x-dist) * calc.cos(angle),
center.at(1) + calc.abs(y-dist) * calc.sin(angle),
center.at(2),
)
(cx + calc.abs(x-dist + eps) * calc.cos(angle),
cy + calc.abs(y-dist + eps) * calc.sin(angle),
cz)
)

let pts = ()
for drawable in drawables {
if drawable.type != "path" {
continue
}
pts += intersection.line-path(..test-line, drawable)
}

Expand All @@ -73,7 +74,6 @@
}
}


/// 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.
Expand Down Expand Up @@ -113,13 +113,17 @@
callback = (anchor) => {}
}

if type(radii) != array {
radii = (radii, radii)
}

// Add enabled anchor names
if name != none or offset-anchor != none {
if border-anchors {
if border-anchors and border-anchor-callback == none {
assert("center" in anchor-names and radii != none and path != none,
message: "Border anchors need a center anchor, radii and the path set!")
}
if path-anchors {
if path-anchors and path-anchor-callback == none {
assert.ne(path, none,
message: "Path anchors need the path set!")
}
Expand Down Expand Up @@ -193,7 +197,7 @@
out = if border-anchor-callback != none {
border-anchor-callback(callback("center"), anchor)
} else {
shape-border(callback("center"), ..radii, path, anchor)
_shape-border(callback("center"), ..radii, anchor, path)
}
if out == none {
panic(strfmt("Element '{}' does not have a border for anchor '{}'.", name, anchor))
Expand Down
Loading
Loading