Offsetting a polyline in one direction part II

162 Views Asked by At

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

0

There are 0 best solutions below