343 lines
8.6 KiB
Go
343 lines
8.6 KiB
Go
package clipboard
|
|
|
|
import (
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"aego/asset"
|
|
"aego/core/errs"
|
|
"aego/core/ids"
|
|
"aego/core/mathx"
|
|
"aego/editor/core/context"
|
|
"aego/editor/core/document"
|
|
"aego/editor/core/history/commands"
|
|
"aego/editor/core/props"
|
|
"aego/scene"
|
|
"aego/scene/sceneio"
|
|
"aego/schema"
|
|
"aego/ui/widgets"
|
|
)
|
|
|
|
const (
|
|
MIMEComponent = "application/x-aego-component"
|
|
MIMEComponents = "application/x-aego-components"
|
|
MIMEValue = "application/x-aego-value"
|
|
componentText = "aego-component\n"
|
|
componentsText = "aego-components\n"
|
|
)
|
|
|
|
type Kind uint8
|
|
|
|
const (
|
|
KindEmpty Kind = iota
|
|
KindNodes
|
|
KindComponent
|
|
KindValue
|
|
KindText
|
|
KindComponents
|
|
)
|
|
|
|
type Env struct {
|
|
Clip context.DataClipboard
|
|
Types *scene.Types
|
|
Res asset.Resolver
|
|
OnAdd func(*schema.TypeDescriptor) func(*scene.Scene, scene.NodeHandle)
|
|
}
|
|
|
|
func EnvOf(ctx *context.Editor) Env {
|
|
e := Env{Clip: ctx.Clipboard, Types: ctx.Types, OnAdd: ctx.OnAdd}
|
|
if ctx.Loader != nil {
|
|
e.Res = ctx.Loader
|
|
}
|
|
return e
|
|
}
|
|
|
|
type target struct {
|
|
s *scene.Scene
|
|
}
|
|
|
|
func (t target) Scene() *scene.Scene {
|
|
return t.s
|
|
}
|
|
|
|
func (e Env) Peek() Kind {
|
|
c := e.Clip
|
|
if c == nil {
|
|
return KindEmpty
|
|
}
|
|
if d, ok := c.Data(MIME); ok && len(d) > 0 {
|
|
return KindNodes
|
|
}
|
|
if d, ok := c.Data(MIMEComponents); ok && len(d) > 0 {
|
|
return KindComponents
|
|
}
|
|
if d, ok := c.Data(MIMEComponent); ok && len(d) > 0 {
|
|
return KindComponent
|
|
}
|
|
if d, ok := c.Data(MIMEValue); ok && len(d) > 0 {
|
|
return KindValue
|
|
}
|
|
t := strings.TrimSpace(c.Text())
|
|
switch {
|
|
case t == "":
|
|
return KindEmpty
|
|
case strings.HasPrefix(t, "prefab"):
|
|
return KindNodes
|
|
case strings.HasPrefix(t, strings.TrimSpace(componentsText)):
|
|
return KindComponents
|
|
case strings.HasPrefix(t, strings.TrimSpace(componentText)):
|
|
return KindComponent
|
|
}
|
|
return KindText
|
|
}
|
|
|
|
func (e Env) scratch() *scene.Scene {
|
|
return scene.New(e.Types, e.Res, scene.Options{InitialNodes: 4})
|
|
}
|
|
|
|
func componentValues(s *scene.Scene, node ids.NodeID, td *schema.TypeDescriptor) ([]commands.FieldValue, error) {
|
|
a := props.New(s, nil)
|
|
out := make([]commands.FieldValue, 0, len(td.Fields))
|
|
for i := range td.Fields {
|
|
v, err := a.Get(node, td.ID, td.Fields[i].ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, commands.FieldValue{Field: td.Fields[i].ID, Value: v})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (e Env) encodeComponent(td *schema.TypeDescriptor, values []commands.FieldValue) ([]byte, error) {
|
|
sc := e.scratch()
|
|
h := sc.Create(sc.Root(), "component")
|
|
sc.Flush()
|
|
if err := putComponent(sc, h, td, values); err != nil {
|
|
return nil, err
|
|
}
|
|
return sceneio.SaveSubtree(sc, h, true), nil
|
|
}
|
|
|
|
func (e Env) decodeComponent(data []byte) (*schema.TypeDescriptor, []commands.FieldValue, error) {
|
|
sc := e.scratch()
|
|
root, err := loadScratch(sc, data)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
reg := sc.Registry()
|
|
unknown, hasUnknown := reg.ByName("engine.Unknown")
|
|
var td *schema.TypeDescriptor
|
|
sc.ComponentsOf(sc.Handle(root), func(t schema.TypeID) {
|
|
if td != nil || (hasUnknown && t == unknown.ID) {
|
|
return
|
|
}
|
|
td, _ = reg.ByID(t)
|
|
})
|
|
if td == nil {
|
|
return nil, nil, errs.New(errs.EditorCommand, "clipboard component type is not loaded in this editor")
|
|
}
|
|
values, err := componentValues(sc, root, td)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return td, values, nil
|
|
}
|
|
|
|
func (e Env) CopyComponent(s *scene.Scene, node ids.NodeID, t schema.TypeID) error {
|
|
if e.Clip == nil {
|
|
return errs.New(errs.EditorCommand, "clipboard is unavailable")
|
|
}
|
|
td, ok := s.Registry().ByID(t)
|
|
if !ok {
|
|
return errs.New(errs.SchemaUnknownType, "component type is not registered")
|
|
}
|
|
values, err := componentValues(s, node, td)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data, err := e.encodeComponent(td, values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.Clip.SetData(MIMEComponent, data, componentText+string(data))
|
|
return nil
|
|
}
|
|
|
|
func (e Env) ReadComponent() (*schema.TypeDescriptor, []commands.FieldValue, bool) {
|
|
c := e.Clip
|
|
if c == nil {
|
|
return nil, nil, false
|
|
}
|
|
data, ok := c.Data(MIMEComponent)
|
|
if !ok || len(data) == 0 {
|
|
t := c.Text()
|
|
if !strings.HasPrefix(t, componentText) {
|
|
return nil, nil, false
|
|
}
|
|
data = []byte(strings.TrimPrefix(t, componentText))
|
|
}
|
|
td, values, err := e.decodeComponent(data)
|
|
if err != nil {
|
|
return nil, nil, false
|
|
}
|
|
return td, values, true
|
|
}
|
|
|
|
func (e Env) PasteComponent(d *document.SceneDocument, nodes []ids.NodeID) error {
|
|
td, values, ok := e.ReadComponent()
|
|
if !ok {
|
|
return errs.New(errs.EditorCommand, "clipboard does not hold a component")
|
|
}
|
|
if len(nodes) == 0 {
|
|
return errs.New(errs.EditorCommand, "select the nodes to paste the component onto")
|
|
}
|
|
tx := d.History.Begin("Paste " + td.FullName)
|
|
if err := e.pasteInto(tx, d, nodes, td, values); err != nil {
|
|
tx.Abort()
|
|
return err
|
|
}
|
|
tx.Commit()
|
|
return nil
|
|
}
|
|
|
|
func (e Env) CopyValue(s *scene.Scene, node ids.NodeID, t schema.TypeID, f schema.FieldID, text string) error {
|
|
if e.Clip == nil {
|
|
return errs.New(errs.EditorCommand, "clipboard is unavailable")
|
|
}
|
|
td, ok := s.Registry().ByID(t)
|
|
if !ok {
|
|
return errs.New(errs.SchemaUnknownType, "component type is not registered")
|
|
}
|
|
fd, ok := td.FieldByID(f)
|
|
if !ok {
|
|
return errs.New(errs.SchemaBadField, "unknown field")
|
|
}
|
|
values, err := componentValues(s, node, td)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data, err := e.encodeComponent(td, values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
payload := append([]byte(fd.Name+"\n"), data...)
|
|
e.Clip.SetData(MIMEValue, payload, text)
|
|
return nil
|
|
}
|
|
|
|
func (e Env) ReadValue(fd *schema.FieldDescriptor) (schema.Value, bool) {
|
|
c := e.Clip
|
|
if c == nil {
|
|
return schema.Value{}, false
|
|
}
|
|
if data, ok := c.Data(MIMEValue); ok && len(data) > 0 {
|
|
name, rest, found := strings.Cut(string(data), "\n")
|
|
if found {
|
|
if td, values, err := e.decodeComponent([]byte(rest)); err == nil {
|
|
for i := range td.Fields {
|
|
src := &td.Fields[i]
|
|
if src.Name != name || !compatible(fd, src) {
|
|
continue
|
|
}
|
|
for _, fv := range values {
|
|
if fv.Field == src.ID {
|
|
return fv.Value, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return parseText(fd, strings.TrimSpace(c.Text()))
|
|
}
|
|
|
|
func Peek(ctx *context.Editor) Kind {
|
|
return EnvOf(ctx).Peek()
|
|
}
|
|
|
|
func CopyComponent(ctx *context.Editor, s *scene.Scene, node ids.NodeID, t schema.TypeID) error {
|
|
return EnvOf(ctx).CopyComponent(s, node, t)
|
|
}
|
|
|
|
func ReadComponent(ctx *context.Editor) (*schema.TypeDescriptor, []commands.FieldValue, bool) {
|
|
return EnvOf(ctx).ReadComponent()
|
|
}
|
|
|
|
func PasteComponent(ctx *context.Editor, d *document.SceneDocument, nodes []ids.NodeID) error {
|
|
return EnvOf(ctx).PasteComponent(d, nodes)
|
|
}
|
|
|
|
func CopyValue(ctx *context.Editor, s *scene.Scene, node ids.NodeID, t schema.TypeID, f schema.FieldID, text string) error {
|
|
return EnvOf(ctx).CopyValue(s, node, t, f, text)
|
|
}
|
|
|
|
func ReadValue(ctx *context.Editor, fd *schema.FieldDescriptor) (schema.Value, bool) {
|
|
return EnvOf(ctx).ReadValue(fd)
|
|
}
|
|
|
|
func compatible(a, b *schema.FieldDescriptor) bool {
|
|
if a.Kind != b.Kind {
|
|
return false
|
|
}
|
|
switch a.Kind {
|
|
case schema.KindEnum:
|
|
return a.EnumName == b.EnumName
|
|
case schema.KindStruct:
|
|
return a.Struct == b.Struct
|
|
case schema.KindList:
|
|
return a.Elem == b.Elem && a.Struct == b.Struct
|
|
case schema.KindAssetRef:
|
|
return a.AssetKind == b.AssetKind || a.AssetKind == "" || b.AssetKind == ""
|
|
}
|
|
return true
|
|
}
|
|
|
|
func parseFloat(s string) (float64, bool) {
|
|
f, err := strconv.ParseFloat(strings.ReplaceAll(strings.TrimSpace(s), ",", "."), 64)
|
|
return f, err == nil && !math.IsNaN(f) && !math.IsInf(f, 0)
|
|
}
|
|
|
|
func parseText(fd *schema.FieldDescriptor, s string) (schema.Value, bool) {
|
|
if s == "" {
|
|
return schema.Value{}, false
|
|
}
|
|
switch fd.Kind {
|
|
case schema.KindBool:
|
|
b, err := strconv.ParseBool(strings.ToLower(s))
|
|
return schema.Bool(b), err == nil
|
|
case schema.KindInt32:
|
|
f, ok := parseFloat(s)
|
|
return schema.Int32(int32(math.Round(f))), ok
|
|
case schema.KindInt64:
|
|
f, ok := parseFloat(s)
|
|
return schema.Int64(int64(math.Round(f))), ok
|
|
case schema.KindFloat32:
|
|
f, ok := parseFloat(s)
|
|
return schema.Float32(float32(f)), ok
|
|
case schema.KindFloat64:
|
|
f, ok := parseFloat(s)
|
|
return schema.Float64(f), ok
|
|
case schema.KindString:
|
|
return schema.String(s), true
|
|
case schema.KindVec2:
|
|
parts := strings.FieldsFunc(s, func(r rune) bool { return r == ',' || r == ' ' || r == ';' || r == '\t' })
|
|
if len(parts) != 2 {
|
|
return schema.Value{}, false
|
|
}
|
|
x, okx := parseFloat(parts[0])
|
|
y, oky := parseFloat(parts[1])
|
|
return schema.Vec2(mathx.Vec2{X: float32(x), Y: float32(y)}), okx && oky
|
|
case schema.KindColor:
|
|
c, ok := widgets.ParseHexColor(s)
|
|
return schema.Color(c), ok
|
|
case schema.KindEnum:
|
|
for _, ev := range fd.Enum {
|
|
if strings.EqualFold(ev.Name, s) {
|
|
return schema.Enum(ev.Value), true
|
|
}
|
|
}
|
|
}
|
|
return schema.Value{}, false
|
|
}
|