2

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

teunbrand
  • 33,645
  • 4
  • 37
  • 63
  • Sorry that was an error on my part, it was a shortcut for `unlist(..., recursive = FALSE, use.names = FALSE)`, but `unlist()` works just as well. – teunbrand Nov 06 '22 at 07:39
  • You're injecting your spikes in the creation of `x`, with 0.25: some seq: 1.75, per requirement 4 above, which breaks the notion of equidistant that an offset implies and might be hampering your efforts. – Chris Nov 06 '22 at 15:10
  • I'm not sure I understand. The input doesn't have any spikes at the corners. The first spike on the left appears because the curved bit after the straight line is so densely spaced that the bisector at the point after the corner goes up into the buffer for the first line. I haven't figured out how to automatically detect that and then decide which parts to adjust. – teunbrand Nov 06 '22 at 16:49
  • plot x... and ask, is that a spike? – Chris Nov 06 '22 at 17:06
  • They're regular corners that you could expect in any sort of polyline. Perhaps I wasn't entirely clear with what I mean with spikes. In the last picture, you can see in the red line near the left- and rightmost positions that there is some weird folding going on. Also in the first picture there are smaller spikes near the same corners in the black polygon. – teunbrand Nov 06 '22 at 17:55
  • It appears a test ( presumably on `+/-` sign ) is needed for 'inside' in 'near polygon' settings, as to how much to 'shorten' inside offsets, which can be seen plotting x[2:361], y[2:361], that will encircle (complete the polygon) on lines -0.1, col = 3. – Chris Nov 06 '22 at 20:42

0 Answers0