Skip to content

Commit

Permalink
Move clipping logic to axis style
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesrswift committed Aug 1, 2024
1 parent 8e9f87f commit 648bccc
Show file tree
Hide file tree
Showing 6 changed files with 401 additions and 199 deletions.
184 changes: 184 additions & 0 deletions src/plot/axis-styles/orthorect-2d/clipper.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/// Clip line-strip in rect
///
/// - points (array): Array of vectors representing a line-strip
/// - low (vector): Lower clip-window coordinate
/// - high (vector): Upper clip-window coordinate
/// -> array List of line-strips representing the paths insides the clip-window
#let clipped-paths-rect(points, ctx, fill: false) = {
let (low, high) = ctx.clip
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 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

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
}
if added { break }
}

if not added {
paths.push(path)
}
return paths
}

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)))
}

let paths = ()

let path = ()
let prev = points.at(0)
let was-inside = in-rect(prev)
if was-inside {
path.push(prev)
} else if fill {
path.push(clamped-pt(prev))
}

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)
}
}
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)
}
}
}

if path.len() > 0 and not fill {
paths = append-path(paths, path)
path = ()
}
}

was-inside = is-inside
}

// Append clamped last point if filling
if fill and not in-rect(points.last()) {
path.push(clamped-pt(points.last()))
}

if path.len() > 1 {
paths = append-path(paths, path)
}

return paths
}

/// Compute clipped stroke paths
///
/// - points (array): X/Y data points
/// - low (vector): Lower clip-window coordinate
/// - high (vector): Upper clip-window coordinate
/// -> array List of stroke paths
#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 = clipped-paths-rect.with(fill: true)
11 changes: 10 additions & 1 deletion src/plot/axis-styles/orthorect-2d/orthorect-2d.typ
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#import "grid.typ"
#import "axis.typ": draw-axis-line, inset-axis-points, place-ticks-on-line
#import "transforms.typ": data-viewport, axis-viewport,
#import "clipper.typ"

#let default-style-orthorect-2d = util.merge-dictionary(
default-style,
Expand All @@ -31,7 +32,15 @@
(x-scale, y-scale) = (y-scale, x-scale)
}

return (x: x, y: y, size: size, x-scale: x-scale, y-scale: y-scale)
return (
axes: (x,y),
size: size,
x-scale: x-scale,
y-scale: y-scale,
clip: ((x.min, y.min), (x.max, y.max)),
compute-stroke-paths: clipper.compute-stroke-paths,
compute-fill-paths: clipper.compute-fill-paths
)
}

#let draw-axes(
Expand Down
187 changes: 187 additions & 0 deletions src/plot/axis-styles/polar-2d/clipper.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
#import "/src/cetz.typ"
#import cetz.util: bezier

/// Clip line-strip in rect
///
/// - points (array): Array of vectors representing a line-strip
/// - low (vector): Lower clip-window coordinate
/// - high (vector): Upper clip-window coordinate
/// -> array List of line-strips representing the paths insides the clip-window
#let clipped-paths-circle(points, ctx, fill: false) = {
let (low, high) = ctx.clip
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 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

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
}
if added { break }
}

if not added {
paths.push(path)
}
return paths
}

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)))
}

let paths = ()

let path = ()
let prev = points.at(0)
let was-inside = in-rect(prev)
if was-inside {
path.push(prev)
} else if fill {
path.push(clamped-pt(prev))
}

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)
}
}
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)
}
}
}

if path.len() > 0 and not fill {
paths = append-path(paths, path)
path = ()
}
}

was-inside = is-inside
}

// Append clamped last point if filling
if fill and not in-rect(points.last()) {
path.push(clamped-pt(points.last()))
}

if path.len() > 1 {
paths = append-path(paths, path)
}

return paths
}

/// Compute clipped stroke paths
///
/// - points (array): X/Y data points
/// - low (vector): Lower clip-window coordinate
/// - high (vector): Upper clip-window coordinate
/// -> array List of stroke paths
#let compute-stroke-paths = clipped-paths-circle.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 = clipped-paths-circle.with(fill: true)
Loading

0 comments on commit 648bccc

Please sign in to comment.