@tanagram/cli 0.4.14 → 0.4.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/tui/renderer.go DELETED
@@ -1,359 +0,0 @@
1
- package tui
2
-
3
- import (
4
- "encoding/json"
5
- "math"
6
- "os"
7
-
8
- "github.com/charmbracelet/lipgloss"
9
- )
10
-
11
- // TanagramRenderer handles rendering tangram pieces to an ASCII canvas
12
- type TanagramRenderer struct {
13
- width int
14
- height int
15
- }
16
-
17
- // NewTanagramRenderer creates a new renderer with the given dimensions
18
- func NewTanagramRenderer(width, height int) *TanagramRenderer {
19
- return &TanagramRenderer{
20
- width: width,
21
- height: height,
22
- }
23
- }
24
-
25
- // RenderPiecesToCanvas renders a slice of pieces to an ASCII canvas
26
- func (r *TanagramRenderer) RenderPiecesToCanvas(pieces []TangramPiece) [][]int {
27
- canvas := make([][]int, r.height)
28
- for i := range canvas {
29
- canvas[i] = make([]int, r.width)
30
- }
31
-
32
- // Render each piece
33
- for idx, piece := range pieces {
34
- r.renderPieceToCanvas(&canvas, piece, idx)
35
- }
36
-
37
- return canvas
38
- }
39
-
40
- // renderPieceToCanvas renders a single piece to the canvas
41
- func (r *TanagramRenderer) renderPieceToCanvas(canvas *[][]int, piece TangramPiece, pieceIndex int) {
42
- points := r.getPiecePoints(piece)
43
-
44
- for _, point := range points {
45
- x := piece.Position.X + point.X
46
- y := piece.Position.Y + point.Y
47
-
48
- if y >= 0 && y < len(*canvas) && x >= 0 && x < len((*canvas)[0]) {
49
- (*canvas)[y][x] = pieceIndex + 1
50
- }
51
- }
52
- }
53
-
54
- // getPiecePoints generates all points for a piece (handles rotation, flip, aspect ratio)
55
- func (r *TanagramRenderer) getPiecePoints(piece TangramPiece) []Position {
56
- var basePoints []Position
57
-
58
- // Generate pieces in "true" square proportions
59
- switch piece.Type {
60
- case SmallTriangle:
61
- height := 8
62
- for y := 0; y < height; y++ {
63
- for x := 0; x <= height-1-y; x++ {
64
- basePoints = append(basePoints, Position{x, y})
65
- }
66
- }
67
- case MediumTriangle:
68
- height := 10
69
- for y := 0; y < height; y++ {
70
- for x := 0; x <= height-1-y; x++ {
71
- basePoints = append(basePoints, Position{x, y})
72
- }
73
- }
74
- case LargeTriangle:
75
- height := 14
76
- for y := 0; y < height; y++ {
77
- for x := 0; x <= height-1-y; x++ {
78
- basePoints = append(basePoints, Position{x, y})
79
- }
80
- }
81
- case Square:
82
- size := 9
83
- for y := 0; y < size; y++ {
84
- for x := 0; x < size; x++ {
85
- basePoints = append(basePoints, Position{x, y})
86
- }
87
- }
88
- case Parallelogram:
89
- height := 5
90
- baseLength := 11
91
- for y := 0; y < height; y++ {
92
- offset := height - 1 - y
93
- for x := 0; x < baseLength; x++ {
94
- basePoints = append(basePoints, Position{x + offset, y})
95
- }
96
- }
97
- }
98
-
99
- // Apply rotation
100
- rotatedPoints := r.rotatePoints(basePoints, piece.Rotation)
101
-
102
- // Fill gaps
103
- rotatedPoints = r.fillShape(rotatedPoints)
104
-
105
- // Apply flip
106
- if piece.Flipped {
107
- rotatedPoints = r.flipPoints(rotatedPoints)
108
- }
109
-
110
- // Apply aspect ratio correction
111
- aspectCorrectedPoints := make([]Position, 0, len(rotatedPoints)*2)
112
- for _, p := range rotatedPoints {
113
- aspectCorrectedPoints = append(aspectCorrectedPoints, Position{p.X * 2, p.Y})
114
- aspectCorrectedPoints = append(aspectCorrectedPoints, Position{p.X*2 + 1, p.Y})
115
- }
116
-
117
- return aspectCorrectedPoints
118
- }
119
-
120
- // rotatePoints rotates points by the given angle
121
- func (r *TanagramRenderer) rotatePoints(points []Position, angle int) []Position {
122
- if angle == 0 {
123
- return points
124
- }
125
-
126
- var rotated []Position
127
- rad := float64(angle) * math.Pi / 180.0
128
- cos := math.Cos(rad)
129
- sin := math.Sin(rad)
130
-
131
- var sumX, sumY int
132
- for _, p := range points {
133
- sumX += p.X
134
- sumY += p.Y
135
- }
136
- centerX := float64(sumX) / float64(len(points))
137
- centerY := float64(sumY) / float64(len(points))
138
-
139
- for _, p := range points {
140
- x := float64(p.X) - centerX
141
- y := float64(p.Y) - centerY
142
-
143
- newX := x*cos - y*sin
144
- newY := x*sin + y*cos
145
-
146
- rotated = append(rotated, Position{
147
- X: int(math.Round(newX + centerX)),
148
- Y: int(math.Round(newY + centerY)),
149
- })
150
- }
151
-
152
- // Normalize to positive coordinates
153
- minX, minY := rotated[0].X, rotated[0].Y
154
- for _, p := range rotated {
155
- if p.X < minX {
156
- minX = p.X
157
- }
158
- if p.Y < minY {
159
- minY = p.Y
160
- }
161
- }
162
-
163
- for i := range rotated {
164
- rotated[i].X -= minX
165
- rotated[i].Y -= minY
166
- }
167
-
168
- return rotated
169
- }
170
-
171
- // fillShape fills gaps in rotated shapes
172
- func (r *TanagramRenderer) fillShape(points []Position) []Position {
173
- if len(points) == 0 {
174
- return points
175
- }
176
-
177
- minX, maxX := points[0].X, points[0].X
178
- minY, maxY := points[0].Y, points[0].Y
179
- for _, p := range points {
180
- if p.X < minX {
181
- minX = p.X
182
- }
183
- if p.X > maxX {
184
- maxX = p.X
185
- }
186
- if p.Y < minY {
187
- minY = p.Y
188
- }
189
- if p.Y > maxY {
190
- maxY = p.Y
191
- }
192
- }
193
-
194
- filledMap := make(map[Position]bool)
195
-
196
- // Fill horizontally
197
- for y := minY; y <= maxY; y++ {
198
- var xCoords []int
199
- for _, p := range points {
200
- if p.Y == y {
201
- xCoords = append(xCoords, p.X)
202
- }
203
- }
204
- if len(xCoords) == 0 {
205
- continue
206
- }
207
- rowMinX, rowMaxX := xCoords[0], xCoords[0]
208
- for _, x := range xCoords {
209
- if x < rowMinX {
210
- rowMinX = x
211
- }
212
- if x > rowMaxX {
213
- rowMaxX = x
214
- }
215
- }
216
- for x := rowMinX; x <= rowMaxX; x++ {
217
- filledMap[Position{x, y}] = true
218
- }
219
- }
220
-
221
- // Fill vertically
222
- for x := minX; x <= maxX; x++ {
223
- var yCoords []int
224
- for p := range filledMap {
225
- if p.X == x {
226
- yCoords = append(yCoords, p.Y)
227
- }
228
- }
229
- if len(yCoords) == 0 {
230
- continue
231
- }
232
- colMinY, colMaxY := yCoords[0], yCoords[0]
233
- for _, y := range yCoords {
234
- if y < colMinY {
235
- colMinY = y
236
- }
237
- if y > colMaxY {
238
- colMaxY = y
239
- }
240
- }
241
- for y := colMinY; y <= colMaxY; y++ {
242
- filledMap[Position{x, y}] = true
243
- }
244
- }
245
-
246
- filledPoints := make([]Position, 0, len(filledMap))
247
- for p := range filledMap {
248
- filledPoints = append(filledPoints, p)
249
- }
250
-
251
- return filledPoints
252
- }
253
-
254
- // flipPoints flips points horizontally
255
- func (r *TanagramRenderer) flipPoints(points []Position) []Position {
256
- if len(points) == 0 {
257
- return points
258
- }
259
-
260
- maxX := points[0].X
261
- for _, p := range points {
262
- if p.X > maxX {
263
- maxX = p.X
264
- }
265
- }
266
-
267
- var flipped []Position
268
- for _, p := range points {
269
- flipped = append(flipped, Position{
270
- X: maxX - p.X,
271
- Y: p.Y,
272
- })
273
- }
274
-
275
- return flipped
276
- }
277
-
278
- // CanvasToString converts a canvas to a styled string
279
- func (r *TanagramRenderer) CanvasToString(canvas [][]int, pieces []TangramPiece) string {
280
- var lines []string
281
- for y, row := range canvas {
282
- var line string
283
- for x := range row {
284
- char, color := r.getCellDisplay(canvas, x, y, pieces)
285
- if color != "" {
286
- style := lipgloss.NewStyle().Foreground(color)
287
- line += style.Render(string(char))
288
- } else {
289
- line += string(char)
290
- }
291
- }
292
- lines = append(lines, line)
293
- }
294
- return lipgloss.JoinVertical(lipgloss.Left, lines...)
295
- }
296
-
297
- // getCellDisplay returns the character and color for a cell
298
- func (r *TanagramRenderer) getCellDisplay(canvas [][]int, x, y int, pieces []TangramPiece) (rune, lipgloss.Color) {
299
- pieceID := canvas[y][x]
300
-
301
- if pieceID == 0 {
302
- return ' ', ""
303
- }
304
-
305
- pieceIndex := pieceID - 1
306
- if pieceIndex >= len(pieces) {
307
- return ' ', ""
308
- }
309
-
310
- piece := pieces[pieceIndex]
311
- return '█', piece.Color
312
- }
313
-
314
- // LoadTanagramConfig loads a tangram configuration from a JSON file
315
- func LoadTanagramConfig(filename string) ([]TangramPiece, error) {
316
- data, err := os.ReadFile(filename)
317
- if err != nil {
318
- return nil, err
319
- }
320
-
321
- type SavedPiece struct {
322
- ID int `json:"id"`
323
- Type string `json:"type"`
324
- Position Position `json:"position"`
325
- Rotation int `json:"rotation"`
326
- Flipped bool `json:"flipped"`
327
- }
328
-
329
- var savedPieces []SavedPiece
330
- err = json.Unmarshal(data, &savedPieces)
331
- if err != nil {
332
- return nil, err
333
- }
334
-
335
- // Color mapping for each piece ID
336
- colors := map[int]lipgloss.Color{
337
- 1: "#BA68C8", // Purple
338
- 2: "#FFB74D", // Orange
339
- 3: "#42A5F5", // Blue
340
- 4: "#EF4444", // Red
341
- 5: "#FF9800", // Orange
342
- 6: "#7E57C2", // Purple
343
- 7: "#66BB6A", // Green
344
- }
345
-
346
- var pieces []TangramPiece
347
- for _, saved := range savedPieces {
348
- pieces = append(pieces, TangramPiece{
349
- ID: saved.ID,
350
- Type: PieceType(saved.Type),
351
- Color: colors[saved.ID],
352
- Position: saved.Position,
353
- Rotation: saved.Rotation,
354
- Flipped: saved.Flipped,
355
- })
356
- }
357
-
358
- return pieces, nil
359
- }