Round 6: compact preview, Optimize tab, deselect, click improvements

- Remove Preview toggle button from main dialog (preview is primary)
- Preview buttons compact: Setup / All|One / Fit on single row
- Preview navigation: L-click=prev, R-click=next in single mode
- Preview title: "AniPhallow: All" or "AniPhallow: <name>"
- Rename GB tab to Optimize, config labels updated accordingly
- Combined separator texts: Source || Frames, Source || Optimized
- GB canvases taller (240px each) to match Animations tab
- Optimized canvas: candidate-based click with offset/silhouette support
- R-click deselects in both Occurrences and Optimized canvases
- Frame strip: L-click on selected frame deselects it
- Remove view height config options (didn't work)
- Clean up stale IDs (lblFrames, lblGbOpt, tabPreview)
This commit is contained in:
Cidwel Highwind 2026-04-03 19:01:56 +02:00
parent b9b54867f1
commit 91af5f5691
1 changed files with 156 additions and 122 deletions

View File

@ -23,7 +23,7 @@ local SOURCE_VIEWPORT_W = 300
local SOURCE_VIEWPORT_H = 400
local MAX_ANIMS = 20 -- max number of dynamic animations
local TABS = { "Animations", "GB" }
local TABS = { "Animations", "Optimize" }
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)
@ -119,7 +119,7 @@ local SETUP_IDS = {
"sepAnims", "cmbAnimList", "btnNewAnim", "btnDelAnim", "btnRenameAnim", "btnCloneAnim",
"sepSource", "canvasSource",
"canvasStrips",
"lblFrames", "btnFlipX", "btnFlipY", "btnMoveLeft", "btnMoveRight", "btnRemoveFrame",
"btnFlipX", "btnFlipY", "btnMoveLeft", "btnMoveRight", "btnRemoveFrame",
"btnFrameUp", "btnFrameDown", "btnFrameLeft", "btnFrameRight", "btnFrameReset",
}
@ -129,13 +129,13 @@ local GB_IDS = {
"gbAnalyzeMode",
"gbSimilarThreshold", "btnFindSimilar",
"sepGbSource", "canvasGbSource",
"canvasGbOpt", "lblGbOpt",
"canvasGbOpt",
"btnGbSaveLayer", "btnGbCopyClipboard", "btnGbSave",
}
-- GB canvas heights (taller to match Animations tab and prevent resize on tab switch)
local GB_SRC_H = 220
local GB_OPT_H = 220
local GB_SRC_H = 240
local GB_OPT_H = 240
----------------------------------------------------------------------
-- Module-level window/timer variables
@ -209,7 +209,9 @@ local function loadPrefs()
-- Migrate old tab names
if v == "Setup" then v = "Animations"
elseif v == "Render" then v = "Animations"
elseif v == "Preview" then v = "Animations" end
elseif v == "Preview" then v = "Animations"
elseif v == "GB" then v = "Optimize"
end
S.currentTab = v
end
if k == "animNames" and v ~= "" then
@ -967,6 +969,21 @@ local function buildOptimizedImage(img, tiles, compress, silhouette, silhouetteC
end
end
----------------------------------------------------------------------
-- Helper: get preview title based on current mode/animation
----------------------------------------------------------------------
local function getPreviewTitle()
if S.previewMode == "all" then
return "AniPhallow: All"
else
local name = ""
if S.previewSingleIdx >= 1 and S.previewSingleIdx <= #S.animNames then
name = S.animNames[S.previewSingleIdx]
end
return "AniPhallow: " .. name
end
end
----------------------------------------------------------------------
-- Preview Window (primary window)
----------------------------------------------------------------------
@ -1017,7 +1034,7 @@ openPreviewWindow = function()
pvHeight = math.max(pvHeight, 40)
previewDlg = Dialog{
title = "AniPhallow Preview",
title = getPreviewTitle(),
onclose = function()
if previewTimer then pcall(function() previewTimer:stop() end) end
if pvRefreshTimer then pcall(function() pvRefreshTimer:stop() end) end
@ -1026,12 +1043,10 @@ openPreviewWindow = function()
previewDlg = nil
S.previewWindowOpen = false
savePrefs()
if mainDlg then
pcall(function() mainDlg:modify{ id = "tabPreview", text = " Preview " } end)
end
end
}
-- All buttons on the same row: Setup, toggle mode, Fit
previewDlg:button{
id = "pvSetup",
text = "Setup",
@ -1042,7 +1057,7 @@ openPreviewWindow = function()
previewDlg:button{
id = "pvToggleMode",
text = S.previewMode == "all" and "Show One" or "Show All",
text = S.previewMode == "all" and "All" or "One",
onclick = function()
if S.previewMode == "all" then
S.previewMode = "single"
@ -1050,52 +1065,22 @@ openPreviewWindow = function()
if S.previewSingleIdx < 1 then S.previewSingleIdx = 1 end
if S.previewSingleIdx > #S.animNames then S.previewSingleIdx = #S.animNames end
end
previewDlg:modify{ id = "pvToggleMode", text = "Show All" }
previewDlg:modify{ id = "pvPrev", visible = true }
previewDlg:modify{ id = "pvNext", visible = true }
previewDlg:modify{ id = "pvAnimName", visible = true }
else
S.previewMode = "all"
previewDlg:modify{ id = "pvToggleMode", text = "Show One" }
previewDlg:modify{ id = "pvPrev", visible = false }
previewDlg:modify{ id = "pvNext", visible = false }
previewDlg:modify{ id = "pvAnimName", visible = false }
end
previewDlg:repaint()
-- Close and reopen to update title
pcall(function() previewDlg:close() end)
openPreviewWindow()
end
}
-- Navigation buttons on a new row, only in single mode
previewDlg:newrow()
previewDlg:button{
id = "pvPrev",
text = "<-",
visible = (S.previewMode == "single"),
id = "pvFit",
text = "Fit",
onclick = function()
if #S.animNames > 0 then
S.previewSingleIdx = S.previewSingleIdx - 1
if S.previewSingleIdx < 1 then S.previewSingleIdx = #S.animNames end
-- Update name label
pcall(function()
previewDlg:modify{ id = "pvAnimName", text = S.animNames[S.previewSingleIdx] or "" }
end)
previewDlg:repaint()
end
end
}
previewDlg:button{
id = "pvNext",
text = "->",
visible = (S.previewMode == "single"),
onclick = function()
if #S.animNames > 0 then
S.previewSingleIdx = S.previewSingleIdx + 1
if S.previewSingleIdx > #S.animNames then S.previewSingleIdx = 1 end
pcall(function()
previewDlg:modify{ id = "pvAnimName", text = S.animNames[S.previewSingleIdx] or "" }
end)
previewDlg:repaint()
end
-- Close and reopen to refit
pcall(function() previewDlg:close() end)
openPreviewWindow()
end
}
@ -1171,6 +1156,28 @@ openPreviewWindow = function()
end
end
end,
onmousedown = function(ev)
-- In single mode: L-click=prev anim, R-click=next anim
if S.previewMode == "single" then
if ev.button == MouseButton.LEFT then
if #S.animNames > 0 then
S.previewSingleIdx = S.previewSingleIdx - 1
if S.previewSingleIdx < 1 then S.previewSingleIdx = #S.animNames end
-- Close and reopen to update title
pcall(function() previewDlg:close() end)
openPreviewWindow()
end
elseif ev.button == MouseButton.RIGHT then
if #S.animNames > 0 then
S.previewSingleIdx = S.previewSingleIdx + 1
if S.previewSingleIdx > #S.animNames then S.previewSingleIdx = 1 end
-- Close and reopen to update title
pcall(function() previewDlg:close() end)
openPreviewWindow()
end
end
end
end,
onwheel = function(ev)
local dz = ev.deltaY < 0 and 1 or -1
if S.previewMode == "single" then
@ -1182,29 +1189,6 @@ openPreviewWindow = function()
end
}
-- Animation name label (only in single mode)
local singleName = ""
if S.previewSingleIdx >= 1 and S.previewSingleIdx <= #S.animNames then
singleName = S.animNames[S.previewSingleIdx]
end
previewDlg:label{
id = "pvAnimName",
text = singleName,
visible = (S.previewMode == "single")
}
-- Adjust button: re-fits window to content by closing and reopening
previewDlg:newrow()
previewDlg:button{
id = "pvAdjust",
text = "Adjust",
onclick = function()
-- Close and reopen to refit
pcall(function() previewDlg:close() end)
openPreviewWindow()
end
}
previewTimer = Timer{
interval = S.animSpeed / 1000.0,
ontick = function()
@ -1267,18 +1251,18 @@ openMainDialog = function()
dlg:repaint()
end
-- Switch visible tab (only Animations and GB)
-- Switch visible tab (only Animations and Optimize)
local function switchTab(tab)
S.currentTab = tab
local isSetup = (tab == "Animations")
local isGB = (tab == "GB")
local isOpt = (tab == "Optimize")
for _, id in ipairs(SETUP_IDS) do
pcall(function() dlg:modify{ id = id, visible = isSetup } end)
end
for _, id in ipairs(GB_IDS) do
pcall(function() dlg:modify{ id = id, visible = isGB } end)
pcall(function() dlg:modify{ id = id, visible = isOpt } end)
end
-- Update tab button labels for Animations and GB only
-- Update tab button labels
for _, t in ipairs(TABS) do
local label = (t == tab) and ("[" .. t .. "]") or (" " .. t .. " ")
pcall(function() dlg:modify{ id = "tab" .. t, text = label } end)
@ -1316,7 +1300,7 @@ openMainDialog = function()
mainDlg = dlg
----------------------------------------------------------------
-- TAB BUTTONS (Animations, GB, Preview toggle)
-- TAB BUTTONS (Animations, Optimize)
----------------------------------------------------------------
for _, tab in ipairs(TABS) do
local label = (tab == S.currentTab) and ("[" .. tab .. "]") or (" " .. tab .. " ")
@ -1329,23 +1313,6 @@ openMainDialog = function()
}
end
-- Preview toggle button
dlg:button{
id = "tabPreview",
text = S.previewWindowOpen and "[Preview]" or " Preview ",
onclick = function()
if previewDlg then
-- Close it
pcall(function() previewDlg:close() end)
-- previewDlg onclose handler sets S.previewWindowOpen = false
dlg:modify{ id = "tabPreview", text = " Preview " }
else
openPreviewWindow()
dlg:modify{ id = "tabPreview", text = "[Preview]" }
end
end
}
----------------------------------------------------------------
-- Config button (opens sub-dialog) - visible on ALL tabs
----------------------------------------------------------------
@ -1359,9 +1326,9 @@ openMainDialog = function()
d:slider{ id = "animSpeed", label = "Speed (ms):", min = 50, max = 1000, value = S.animSpeed }
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 = "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 }
d:separator{ text = "Optimization" }
d:number{ id = "gbTileW", label = "Tile W:", text = tostring(S.gbTileW), decimals = 0 }
d:number{ id = "gbTileH", label = "Tile H:", text = tostring(S.gbTileH), decimals = 0 }
local silColor = S.gbSilhouetteColor
if not S.gbSilhouetteColorSet then
silColor = getDarkestPaletteColor()
@ -1551,7 +1518,7 @@ openMainDialog = function()
----------------------------------------------------------------
-- Source canvas (L-click=add, R-click=add flipped)
----------------------------------------------------------------
dlg:separator{ id = "sepSource", text = "Source (L-click=add frame, R-click=add flipped)" }
dlg:separator{ id = "sepSource", text = "Source (L-click=add, R-click=flip add) || Frames (L-click=select, R-click=remove, drag=move)" }
dlg:canvas{
id = "canvasSource",
@ -1777,6 +1744,12 @@ openMainDialog = function()
local idx = row * thumbsPerRow + col + 1
if idx >= 1 and idx <= #frames then
if ev.button == MouseButton.LEFT then
-- Click on already-selected frame: deselect
if S.selectedFrame == idx then
S.selectedFrame = 0
dlg:repaint()
return -- don't start drag
end
S.selectedFrame = idx
S.frameDragging = true
S.frameDragFrom = idx
@ -1830,10 +1803,8 @@ openMainDialog = function()
}
----------------------------------------------------------------
-- Frame action buttons
-- Frame action buttons (no separator - combined into sepSource)
----------------------------------------------------------------
dlg:separator{ id = "lblFrames", text = "Frames (L-click=select, R-click=remove, drag=reorder)" }
dlg:button{
id = "btnFlipX",
text = "FlipX",
@ -1999,9 +1970,9 @@ openMainDialog = function()
}
----------------------------------------------------------------
-- GB TAB
-- OPTIMIZE TAB
----------------------------------------------------------------
dlg:separator{ id = "sepGb", text = "GB Tile Optimizer", visible = false }
dlg:separator{ id = "sepGb", text = "Tile Optimizer", visible = false }
-- Row: Flip + Offset + Silhouette + Compress (reordered)
dlg:check{
@ -2118,7 +2089,7 @@ openMainDialog = function()
----------------------------------------------------------------
-- Occurrences in Source canvas (BEFORE Optimized)
----------------------------------------------------------------
dlg:separator{ id = "sepGbSource", text = "Occurrences in Source", visible = false }
dlg:separator{ id = "sepGbSource", text = "Source || Optimized (L-click=check occurrences, R-click=deselect)", visible = false }
local GB_SRC_W = SOURCE_VIEWPORT_W
@ -2202,6 +2173,12 @@ openMainDialog = function()
S.gbSrcDragging = true
S.gbSrcDragLastX = ev.x
S.gbSrcDragLastY = ev.y
elseif ev.button == MouseButton.RIGHT then
-- R-click: deselect
if S.gbSelectedTile > 0 then
S.gbSelectedTile = 0
dlg:repaint()
end
end
end,
onmousemove = function(ev)
@ -2283,7 +2260,7 @@ openMainDialog = function()
}
----------------------------------------------------------------
-- Optimized Spritesheet canvas (L-click=select)
-- Optimized Spritesheet canvas (L-click=select, R-click=deselect)
----------------------------------------------------------------
local GB_OPT_W = SOURCE_VIEWPORT_W
@ -2453,6 +2430,12 @@ openMainDialog = function()
S.gbDragging = true
S.gbDragLastX = ev.x
S.gbDragLastY = ev.y
elseif ev.button == MouseButton.RIGHT then
-- R-click: deselect
if S.gbSelectedTile > 0 then
S.gbSelectedTile = 0
dlg:repaint()
end
end
end,
onmousemove = function(ev)
@ -2474,7 +2457,7 @@ openMainDialog = function()
end,
onmouseup = function(ev)
if S.gbDragging and not S.gbOptIsDrag then
-- Click: select tile
-- Click: select tile using candidate-based logic
if not S.gbOptImage then
S.gbDragging = false
return
@ -2485,33 +2468,86 @@ openMainDialog = function()
local pixelX = math.floor((S.gbOptClickX + S.gbScrollX) / z)
local pixelY = math.floor((S.gbOptClickY + S.gbScrollY) / z)
-- Find all tiles whose positions cover the clicked pixel
local candidates = {}
if S.gbCompress then
-- In compress mode, tiles are laid out in a grid
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
-- Toggle: deselect if same tile clicked again
if S.gbSelectedTile == idx then
S.gbSelectedTile = 0
else
S.gbSelectedTile = idx
local isOpaque = false
if pixelX >= 0 and pixelX < S.gbOptImage.width
and pixelY >= 0 and pixelY < S.gbOptImage.height then
isOpaque = pc.rgbaA(S.gbOptImage:getPixel(pixelX, pixelY)) > 0
end
table.insert(candidates, { tileIdx = idx, opaque = isOpaque })
end
-- Also check silhouette areas in non-compress mode is N/A here,
-- but in compress mode with silhouette there's only one tile per grid cell
else
local clickTileX = math.floor(pixelX / tw) * tw
local clickTileY = math.floor(pixelY / th) * th
-- Non-compress mode: candidate-based click like Occurrences canvas
for i, tile in ipairs(S.gbTiles) do
local pos = tile.positions[1]
if pos.x == clickTileX and pos.y == clickTileY then
-- Toggle: deselect if same tile clicked again
if S.gbSelectedTile == i then
S.gbSelectedTile = 0
else
S.gbSelectedTile = i
if pixelX >= pos.x and pixelX < pos.x + tw and
pixelY >= pos.y and pixelY < pos.y + th then
local isOpaque = false
if S.gbOptImage and pixelX >= 0 and pixelX < S.gbOptImage.width
and pixelY >= 0 and pixelY < S.gbOptImage.height then
isOpaque = pc.rgbaA(S.gbOptImage:getPixel(pixelX, pixelY)) > 0
end
table.insert(candidates, { tileIdx = i, opaque = isOpaque })
end
end
-- Also check silhouette positions (duplicate tile positions rendered as silhouettes)
if S.gbSilhouette then
for i, tile in ipairs(S.gbTiles) do
for pi = 2, #tile.positions do
local pos = tile.positions[pi]
if pixelX >= pos.x and pixelX < pos.x + tw and
pixelY >= pos.y and pixelY < pos.y + th then
local isOpaque = false
if S.gbOptImage and pixelX >= 0 and pixelX < S.gbOptImage.width
and pixelY >= 0 and pixelY < S.gbOptImage.height then
isOpaque = pc.rgbaA(S.gbOptImage:getPixel(pixelX, pixelY)) > 0
end
-- Check if not already in candidates
local alreadyIn = false
for _, c in ipairs(candidates) do
if c.tileIdx == i then alreadyIn = true; break end
end
if not alreadyIn then
table.insert(candidates, { tileIdx = i, opaque = isOpaque })
end
end
end
end
end
end
if #candidates > 0 then
-- Sort: opaque tiles first
table.sort(candidates, function(a, b)
if a.opaque ~= b.opaque then return a.opaque end
return a.tileIdx < b.tileIdx
end)
-- Cycle through candidates if current selection is among them
local found = false
for ci, cand in ipairs(candidates) do
if cand.tileIdx == S.gbSelectedTile then
local nextIdx = (ci % #candidates) + 1
S.gbSelectedTile = candidates[nextIdx].tileIdx
found = true
break
end
end
if not found then
S.gbSelectedTile = candidates[1].tileIdx
end
end
end
S.gbDragging = false
@ -2526,8 +2562,6 @@ openMainDialog = function()
end
}
dlg:separator{ id = "lblGbOpt", text = "Optimized Spritesheet (L-click=select)", visible = false }
-- Save as Layer button
dlg:button{
id = "btnGbSaveLayer",
@ -2678,7 +2712,7 @@ openMainDialog = function()
interval = 0.5,
ontick = function()
local isDragging = S.dragging or S.gbDragging or S.gbSrcDragging
if S.currentTab == "GB" and not isDragging then
if S.currentTab == "Optimize" and not isDragging then
gbRefreshCounter = gbRefreshCounter + 1
if gbRefreshCounter >= 4 then
gbRefreshCounter = 0