Skip to content

Commit

Permalink
bezier: Improve cubic-t-for-distance (#338)
Browse files Browse the repository at this point in the history
Fixes a _major_ precision bug in `cubic-t-for-distance` and
`cubic-shorten` leading to bad mark/anchor placement on Bézier paths.

Also uses correct distance based placement for `point-on-path` if Bézier
curves are involved!
  • Loading branch information
johannes-wolf authored Nov 22, 2023
1 parent afea337 commit b5c674e
Show file tree
Hide file tree
Showing 8 changed files with 36 additions and 67 deletions.
64 changes: 24 additions & 40 deletions src/bezier.typ
Original file line number Diff line number Diff line change
Expand Up @@ -291,27 +291,31 @@
/// start s, if d is negative, it starts form the curves end
/// e.
/// -> float Bezier t value from [0,1]
#let cubic-t-for-distance(s, e, c1, c2, d, samples: 10) = {
#let cubic-t-for-distance(s, e, c1, c2, d, samples: 20) = {
let travel-forwards(s, e, c1, c2, d) = {
let sum = 0
for n in range(1, samples + 1) {
let t0 = (n - 1) / samples
let t1 = n / samples

let segment-dist = vector.dist(cubic-point(s, e, c1, c2, t0),
cubic-point(s, e, c1, c2, t1))
if sum <= d and d <= sum + segment-dist {
return t0 + (d - sum) / segment-dist / samples
}
sum += segment-dist
}
return 1
}

if d == 0 {
return 0
}

if d > 0 {
let travel = 0 // Distance traveled along the curve
let last = s
for t in range(1, samples + 1) {
let t = t / samples
let curr = cubic-point(s, e, c1, c2, t)
let dist = vector.dist(last, curr)
travel += dist
if travel >= d {
return t - 1/samples + d / (travel * samples)
}
last = curr
}
return 1
return travel-forwards(s, e, c1, c2, d)
} else {
return 1 - cubic-t-for-distance(e, s, c2, c1, -d, samples: samples)
return 1 - travel-forwards(e, s, c2, c1, -d)
}
}

Expand All @@ -330,34 +334,14 @@
/// - samples (int): Maximum of samples/steps to use
/// -> (s, e, c1, c2) Shortened curve
#let cubic-shorten(s, e, c1, c2, d, samples: 15) = {
if d == 0 {
return (s, e, c1, c2)
}
if d == 0 { return (s, e, c1, c2) }

let split-t = 0
if d > 0 {
let travel = 0 // Distance traveled along the curve

let last = s
for t in range(1, samples + 1) {
let t = t / samples
let curr = cubic-point(s, e, c1, c2, t)
let dist = vector.dist(last, curr)
travel += dist
if travel >= d {
split-t = t - 1/samples + d / (travel * samples)
break
}
last = curr
}
let (left, right) = split(s, e, c1, c2, cubic-t-for-distance(s, e, c1, c2, d, samples: samples))
return if d > 0 {
right
} else {
// Run the algorithm from end to start by swapping the curve.
let (e, s, c2, c1) = cubic-shorten(e, s, c2, c1, -d, samples: samples)
return (s, e, c1, c2)
left
}

let (_, right) = split(s, e, c1, c2, split-t)
return right
}

/// Align curve points pts to the line start-end
Expand Down
39 changes: 12 additions & 27 deletions src/path-util.typ
Original file line number Diff line number Diff line change
Expand Up @@ -54,29 +54,18 @@
/// -> float: Length of the segment in canvas units
#let segment-length(s) = {
let samples = default-samples
let type = s.at(0)
let pts = ()
let (type, ..pts) = s
if type == "line" {
pts = s.slice(1)
let len = 0
for i in range(1, pts.len()) {
len += vector.len(vector.sub(pts.at(i - 1), pts.at(i)))
}
return len
} else if type == "cubic" {
let (a, b, c, d) = s.slice(1)
pts.push(a)
pts = range(1, samples).map(t =>
bezier.cubic-point(a, b, c, d, t / samples))
pts.push(b)
return bezier.cubic-arclen(..pts, samples: samples)
} else {
panic("Not implemented")
}

let l = 0

let pt = pts.at(0)
for i in range(1, pts.len()) {
l += vector.len(vector.sub(pts.at(i), pt))
pt = pts.at(i)
panic("Invalid segment: " + type)
}

return l
}

/// Find point at position on polyline segment
Expand All @@ -96,13 +85,9 @@
return s.at(1)
}

let dist = (a, b) => {
vector.len(vector.sub(b, a))
}

let traveled-length = 0
for i in range(2, s.len()) {
let part-length = dist(s.at(i - 1), s.at(i))
let part-length = vector.dist(s.at(i - 1), s.at(i))

if traveled-length / l <= t and (traveled-length + part-length) / l >= t {
let f = (t - traveled-length / l) / (part-length / l)
Expand All @@ -124,12 +109,12 @@
/// - t (float): Position (from 0 to 1)
/// -> vector: Position on segment
#let point-on-segment(s, t) = {
let type = s.at(0)
let (type, ..pts) = s
if type == "line" {
return point-on-polyline(s, t)
} else if type == "cubic" {
let (a, b, c, d) = s.slice(1)
return bezier.cubic-point(a, b, c, d, t)
let len = bezier.cubic-arclen(..pts) * calc.min(calc.max(0, t), 1)
return bezier.cubic-point(..pts, bezier.cubic-t-for-distance(..pts, len))
}
}

Expand Down
Binary file modified tests/anchor-on-path/ref.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/bezier-through/ref.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/bezier/shorten/ref.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/mark/auto-offset/ref.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/mark/multiple/ref.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/mark/place-marks/ref.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit b5c674e

Please sign in to comment.