@tanagram/cli 0.4.14 → 0.4.19
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/README.md +2 -0
- package/dist/npm/darwin-arm64/LICENSE +21 -0
- package/dist/npm/darwin-arm64/README.md +267 -0
- package/dist/npm/darwin-arm64/tanagram +0 -0
- package/dist/npm/darwin-x64/LICENSE +21 -0
- package/dist/npm/darwin-x64/README.md +267 -0
- package/dist/npm/darwin-x64/tanagram +0 -0
- package/dist/npm/linux-arm64/LICENSE +21 -0
- package/dist/npm/linux-arm64/README.md +267 -0
- package/dist/npm/linux-arm64/tanagram +0 -0
- package/dist/npm/linux-x64/LICENSE +21 -0
- package/dist/npm/linux-x64/README.md +267 -0
- package/dist/npm/linux-x64/tanagram +0 -0
- package/dist/npm/tanagram_0.4.19_darwin_amd64.tar.gz +0 -0
- package/dist/npm/tanagram_0.4.19_darwin_arm64.tar.gz +0 -0
- package/dist/npm/tanagram_0.4.19_linux_amd64.tar.gz +0 -0
- package/dist/npm/tanagram_0.4.19_linux_arm64.tar.gz +0 -0
- package/dist/npm/tanagram_0.4.19_windows_amd64.zip +0 -0
- package/dist/npm/win32-x64/LICENSE +21 -0
- package/dist/npm/win32-x64/README.md +267 -0
- package/dist/npm/win32-x64/tanagram.exe +0 -0
- package/go.mod +1 -0
- package/go.sum +2 -0
- package/install.js +176 -22
- package/main.go +29 -13
- package/package.json +5 -4
- package/tui/welcome.go +1 -13
- package/tui/puzzle.go +0 -694
- package/tui/renderer.go +0 -359
package/tui/puzzle.go
DELETED
|
@@ -1,694 +0,0 @@
|
|
|
1
|
-
package tui
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"encoding/json"
|
|
5
|
-
"fmt"
|
|
6
|
-
"math"
|
|
7
|
-
"os"
|
|
8
|
-
"strings"
|
|
9
|
-
|
|
10
|
-
tea "github.com/charmbracelet/bubbletea"
|
|
11
|
-
"github.com/charmbracelet/lipgloss"
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
// PieceType represents the type of tangram piece
|
|
15
|
-
type PieceType string
|
|
16
|
-
|
|
17
|
-
const (
|
|
18
|
-
LargeTriangle PieceType = "large-triangle"
|
|
19
|
-
MediumTriangle PieceType = "medium-triangle"
|
|
20
|
-
SmallTriangle PieceType = "small-triangle"
|
|
21
|
-
Square PieceType = "square"
|
|
22
|
-
Parallelogram PieceType = "parallelogram"
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
// Position represents x,y coordinates
|
|
26
|
-
type Position struct {
|
|
27
|
-
X int
|
|
28
|
-
Y int
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// TangramPiece represents a single tangram piece
|
|
32
|
-
type TangramPiece struct {
|
|
33
|
-
ID int
|
|
34
|
-
Type PieceType
|
|
35
|
-
Color lipgloss.Color
|
|
36
|
-
Position Position
|
|
37
|
-
Rotation int // 0, 90, 180, 270
|
|
38
|
-
Flipped bool
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// PuzzleModel is the Bubble Tea model for the puzzle editor
|
|
42
|
-
type PuzzleModel struct {
|
|
43
|
-
pieces []TangramPiece
|
|
44
|
-
selectedIndex int
|
|
45
|
-
canvasWidth int
|
|
46
|
-
canvasHeight int
|
|
47
|
-
width int
|
|
48
|
-
height int
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// NewPuzzleModel creates a new puzzle editor model
|
|
52
|
-
func NewPuzzleModel() PuzzleModel {
|
|
53
|
-
// Define the 7 tangram pieces with initial scattered positions
|
|
54
|
-
pieces := []TangramPiece{
|
|
55
|
-
{ID: 1, Type: SmallTriangle, Color: "#BA68C8", Position: Position{2, 1}, Rotation: 0, Flipped: false},
|
|
56
|
-
{ID: 2, Type: SmallTriangle, Color: "#FFB74D", Position: Position{20, 1}, Rotation: 0, Flipped: false},
|
|
57
|
-
{ID: 3, Type: MediumTriangle, Color: "#42A5F5", Position: Position{38, 1}, Rotation: 0, Flipped: false},
|
|
58
|
-
{ID: 4, Type: LargeTriangle, Color: "#EF4444", Position: Position{2, 10}, Rotation: 0, Flipped: false},
|
|
59
|
-
{ID: 5, Type: LargeTriangle, Color: "#FF9800", Position: Position{32, 10}, Rotation: 0, Flipped: false},
|
|
60
|
-
{ID: 6, Type: Square, Color: "#7E57C2", Position: Position{62, 1}, Rotation: 0, Flipped: false},
|
|
61
|
-
{ID: 7, Type: Parallelogram, Color: "#66BB6A", Position: Position{62, 10}, Rotation: 0, Flipped: false},
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return PuzzleModel{
|
|
65
|
-
pieces: pieces,
|
|
66
|
-
selectedIndex: 0,
|
|
67
|
-
canvasWidth: 120,
|
|
68
|
-
canvasHeight: 45,
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Init initializes the model
|
|
73
|
-
func (m PuzzleModel) Init() tea.Cmd {
|
|
74
|
-
return nil
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Update handles messages
|
|
78
|
-
func (m PuzzleModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
79
|
-
switch msg := msg.(type) {
|
|
80
|
-
case tea.KeyMsg:
|
|
81
|
-
switch msg.String() {
|
|
82
|
-
case "ctrl+c", "q", "esc":
|
|
83
|
-
return m, tea.Quit
|
|
84
|
-
case "left", "h":
|
|
85
|
-
// Select previous piece
|
|
86
|
-
m.selectedIndex = (m.selectedIndex - 1 + len(m.pieces)) % len(m.pieces)
|
|
87
|
-
case "right", "l":
|
|
88
|
-
// Select next piece
|
|
89
|
-
m.selectedIndex = (m.selectedIndex + 1) % len(m.pieces)
|
|
90
|
-
case "up", "k":
|
|
91
|
-
// Move selected piece up
|
|
92
|
-
if m.pieces[m.selectedIndex].Position.Y > 0 {
|
|
93
|
-
m.pieces[m.selectedIndex].Position.Y--
|
|
94
|
-
}
|
|
95
|
-
case "down", "j":
|
|
96
|
-
// Move selected piece down
|
|
97
|
-
if m.pieces[m.selectedIndex].Position.Y < m.canvasHeight-15 {
|
|
98
|
-
m.pieces[m.selectedIndex].Position.Y++
|
|
99
|
-
}
|
|
100
|
-
case "shift+left", "H":
|
|
101
|
-
// Move selected piece left
|
|
102
|
-
if m.pieces[m.selectedIndex].Position.X > 0 {
|
|
103
|
-
m.pieces[m.selectedIndex].Position.X--
|
|
104
|
-
}
|
|
105
|
-
case "shift+right", "L":
|
|
106
|
-
// Move selected piece right
|
|
107
|
-
if m.pieces[m.selectedIndex].Position.X < m.canvasWidth-35 {
|
|
108
|
-
m.pieces[m.selectedIndex].Position.X++
|
|
109
|
-
}
|
|
110
|
-
case "r":
|
|
111
|
-
// Rotate 90° clockwise
|
|
112
|
-
m.pieces[m.selectedIndex].Rotation = (m.pieces[m.selectedIndex].Rotation + 90) % 360
|
|
113
|
-
case "R":
|
|
114
|
-
// Rotate 90° counter-clockwise
|
|
115
|
-
m.pieces[m.selectedIndex].Rotation = (m.pieces[m.selectedIndex].Rotation - 90 + 360) % 360
|
|
116
|
-
case "[":
|
|
117
|
-
// Rotate 45° counter-clockwise (fine rotation)
|
|
118
|
-
m.pieces[m.selectedIndex].Rotation = (m.pieces[m.selectedIndex].Rotation - 45 + 360) % 360
|
|
119
|
-
case "]":
|
|
120
|
-
// Rotate 45° clockwise (fine rotation)
|
|
121
|
-
m.pieces[m.selectedIndex].Rotation = (m.pieces[m.selectedIndex].Rotation + 45) % 360
|
|
122
|
-
case "f":
|
|
123
|
-
// Flip piece
|
|
124
|
-
m.pieces[m.selectedIndex].Flipped = !m.pieces[m.selectedIndex].Flipped
|
|
125
|
-
case "e":
|
|
126
|
-
// Export configuration to console
|
|
127
|
-
m.exportConfig()
|
|
128
|
-
case "s":
|
|
129
|
-
// Save configuration to file
|
|
130
|
-
m.saveConfig()
|
|
131
|
-
case "o":
|
|
132
|
-
// Open/load configuration from file
|
|
133
|
-
m.loadConfig()
|
|
134
|
-
}
|
|
135
|
-
case tea.WindowSizeMsg:
|
|
136
|
-
m.width = msg.Width
|
|
137
|
-
m.height = msg.Height
|
|
138
|
-
}
|
|
139
|
-
return m, nil
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// View renders the UI
|
|
143
|
-
func (m PuzzleModel) View() string {
|
|
144
|
-
// Create canvas with piece IDs (0 = empty, 1-7 = piece IDs)
|
|
145
|
-
canvas := make([][]int, m.canvasHeight)
|
|
146
|
-
for i := range canvas {
|
|
147
|
-
canvas[i] = make([]int, m.canvasWidth)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Render pieces (in order, so later pieces appear on top)
|
|
151
|
-
for idx, piece := range m.pieces {
|
|
152
|
-
m.renderPieceToCanvas(&canvas, piece, idx)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Convert canvas to styled string with smooth edges
|
|
156
|
-
var lines []string
|
|
157
|
-
for y, row := range canvas {
|
|
158
|
-
var line strings.Builder
|
|
159
|
-
for x := range row {
|
|
160
|
-
char, color := m.getCellDisplay(canvas, x, y)
|
|
161
|
-
if color != "" {
|
|
162
|
-
style := lipgloss.NewStyle().Foreground(color)
|
|
163
|
-
line.WriteString(style.Render(string(char)))
|
|
164
|
-
} else {
|
|
165
|
-
line.WriteRune(char)
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
lines = append(lines, line.String())
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
canvasOutput := strings.Join(lines, "\n")
|
|
172
|
-
|
|
173
|
-
// Border for canvas
|
|
174
|
-
canvasBorder := lipgloss.NewStyle().
|
|
175
|
-
Border(lipgloss.RoundedBorder()).
|
|
176
|
-
BorderForeground(lipgloss.Color("#8B5CF6")).
|
|
177
|
-
Padding(0, 1)
|
|
178
|
-
|
|
179
|
-
borderedCanvas := canvasBorder.Render(canvasOutput)
|
|
180
|
-
|
|
181
|
-
// Title
|
|
182
|
-
titleStyle := lipgloss.NewStyle().
|
|
183
|
-
Bold(true).
|
|
184
|
-
Foreground(lipgloss.Color("#FFFFFF")).
|
|
185
|
-
Background(lipgloss.Color("#8B5CF6")).
|
|
186
|
-
Padding(0, 2)
|
|
187
|
-
|
|
188
|
-
title := titleStyle.Render(" TANAGRAM PUZZLE EDITOR ")
|
|
189
|
-
|
|
190
|
-
// Selected piece info
|
|
191
|
-
selectedPiece := m.pieces[m.selectedIndex]
|
|
192
|
-
infoStyle := lipgloss.NewStyle().
|
|
193
|
-
Foreground(lipgloss.Color("#AAAAAA"))
|
|
194
|
-
|
|
195
|
-
info := infoStyle.Render(fmt.Sprintf(
|
|
196
|
-
" Piece #%d (%s) @ (%d,%d) • %d° • Flipped:%t",
|
|
197
|
-
selectedPiece.ID,
|
|
198
|
-
m.shortType(selectedPiece.Type),
|
|
199
|
-
selectedPiece.Position.X,
|
|
200
|
-
selectedPiece.Position.Y,
|
|
201
|
-
selectedPiece.Rotation,
|
|
202
|
-
selectedPiece.Flipped,
|
|
203
|
-
))
|
|
204
|
-
|
|
205
|
-
// Piece legend - compact version
|
|
206
|
-
var legendParts []string
|
|
207
|
-
for idx, piece := range m.pieces {
|
|
208
|
-
pieceStyle := lipgloss.NewStyle().Foreground(piece.Color).Bold(true)
|
|
209
|
-
marker := " "
|
|
210
|
-
if idx == m.selectedIndex {
|
|
211
|
-
marker = "▶"
|
|
212
|
-
}
|
|
213
|
-
legendParts = append(legendParts, fmt.Sprintf("%s%s%d",
|
|
214
|
-
marker,
|
|
215
|
-
pieceStyle.Render("█"),
|
|
216
|
-
piece.ID,
|
|
217
|
-
))
|
|
218
|
-
}
|
|
219
|
-
legendStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
|
220
|
-
legend := legendStyle.Render(" Pieces: " + strings.Join(legendParts, " "))
|
|
221
|
-
|
|
222
|
-
// Controls help - more compact
|
|
223
|
-
helpStyle := lipgloss.NewStyle().
|
|
224
|
-
Foreground(lipgloss.Color("#666666")).
|
|
225
|
-
Italic(true)
|
|
226
|
-
|
|
227
|
-
help := helpStyle.Render(
|
|
228
|
-
" ←→/hl:select ↑↓/kj:move H/L:move left/right r/R:90° [/]:45° f:flip s:save o:load e:export q:quit",
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
// Combine all parts with consistent spacing
|
|
232
|
-
return lipgloss.JoinVertical(lipgloss.Left,
|
|
233
|
-
"",
|
|
234
|
-
title,
|
|
235
|
-
info,
|
|
236
|
-
"",
|
|
237
|
-
borderedCanvas,
|
|
238
|
-
"",
|
|
239
|
-
legend,
|
|
240
|
-
help,
|
|
241
|
-
"",
|
|
242
|
-
)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// shortType returns abbreviated type name
|
|
246
|
-
func (m PuzzleModel) shortType(t PieceType) string {
|
|
247
|
-
switch t {
|
|
248
|
-
case LargeTriangle:
|
|
249
|
-
return "Lg△"
|
|
250
|
-
case MediumTriangle:
|
|
251
|
-
return "Md△"
|
|
252
|
-
case SmallTriangle:
|
|
253
|
-
return "Sm△"
|
|
254
|
-
case Square:
|
|
255
|
-
return "□"
|
|
256
|
-
case Parallelogram:
|
|
257
|
-
return "▱"
|
|
258
|
-
default:
|
|
259
|
-
return string(t)
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// renderPieceToCanvas renders a piece to the canvas by piece ID
|
|
264
|
-
func (m PuzzleModel) renderPieceToCanvas(canvas *[][]int, piece TangramPiece, pieceIndex int) {
|
|
265
|
-
// Get piece shape based on type and rotation
|
|
266
|
-
points := m.getPiecePoints(piece)
|
|
267
|
-
|
|
268
|
-
// Fill the piece area with piece index + 1 (so 0 remains empty)
|
|
269
|
-
for _, point := range points {
|
|
270
|
-
x := piece.Position.X + point.X
|
|
271
|
-
y := piece.Position.Y + point.Y
|
|
272
|
-
|
|
273
|
-
if y >= 0 && y < len(*canvas) && x >= 0 && x < len((*canvas)[0]) {
|
|
274
|
-
(*canvas)[y][x] = pieceIndex + 1
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// getCellDisplay returns the character and color for a cell
|
|
280
|
-
func (m PuzzleModel) getCellDisplay(canvas [][]int, x, y int) (rune, lipgloss.Color) {
|
|
281
|
-
pieceID := canvas[y][x]
|
|
282
|
-
|
|
283
|
-
// Empty cell - subtle grid pattern
|
|
284
|
-
if pieceID == 0 {
|
|
285
|
-
// Show very faint grid lines
|
|
286
|
-
if x%5 == 0 || y%5 == 0 {
|
|
287
|
-
return '·', lipgloss.Color("#444444")
|
|
288
|
-
}
|
|
289
|
-
return ' ', ""
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
pieceIndex := pieceID - 1
|
|
293
|
-
piece := m.pieces[pieceIndex]
|
|
294
|
-
isSelected := pieceIndex == m.selectedIndex
|
|
295
|
-
|
|
296
|
-
// Check if this is an edge cell
|
|
297
|
-
isEdge := m.isEdgeCell(canvas, x, y, pieceID)
|
|
298
|
-
|
|
299
|
-
// Use different characters for edges and fills to create depth
|
|
300
|
-
var char rune
|
|
301
|
-
if isSelected {
|
|
302
|
-
// Selected piece has lighter fill to show it's active
|
|
303
|
-
if isEdge {
|
|
304
|
-
char = '█' // Solid edge
|
|
305
|
-
} else {
|
|
306
|
-
char = '▓' // Lighter fill
|
|
307
|
-
}
|
|
308
|
-
} else {
|
|
309
|
-
// Normal pieces are all solid
|
|
310
|
-
char = '█'
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return char, piece.Color
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// isEdgeCell checks if a cell is on the edge of its piece
|
|
317
|
-
func (m PuzzleModel) isEdgeCell(canvas [][]int, x, y int, pieceID int) bool {
|
|
318
|
-
// Check all 4 directions
|
|
319
|
-
directions := []struct{ dx, dy int }{
|
|
320
|
-
{-1, 0}, {1, 0}, {0, -1}, {0, 1},
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
for _, dir := range directions {
|
|
324
|
-
nx, ny := x+dir.dx, y+dir.dy
|
|
325
|
-
|
|
326
|
-
// Out of bounds = edge
|
|
327
|
-
if ny < 0 || ny >= len(canvas) || nx < 0 || nx >= len(canvas[0]) {
|
|
328
|
-
return true
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Different piece or empty = edge
|
|
332
|
-
if canvas[ny][nx] != pieceID {
|
|
333
|
-
return true
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return false
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// getPiecePoints returns the relative points for a piece based on its type and rotation
|
|
341
|
-
func (m PuzzleModel) getPiecePoints(piece TangramPiece) []Position {
|
|
342
|
-
var basePoints []Position
|
|
343
|
-
|
|
344
|
-
// Generate pieces in "true" square proportions (before aspect ratio correction)
|
|
345
|
-
switch piece.Type {
|
|
346
|
-
case SmallTriangle:
|
|
347
|
-
// Small right triangle - 8x8 in true proportions
|
|
348
|
-
height := 8
|
|
349
|
-
for y := 0; y < height; y++ {
|
|
350
|
-
// Right triangle: width decreases by 1 each row
|
|
351
|
-
for x := 0; x <= height-1-y; x++ {
|
|
352
|
-
basePoints = append(basePoints, Position{x, y})
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
case MediumTriangle:
|
|
356
|
-
// Medium right triangle - 10x10 in true proportions
|
|
357
|
-
height := 10
|
|
358
|
-
for y := 0; y < height; y++ {
|
|
359
|
-
for x := 0; x <= height-1-y; x++ {
|
|
360
|
-
basePoints = append(basePoints, Position{x, y})
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
case LargeTriangle:
|
|
364
|
-
// Large right triangle - 14x14 in true proportions
|
|
365
|
-
height := 14
|
|
366
|
-
for y := 0; y < height; y++ {
|
|
367
|
-
for x := 0; x <= height-1-y; x++ {
|
|
368
|
-
basePoints = append(basePoints, Position{x, y})
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
case Square:
|
|
372
|
-
// Square - 9x9 in true proportions (between small and medium triangle)
|
|
373
|
-
size := 9
|
|
374
|
-
for y := 0; y < size; y++ {
|
|
375
|
-
for x := 0; x < size; x++ {
|
|
376
|
-
basePoints = append(basePoints, Position{x, y})
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
case Parallelogram:
|
|
380
|
-
// Parallelogram in true proportions
|
|
381
|
-
height := 5
|
|
382
|
-
baseLength := 11
|
|
383
|
-
for y := 0; y < height; y++ {
|
|
384
|
-
// Slant offset decreases as we go down
|
|
385
|
-
offset := height - 1 - y
|
|
386
|
-
for x := 0; x < baseLength; x++ {
|
|
387
|
-
basePoints = append(basePoints, Position{x + offset, y})
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Apply rotation FIRST (on true proportions)
|
|
393
|
-
rotatedPoints := m.rotatePoints(basePoints, piece.Rotation)
|
|
394
|
-
|
|
395
|
-
// Fill any gaps in the rotated shape
|
|
396
|
-
rotatedPoints = m.fillShape(rotatedPoints)
|
|
397
|
-
|
|
398
|
-
// Apply flip
|
|
399
|
-
if piece.Flipped {
|
|
400
|
-
rotatedPoints = m.flipPoints(rotatedPoints)
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// THEN apply aspect ratio correction (stretch horizontally by 2x)
|
|
404
|
-
aspectCorrectedPoints := make([]Position, 0, len(rotatedPoints)*2)
|
|
405
|
-
for _, p := range rotatedPoints {
|
|
406
|
-
// Each point becomes 2 points horizontally
|
|
407
|
-
aspectCorrectedPoints = append(aspectCorrectedPoints, Position{p.X * 2, p.Y})
|
|
408
|
-
aspectCorrectedPoints = append(aspectCorrectedPoints, Position{p.X*2 + 1, p.Y})
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return aspectCorrectedPoints
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// fillShape fills in any gaps in a rotated shape using scan-line fill
|
|
415
|
-
func (m PuzzleModel) fillShape(points []Position) []Position {
|
|
416
|
-
if len(points) == 0 {
|
|
417
|
-
return points
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Create a map of existing points for fast lookup
|
|
421
|
-
pointMap := make(map[Position]bool)
|
|
422
|
-
for _, p := range points {
|
|
423
|
-
pointMap[p] = true
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// Find bounding box
|
|
427
|
-
minX, maxX := points[0].X, points[0].X
|
|
428
|
-
minY, maxY := points[0].Y, points[0].Y
|
|
429
|
-
for _, p := range points {
|
|
430
|
-
if p.X < minX {
|
|
431
|
-
minX = p.X
|
|
432
|
-
}
|
|
433
|
-
if p.X > maxX {
|
|
434
|
-
maxX = p.X
|
|
435
|
-
}
|
|
436
|
-
if p.Y < minY {
|
|
437
|
-
minY = p.Y
|
|
438
|
-
}
|
|
439
|
-
if p.Y > maxY {
|
|
440
|
-
maxY = p.Y
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Fill horizontally (row by row)
|
|
445
|
-
filledMap := make(map[Position]bool)
|
|
446
|
-
for y := minY; y <= maxY; y++ {
|
|
447
|
-
var xCoords []int
|
|
448
|
-
for _, p := range points {
|
|
449
|
-
if p.Y == y {
|
|
450
|
-
xCoords = append(xCoords, p.X)
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
if len(xCoords) == 0 {
|
|
455
|
-
continue
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
rowMinX, rowMaxX := xCoords[0], xCoords[0]
|
|
459
|
-
for _, x := range xCoords {
|
|
460
|
-
if x < rowMinX {
|
|
461
|
-
rowMinX = x
|
|
462
|
-
}
|
|
463
|
-
if x > rowMaxX {
|
|
464
|
-
rowMaxX = x
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
for x := rowMinX; x <= rowMaxX; x++ {
|
|
469
|
-
filledMap[Position{x, y}] = true
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Fill vertically (column by column) to catch any remaining gaps
|
|
474
|
-
for x := minX; x <= maxX; x++ {
|
|
475
|
-
var yCoords []int
|
|
476
|
-
for p := range filledMap {
|
|
477
|
-
if p.X == x {
|
|
478
|
-
yCoords = append(yCoords, p.Y)
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if len(yCoords) == 0 {
|
|
483
|
-
continue
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
colMinY, colMaxY := yCoords[0], yCoords[0]
|
|
487
|
-
for _, y := range yCoords {
|
|
488
|
-
if y < colMinY {
|
|
489
|
-
colMinY = y
|
|
490
|
-
}
|
|
491
|
-
if y > colMaxY {
|
|
492
|
-
colMaxY = y
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
for y := colMinY; y <= colMaxY; y++ {
|
|
497
|
-
filledMap[Position{x, y}] = true
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Convert map back to slice
|
|
502
|
-
filledPoints := make([]Position, 0, len(filledMap))
|
|
503
|
-
for p := range filledMap {
|
|
504
|
-
filledPoints = append(filledPoints, p)
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
return filledPoints
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// rotatePoints rotates points by the given angle (0, 90, 180, 270)
|
|
511
|
-
func (m PuzzleModel) rotatePoints(points []Position, angle int) []Position {
|
|
512
|
-
if angle == 0 {
|
|
513
|
-
return points
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
var rotated []Position
|
|
517
|
-
rad := float64(angle) * math.Pi / 180.0
|
|
518
|
-
cos := math.Cos(rad)
|
|
519
|
-
sin := math.Sin(rad)
|
|
520
|
-
|
|
521
|
-
// Find center of points
|
|
522
|
-
var sumX, sumY int
|
|
523
|
-
for _, p := range points {
|
|
524
|
-
sumX += p.X
|
|
525
|
-
sumY += p.Y
|
|
526
|
-
}
|
|
527
|
-
centerX := float64(sumX) / float64(len(points))
|
|
528
|
-
centerY := float64(sumY) / float64(len(points))
|
|
529
|
-
|
|
530
|
-
for _, p := range points {
|
|
531
|
-
// Translate to origin
|
|
532
|
-
x := float64(p.X) - centerX
|
|
533
|
-
y := float64(p.Y) - centerY
|
|
534
|
-
|
|
535
|
-
// Rotate
|
|
536
|
-
newX := x*cos - y*sin
|
|
537
|
-
newY := x*sin + y*cos
|
|
538
|
-
|
|
539
|
-
// Translate back
|
|
540
|
-
rotated = append(rotated, Position{
|
|
541
|
-
X: int(math.Round(newX + centerX)),
|
|
542
|
-
Y: int(math.Round(newY + centerY)),
|
|
543
|
-
})
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Normalize to positive coordinates
|
|
547
|
-
minX, minY := rotated[0].X, rotated[0].Y
|
|
548
|
-
for _, p := range rotated {
|
|
549
|
-
if p.X < minX {
|
|
550
|
-
minX = p.X
|
|
551
|
-
}
|
|
552
|
-
if p.Y < minY {
|
|
553
|
-
minY = p.Y
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
for i := range rotated {
|
|
558
|
-
rotated[i].X -= minX
|
|
559
|
-
rotated[i].Y -= minY
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
return rotated
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// flipPoints flips points horizontally
|
|
566
|
-
func (m PuzzleModel) flipPoints(points []Position) []Position {
|
|
567
|
-
if len(points) == 0 {
|
|
568
|
-
return points
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// Find max X
|
|
572
|
-
maxX := points[0].X
|
|
573
|
-
for _, p := range points {
|
|
574
|
-
if p.X > maxX {
|
|
575
|
-
maxX = p.X
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Flip horizontally
|
|
580
|
-
var flipped []Position
|
|
581
|
-
for _, p := range points {
|
|
582
|
-
flipped = append(flipped, Position{
|
|
583
|
-
X: maxX - p.X,
|
|
584
|
-
Y: p.Y,
|
|
585
|
-
})
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
return flipped
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// exportConfig prints the current configuration
|
|
592
|
-
func (m PuzzleModel) exportConfig() {
|
|
593
|
-
fmt.Println("\n=== TANGRAM CONFIGURATION ===")
|
|
594
|
-
for _, piece := range m.pieces {
|
|
595
|
-
fmt.Printf("Piece #%d (%s): Position(%d, %d), Rotation: %d°, Flipped: %t\n",
|
|
596
|
-
piece.ID,
|
|
597
|
-
piece.Type,
|
|
598
|
-
piece.Position.X,
|
|
599
|
-
piece.Position.Y,
|
|
600
|
-
piece.Rotation,
|
|
601
|
-
piece.Flipped,
|
|
602
|
-
)
|
|
603
|
-
}
|
|
604
|
-
fmt.Println("============================\n")
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// saveConfig saves the current configuration to a JSON file
|
|
608
|
-
func (m *PuzzleModel) saveConfig() {
|
|
609
|
-
// Create a simplified structure for saving
|
|
610
|
-
type SavedPiece struct {
|
|
611
|
-
ID int `json:"id"`
|
|
612
|
-
Type string `json:"type"`
|
|
613
|
-
Position Position `json:"position"`
|
|
614
|
-
Rotation int `json:"rotation"`
|
|
615
|
-
Flipped bool `json:"flipped"`
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
var savedPieces []SavedPiece
|
|
619
|
-
for _, piece := range m.pieces {
|
|
620
|
-
savedPieces = append(savedPieces, SavedPiece{
|
|
621
|
-
ID: piece.ID,
|
|
622
|
-
Type: string(piece.Type),
|
|
623
|
-
Position: piece.Position,
|
|
624
|
-
Rotation: piece.Rotation,
|
|
625
|
-
Flipped: piece.Flipped,
|
|
626
|
-
})
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
data, err := json.MarshalIndent(savedPieces, "", " ")
|
|
630
|
-
if err != nil {
|
|
631
|
-
fmt.Printf("\n❌ Error saving configuration: %v\n", err)
|
|
632
|
-
return
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
filename := "tanagram-config.json"
|
|
636
|
-
err = os.WriteFile(filename, data, 0644)
|
|
637
|
-
if err != nil {
|
|
638
|
-
fmt.Printf("\n❌ Error writing file: %v\n", err)
|
|
639
|
-
return
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
fmt.Printf("\n✓ Configuration saved to %s\n", filename)
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// loadConfig loads a configuration from a JSON file
|
|
646
|
-
func (m *PuzzleModel) loadConfig() {
|
|
647
|
-
filename := "tanagram-config.json"
|
|
648
|
-
|
|
649
|
-
data, err := os.ReadFile(filename)
|
|
650
|
-
if err != nil {
|
|
651
|
-
fmt.Printf("\n❌ Error reading file: %v\n", err)
|
|
652
|
-
fmt.Println(" Make sure tanagram-config.json exists in the current directory")
|
|
653
|
-
return
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
type SavedPiece struct {
|
|
657
|
-
ID int `json:"id"`
|
|
658
|
-
Type string `json:"type"`
|
|
659
|
-
Position Position `json:"position"`
|
|
660
|
-
Rotation int `json:"rotation"`
|
|
661
|
-
Flipped bool `json:"flipped"`
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
var savedPieces []SavedPiece
|
|
665
|
-
err = json.Unmarshal(data, &savedPieces)
|
|
666
|
-
if err != nil {
|
|
667
|
-
fmt.Printf("\n❌ Error parsing configuration: %v\n", err)
|
|
668
|
-
return
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// Update piece positions, rotations, and flips
|
|
672
|
-
for i, saved := range savedPieces {
|
|
673
|
-
if i < len(m.pieces) && saved.ID == m.pieces[i].ID {
|
|
674
|
-
m.pieces[i].Position = saved.Position
|
|
675
|
-
m.pieces[i].Rotation = saved.Rotation
|
|
676
|
-
m.pieces[i].Flipped = saved.Flipped
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
fmt.Printf("\n✓ Configuration loaded from %s\n", filename)
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// RunPuzzleEditor displays the puzzle editor
|
|
684
|
-
func RunPuzzleEditor() error {
|
|
685
|
-
model := NewPuzzleModel()
|
|
686
|
-
p := tea.NewProgram(model, tea.WithAltScreen())
|
|
687
|
-
|
|
688
|
-
_, err := p.Run()
|
|
689
|
-
if err != nil {
|
|
690
|
-
return fmt.Errorf("error running puzzle editor: %w", err)
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
return nil
|
|
694
|
-
}
|