diff --git a/src/plot/line.typ b/src/plot/line.typ index 12b5c6c..70cb113 100644 --- a/src/plot/line.typ +++ b/src/plot/line.typ @@ -217,7 +217,7 @@ mark-style: (:), samples: 50, sample-at: (), - line: "linear", + line: "raw", axes: ("x", "y"), label: none, data diff --git a/src/plot/util.typ b/src/plot/util.typ index bff54fc..8ec6dcf 100644 --- a/src/plot/util.typ +++ b/src/plot/util.typ @@ -6,165 +6,114 @@ /// - points (array): Array of vectors representing a line-strip /// - low (vector): Lower clip-window coordinate /// - high (vector): Upper clip-window coordinate +/// - fill (bool): Return fillable shapes +/// - generate-edge-points (bool): Generate interpolated points on clipped edges /// -> array List of line-strips representing the paths insides the clip-window -#let clipped-paths(points, low, high, fill: false) = { +#let clipped-paths-rect(points, low, high, fill: false, generate-edge-points: false) = { let (min-x, max-x) = (calc.min(low.at(0), high.at(0)), calc.max(low.at(0), high.at(0))) let (min-y, max-y) = (calc.min(low.at(1), high.at(1)), calc.max(low.at(1), high.at(1))) - let in-rect(pt) = { - return (pt.at(0) >= min-x and pt.at(0) <= max-x and - pt.at(1) >= min-y and pt.at(1) <= max-y) + let in-rect((x, y)) = { + return (x >= min-x and x <= max-x and + y >= min-y and y <= max-y) } - let interpolated-end(a, b) = { - if in-rect(a) and in-rect(b) { - return b - } - - let (x1, y1, ..) = a - let (x2, y2, ..) = b - - if x2 - x1 == 0 { - return (x2, calc.min(max-y, calc.max(y2, min-y))) - } - - if y2 - y1 == 0 { - return (calc.min(max-x, calc.max(x2, min-x)), y2) - } - - let m = (y2 - y1) / (x2 - x1) - let n = y2 - m * x2 - - let x = x2 - let y = y2 + let edges = ( + ((min-x, min-y), (min-x, max-y)), + ((max-x, min-y), (max-x, max-y)), + ((min-x, min-y), (max-x, min-y)), + ((min-x, max-y), (max-x, max-y)), + ) - y = calc.min(max-y, calc.max(y, min-y)) - x = (y - n) / m - - x = calc.min(max-x, calc.max(x, min-x)) - y = m * x + n - - return (x, y) - } - - // Append path to paths and return paths - // - // If path starts or ends with a vector of another part, merge those - // paths instead appending path as a new path. - let append-path(paths, path) = { - if path.len() <= 1 { - return paths - } - - let cmp(a, b) = { - return a.map(calc.round.with(digits: 8)) == b.map(calc.round.with(digits: 8)) - } - - let added = false - for i in range(0, paths.len()) { - let p = paths.at(i) - if cmp(p.first(), path.last()) { - paths.at(i) = path + p - added = true - } else if cmp(p.first(), path.first()) { - paths.at(i) = path.rev() + p - added = true - } else if cmp(p.last(), path.first()) { - paths.at(i) = p + path - added = true - } else if cmp(p.last(), path.last()) { - paths.at(i) = p + path.rev() - added = true + let interpolated-end(a, b) = { + let pts = () + for (edge-a, edge-b) in edges { + let pt = cetz.intersection.line-line(a, b, edge-a, edge-b) + if pt != none { + pts.push(pt) } - if added { break } } - - if not added { - paths.push(path) - } - return paths + return pts } - let clamped-pt(pt) = { - return (calc.max(min-x, calc.min(pt.at(0), max-x)), - calc.max(min-y, calc.min(pt.at(1), max-y))) - } + // Find lines crossing the rect bounds + // by storing all crossings as tuples (, , ) + let crossings = () - let paths = () - - let path = () - let prev = points.at(0) - let was-inside = in-rect(prev) + // Push a pseudo entry for the last point, if it is insides the bounds. + let was-inside = in-rect(points.at(0)) if was-inside { - path.push(prev) - } else if fill { - path.push(clamped-pt(prev)) + crossings.push((0, true, points.first())) } + // Find crossings and compute intersection points. for i in range(1, points.len()) { - let prev = points.at(i - 1) - let pt = points.at(i) - - let is-inside = in-rect(pt) - - let (x1, y1) = prev - let (x2, y2) = pt - - // Ignore lines if both ends are outsides the x-window and on the - // same side. - if (x1 < min-x and x2 < min-x) or (x1 > max-x and x2 > max-x) { - if fill { - let clamped = clamped-pt(pt) - if path.last() != clamped { - path.push(clamped) + let current-inside = in-rect(points.at(i)) + if current-inside != was-inside { + crossings.push(( + i, + current-inside, + interpolated-end(points.at(i - 1), points.at(i)).first())) + was-inside = current-inside + } else if not current-inside { + let (px, py) = points.at(i - 1) + let (cx, cy) = points.at(i) + let (lo-x, hi-x) = (calc.min(px, cx), calc.max(px, cx)) + let (lo-y, hi-y) = (calc.min(py, cy), calc.max(py, cy)) + + let x-differs = (lo-x < min-x and hi-x > max-x) or (lo-x < max-x and hi-x > max-x) + let y-differs = (lo-y < min-y and hi-y > max-y) or (lo-y < max-y and hi-y > max-y) + if x-differs or y-differs { + for pt in interpolated-end(points.at(i - 1), points.at(i)) { + crossings.push((i, not current-inside, pt)) + current-inside = not current-inside } } - was-inside = false - continue } + } - if is-inside { - if was-inside { - path.push(pt) - } else { - path.push(interpolated-end(pt, prev)) - path.push(pt) - } - } else { - if was-inside { - path.push(interpolated-end(prev, pt)) - } else { - let (a, b) = (interpolated-end(pt, prev), - interpolated-end(prev, pt)) - if in-rect(a) and in-rect(b) { - path.push(a) - path.push(b) - } else if fill { - let clamped = clamped-pt(pt) - if path.last() != clamped { - path.push(clamped) - } + // Push a pseudo entry for the last point, if it is insides the bounds. + if in-rect(points.last()) and crossings.last().at(1) { + crossings.push((points.len() - 1, false, points.last())) + } + + // Generate paths + let paths = () + for i in range(1, crossings.len()) { + let (a-index, a-dir, a-pt) = crossings.at(i - 1) + let (b-index, b-dir, b-pt) = crossings.at(i) + + if a-dir { + let path = () + + // If we must generate edge points, take the previous crossing + // as source point and interpolate between that and the current one. + if generate-edge-points and i > 2 { + let (c-index, c-dir, c-pt) = crossings.at(i - 2) + + let n = a-index - c-index + if n > 1 { + path += range(0, n).map(t => { + cetz.vector.lerp(c-pt, a-pt, t / (n - 1)) + }) } } - if path.len() > 0 and not fill { - paths = append-path(paths, path) - path = () - } - } - - was-inside = is-inside - } + // Append the path insides the bounds + path.push(a-pt) + path += points.slice(a-index, b-index) + path.push(b-pt) - // Append clamped last point if filling - if fill and not in-rect(points.last()) { - path.push(clamped-pt(points.last())) - } + // Insert the last end point to connect + // to a filled area. + if fill and paths.len() > 0 { + path.insert(0, paths.last().last()) + } - if path.len() > 1 { - paths = append-path(paths, path) + paths.push(path) + } } return paths @@ -176,19 +125,14 @@ /// - low (vector): Lower clip-window coordinate /// - high (vector): Upper clip-window coordinate /// -> array List of stroke paths -#let compute-stroke-paths(points, low, high) = { - clipped-paths(points, low, high, fill: false) -} - +#let compute-stroke-paths = clipped-paths-rect.with(fill: false) /// Compute clipped fill path /// /// - points (array): X/Y data points /// - low (vector): Lower clip-window coordinate /// - high (vector): Upper clip-window coordinate /// -> array List of fill paths -#let compute-fill-paths(points, low, high) = { - clipped-paths(points, low, high, fill: true) -} +#let compute-fill-paths = clipped-paths-rect.with(fill: true) /// Return points of a sampled catmull-rom through the /// input points. diff --git a/tests/plot/legend/ref/1.png b/tests/plot/legend/ref/1.png index cc29e58..a0d3893 100644 Binary files a/tests/plot/legend/ref/1.png and b/tests/plot/legend/ref/1.png differ diff --git a/tests/plot/ref/1.png b/tests/plot/ref/1.png index f3e3a1c..da89124 100644 Binary files a/tests/plot/ref/1.png and b/tests/plot/ref/1.png differ