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

feat: port two-color thick line rasterizer, all 21 snapshot tests pass

Browse files

Pure Rust port of 0x4F7A60 (draw_line_two_color): 2px-wide lines with
separate outline and fill colors. First pass draws 8 surrounding pixels
(color2), second pass overwrites 4 center pixels (color1). Both single-
color (11 tests) and two-color (8 tests) 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
@@ -15,8 +15,6 @@
15
  //! └── DDA rasterizer — 1 pixel per step (horizontal or vertical major)
16
  //! ```
17
 
18
- use crate::fixed::Fixed;
19
-
20
  /// Trait for pixel-addressable surfaces used by line-drawing algorithms.
21
  ///
22
  /// Implemented for `PixelGrid` (test-only, pure Rust) and can be implemented
@@ -337,6 +335,219 @@ pub fn draw_line_clipped(
337
  }
338
  }
339
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  // =========================================================================
341
  // Tests
342
  // =========================================================================
@@ -533,4 +744,98 @@ mod tests {
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
  }
 
15
  //! └── DDA rasterizer — 1 pixel per step (horizontal or vertical major)
16
  //! ```
17
 
 
 
18
  /// Trait for pixel-addressable surfaces used by line-drawing algorithms.
19
  ///
20
  /// Implemented for `PixelGrid` (test-only, pure Rust) and can be implemented
 
335
  }
336
  }
337
 
338
+ // =========================================================================
339
+ // Two-color thick line rasterizer (port of 0x4F7A60)
340
+ // =========================================================================
341
+
342
+ /// Horizontal-major thick outline rasterizer (port of 0x4F77C0).
343
+ ///
344
+ /// Draws 8 outline pixels per step around a 2x2 body center.
345
+ fn raster_thick_hmajor_outline(
346
+ writer: &mut dyn PixelWriter,
347
+ x1: i32,
348
+ y1: i32,
349
+ x2: i32,
350
+ y2: i32,
351
+ color: u8,
352
+ ) {
353
+ let start = (x1 & !0xFFFF_i32) + 0x8000;
354
+ let end = (x2 & !0xFFFF_i32) + 0x8000;
355
+ if start == end {
356
+ return;
357
+ }
358
+
359
+ // slope = ((y2 - y1) << 16) / (x2 - x1)
360
+ let slope = fixed_div(y2 - y1, x2 - x1);
361
+ // Sub-pixel y adjustment for pixel-center start
362
+ let mut y = y1 + fixed_mul_shift(slope, start - x1);
363
+ let mut px = start >> 16;
364
+ let count = (end - start) >> 16;
365
+ if count == 0 {
366
+ return;
367
+ }
368
+
369
+ for _ in 0..count {
370
+ let py = y >> 16;
371
+ // 8 outline pixels around the 2x2 body
372
+ writer.put_pixel_clipped(px, py - 1, color);
373
+ writer.put_pixel_clipped(px + 1, py - 1, color);
374
+ writer.put_pixel_clipped(px - 1, py, color);
375
+ writer.put_pixel_clipped(px + 2, py, color);
376
+ writer.put_pixel_clipped(px - 1, py + 1, color);
377
+ writer.put_pixel_clipped(px + 2, py + 1, color);
378
+ writer.put_pixel_clipped(px, py + 2, color);
379
+ writer.put_pixel_clipped(px + 1, py + 2, color);
380
+
381
+ y += slope;
382
+ px += 1;
383
+ }
384
+ }
385
+
386
+ /// Horizontal-major thick fill rasterizer (port of 0x4F75F0).
387
+ ///
388
+ /// Draws 4 body pixels per step (2x2 center).
389
+ fn raster_thick_hmajor_fill(
390
+ writer: &mut dyn PixelWriter,
391
+ x1: i32,
392
+ y1: i32,
393
+ x2: i32,
394
+ y2: i32,
395
+ color: u8,
396
+ ) {
397
+ let start = (x1 & !0xFFFF_i32) + 0x8000;
398
+ let end = (x2 & !0xFFFF_i32) + 0x8000;
399
+ if start == end {
400
+ return;
401
+ }
402
+
403
+ let slope = fixed_div(y2 - y1, x2 - x1);
404
+ let mut y = y1 + fixed_mul_shift(slope, start - x1);
405
+ let mut px = start >> 16;
406
+ let count = (end - start) >> 16;
407
+ if count == 0 {
408
+ return;
409
+ }
410
+
411
+ for _ in 0..count {
412
+ let py = y >> 16;
413
+ // 2x2 body
414
+ writer.put_pixel_clipped(px, py, color);
415
+ writer.put_pixel_clipped(px + 1, py, color);
416
+ writer.put_pixel_clipped(px, py + 1, color);
417
+ writer.put_pixel_clipped(px + 1, py + 1, color);
418
+
419
+ y += slope;
420
+ px += 1;
421
+ }
422
+ }
423
+
424
+ /// Vertical-major thick outline rasterizer (port of 0x4F7910).
425
+ ///
426
+ /// Transposed version of the horizontal-major outline — draws 8 pixels per step.
427
+ fn raster_thick_vmajor_outline(
428
+ writer: &mut dyn PixelWriter,
429
+ x1: i32,
430
+ y1: i32,
431
+ x2: i32,
432
+ y2: i32,
433
+ color: u8,
434
+ ) {
435
+ let start = (y1 & !0xFFFF_i32) + 0x8000;
436
+ let end = (y2 & !0xFFFF_i32) + 0x8000;
437
+ if start == end {
438
+ return;
439
+ }
440
+
441
+ let slope = fixed_div(x2 - x1, y2 - y1);
442
+ let mut x = x1 + fixed_mul_shift(slope, start - y1);
443
+ let mut py = start >> 16;
444
+ let count = (end - start) >> 16;
445
+
446
+ for _ in 0..count {
447
+ let px = x >> 16;
448
+ // 8 outline pixels (transposed pattern)
449
+ writer.put_pixel_clipped(px, py - 1, color);
450
+ writer.put_pixel_clipped(px + 1, py - 1, color);
451
+ writer.put_pixel_clipped(px - 1, py, color);
452
+ writer.put_pixel_clipped(px + 2, py, color);
453
+ writer.put_pixel_clipped(px - 1, py + 1, color);
454
+ writer.put_pixel_clipped(px + 2, py + 1, color);
455
+ writer.put_pixel_clipped(px, py + 2, color);
456
+ writer.put_pixel_clipped(px + 1, py + 2, color);
457
+
458
+ x += slope;
459
+ py += 1;
460
+ }
461
+ }
462
+
463
+ /// Vertical-major thick fill rasterizer (port of 0x4F76D0).
464
+ ///
465
+ /// Transposed version — draws 4 body pixels per step.
466
+ fn raster_thick_vmajor_fill(
467
+ writer: &mut dyn PixelWriter,
468
+ x1: i32,
469
+ y1: i32,
470
+ x2: i32,
471
+ y2: i32,
472
+ color: u8,
473
+ ) {
474
+ let start = (y1 & !0xFFFF_i32) + 0x8000;
475
+ let end = (y2 & !0xFFFF_i32) + 0x8000;
476
+ if start == end {
477
+ return;
478
+ }
479
+
480
+ let slope = fixed_div(x2 - x1, y2 - y1);
481
+ let mut x = x1 + fixed_mul_shift(slope, start - y1);
482
+ let mut py = start >> 16;
483
+ let count = (end - start) >> 16;
484
+
485
+ for _ in 0..count {
486
+ let px = x >> 16;
487
+ // 2x2 body
488
+ writer.put_pixel_clipped(px, py, color);
489
+ writer.put_pixel_clipped(px + 1, py, color);
490
+ writer.put_pixel_clipped(px, py + 1, color);
491
+ writer.put_pixel_clipped(px + 1, py + 1, color);
492
+
493
+ x += slope;
494
+ py += 1;
495
+ }
496
+ }
497
+
498
+ /// Draw a two-color thick clipped line. Port of 0x4F7A60.
499
+ ///
500
+ /// All coordinates are Fixed-point (16.16). Draws a 2px-wide line with
501
+ /// `color1` as outline and `color2` as body fill.
502
+ pub fn draw_line_two_color(
503
+ writer: &mut dyn PixelWriter,
504
+ mut x1: i32,
505
+ mut y1: i32,
506
+ mut x2: i32,
507
+ mut y2: i32,
508
+ color1: u8,
509
+ color2: u8,
510
+ ) {
511
+ let abs_dx = (x2 - x1).unsigned_abs();
512
+ let abs_dy = (y2 - y1).unsigned_abs();
513
+
514
+ if abs_dx < abs_dy {
515
+ // Vertical-major
516
+ // Special case: zero horizontal extent → nudge x coords back by 1 pixel
517
+ if abs_dx == 0 {
518
+ x1 -= 0x10000;
519
+ x2 -= 0x10000;
520
+ }
521
+ // Sort so y1 <= y2
522
+ if y1 > y2 {
523
+ core::mem::swap(&mut x1, &mut x2);
524
+ core::mem::swap(&mut y1, &mut y2);
525
+ }
526
+ if clip_line(&mut x1, &mut y1, &mut x2, &mut y2, writer) {
527
+ // First pass: color2 wide (8 pixels/step), then color1 narrow (4 pixels/step)
528
+ raster_thick_vmajor_outline(writer, x1, y1, x2, y2, color2);
529
+ raster_thick_vmajor_fill(writer, x1, y1, x2, y2, color1);
530
+ }
531
+ } else {
532
+ // Horizontal-major
533
+ // Special case: zero vertical extent → nudge y coords back by 1 pixel
534
+ if abs_dy == 0 {
535
+ y1 -= 0x10000;
536
+ y2 -= 0x10000;
537
+ }
538
+ // Sort so x1 <= x2
539
+ if x1 > x2 {
540
+ core::mem::swap(&mut x1, &mut x2);
541
+ core::mem::swap(&mut y1, &mut y2);
542
+ }
543
+ if clip_line(&mut x1, &mut y1, &mut x2, &mut y2, writer) {
544
+ // First pass: color2 wide (8 pixels/step), then color1 narrow (4 pixels/step)
545
+ raster_thick_hmajor_outline(writer, x1, y1, x2, y2, color2);
546
+ raster_thick_hmajor_fill(writer, x1, y1, x2, y2, color1);
547
+ }
548
+ }
549
+ }
550
+
551
  // =========================================================================
552
  // Tests
553
  // =========================================================================
 
744
  draw_line_clipped(&mut grid, f(10), f(10), f(118), f(118), 11);
745
  assert_matches_snapshot(&grid, "clipped_restricted_clip");
746
  }
747
+
748
+ // Two-color snapshot tests
749
+ macro_rules! snapshot_test_twocol {
750
+ ($name:ident, $snap:expr, $x1:expr, $y1:expr, $x2:expr, $y2:expr, $c1:expr, $c2:expr) => {
751
+ #[test]
752
+ fn $name() {
753
+ let mut grid = PixelGrid::new(128, 128);
754
+ draw_line_two_color(&mut grid, $x1, $y1, $x2, $y2, $c1, $c2);
755
+ assert_matches_snapshot(&grid, $snap);
756
+ }
757
+ };
758
+ }
759
+
760
+ snapshot_test_twocol!(
761
+ snap_twocol_horizontal,
762
+ "twocol_horizontal",
763
+ f(10),
764
+ f(64),
765
+ f(118),
766
+ f(64),
767
+ 1,
768
+ 2
769
+ );
770
+ snapshot_test_twocol!(
771
+ snap_twocol_vertical,
772
+ "twocol_vertical",
773
+ f(64),
774
+ f(10),
775
+ f(64),
776
+ f(118),
777
+ 1,
778
+ 2
779
+ );
780
+ snapshot_test_twocol!(
781
+ snap_twocol_diagonal_45,
782
+ "twocol_diagonal_45",
783
+ f(10),
784
+ f(10),
785
+ f(118),
786
+ f(118),
787
+ 1,
788
+ 2
789
+ );
790
+ snapshot_test_twocol!(
791
+ snap_twocol_steep,
792
+ "twocol_steep",
793
+ f(60),
794
+ f(10),
795
+ f(68),
796
+ f(118),
797
+ 3,
798
+ 4
799
+ );
800
+ snapshot_test_twocol!(
801
+ snap_twocol_shallow,
802
+ "twocol_shallow",
803
+ f(10),
804
+ f(60),
805
+ f(118),
806
+ f(68),
807
+ 3,
808
+ 4
809
+ );
810
+ snapshot_test_twocol!(
811
+ snap_twocol_negative,
812
+ "twocol_negative",
813
+ f(118),
814
+ f(10),
815
+ f(10),
816
+ f(118),
817
+ 5,
818
+ 6
819
+ );
820
+ snapshot_test_twocol!(
821
+ snap_twocol_subpixel,
822
+ "twocol_subpixel",
823
+ f(10) + 0x8000,
824
+ f(20) + 0x4000,
825
+ f(100) + 0xC000,
826
+ f(80) + 0x2000,
827
+ 7,
828
+ 8
829
+ );
830
+
831
+ #[test]
832
+ fn snap_twocol_restricted_clip() {
833
+ let mut grid = PixelGrid::new(128, 128);
834
+ grid.clip_left = 30;
835
+ grid.clip_top = 30;
836
+ grid.clip_right = 98;
837
+ grid.clip_bottom = 98;
838
+ draw_line_two_color(&mut grid, f(10), f(10), f(118), f(118), 9, 10);
839
+ assert_matches_snapshot(&grid, "twocol_restricted_clip");
840
+ }
841
  }