paavohtl Claude Opus 4.6 (1M context) commited on
Commit
6f89295
·
1 Parent(s): 96b8a07

feat: port single-color DDA line rasterizer with snapshot tests

Browse files

Pure Rust port of 0x4F7500 (draw_line_clipped): Cohen-Sutherland
Fixed-point clipper + DDA rasterizer. All 11 snapshot tests pass
pixel-perfect against WA's native implementation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/display/line_draw.rs CHANGED
@@ -1,19 +1,18 @@
1
  //! Line-drawing algorithms for 8bpp BitGrid surfaces.
2
  //!
3
- //! Pure Rust ports of WA's thick-line drawing system (0x4F7500 / 0x4F7A60).
4
- //! The original draws 2-pixel-wide Bresenham-style lines with Fixed-point
5
- //! Cohen-Sutherland clipping.
6
  //!
7
- //! ## Architecture
 
 
 
8
  //!
9
  //! ```text
10
- //! draw_line_clipped / draw_line_two_color (outer dispatch)
11
  //! ├── Determine dominant axis (|dx| vs |dy|)
12
  //! ├── Sort endpoints in positive direction
13
  //! ├── clip_line() — Cohen-Sutherland, Fixed-point
14
- //! ├── Rasterizer (horizontal-major or vertical-major)
15
- //! │ └── put_pixel_clipped × 8 per step (outline)
16
- //! └── Fill rasterizer — 4 pixels per step (body color)
17
  //! ```
18
 
19
  use crate::fixed::Fixed;
@@ -66,6 +65,25 @@ impl PixelGrid {
66
  pub fn clear(&mut self) {
67
  self.data.fill(0);
68
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  }
70
 
71
  impl PixelWriter for PixelGrid {
@@ -102,153 +120,227 @@ impl PixelWriter for PixelGrid {
102
  // Fixed-point line clipping (port of 0x4F7150)
103
  // =========================================================================
104
 
105
- /// Cohen-Sutherland outcode bits for line clipping.
106
- const LEFT: u8 = 1;
107
- const RIGHT: u8 = 2;
108
- const TOP: u8 = 4;
109
- const BOTTOM: u8 = 8;
110
-
111
- /// Compute Cohen-Sutherland outcode for a Fixed-point coordinate.
112
- fn outcode(x: Fixed, y: Fixed, clip: &ClipRect) -> u8 {
113
- let mut code = 0u8;
114
- if x.to_raw() < clip.left.to_raw() {
115
- code |= LEFT;
116
- }
117
- if x.to_raw() > clip.right.to_raw() {
118
- code |= RIGHT;
119
- }
120
- if y.to_raw() < clip.top.to_raw() {
121
- code |= TOP;
122
- }
123
- if y.to_raw() > clip.bottom.to_raw() {
124
- code |= BOTTOM;
125
  }
126
- code
127
- }
128
-
129
- /// Clip rectangle in Fixed-point coordinates.
130
- struct ClipRect {
131
- left: Fixed,
132
- top: Fixed,
133
- right: Fixed,
134
- bottom: Fixed,
135
  }
136
 
137
- /// Fixed-point multiply then shift: `(a * b) >> 16`.
138
- ///
139
- /// Port of the pattern used throughout the WA line clipper.
140
  #[inline]
141
  fn fixed_mul_shift(a: i32, b: i32) -> i32 {
142
  ((a as i64 * b as i64) >> 16) as i32
143
  }
144
 
145
- /// Clip a line segment to the clip rectangle.
146
  ///
147
- /// Port of 0x4F7150. Returns true if the clipped line is visible.
148
- /// Modifies coordinates in-place.
149
  ///
150
- /// The clip rectangle is read from the PixelWriter's bounds, scaled to Fixed.
151
  fn clip_line(
152
- x1: &mut Fixed,
153
- y1: &mut Fixed,
154
- x2: &mut Fixed,
155
- y2: &mut Fixed,
156
  writer: &dyn PixelWriter,
157
  ) -> bool {
158
- let clip = ClipRect {
159
- left: Fixed::from_int(writer.clip_left()),
160
- top: Fixed::from_int(writer.clip_top()),
161
- right: Fixed::from_int(writer.clip_right()),
162
- bottom: Fixed::from_int(writer.clip_bottom()),
163
- };
164
 
165
- let code1 = outcode(*x1, *y1, &clip);
166
- let code2 = outcode(*x2, *y2, &clip);
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
- // Trivial reject: both endpoints on same side
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  if code1 & code2 != 0 {
170
  return false;
171
  }
172
 
173
- // Clip x1 against left/right bounds
174
- if x1.to_raw() < clip.left.to_raw() {
175
- let ratio = fixed_div(clip.left.to_raw() - x1.to_raw(), x2.to_raw() - x1.to_raw());
176
- y1.0 += fixed_mul_shift(y2.to_raw() - y1.to_raw(), ratio);
177
- *x1 = clip.left;
178
- } else if x1.to_raw() > clip.right.to_raw() {
179
- let ratio = fixed_div(clip.right.to_raw() - x1.to_raw(), x2.to_raw() - x1.to_raw());
180
- y1.0 += fixed_mul_shift(y2.to_raw() - y1.to_raw(), ratio);
181
- *x1 = clip.right;
182
  }
183
 
184
- // Clip x2 against left/right bounds
185
- if x2.to_raw() < clip.left.to_raw() {
186
- let ratio = fixed_div(clip.left.to_raw() - x2.to_raw(), x1.to_raw() - x2.to_raw());
187
- y2.0 += fixed_mul_shift(y1.to_raw() - y2.to_raw(), ratio);
188
- *x2 = clip.left;
189
- } else if x2.to_raw() > clip.right.to_raw() {
190
- let ratio = fixed_div(clip.right.to_raw() - x2.to_raw(), x1.to_raw() - x2.to_raw());
191
- y2.0 += fixed_mul_shift(y1.to_raw() - y2.to_raw(), ratio);
192
- *x2 = clip.right;
193
  }
194
 
195
- // Check y bounds after x clipping
196
- if y1.to_raw() > clip.bottom.to_raw() || y2.to_raw() > clip.bottom.to_raw() {
 
 
 
197
  return false;
198
  }
199
- if y1.to_raw() < clip.top.to_raw() && y2.to_raw() < clip.top.to_raw() {
200
  return false;
201
  }
202
 
203
- // Clip y1 against top/bottom bounds
204
- if y1.to_raw() < clip.top.to_raw() {
205
- let ratio = fixed_div(clip.top.to_raw() - y1.to_raw(), y2.to_raw() - y1.to_raw());
206
- x1.0 += fixed_mul_shift(x2.to_raw() - x1.to_raw(), ratio);
207
- *y1 = clip.top;
208
- } else if y1.to_raw() > clip.bottom.to_raw() {
209
- let ratio = fixed_div(
210
- clip.bottom.to_raw() - y1.to_raw(),
211
- y2.to_raw() - y1.to_raw(),
212
- );
213
- x1.0 += fixed_mul_shift(x2.to_raw() - x1.to_raw(), ratio);
214
- *y1 = clip.bottom;
215
- }
216
-
217
- // Clip y2 against top/bottom bounds
218
- if y2.to_raw() < clip.top.to_raw() {
219
- let ratio = fixed_div(clip.top.to_raw() - y2.to_raw(), y1.to_raw() - y2.to_raw());
220
- x2.0 += fixed_mul_shift(x1.to_raw() - x2.to_raw(), ratio);
221
- *y2 = clip.top;
222
- } else if y2.to_raw() > clip.bottom.to_raw() {
223
- let ratio = fixed_div(
224
- clip.bottom.to_raw() - y2.to_raw(),
225
- y1.to_raw() - y2.to_raw(),
226
- );
227
- x2.0 += fixed_mul_shift(x1.to_raw() - x2.to_raw(), ratio);
228
- *y2 = clip.bottom;
229
  }
230
 
231
- // Final check: both endpoints must be within bounds
232
- if (x1.to_raw() < clip.left.to_raw() || x1.to_raw() > clip.right.to_raw())
233
- && (x2.to_raw() < clip.left.to_raw() || x2.to_raw() > clip.right.to_raw())
234
- {
 
 
 
 
 
 
 
 
 
235
  return false;
236
  }
237
 
238
  true
239
  }
240
 
241
- /// Fixed-point divide used by the line clipper.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  ///
243
- /// Port of FUN_005b3501: computes `(numerator << 16) / denominator`.
244
- #[inline]
245
- fn fixed_div(numerator: i32, denominator: i32) -> i32 {
246
- if denominator == 0 {
247
- return 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  }
249
- (((numerator as i64) << 16) / denominator as i64) as i32
250
  }
251
 
 
 
 
 
252
  #[cfg(test)]
253
  mod tests {
254
  use super::*;
@@ -274,16 +366,171 @@ mod tests {
274
  grid.clip_right = 12;
275
  grid.clip_bottom = 12;
276
 
277
- // Inside clip rect — should write
278
  grid.put_pixel_clipped(4, 4, 1);
279
  assert_eq!(grid.data[4 * grid.row_stride as usize + 4], 1);
280
 
281
- // At clip boundary (exclusive) — should NOT write
282
  grid.put_pixel_clipped(12, 4, 2);
283
  assert_eq!(grid.data[4 * grid.row_stride as usize + 12], 0);
284
 
285
- // Outside clip rect — should NOT write
286
  grid.put_pixel_clipped(3, 4, 3);
287
  assert_eq!(grid.data[4 * grid.row_stride as usize + 3], 0);
288
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  }
 
1
  //! Line-drawing algorithms for 8bpp BitGrid surfaces.
2
  //!
3
+ //! Pure Rust ports of WA's line drawing system:
 
 
4
  //!
5
+ //! - `draw_line_clipped` (0x4F7500): Single-color DDA line with Cohen-Sutherland clipping.
6
+ //! - `draw_line_two_color` (0x4F7A60): Two-color thick (2px) line with clipping.
7
+ //!
8
+ //! ## Single-color architecture (0x4F7500)
9
  //!
10
  //! ```text
11
+ //! draw_line_clipped
12
  //! ├── Determine dominant axis (|dx| vs |dy|)
13
  //! ├── Sort endpoints in positive direction
14
  //! ├── clip_line() — Cohen-Sutherland, Fixed-point
15
+ //! └── DDA rasterizer — 1 pixel per step (horizontal or vertical major)
 
 
16
  //! ```
17
 
18
  use crate::fixed::Fixed;
 
65
  pub fn clear(&mut self) {
66
  self.data.fill(0);
67
  }
68
+
69
+ /// Load a snapshot file and return (header, pixel_data).
70
+ #[cfg(test)]
71
+ pub fn from_snapshot(bytes: &[u8]) -> Self {
72
+ let width = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
73
+ let height = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
74
+ let row_stride = u32::from_le_bytes(bytes[8..12].try_into().unwrap());
75
+ let data = bytes[12..].to_vec();
76
+ Self {
77
+ data,
78
+ width,
79
+ height,
80
+ row_stride,
81
+ clip_left: 0,
82
+ clip_top: 0,
83
+ clip_right: width,
84
+ clip_bottom: height,
85
+ }
86
+ }
87
  }
88
 
89
  impl PixelWriter for PixelGrid {
 
120
  // Fixed-point line clipping (port of 0x4F7150)
121
  // =========================================================================
122
 
123
+ /// Fixed-point divide: `(numerator << 16) / denominator`.
124
+ ///
125
+ /// Port of FUN_005b3501. Used by the line clipper for endpoint interpolation.
126
+ #[inline]
127
+ fn fixed_div(numerator: i32, denominator: i32) -> i32 {
128
+ if denominator == 0 {
129
+ return 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  }
131
+ (((numerator as i64) << 16) / denominator as i64) as i32
 
 
 
 
 
 
 
 
132
  }
133
 
134
+ /// Fixed-point multiply-shift: `(a * b) >> 16`.
 
 
135
  #[inline]
136
  fn fixed_mul_shift(a: i32, b: i32) -> i32 {
137
  ((a as i64 * b as i64) >> 16) as i32
138
  }
139
 
140
+ /// Clip a line segment to the writer's clip rectangle.
141
  ///
142
+ /// Port of 0x4F7150. All coordinates are Fixed-point (16.16).
143
+ /// Returns true if the clipped line is (partially) visible.
144
  ///
145
+ /// The clip rectangle is scaled from the writer's pixel bounds to Fixed.
146
  fn clip_line(
147
+ x1: &mut i32,
148
+ y1: &mut i32,
149
+ x2: &mut i32,
150
+ y2: &mut i32,
151
  writer: &dyn PixelWriter,
152
  ) -> bool {
153
+ let cl = writer.clip_left() << 16;
154
+ let ct = writer.clip_top() << 16;
155
+ let cr = writer.clip_right() << 16;
156
+ let cb = writer.clip_bottom() << 16;
 
 
157
 
158
+ // Cohen-Sutherland outcodes
159
+ let mut code1 = 0u8;
160
+ if *x1 < cl {
161
+ code1 |= 1;
162
+ }
163
+ if *x1 > cr {
164
+ code1 |= 2;
165
+ }
166
+ if *y1 < ct {
167
+ code1 |= 4;
168
+ }
169
+ if *y1 > cb {
170
+ code1 |= 8;
171
+ }
172
 
173
+ let mut code2 = 0u8;
174
+ if *x2 < cl {
175
+ code2 |= 1;
176
+ }
177
+ if *x2 > cr {
178
+ code2 |= 2;
179
+ }
180
+ if *y2 < ct {
181
+ code2 |= 4;
182
+ }
183
+ if *y2 > cb {
184
+ code2 |= 8;
185
+ }
186
+
187
+ // Trivial reject
188
  if code1 & code2 != 0 {
189
  return false;
190
  }
191
 
192
+ // Clip endpoint 1 against x bounds
193
+ if *x1 < cl {
194
+ let ratio = fixed_div(cl - *x1, *x2 - *x1);
195
+ *y1 += fixed_mul_shift(*y2 - *y1, ratio);
196
+ *x1 = cl;
197
+ } else if *x1 > cr {
198
+ let ratio = fixed_div(cr - *x1, *x2 - *x1);
199
+ *y1 += fixed_mul_shift(*y2 - *y1, ratio);
200
+ *x1 = cr;
201
  }
202
 
203
+ // Clip endpoint 2 against x bounds
204
+ if *x2 < cl {
205
+ let ratio = fixed_div(cl - *x2, *x1 - *x2);
206
+ *y2 += fixed_mul_shift(*y1 - *y2, ratio);
207
+ *x2 = cl;
208
+ } else if *x2 > cr {
209
+ let ratio = fixed_div(cr - *x2, *x1 - *x2);
210
+ *y2 += fixed_mul_shift(*y1 - *y2, ratio);
211
+ *x2 = cr;
212
  }
213
 
214
+ // Check y visibility after x clipping
215
+ if (*y1 > cb || *y2 > cb) && (*y1 < ct || *y2 < ct) {
216
+ // Spans both sides — skip (original logic)
217
+ }
218
+ if !(*y1 <= cb || *y2 <= cb) {
219
  return false;
220
  }
221
+ if !(*y1 >= ct || *y2 >= ct) {
222
  return false;
223
  }
224
 
225
+ // Clip endpoint 1 against y bounds
226
+ if *y1 < ct {
227
+ let ratio = fixed_div(ct - *y1, *y2 - *y1);
228
+ *x1 += fixed_mul_shift(*x2 - *x1, ratio);
229
+ *y1 = ct;
230
+ } else if *y1 > cb {
231
+ let ratio = fixed_div(cb - *y1, *y2 - *y1);
232
+ *x1 += fixed_mul_shift(*x2 - *x1, ratio);
233
+ *y1 = cb;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  }
235
 
236
+ // Clip endpoint 2 against y bounds
237
+ if *y2 < ct {
238
+ let ratio = fixed_div(ct - *y2, *y1 - *y2);
239
+ *x2 += fixed_mul_shift(*x1 - *x2, ratio);
240
+ *y2 = ct;
241
+ } else if *y2 > cb {
242
+ let ratio = fixed_div(cb - *y2, *y1 - *y2);
243
+ *x2 += fixed_mul_shift(*x1 - *x2, ratio);
244
+ *y2 = cb;
245
+ }
246
+
247
+ // Final visibility check
248
+ if (*x1 < cl || *x1 > cr) && (*x2 < cl || *x2 > cr) {
249
  return false;
250
  }
251
 
252
  true
253
  }
254
 
255
+ // =========================================================================
256
+ // Single-color DDA line rasterizer (port of 0x4F7500)
257
+ // =========================================================================
258
+
259
+ /// Horizontal-major DDA rasterizer (port of 0x4F7400).
260
+ ///
261
+ /// Iterates from x1 to x2 (pixel-rounded), computing y via Fixed-point slope.
262
+ fn raster_hmajor(writer: &mut dyn PixelWriter, x1: i32, y1: i32, x2: i32, y2: i32, color: u8) {
263
+ let start = (x1 & !0xFFFF) + 0x8000; // round to pixel center
264
+ let end = (x2 & !0xFFFF) + 0x8000;
265
+ if start == end {
266
+ return;
267
+ }
268
+
269
+ // slope = ((y2 - y1) << 16) / (x2 - x1), in 16.16 fixed
270
+ let slope = fixed_div(y2 - y1, x2 - x1);
271
+ let mut px = start >> 16;
272
+ let mut y = y1; // starts at raw y1, NOT adjusted for sub-pixel offset
273
+ let count = (end - start) >> 16;
274
+
275
+ for _ in 0..=count {
276
+ writer.put_pixel_clipped(px, (y + 0x8000) >> 16, color);
277
+ y += slope;
278
+ px += 1;
279
+ }
280
+ }
281
+
282
+ /// Vertical-major DDA rasterizer (port of 0x4F7480).
283
  ///
284
+ /// Iterates from y1 to y2 (pixel-rounded), computing x via Fixed-point slope.
285
+ fn raster_vmajor(writer: &mut dyn PixelWriter, x1: i32, y1: i32, x2: i32, y2: i32, color: u8) {
286
+ let start = (y1 & !0xFFFF) + 0x8000;
287
+ let end = (y2 & !0xFFFF) + 0x8000;
288
+ if start == end {
289
+ return;
290
+ }
291
+
292
+ // slope = ((x2 - x1) << 16) / (y2 - y1)
293
+ let slope = fixed_div(x2 - x1, y2 - y1);
294
+ let mut py = start >> 16;
295
+ let mut x = x1; // starts at raw x1
296
+ let count = (end - start) >> 16;
297
+
298
+ for _ in 0..=count {
299
+ writer.put_pixel_clipped((x + 0x8000) >> 16, py, color);
300
+ x += slope;
301
+ py += 1;
302
+ }
303
+ }
304
+
305
+ /// Draw a single-color clipped line. Port of 0x4F7500.
306
+ ///
307
+ /// All coordinates are Fixed-point (16.16).
308
+ pub fn draw_line_clipped(
309
+ writer: &mut dyn PixelWriter,
310
+ mut x1: i32,
311
+ mut y1: i32,
312
+ mut x2: i32,
313
+ mut y2: i32,
314
+ color: u8,
315
+ ) {
316
+ let abs_dx = (x2 - x1).unsigned_abs();
317
+ let abs_dy = (y2 - y1).unsigned_abs();
318
+
319
+ if abs_dx < abs_dy {
320
+ // Vertical-major: sort so y1 <= y2
321
+ if y1 > y2 {
322
+ core::mem::swap(&mut x1, &mut x2);
323
+ core::mem::swap(&mut y1, &mut y2);
324
+ }
325
+ if clip_line(&mut x1, &mut y1, &mut x2, &mut y2, writer) {
326
+ raster_vmajor(writer, x1, y1, x2, y2, color);
327
+ }
328
+ } else {
329
+ // Horizontal-major: sort so x1 <= x2
330
+ if x1 > x2 {
331
+ core::mem::swap(&mut x1, &mut x2);
332
+ core::mem::swap(&mut y1, &mut y2);
333
+ }
334
+ if clip_line(&mut x1, &mut y1, &mut x2, &mut y2, writer) {
335
+ raster_hmajor(writer, x1, y1, x2, y2, color);
336
+ }
337
  }
 
338
  }
339
 
340
+ // =========================================================================
341
+ // Tests
342
+ // =========================================================================
343
+
344
  #[cfg(test)]
345
  mod tests {
346
  use super::*;
 
366
  grid.clip_right = 12;
367
  grid.clip_bottom = 12;
368
 
 
369
  grid.put_pixel_clipped(4, 4, 1);
370
  assert_eq!(grid.data[4 * grid.row_stride as usize + 4], 1);
371
 
 
372
  grid.put_pixel_clipped(12, 4, 2);
373
  assert_eq!(grid.data[4 * grid.row_stride as usize + 12], 0);
374
 
 
375
  grid.put_pixel_clipped(3, 4, 3);
376
  assert_eq!(grid.data[4 * grid.row_stride as usize + 3], 0);
377
  }
378
+
379
+ fn f(x: i32) -> i32 {
380
+ x << 16
381
+ }
382
+
383
+ fn load_snapshot(name: &str) -> PixelGrid {
384
+ let path = format!(
385
+ "{}/../../testdata/snapshots/{}.bin",
386
+ env!("CARGO_MANIFEST_DIR"),
387
+ name
388
+ );
389
+ let bytes = std::fs::read(&path)
390
+ .unwrap_or_else(|e| panic!("Failed to load snapshot {}: {} (path: {})", name, e, path));
391
+ PixelGrid::from_snapshot(&bytes)
392
+ }
393
+
394
+ fn assert_matches_snapshot(actual: &PixelGrid, name: &str) {
395
+ let expected = load_snapshot(name);
396
+ assert_eq!(actual.width, expected.width, "{name}: width mismatch");
397
+ assert_eq!(actual.height, expected.height, "{name}: height mismatch");
398
+ assert_eq!(
399
+ actual.row_stride, expected.row_stride,
400
+ "{name}: stride mismatch"
401
+ );
402
+ if actual.data != expected.data {
403
+ // Find first differing pixel for a helpful error message
404
+ let mut diff_count = 0;
405
+ let mut first_diff = None;
406
+ for y in 0..actual.height {
407
+ for x in 0..actual.width {
408
+ let idx = (y * actual.row_stride + x) as usize;
409
+ if actual.data[idx] != expected.data[idx] {
410
+ diff_count += 1;
411
+ if first_diff.is_none() {
412
+ first_diff = Some((x, y, actual.data[idx], expected.data[idx]));
413
+ }
414
+ }
415
+ }
416
+ }
417
+ let (fx, fy, got, want) = first_diff.unwrap();
418
+ panic!(
419
+ "{name}: {diff_count} pixel(s) differ. First at ({fx},{fy}): got {got}, want {want}"
420
+ );
421
+ }
422
+ }
423
+
424
+ macro_rules! snapshot_test_clipped {
425
+ ($name:ident, $snap:expr, $x1:expr, $y1:expr, $x2:expr, $y2:expr, $color:expr) => {
426
+ #[test]
427
+ fn $name() {
428
+ let mut grid = PixelGrid::new(128, 128);
429
+ draw_line_clipped(&mut grid, $x1, $y1, $x2, $y2, $color);
430
+ assert_matches_snapshot(&grid, $snap);
431
+ }
432
+ };
433
+ }
434
+
435
+ snapshot_test_clipped!(
436
+ snap_clipped_horizontal,
437
+ "clipped_horizontal",
438
+ f(10),
439
+ f(64),
440
+ f(118),
441
+ f(64),
442
+ 1
443
+ );
444
+ snapshot_test_clipped!(
445
+ snap_clipped_vertical,
446
+ "clipped_vertical",
447
+ f(64),
448
+ f(10),
449
+ f(64),
450
+ f(118),
451
+ 2
452
+ );
453
+ snapshot_test_clipped!(
454
+ snap_clipped_diagonal_45,
455
+ "clipped_diagonal_45",
456
+ f(10),
457
+ f(10),
458
+ f(118),
459
+ f(118),
460
+ 3
461
+ );
462
+ snapshot_test_clipped!(
463
+ snap_clipped_diagonal_steep,
464
+ "clipped_diagonal_steep",
465
+ f(60),
466
+ f(10),
467
+ f(68),
468
+ f(118),
469
+ 4
470
+ );
471
+ snapshot_test_clipped!(
472
+ snap_clipped_diagonal_shallow,
473
+ "clipped_diagonal_shallow",
474
+ f(10),
475
+ f(60),
476
+ f(118),
477
+ f(68),
478
+ 5
479
+ );
480
+ snapshot_test_clipped!(
481
+ snap_clipped_negative_slope,
482
+ "clipped_negative_slope",
483
+ f(118),
484
+ f(10),
485
+ f(10),
486
+ f(118),
487
+ 6
488
+ );
489
+ snapshot_test_clipped!(
490
+ snap_clipped_subpixel,
491
+ "clipped_subpixel",
492
+ f(10) + 0x8000,
493
+ f(20) + 0x4000,
494
+ f(100) + 0xC000,
495
+ f(80) + 0x2000,
496
+ 7
497
+ );
498
+ snapshot_test_clipped!(
499
+ snap_clipped_zero_length,
500
+ "clipped_zero_length",
501
+ f(64),
502
+ f(64),
503
+ f(64),
504
+ f(64),
505
+ 8
506
+ );
507
+ snapshot_test_clipped!(
508
+ snap_clipped_partially_outside,
509
+ "clipped_partially_outside",
510
+ f(-20),
511
+ f(64),
512
+ f(148),
513
+ f(64),
514
+ 9
515
+ );
516
+ snapshot_test_clipped!(
517
+ snap_clipped_fully_outside,
518
+ "clipped_fully_outside",
519
+ f(-50),
520
+ f(-50),
521
+ f(-10),
522
+ f(-10),
523
+ 10
524
+ );
525
+
526
+ #[test]
527
+ fn snap_clipped_restricted_clip() {
528
+ let mut grid = PixelGrid::new(128, 128);
529
+ grid.clip_left = 30;
530
+ grid.clip_top = 30;
531
+ grid.clip_right = 98;
532
+ grid.clip_bottom = 98;
533
+ draw_line_clipped(&mut grid, f(10), f(10), f(118), f(118), 11);
534
+ assert_matches_snapshot(&grid, "clipped_restricted_clip");
535
+ }
536
  }