1package renderer
2
3import (
4 "cmp"
5 "slices"
6)
7
8const maxPlannedDamageSpans = 4096
9
10type damageSpan struct {
11 y, x, width int
12}
13
14// buildDamagePlan creates the bounded, canonical view of text and clear damage
15// used for terminal emission. The source damage remains untouched because the
16// scroll and shadow paths need its original structural information.
17func buildDamagePlan(frame Frame, damage []Damage, skip *Damage) ([]damageSpan, bool) {
18 spans := make([]damageSpan, 0)
19 for _, d := range damage {
20 if skip != nil && sameDamage(d, *skip) {
21 continue
22 }
23 if d.Kind != DamageText && d.Kind != DamageClear {
24 continue
25 }
26 x, y, width, height, ok := clampRect(frame, d.X, d.Y, d.Width, d.Height)
27 if !ok {
28 continue
29 }
30 for row := y; row < y+height; row++ {
31 if len(spans) == maxPlannedDamageSpans {
32 return nil, true
33 }
34 spans = append(spans, damageSpan{y: row, x: x, width: width})
35 }
36 }
37
38 slices.SortFunc(spans, func(a, b damageSpan) int {
39 if c := cmp.Compare(a.y, b.y); c != 0 {
40 return c
41 }
42 return cmp.Compare(a.x, b.x)
43 })
44
45 return mergeDamageSpans(spans), false
46}
47
48func mergeDamageSpans(spans []damageSpan) []damageSpan {
49 if len(spans) < 2 {
50 return spans
51 }
52
53 out := spans[:0]
54 for _, span := range spans {
55 if len(out) == 0 {
56 out = append(out, span)
57 continue
58 }
59 last := &out[len(out)-1]
60 // Both endpoints are bounded by frame.Width after clampRect, so these
61 // additions cannot overflow. The comparison includes adjacency.
62 lastEnd := last.x + last.width
63 spanEnd := span.x + span.width
64 if span.y != last.y || span.x > lastEnd {
65 out = append(out, span)
66 continue
67 }
68 if spanEnd > lastEnd {
69 last.width = spanEnd - last.x
70 }
71 }
72 return out
73}