Files
aego/art/transform.go
sanspie a46347e2c6
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 12m19s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 23m57s
ci / release (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: failure
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
editor: add art studio and stage 12.5 workflows
2026-11-08 18:47:57 +03:00

195 lines
5.1 KiB
Go

package art
import (
"fmt"
"image"
)
type Clipboard struct {
Width, Height int
Origin image.Point
Pixels []Color
Mask []bool
}
func (d *Document) Copy(cel ID, sel *Selection) (*Clipboard, error) {
d.mu.Lock()
defer d.mu.Unlock()
if d.closed {
return nil, ErrClosed
}
return copyPixels(d.current, cel, sel)
}
func copyPixels(s *state, cel ID, sel *Selection) (*Clipboard, error) {
i, ok := s.cel(cel)
if !ok {
return nil, fmt.Errorf("art: missing cel")
}
c := s.Cels[i]
p := s.Images[c.Image]
r := image.Rect(0, 0, s.Canvas.Width, s.Canvas.Height)
if sel != nil {
if sel.Width != s.Canvas.Width || sel.Height != s.Canvas.Height {
return nil, fmt.Errorf("art: selection dimensions mismatch")
}
r = sel.Bounds()
}
if r.Empty() {
return nil, fmt.Errorf("art: selection is empty")
}
out := &Clipboard{Width: r.Dx(), Height: r.Dy(), Origin: r.Min, Pixels: make([]Color, r.Dx()*r.Dy()), Mask: make([]bool, r.Dx()*r.Dy())}
for y := 0; y < out.Height; y++ {
for x := 0; x < out.Width; x++ {
at := y*out.Width + x
out.Mask[at] = sel.Contains(x+r.Min.X, y+r.Min.Y)
if out.Mask[at] {
out.Pixels[at] = decode(s.forChannel(c.Channel), p.at(x+r.Min.X-c.X, y+r.Min.Y-c.Y))
}
}
}
return out, nil
}
func (e *Edit) Clear(cel ID, sel *Selection) error {
c, err := e.celForWrite(cel)
if err != nil {
return err
}
for y := 0; y < e.work.Canvas.Height; y++ {
for x := 0; x < e.work.Canvas.Width; x++ {
if err = e.write(c, x, y, 0, sel, true); err != nil {
return err
}
}
}
return nil
}
func (e *Edit) Paste(cel ID, clip *Clipboard, at image.Point, sel *Selection) error {
c, err := e.celForWrite(cel)
if err != nil {
return err
}
if clip == nil || clip.Width < 1 || clip.Height < 1 || clip.Width > e.doc.limits.MaxDimension || clip.Height > e.doc.limits.MaxDimension || len(clip.Pixels) != clip.Width*clip.Height || (len(clip.Mask) != 0 && len(clip.Mask) != len(clip.Pixels)) {
return fmt.Errorf("art: invalid clipboard")
}
if !clampPoint(at) {
return ErrLimit
}
for y := 0; y < clip.Height; y++ {
for x := 0; x < clip.Width; x++ {
i := y*clip.Width + x
if len(clip.Mask) > 0 && !clip.Mask[i] {
continue
}
if err = e.write(c, x+at.X, y+at.Y, encode(e.work.forChannel(c.Channel), clip.Pixels[i]), sel, false); err != nil {
return err
}
}
}
return nil
}
func (c *Clipboard) Transform(kind string, width, height int) (*Clipboard, error) {
if c == nil || c.Width < 1 || c.Height < 1 || len(c.Pixels) != c.Width*c.Height || (len(c.Mask) != 0 && len(c.Mask) != len(c.Pixels)) {
return nil, fmt.Errorf("art: invalid clipboard")
}
w, h := c.Width, c.Height
switch kind {
case "rotate-cw", "rotate-ccw":
w, h = h, w
case "flip-x", "flip-y":
case "scale":
w, h = width, height
default:
return nil, fmt.Errorf("art: unsupported transform %q", kind)
}
if w < 1 || h < 1 || w > 4096 || h > 4096 || int64(w)*int64(h) > 4194304 {
return nil, ErrLimit
}
out := &Clipboard{Width: w, Height: h, Origin: c.Origin, Pixels: make([]Color, w*h), Mask: make([]bool, w*h)}
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
sx, sy := x, y
switch kind {
case "flip-x":
sx = c.Width - 1 - x
case "flip-y":
sy = c.Height - 1 - y
case "rotate-cw":
sx, sy = y, c.Height-1-x
case "rotate-ccw":
sx, sy = c.Width-1-y, x
case "scale":
sx, sy = x*c.Width/w, y*c.Height/h
}
dst, src := y*w+x, sy*c.Width+sx
out.Pixels[dst] = c.Pixels[src]
out.Mask[dst] = len(c.Mask) == 0 || c.Mask[src]
}
}
return out, nil
}
func (e *Edit) Transform(cel ID, sel *Selection, kind string, width, height, dx, dy int) error {
if _, err := e.celForWrite(cel); err != nil {
return err
}
clip, err := copyPixels(e.work, cel, sel)
if err != nil {
return err
}
out := clip
if kind != "move" {
out, err = clip.Transform(kind, width, height)
if err != nil {
return err
}
}
if err = e.Clear(cel, sel); err != nil {
return err
}
return e.Paste(cel, out, clip.Origin.Add(image.Pt(dx, dy)), nil)
}
// DuplicateLayer clones a raster/reference layer and its cels. Link groups are
// preserved within the duplicate but never share storage identity with source.
func (e *Edit) DuplicateLayer(id ID) (ID, error) {
if err := e.check(); err != nil {
return "", err
}
i, ok := e.work.layer(id)
if !ok {
return "", fmt.Errorf("art: missing layer")
}
source := e.work.Layers[i]
if source.Kind == Group {
return "", fmt.Errorf("art: duplicate a raster layer, not a group")
}
dest, err := e.AddLayer(source.Name+" copy", source.Kind, source.Parent)
if err != nil {
return "", err
}
di, _ := e.work.layer(dest)
source.ID = dest
source.Locked = false
e.work.Layers[di] = source
remap := make(map[ID]ID)
old := append([]Cel(nil), e.work.Cels...)
for _, c := range old {
if c.Layer != id {
continue
}
if len(e.work.Cels) >= e.doc.limits.MaxCels {
return "", ErrLimit
}
im := remap[c.Image]
if im == "" {
im = e.doc.newID()
remap[c.Image] = im
e.shareIndependent(c.Image, im)
}
c.ID = e.doc.newID()
c.Layer = dest
c.Image = im
e.work.Cels = append(e.work.Cels, c)
}
return dest, nil
}