-
Notifications
You must be signed in to change notification settings - Fork 43
/
canvas.go
427 lines (361 loc) · 9.43 KB
/
canvas.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
package goat
import (
"bufio"
"log"
"io"
"os"
)
type (
exists struct{}
runeSet map[rune]exists
)
// Characters where more than one line segment can come together.
var jointRunes = []rune{
'.', // possible ... top corner of a 90 degree angle, or curve
'\'', // possible ... bottom corner of a 90 degree angle, or curve
'+',
'*',
'o',
}
var reserved = append(
jointRunes,
[]rune{
'-',
'_',
'|',
'v',
'^',
'>',
'<',
'/',
'\\',
')',
'(',
' ', // X SPACE is reserved
}...,
)
var reservedSet runeSet
var doubleWideSVG = []rune{
'o',
'*',
}
var wideSVG = []rune{
'v', // X Input containing " over " needs to be considered text.
// '>', // Uncommenting would get 'o<' and '>o' wrong. But o> and >o -- never desired to be text?
// '<', // ibid.
'^',
')',
'(',
'.', // Dropping this would cause " v. " to be considered graphics.
}
var wideSVGSet = makeSet(append(doubleWideSVG, wideSVG...))
func makeSet(runeSlice []rune) (rs runeSet) {
rs = make(runeSet)
for _, r := range runeSlice {
rs[r] = exists{}
}
return
}
func init() {
// Recall that ranging over a 'string' type extracts values of type 'rune'.
reservedSet = make(runeSet)
for _, r := range reserved {
reservedSet[r] = exists{}
}
}
// XX linear search of slice -- alternative to a map test
func contains(in []rune, r rune) bool {
for _, v := range in {
if r == v {
return true
}
}
return false
}
func isJoint(r rune) bool {
return contains(jointRunes, r)
}
// XX rename 'isSpot()'?
func isDot(r rune) bool {
return r == 'o' || r == '*'
}
func isTriangle(r rune) bool {
return r == '^' || r == 'v' || r == '<' || r == '>'
}
// Arg 'canvasMap' is typically either Canvas.data or Canvas.text
func inSet(set runeSet, canvasMap map[Index]rune, i Index) (inset bool) {
r, inMap := canvasMap[i]
if !inMap {
return false // r == rune(0)
}
_, inset = set[r]
return
}
// Looks only at c.data[], ignores c.text[].
// Returns the rune for ASCII Space i.e. ' ', in the event that map lookup fails.
// XX Name 'dataRuneAt()' would be more descriptive, but maybe too bulky.
func (c *Canvas) runeAt(i Index) rune {
if val, ok := c.data[i]; ok {
return val
}
return ' '
}
// Canvas represents a 2D ASCII rectangle.
type Canvas struct {
// units of cells
Width, Height int
data map[Index]rune
text map[Index]rune
}
func (c *Canvas) heightScreen() int {
// XX Why " + 8 + 1"?
return c.Height*16 + 8 + 1
}
func (c *Canvas) widthScreen() int {
// XX Why "c.Width + 1"?
return (c.Width + 1) * 8
}
// NewCanvas creates a fully-populated Canvas according to GoAT-formatted text read from
// an io.Reader, consuming all bytes available.
func NewCanvas(in io.Reader) (c Canvas) {
// XX Move this function to top of file.
width := 0
height := 0
scanner := bufio.NewScanner(in)
c = Canvas{
data: make(map[Index]rune),
text: nil,
}
// Fill the 'data' map.
for scanner.Scan() {
lineStr := scanner.Text()
w := 0
// X Type of second value assigned from "for ... range" operator over a string is "rune".
// https://go.dev/ref/spec#For_statements
// But yet, counterintuitively, type of lineStr[_index_] is 'byte'.
// https://go.dev/ref/spec#String_types
for _, r := range lineStr {
//if r > 255 {
// fmt.Printf("linestr=\"%s\"\n", lineStr)
// fmt.Printf("r == 0x%x\n", r)
//}
if r == ' ' {
file, isFile := in.(*os.File)
fileName := "unknown"
if isFile {
fileName = file.Name()
}
log.Panicf("\n\tFound TAB in %s, row %d, column %d\n",
fileName, height+1, w)
}
i := Index{w, height}
c.data[i] = r
w++
}
if w > width {
width = w
}
height++
}
c.Width = width
c.Height = height
c.text = make(map[Index]rune)
// Fill the 'text' map, with runes removed from 'data'.
c.MoveToText()
return
}
// Move contents of every cell that appears, according to a tricky set of rules,
// to be "text", into a separate map: from data[] to text[].
// So data[] and text[] are an exact partitioning of the
// incoming grid-aligned runes.
func (c *Canvas) MoveToText() {
for i := range leftRight(c.Width, c.Height) {
if c.shouldMoveToText(i) {
c.text[i] = c.runeAt(i) // c.runeAt() Reads from c.data[]
}
}
for i := range c.text {
delete(c.data, i)
}
}
func (c *Canvas) shouldMoveToText(i Index) bool {
i_r := c.runeAt(i)
if i_r == ' ' {
// X Note that c.runeAt(i) returns ' ' if i lies right of all chars on line i.Y
return false
}
// Returns true if the character at index 'i' of c.data[] is reserved for diagrams.
// Characters like 'o' and 'v' need more context (e.g., are other text characters
// nearby) to determine whether they're part of a diagram.
isReserved := func(i Index) (found bool) {
i_r, inData := c.data[i]
if !inData {
// lies off left or right end of line, treat as reserved
return true
}
_, found = reservedSet[i_r]
return
}
if !isReserved(i) {
return true
}
// This is a reserved character with an incoming line (e.g., "|") above or below it,
// so call it non-text.
if c.hasLineAboveOrBelow(i) {
return false
}
w := i.west()
e := i.east()
// Reserved characters like "o" or "*" with letters sitting next to them
// are probably text.
// TODO: Fix this to count contiguous blocks of text. If we had a bunch of
// reserved characters previously that were counted as text then this
// should be as well, e.g., "A----B".
// 'i' is reserved but surrounded by text and probably part of an existing word.
// Preserve chains of reserved-but-text characters like "foo----bar".
if textLeft := !isReserved(w); textLeft {
return true
}
if textRight := !isReserved(e); textRight {
return true
}
crowded := func (l, r Index) bool {
return inSet(wideSVGSet, c.data, l) &&
inSet(wideSVGSet, c.data, r)
}
if crowded(w, i) || crowded(i, e) {
return true
}
// If 'i' has anything other than a space to either left or right, treat as non-text.
if !(c.runeAt(w) == ' ' && c.runeAt(e) == ' ') {
return false
}
// Circles surrounded by whitespace shouldn't be shown as text.
if i_r == 'o' || i_r == '*' {
return false
}
// 'i' is surrounded by whitespace or text on one side or the other, at two cell's distance.
if !isReserved(w.west()) || !isReserved(e.east()) {
return true
}
return false
}
// Returns true if it looks like this character belongs to anything besides a
// horizontal line. This is the context we use to determine if a reserved
// character is text or not.
func (c *Canvas) hasLineAboveOrBelow(i Index) bool {
i_r := c.runeAt(i)
switch i_r {
case '*', 'o', '+', 'v', '^':
return c.partOfDiagonalLine(i) || c.partOfVerticalLine(i)
case '|':
return c.partOfVerticalLine(i) || c.partOfRoundedCorner(i)
case '/', '\\':
return c.partOfDiagonalLine(i)
case '-':
return c.partOfRoundedCorner(i)
case '(', ')':
return c.partOfVerticalLine(i)
}
return false
}
// Returns true if a "|" segment passes through this index.
func (c *Canvas) partOfVerticalLine(i Index) bool {
this := c.runeAt(i)
north := c.runeAt(i.north())
south := c.runeAt(i.south())
jointAboveMe := this == '|' && isJoint(north)
if north == '|' || jointAboveMe {
return true
}
jointBelowMe := this == '|' && isJoint(south)
if south == '|' || jointBelowMe {
return true
}
return false
}
// Return true if a "--" segment passes through this index.
func (c *Canvas) partOfHorizontalLine(i Index) bool {
return c.runeAt(i.east()) == '-' || c.runeAt(i.west()) == '-'
}
func (c *Canvas) partOfDiagonalLine(i Index) bool {
r := c.runeAt(i)
n := c.runeAt(i.north())
s := c.runeAt(i.south())
nw := c.runeAt(i.nWest())
se := c.runeAt(i.sEast())
ne := c.runeAt(i.nEast())
sw := c.runeAt(i.sWest())
switch r {
// Diagonal segments can be connected to joint or other segments.
case '/':
return ne == r || sw == r || isJoint(ne) || isJoint(sw) || n == '\\' || s == '\\'
case '\\':
return nw == r || se == r || isJoint(nw) || isJoint(se) || n == '/' || s == '/'
// For everything else just check if we have segments next to us.
default:
return nw == '\\' || ne == '/' || sw == '/' || se == '\\'
}
}
// For "-" and "|" characters returns true if they could be part of a rounded
// corner.
func (c *Canvas) partOfRoundedCorner(i Index) bool {
r := c.runeAt(i)
switch r {
case '-':
dotNext := c.runeAt(i.west()) == '.' || c.runeAt(i.east()) == '.'
hyphenNext := c.runeAt(i.west()) == '\'' || c.runeAt(i.east()) == '\''
return dotNext || hyphenNext
case '|':
dotAbove := c.runeAt(i.nWest()) == '.' || c.runeAt(i.nEast()) == '.'
hyphenBelow := c.runeAt(i.sWest()) == '\'' || c.runeAt(i.sEast()) == '\''
return dotAbove || hyphenBelow
}
return false
}
// TODO: Have this take care of all the vertical line nudging.
func (c *Canvas) partOfHalfStep(i Index) Orientation {
r := c.runeAt(i)
if r != '\'' && r != '.' && r != '|' {
return NONE
}
if c.isRoundedCorner(i) != NONE {
return NONE
}
w := c.runeAt(i.west())
e := c.runeAt(i.east())
n := c.runeAt(i.north())
s := c.runeAt(i.south())
nw := c.runeAt(i.nWest())
ne := c.runeAt(i.nEast())
switch r {
case '\'':
// _ _
// '- -'
if (nw == '_' && e == '-') || (w == '-' && ne == '_') {
return N
}
case '.':
// _.- -._
if (w == '-' && e == '_') || (w == '_' && e == '-') {
return S
}
case '|':
//// _ _
//// | |
if n != '|' && (ne == '_' || nw == '_') {
return N
}
if n == '-' {
return N
}
//// _| |_
if s != '|' && (w == '_' || e == '_') {
return S
}
if s == '-' {
return S
}
}
return NONE
}