diff --git a/aniphallow.lua b/aniphallow.lua index f92f5ab..714e55f 100644 --- a/aniphallow.lua +++ b/aniphallow.lua @@ -18,13 +18,14 @@ local DEFAULT_ANIM_SPEED = 200 local DEFAULT_PREVIEW_ZOOM = 4 local DEFAULT_SOURCE_ZOOM = 2 local MAX_PREVIEW_ZOOM = 10 -local THUMB_SIZE = 16 +local THUMB_SIZE = 24 local SOURCE_VIEWPORT_W = 300 -local SOURCE_VIEWPORT_H = 250 +local SOURCE_VIEWPORT_H = 400 local MAX_ANIMS = 20 -- max number of dynamic animations local TABS = { "Animations", "Preview", "GB" } -local DEFAULT_GB_TILE = 8 -- Default Game Boy tile size +local DEFAULT_GB_TILE_W = 8 -- Default Game Boy tile width +local DEFAULT_GB_TILE_H = 8 -- Default Game Boy tile height local GB_COLS = 16 -- tiles per row in optimized image (128px = GB standard) ---------------------------------------------------------------------- @@ -79,7 +80,15 @@ local S = { gbLastSavePath = "", -- remembers last PNG export path gbLayerName = "auto-optimized-tiles", -- default layer name gbAlwaysOverwrite = false, -- overwrite layer checkbox - gbTileSize = DEFAULT_GB_TILE, -- configurable tile size for GB analysis + gbTileW = DEFAULT_GB_TILE_W, -- configurable tile width for GB analysis + gbTileH = DEFAULT_GB_TILE_H, -- configurable tile height for GB analysis + -- Configurable view heights + sourceViewH = 400, -- configurable source canvas height + gbSrcViewH = 150, -- configurable occurrences canvas height + gbOptViewH = 120, -- configurable optimized canvas height + -- Preview offsets + previewOffsets = {}, -- name -> {x=0, y=0} for preview visual offsets + previewSelected = nil, -- name of selected animation in Preview tab -- Click vs drag for GB canvases gbOptClickX = 0, gbOptClickY = 0, @@ -104,9 +113,9 @@ local S = { ---------------------------------------------------------------------- -- Tab widget IDs (for show/hide) ---------------------------------------------------------------------- --- Setup tab elements +-- Setup tab elements (btnConfig removed so it stays visible on all tabs) local SETUP_IDS = { - "btnConfig", "sepAnims", "cmbAnimList", "btnNewAnim", "btnDelAnim", "btnRenameAnim", + "sepAnims", "cmbAnimList", "btnNewAnim", "btnDelAnim", "btnRenameAnim", "btnCloneAnim", "sepSource", "canvasSource", "sepFrames", "canvasStrips", "sepActions", "btnFlipX", "btnFlipY", "btnMoveLeft", "btnMoveRight", "btnRemoveFrame", @@ -115,12 +124,13 @@ local SETUP_IDS = { -- Render tab elements local RENDER_IDS = { "sepRender", "btnSeparateWindow", "canvasRender", + "pvBtnUp", "pvBtnDown", "pvBtnLeft", "pvBtnRight", "pvBtnReset", } --- GB tab elements +-- GB tab elements (reordered: Flip, Offset, Silhouette, Compress) local GB_IDS = { - "sepGb", "btnAnalyze", "gbFlipOpt", "gbOffsetOpt", "gbCompress", - "gbSilhouette", "gbAnalyzeMode", + "sepGb", "btnAnalyze", "gbFlipOpt", "gbOffsetOpt", "gbSilhouette", "gbCompress", + "gbAnalyzeMode", "gbSimilarThreshold", "btnFindSimilar", "sepGbSource", "canvasGbSource", "sepGbOpt", "canvasGbOpt", @@ -203,7 +213,29 @@ local function loadPrefs() if k == "gbLastSavePath" then S.gbLastSavePath = v end if k == "gbLayerName" then S.gbLayerName = v end if k == "gbAlwaysOverwrite" then S.gbAlwaysOverwrite = (v == "true") end - if k == "gbTileSize" then S.gbTileSize = tonumber(v) or DEFAULT_GB_TILE end + -- Load gbTileW and gbTileH + if k == "gbTileW" then S.gbTileW = tonumber(v) or DEFAULT_GB_TILE_W end + if k == "gbTileH" then S.gbTileH = tonumber(v) or DEFAULT_GB_TILE_H end + -- Backwards compatibility: load old gbTileSize into both W and H + if k == "gbTileSize" then + local ts = tonumber(v) or DEFAULT_GB_TILE_W + S.gbTileW = ts + S.gbTileH = ts + end + -- Load configurable view heights + if k == "sourceViewH" then S.sourceViewH = tonumber(v) or 400 end + if k == "gbSrcViewH" then S.gbSrcViewH = tonumber(v) or 150 end + if k == "gbOptViewH" then S.gbOptViewH = tonumber(v) or 120 end + -- Load preview offsets: pvOff_=x,y + if k then + local pvName = string.match(k, "^pvOff_(.+)$") + if pvName then + local ox, oy = string.match(v, "([%d-]+),([%d-]+)") + if ox and oy then + S.previewOffsets[pvName] = { x = tonumber(ox) or 0, y = tonumber(oy) or 0 } + end + end + end -- Dynamic anim frames: anim_0, anim_1, ... local animIdx = k and string.match(k, "^anim_(%d+)$") if animIdx then @@ -267,7 +299,16 @@ local function savePrefs() f:write("gbLastSavePath=" .. S.gbLastSavePath .. "\n") f:write("gbLayerName=" .. S.gbLayerName .. "\n") f:write("gbAlwaysOverwrite=" .. tostring(S.gbAlwaysOverwrite) .. "\n") - f:write("gbTileSize=" .. S.gbTileSize .. "\n") + f:write("gbTileW=" .. S.gbTileW .. "\n") + f:write("gbTileH=" .. S.gbTileH .. "\n") + -- Save configurable view heights + f:write("sourceViewH=" .. S.sourceViewH .. "\n") + f:write("gbSrcViewH=" .. S.gbSrcViewH .. "\n") + f:write("gbOptViewH=" .. S.gbOptViewH .. "\n") + -- Save preview offsets + for name, off in pairs(S.previewOffsets) do + f:write("pvOff_" .. name .. "=" .. off.x .. "," .. off.y .. "\n") + end for i, name in ipairs(S.animNames) do f:write("anim_" .. (i - 1) .. "=" .. serializeFrames(S.anims[name] or {}) .. "\n") end @@ -341,7 +382,7 @@ end local function clampScroll() local contentW, contentH = getSourceContentSize() local maxScrollX = math.max(0, contentW - SOURCE_VIEWPORT_W) - local maxScrollY = math.max(0, contentH - SOURCE_VIEWPORT_H) + local maxScrollY = math.max(0, contentH - S.sourceViewH) S.scrollX = clamp(S.scrollX, 0, maxScrollX) S.scrollY = clamp(S.scrollY, 0, maxScrollY) end @@ -373,15 +414,14 @@ end ---------------------------------------------------------------------- --- GB Tile Hashing & Deduplication (parameterized by tileSize) +-- GB Tile Hashing & Deduplication (parameterized by tw, th) ---------------------------------------------------------------------- -- Hash a tile at position (sx,sy) in source image -local function tileHash(img, sx, sy, tileSize) - local ts = tileSize or S.gbTileSize +local function tileHash(img, sx, sy, tw, th) local parts = {} - for y = 0, ts - 1 do - for x = 0, ts - 1 do + for y = 0, th - 1 do + for x = 0, tw - 1 do local rx, ry = sx + x, sy + y if rx < img.width and ry < img.height then parts[#parts + 1] = tostring(img:getPixel(rx, ry)) @@ -394,11 +434,10 @@ local function tileHash(img, sx, sy, tileSize) end -- Hash with horizontal flip -local function tileHashFlipH(img, sx, sy, tileSize) - local ts = tileSize or S.gbTileSize +local function tileHashFlipH(img, sx, sy, tw, th) local parts = {} - for y = 0, ts - 1 do - for x = ts - 1, 0, -1 do + for y = 0, th - 1 do + for x = tw - 1, 0, -1 do local rx, ry = sx + x, sy + y if rx < img.width and ry < img.height then parts[#parts + 1] = tostring(img:getPixel(rx, ry)) @@ -411,11 +450,10 @@ local function tileHashFlipH(img, sx, sy, tileSize) end -- Hash with vertical flip -local function tileHashFlipV(img, sx, sy, tileSize) - local ts = tileSize or S.gbTileSize +local function tileHashFlipV(img, sx, sy, tw, th) local parts = {} - for y = ts - 1, 0, -1 do - for x = 0, ts - 1 do + for y = th - 1, 0, -1 do + for x = 0, tw - 1 do local rx, ry = sx + x, sy + y if rx < img.width and ry < img.height then parts[#parts + 1] = tostring(img:getPixel(rx, ry)) @@ -428,11 +466,10 @@ local function tileHashFlipV(img, sx, sy, tileSize) end -- Hash with both flips -local function tileHashFlipHV(img, sx, sy, tileSize) - local ts = tileSize or S.gbTileSize +local function tileHashFlipHV(img, sx, sy, tw, th) local parts = {} - for y = ts - 1, 0, -1 do - for x = ts - 1, 0, -1 do + for y = th - 1, 0, -1 do + for x = tw - 1, 0, -1 do local rx, ry = sx + x, sy + y if rx < img.width and ry < img.height then parts[#parts + 1] = tostring(img:getPixel(rx, ry)) @@ -445,10 +482,9 @@ local function tileHashFlipHV(img, sx, sy, tileSize) end -- Check if a tile is fully transparent -local function isTileEmpty(img, sx, sy, tileSize) - local ts = tileSize or S.gbTileSize - for y = 0, ts - 1 do - for x = 0, ts - 1 do +local function isTileEmpty(img, sx, sy, tw, th) + for y = 0, th - 1 do + for x = 0, tw - 1 do if pc.rgbaA(img:getPixel(sx + x, sy + y)) > 0 then return false end @@ -458,19 +494,19 @@ local function isTileEmpty(img, sx, sy, tileSize) end -- Check if tile B (at bx,by) is tile A (at ax,ay) placed with offset+flip -local function tilesMatchOffset(img, ax, ay, bx, by, ox, oy, flipH, flipV, tileSize) - local ts = tileSize or S.gbTileSize - local t = ts - 1 - for y = 0, t do - for x = 0, t do +local function tilesMatchOffset(img, ax, ay, bx, by, ox, oy, flipH, flipV, tw, th) + local txMax = tw - 1 + local tyMax = th - 1 + for y = 0, tyMax do + for x = 0, txMax do local pb = img:getPixel(bx + x, by + y) local bPainted = pc.rgbaA(pb) > 0 local axp = x - ox local ayp = y - oy - if axp >= 0 and axp <= t and ayp >= 0 and ayp <= t then - local srcAx = flipH and (t - axp) or axp - local srcAy = flipV and (t - ayp) or ayp + if axp >= 0 and axp <= txMax and ayp >= 0 and ayp <= tyMax then + local srcAx = flipH and (txMax - axp) or axp + local srcAy = flipV and (tyMax - ayp) or ayp local pa = img:getPixel(ax + srcAx, ay + srcAy) local aPainted = pc.rgbaA(pa) > 0 if bPainted ~= aPainted then return false end @@ -484,14 +520,13 @@ local function tilesMatchOffset(img, ax, ay, bx, by, ox, oy, flipH, flipV, tileS end -- Analyze source image and build deduplicated tile list -local function analyzeTiles(img, flipOpt, offsetOpt, tileSize) - local ts = tileSize or S.gbTileSize +local function analyzeTiles(img, flipOpt, offsetOpt, tw, th) local tiles = {} local hashIndex = {} local totalCount = 0 - local cols = math.floor(img.width / ts) - local rows = math.floor(img.height / ts) + local cols = math.floor(img.width / tw) + local rows = math.floor(img.height / th) local flipModes = { {false, false, "none"} } if flipOpt then @@ -502,12 +537,12 @@ local function analyzeTiles(img, flipOpt, offsetOpt, tileSize) for row = 0, rows - 1 do for col = 0, cols - 1 do - local sx = col * ts - local sy = row * ts + local sx = col * tw + local sy = row * th - if isTileEmpty(img, sx, sy, ts) then goto continueAnalyze end + if isTileEmpty(img, sx, sy, tw, th) then goto continueAnalyze end - local hash = tileHash(img, sx, sy, ts) + local hash = tileHash(img, sx, sy, tw, th) totalCount = totalCount + 1 if hashIndex[hash] then @@ -517,9 +552,9 @@ local function analyzeTiles(img, flipOpt, offsetOpt, tileSize) end if flipOpt then - local hashH = tileHashFlipH(img, sx, sy, ts) - local hashV = tileHashFlipV(img, sx, sy, ts) - local hashHV = tileHashFlipHV(img, sx, sy, ts) + local hashH = tileHashFlipH(img, sx, sy, tw, th) + local hashV = tileHashFlipV(img, sx, sy, tw, th) + local hashHV = tileHashFlipHV(img, sx, sy, tw, th) if hashIndex[hashH] then table.insert(tiles[hashIndex[hashH]].positions, {x = sx, y = sy, flip = "h", ox = 0, oy = 0}) @@ -540,13 +575,13 @@ local function analyzeTiles(img, flipOpt, offsetOpt, tileSize) for ti = 1, #tiles do local posA = tiles[ti].positions[1] for _, fm in ipairs(flipModes) do - for oy = -(ts - 1), ts - 1 do - for ox = -(ts - 1), ts - 1 do - if ox == 0 and oy == 0 then goto continueOff end + for ofy = -(th - 1), th - 1 do + for ofx = -(tw - 1), tw - 1 do + if ofx == 0 and ofy == 0 then goto continueOff end if tilesMatchOffset(img, posA.x, posA.y, - sx, sy, ox, oy, fm[1], fm[2], ts) then + sx, sy, ofx, ofy, fm[1], fm[2], tw, th) then table.insert(tiles[ti].positions, - {x = sx, y = sy, flip = fm[3], ox = ox, oy = oy}) + {x = sx, y = sy, flip = fm[3], ox = ofx, oy = ofy}) found = true end if found then break end @@ -577,7 +612,8 @@ local function analyzeTilesTileMode(sprite, frameNum) local tiles = {} local hashIndex = {} local totalCount = 0 - local tileSize = S.gbTileSize + local tileW = S.gbTileW + local tileH = S.gbTileH local ok, err = pcall(function() for _, layer in ipairs(sprite.layers) do @@ -590,7 +626,8 @@ local function analyzeTilesTileMode(sprite, frameNum) if not tileset then goto continueLayer end local ts = tileset.grid.tileSize - tileSize = ts.width -- assume square tiles + tileW = ts.width + tileH = ts.height local celImg = cel.image local celPos = cel.position @@ -652,25 +689,25 @@ local function analyzeTilesTileMode(sprite, frameNum) app.alert("Tile mode error: " .. tostring(err)) end - return tiles, totalCount, tileSize + return tiles, totalCount, tileW, tileH end -- Compare two tile regions with offset and optional flip -local function tileSimilarityEx(img, ax, ay, bx, by, ox, oy, flipH, flipV, tileSize) - local ts = tileSize or S.gbTileSize +local function tileSimilarityEx(img, ax, ay, bx, by, ox, oy, flipH, flipV, tw, th) local matches = 0 local paintedA = 0 - local t = ts - 1 - for y = 0, t do - for x = 0, t do + local txMax = tw - 1 + local tyMax = th - 1 + for y = 0, tyMax do + for x = 0, txMax do local pa = img:getPixel(ax + x, ay + y) if pc.rgbaA(pa) > 0 then paintedA = paintedA + 1 local bxp = x - ox local byp = y - oy - if bxp >= 0 and bxp <= t and byp >= 0 and byp <= t then - local srcBx = flipH and (t - bxp) or bxp - local srcBy = flipV and (t - byp) or byp + if bxp >= 0 and bxp <= txMax and byp >= 0 and byp <= tyMax then + local srcBx = flipH and (txMax - bxp) or bxp + local srcBy = flipV and (tyMax - byp) or byp local pb = img:getPixel(bx + srcBx, by + srcBy) if pa == pb then matches = matches + 1 @@ -683,11 +720,10 @@ local function tileSimilarityEx(img, ax, ay, bx, by, ox, oy, flipH, flipV, tileS end -- Count non-transparent pixels in a tile -local function countPaintedPixels(img, sx, sy, tileSize) - local ts = tileSize or S.gbTileSize +local function countPaintedPixels(img, sx, sy, tw, th) local count = 0 - for y = 0, ts - 1 do - for x = 0, ts - 1 do + for y = 0, th - 1 do + for x = 0, tw - 1 do if pc.rgbaA(img:getPixel(sx + x, sy + y)) > 0 then count = count + 1 end @@ -697,8 +733,7 @@ local function countPaintedPixels(img, sx, sy, tileSize) end -- Find pairs of unique tiles that are similar (above threshold percentage) -local function findSimilarTiles(img, tiles, thresholdPct, flipOpt, offsetOpt, tileSize) - local ts = tileSize or S.gbTileSize +local function findSimilarTiles(img, tiles, thresholdPct, flipOpt, offsetOpt, tw, th) local results = {} local flipModes = { {false, false, "none"} } if flipOpt then @@ -710,9 +745,9 @@ local function findSimilarTiles(img, tiles, thresholdPct, flipOpt, offsetOpt, ti local offsets = { {0, 0} } if offsetOpt then offsets = {} - for oy = -(ts - 1), ts - 1 do - for ox = -(ts - 1), ts - 1 do - table.insert(offsets, {ox, oy}) + for ofy = -(th - 1), th - 1 do + for ofx = -(tw - 1), tw - 1 do + table.insert(offsets, {ofx, ofy}) end end end @@ -720,7 +755,7 @@ local function findSimilarTiles(img, tiles, thresholdPct, flipOpt, offsetOpt, ti local paintedCount = {} for i = 1, #tiles do local p = tiles[i].positions[1] - paintedCount[i] = countPaintedPixels(img, p.x, p.y, ts) + paintedCount[i] = countPaintedPixels(img, p.x, p.y, tw, th) end for i = 1, #tiles do @@ -744,7 +779,7 @@ local function findSimilarTiles(img, tiles, thresholdPct, flipOpt, offsetOpt, ti for _, off in ipairs(offsets) do local m = tileSimilarityEx( img, posA.x, posA.y, posB.x, posB.y, - off[1], off[2], fm[1], fm[2], ts) + off[1], off[2], fm[1], fm[2], tw, th) if m > bestMatch then bestMatch = m bestFlip = fm[3] @@ -774,17 +809,16 @@ local function findSimilarTiles(img, tiles, thresholdPct, flipOpt, offsetOpt, ti end -- Build optimized tileset image -local function buildOptimizedImage(img, tiles, compress, silhouette, silhouetteColor, tileSize) - local ts = tileSize or S.gbTileSize - local srcCols = math.floor(img.width / ts) +local function buildOptimizedImage(img, tiles, compress, silhouette, silhouetteColor, tw, th) + local srcCols = math.floor(img.width / tw) if srcCols < 1 then srcCols = 1 end local numTiles = #tiles if numTiles == 0 then return nil, srcCols end if compress then local imgRows = math.ceil(numTiles / srcCols) - local optW = srcCols * ts - local optH = imgRows * ts + local optW = srcCols * tw + local optH = imgRows * th local optImg = Image(optW, optH, ColorMode.RGB) optImg:clear(pc.rgba(0, 0, 0, 0)) @@ -793,11 +827,11 @@ local function buildOptimizedImage(img, tiles, compress, silhouette, silhouetteC local pos = tile.positions[1] local destCol = (i - 1) % srcCols local destRow = math.floor((i - 1) / srcCols) - local dx = destCol * ts - local dy = destRow * ts + local dx = destCol * tw + local dy = destRow * th - for y = 0, ts - 1 do - for x = 0, ts - 1 do + for y = 0, th - 1 do + for x = 0, tw - 1 do local rx, ry = pos.x + x, pos.y + y if rx < img.width and ry < img.height then optImg:putPixel(dx + x, dy + y, img:getPixel(rx, ry)) @@ -819,8 +853,8 @@ local function buildOptimizedImage(img, tiles, compress, silhouette, silhouetteC for _, tile in ipairs(tiles) do local pos = tile.positions[1] - for y = 0, ts - 1 do - for x = 0, ts - 1 do + for y = 0, th - 1 do + for x = 0, tw - 1 do local rx, ry = pos.x + x, pos.y + y if rx < img.width and ry < img.height then optImg:putPixel(rx, ry, img:getPixel(rx, ry)) @@ -837,8 +871,8 @@ local function buildOptimizedImage(img, tiles, compress, silhouette, silhouetteC for _, tile in ipairs(tiles) do for pi = 2, #tile.positions do local pos = tile.positions[pi] - for y = 0, ts - 1 do - for x = 0, ts - 1 do + for y = 0, th - 1 do + for x = 0, tw - 1 do local rx, ry = pos.x + x, pos.y + y if rx >= 0 and rx < img.width and ry >= 0 and ry < img.height then local srcPx = img:getPixel(rx, ry) @@ -871,6 +905,9 @@ local function run() return end + -- Force tab to Animations on start + S.currentTab = "Animations" + local animTimer = nil local refreshTimer = nil @@ -893,7 +930,7 @@ local function run() local contentW, contentH = getSourceContentSize() local viewW = math.min(contentW, SOURCE_VIEWPORT_W) - local viewH = math.min(contentH, SOURCE_VIEWPORT_H) + local viewH = math.min(contentH, S.sourceViewH) local dlg = Dialog{ title = "AniPhallow - Animation Builder", @@ -972,6 +1009,23 @@ local function run() local label = (t == tab) and ("[" .. t .. "]") or (" " .. t .. " ") dlg:modify{ id = "tab" .. t, text = label } end + -- If not Preview tab, deselect preview and hide buttons + if tab ~= "Preview" then + S.previewSelected = nil + pcall(function() dlg:modify{ id = "pvBtnUp", visible = false } end) + pcall(function() dlg:modify{ id = "pvBtnDown", visible = false } end) + pcall(function() dlg:modify{ id = "pvBtnLeft", visible = false } end) + pcall(function() dlg:modify{ id = "pvBtnRight", visible = false } end) + pcall(function() dlg:modify{ id = "pvBtnReset", visible = false } end) + else + -- On Preview tab, show buttons only if something is selected + local pvVis = (S.previewSelected ~= nil) + pcall(function() dlg:modify{ id = "pvBtnUp", visible = pvVis } end) + pcall(function() dlg:modify{ id = "pvBtnDown", visible = pvVis } end) + pcall(function() dlg:modify{ id = "pvBtnLeft", visible = pvVis } end) + pcall(function() dlg:modify{ id = "pvBtnRight", visible = pvVis } end) + pcall(function() dlg:modify{ id = "pvBtnReset", visible = pvVis } end) + end dlg:repaint() end @@ -994,21 +1048,21 @@ local function run() ---------------------------------------------------------------- ---------------------------------------------------------------- - -- Config button (opens sub-dialog) + -- Config button (opens sub-dialog) - visible on ALL tabs ---------------------------------------------------------------- dlg:button{ id = "btnConfig", text = "Config", onclick = function() local d = Dialog{ title = "Config" } - d:number{ id = "tileW", label = "Tile W:", text = tostring(S.tileW), decimals = 0 } - d:number{ id = "tileH", label = "Tile H:", text = tostring(S.tileH), decimals = 0 } + d:number{ id = "tileW", label = "Sprite W:", text = tostring(S.tileW), decimals = 0 } + d:number{ id = "tileH", label = "Sprite H:", text = tostring(S.tileH), decimals = 0 } d:slider{ id = "animSpeed", label = "Speed (ms):", min = 50, max = 1000, value = S.animSpeed } - d:number{ id = "previewZoom", label = "Zoom:", text = tostring(S.previewZoom), decimals = 0 } d:check{ id = "useBgColor", text = "Solid background", selected = S.useBgColor } d:color{ id = "bgColor", label = "Bg Color:", color = S.bgColor } d:separator{ text = "GB Options" } - d:number{ id = "gbTileSize", label = "GB Tile Size:", text = tostring(S.gbTileSize), decimals = 0 } + d:number{ id = "gbTileW", label = "GB Tile W:", text = tostring(S.gbTileW), decimals = 0 } + d:number{ id = "gbTileH", label = "GB Tile H:", text = tostring(S.gbTileH), decimals = 0 } local silColor = S.gbSilhouetteColor if not S.gbSilhouetteColorSet then silColor = getDarkestPaletteColor() @@ -1016,6 +1070,10 @@ local function run() d:color{ id = "silhouetteColor", label = "Silhouette:", color = silColor } d:entry{ id = "layerName", label = "Layer name:", text = S.gbLayerName } d:check{ id = "alwaysOverwrite", text = "Always overwrite layer", selected = S.gbAlwaysOverwrite } + d:separator{ text = "View Heights" } + d:number{ id = "sourceViewH", label = "Source View H:", text = tostring(S.sourceViewH), decimals = 0 } + d:number{ id = "gbSrcViewH", label = "Occurrences View H:", text = tostring(S.gbSrcViewH), decimals = 0 } + d:number{ id = "gbOptViewH", label = "Optimized View H:", text = tostring(S.gbOptViewH), decimals = 0 } d:button{ id = "ok", text = "OK" } d:button{ text = "Cancel" } d:show() @@ -1028,18 +1086,38 @@ local function run() S.tileH = th S.animSpeed = d.data.animSpeed if animTimer then animTimer.interval = S.animSpeed / 1000.0 end - local pz = d.data.previewZoom - if pz < 1 then pz = 1 elseif pz > MAX_PREVIEW_ZOOM then pz = MAX_PREVIEW_ZOOM end - S.previewZoom = pz S.useBgColor = d.data.useBgColor S.bgColor = d.data.bgColor - local gts = d.data.gbTileSize - if gts < 1 then gts = 1 elseif gts > 128 then gts = 128 end - S.gbTileSize = gts + local gtw = d.data.gbTileW + if gtw < 1 then gtw = 1 elseif gtw > 128 then gtw = 128 end + S.gbTileW = gtw + local gth = d.data.gbTileH + if gth < 1 then gth = 1 elseif gth > 128 then gth = 128 end + S.gbTileH = gth S.gbSilhouetteColor = d.data.silhouetteColor S.gbSilhouetteColorSet = true S.gbLayerName = d.data.layerName S.gbAlwaysOverwrite = d.data.alwaysOverwrite + -- View heights + local oldSrcH = S.sourceViewH + local oldGbSrcH = S.gbSrcViewH + local oldGbOptH = S.gbOptViewH + local svh = d.data.sourceViewH + if svh < 50 then svh = 50 elseif svh > 600 then svh = 600 end + S.sourceViewH = svh + local gsvh = d.data.gbSrcViewH + if gsvh < 50 then gsvh = 50 elseif gsvh > 600 then gsvh = 600 end + S.gbSrcViewH = gsvh + local govh = d.data.gbOptViewH + if govh < 50 then govh = 50 elseif govh > 600 then govh = 600 end + S.gbOptViewH = govh + -- If view heights changed, rebuild dialog + if svh ~= oldSrcH or gsvh ~= oldGbSrcH or govh ~= oldGbOptH then + savePrefs() + dlg:close() + run() + return + end dlg:repaint() end end @@ -1069,7 +1147,7 @@ local function run() dlg:button{ id = "btnNewAnim", - text = "Add", + text = "+", onclick = function() local d = Dialog{ title = "New Animation" } d:entry{ id = "name", label = "Name:", text = "" } @@ -1096,7 +1174,7 @@ local function run() dlg:button{ id = "btnDelAnim", - text = "Remove", + text = "-", onclick = function() local name = currentAnimName() if not name then return end @@ -1134,6 +1212,11 @@ local function run() S.anims[newName] = S.anims[name] S.anims[name] = nil S.animNames[S.currentAnim] = newName + -- Transfer preview offset if exists + if S.previewOffsets[name] then + S.previewOffsets[newName] = S.previewOffsets[name] + S.previewOffsets[name] = nil + end -- Re-sort alphabetically table.sort(S.animNames, function(a, b) return a:lower() < b:lower() end) for i, n in ipairs(S.animNames) do @@ -1147,6 +1230,45 @@ local function run() end } + dlg:button{ + id = "btnCloneAnim", + text = "Clone", + onclick = function() + local name = currentAnimName() + if not name then return end + local d = Dialog{ title = "Clone Animation" } + d:entry{ id = "cloneName", label = "Name:", text = name .. "_clone" } + d:check{ id = "flipX", text = "Flip X", selected = true } + d:button{ id = "ok", text = "OK" } + d:button{ text = "Cancel" } + d:show() + if d.data.ok then + local cloneName = d.data.cloneName + if cloneName and cloneName ~= "" and not S.anims[cloneName] then + local srcFrames = S.anims[name] or {} + local newFrames = {} + for _, fr in ipairs(srcFrames) do + local nf = { x = fr.x, y = fr.y, flipped = fr.flipped, flippedV = fr.flippedV } + if d.data.flipX then + nf.flipped = not nf.flipped + end + table.insert(newFrames, nf) + end + table.insert(S.animNames, cloneName) + S.anims[cloneName] = newFrames + -- Re-sort alphabetically + table.sort(S.animNames, function(a, b) return a:lower() < b:lower() end) + for i, n in ipairs(S.animNames) do + if n == cloneName then S.currentAnim = i; break end + end + savePrefs() + dlg:close() + run() + end + end + end + } + ---------------------------------------------------------------- -- Source canvas (L-click=add, R-click=add flipped) ---------------------------------------------------------------- @@ -1161,7 +1283,7 @@ local function run() local gc = ev.context local cW, cH = getSourceContentSize() local vw = math.min(cW, SOURCE_VIEWPORT_W) - local vh = math.min(cH, SOURCE_VIEWPORT_H) + local vh = math.min(cH, S.sourceViewH) drawCheckerboard(gc, vw, vh, S.sourceZoom) @@ -1196,13 +1318,13 @@ local function run() -- Highlight tiles assigned to current animation local name = currentAnimName() if name and S.anims[name] then - for _, f in ipairs(S.anims[name]) do - local rx = f.x * S.sourceZoom - S.scrollX - local ry = f.y * S.sourceZoom - S.scrollY + for _, fr in ipairs(S.anims[name]) do + local rx = fr.x * S.sourceZoom - S.scrollX + local ry = fr.y * S.sourceZoom - S.scrollY local rw = S.tileW * S.sourceZoom local rh = S.tileH * S.sourceZoom if rx + rw > 0 and rx < vw and ry + rh > 0 and ry < vh then - gc.color = f.flipped + gc.color = fr.flipped and Color(255, 160, 0, 160) or Color(100, 200, 255, 160) gc:fillRect(Rectangle(rx, ry, rw, 2)) @@ -1309,10 +1431,10 @@ local function run() local fimg = getFrameImage(name, i) if fimg then - -- Maintain aspect ratio within thumbnail - local scale = math.min(THUMB_SIZE / S.tileW, THUMB_SIZE / S.tileH) - local dw = math.max(1, math.floor(S.tileW * scale)) - local dh = math.max(1, math.floor(S.tileH * scale)) + -- Pixel-perfect integer scaling + local intScale = math.max(1, math.floor(math.min(THUMB_SIZE / S.tileW, THUMB_SIZE / S.tileH))) + local dw = S.tileW * intScale + local dh = S.tileH * intScale local dx = tx + math.floor((THUMB_SIZE - dw) / 2) local dy = ty + math.floor((THUMB_SIZE - dh) / 2) gc:drawImage( @@ -1425,7 +1547,7 @@ local function run() dlg:button{ id = "btnFlipX", - text = "Flip X", + text = "FlipX", onclick = function() local name = currentAnimName() if not name or S.selectedFrame < 1 then return end @@ -1438,7 +1560,7 @@ local function run() dlg:button{ id = "btnFlipY", - text = "Flip Y", + text = "FlipY", onclick = function() local name = currentAnimName() if not name or S.selectedFrame < 1 then return end @@ -1451,7 +1573,7 @@ local function run() dlg:button{ id = "btnMoveLeft", - text = "Move Left", + text = "<-", onclick = function() local name = currentAnimName() if not name or S.selectedFrame < 2 then return end @@ -1466,7 +1588,7 @@ local function run() dlg:button{ id = "btnMoveRight", - text = "Move Right", + text = "->", onclick = function() local name = currentAnimName() if not name or S.selectedFrame < 1 then return end @@ -1481,7 +1603,7 @@ local function run() dlg:button{ id = "btnRemoveFrame", - text = "Remove", + text = "Del", onclick = function() local name = currentAnimName() if not name then return end @@ -1556,17 +1678,17 @@ local function run() previewDlg:canvas{ id = "pvCanvas", width = SOURCE_VIEWPORT_W, - height = SOURCE_VIEWPORT_H, + height = S.sourceViewH, autoscaling = false, onpaint = function(ev) local gc = ev.context local numAnims = #S.animNames if numAnims == 0 then return end - local tw = S.tileW * S.previewZoom - local th = S.tileH * S.previewZoom - local cellW = tw + RENDER_MARGIN * 2 - local cellH = th + RENDER_MARGIN * 2 + local ptw = S.tileW * S.previewZoom + local pth = S.tileH * S.previewZoom + local cellW = ptw + RENDER_MARGIN * 2 + local cellH = pth + RENDER_MARGIN * 2 local cols = 2 local af = pvAnimFrame.value @@ -1576,11 +1698,16 @@ local function run() local ox = col * cellW local oy = row * cellH + -- Apply preview offsets + local pvOff = S.previewOffsets[name] + local offX = pvOff and (pvOff.x * S.previewZoom) or 0 + local offY = pvOff and (pvOff.y * S.previewZoom) or 0 + if S.useBgColor then gc.color = S.bgColor - gc:fillRect(Rectangle(ox + RENDER_MARGIN, oy + RENDER_MARGIN, tw, th)) + gc:fillRect(Rectangle(ox + RENDER_MARGIN, oy + RENDER_MARGIN, ptw, pth)) else - drawCheckerboard(gc, tw, th, S.previewZoom, + drawCheckerboard(gc, ptw, pth, S.previewZoom, ox + RENDER_MARGIN, oy + RENDER_MARGIN) end @@ -1593,7 +1720,7 @@ local function run() gc:drawImage( fimg, Rectangle(0, 0, S.tileW, S.tileH), - Rectangle(ox + RENDER_MARGIN, oy + RENDER_MARGIN, tw, th) + Rectangle(ox + RENDER_MARGIN + offX, oy + RENDER_MARGIN + offY, ptw, pth) ) end end @@ -1623,7 +1750,7 @@ local function run() dlg:canvas{ id = "canvasRender", width = SOURCE_VIEWPORT_W, - height = SOURCE_VIEWPORT_H, + height = S.sourceViewH, autoscaling = false, visible = false, onpaint = function(ev) @@ -1631,10 +1758,10 @@ local function run() local numAnims = #S.animNames if numAnims == 0 then return end - local tw = S.tileW * S.previewZoom - local th = S.tileH * S.previewZoom - local cellW = tw + RENDER_MARGIN * 2 - local cellH = th + RENDER_MARGIN * 2 + local ptw = S.tileW * S.previewZoom + local pth = S.tileH * S.previewZoom + local cellW = ptw + RENDER_MARGIN * 2 + local cellH = pth + RENDER_MARGIN * 2 local cols = 2 for i, name in ipairs(S.animNames) do @@ -1643,11 +1770,16 @@ local function run() local ox = col * cellW local oy = row * cellH + -- Apply preview offsets + local pvOff = S.previewOffsets[name] + local offX = pvOff and (pvOff.x * S.previewZoom) or 0 + local offY = pvOff and (pvOff.y * S.previewZoom) or 0 + if S.useBgColor then gc.color = S.bgColor - gc:fillRect(Rectangle(ox + RENDER_MARGIN, oy + RENDER_MARGIN, tw, th)) + gc:fillRect(Rectangle(ox + RENDER_MARGIN, oy + RENDER_MARGIN, ptw, pth)) else - drawCheckerboard(gc, tw, th, S.previewZoom, + drawCheckerboard(gc, ptw, pth, S.previewZoom, ox + RENDER_MARGIN, oy + RENDER_MARGIN) end @@ -1660,10 +1792,56 @@ local function run() gc:drawImage( fimg, Rectangle(0, 0, S.tileW, S.tileH), - Rectangle(ox + RENDER_MARGIN, oy + RENDER_MARGIN, tw, th) + Rectangle(ox + RENDER_MARGIN + offX, oy + RENDER_MARGIN + offY, ptw, pth) ) end end + + -- Draw border around selected animation + if S.previewSelected == name then + gc.color = Color(255, 255, 0, 220) + gc:fillRect(Rectangle(ox, oy, cellW, 2)) + gc:fillRect(Rectangle(ox, oy + cellH - 2, cellW, 2)) + gc:fillRect(Rectangle(ox, oy, 2, cellH)) + gc:fillRect(Rectangle(ox + cellW - 2, oy, 2, cellH)) + end + end + end, + onmousedown = function(ev) + if ev.button == MouseButton.LEFT then + local numAnims = #S.animNames + if numAnims == 0 then return end + + local ptw = S.tileW * S.previewZoom + local pth = S.tileH * S.previewZoom + local cellW = ptw + RENDER_MARGIN * 2 + local cellH = pth + RENDER_MARGIN * 2 + local cols = 2 + + for i, name in ipairs(S.animNames) do + local col = (i - 1) % cols + local row = math.floor((i - 1) / cols) + local ox = col * cellW + local oy = row * cellH + + if ev.x >= ox and ev.x < ox + cellW and ev.y >= oy and ev.y < oy + cellH then + -- Toggle selection + if S.previewSelected == name then + S.previewSelected = nil + else + S.previewSelected = name + end + -- Show/hide preview offset buttons + local pvVis = (S.previewSelected ~= nil) + pcall(function() dlg:modify{ id = "pvBtnUp", visible = pvVis } end) + pcall(function() dlg:modify{ id = "pvBtnDown", visible = pvVis } end) + pcall(function() dlg:modify{ id = "pvBtnLeft", visible = pvVis } end) + pcall(function() dlg:modify{ id = "pvBtnRight", visible = pvVis } end) + pcall(function() dlg:modify{ id = "pvBtnReset", visible = pvVis } end) + dlg:repaint() + return + end + end end end, onwheel = function(ev) @@ -1673,12 +1851,81 @@ local function run() end } + -- Preview offset buttons (hidden by default) + dlg:button{ + id = "pvBtnUp", + text = "Up", + visible = false, + onclick = function() + if not S.previewSelected then return end + if not S.previewOffsets[S.previewSelected] then + S.previewOffsets[S.previewSelected] = { x = 0, y = 0 } + end + S.previewOffsets[S.previewSelected].y = S.previewOffsets[S.previewSelected].y - 1 + savePrefs() + dlg:repaint() + end + } + dlg:button{ + id = "pvBtnDown", + text = "Down", + visible = false, + onclick = function() + if not S.previewSelected then return end + if not S.previewOffsets[S.previewSelected] then + S.previewOffsets[S.previewSelected] = { x = 0, y = 0 } + end + S.previewOffsets[S.previewSelected].y = S.previewOffsets[S.previewSelected].y + 1 + savePrefs() + dlg:repaint() + end + } + dlg:button{ + id = "pvBtnLeft", + text = "Left", + visible = false, + onclick = function() + if not S.previewSelected then return end + if not S.previewOffsets[S.previewSelected] then + S.previewOffsets[S.previewSelected] = { x = 0, y = 0 } + end + S.previewOffsets[S.previewSelected].x = S.previewOffsets[S.previewSelected].x - 1 + savePrefs() + dlg:repaint() + end + } + dlg:button{ + id = "pvBtnRight", + text = "Right", + visible = false, + onclick = function() + if not S.previewSelected then return end + if not S.previewOffsets[S.previewSelected] then + S.previewOffsets[S.previewSelected] = { x = 0, y = 0 } + end + S.previewOffsets[S.previewSelected].x = S.previewOffsets[S.previewSelected].x + 1 + savePrefs() + dlg:repaint() + end + } + dlg:button{ + id = "pvBtnReset", + text = "Reset", + visible = false, + onclick = function() + if not S.previewSelected then return end + S.previewOffsets[S.previewSelected] = { x = 0, y = 0 } + savePrefs() + dlg:repaint() + end + } + ---------------------------------------------------------------- -- GB TAB ---------------------------------------------------------------- dlg:separator{ id = "sepGb", text = "GB Tile Optimizer", visible = false } - -- Row: Flip + Offset + Analyze + -- Row: Flip + Offset + Silhouette + Compress (reordered) dlg:check{ id = "gbFlipOpt", text = "Flip", @@ -1693,13 +1940,6 @@ local function run() visible = false, onclick = function() S.gbOffsetOpt = dlg.data.gbOffsetOpt end } - dlg:check{ - id = "gbCompress", - text = "Compress", - selected = S.gbCompress, - visible = false, - onclick = function() S.gbCompress = dlg.data.gbCompress end - } dlg:check{ id = "gbSilhouette", text = "Silhouette", @@ -1707,6 +1947,13 @@ local function run() visible = false, onclick = function() S.gbSilhouette = dlg.data.gbSilhouette end } + dlg:check{ + id = "gbCompress", + text = "Compress", + selected = S.gbCompress, + visible = false, + onclick = function() S.gbCompress = dlg.data.gbCompress end + } dlg:combobox{ id = "gbAnalyzeMode", option = S.gbAnalyzeMode, @@ -1722,18 +1969,21 @@ local function run() visible = false, onclick = function() if not S.sourceImage then return end - local usedTileSize = S.gbTileSize + local usedTileW = S.gbTileW + local usedTileH = S.gbTileH if S.gbAnalyzeMode == "tile" then - local ts - S.gbTiles, S.gbTotalTiles, ts = analyzeTilesTileMode(app.sprite, app.frame.frameNumber) - if ts then usedTileSize = ts end + local rtw, rth + S.gbTiles, S.gbTotalTiles, rtw, rth = analyzeTilesTileMode(app.sprite, app.frame.frameNumber) + if rtw then usedTileW = rtw end + if rth then usedTileH = rth end else - S.gbTiles, S.gbTotalTiles = analyzeTiles(S.sourceImage, S.gbFlipOpt, S.gbOffsetOpt, usedTileSize) + S.gbTiles, S.gbTotalTiles = analyzeTiles(S.sourceImage, S.gbFlipOpt, S.gbOffsetOpt, usedTileW, usedTileH) end - S.gbTileSize = usedTileSize + S.gbTileW = usedTileW + S.gbTileH = usedTileH S.gbOptImage, S.gbCols = buildOptimizedImage( S.sourceImage, S.gbTiles, S.gbCompress, - S.gbSilhouette, S.gbSilhouetteColor, usedTileSize) + S.gbSilhouette, S.gbSilhouetteColor, usedTileW, usedTileH) S.gbSelectedTile = 0 S.gbScrollX = 0 S.gbScrollY = 0 @@ -1744,7 +1994,7 @@ local function run() local z = S.gbZoomOpt dlg:modify{ id = "canvasGbOpt", width = math.min(S.gbOptImage.width * z, SOURCE_VIEWPORT_W), - height = math.min(S.gbOptImage.height * z, 120) } + height = math.min(S.gbOptImage.height * z, S.gbOptViewH) } end dlg:repaint() end @@ -1760,7 +2010,7 @@ local function run() end S.gbSimilarPairs = findSimilarTiles( S.sourceImage, S.gbTiles, S.gbSimilarThreshold, - S.gbFlipOpt, S.gbOffsetOpt, S.gbTileSize) + S.gbFlipOpt, S.gbOffsetOpt, S.gbTileW, S.gbTileH) dlg:modify{ id = "btnFindSimilar", text = "Find similars (" .. #S.gbSimilarPairs .. ")" } dlg:repaint() @@ -1793,7 +2043,7 @@ local function run() dlg:separator{ id = "sepGbSource", text = "Occurrences in Source", visible = false } local GB_SRC_W = SOURCE_VIEWPORT_W - local GB_SRC_H = 150 + local GB_SRC_H = S.gbSrcViewH -- Flip colors local FLIP_COLORS = { @@ -1811,7 +2061,8 @@ local function run() visible = false, onpaint = function(ev) local gc = ev.context - local ts = S.gbTileSize + local tw = S.gbTileW + local th = S.gbTileH gc.color = Color(30, 30, 30) gc:fillRect(Rectangle(0, 0, GB_SRC_W, GB_SRC_H)) @@ -1832,14 +2083,14 @@ local function run() -- Draw tile grid gc.color = Color(255, 255, 255, 20) - for col = 0, math.floor(srcW / ts) do - local lx = col * ts * z - S.gbSrcScrollX + for col = 0, math.floor(srcW / tw) do + local lx = col * tw * z - S.gbSrcScrollX if lx >= 0 and lx < GB_SRC_W then gc:fillRect(Rectangle(lx, 0, 1, math.min(srcH * z, GB_SRC_H))) end end - for row = 0, math.floor(srcH / ts) do - local ly = row * ts * z - S.gbSrcScrollY + for row = 0, math.floor(srcH / th) do + local ly = row * th * z - S.gbSrcScrollY if ly >= 0 and ly < GB_SRC_H then gc:fillRect(Rectangle(0, ly, math.min(srcW * z, GB_SRC_W), 1)) end @@ -1851,8 +2102,8 @@ local function run() for _, pos in ipairs(tile.positions) do local rx = pos.x * z - S.gbSrcScrollX local ry = pos.y * z - S.gbSrcScrollY - local rw = ts * z - local rh = ts * z + local rw = tw * z + local rh = th * z if rx + rw > 0 and rx < GB_SRC_W and ry + rh > 0 and ry < GB_SRC_H then local c = FLIP_COLORS[pos.flip] or FLIP_COLORS.none gc.color = Color(c.red, c.green, c.blue, 60) @@ -1897,7 +2148,8 @@ local function run() if S.gbSrcDragging and not S.gbSrcIsDrag then -- Click: select tile at this position local z = S.gbZoomSrc - local ts = S.gbTileSize + local tw = S.gbTileW + local th = S.gbTileH local pixelX = math.floor((S.gbSrcClickX + S.gbSrcScrollX) / z) local pixelY = math.floor((S.gbSrcClickY + S.gbSrcScrollY) / z) @@ -1905,8 +2157,8 @@ local function run() local candidates = {} for i, tile in ipairs(S.gbTiles) do for _, pos in ipairs(tile.positions) do - if pixelX >= pos.x and pixelX < pos.x + ts and - pixelY >= pos.y and pixelY < pos.y + ts then + if pixelX >= pos.x and pixelX < pos.x + tw and + pixelY >= pos.y and pixelY < pos.y + th then -- Check if pixel is non-transparent at this position local isOpaque = false if S.sourceImage and pixelX >= 0 and pixelX < S.sourceImage.width @@ -1952,12 +2204,12 @@ local function run() } ---------------------------------------------------------------- - -- Optimized Spritesheet canvas (L-click to select) + -- Optimized Spritesheet canvas (L-click=select) ---------------------------------------------------------------- - dlg:separator{ id = "sepGbOpt", text = "Optimized Spritesheet (L-click to select)", visible = false } + dlg:separator{ id = "sepGbOpt", text = "Optimized Spritesheet (L-click=select)", visible = false } local GB_OPT_W = SOURCE_VIEWPORT_W - local GB_OPT_H = 120 + local GB_OPT_H = S.gbOptViewH dlg:canvas{ id = "canvasGbOpt", @@ -1968,7 +2220,8 @@ local function run() onpaint = function(ev) local gc = ev.context local z = S.gbZoomOpt - local ts = S.gbTileSize + local tw = S.gbTileW + local th = S.gbTileH gc.color = Color(30, 30, 30) gc:fillRect(Rectangle(0, 0, GB_OPT_W, GB_OPT_H)) @@ -1988,14 +2241,14 @@ local function run() -- Draw tile grid gc.color = Color(255, 255, 255, 30) - for col = 0, math.floor(imgW / ts) do - local lx = col * ts * z - S.gbScrollX + for col = 0, math.floor(imgW / tw) do + local lx = col * tw * z - S.gbScrollX if lx >= 0 and lx < GB_OPT_W then gc:fillRect(Rectangle(lx, 0, 1, math.min(imgH * z, GB_OPT_H))) end end - for row = 0, math.floor(imgH / ts) do - local ly = row * ts * z - S.gbScrollY + for row = 0, math.floor(imgH / th) do + local ly = row * th * z - S.gbScrollY if ly >= 0 and ly < GB_OPT_H then gc:fillRect(Rectangle(0, ly, math.min(imgW * z, GB_OPT_W), 1)) end @@ -2006,7 +2259,7 @@ local function run() if S.gbCompress then local c = (i - 1) % S.gbCols local r = math.floor((i - 1) / S.gbCols) - return c * ts, r * ts + return c * tw, r * th else local pos = S.gbTiles[i].positions[1] return pos.x, pos.y @@ -2065,12 +2318,12 @@ local function run() similarSet[pair.j] = groupColors[find(pair.j)] end - local rw = ts * z - local rh = ts * z + local rw = tw * z + local rh = th * z for idx, c in pairs(similarSet) do - local tx, ty = tilePos(idx) - local srx = tx * z - S.gbScrollX - local sry = ty * z - S.gbScrollY + local ttx, tty = tilePos(idx) + local srx = ttx * z - S.gbScrollX + local sry = tty * z - S.gbScrollY gc.color = Color(c.red, c.green, c.blue, 40) gc:fillRect(Rectangle(srx, sry, rw, rh)) gc.color = c @@ -2103,11 +2356,11 @@ local function run() -- Highlight selected tile if S.gbSelectedTile > 0 and S.gbSelectedTile <= #S.gbTiles then - local tx, ty = tilePos(S.gbSelectedTile) - local rx = tx * z - S.gbScrollX - local ry = ty * z - S.gbScrollY - local rw = ts * z - local rh = ts * z + local ttx, tty = tilePos(S.gbSelectedTile) + local rx = ttx * z - S.gbScrollX + local ry = tty * z - S.gbScrollY + local rw = tw * z + local rh = th * z gc.color = Color(255, 0, 0, 220) gc:fillRect(Rectangle(rx, ry, rw, 2)) gc:fillRect(Rectangle(rx, ry + rh - 2, rw, 2)) @@ -2151,24 +2404,35 @@ local function run() return end local z = S.gbZoomOpt - local ts = S.gbTileSize + local tw = S.gbTileW + local th = S.gbTileH local pixelX = math.floor((S.gbOptClickX + S.gbScrollX) / z) local pixelY = math.floor((S.gbOptClickY + S.gbScrollY) / z) if S.gbCompress then - local col = math.floor(pixelX / ts) - local row = math.floor(pixelY / ts) + local col = math.floor(pixelX / tw) + local row = math.floor(pixelY / th) local idx = row * S.gbCols + col + 1 if idx >= 1 and idx <= #S.gbTiles then - S.gbSelectedTile = idx + -- Toggle: deselect if same tile clicked again + if S.gbSelectedTile == idx then + S.gbSelectedTile = 0 + else + S.gbSelectedTile = idx + end end else - local clickTileX = math.floor(pixelX / ts) * ts - local clickTileY = math.floor(pixelY / ts) * ts + local clickTileX = math.floor(pixelX / tw) * tw + local clickTileY = math.floor(pixelY / th) * th for i, tile in ipairs(S.gbTiles) do local pos = tile.positions[1] if pos.x == clickTileX and pos.y == clickTileY then - S.gbSelectedTile = i + -- Toggle: deselect if same tile clicked again + if S.gbSelectedTile == i then + S.gbSelectedTile = 0 + else + S.gbSelectedTile = i + end break end end @@ -2260,23 +2524,16 @@ local function run() app.alert("Run Analyze first.") return end - local origSprite = app.sprite local optW = S.gbOptImage.width local optH = S.gbOptImage.height local tmpSprite = Sprite(optW, optH, ColorMode.RGB) - -- Convert background layer to normal layer to preserve transparency app.command.LayerFromBackground() - local cel = tmpSprite.cels[1] - local img = Image(optW, optH, ColorMode.RGB) - img:clear() - img:drawImage(S.gbOptImage, Point(0, 0)) - cel.image = img + local layer = tmpSprite.layers[1] + for _, c in ipairs(layer.cels) do tmpSprite:deleteCel(c) end + tmpSprite:newCel(layer, 1, S.gbOptImage:clone(), Point(0, 0)) app.command.MaskAll() - app.command.CopyMerged() + app.command.Copy() tmpSprite:close() - if origSprite then - app.sprite = origSprite - end end }