Round 7: presets system, file associations, plugin toggle
Major features: - Presets system: save/load/clone/delete animation configurations - Each preset stored as .ini in ~/.config/aseprite/aniphallow_presets/ - Master file tracks global prefs and file-to-preset associations - Config dialog has Presets section with combobox + Save/Load/Clone/Delete - Default preset created automatically on first run - Migration from old single prefs file to preset system - File-to-preset association: each .aseprite file remembers its preset - Auto-detects file changes and loads appropriate preset - Preset association saved when loading a preset - Plugin toggle: running the plugin again closes it (no duplicate windows) - Preview title fixed to "AniPhallow Preview" (always) - Animation name label shown below canvas in Show One mode - Show One navigation (L/R click) no longer closes/reopens window - Config dialog reorganized: Presets > Animations > Optimization > Preview
This commit is contained in:
parent
91af5f5691
commit
9e3a2702dc
673
aniphallow.lua
673
aniphallow.lua
|
|
@ -28,6 +28,13 @@ 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)
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Presets paths
|
||||
----------------------------------------------------------------------
|
||||
local PRESETS_DIR = app.fs.joinPath(app.fs.userConfigPath, "aniphallow_presets")
|
||||
local MASTER_FILE = app.fs.joinPath(app.fs.userConfigPath, "aniphallow_master.ini")
|
||||
local OLD_PREFS_FILE = app.fs.joinPath(app.fs.userConfigPath, "aniphallow_prefs.ini")
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- State
|
||||
----------------------------------------------------------------------
|
||||
|
|
@ -109,8 +116,17 @@ local S = {
|
|||
previewLayoutValue = 2, -- number for fixed cols/rows
|
||||
previewMode = "all", -- "all" or "single"
|
||||
previewSingleIdx = 1, -- which animation to show in single mode
|
||||
-- Current preset
|
||||
currentPreset = "Default",
|
||||
}
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- File-to-preset association map
|
||||
----------------------------------------------------------------------
|
||||
local filePresetMap = {} -- filepath -> presetName
|
||||
local knownPresetNames = { "Default" } -- list of all known preset names
|
||||
local lastSpriteFilename = "" -- for auto-detect file change
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Tab widget IDs (for show/hide)
|
||||
----------------------------------------------------------------------
|
||||
|
|
@ -152,10 +168,8 @@ local openPreviewWindow
|
|||
local openMainDialog
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Preferences
|
||||
-- Serialize / Deserialize frames
|
||||
----------------------------------------------------------------------
|
||||
local PREFS_FILE = app.fs.joinPath(app.fs.userConfigPath, "aniphallow_prefs.ini")
|
||||
|
||||
-- Serialize frames list: "x1,y1,f,fv,ox,oy;x2,y2,f,fv,ox,oy;..."
|
||||
local function serializeFrames(frames)
|
||||
local parts = {}
|
||||
|
|
@ -184,29 +198,122 @@ local function deserializeFrames(str)
|
|||
return frames
|
||||
end
|
||||
|
||||
local function loadPrefs()
|
||||
local f = io.open(PREFS_FILE, "r")
|
||||
----------------------------------------------------------------------
|
||||
-- Reset to defaults
|
||||
----------------------------------------------------------------------
|
||||
local function resetToDefaults()
|
||||
S.tileW = DEFAULT_TILE_W
|
||||
S.tileH = DEFAULT_TILE_H
|
||||
S.animSpeed = DEFAULT_ANIM_SPEED
|
||||
S.sourceZoom = DEFAULT_SOURCE_ZOOM
|
||||
S.useBgColor = false
|
||||
S.bgColor = Color(0, 0, 0)
|
||||
S.gbFlipOpt = false
|
||||
S.gbOffsetOpt = false
|
||||
S.gbCompress = true
|
||||
S.gbSilhouette = false
|
||||
S.gbSilhouetteColor = Color(0, 0, 0)
|
||||
S.gbSilhouetteColorSet = false
|
||||
S.gbAnalyzeMode = "pixel"
|
||||
S.gbLayerName = "auto-optimized-tiles"
|
||||
S.gbAlwaysOverwrite = false
|
||||
S.gbTileW = DEFAULT_GB_TILE_W
|
||||
S.gbTileH = DEFAULT_GB_TILE_H
|
||||
S.gbSimilarThreshold = 80
|
||||
S.animNames = {}
|
||||
S.anims = {}
|
||||
S.currentAnim = 1
|
||||
S.selectedFrame = 0
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Preset system: save/load preset files and master file
|
||||
----------------------------------------------------------------------
|
||||
|
||||
-- Get list of all known preset names
|
||||
local function getAllPresetNames()
|
||||
-- Ensure "Default" is always first
|
||||
local seen = {}
|
||||
local result = { "Default" }
|
||||
seen["Default"] = true
|
||||
for _, name in ipairs(knownPresetNames) do
|
||||
if not seen[name] then
|
||||
table.insert(result, name)
|
||||
seen[name] = true
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Save current state to a preset file
|
||||
local function savePreset(presetName)
|
||||
-- Create presets dir if needed
|
||||
app.fs.makeDirectory(PRESETS_DIR)
|
||||
local path = app.fs.joinPath(PRESETS_DIR, presetName .. ".ini")
|
||||
local f = io.open(path, "w")
|
||||
if not f then return end
|
||||
f:write("tileW=" .. S.tileW .. "\n")
|
||||
f:write("tileH=" .. S.tileH .. "\n")
|
||||
f:write("animSpeed=" .. S.animSpeed .. "\n")
|
||||
f:write("sourceZoom=" .. S.sourceZoom .. "\n")
|
||||
-- Save current animation name for robust resolution after sort
|
||||
local curName = ""
|
||||
if S.currentAnim >= 1 and S.currentAnim <= #S.animNames then
|
||||
curName = S.animNames[S.currentAnim]
|
||||
end
|
||||
f:write("currentAnimName=" .. curName .. "\n")
|
||||
f:write("currentTab=" .. S.currentTab .. "\n")
|
||||
f:write("animNames=" .. table.concat(S.animNames, "|") .. "\n")
|
||||
f:write("useBgColor=" .. tostring(S.useBgColor) .. "\n")
|
||||
f:write("bgColorR=" .. S.bgColor.red .. "\n")
|
||||
f:write("bgColorG=" .. S.bgColor.green .. "\n")
|
||||
f:write("bgColorB=" .. S.bgColor.blue .. "\n")
|
||||
f:write("gbFlipOpt=" .. tostring(S.gbFlipOpt) .. "\n")
|
||||
f:write("gbOffsetOpt=" .. tostring(S.gbOffsetOpt) .. "\n")
|
||||
f:write("gbCompress=" .. tostring(S.gbCompress) .. "\n")
|
||||
f:write("gbSimilarThreshold=" .. S.gbSimilarThreshold .. "\n")
|
||||
f:write("gbZoomOpt=" .. S.gbZoomOpt .. "\n")
|
||||
f:write("gbZoomSrc=" .. S.gbZoomSrc .. "\n")
|
||||
f:write("gbSilhouette=" .. tostring(S.gbSilhouette) .. "\n")
|
||||
f:write("gbSilhouetteColorR=" .. S.gbSilhouetteColor.red .. "\n")
|
||||
f:write("gbSilhouetteColorG=" .. S.gbSilhouetteColor.green .. "\n")
|
||||
f:write("gbSilhouetteColorB=" .. S.gbSilhouetteColor.blue .. "\n")
|
||||
f:write("gbSilhouetteColorSet=" .. tostring(S.gbSilhouetteColorSet) .. "\n")
|
||||
f:write("gbAnalyzeMode=" .. S.gbAnalyzeMode .. "\n")
|
||||
f:write("gbLastSavePath=" .. S.gbLastSavePath .. "\n")
|
||||
f:write("gbLayerName=" .. S.gbLayerName .. "\n")
|
||||
f:write("gbAlwaysOverwrite=" .. tostring(S.gbAlwaysOverwrite) .. "\n")
|
||||
f:write("gbTileW=" .. S.gbTileW .. "\n")
|
||||
f:write("gbTileH=" .. S.gbTileH .. "\n")
|
||||
for i, name in ipairs(S.animNames) do
|
||||
f:write("anim_" .. (i - 1) .. "=" .. serializeFrames(S.anims[name] or {}) .. "\n")
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
|
||||
-- Load a preset file into S
|
||||
local function loadPreset(presetName)
|
||||
local path = app.fs.joinPath(PRESETS_DIR, presetName .. ".ini")
|
||||
local f = io.open(path, "r")
|
||||
if not f then
|
||||
-- If preset doesn't exist, reset to defaults
|
||||
resetToDefaults()
|
||||
S.currentPreset = presetName
|
||||
return
|
||||
end
|
||||
-- Reset animation data before loading
|
||||
S.animNames = {}
|
||||
S.anims = {}
|
||||
local savedAnimName = nil
|
||||
for line in f:lines() do
|
||||
local k, v = string.match(line, "^([%w_]+)=(.+)$")
|
||||
if k == "tileW" then S.tileW = tonumber(v) or DEFAULT_TILE_W end
|
||||
if k == "tileH" then S.tileH = tonumber(v) or DEFAULT_TILE_H end
|
||||
if k == "animSpeed" then S.animSpeed = tonumber(v) or DEFAULT_ANIM_SPEED end
|
||||
if k == "previewZoom" then
|
||||
-- Legacy: load into both
|
||||
local pz = tonumber(v) or DEFAULT_PREVIEW_ZOOM
|
||||
S.previewZoomAll = pz
|
||||
S.previewZoomOne = pz
|
||||
end
|
||||
if k == "previewZoomAll" then S.previewZoomAll = tonumber(v) or DEFAULT_PREVIEW_ZOOM end
|
||||
if k == "previewZoomOne" then S.previewZoomOne = tonumber(v) or DEFAULT_PREVIEW_ZOOM end
|
||||
if k == "sourceZoom" then S.sourceZoom = tonumber(v) or DEFAULT_SOURCE_ZOOM end
|
||||
-- Legacy: currentAnim as index (for migration)
|
||||
if k == "currentAnim" then S.currentAnim = tonumber(v) or 1 end
|
||||
if k == "currentAnimName" then savedAnimName = v end
|
||||
if k == "currentTab" and v ~= "" then
|
||||
-- Migrate old tab names
|
||||
if v == "Setup" then v = "Animations"
|
||||
elseif v == "Render" then v = "Animations"
|
||||
elseif v == "Preview" then v = "Animations"
|
||||
|
|
@ -239,22 +346,13 @@ 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
|
||||
-- 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 previewWindowOpen
|
||||
if k == "previewWindowOpen" then S.previewWindowOpen = (v == "true") end
|
||||
-- Preview layout prefs
|
||||
if k == "previewLayout" then S.previewLayout = v end
|
||||
if k == "previewLayoutValue" then S.previewLayoutValue = tonumber(v) or 2 end
|
||||
if k == "previewMode" then S.previewMode = v end
|
||||
if k == "previewSingleIdx" then S.previewSingleIdx = tonumber(v) or 1 end
|
||||
-- Dynamic anim frames: anim_0, anim_1, ...
|
||||
local animIdx = k and string.match(k, "^anim_(%d+)$")
|
||||
if animIdx then
|
||||
|
|
@ -281,6 +379,215 @@ local function loadPrefs()
|
|||
-- Clamp currentAnim to valid range
|
||||
if S.currentAnim < 1 then S.currentAnim = 1 end
|
||||
if S.currentAnim > #S.animNames then S.currentAnim = math.max(1, #S.animNames) end
|
||||
|
||||
S.currentPreset = presetName
|
||||
end
|
||||
|
||||
-- Save master file (global prefs + file associations)
|
||||
local function saveMaster()
|
||||
local f = io.open(MASTER_FILE, "w")
|
||||
if not f then return end
|
||||
f:write("currentPreset=" .. S.currentPreset .. "\n")
|
||||
f:write("previewWindowOpen=" .. tostring(S.previewWindowOpen) .. "\n")
|
||||
f:write("previewLayout=" .. S.previewLayout .. "\n")
|
||||
f:write("previewLayoutValue=" .. S.previewLayoutValue .. "\n")
|
||||
f:write("previewMode=" .. S.previewMode .. "\n")
|
||||
f:write("previewZoomAll=" .. S.previewZoomAll .. "\n")
|
||||
f:write("previewZoomOne=" .. S.previewZoomOne .. "\n")
|
||||
f:write("previewSingleIdx=" .. S.previewSingleIdx .. "\n")
|
||||
-- File associations
|
||||
for filepath, preset in pairs(filePresetMap) do
|
||||
if preset ~= "Default" then
|
||||
f:write("file_" .. filepath .. "=" .. preset .. "\n")
|
||||
end
|
||||
end
|
||||
-- Preset names list (for discovery since we can't list dir)
|
||||
f:write("presetNames=" .. table.concat(getAllPresetNames(), "|") .. "\n")
|
||||
f:close()
|
||||
end
|
||||
|
||||
-- Load master file
|
||||
local function loadMaster()
|
||||
local f = io.open(MASTER_FILE, "r")
|
||||
if not f then return end
|
||||
filePresetMap = {}
|
||||
for line in f:lines() do
|
||||
local k, v = string.match(line, "^(.-)=(.+)$")
|
||||
if k and v then
|
||||
if k == "currentPreset" then S.currentPreset = v end
|
||||
if k == "previewWindowOpen" then S.previewWindowOpen = (v == "true") end
|
||||
if k == "previewLayout" then S.previewLayout = v end
|
||||
if k == "previewLayoutValue" then S.previewLayoutValue = tonumber(v) or 2 end
|
||||
if k == "previewMode" then S.previewMode = v end
|
||||
if k == "previewZoomAll" then S.previewZoomAll = tonumber(v) or DEFAULT_PREVIEW_ZOOM end
|
||||
if k == "previewZoomOne" then S.previewZoomOne = tonumber(v) or DEFAULT_PREVIEW_ZOOM end
|
||||
if k == "previewSingleIdx" then S.previewSingleIdx = tonumber(v) or 1 end
|
||||
if k == "presetNames" and v ~= "" then
|
||||
knownPresetNames = {}
|
||||
for name in string.gmatch(v, "([^|]+)") do
|
||||
table.insert(knownPresetNames, name)
|
||||
end
|
||||
end
|
||||
-- File associations: keys starting with "file_"
|
||||
local filePath = string.match(k, "^file_(.+)$")
|
||||
if filePath then
|
||||
filePresetMap[filePath] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
-- Clamp previewSingleIdx
|
||||
if S.previewSingleIdx < 1 then S.previewSingleIdx = 1 end
|
||||
end
|
||||
|
||||
-- Convenience: save both preset and master
|
||||
local function saveAll()
|
||||
savePreset(S.currentPreset)
|
||||
saveMaster()
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Migration from old prefs file
|
||||
----------------------------------------------------------------------
|
||||
local function migrateOldPrefs()
|
||||
-- Check if old prefs file exists and no presets exist yet
|
||||
local oldF = io.open(OLD_PREFS_FILE, "r")
|
||||
if not oldF then return false end
|
||||
-- Check if Default preset already exists
|
||||
local defaultPath = app.fs.joinPath(PRESETS_DIR, "Default.ini")
|
||||
local defF = io.open(defaultPath, "r")
|
||||
if defF then
|
||||
defF:close()
|
||||
oldF:close()
|
||||
return false -- Already migrated
|
||||
end
|
||||
oldF:close()
|
||||
|
||||
-- Load the old prefs into S using the same parsing logic
|
||||
local f = io.open(OLD_PREFS_FILE, "r")
|
||||
if not f then return false end
|
||||
local savedAnimName = nil
|
||||
for line in f:lines() do
|
||||
local k, v = string.match(line, "^([%w_]+)=(.+)$")
|
||||
if k == "tileW" then S.tileW = tonumber(v) or DEFAULT_TILE_W end
|
||||
if k == "tileH" then S.tileH = tonumber(v) or DEFAULT_TILE_H end
|
||||
if k == "animSpeed" then S.animSpeed = tonumber(v) or DEFAULT_ANIM_SPEED end
|
||||
if k == "previewZoom" then
|
||||
local pz = tonumber(v) or DEFAULT_PREVIEW_ZOOM
|
||||
S.previewZoomAll = pz
|
||||
S.previewZoomOne = pz
|
||||
end
|
||||
if k == "previewZoomAll" then S.previewZoomAll = tonumber(v) or DEFAULT_PREVIEW_ZOOM end
|
||||
if k == "previewZoomOne" then S.previewZoomOne = tonumber(v) or DEFAULT_PREVIEW_ZOOM end
|
||||
if k == "sourceZoom" then S.sourceZoom = tonumber(v) or DEFAULT_SOURCE_ZOOM end
|
||||
if k == "currentAnim" then S.currentAnim = tonumber(v) or 1 end
|
||||
if k == "currentAnimName" then savedAnimName = v end
|
||||
if k == "currentTab" and v ~= "" then
|
||||
if v == "Setup" then v = "Animations"
|
||||
elseif v == "Render" then v = "Animations"
|
||||
elseif v == "Preview" then v = "Animations"
|
||||
elseif v == "GB" then v = "Optimize"
|
||||
end
|
||||
S.currentTab = v
|
||||
end
|
||||
if k == "animNames" and v ~= "" then
|
||||
S.animNames = {}
|
||||
for name in string.gmatch(v, "([^|]+)") do
|
||||
table.insert(S.animNames, name)
|
||||
end
|
||||
end
|
||||
if k == "useBgColor" then S.useBgColor = (v == "true") end
|
||||
if k == "bgColorR" then S.bgColor = Color(tonumber(v) or 0, S.bgColor.green, S.bgColor.blue) end
|
||||
if k == "bgColorG" then S.bgColor = Color(S.bgColor.red, tonumber(v) or 0, S.bgColor.blue) end
|
||||
if k == "bgColorB" then S.bgColor = Color(S.bgColor.red, S.bgColor.green, tonumber(v) or 0) end
|
||||
if k == "gbFlipOpt" then S.gbFlipOpt = (v == "true") end
|
||||
if k == "gbOffsetOpt" then S.gbOffsetOpt = (v == "true") end
|
||||
if k == "gbCompress" then S.gbCompress = (v == "true") end
|
||||
if k == "gbSimilarThreshold" then S.gbSimilarThreshold = tonumber(v) or 80 end
|
||||
if k == "gbZoomOpt" then S.gbZoomOpt = tonumber(v) or 3 end
|
||||
if k == "gbZoomSrc" then S.gbZoomSrc = tonumber(v) or 2 end
|
||||
if k == "gbSilhouette" then S.gbSilhouette = (v == "true") end
|
||||
if k == "gbSilhouetteColorR" then S.gbSilhouetteColor = Color(tonumber(v) or 0, S.gbSilhouetteColor.green, S.gbSilhouetteColor.blue) end
|
||||
if k == "gbSilhouetteColorG" then S.gbSilhouetteColor = Color(S.gbSilhouetteColor.red, tonumber(v) or 0, S.gbSilhouetteColor.blue) end
|
||||
if k == "gbSilhouetteColorB" then S.gbSilhouetteColor = Color(S.gbSilhouetteColor.red, S.gbSilhouetteColor.green, tonumber(v) or 0) end
|
||||
if k == "gbSilhouetteColorSet" then S.gbSilhouetteColorSet = (v == "true") end
|
||||
if k == "gbAnalyzeMode" then S.gbAnalyzeMode = v end
|
||||
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 == "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
|
||||
if k == "gbTileSize" then
|
||||
local ts = tonumber(v) or DEFAULT_GB_TILE_W
|
||||
S.gbTileW = ts
|
||||
S.gbTileH = ts
|
||||
end
|
||||
if k == "previewWindowOpen" then S.previewWindowOpen = (v == "true") end
|
||||
if k == "previewLayout" then S.previewLayout = v end
|
||||
if k == "previewLayoutValue" then S.previewLayoutValue = tonumber(v) or 2 end
|
||||
if k == "previewMode" then S.previewMode = v end
|
||||
if k == "previewSingleIdx" then S.previewSingleIdx = tonumber(v) or 1 end
|
||||
local animIdx = k and string.match(k, "^anim_(%d+)$")
|
||||
if animIdx then
|
||||
local idx = tonumber(animIdx) + 1
|
||||
if S.animNames[idx] then
|
||||
S.anims[S.animNames[idx]] = deserializeFrames(v)
|
||||
end
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
|
||||
-- Sort animation names alphabetically
|
||||
table.sort(S.animNames, function(a, b) return a:lower() < b:lower() end)
|
||||
|
||||
-- Resolve current animation by name
|
||||
if savedAnimName and savedAnimName ~= "" then
|
||||
for i, name in ipairs(S.animNames) do
|
||||
if name == savedAnimName then
|
||||
S.currentAnim = i
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if S.currentAnim < 1 then S.currentAnim = 1 end
|
||||
if S.currentAnim > #S.animNames then S.currentAnim = math.max(1, #S.animNames) end
|
||||
if S.previewSingleIdx < 1 then S.previewSingleIdx = 1 end
|
||||
if #S.animNames > 0 and S.previewSingleIdx > #S.animNames then
|
||||
S.previewSingleIdx = #S.animNames
|
||||
end
|
||||
|
||||
-- Save as Default preset
|
||||
S.currentPreset = "Default"
|
||||
savePreset("Default")
|
||||
saveMaster()
|
||||
return true
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Initialize: load master, then load appropriate preset
|
||||
----------------------------------------------------------------------
|
||||
local function initPresets()
|
||||
-- Try migration first
|
||||
local migrated = migrateOldPrefs()
|
||||
if migrated then return end
|
||||
|
||||
-- Load master file
|
||||
loadMaster()
|
||||
|
||||
-- Determine which preset to load
|
||||
local presetToLoad = S.currentPreset or "Default"
|
||||
|
||||
-- Check if current sprite has an associated preset
|
||||
if app.sprite and app.sprite.filename and app.sprite.filename ~= "" then
|
||||
local assoc = filePresetMap[app.sprite.filename]
|
||||
if assoc then
|
||||
presetToLoad = assoc
|
||||
end
|
||||
end
|
||||
|
||||
-- Load the preset
|
||||
loadPreset(presetToLoad)
|
||||
|
||||
-- Clamp previewSingleIdx
|
||||
if S.previewSingleIdx < 1 then S.previewSingleIdx = 1 end
|
||||
if #S.animNames > 0 and S.previewSingleIdx > #S.animNames then
|
||||
|
|
@ -288,58 +595,7 @@ local function loadPrefs()
|
|||
end
|
||||
end
|
||||
|
||||
local function savePrefs()
|
||||
local f = io.open(PREFS_FILE, "w")
|
||||
if not f then return end
|
||||
f:write("tileW=" .. S.tileW .. "\n")
|
||||
f:write("tileH=" .. S.tileH .. "\n")
|
||||
f:write("animSpeed=" .. S.animSpeed .. "\n")
|
||||
f:write("previewZoomAll=" .. S.previewZoomAll .. "\n")
|
||||
f:write("previewZoomOne=" .. S.previewZoomOne .. "\n")
|
||||
f:write("sourceZoom=" .. S.sourceZoom .. "\n")
|
||||
-- Save current animation name for robust resolution after sort
|
||||
local curName = ""
|
||||
if S.currentAnim >= 1 and S.currentAnim <= #S.animNames then
|
||||
curName = S.animNames[S.currentAnim]
|
||||
end
|
||||
f:write("currentAnimName=" .. curName .. "\n")
|
||||
f:write("currentTab=" .. S.currentTab .. "\n")
|
||||
f:write("animNames=" .. table.concat(S.animNames, "|") .. "\n")
|
||||
f:write("useBgColor=" .. tostring(S.useBgColor) .. "\n")
|
||||
f:write("bgColorR=" .. S.bgColor.red .. "\n")
|
||||
f:write("bgColorG=" .. S.bgColor.green .. "\n")
|
||||
f:write("bgColorB=" .. S.bgColor.blue .. "\n")
|
||||
f:write("gbFlipOpt=" .. tostring(S.gbFlipOpt) .. "\n")
|
||||
f:write("gbOffsetOpt=" .. tostring(S.gbOffsetOpt) .. "\n")
|
||||
f:write("gbCompress=" .. tostring(S.gbCompress) .. "\n")
|
||||
f:write("gbSimilarThreshold=" .. S.gbSimilarThreshold .. "\n")
|
||||
f:write("gbZoomOpt=" .. S.gbZoomOpt .. "\n")
|
||||
f:write("gbZoomSrc=" .. S.gbZoomSrc .. "\n")
|
||||
f:write("gbSilhouette=" .. tostring(S.gbSilhouette) .. "\n")
|
||||
f:write("gbSilhouetteColorR=" .. S.gbSilhouetteColor.red .. "\n")
|
||||
f:write("gbSilhouetteColorG=" .. S.gbSilhouetteColor.green .. "\n")
|
||||
f:write("gbSilhouetteColorB=" .. S.gbSilhouetteColor.blue .. "\n")
|
||||
f:write("gbSilhouetteColorSet=" .. tostring(S.gbSilhouetteColorSet) .. "\n")
|
||||
f:write("gbAnalyzeMode=" .. S.gbAnalyzeMode .. "\n")
|
||||
f:write("gbLastSavePath=" .. S.gbLastSavePath .. "\n")
|
||||
f:write("gbLayerName=" .. S.gbLayerName .. "\n")
|
||||
f:write("gbAlwaysOverwrite=" .. tostring(S.gbAlwaysOverwrite) .. "\n")
|
||||
f:write("gbTileW=" .. S.gbTileW .. "\n")
|
||||
f:write("gbTileH=" .. S.gbTileH .. "\n")
|
||||
-- Save previewWindowOpen
|
||||
f:write("previewWindowOpen=" .. tostring(S.previewWindowOpen) .. "\n")
|
||||
-- Save preview layout prefs
|
||||
f:write("previewLayout=" .. S.previewLayout .. "\n")
|
||||
f:write("previewLayoutValue=" .. S.previewLayoutValue .. "\n")
|
||||
f:write("previewMode=" .. S.previewMode .. "\n")
|
||||
f:write("previewSingleIdx=" .. S.previewSingleIdx .. "\n")
|
||||
for i, name in ipairs(S.animNames) do
|
||||
f:write("anim_" .. (i - 1) .. "=" .. serializeFrames(S.anims[name] or {}) .. "\n")
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
|
||||
loadPrefs()
|
||||
initPresets()
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Helpers
|
||||
|
|
@ -969,21 +1225,6 @@ 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)
|
||||
----------------------------------------------------------------------
|
||||
|
|
@ -1034,7 +1275,7 @@ openPreviewWindow = function()
|
|||
pvHeight = math.max(pvHeight, 40)
|
||||
|
||||
previewDlg = Dialog{
|
||||
title = getPreviewTitle(),
|
||||
title = "AniPhallow Preview",
|
||||
onclose = function()
|
||||
if previewTimer then pcall(function() previewTimer:stop() end) end
|
||||
if pvRefreshTimer then pcall(function() pvRefreshTimer:stop() end) end
|
||||
|
|
@ -1042,7 +1283,7 @@ openPreviewWindow = function()
|
|||
pvRefreshTimer = nil
|
||||
previewDlg = nil
|
||||
S.previewWindowOpen = false
|
||||
savePrefs()
|
||||
saveAll()
|
||||
end
|
||||
}
|
||||
|
||||
|
|
@ -1068,7 +1309,7 @@ openPreviewWindow = function()
|
|||
else
|
||||
S.previewMode = "all"
|
||||
end
|
||||
-- Close and reopen to update title
|
||||
-- Close and reopen to resize canvas
|
||||
pcall(function() previewDlg:close() end)
|
||||
openPreviewWindow()
|
||||
end
|
||||
|
|
@ -1163,17 +1404,21 @@ openPreviewWindow = function()
|
|||
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()
|
||||
-- Update label text and repaint without closing/reopening
|
||||
pcall(function()
|
||||
previewDlg:modify{ id = "pvAnimName", text = S.animNames[S.previewSingleIdx] or "" }
|
||||
end)
|
||||
previewDlg:repaint()
|
||||
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()
|
||||
-- Update label text and repaint without closing/reopening
|
||||
pcall(function()
|
||||
previewDlg:modify{ id = "pvAnimName", text = S.animNames[S.previewSingleIdx] or "" }
|
||||
end)
|
||||
previewDlg:repaint()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1189,6 +1434,17 @@ openPreviewWindow = function()
|
|||
end
|
||||
}
|
||||
|
||||
-- Animation name label for 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")
|
||||
}
|
||||
|
||||
previewTimer = Timer{
|
||||
interval = S.animSpeed / 1000.0,
|
||||
ontick = function()
|
||||
|
|
@ -1203,12 +1459,29 @@ openPreviewWindow = function()
|
|||
ontick = function()
|
||||
refreshSource()
|
||||
pcall(function() previewDlg:repaint() end)
|
||||
|
||||
-- Auto-detect file change
|
||||
local currentFile = app.sprite and app.sprite.filename or ""
|
||||
if currentFile ~= lastSpriteFilename then
|
||||
lastSpriteFilename = currentFile
|
||||
-- Look up preset for this file
|
||||
local presetForFile = filePresetMap[currentFile] or "Default"
|
||||
if presetForFile ~= S.currentPreset then
|
||||
loadPreset(presetForFile)
|
||||
S.currentPreset = presetForFile
|
||||
-- Refresh UI if main dialog is open
|
||||
if mainDlg then
|
||||
pcall(function() mainDlg:close() end)
|
||||
openMainDialog()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
}
|
||||
pvRefreshTimer:start()
|
||||
|
||||
S.previewWindowOpen = true
|
||||
savePrefs()
|
||||
saveAll()
|
||||
|
||||
previewDlg:show{ wait = false }
|
||||
end
|
||||
|
|
@ -1294,7 +1567,7 @@ openMainDialog = function()
|
|||
mainAnimTimer = nil
|
||||
mainRefreshTimer = nil
|
||||
mainDlg = nil
|
||||
savePrefs()
|
||||
saveAll()
|
||||
end
|
||||
}
|
||||
mainDlg = dlg
|
||||
|
|
@ -1321,11 +1594,171 @@ openMainDialog = function()
|
|||
text = "Config",
|
||||
onclick = function()
|
||||
local d = Dialog{ title = "Config" }
|
||||
|
||||
--------------------------------------------------------
|
||||
-- Presets section
|
||||
--------------------------------------------------------
|
||||
d:separator{ text = "Presets" }
|
||||
local presetNames = getAllPresetNames()
|
||||
d:combobox{ id = "presetList", label = "Preset:", option = S.currentPreset, options = presetNames }
|
||||
d:button{ id = "btnPresetSave", text = "Save", onclick = function()
|
||||
local saveDlg = Dialog{ title = "Save Preset" }
|
||||
saveDlg:entry{ id = "name", label = "Name:", text = S.currentPreset }
|
||||
saveDlg:button{ id = "ok", text = "Save" }
|
||||
saveDlg:button{ text = "Cancel" }
|
||||
saveDlg:show()
|
||||
if saveDlg.data.ok then
|
||||
local pname = saveDlg.data.name
|
||||
if pname and pname ~= "" then
|
||||
-- Check if name exists and differs from current
|
||||
if pname ~= S.currentPreset then
|
||||
-- Check if preset already exists
|
||||
local exists = false
|
||||
for _, n in ipairs(knownPresetNames) do
|
||||
if n == pname then exists = true; break end
|
||||
end
|
||||
if exists then
|
||||
local confirm = app.alert{
|
||||
title = "Overwrite Preset",
|
||||
text = "Preset '" .. pname .. "' already exists. Overwrite?",
|
||||
buttons = { "Overwrite", "Cancel" }
|
||||
}
|
||||
if confirm ~= 1 then return end
|
||||
end
|
||||
end
|
||||
S.currentPreset = pname
|
||||
-- Add to known names if new
|
||||
local found = false
|
||||
for _, n in ipairs(knownPresetNames) do
|
||||
if n == pname then found = true; break end
|
||||
end
|
||||
if not found then
|
||||
table.insert(knownPresetNames, pname)
|
||||
end
|
||||
-- Associate with current file
|
||||
if app.sprite and app.sprite.filename and app.sprite.filename ~= "" then
|
||||
filePresetMap[app.sprite.filename] = pname
|
||||
end
|
||||
saveAll()
|
||||
-- Update combobox
|
||||
pcall(function()
|
||||
d:modify{ id = "presetList", options = getAllPresetNames(), option = S.currentPreset }
|
||||
end)
|
||||
end
|
||||
end
|
||||
end }
|
||||
d:button{ id = "btnPresetLoad", text = "Load", onclick = function()
|
||||
local selectedPreset = d.data.presetList
|
||||
if selectedPreset and selectedPreset ~= "" then
|
||||
-- Save current preset first
|
||||
savePreset(S.currentPreset)
|
||||
-- Load the new preset
|
||||
loadPreset(selectedPreset)
|
||||
S.currentPreset = selectedPreset
|
||||
-- Associate with current file
|
||||
if app.sprite and app.sprite.filename and app.sprite.filename ~= "" then
|
||||
filePresetMap[app.sprite.filename] = selectedPreset
|
||||
end
|
||||
saveMaster()
|
||||
-- Close config dialog and reopen main dialog
|
||||
d:close()
|
||||
if mainDlg then
|
||||
pcall(function() mainDlg:close() end)
|
||||
end
|
||||
openMainDialog()
|
||||
end
|
||||
end }
|
||||
d:button{ id = "btnPresetClone", text = "Clone", onclick = function()
|
||||
local cloneDlg = Dialog{ title = "Clone Preset" }
|
||||
cloneDlg:entry{ id = "name", label = "New name:", text = S.currentPreset .. "_copy" }
|
||||
cloneDlg:button{ id = "ok", text = "Clone" }
|
||||
cloneDlg:button{ text = "Cancel" }
|
||||
cloneDlg:show()
|
||||
if cloneDlg.data.ok then
|
||||
local newName = cloneDlg.data.name
|
||||
if newName and newName ~= "" then
|
||||
-- Check if name already exists
|
||||
local exists = false
|
||||
for _, n in ipairs(knownPresetNames) do
|
||||
if n == newName then exists = true; break end
|
||||
end
|
||||
if exists then
|
||||
app.alert("Preset '" .. newName .. "' already exists.")
|
||||
return
|
||||
end
|
||||
-- Save current state as the new preset
|
||||
savePreset(newName)
|
||||
table.insert(knownPresetNames, newName)
|
||||
S.currentPreset = newName
|
||||
-- Associate with current file
|
||||
if app.sprite and app.sprite.filename and app.sprite.filename ~= "" then
|
||||
filePresetMap[app.sprite.filename] = newName
|
||||
end
|
||||
saveMaster()
|
||||
-- Update combobox
|
||||
pcall(function()
|
||||
d:modify{ id = "presetList", options = getAllPresetNames(), option = S.currentPreset }
|
||||
end)
|
||||
end
|
||||
end
|
||||
end }
|
||||
d:button{ id = "btnPresetDelete", text = "Delete", onclick = function()
|
||||
local selectedPreset = d.data.presetList
|
||||
if not selectedPreset or selectedPreset == "" then return end
|
||||
if selectedPreset == "Default" then
|
||||
app.alert("Cannot delete the Default preset.")
|
||||
return
|
||||
end
|
||||
local confirm = app.alert{
|
||||
title = "Delete Preset",
|
||||
text = "Delete preset '" .. selectedPreset .. "'?",
|
||||
buttons = { "Delete", "Cancel" }
|
||||
}
|
||||
if confirm ~= 1 then return end
|
||||
-- Delete the preset file
|
||||
local path = app.fs.joinPath(PRESETS_DIR, selectedPreset .. ".ini")
|
||||
os.remove(path)
|
||||
-- Remove from known names
|
||||
for i, n in ipairs(knownPresetNames) do
|
||||
if n == selectedPreset then
|
||||
table.remove(knownPresetNames, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
-- Remove file associations pointing to this preset
|
||||
for filepath, preset in pairs(filePresetMap) do
|
||||
if preset == selectedPreset then
|
||||
filePresetMap[filepath] = nil
|
||||
end
|
||||
end
|
||||
-- If current preset was deleted, switch to Default
|
||||
if S.currentPreset == selectedPreset then
|
||||
loadPreset("Default")
|
||||
S.currentPreset = "Default"
|
||||
if app.sprite and app.sprite.filename and app.sprite.filename ~= "" then
|
||||
filePresetMap[app.sprite.filename] = nil
|
||||
end
|
||||
end
|
||||
saveMaster()
|
||||
-- Update combobox
|
||||
pcall(function()
|
||||
d:modify{ id = "presetList", options = getAllPresetNames(), option = S.currentPreset }
|
||||
end)
|
||||
end }
|
||||
|
||||
--------------------------------------------------------
|
||||
-- Animations section
|
||||
--------------------------------------------------------
|
||||
d:separator{ text = "Animations" }
|
||||
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:check{ id = "useBgColor", text = "Solid background", selected = S.useBgColor }
|
||||
d:color{ id = "bgColor", label = "Bg Color:", color = S.bgColor }
|
||||
|
||||
--------------------------------------------------------
|
||||
-- Optimization section
|
||||
--------------------------------------------------------
|
||||
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 }
|
||||
|
|
@ -1336,6 +1769,10 @@ openMainDialog = function()
|
|||
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 }
|
||||
|
||||
--------------------------------------------------------
|
||||
-- Preview section
|
||||
--------------------------------------------------------
|
||||
d:separator{ text = "Preview" }
|
||||
d:combobox{ id = "previewLayout", label = "Layout:", option = S.previewLayout, options = {"auto", "fixedCols", "fixedRows"} }
|
||||
d:number{ id = "previewLayoutValue", label = "Value:", text = tostring(S.previewLayoutValue), decimals = 0 }
|
||||
|
|
@ -1415,7 +1852,7 @@ openMainDialog = function()
|
|||
for i, n in ipairs(S.animNames) do
|
||||
if n == name then S.currentAnim = i; break end
|
||||
end
|
||||
savePrefs()
|
||||
saveAll()
|
||||
dlg:close()
|
||||
openMainDialog()
|
||||
end
|
||||
|
|
@ -1439,7 +1876,7 @@ openMainDialog = function()
|
|||
table.remove(S.animNames, S.currentAnim)
|
||||
S.currentAnim = math.min(S.currentAnim, math.max(1, #S.animNames))
|
||||
S.selectedFrame = 0
|
||||
savePrefs()
|
||||
saveAll()
|
||||
dlg:close()
|
||||
openMainDialog()
|
||||
end
|
||||
|
|
@ -1468,7 +1905,7 @@ openMainDialog = function()
|
|||
for i, n in ipairs(S.animNames) do
|
||||
if n == newName then S.currentAnim = i; break end
|
||||
end
|
||||
savePrefs()
|
||||
saveAll()
|
||||
dlg:close()
|
||||
openMainDialog()
|
||||
end
|
||||
|
|
@ -1507,7 +1944,7 @@ openMainDialog = function()
|
|||
for i, n in ipairs(S.animNames) do
|
||||
if n == cloneName then S.currentAnim = i; break end
|
||||
end
|
||||
savePrefs()
|
||||
saveAll()
|
||||
dlg:close()
|
||||
openMainDialog()
|
||||
end
|
||||
|
|
@ -1882,7 +2319,7 @@ openMainDialog = function()
|
|||
table.remove(S.animNames, S.currentAnim)
|
||||
S.currentAnim = math.min(S.currentAnim, math.max(1, #S.animNames))
|
||||
S.selectedFrame = 0
|
||||
savePrefs()
|
||||
saveAll()
|
||||
dlg:close()
|
||||
openMainDialog()
|
||||
return
|
||||
|
|
@ -2484,9 +2921,6 @@ openMainDialog = function()
|
|||
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
|
||||
-- Non-compress mode: candidate-based click like Occurrences canvas
|
||||
for i, tile in ipairs(S.gbTiles) do
|
||||
|
|
@ -2726,6 +3160,19 @@ openMainDialog = function()
|
|||
end
|
||||
pcall(function() dlg:repaint() end)
|
||||
end
|
||||
|
||||
-- Auto-detect file change (also in main dialog timer)
|
||||
local currentFile = app.sprite and app.sprite.filename or ""
|
||||
if currentFile ~= lastSpriteFilename then
|
||||
lastSpriteFilename = currentFile
|
||||
local presetForFile = filePresetMap[currentFile] or "Default"
|
||||
if presetForFile ~= S.currentPreset then
|
||||
loadPreset(presetForFile)
|
||||
S.currentPreset = presetForFile
|
||||
pcall(function() dlg:close() end)
|
||||
openMainDialog()
|
||||
end
|
||||
end
|
||||
end
|
||||
}
|
||||
mainRefreshTimer:start()
|
||||
|
|
@ -2743,6 +3190,12 @@ local function run()
|
|||
app.alert("No sprite is open.")
|
||||
return
|
||||
end
|
||||
-- Toggle: if already running, close everything
|
||||
if previewDlg then
|
||||
pcall(function() previewDlg:close() end)
|
||||
if mainDlg then pcall(function() mainDlg:close() end) end
|
||||
return
|
||||
end
|
||||
S.currentTab = "Animations"
|
||||
refreshSource()
|
||||
-- Always open preview window on launch
|
||||
|
|
|
|||
Loading…
Reference in New Issue