progress: Finished the chunk-level ui for the visualization
This commit is contained in:
		@@ -12,12 +12,16 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const fileCacheSize = 8
 | 
					const fileCacheSize = 8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						ChunkNotFoundError = errors.New("chunk was not found in storage")
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type SimpleServer struct {
 | 
					type SimpleServer struct {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Filesystem operations
 | 
					// Filesystem operations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *SimpleServer) FetchChunk(pos world.ChunkPos) (world.ChunkData, error) {
 | 
					func (s *SimpleServer) FetchOrCreateChunk(pos world.ChunkPos) (world.ChunkData, error) {
 | 
				
			||||||
	chunkFileName := filepath.Join(ChunkFileDirectory, pos.ToFileName())
 | 
						chunkFileName := filepath.Join(ChunkFileDirectory, pos.ToFileName())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var chunkData world.ChunkData
 | 
						var chunkData world.ChunkData
 | 
				
			||||||
@@ -43,6 +47,25 @@ func (s *SimpleServer) FetchChunk(pos world.ChunkPos) (world.ChunkData, error) {
 | 
				
			|||||||
			return chunkData, err
 | 
								return chunkData, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						defer chunkFile.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ReadChunkFromFile(chunkFile)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *SimpleServer) FetchChunk(pos world.ChunkPos) (world.ChunkData, error) {
 | 
				
			||||||
 | 
						chunkFileName := filepath.Join(ChunkFileDirectory, pos.ToFileName())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var chunkData world.ChunkData
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						chunkFile, err := os.Open(chunkFileName)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, fs.ErrNotExist) {
 | 
				
			||||||
 | 
								return chunkData, ChunkNotFoundError
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return chunkData, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer chunkFile.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return ReadChunkFromFile(chunkFile)
 | 
						return ReadChunkFromFile(chunkFile)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -53,7 +76,7 @@ func (s *SimpleServer) ChangeBlock(
 | 
				
			|||||||
	worldPosition world.BlockPos,
 | 
						worldPosition world.BlockPos,
 | 
				
			||||||
	targetState world.BlockID,
 | 
						targetState world.BlockID,
 | 
				
			||||||
) error {
 | 
					) error {
 | 
				
			||||||
	chunk, err := s.FetchChunk(worldPosition.ToChunkPos())
 | 
						chunk, err := s.FetchOrCreateChunk(worldPosition.ToChunkPos())
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -71,7 +94,7 @@ func (s *SimpleServer) ChangeBlockRange(
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *SimpleServer) ReadBlockAt(pos world.BlockPos) (world.BlockID, error) {
 | 
					func (s *SimpleServer) ReadBlockAt(pos world.BlockPos) (world.BlockID, error) {
 | 
				
			||||||
	chunk, err := s.FetchChunk(pos.ToChunkPos())
 | 
						chunk, err := s.FetchOrCreateChunk(pos.ToChunkPos())
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return world.Empty, err
 | 
							return world.Empty, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -80,5 +103,5 @@ func (s *SimpleServer) ReadBlockAt(pos world.BlockPos) (world.BlockID, error) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *SimpleServer) ReadChunkAt(pos world.ChunkPos) (world.ChunkData, error) {
 | 
					func (s *SimpleServer) ReadChunkAt(pos world.ChunkPos) (world.ChunkData, error) {
 | 
				
			||||||
	return s.FetchChunk(pos)
 | 
						return s.FetchOrCreateChunk(pos)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,66 +1,138 @@
 | 
				
			|||||||
package visualization
 | 
					package visualization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"errors"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.nicholasnovak.io/nnovak/spatial-db/storage"
 | 
				
			||||||
 | 
						"git.nicholasnovak.io/nnovak/spatial-db/world"
 | 
				
			||||||
	tea "github.com/charmbracelet/bubbletea"
 | 
						tea "github.com/charmbracelet/bubbletea"
 | 
				
			||||||
	"github.com/charmbracelet/lipgloss"
 | 
						"github.com/charmbracelet/lipgloss"
 | 
				
			||||||
	"github.com/spf13/cobra"
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						chunkWidth  = 15
 | 
				
			||||||
 | 
						chunkHeight = 5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						borderPadding = 2
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	modelStyle = lipgloss.NewStyle().
 | 
						missingChunkStyle = lipgloss.NewStyle().
 | 
				
			||||||
			Width(15).
 | 
									Width(chunkWidth).
 | 
				
			||||||
			Height(5).
 | 
									Height(chunkHeight).
 | 
				
			||||||
			Align(lipgloss.Center, lipgloss.Center).
 | 
									Align(lipgloss.Center, lipgloss.Center).
 | 
				
			||||||
			BorderStyle(lipgloss.HiddenBorder())
 | 
									BorderStyle(lipgloss.HiddenBorder()).
 | 
				
			||||||
	focusedModelStyle = lipgloss.NewStyle().
 | 
									BorderForeground(lipgloss.Color("69"))
 | 
				
			||||||
				Width(15).
 | 
						presentChunkStyle = lipgloss.NewStyle().
 | 
				
			||||||
				Height(5).
 | 
									Width(chunkWidth).
 | 
				
			||||||
 | 
									Height(chunkHeight).
 | 
				
			||||||
				Align(lipgloss.Center, lipgloss.Center).
 | 
									Align(lipgloss.Center, lipgloss.Center).
 | 
				
			||||||
				BorderStyle(lipgloss.NormalBorder()).
 | 
									BorderStyle(lipgloss.NormalBorder()).
 | 
				
			||||||
				BorderForeground(lipgloss.Color("69"))
 | 
									BorderForeground(lipgloss.Color("69"))
 | 
				
			||||||
	spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("69"))
 | 
						selectedChunkStyle = lipgloss.NewStyle().
 | 
				
			||||||
	helpStyle    = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
 | 
									Width(chunkWidth).
 | 
				
			||||||
 | 
									Height(chunkHeight).
 | 
				
			||||||
 | 
									Align(lipgloss.Center, lipgloss.Center).
 | 
				
			||||||
 | 
									BorderStyle(lipgloss.DoubleBorder()).
 | 
				
			||||||
 | 
									BorderForeground(lipgloss.Color("202"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						loadedChunkCache = make(map[world.ChunkPos]bool)
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type chunkViewerModel struct {
 | 
					type chunkViewerModel struct {
 | 
				
			||||||
	index int
 | 
						chunkServer *storage.SimpleServer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						visibleChunkRows int
 | 
				
			||||||
 | 
						visibleChunkCols int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						currentPos world.ChunkPos
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *chunkViewerModel) updateShownChunks(newWidth, newHeight int) {
 | 
				
			||||||
 | 
						m.visibleChunkRows = newHeight / (chunkHeight + borderPadding)
 | 
				
			||||||
 | 
						m.visibleChunkCols = newWidth / (chunkWidth + borderPadding)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (m chunkViewerModel) Init() tea.Cmd {
 | 
					func (m chunkViewerModel) Init() tea.Cmd {
 | 
				
			||||||
	return nil
 | 
						return tea.EnterAltScreen
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (m chunkViewerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 | 
					func (m chunkViewerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 | 
				
			||||||
	switch msg := msg.(type) {
 | 
						switch msg := msg.(type) {
 | 
				
			||||||
	case tea.KeyMsg:
 | 
						case tea.KeyMsg:
 | 
				
			||||||
		switch msg.String() {
 | 
							switch msg.String() {
 | 
				
			||||||
		case "ctrl+c", "q":
 | 
							case "ctrl+c", "q", "esc":
 | 
				
			||||||
			return m, tea.Quit
 | 
								return m, tea.Quit
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		switch msg.Type {
 | 
							switch msg.Type {
 | 
				
			||||||
		case tea.KeyRight:
 | 
							case tea.KeyRight:
 | 
				
			||||||
			// pass
 | 
								m.currentPos.X += 1
 | 
				
			||||||
 | 
							case tea.KeyLeft:
 | 
				
			||||||
 | 
								m.currentPos.X -= 1
 | 
				
			||||||
 | 
							case tea.KeyUp:
 | 
				
			||||||
 | 
								m.currentPos.Z -= 1
 | 
				
			||||||
 | 
							case tea.KeyDown:
 | 
				
			||||||
 | 
								m.currentPos.Z += 1
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
						case tea.WindowSizeMsg:
 | 
				
			||||||
 | 
							m.updateShownChunks(msg.Width, msg.Height)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return m, nil
 | 
						return m, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (m chunkViewerModel) View() string {
 | 
					func (m chunkViewerModel) View() string {
 | 
				
			||||||
	var s strings.Builder
 | 
						var s strings.Builder
 | 
				
			||||||
	if m.index == 0 {
 | 
					
 | 
				
			||||||
		s.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, focusedModelStyle.Render(fmt.Sprintf("%4s", "No")), modelStyle.Render("NO")))
 | 
						midRow := m.visibleChunkRows / 2
 | 
				
			||||||
	} else {
 | 
						midCol := m.visibleChunkCols / 2
 | 
				
			||||||
		s.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, modelStyle.Render(fmt.Sprintf("%4s", "Yes")), focusedModelStyle.Render("YES")))
 | 
					
 | 
				
			||||||
 | 
						for rowIndex := 0; rowIndex < m.visibleChunkRows; rowIndex++ {
 | 
				
			||||||
 | 
							renderedRow := make([]string, m.visibleChunkCols)
 | 
				
			||||||
 | 
							for colIndex := 0; colIndex < m.visibleChunkCols; colIndex++ {
 | 
				
			||||||
 | 
								currentChunkPos := world.ChunkPos{
 | 
				
			||||||
 | 
									X: midCol - colIndex - m.currentPos.X,
 | 
				
			||||||
 | 
									Z: midRow - rowIndex - m.currentPos.Z,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								var fetchChunkErr error
 | 
				
			||||||
 | 
								if isPresent, cached := loadedChunkCache[currentChunkPos]; cached {
 | 
				
			||||||
 | 
									if isPresent {
 | 
				
			||||||
 | 
										fetchChunkErr = nil
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										fetchChunkErr = storage.ChunkNotFoundError
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									_, fetchChunkErr = m.chunkServer.FetchChunk(currentChunkPos)
 | 
				
			||||||
 | 
									loadedChunkCache[currentChunkPos] = fetchChunkErr == nil
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								chunkDisplay := currentChunkPos.StringCoords()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if rowIndex == midRow && colIndex == midCol {
 | 
				
			||||||
 | 
									renderedRow[colIndex] = selectedChunkStyle.Render(chunkDisplay)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									if fetchChunkErr == nil {
 | 
				
			||||||
 | 
										renderedRow[colIndex] = presentChunkStyle.Render(chunkDisplay)
 | 
				
			||||||
 | 
									} else if errors.Is(fetchChunkErr, storage.ChunkNotFoundError) {
 | 
				
			||||||
 | 
										renderedRow[colIndex] = missingChunkStyle.Render(chunkDisplay)
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										panic(fetchChunkErr)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							s.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, renderedRow...) + "\n")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return s.String()
 | 
						return s.String()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func initChunkViewer() chunkViewerModel {
 | 
					func initChunkViewer(chunkServer *storage.SimpleServer) chunkViewerModel {
 | 
				
			||||||
	var model chunkViewerModel
 | 
						var model chunkViewerModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						model.chunkServer = chunkServer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return model
 | 
						return model
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -70,7 +142,13 @@ var VisualizeChunkCommand = &cobra.Command{
 | 
				
			|||||||
	Args:  cobra.ExactArgs(1),
 | 
						Args:  cobra.ExactArgs(1),
 | 
				
			||||||
	RunE: func(cmd *cobra.Command, args []string) error {
 | 
						RunE: func(cmd *cobra.Command, args []string) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		prog := tea.NewProgram(initChunkViewer())
 | 
							// Initialize the server in the specified directory
 | 
				
			||||||
 | 
							storage.ChunkFileDirectory = args[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Create a new server to read from those files
 | 
				
			||||||
 | 
							var chunkServer storage.SimpleServer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							prog := tea.NewProgram(initChunkViewer(&chunkServer), tea.WithAltScreen())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if _, err := prog.Run(); err != nil {
 | 
							if _, err := prog.Run(); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,10 +36,14 @@ type ChunkPos struct {
 | 
				
			|||||||
	Z int `json:"z"`
 | 
						Z int `json:"z"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (cp *ChunkPos) ToFileName() string {
 | 
					func (cp ChunkPos) ToFileName() string {
 | 
				
			||||||
	return fmt.Sprintf("p.%d.%d.chunk", cp.X, cp.Z)
 | 
						return fmt.Sprintf("p.%d.%d.chunk", cp.X, cp.Z)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (cp ChunkPos) StringCoords() string {
 | 
				
			||||||
 | 
						return fmt.Sprintf("%d, %d", cp.X, cp.Z)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ChunkSection struct {
 | 
					type ChunkSection struct {
 | 
				
			||||||
	// The count of full blocks in the chunk
 | 
						// The count of full blocks in the chunk
 | 
				
			||||||
	BlockCount  uint                  `json:"block_count"`
 | 
						BlockCount  uint                  `json:"block_count"`
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user