|
14 | 14 | // with two arguments, the z value `z1` to compare against and |
15 | 15 | // the z value `z2` of the data and must return a boolean: `(z1, z2) => boolean`. |
16 | 16 | // - interpolate (bool): Enable cell interpolation for smoother lines |
| 17 | +// - connect-domain (bool): Treat the sample domain boundary as an exterior edge. |
| 18 | +// This closes regions against the plot boundary, which is useful for fills but |
| 19 | +// not for contour lines. |
17 | 20 | // - contour-limit (int): Contour limit after which the algorithm panics |
18 | 21 | // -> array: Array of contour point arrays |
19 | | -#let find-contours(data, offset, op: auto, interpolate: true, contour-limit: 50) = { |
| 22 | +#let find-contours(data, |
| 23 | + offset, |
| 24 | + op: auto, |
| 25 | + interpolate: true, |
| 26 | + connect-domain: false, |
| 27 | + contour-limit: 50) = { |
20 | 28 | assert(data != none and type(data) == array, |
21 | 29 | message: "Data must be of type array") |
22 | 30 | assert(type(offset) in (int, float), |
|
58 | 66 | // Build a binary map that has 0 for unset and 1 for set cells |
59 | 67 | let bin-data = data.map(r => r.map(is-set)) |
60 | 68 |
|
| 69 | + // Get case (0 to 15) |
| 70 | + let get-case(tl, tr, bl, br) = { |
| 71 | + int(tl) * 8 + int(tr) * 4 + int(br) * 2 + int(bl) |
| 72 | + } |
| 73 | + |
| 74 | + let lerp(a, b) = { |
| 75 | + if a == b { return a } |
| 76 | + else if a == none { return 1 } |
| 77 | + else if b == none { return 0 } |
| 78 | + return (offset - a) / (b - a) |
| 79 | + } |
| 80 | + |
| 81 | + let segments = () |
| 82 | + let x-range = if connect-domain { range(-1, n-cols) } else { range(0, n-cols - 1) } |
| 83 | + let y-range = if connect-domain { range(-1, n-rows) } else { range(0, n-rows - 1) } |
| 84 | + |
61 | 85 | // Get binary data at x, y |
62 | 86 | let get-bin(x, y) = { |
63 | 87 | if x >= 0 and x < n-cols and y >= 0 and y < n-rows { |
|
74 | 98 | return none |
75 | 99 | } |
76 | 100 |
|
77 | | - // Get case (0 to 15) |
78 | | - let get-case(tl, tr, bl, br) = { |
79 | | - int(tl) * 8 + int(tr) * 4 + int(br) * 2 + int(bl) |
80 | | - } |
81 | | - |
82 | | - let lerp(a, b) = { |
83 | | - if a == b { return a } |
84 | | - else if a == none { return 1 } |
85 | | - else if b == none { return 0 } |
86 | | - return (offset - a) / (b - a) |
87 | | - } |
88 | | - |
89 | | - // List of all found contours |
90 | | - let contours = () |
91 | | - |
92 | | - let segments = () |
93 | | - for y in range(-1, n-rows) { |
94 | | - for x in range(-1, n-cols) { |
| 101 | + for y in y-range { |
| 102 | + for x in x-range { |
95 | 103 | let tl = get-bin(x, y) |
96 | | - let tr = get-bin(x+1, y) |
97 | | - let bl = get-bin(x, y+1) |
98 | | - let br = get-bin(x+1, y+1) |
| 104 | + let tr = get-bin(x + 1, y) |
| 105 | + let bl = get-bin(x, y + 1) |
| 106 | + let br = get-bin(x + 1, y + 1) |
99 | 107 |
|
100 | 108 | // Corner data |
101 | 109 | // |
|
105 | 113 | // | | |
106 | 114 | // sw-----se |
107 | 115 | let nw = get-data(x, y) |
108 | | - let ne = get-data(x+1, y) |
109 | | - let se = get-data(x+1, y+1) |
110 | | - let sw = get-data(x, y+1) |
| 116 | + let ne = get-data(x + 1, y) |
| 117 | + let se = get-data(x + 1, y + 1) |
| 118 | + let sw = get-data(x, y + 1) |
111 | 119 |
|
112 | 120 | // Interpolated edge points |
113 | 121 | // |
|
204 | 212 | return contours |
205 | 213 | } |
206 | 214 |
|
| 215 | +// Convert contour data points from sample-space to plot coordinates. |
| 216 | +#let _scale-contours(contours, x-min, y-min, dx, dy, z) = { |
| 217 | + contours.map(contour => ( |
| 218 | + z: z, |
| 219 | + line-data: contour.map(pt => ( |
| 220 | + pt.at(0) * dx + x-min, |
| 221 | + pt.at(1) * dy + y-min, |
| 222 | + )), |
| 223 | + )) |
| 224 | +} |
| 225 | + |
207 | 226 | // Prepare line data |
208 | 227 | #let _prepare(self, ctx) = { |
209 | 228 | let (x, y) = (ctx.x, ctx.y) |
210 | 229 |
|
211 | | - self.contours = self.contours.map(c => { |
| 230 | + self.stroke-contours = self.stroke-contours.map(c => { |
212 | 231 | c.stroke-paths = util.compute-stroke-paths(c.line-data, x, y) |
213 | | - |
214 | | - if self.fill { |
215 | | - c.fill-paths = util.compute-fill-paths(c.line-data, x, y) |
216 | | - } |
217 | 232 | return c |
218 | 233 | }) |
219 | 234 |
|
| 235 | + if self.fill { |
| 236 | + self.fill-contours = self.fill-contours.map(c => { |
| 237 | + c.fill-paths = util.compute-fill-paths(c.line-data, x, y) |
| 238 | + return c |
| 239 | + }) |
| 240 | + } |
| 241 | + |
220 | 242 | return self |
221 | 243 | } |
222 | 244 |
|
223 | 245 | // Stroke line data |
224 | 246 | #let _stroke(self, ctx) = { |
225 | | - for c in self.contours { |
| 247 | + for c in self.stroke-contours { |
226 | 248 | for p in c.stroke-paths { |
227 | 249 | draw.line(..p, fill: none, close: p.first() == p.last()) |
228 | 250 | } |
|
232 | 254 | // Fill line data |
233 | 255 | #let _fill(self, ctx) = { |
234 | 256 | if not self.fill { return } |
235 | | - for c in self.contours { |
| 257 | + for c in self.fill-contours { |
236 | 258 | for p in c.fill-paths { |
237 | 259 | draw.line(..p, stroke: none, close: p.first() == p.last()) |
238 | 260 | } |
|
310 | 332 | let (y-min, y-max) = y-domain |
311 | 333 | let dy = (y-max - y-min) / (data.len() - 1) |
312 | 334 |
|
313 | | - let contours = () |
| 335 | + let stroke-contours = () |
| 336 | + let fill-contours = () |
314 | 337 | let z = if type(z) == array { z } else { (z,) } |
315 | 338 | for z in z { |
316 | | - for contour in find-contours(data, z, op: op, interpolate: interpolate, contour-limit: limit) { |
317 | | - let line-data = contour.map(pt => { |
318 | | - (pt.at(0) * dx + x-min, |
319 | | - pt.at(1) * dy + y-min) |
320 | | - }) |
321 | | - |
322 | | - contours.push(( |
323 | | - z: z, |
324 | | - line-data: line-data, |
325 | | - )) |
| 339 | + stroke-contours += _scale-contours( |
| 340 | + find-contours(data, z, op: op, interpolate: interpolate, contour-limit: limit), |
| 341 | + x-min, y-min, dx, dy, z) |
| 342 | + |
| 343 | + if fill { |
| 344 | + fill-contours += _scale-contours( |
| 345 | + find-contours(data, z, op: op, interpolate: interpolate, connect-domain: true, contour-limit: limit), |
| 346 | + x-min, y-min, dx, dy, z) |
326 | 347 | } |
327 | 348 | } |
328 | 349 |
|
329 | 350 | return (( |
330 | 351 | type: "contour", |
331 | 352 | label: label, |
332 | | - contours: contours, |
| 353 | + stroke-contours: stroke-contours, |
| 354 | + fill-contours: fill-contours, |
333 | 355 | axes: axes, |
334 | 356 | x-domain: x-domain, |
335 | 357 | y-domain: y-domain, |
|
0 commit comments