729 lines
18 KiB
Go
729 lines
18 KiB
Go
package art
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"reflect"
|
|
)
|
|
|
|
// Edit is an unpublished, private COW transaction. A caller must not share an
|
|
// Edit between goroutines. Document and Change entrypoints are synchronized.
|
|
type Edit struct {
|
|
doc *Document
|
|
base *state
|
|
work *state
|
|
revision uint64
|
|
label string
|
|
done bool
|
|
mutable map[ID]map[tileKey]bool
|
|
dirty image.Rectangle
|
|
structural bool
|
|
allocated int64
|
|
lastCelID ID
|
|
lastCel int
|
|
}
|
|
|
|
func (d *Document) Begin(expected uint64, label string) (*Edit, error) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
if d.closed {
|
|
return nil, ErrClosed
|
|
}
|
|
if expected != d.revision {
|
|
return nil, ErrConflict
|
|
}
|
|
return &Edit{doc: d, base: d.current, work: d.current.clone(), revision: expected, label: label, mutable: make(map[ID]map[tileKey]bool)}, nil
|
|
}
|
|
func (e *Edit) check() error {
|
|
if e == nil || e.done {
|
|
return ErrDone
|
|
}
|
|
return nil
|
|
}
|
|
func (e *Edit) Info() Info { return info(e.doc.id, e.revision, e.work) }
|
|
func (e *Edit) Cancel() {
|
|
if e != nil {
|
|
e.done = true
|
|
e.work = nil
|
|
e.mutable = nil
|
|
}
|
|
}
|
|
func (e *Edit) DirtyRect() image.Rectangle { return e.dirty }
|
|
func (e *Edit) touch(r image.Rectangle) {
|
|
e.dirty = e.dirty.Union(r.Intersect(image.Rect(0, 0, e.work.Canvas.Width, e.work.Canvas.Height)))
|
|
}
|
|
// celIndex caches only a checked slot within this draft, never cel content.
|
|
func (e *Edit) celIndex(id ID) (int, bool) {
|
|
if id == e.lastCelID && e.lastCel >= 0 && e.lastCel < len(e.work.Cels) && e.work.Cels[e.lastCel].ID == id {
|
|
return e.lastCel, true
|
|
}
|
|
i, ok := e.work.cel(id)
|
|
if ok {
|
|
e.lastCelID, e.lastCel = id, i
|
|
}
|
|
return i, ok
|
|
}
|
|
|
|
func (e *Edit) raw(im ID, x, y int, v uint32) error {
|
|
p := e.work.Images[im]
|
|
if p == nil {
|
|
return fmt.Errorf("art: missing image")
|
|
}
|
|
if x < 0 || y < 0 || x >= p.Width || y >= p.Height {
|
|
return nil
|
|
}
|
|
if p.at(x, y) == v {
|
|
return nil
|
|
}
|
|
m := e.mutable[im]
|
|
if m == nil {
|
|
p = p.clone()
|
|
e.work.Images[im] = p
|
|
m = make(map[tileKey]bool)
|
|
e.mutable[im] = m
|
|
}
|
|
key := tileKey{x / TileSize, y / TileSize}
|
|
if !m[key] {
|
|
t, err := e.allocateTile(p.Tiles[key])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.Tiles[key] = t
|
|
m[key] = true
|
|
}
|
|
p.Tiles[key][(y%TileSize)*TileSize+x%TileSize] = v
|
|
for _, c := range e.work.Cels {
|
|
if c.Image == im {
|
|
e.touch(image.Rect(x+c.X, y+c.Y, x+c.X+1, y+c.Y+1))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Bound draft allocation before publication as well as validating the final
|
|
// canonical state in Prepare. A large proposal cannot allocate an unlimited
|
|
// number of tiles and rely on a late validation error for its only guard.
|
|
func (e *Edit) allocateTile(source *tile) (*tile, error) {
|
|
const bytes = int64(TileSize * TileSize * 4)
|
|
if e.allocated > e.doc.limits.MaxPixelBytes-bytes {
|
|
return nil, ErrLimit
|
|
}
|
|
e.allocated += bytes
|
|
block := new(tile)
|
|
if source != nil {
|
|
*block = *source
|
|
}
|
|
return block, nil
|
|
}
|
|
|
|
// expandCel makes a cropped imported image editable anywhere on the canvas.
|
|
// The image identity is preserved, so linked cels continue to share pixels.
|
|
// Their origins are shifted together; independent copies keep their old image.
|
|
func (e *Edit) expandCel(c Cel) (Cel, error) {
|
|
p := e.work.Images[c.Image]
|
|
r := image.Rect(0, 0, p.Width, p.Height).Union(
|
|
image.Rect(-c.X, -c.Y, e.work.Canvas.Width-c.X, e.work.Canvas.Height-c.Y))
|
|
if r.Dx() > e.doc.limits.MaxDimension || r.Dy() > e.doc.limits.MaxDimension ||
|
|
int64(r.Dx())*int64(r.Dy()) > int64(e.doc.limits.MaxCanvasPixels) {
|
|
return Cel{}, ErrLimit
|
|
}
|
|
q := newPixels(r.Dx(), r.Dy())
|
|
mutable := make(map[tileKey]bool)
|
|
for key, block := range p.Tiles {
|
|
for y := 0; y < TileSize; y++ {
|
|
for x := 0; x < TileSize; x++ {
|
|
v := block[y*TileSize+x]
|
|
sx, sy := key.X*TileSize+x, key.Y*TileSize+y
|
|
if v == 0 || sx >= p.Width || sy >= p.Height {
|
|
continue
|
|
}
|
|
dx, dy := sx-r.Min.X, sy-r.Min.Y
|
|
dk := tileKey{dx / TileSize, dy / TileSize}
|
|
if q.Tiles[dk] == nil {
|
|
block, err := e.allocateTile(nil)
|
|
if err != nil {
|
|
return Cel{}, err
|
|
}
|
|
q.Tiles[dk] = block
|
|
mutable[dk] = true
|
|
}
|
|
q.Tiles[dk][dy%TileSize*TileSize+dx%TileSize] = v
|
|
}
|
|
}
|
|
}
|
|
e.work.Images[c.Image] = q
|
|
e.mutable[c.Image] = mutable
|
|
for i := range e.work.Cels {
|
|
linked := &e.work.Cels[i]
|
|
if linked.Image == c.Image {
|
|
linked.X += r.Min.X
|
|
linked.Y += r.Min.Y
|
|
if linked.ID == c.ID {
|
|
c = *linked
|
|
}
|
|
}
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func (e *Edit) celForWrite(id ID) (Cel, error) {
|
|
if err := e.check(); err != nil {
|
|
return Cel{}, err
|
|
}
|
|
i, ok := e.celIndex(id)
|
|
if !ok {
|
|
return Cel{}, fmt.Errorf("art: missing cel")
|
|
}
|
|
c := e.work.Cels[i]
|
|
if e.work.isLocked(c.Layer) {
|
|
return Cel{}, ErrLocked
|
|
}
|
|
return c, nil
|
|
}
|
|
func (e *Edit) AddLayer(name string, kind LayerKind, parent ID) (ID, error) {
|
|
if err := e.check(); err != nil {
|
|
return "", err
|
|
}
|
|
if len(e.work.Layers) >= e.doc.limits.MaxLayers {
|
|
return "", ErrLimit
|
|
}
|
|
if kind != Raster && kind != Group && kind != Reference {
|
|
return "", fmt.Errorf("art: invalid layer kind")
|
|
}
|
|
if parent != "" {
|
|
i, ok := e.work.layer(parent)
|
|
if !ok || e.work.Layers[i].Kind != Group {
|
|
return "", fmt.Errorf("art: invalid group")
|
|
}
|
|
if e.work.isLocked(parent) {
|
|
return "", ErrLocked
|
|
}
|
|
}
|
|
id := e.doc.newID()
|
|
e.work.Layers = append(e.work.Layers, Layer{ID: id, Parent: parent, Name: name, Kind: kind, Visible: true, Opacity: 255, Blend: BlendNormal})
|
|
e.structural = true
|
|
return id, nil
|
|
}
|
|
func (e *Edit) SetLayer(v Layer) error {
|
|
if err := e.check(); err != nil {
|
|
return err
|
|
}
|
|
i, ok := e.work.layer(v.ID)
|
|
if !ok {
|
|
return fmt.Errorf("art: missing layer")
|
|
}
|
|
old := e.work.Layers[i]
|
|
if v.Kind != old.Kind {
|
|
return fmt.Errorf("art: layer kind cannot be changed")
|
|
}
|
|
// Unlock is an explicit structural edit; it must still respect a locked group.
|
|
if old.Parent != "" && e.work.isLocked(old.Parent) {
|
|
return ErrLocked
|
|
}
|
|
e.work.Layers[i] = v
|
|
e.structural = true
|
|
return nil
|
|
}
|
|
func (e *Edit) MoveLayer(id ID, offset int) error {
|
|
if err := e.check(); err != nil {
|
|
return err
|
|
}
|
|
i, ok := e.work.layer(id)
|
|
if !ok {
|
|
return fmt.Errorf("art: missing layer")
|
|
}
|
|
if e.work.isLocked(id) {
|
|
return ErrLocked
|
|
}
|
|
j := i + offset
|
|
if j < 0 || j >= len(e.work.Layers) {
|
|
return nil
|
|
}
|
|
if e.work.Layers[i].Parent != e.work.Layers[j].Parent {
|
|
return fmt.Errorf("art: reorder must stay within a group")
|
|
}
|
|
v := e.work.Layers[i]
|
|
if j < i {
|
|
copy(e.work.Layers[j+1:i+1], e.work.Layers[j:i])
|
|
} else {
|
|
copy(e.work.Layers[i:j], e.work.Layers[i+1:j+1])
|
|
}
|
|
e.work.Layers[j] = v
|
|
e.structural = true
|
|
return nil
|
|
}
|
|
func (e *Edit) DeleteLayer(id ID) error {
|
|
if err := e.check(); err != nil {
|
|
return err
|
|
}
|
|
if e.work.isLocked(id) {
|
|
return ErrLocked
|
|
}
|
|
gone := map[ID]bool{id: true}
|
|
for changed := true; changed; {
|
|
changed = false
|
|
for _, l := range e.work.Layers {
|
|
if gone[l.Parent] && !gone[l.ID] {
|
|
gone[l.ID] = true
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
var layers []Layer
|
|
for _, l := range e.work.Layers {
|
|
if !gone[l.ID] {
|
|
layers = append(layers, l)
|
|
}
|
|
}
|
|
if len(layers) == 0 {
|
|
return fmt.Errorf("art: cannot delete the last layer")
|
|
}
|
|
var cels []Cel
|
|
for _, c := range e.work.Cels {
|
|
if !gone[c.Layer] {
|
|
cels = append(cels, c)
|
|
}
|
|
}
|
|
e.work.Layers, e.work.Cels = layers, cels
|
|
e.structural = true
|
|
return nil
|
|
}
|
|
func (e *Edit) AddFrame(after ID, duration int) (ID, error) {
|
|
if err := e.check(); err != nil {
|
|
return "", err
|
|
}
|
|
if len(e.work.Frames) >= e.doc.limits.MaxFrames {
|
|
return "", ErrLimit
|
|
}
|
|
i := -1
|
|
if after != "" {
|
|
var ok bool
|
|
i, ok = e.work.frame(after)
|
|
if !ok {
|
|
return "", fmt.Errorf("art: missing frame")
|
|
}
|
|
}
|
|
if duration < 1 || duration > 600000 {
|
|
return "", fmt.Errorf("art: invalid frame duration")
|
|
}
|
|
id := e.doc.newID()
|
|
e.work.Frames = append(e.work.Frames, Frame{})
|
|
copy(e.work.Frames[i+2:], e.work.Frames[i+1:])
|
|
e.work.Frames[i+1] = Frame{id, duration}
|
|
e.structural = true
|
|
return id, nil
|
|
}
|
|
func (e *Edit) SetDuration(frame ID, ms int) error {
|
|
if err := e.check(); err != nil {
|
|
return err
|
|
}
|
|
i, ok := e.work.frame(frame)
|
|
if !ok || ms < 1 || ms > 600000 {
|
|
return fmt.Errorf("art: invalid frame duration")
|
|
}
|
|
e.work.Frames[i].DurationMS = ms
|
|
e.structural = true
|
|
return nil
|
|
}
|
|
func (e *Edit) DeleteFrame(id ID) error {
|
|
if err := e.check(); err != nil {
|
|
return err
|
|
}
|
|
i, ok := e.work.frame(id)
|
|
if !ok {
|
|
return fmt.Errorf("art: missing frame")
|
|
}
|
|
if len(e.work.Frames) == 1 {
|
|
return fmt.Errorf("art: cannot delete the last frame")
|
|
}
|
|
// A tag endpoint is a stable frame ID, not a numeric offset. Retarget it to
|
|
// the nearest surviving frame; remove a tag that only covered this frame.
|
|
var tags []Tag
|
|
for _, t := range e.work.Tags {
|
|
if t.From == id && t.To == id {
|
|
continue
|
|
}
|
|
if t.From == id {
|
|
t.From = e.work.Frames[i+1].ID
|
|
}
|
|
if t.To == id {
|
|
t.To = e.work.Frames[i-1].ID
|
|
}
|
|
tags = append(tags, t)
|
|
}
|
|
e.work.Tags = tags
|
|
e.work.Frames = append(e.work.Frames[:i], e.work.Frames[i+1:]...)
|
|
var cels []Cel
|
|
for _, c := range e.work.Cels {
|
|
if c.Frame != id {
|
|
cels = append(cels, c)
|
|
}
|
|
}
|
|
e.work.Cels = cels
|
|
var slices []Slice
|
|
for _, s := range e.work.Slices {
|
|
if s.Frame != id {
|
|
slices = append(slices, s)
|
|
}
|
|
}
|
|
e.work.Slices = slices
|
|
e.structural = true
|
|
return nil
|
|
}
|
|
func (e *Edit) EnsureCel(layer, frame ID, ch Channel) (ID, error) {
|
|
if err := e.check(); err != nil {
|
|
return "", err
|
|
}
|
|
if i, ok := e.work.findCel(layer, frame, ch); ok {
|
|
return e.work.Cels[i].ID, nil
|
|
}
|
|
li, ok := e.work.layer(layer)
|
|
if !ok || e.work.Layers[li].Kind == Group {
|
|
return "", fmt.Errorf("art: invalid cel layer")
|
|
}
|
|
if e.work.isLocked(layer) {
|
|
return "", ErrLocked
|
|
}
|
|
if _, ok = e.work.frame(frame); !ok || !validChannel(ch) {
|
|
return "", fmt.Errorf("art: invalid cel frame/channel")
|
|
}
|
|
if len(e.work.Cels) >= e.doc.limits.MaxCels {
|
|
return "", ErrLimit
|
|
}
|
|
im, id := e.doc.newID(), e.doc.newID()
|
|
e.work.Images[im] = newPixels(e.work.Canvas.Width, e.work.Canvas.Height)
|
|
e.work.Cels = append(e.work.Cels, Cel{ID: id, Layer: layer, Frame: frame, Image: im, Channel: ch, Opacity: 255})
|
|
e.structural = true
|
|
return id, nil
|
|
}
|
|
|
|
// shareIndependent freezes any draft-owned tiles before sharing them with an
|
|
// independent image. Without revoking ownership, a subsequent write to the
|
|
// original in this same transaction would silently change its new copy too.
|
|
func (e *Edit) shareIndependent(source, dest ID) {
|
|
p := e.work.Images[source]
|
|
e.work.Images[dest] = p
|
|
if owned := e.mutable[source]; owned != nil {
|
|
e.work.Images[source] = p.clone()
|
|
for key := range owned {
|
|
owned[key] = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// CopyCel replaces the destination cel. A normal copy gets a distinct storage
|
|
// identity but shares immutable tiles until a write; a linked copy shares ID.
|
|
func (e *Edit) CopyCel(source, layer, frame ID, linked bool) (ID, error) {
|
|
c, err := e.celForWrite(source)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if e.work.isLocked(layer) {
|
|
return "", ErrLocked
|
|
}
|
|
dst, err := e.EnsureCel(layer, frame, c.Channel)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
di, _ := e.work.cel(dst)
|
|
if dst == source {
|
|
return dst, nil
|
|
}
|
|
im := c.Image
|
|
if !linked {
|
|
im = e.doc.newID()
|
|
e.shareIndependent(c.Image, im)
|
|
}
|
|
e.work.Cels[di].Image = im
|
|
e.work.Cels[di].X = c.X
|
|
e.work.Cels[di].Y = c.Y
|
|
e.work.Cels[di].Opacity = c.Opacity
|
|
e.structural = true
|
|
return dst, nil
|
|
}
|
|
func (e *Edit) Unlink(cel ID) error {
|
|
c, err := e.celForWrite(cel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n := 0
|
|
for _, v := range e.work.Cels {
|
|
if v.Image == c.Image {
|
|
n++
|
|
}
|
|
}
|
|
if n < 2 {
|
|
return nil
|
|
}
|
|
im := e.doc.newID()
|
|
e.shareIndependent(c.Image, im)
|
|
i, _ := e.work.cel(cel)
|
|
e.work.Cels[i].Image = im
|
|
e.structural = true
|
|
return nil
|
|
}
|
|
func (e *Edit) SetCelPosition(cel ID, x, y int) error {
|
|
_, err := e.celForWrite(cel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
i, _ := e.work.cel(cel)
|
|
e.work.Cels[i].X = x
|
|
e.work.Cels[i].Y = y
|
|
e.structural = true
|
|
return nil
|
|
}
|
|
func (e *Edit) DuplicateFrame(source ID, linked bool) (ID, error) {
|
|
if err := e.check(); err != nil {
|
|
return "", err
|
|
}
|
|
i, ok := e.work.frame(source)
|
|
if !ok {
|
|
return "", fmt.Errorf("art: missing frame")
|
|
}
|
|
id, err := e.AddFrame(source, e.work.Frames[i].DurationMS)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
old := append([]Cel(nil), e.work.Cels...)
|
|
for _, c := range old {
|
|
if c.Frame == source {
|
|
if _, err = e.CopyCel(c.ID, c.Layer, id, linked); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
}
|
|
return id, nil
|
|
}
|
|
func (e *Edit) SetTag(tag Tag) (ID, error) {
|
|
if err := e.check(); err != nil {
|
|
return "", err
|
|
}
|
|
from, a := e.work.frame(tag.From)
|
|
to, b := e.work.frame(tag.To)
|
|
if !a || !b || from > to || len(tag.Name) == 0 || len(tag.Name) > 256 ||
|
|
(tag.Direction != Forward && tag.Direction != Reverse && tag.Direction != PingPong) {
|
|
return "", fmt.Errorf("art: invalid animation tag")
|
|
}
|
|
if tag.ID != "" {
|
|
for i, t := range e.work.Tags {
|
|
if t.ID == tag.ID {
|
|
e.work.Tags[i] = tag
|
|
e.structural = true
|
|
return tag.ID, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("art: missing tag")
|
|
}
|
|
tag.ID = e.doc.newID()
|
|
e.work.Tags = append(e.work.Tags, tag)
|
|
e.structural = true
|
|
return tag.ID, nil
|
|
}
|
|
func (e *Edit) SetSlice(v Slice) (ID, error) {
|
|
if err := e.check(); err != nil {
|
|
return "", err
|
|
}
|
|
if v.ID != "" {
|
|
for i, s := range e.work.Slices {
|
|
if s.ID == v.ID {
|
|
e.work.Slices[i] = v
|
|
e.structural = true
|
|
return v.ID, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("art: missing slice")
|
|
}
|
|
v.ID = e.doc.newID()
|
|
e.work.Slices = append(e.work.Slices, v)
|
|
e.structural = true
|
|
return v.ID, nil
|
|
}
|
|
|
|
func (e *Edit) DeleteSlice(id ID) error {
|
|
if err := e.check(); err != nil {
|
|
return err
|
|
}
|
|
for i, slice := range e.work.Slices {
|
|
if slice.ID == id {
|
|
e.work.Slices = append(e.work.Slices[:i], e.work.Slices[i+1:]...)
|
|
e.structural = true
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("art: missing slice")
|
|
}
|
|
|
|
func (e *Edit) collect() {
|
|
used := make(map[ID]bool)
|
|
for _, c := range e.work.Cels {
|
|
used[c.Image] = true
|
|
}
|
|
for id := range e.work.Images {
|
|
if !used[id] {
|
|
delete(e.work.Images, id)
|
|
}
|
|
}
|
|
for id, keys := range e.mutable {
|
|
p := e.work.Images[id]
|
|
if p == nil {
|
|
continue
|
|
}
|
|
before := e.base.Images[id]
|
|
for key := range keys {
|
|
t := p.Tiles[key]
|
|
if t == nil {
|
|
continue
|
|
}
|
|
if before != nil && before.Tiles[key] != nil && *t == *before.Tiles[key] {
|
|
p.Tiles[key] = before.Tiles[key]
|
|
} else if *t == (tile{}) {
|
|
delete(p.Tiles, key)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prepare seals but does not publish the transaction. Hosts route the returned
|
|
// Change through their existing history/proposal service before Apply.
|
|
func (e *Edit) Prepare() (*Change, error) {
|
|
if err := e.check(); err != nil {
|
|
return nil, err
|
|
}
|
|
e.collect()
|
|
if err := e.work.validate(e.doc.limits); err != nil {
|
|
return nil, err
|
|
}
|
|
e.doc.mu.Lock()
|
|
defer e.doc.mu.Unlock()
|
|
if e.doc.closed {
|
|
return nil, ErrClosed
|
|
}
|
|
if e.doc.revision != e.revision || e.doc.current != e.base {
|
|
return nil, ErrConflict
|
|
}
|
|
e.done = true
|
|
if reflect.DeepEqual(e.base, e.work) {
|
|
return nil, nil
|
|
}
|
|
size := retainedChangeBytes(e.base, e.work)
|
|
damage := e.dirty
|
|
if e.structural {
|
|
damage = image.Rect(0, 0, e.work.Canvas.Width, e.work.Canvas.Height)
|
|
}
|
|
return &Change{doc: e.doc, before: e.base, after: e.work, expected: e.revision, label: e.label, bytes: size, dirty: damage}, nil
|
|
}
|
|
|
|
// Change retains immutable before/after states. Undo restores both pixels and
|
|
// topology, including link groups. Revision is monotonic even across Undo.
|
|
type Change struct {
|
|
doc *Document
|
|
before, after *state
|
|
expected uint64
|
|
label string
|
|
bytes int64
|
|
dirty image.Rectangle
|
|
status uint8
|
|
}
|
|
|
|
func (c *Change) Label() string { return c.label }
|
|
func (c *Change) Bytes() int64 { return c.bytes }
|
|
func (c *Change) DirtyRect() image.Rectangle { return c.dirty }
|
|
func (c *Change) Apply() error {
|
|
d := c.doc
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
if d.closed {
|
|
return ErrClosed
|
|
}
|
|
if c.status == 1 || d.current != c.before || (c.status == 0 && d.revision != c.expected) {
|
|
return ErrConflict
|
|
}
|
|
d.current = c.after
|
|
d.revision++
|
|
c.status = 1
|
|
return nil
|
|
}
|
|
func (c *Change) Undo() error {
|
|
d := c.doc
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
if d.closed {
|
|
return ErrClosed
|
|
}
|
|
if c.status != 1 || d.current != c.after {
|
|
return ErrConflict
|
|
}
|
|
d.current = c.before
|
|
d.revision++
|
|
c.status = 2
|
|
return nil
|
|
}
|
|
|
|
// Journal is the single history host for standalone/web clients. The native
|
|
// adapter must use history.History instead, not a Journal alongside it.
|
|
type Journal struct {
|
|
undo, redo []*Change
|
|
bytes int64
|
|
maxBytes int64
|
|
maxSteps int
|
|
}
|
|
|
|
func NewJournal(l Limits) *Journal {
|
|
l = limitsOrDefault(l)
|
|
return &Journal{maxBytes: l.MaxHistoryBytes, maxSteps: l.MaxHistorySteps}
|
|
}
|
|
func (j *Journal) Exec(c *Change) error {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
if c.Bytes() > j.maxBytes {
|
|
return fmt.Errorf("art: operation exceeds undo budget")
|
|
}
|
|
if err := c.Apply(); err != nil {
|
|
return err
|
|
}
|
|
for _, r := range j.redo {
|
|
j.bytes -= r.Bytes()
|
|
}
|
|
j.redo = nil
|
|
j.undo = append(j.undo, c)
|
|
j.bytes += c.Bytes()
|
|
for len(j.undo) > j.maxSteps || j.bytes > j.maxBytes {
|
|
j.bytes -= j.undo[0].Bytes()
|
|
j.undo[0] = nil
|
|
j.undo = j.undo[1:]
|
|
}
|
|
return nil
|
|
}
|
|
func (j *Journal) Undo() error {
|
|
n := len(j.undo)
|
|
if n == 0 {
|
|
return nil
|
|
}
|
|
c := j.undo[n-1]
|
|
if err := c.Undo(); err != nil {
|
|
return err
|
|
}
|
|
j.undo[n-1] = nil
|
|
j.undo = j.undo[:n-1]
|
|
j.redo = append(j.redo, c)
|
|
return nil
|
|
}
|
|
func (j *Journal) Redo() error {
|
|
n := len(j.redo)
|
|
if n == 0 {
|
|
return nil
|
|
}
|
|
c := j.redo[n-1]
|
|
if err := c.Apply(); err != nil {
|
|
return err
|
|
}
|
|
j.redo[n-1] = nil
|
|
j.redo = j.redo[:n-1]
|
|
j.undo = append(j.undo, c)
|
|
return nil
|
|
}
|
|
func (j *Journal) Counts() (int, int) { return len(j.undo), len(j.redo) }
|
|
func (j *Journal) Clear() { j.undo = nil; j.redo = nil; j.bytes = 0 }
|