316 lines
7.1 KiB
Go
316 lines
7.1 KiB
Go
package clipboard
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"aego/core/errs"
|
|
"aego/core/ids"
|
|
"aego/core/log"
|
|
"aego/editor/core/context"
|
|
"aego/editor/core/history/commands"
|
|
"aego/format/aetext"
|
|
"aego/scene"
|
|
"aego/scene/mutate"
|
|
"aego/scene/sceneio"
|
|
)
|
|
|
|
const MIME = "application/x-aego-fragment"
|
|
|
|
func TopLevel(s *scene.Scene, nodes []ids.NodeID) []scene.NodeHandle {
|
|
root := s.Root()
|
|
set := make(map[scene.NodeHandle]bool, len(nodes))
|
|
for _, id := range nodes {
|
|
if h := s.Handle(id); !h.IsZero() && s.IsAlive(h) && h != root {
|
|
set[h] = true
|
|
}
|
|
}
|
|
if len(set) == 0 {
|
|
return nil
|
|
}
|
|
var out []scene.NodeHandle
|
|
for _, h := range s.PreOrder() {
|
|
if !set[h] {
|
|
continue
|
|
}
|
|
nested := false
|
|
for q := s.Parent(h); !q.IsZero(); q = s.Parent(q) {
|
|
if set[q] {
|
|
nested = true
|
|
break
|
|
}
|
|
}
|
|
if !nested {
|
|
out = append(out, h)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func Fragment(s *scene.Scene, nodes []ids.NodeID) ([]byte, []scene.NodeHandle, error) {
|
|
tops := TopLevel(s, nodes)
|
|
if len(tops) == 0 {
|
|
return nil, nil, errs.New(errs.EditorCommand, "nothing is selected")
|
|
}
|
|
root := &aetext.Node{Head: "prefab"}
|
|
root.Attrs = append(root.Attrs, aetext.Field{Name: "version", Value: aetext.Int(sceneio.FormatVersion)})
|
|
for _, h := range tops {
|
|
doc, err := aetext.Parse(sceneio.SaveSubtree(s, h, true), "fragment")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
root.Blocks = append(root.Blocks, doc.Blocks...)
|
|
}
|
|
return aetext.Write(root), tops, nil
|
|
}
|
|
|
|
func Valid(data []byte) bool {
|
|
doc, err := aetext.Parse(data, "fragment")
|
|
return err == nil && (doc.Head == "prefab" || doc.Head == "scene")
|
|
}
|
|
|
|
func remapKey(s string, mapping map[string]string) string {
|
|
parts := strings.Split(s, "/")
|
|
changed := false
|
|
for i, p := range parts {
|
|
if fresh, ok := mapping[p]; ok {
|
|
parts[i] = fresh
|
|
changed = true
|
|
}
|
|
}
|
|
if !changed {
|
|
return s
|
|
}
|
|
return strings.Join(parts, "/")
|
|
}
|
|
|
|
func Remap(data []byte) ([]byte, error) {
|
|
doc, err := aetext.Parse(data, "fragment")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if doc.Head != "prefab" && doc.Head != "scene" {
|
|
return nil, errs.New(errs.EditorCommand, "clipboard does not hold scene nodes", log.F("head", doc.Head))
|
|
}
|
|
mapping := map[string]string{}
|
|
var collect func(n *aetext.Node)
|
|
collect = func(n *aetext.Node) {
|
|
switch n.Head {
|
|
case "node":
|
|
if n.Label != "" {
|
|
mapping[n.Label] = ids.NewNodeID().String()
|
|
}
|
|
case "id":
|
|
if v, ok := n.Attr("node"); ok && v.Str != "" {
|
|
mapping[v.Str] = ids.NewNodeID().String()
|
|
}
|
|
}
|
|
for _, b := range n.Blocks {
|
|
collect(b)
|
|
}
|
|
}
|
|
collect(doc)
|
|
var rewrite func(n *aetext.Node)
|
|
rewrite = func(n *aetext.Node) {
|
|
switch n.Head {
|
|
case "node":
|
|
if fresh, ok := mapping[n.Label]; ok {
|
|
n.Label = fresh
|
|
}
|
|
case "override", "addComponent", "removeComponent", "addNode", "remove", "order":
|
|
n.Label = remapKey(n.Label, mapping)
|
|
}
|
|
for i := range n.Attrs {
|
|
if n.Head == "order" && n.Attrs[i].Name == "children" {
|
|
for j := range n.Attrs[i].Value.List {
|
|
item := &n.Attrs[i].Value.List[j]
|
|
item.Str = remapKey(item.Str, mapping)
|
|
}
|
|
continue
|
|
}
|
|
remapValue(&n.Attrs[i].Value, mapping)
|
|
}
|
|
for i := range n.Fields {
|
|
remapValue(&n.Fields[i].Value, mapping)
|
|
}
|
|
for _, b := range n.Blocks {
|
|
rewrite(b)
|
|
}
|
|
}
|
|
rewrite(doc)
|
|
return aetext.Write(doc), nil
|
|
}
|
|
|
|
func remapValue(v *aetext.Value, mapping map[string]string) {
|
|
switch v.Kind {
|
|
case aetext.VNode:
|
|
if fresh, ok := mapping[v.Str]; ok {
|
|
v.Str = fresh
|
|
}
|
|
case aetext.VList:
|
|
for i := range v.List {
|
|
remapValue(&v.List[i], mapping)
|
|
}
|
|
case aetext.VStruct:
|
|
for i := range v.Obj {
|
|
remapValue(&v.Obj[i].Value, mapping)
|
|
}
|
|
}
|
|
}
|
|
|
|
func Copy(ctx *context.Editor) error {
|
|
s := ctx.Scene()
|
|
if s == nil || ctx.Clipboard == nil {
|
|
return nil
|
|
}
|
|
data, _, err := Fragment(s, ctx.Selected())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx.Clipboard.SetData(MIME, data, string(data))
|
|
return nil
|
|
}
|
|
|
|
func Cut(ctx *context.Editor) error {
|
|
if err := Copy(ctx); err != nil {
|
|
return err
|
|
}
|
|
return Delete(ctx)
|
|
}
|
|
|
|
func clipboardData(ctx *context.Editor) ([]byte, error) {
|
|
if ctx.Clipboard == nil {
|
|
return nil, errs.New(errs.EditorCommand, "clipboard is unavailable")
|
|
}
|
|
if data, ok := ctx.Clipboard.Data(MIME); ok && len(data) > 0 {
|
|
return data, nil
|
|
}
|
|
text := strings.TrimSpace(ctx.Clipboard.Text())
|
|
if strings.HasPrefix(text, "prefab") && Valid([]byte(text)) {
|
|
return []byte(text), nil
|
|
}
|
|
return nil, errs.New(errs.EditorCommand, "clipboard does not hold scene nodes")
|
|
}
|
|
|
|
func pasteTarget(ctx *context.Editor) (ids.NodeID, int) {
|
|
d := ctx.Doc()
|
|
s := d.Scene()
|
|
it, ok := ctx.Selection.Active()
|
|
if !ok || it.Doc != d.ID {
|
|
return ids.NodeID{}, -1
|
|
}
|
|
h := s.Handle(it.Node)
|
|
if h.IsZero() || !s.IsAlive(h) {
|
|
return ids.NodeID{}, -1
|
|
}
|
|
return s.ID(s.Parent(h)), commands.ChildIndex(s, h) + 1
|
|
}
|
|
|
|
func Paste(ctx *context.Editor) error {
|
|
d := ctx.Doc()
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
switch Peek(ctx) {
|
|
case KindComponent:
|
|
nodes := append([]ids.NodeID(nil), ctx.Selected()...)
|
|
if len(nodes) == 0 {
|
|
ctx.Notify(context.LevelInfo, "Select the nodes to paste the component onto")
|
|
return nil
|
|
}
|
|
return PasteComponent(ctx, d, nodes)
|
|
case KindComponents:
|
|
nodes := append([]ids.NodeID(nil), ctx.Selected()...)
|
|
if len(nodes) == 0 {
|
|
ctx.Notify(context.LevelInfo, "Select the nodes to paste the components onto")
|
|
return nil
|
|
}
|
|
return EnvOf(ctx).PasteComponents(d, nodes)
|
|
case KindNodes:
|
|
default:
|
|
ctx.Notify(context.LevelInfo, "The clipboard holds nothing that can be pasted into the scene")
|
|
return nil
|
|
}
|
|
data, err := clipboardData(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data, err = Remap(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
frags, err := commands.Fragments(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
parent, index := pasteTarget(ctx)
|
|
ops := make([]mutate.Op, len(frags))
|
|
for i, f := range frags {
|
|
at := -1
|
|
if index >= 0 {
|
|
at = index + i
|
|
}
|
|
ops[i] = commands.Insert(parent, at, f)
|
|
}
|
|
cmd := commands.NewMutation("Create Paste", commands.EnvFor(d.Scene()), ops...)
|
|
if err := d.History.Exec(cmd); err != nil {
|
|
return err
|
|
}
|
|
ctx.Select(cmd.Created()...)
|
|
return nil
|
|
}
|
|
|
|
func Duplicate(ctx *context.Editor) error {
|
|
d := ctx.Doc()
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
s := d.Scene()
|
|
tops := TopLevel(s, ctx.Selected())
|
|
if len(tops) == 0 {
|
|
return nil
|
|
}
|
|
tx := d.History.Begin("Duplicate")
|
|
var created []ids.NodeID
|
|
for _, h := range tops {
|
|
data, _, err := Fragment(s, []ids.NodeID{s.ID(h)})
|
|
if err == nil {
|
|
data, err = Remap(data)
|
|
}
|
|
if err != nil {
|
|
tx.Abort()
|
|
return err
|
|
}
|
|
c := commands.NewMutation("Create "+s.Name(h), commands.EnvFor(s),
|
|
commands.Insert(s.ID(s.Parent(h)), commands.ChildIndex(s, h)+1, string(data)))
|
|
if err := tx.Exec(c); err != nil {
|
|
tx.Abort()
|
|
return err
|
|
}
|
|
created = append(created, c.Created()...)
|
|
}
|
|
tx.Commit()
|
|
ctx.Select(created...)
|
|
return nil
|
|
}
|
|
|
|
func Delete(ctx *context.Editor) error {
|
|
d := ctx.Doc()
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
s := d.Scene()
|
|
tops := TopLevel(s, ctx.Selected())
|
|
if len(tops) == 0 {
|
|
return nil
|
|
}
|
|
ops := make([]mutate.Op, len(tops))
|
|
for i, h := range tops {
|
|
ops[i] = mutate.RemoveNode(commands.Ref(s.ID(h)))
|
|
}
|
|
if err := d.History.Exec(commands.NewMutation("Delete", commands.EnvFor(s), ops...)); err != nil {
|
|
return err
|
|
}
|
|
ctx.Select()
|
|
return nil
|
|
}
|