288 lines
8.4 KiB
Go
288 lines
8.4 KiB
Go
package state
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"time"
|
|
|
|
"aego/core/errs"
|
|
"aego/core/ids"
|
|
"aego/core/log"
|
|
"aego/core/mathx"
|
|
"aego/editor/core/document"
|
|
"aego/format/aetext"
|
|
)
|
|
|
|
const (
|
|
UserDir = ".aego/user"
|
|
EditorFile = "editor.aetext"
|
|
LayoutFile = "layout.aetext"
|
|
Version = 1
|
|
)
|
|
|
|
type Doc struct {
|
|
Path string
|
|
Camera document.Camera
|
|
Mode int
|
|
Orbit document.Orbit
|
|
Expanded []ids.NodeID
|
|
Selection []ids.NodeID
|
|
}
|
|
|
|
type Editor struct {
|
|
Docs []Doc
|
|
Active string
|
|
Workspace string
|
|
PlayDiff string
|
|
Tool int
|
|
Grid bool
|
|
Snap bool
|
|
SnapMove float32
|
|
SnapRotate float32
|
|
SnapScale float32
|
|
WindowW int
|
|
WindowH int
|
|
Collapsed []string
|
|
Watches []string
|
|
Autosave bool
|
|
InlayHints bool
|
|
Minimap bool
|
|
RainbowBrackets bool
|
|
HighlightBlock bool
|
|
}
|
|
|
|
func Default() *Editor {
|
|
return &Editor{PlayDiff: "if-changes", Grid: true, SnapMove: 0.5, SnapRotate: 15, SnapScale: 0.1, Autosave: true, InlayHints: true, Minimap: true, RainbowBrackets: true, HighlightBlock: true}
|
|
}
|
|
|
|
func userPath(root, name string) string {
|
|
return filepath.Join(root, filepath.FromSlash(UserDir), name)
|
|
}
|
|
|
|
func WriteEvents(root string, data []byte) (string, error) {
|
|
p := userPath(root, "events.log")
|
|
return p, document.WriteAtomic(p, data)
|
|
}
|
|
|
|
func WriteTrace(root string, data []byte) (string, error) {
|
|
p := userPath(root, filepath.Join("traces", "trace-"+time.Now().Format("20060102-150405")+".json"))
|
|
return p, document.WriteAtomic(p, data)
|
|
}
|
|
|
|
func Load(root string) (*Editor, error) {
|
|
data, err := os.ReadFile(userPath(root, EditorFile))
|
|
if os.IsNotExist(err) {
|
|
return Default(), nil
|
|
}
|
|
if err != nil {
|
|
return Default(), errs.Wrap(err, errs.EditorDocument, "cannot read the editor state")
|
|
}
|
|
return Decode(data, EditorFile)
|
|
}
|
|
|
|
func Save(root string, s *Editor) error {
|
|
return document.WriteAtomic(userPath(root, EditorFile), Encode(s))
|
|
}
|
|
|
|
func LoadLayout(root string) ([]byte, bool) {
|
|
data, err := os.ReadFile(userPath(root, LayoutFile))
|
|
return data, err == nil
|
|
}
|
|
|
|
func SaveLayout(root string, data []byte) error {
|
|
return document.WriteAtomic(userPath(root, LayoutFile), data)
|
|
}
|
|
|
|
func RemoveLayout(root string) error {
|
|
err := os.Remove(userPath(root, LayoutFile))
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func SortNodes(list []ids.NodeID) {
|
|
sort.Slice(list, func(i, j int) bool {
|
|
if list[i].Hi != list[j].Hi {
|
|
return list[i].Hi < list[j].Hi
|
|
}
|
|
return list[i].Lo < list[j].Lo
|
|
})
|
|
}
|
|
|
|
func nodeList(list []ids.NodeID) aetext.Value {
|
|
items := make([]aetext.Value, len(list))
|
|
for i, id := range list {
|
|
items[i] = aetext.NodeRef(id.String())
|
|
}
|
|
return aetext.List(items...)
|
|
}
|
|
|
|
func readNodes(v aetext.Value) []ids.NodeID {
|
|
var out []ids.NodeID
|
|
for _, item := range v.List {
|
|
if id, err := ids.ParseNodeID(item.Str); err == nil {
|
|
out = append(out, id)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func Encode(s *Editor) []byte {
|
|
root := &aetext.Node{Head: "editor"}
|
|
root.Attrs = append(root.Attrs, aetext.Field{Name: "version", Value: aetext.Int(Version)})
|
|
root.Fields = append(root.Fields,
|
|
aetext.Field{Name: "active", Value: aetext.String(s.Active)},
|
|
aetext.Field{Name: "workspace", Value: aetext.String(s.Workspace)},
|
|
aetext.Field{Name: "play_diff", Value: aetext.String(s.PlayDiff)},
|
|
aetext.Field{Name: "tool", Value: aetext.Int(int64(s.Tool))},
|
|
aetext.Field{Name: "grid", Value: aetext.Bool(s.Grid)},
|
|
aetext.Field{Name: "snap", Value: aetext.Bool(s.Snap)},
|
|
aetext.Field{Name: "snapMove", Value: aetext.Float32(s.SnapMove)},
|
|
aetext.Field{Name: "snapRotate", Value: aetext.Float32(s.SnapRotate)},
|
|
aetext.Field{Name: "snapScale", Value: aetext.Float32(s.SnapScale)},
|
|
aetext.Field{Name: "window", Value: aetext.List(aetext.Int(int64(s.WindowW)), aetext.Int(int64(s.WindowH)))},
|
|
aetext.Field{Name: "autosave", Value: aetext.Bool(s.Autosave)},
|
|
aetext.Field{Name: "inlayHints", Value: aetext.Bool(s.InlayHints)},
|
|
aetext.Field{Name: "minimap", Value: aetext.Bool(s.Minimap)},
|
|
aetext.Field{Name: "codeRainbow", Value: aetext.Bool(s.RainbowBrackets)},
|
|
aetext.Field{Name: "highlightBlock", Value: aetext.Bool(s.HighlightBlock)},
|
|
)
|
|
collapsed := make([]aetext.Value, len(s.Collapsed))
|
|
for i, k := range s.Collapsed {
|
|
collapsed[i] = aetext.String(k)
|
|
}
|
|
root.Fields = append(root.Fields, aetext.Field{Name: "collapsed", Value: aetext.List(collapsed...)})
|
|
watches := make([]aetext.Value, len(s.Watches))
|
|
for i, w := range s.Watches {
|
|
watches[i] = aetext.String(w)
|
|
}
|
|
root.Fields = append(root.Fields, aetext.Field{Name: "watches", Value: aetext.List(watches...)})
|
|
for _, d := range s.Docs {
|
|
o := d.Orbit
|
|
n := &aetext.Node{Head: "doc", Label: d.Path}
|
|
n.Fields = append(n.Fields,
|
|
aetext.Field{Name: "camera", Value: aetext.List(aetext.Float32(d.Camera.Position.X), aetext.Float32(d.Camera.Position.Y))},
|
|
aetext.Field{Name: "zoom", Value: aetext.Float32(d.Camera.Zoom)},
|
|
aetext.Field{Name: "rotation", Value: aetext.Float32(d.Camera.Rotation)},
|
|
aetext.Field{Name: "mode", Value: aetext.Int(int64(d.Mode))},
|
|
aetext.Field{Name: "orbit", Value: aetext.List(
|
|
aetext.Float32(o.Target.X), aetext.Float32(o.Target.Y), aetext.Float32(o.Target.Z),
|
|
aetext.Float32(o.Distance), aetext.Float32(o.Yaw), aetext.Float32(o.Pitch), aetext.Float32(o.FOV),
|
|
)},
|
|
aetext.Field{Name: "expanded", Value: nodeList(d.Expanded)},
|
|
aetext.Field{Name: "selection", Value: nodeList(d.Selection)},
|
|
)
|
|
if o.Ortho {
|
|
n.Fields = append(n.Fields, aetext.Field{Name: "ortho", Value: aetext.Bool(true)})
|
|
}
|
|
root.Blocks = append(root.Blocks, n)
|
|
}
|
|
return aetext.Write(root)
|
|
}
|
|
|
|
func Decode(data []byte, name string) (*Editor, error) {
|
|
doc, err := aetext.Parse(data, name)
|
|
if err != nil {
|
|
return Default(), err
|
|
}
|
|
if doc.Head != "editor" {
|
|
return Default(), errs.New(errs.EditorDocument, "not an editor state file", log.F("head", doc.Head))
|
|
}
|
|
if v, ok := doc.Attr("version"); ok && int(v.AsInt()) > Version {
|
|
return Default(), errs.New(errs.EditorDocument, "editor state was written by a newer editor")
|
|
}
|
|
s := Default()
|
|
if v, ok := doc.Field("active"); ok {
|
|
s.Active = v.Str
|
|
}
|
|
if v, ok := doc.Field("workspace"); ok {
|
|
s.Workspace = v.Str
|
|
}
|
|
if v, ok := doc.Field("play_diff"); ok && v.Str != "" {
|
|
s.PlayDiff = v.Str
|
|
}
|
|
if v, ok := doc.Field("tool"); ok {
|
|
s.Tool = int(v.AsInt())
|
|
}
|
|
if v, ok := doc.Field("grid"); ok {
|
|
s.Grid = v.Bool
|
|
}
|
|
if v, ok := doc.Field("snap"); ok {
|
|
s.Snap = v.Bool
|
|
}
|
|
if v, ok := doc.Field("snapMove"); ok {
|
|
s.SnapMove = float32(v.AsFloat())
|
|
}
|
|
if v, ok := doc.Field("snapRotate"); ok {
|
|
s.SnapRotate = float32(v.AsFloat())
|
|
}
|
|
if v, ok := doc.Field("snapScale"); ok {
|
|
s.SnapScale = float32(v.AsFloat())
|
|
}
|
|
if v, ok := doc.Field("window"); ok && len(v.List) == 2 {
|
|
s.WindowW, s.WindowH = int(v.List[0].AsInt()), int(v.List[1].AsInt())
|
|
}
|
|
if v, ok := doc.Field("autosave"); ok {
|
|
s.Autosave = v.Bool
|
|
}
|
|
if v, ok := doc.Field("inlayHints"); ok {
|
|
s.InlayHints = v.Bool
|
|
}
|
|
if v, ok := doc.Field("minimap"); ok {
|
|
s.Minimap = v.Bool
|
|
}
|
|
if v, ok := doc.Field("codeRainbow"); ok {
|
|
s.RainbowBrackets = v.Bool
|
|
}
|
|
if v, ok := doc.Field("highlightBlock"); ok {
|
|
s.HighlightBlock = v.Bool
|
|
}
|
|
if v, ok := doc.Field("collapsed"); ok {
|
|
for _, item := range v.List {
|
|
s.Collapsed = append(s.Collapsed, item.Str)
|
|
}
|
|
}
|
|
if v, ok := doc.Field("watches"); ok {
|
|
for _, item := range v.List {
|
|
s.Watches = append(s.Watches, item.Str)
|
|
}
|
|
}
|
|
doc.EachBlock("doc", func(n *aetext.Node) {
|
|
d := Doc{Path: n.Label, Camera: document.Camera{Zoom: 64}}
|
|
if v, ok := n.Field("camera"); ok && len(v.List) == 2 {
|
|
d.Camera.Position = mathx.Vec2{X: float32(v.List[0].AsFloat()), Y: float32(v.List[1].AsFloat())}
|
|
}
|
|
if v, ok := n.Field("zoom"); ok && v.AsFloat() > 0 {
|
|
d.Camera.Zoom = float32(v.AsFloat())
|
|
}
|
|
if v, ok := n.Field("rotation"); ok {
|
|
d.Camera.Rotation = float32(v.AsFloat())
|
|
}
|
|
if v, ok := n.Field("mode"); ok {
|
|
d.Mode = int(v.AsInt())
|
|
}
|
|
if v, ok := n.Field("orbit"); ok && len(v.List) == 7 {
|
|
f := func(i int) float32 { return float32(v.List[i].AsFloat()) }
|
|
d.Orbit = document.Orbit{
|
|
Target: mathx.Vec3{X: f(0), Y: f(1), Z: f(2)},
|
|
Distance: f(3), Yaw: f(4), Pitch: f(5), FOV: f(6),
|
|
}
|
|
}
|
|
if v, ok := n.Field("ortho"); ok {
|
|
d.Orbit.Ortho = v.Bool
|
|
}
|
|
if v, ok := n.Field("expanded"); ok {
|
|
d.Expanded = readNodes(v)
|
|
}
|
|
if v, ok := n.Field("selection"); ok {
|
|
d.Selection = readNodes(v)
|
|
}
|
|
if d.Path != "" {
|
|
s.Docs = append(s.Docs, d)
|
|
}
|
|
})
|
|
return s, nil
|
|
}
|