Files
sanspie 5eb1832907
ci-platforms / darwin amd64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 15m28s
ci / release (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 7m47s
Complete Stage 12 for v0.12.0
2026-11-05 11:46:53 +03:00

189 lines
5.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package slice cuts a sprite sheet into cells stored as sub-assets in its meta (D339).
package slice
import (
"cmp"
"fmt"
"slices"
"aego/asset/pipeline"
"aego/core/ids"
"aego/editor/core/history"
"aego/engine/i18n"
)
type Rect = [4]int
// Grid cuts w×h into cells of size cell, after offset, with padding between them, row by row; count 0 keeps every cell.
func Grid(w, h int, cell, offset, padding [2]int, count int) []Rect {
var out []Rect
if cell[0] <= 0 || cell[1] <= 0 || cell[0]+padding[0] <= 0 || cell[1]+padding[1] <= 0 {
return out
}
for y := offset[1]; y+cell[1] <= h; y += cell[1] + padding[1] {
for x := offset[0]; x+cell[0] <= w; x += cell[0] + padding[0] {
if count > 0 && len(out) == count {
return out
}
out = append(out, Rect{x, y, cell[0], cell[1]})
}
}
return out
}
// Alpha finds the bounding boxes of the 8-connected areas whose alpha is above threshold, in rows from the top, each row from the left.
func Alpha(rgba []byte, w, h int, threshold uint8) []Rect {
seen := make([]bool, w*h)
var out []Rect
var stack []int
solid := func(i int) bool { return rgba[i*4+3] > threshold }
for start := range w * h {
if seen[start] || !solid(start) {
continue
}
x0, y0, x1, y1 := w, h, -1, -1
stack = append(stack[:0], start)
seen[start] = true
for len(stack) > 0 {
i := stack[len(stack)-1]
stack = stack[:len(stack)-1]
x, y := i%w, i/w
x0, y0, x1, y1 = min(x0, x), min(y0, y), max(x1, x), max(y1, y)
for dy := -1; dy <= 1; dy++ {
for dx := -1; dx <= 1; dx++ {
nx, ny := x+dx, y+dy
if nx < 0 || ny < 0 || nx >= w || ny >= h {
continue
}
if j := ny*w + nx; !seen[j] && solid(j) {
seen[j] = true
stack = append(stack, j)
}
}
}
}
out = append(out, Rect{x0, y0, x1 - x0 + 1, y1 - y0 + 1})
}
return rows(out)
}
// rows orders boxes in rows: a box joins the row of the first box it overlaps vertically.
func rows(boxes []Rect) []Rect {
slices.SortFunc(boxes, func(a, b Rect) int { return cmp.Or(cmp.Compare(a[1], b[1]), cmp.Compare(a[0], b[0])) })
var out []Rect
for len(boxes) > 0 {
top := boxes[0]
var row, rest []Rect
for _, b := range boxes {
if b[1] < top[1]+top[3] {
row = append(row, b)
} else {
rest = append(rest, b)
}
}
slices.SortFunc(row, func(a, b Rect) int { return cmp.Compare(a[0], b[0]) })
out = append(out, row...)
boxes = rest
}
return out
}
// Name is the name of cell i of a sheet: hero_000, hero_001…
func Name(base string, i int) string {
return fmt.Sprintf("%s_%03d", base, i)
}
// Plan retains exact source regions and their authored metadata first, then
// matches resized cells by name. Only genuinely new cells receive new IDs.
func Plan(base string, old []pipeline.SubAsset, rects []Rect) []pipeline.SubAsset {
// Reserve exact geometry first: user-renamed frames retain their IDs,
// pivots and associated surface channels even when the slice order changes.
byRect := make(map[Rect][]pipeline.SubAsset, len(old))
byName := make(map[string]pipeline.SubAsset, len(old))
for _, f := range old {
byRect[f.Rect] = append(byRect[f.Rect], f)
byName[f.Name] = f
}
out := make([]pipeline.SubAsset, len(rects))
used := make(map[string]bool, len(rects))
for i, r := range rects {
if candidates := byRect[r]; len(candidates) != 0 {
out[i] = candidates[0]
byRect[r] = candidates[1:]
used[out[i].Name] = true
}
}
for i, r := range rects {
if !out[i].ID.IsZero() {
continue
}
name := Name(base, i)
if f, ok := byName[name]; ok && !used[name] {
// An edited cell size keeps the familiar named cell's identity.
out[i], used[name] = f, true
out[i].Rect = r
}
}
for i, r := range rects {
if !out[i].ID.IsZero() {
continue
}
name := Name(base, i)
for n := i + 1; used[name]; n++ {
name = Name(base, n)
}
out[i] = pipeline.SubAsset{Name: name, ID: ids.NewAssetID(), Rect: r}
used[name] = true
}
return out
}
// Edit writes the cells or, when AfterSettings is set, the import settings of a sheet and reimports it; Undo writes the previous ones back.
type Edit struct {
Pipeline *pipeline.Pipeline
Sheet ids.AssetID
Before []pipeline.SubAsset
After []pipeline.SubAsset
BeforeSettings pipeline.Settings
AfterSettings pipeline.Settings
Apply func(*pipeline.Batch)
}
func (e *Edit) Label() string {
if e.AfterSettings != nil {
return i18n.Tr("editor.slice.import_settings", "Import Settings")
}
return i18n.Tr("editor.slice.slice_sprite_sheet", "Slice Sprite Sheet")
}
func (e *Edit) Do(history.Target) error { return e.write(e.After, e.AfterSettings) }
func (e *Edit) Undo(history.Target) error { return e.write(e.Before, e.BeforeSettings) }
func (e *Edit) Merge(history.Command) bool {
return false
}
func (e *Edit) write(subs []pipeline.SubAsset, set pipeline.Settings) error {
var err error
if e.AfterSettings != nil {
err = e.Pipeline.SetSettings(e.Sheet, set)
} else {
err = e.Pipeline.SetSubs(e.Sheet, subs)
}
if err != nil {
return err
}
b, err := e.Pipeline.Reimport(e.Sheet)
if e.Apply != nil {
e.Apply(b)
}
if err != nil {
return err
}
if ferr := b.Failed[e.Sheet]; ferr != nil {
return ferr
}
return nil
}
var _ history.Command = (*Edit)(nil)