This is a follow-up question from: Offsetting a polyline in one direction.
Essentially, I still want to offset a polyline in one direction, but, since asking the original question, I have become more fussy about particular details.
In particular, I'm looking for the following properties of the offsetting:
- I only want to offset to one side. Which one shouldn't matter as we should be able to flip the sign on the distance.
- Allow a self-intersecting polyline to be offset, without merging crossover points.
- Corners should be flush, i.e. no wierd spikes. What would be even more perfect is if we can specify the linejoin style.
- I'd like the preserve the order. I.e. I want my first and last xy-coordinate to correspond to the the xy-coordinate of the offset line. The points in the middle don't need equivalents.
Here is what I tried:
# Make self-intersecting shape with sharp corners
t <- seq(0, 2 * pi, length.out = 360)
x <- c(0.25, sin(t) + seq(0, 2, length.out = 360), 1.75)
y <- c(2, cos(t), 2)
plot(x, y, type = 'l')
# Weird spikes, merges crossover point
plot(sf::st_buffer(
sf::st_linestring(cbind(x, y)),
dist = 0.1, singleSide = TRUE
))
lines(x, y, col = 2)
# Does exactly what I want for shapes that don't self-intersect.
# Messes up with self-intersections though
plot(geos::geos_offset_curve(
geos::geos_make_linestring(x, y),
distance = 0.1
))
lines(x, y, col = 2)
# No weird spikes, flush corners, but merges crossover point and doesn't preserve
# the order very well
pc <- polyclip::polylineoffset(list(x = x, y = y), 0.1, endtype = "openbutt")
plot(x, y, type = 'l', col = 2)
for (i in pc) {
lines(i)
}
# Does almost exactly what I want, but unfortunately has
# spikes in the corner
plot(geomtextpath:::.get_offset(x, y, d = 0.1), type = 'l')
lines(x, y, col = 2)
Created on 2022-11-05 by the reprex package (v2.0.1)
In addition, I've tried a few homebrew variations of geomtextpath::.get_offset()
, where I got e.g. rounded linejoins to work, but I get stuck on these spikes in the corners.
Work in progress
This is my current function:
offset_round <- function(x, y, dist, min_arc = 0.1) {
start <- 1
end <- length(x)
se <- c(start, end)
theta <- atan2(diff(y), diff(x)) + pi / 2
# Fill in angle for first and last points
before <- c(NA, theta)
after <- c(theta, NA)
before[start] <- before[start + 1]
after[end] <- after[end - 1]
# Calculate bisector and associated length
bisector <- (before + after) / 2
bi_length <- dist / cos(bisector - after)
# Difference in angles to calculate number of segments
delta <- (after - before) %% (2 * pi)
n_segs <- if (sign(dist) == 1) {
pmax(min_arc, delta - pi) %/% min_arc
} else {
pmin(-min_arc, delta - pi) %/% -min_arc
}
n_segs[se] <- 1
# Expand for number of segments
idx <- rep.int(seq_along(n_segs), n_segs)
xnew <- x[idx]
ynew <- y[idx]
# Calculate angles for rounded corners
new_delta <- if (sign(dist) == 1) delta - 2 * pi else delta
angle <- unlist(
Map(seq, 0, new_delta, length.out = n_segs)
) + before[idx]
# Choose bisector angle for 1-segment corners
singles <- n_segs == 1
angle[singles[idx]] <- bisector[singles]
# Ditto set appropriate lengths
dist <- rep_len(dist, end)
len <- dist[idx]
len[singles[idx]] <- bi_length[singles]
# Apply transformation
xnew <- xnew + cos(angle) * len
ynew <- ynew + sin(angle) * len
list(
x = xnew,
y = ynew
)
}
Which gives:
t <- seq(0, 2 * pi, length.out = 360)
x <- c(0.25, sin(t) + seq(0, 2, length.out = 360), 1.75)
y <- c(2, cos(t), 2)
plot(x, y, type = 'l', xlim = c(-0.1, 2.1))
lines(offset_round(x, y, 0.1), col = 2)
lines(offset_round(x, y, -0.1), col = 3)
Created on 2022-11-05 by the reprex package (v2.0.1)
I'd like to know a way to remove the corner spikes in the plot above