Advertisement
dominus

Untitled

Mar 19th, 2025
124
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 25.63 KB | None | 0 0
  1. -- Exult SHP Format Extension for Aseprite
  2.  
  3. local pluginName = "exult-shp"
  4. local pluginDir = app.fs.joinPath(app.fs.userConfigPath, "extensions", pluginName)
  5. local converterPath = app.fs.joinPath(pluginDir, "exult_shp")
  6.  
  7. -- Debug system with toggle
  8. local debugEnabled = true  -- toggle debug messages
  9.  
  10. local function debug(message)
  11.   if debugEnabled then
  12.     print("[Exult SHP] " .. message)
  13.   end
  14. end
  15.  
  16. local function logError(message)
  17.   -- Always print errors regardless of debug setting
  18.   print("[Exult SHP ERROR] " .. message)
  19. end
  20.  
  21. -- Global utility function for quoting paths with spaces
  22. function quoteIfNeeded(path)
  23.   if path:find(" ") then
  24.     return '"' .. path .. '"'
  25.   else
  26.     return path
  27.   end
  28. end
  29.  
  30. -- Helper to run commands with hidden output
  31. function executeHidden(cmd)
  32.   -- For debugging, run raw command instead with output captured
  33.   if debugEnabled then
  34.     debug("Executing with output capture: " .. cmd)
  35.     local tmpFile = app.fs.joinPath(app.fs.tempPath, "exult-shp-output-" .. os.time() .. ".txt")
  36.    
  37.     -- Add output redirection to file
  38.     local redirectCmd
  39.     if app.fs.pathSeparator == "\\" then
  40.       -- Windows
  41.       redirectCmd = cmd .. " > " .. quoteIfNeeded(tmpFile) .. " 2>&1"
  42.     else
  43.       -- Unix-like (macOS, Linux)
  44.       redirectCmd = cmd .. " > " .. quoteIfNeeded(tmpFile) .. " 2>&1"
  45.     end
  46.    
  47.     -- Execute the command
  48.     local success = os.execute(redirectCmd)
  49.    
  50.     -- Read and log the output
  51.     if app.fs.isFile(tmpFile) then
  52.       local file = io.open(tmpFile, "r")
  53.       if file then
  54.         debug("Command output:")
  55.         local output = file:read("*all")
  56.         debug(output or "<no output>")
  57.         file:close()
  58.       end
  59.       -- Clean up temp file (comment this out if you want to keep the logs)
  60.       -- app.fs.removeFile(tmpFile)
  61.     end
  62.    
  63.     return success
  64.   else
  65.     -- Check operating system and add appropriate redirection
  66.     local redirectCmd
  67.     if app.fs.pathSeparator == "\\" then
  68.       -- Windows
  69.       redirectCmd = cmd .. " > NUL 2>&1"
  70.     else
  71.       -- Unix-like (macOS, Linux)
  72.       redirectCmd = cmd .. " > /dev/null 2>&1"
  73.     end
  74.    
  75.     return os.execute(redirectCmd)
  76.   end
  77. end
  78.  
  79. debug("Plugin initializing...")
  80. debug("System Information:")
  81. debug("OS: " .. (app.fs.pathSeparator == "/" and "Unix-like" or "Windows"))
  82. debug("Temp path: " .. app.fs.tempPath)
  83. debug("User config path: " .. app.fs.userConfigPath)
  84.  
  85. debug("Converter expected at: " .. converterPath)
  86.  
  87. -- Check if converterPath exists with OS-specific extension
  88. if not app.fs.isFile(converterPath) then
  89.   debug("Converter not found, checking for extensions...")
  90.   if app.fs.isFile(converterPath..".exe") then
  91.     converterPath = converterPath..".exe"
  92.     debug("Found Windows converter: " .. converterPath)
  93.   elseif app.fs.isFile(converterPath..".bin") then
  94.     converterPath = converterPath..".bin"
  95.     debug("Found binary converter: " .. converterPath)
  96.   end
  97. end
  98.  
  99. -- Verify converter exists at startup
  100. local converterExists = app.fs.isFile(converterPath)
  101. debug("Converter exists: " .. tostring(converterExists))
  102.  
  103. -- Make the converter executable once at startup if needed
  104. if converterExists and app.fs.pathSeparator == "/" then
  105.   local chmodCmd = "chmod +x " .. quoteIfNeeded(converterPath)
  106.   debug("Executing chmod at plugin initialization: " .. chmodCmd)
  107.   executeHidden(chmodCmd)
  108. end
  109.  
  110. -- Error display helper
  111. function showError(message)
  112.   logError(message)
  113.   app.alert{
  114.     title="Exult SHP Error",
  115.     text=message
  116.   }
  117. end
  118.  
  119. -- Don't detect Animation sequences when opening files
  120. function disableAnimationDetection()
  121.   -- Store the original preference value if it exists
  122.   if app.preferences and app.preferences.open_file and app.preferences.open_file.open_sequence ~= nil then
  123.     _G._originalSeqPref = app.preferences.open_file.open_sequence
  124.     -- Set to 2 which means "skip the prompt without loading as animation"
  125.     app.preferences.open_file.open_sequence = 2
  126.   end
  127. end
  128.  
  129. function restoreAnimationDetection()
  130.   -- Restore the original preference if we saved it
  131.   if app.preferences and app.preferences.open_file and _G._originalSeqPref ~= nil then
  132.     app.preferences.open_file.open_sequence = _G._originalSeqPref
  133.   end
  134. end
  135.  
  136. -- File format registration function
  137. function registerSHPFormat()
  138.   if not converterExists then
  139.     showError("SHP converter tool not found at:\n" .. converterPath ..
  140.               "\nSHP files cannot be opened until this is fixed.")
  141.     return false
  142.   end
  143.   return true
  144. end
  145.  
  146. function importSHP(filename)
  147.   -- Handle direct file opening (when user opens .shp file)
  148.   if filename then
  149.     debug("Opening file directly: " .. filename)
  150.    
  151.     -- Create temp directory for files
  152.     local tempDir = app.fs.joinPath(app.fs.tempPath, "exult-shp-" .. os.time())
  153.     app.fs.makeDirectory(tempDir)
  154.    
  155.     -- Prepare output file path
  156.     local outputBasePath = app.fs.joinPath(tempDir, "output")
  157.    
  158.     -- Use default palette and separate frames
  159.     return processImport(filename, "", outputBasePath, true)
  160.   end
  161.  
  162.   -- Normal dialog flow for manual import
  163.   local dlg = Dialog("Import SHP File")
  164.   dlg:file{
  165.     id="shpFile",
  166.     label="SHP File:",
  167.     title="Select SHP File",
  168.     open=true,
  169.     filetypes={"shp"},
  170.     focus=true
  171.   }
  172.   dlg:file{
  173.     id="paletteFile",
  174.     label="Palette File (optional):",
  175.     open=true
  176.   }
  177.  
  178.   -- Store dialog result in outer scope
  179.   local dialogResult = false
  180.   local importSettings = {}
  181.  
  182.   dlg:button{
  183.     id="import",
  184.     text="Import",
  185.     onclick=function()
  186.       dialogResult = true
  187.       importSettings.shpFile = dlg.data.shpFile
  188.       importSettings.paletteFile = dlg.data.paletteFile
  189.       dlg:close()
  190.     end
  191.   }
  192.  
  193.   dlg:button{
  194.     id="cancel",
  195.     text="Cancel",
  196.     onclick=function()
  197.       dialogResult = false
  198.       dlg:close()
  199.     end
  200.   }
  201.  
  202.   -- Show dialog
  203.   dlg:show()
  204.  
  205.   -- Handle result
  206.   if not dialogResult then return end
  207.  
  208.   if not importSettings.shpFile or importSettings.shpFile == "" then
  209.     showError("Please select an SHP file to import")
  210.     return
  211.   end
  212.  
  213.   -- Create temp directory for files
  214.   local tempDir = app.fs.joinPath(app.fs.tempPath, "exult-shp-" .. os.time())
  215.   app.fs.makeDirectory(tempDir)
  216.  
  217.   -- Prepare output file path
  218.   local outputBasePath = app.fs.joinPath(tempDir, "output")
  219.  
  220.   return processImport(importSettings.shpFile,
  221.                       importSettings.paletteFile or "",
  222.                       outputBasePath,
  223.                       true)
  224. end
  225.  
  226. -- Global table to store layer offset data
  227. if not _G.exultLayerOffsets then
  228.   _G.exultLayerOffsets = {}
  229. end
  230.  
  231. -- Enhance the getLayerOffsetData function to parse from layer names when not in global table:
  232. function getLayerOffsetData(layer)
  233.   -- Generate a unique key for the layer based on sprite and layer name
  234.   local key = ""
  235.   if layer.sprite and layer.sprite.filename then
  236.     key = layer.sprite.filename .. ":"
  237.   end
  238.  
  239.   -- Get clean name (without offset info)
  240.   local cleanName = layer.name:gsub(" %[.*%]$", "")
  241.   key = key .. cleanName  -- Use clean name for lookup
  242.  
  243.   -- First check our global table
  244.   local data = _G.exultLayerOffsets[key]
  245.   if data then
  246.     return data
  247.   end
  248.  
  249.   -- If not found in table, try to extract from layer name
  250.   local offsetX, offsetY = layer.name:match(" %[(%d+),(%d+)%]$")
  251.   if offsetX and offsetY then
  252.     -- Store extracted values in global table for future use
  253.     offsetX = tonumber(offsetX)
  254.     offsetY = tonumber(offsetY)
  255.    
  256.     _G.exultLayerOffsets[key] = {
  257.       offsetX = offsetX,
  258.       offsetY = offsetY
  259.     }
  260.    
  261.     debug("Extracted offset data from layer name: " .. offsetX .. "," .. offsetY)
  262.     return _G.exultLayerOffsets[key]
  263.   end
  264.  
  265.   return nil
  266. end
  267.  
  268. -- Also update setLayerOffsetData to store with clean name:
  269. function setLayerOffsetData(layer, offsetX, offsetY)
  270.   -- Generate a unique key for the layer based on sprite and layer name
  271.   local key = ""
  272.   if layer.sprite and layer.sprite.filename then
  273.     key = layer.sprite.filename .. ":"
  274.   end
  275.  
  276.   -- Get clean name (without offset info)
  277.   local cleanName = layer.name:gsub(" %[.*%]$", "")
  278.   key = key .. cleanName  -- Use clean name for lookup
  279.  
  280.   -- Store in global table
  281.   _G.exultLayerOffsets[key] = {
  282.     offsetX = offsetX,
  283.     offsetY = offsetY
  284.   }
  285.  
  286.   -- Also encode the offset in the layer name for visualization
  287.   layer.name = cleanName .. " [" .. offsetX .. "," .. offsetY .. "]"
  288. end
  289.  
  290. -- Modify the processImport function to use layers instead of frames
  291. function processImport(shpFile, paletteFile, outputBasePath, createSeparateFrames)
  292.   if not converterExists then
  293.     showError("SHP converter not found at: " .. converterPath)
  294.     return false
  295.   end
  296.  
  297.   debug("Importing SHP: " .. shpFile)
  298.   debug("Palette: " .. (paletteFile ~= "" and paletteFile or "default"))
  299.   debug("Output: " .. outputBasePath)
  300.  
  301.   -- Check if file exists
  302.   if not app.fs.isFile(shpFile) then
  303.     showError("SHP file not found: " .. shpFile)
  304.     return false
  305.   end
  306.  
  307.   -- Extract base filename from the SHP file (without path and extension)
  308.   local shpBaseName = shpFile:match("([^/\\]+)%.[^.]*$") or "output"
  309.   shpBaseName = shpBaseName:gsub("%.shp$", "")
  310.   debug("Extracted SHP base name: " .. shpBaseName)
  311.  
  312.   -- Extract output directory from outputBasePath
  313.   local outputDir = outputBasePath:match("(.*[/\\])") or ""
  314.  
  315.   -- Combine to get the actual path where files will be created
  316.   local actualOutputBase = outputDir .. shpBaseName
  317.   debug("Expected output base: " .. actualOutputBase)
  318.  
  319.   -- Create command - always use separate frames mode
  320.   local cmd = quoteIfNeeded(converterPath) .. " import " .. quoteIfNeeded(shpFile) .. " " .. quoteIfNeeded(outputBasePath)
  321.  
  322.   -- Only add palette if it's not empty
  323.   if paletteFile and paletteFile ~= "" then
  324.     cmd = cmd .. " " .. quoteIfNeeded(paletteFile)
  325.   end
  326.  
  327.   -- Always use separate frames
  328.   cmd = cmd .. " separate"
  329.  
  330.   debug("Executing: " .. cmd)
  331.  
  332.   -- Execute command
  333.   local success = executeHidden(cmd)
  334.   debug("Command execution " .. (success and "succeeded" or "failed"))
  335.  
  336.   -- Check for output files - using the actual base path with SHP filename
  337.   local firstFrame = actualOutputBase .. "_0.png"
  338.  
  339.   debug("Looking for first frame at: " .. firstFrame)
  340.   debug("File exists: " .. tostring(app.fs.isFile(firstFrame)))
  341.  
  342.   if not app.fs.isFile(firstFrame) then
  343.     debug("ERROR: Failed to convert SHP file")
  344.     return false
  345.   end
  346.  
  347.   -- Continue with loading the frames
  348.   debug("Loading output files into Aseprite")
  349.  
  350.   -- First scan for all frames to find max dimensions for canvas
  351.   local maxWidth, maxHeight = 0, 0
  352.   local frameIndex = 0
  353.  
  354.   while true do
  355.     local framePath = actualOutputBase .. "_" .. frameIndex .. ".png"
  356.     if not app.fs.isFile(framePath) then break end
  357.    
  358.     local image = Image{fromFile=framePath}
  359.     maxWidth = math.max(maxWidth, image.width)
  360.     maxHeight = math.max(maxHeight, image.height)
  361.     frameIndex = frameIndex + 1
  362.   end
  363.  
  364.   debug("Maximum dimensions across all frames: " .. maxWidth .. "x" .. maxHeight)
  365.  
  366.   -- Now load first frame
  367.   local firstFrame = actualOutputBase .. "_0.png"
  368.   local firstMeta = actualOutputBase .. "_0.meta"
  369.  
  370.   if not app.fs.isFile(firstFrame) then
  371.     showError("First frame not found: " .. firstFrame)
  372.     return false
  373.   end
  374.  
  375.   -- Open the first image as a sprite
  376.   debug("Opening first frame: " .. firstFrame)
  377.  
  378.   -- Disable animation detection before opening file
  379.   disableAnimationDetection()
  380.  
  381.   -- Open the file normally
  382.   local sprite = app.open(firstFrame)
  383.  
  384.   -- Restore original settings
  385.   restoreAnimationDetection()
  386.  
  387.   if not sprite then
  388.     showError("Failed to open first frame")
  389.     return false
  390.   end
  391.  
  392.   -- RESIZE TO MAXIMUM DIMENSIONS - add this block
  393.   if sprite.width < maxWidth or sprite.height < maxHeight then
  394.     debug("Resizing sprite to maximum dimensions: " .. maxWidth .. "x" .. maxHeight)
  395.    
  396.     -- Calculate center offsets to keep the content centered
  397.     local offsetX = math.floor((maxWidth - sprite.width) / 2)
  398.     local offsetY = math.floor((maxHeight - sprite.height) / 2)
  399.    
  400.     -- Resize the sprite with calculated offsets
  401.     sprite:resize(maxWidth, maxHeight, offsetX, offsetY)
  402.   end
  403.  
  404.   -- Rename the first layer to indicate it's frame 1
  405.   local baseLayer = sprite.layers[1]
  406.   baseLayer.name = "Frame 1"
  407.  
  408.   -- Set pivot from metadata for first frame
  409.   if app.fs.isFile(firstMeta) then
  410.     local meta = io.open(firstMeta, "r")
  411.     if meta then
  412.       local offsetX, offsetY = 0, 0
  413.       for line in meta:lines() do
  414.         local key, value = line:match("(.+)=(.+)")
  415.         if key == "offset_x" then offsetX = tonumber(value) end
  416.         if key == "offset_y" then offsetY = tonumber(value) end
  417.       end
  418.       meta:close()
  419.      
  420.       -- Store offset data in the layer's user data
  421.       setLayerOffsetData(baseLayer, offsetX, offsetY)
  422.     end
  423.   end
  424.  
  425.   -- Now add additional frames as layers
  426.   local frameIndex = 1
  427.   while true do
  428.     local framePath = actualOutputBase .. "_" .. frameIndex .. ".png"
  429.     local metaPath = actualOutputBase .. "_" .. frameIndex .. ".meta"
  430.    
  431.     if not app.fs.isFile(framePath) then
  432.       debug("No more frames at index " .. frameIndex)
  433.       break
  434.     end
  435.    
  436.     debug("Adding frame " .. frameIndex .. " as layer")
  437.    
  438.     -- Load the image
  439.     local frameImage = Image{fromFile=framePath}
  440.    
  441.     -- Add new layer (instead of frame)
  442.     local newLayer = sprite:newLayer()
  443.     newLayer.name = "Frame " .. (frameIndex + 1) -- 1-based naming
  444.    
  445.     -- Create new cel with this image (in the first frame)
  446.     local cel = sprite:newCel(newLayer, 1, frameImage, Point(0,0))
  447.    
  448.     -- Load and set offset data from metadata
  449.     if app.fs.isFile(metaPath) then
  450.       local meta = io.open(metaPath, "r")
  451.       if meta then
  452.         local offsetX, offsetY = 0, 0
  453.         for line in meta:lines() do
  454.           local key, value = line:match("(.+)=(.+)")
  455.           if key == "offset_x" then offsetX = tonumber(value) end
  456.           if key == "offset_y" then offsetY = tonumber(value) end
  457.         end
  458.         meta:close()
  459.        
  460.         -- Store offset in layer user data
  461.         setLayerOffsetData(newLayer, offsetX, offsetY)
  462.        
  463.         debug("Stored offset data for layer " .. newLayer.name .. ": " .. offsetX .. "," .. offsetY)
  464.       end
  465.     end
  466.    
  467.     frameIndex = frameIndex + 1
  468.   end
  469.  
  470.   return true, sprite
  471. end
  472.  
  473. -- Add this helper function to check for offset tags
  474. function spriteHasOffsetTags(sprite)
  475.   if not sprite or not sprite.tags then return false end
  476.  
  477.   for _, tag in ipairs(sprite.tags) do
  478.     -- Check for any tag starting with "offset_"
  479.     if tag.name:match("^offset_") then
  480.       return true
  481.     end
  482.   end
  483.   return false
  484. end
  485.  
  486. -- Replace the exportSHP function with this improved version:
  487. function exportSHP()
  488.   -- Get active sprite
  489.   local sprite = app.activeSprite
  490.   if not sprite then
  491.     showError("No active sprite to export")
  492.     return
  493.   end
  494.  
  495.   -- Check if sprite uses indexed color mode
  496.   if sprite.colorMode ~= ColorMode.INDEXED then
  497.     showError("SHP format needs an indexed palette. Convert your sprite to Indexed color mode first.")
  498.     return
  499.   end
  500.  
  501.   -- Default offset values for fallback
  502.   local defaultOffsetX = math.floor(sprite.width / 2)
  503.   local defaultOffsetY = math.floor(sprite.height / 2)
  504.  
  505.   -- Count how many layers already have offset data
  506.   local layersWithOffsets = 0
  507.   for _, layer in ipairs(sprite.layers) do
  508.     if getLayerOffsetData(layer) then
  509.       layersWithOffsets = layersWithOffsets + 1
  510.     end
  511.   end
  512.  
  513.   -- Show initial export dialog - simplified without offset fields
  514.   local dlg = Dialog("Export SHP File")
  515.   dlg:file{
  516.     id="outFile",
  517.     label="Output SHP File:",
  518.     save=true,
  519.     filetypes={"shp"},
  520.     focus=true
  521.   }
  522.  
  523.   -- Show informational text about layer offsets
  524.   if layersWithOffsets == #sprite.layers then
  525.     dlg:label{
  526.       id="allOffsetsSet",
  527.       text="All layers have offset data. Ready to export."
  528.     }
  529.   elseif layersWithOffsets > 0 then
  530.     dlg:label{
  531.       id="someOffsetsSet",
  532.       text=layersWithOffsets .. " of " .. #sprite.layers .. " layers have offset data. You'll be prompted for the rest."
  533.     }
  534.   else
  535.     dlg:label{
  536.       id="noOffsets",
  537.       text="No layers have offset data. You'll be prompted to set offsets for each layer."
  538.     }
  539.   end
  540.  
  541.   -- Add option to edit existing offsets
  542.   dlg:check{
  543.     id="editExisting",
  544.     text="Edit existing offsets",
  545.     selected=false
  546.   }
  547.  
  548.   -- Store dialog result in outer scope
  549.   local dialogResult = false
  550.   local exportSettings = {}
  551.  
  552.   dlg:button{
  553.     id="export",
  554.     text="Export",
  555.     onclick=function()
  556.       dialogResult = true
  557.       exportSettings.outFile = dlg.data.outFile
  558.       exportSettings.editExisting = dlg.data.editExisting
  559.       dlg:close()
  560.     end
  561.   }
  562.  
  563.   dlg:button{
  564.     id="cancel",
  565.     text="Cancel",
  566.     onclick=function()
  567.       dialogResult = false
  568.       dlg:close()
  569.     end
  570.   }
  571.  
  572.   -- Show dialog
  573.   dlg:show()
  574.  
  575.   -- Handle result
  576.   if not dialogResult then return end
  577.  
  578.   if not exportSettings.outFile or exportSettings.outFile == "" then
  579.     showError("Please specify an output SHP file")
  580.     return
  581.   end
  582.  
  583.   if not converterExists then
  584.     showError("SHP converter not found at: " .. converterPath)
  585.     return
  586.   end
  587.  
  588.   -- Create temp directory for files
  589.   local tempDir = app.fs.joinPath(app.fs.tempPath, "exult-shp-" .. os.time())
  590.   app.fs.makeDirectory(tempDir)
  591.  
  592.   -- Prepare file paths
  593.   local metaPath = app.fs.joinPath(tempDir, "metadata.txt")
  594.   local basePath = app.fs.joinPath(tempDir, "frame")
  595.  
  596.   -- Create metadata file
  597.   local meta = io.open(metaPath, "w")
  598.   meta:write(string.format("num_frames=%d\n", #sprite.layers))
  599.  
  600.   -- Track frame offsets - the index in this array is the export frame number
  601.   local layerOffsets = {}
  602.  
  603.   -- First pass: Check which layers need offset prompts
  604.   for i, layer in ipairs(sprite.layers) do
  605.     local offsetNeeded = true
  606.    
  607.     -- Check for existing offset data
  608.     local offsetData = getLayerOffsetData(layer)
  609.     if offsetData and not exportSettings.editExisting then
  610.       layerOffsets[i] = {
  611.         x = offsetData.offsetX,
  612.         y = offsetData.offsetY,
  613.         fromData = true
  614.       }
  615.       offsetNeeded = false
  616.       debug("Using existing offset for layer " .. i .. ": " .. offsetData.offsetX .. "," .. offsetData.offsetY)
  617.     end
  618.    
  619.     -- Mark for prompting if still needed
  620.     if offsetNeeded then
  621.       layerOffsets[i] = {
  622.         needsPrompt = true
  623.       }
  624.     end
  625.   end
  626.  
  627.   -- Second pass: Prompt for missing offsets
  628.   for i, layerData in ipairs(layerOffsets) do
  629.     if layerData.needsPrompt then
  630.       -- Make this layer visible and others invisible for visual reference
  631.       for j, otherLayer in ipairs(sprite.layers) do
  632.         otherLayer.isVisible = (j == i)
  633.       end
  634.      
  635.       -- Create prompt dialog for this specific layer
  636.       local layerDlg = Dialog("Layer Offset")
  637.      
  638.       -- Get cleaner name (without offset info)
  639.       local cleanName = sprite.layers[i].name:gsub(" %[.*%]$", "")
  640.      
  641.       layerDlg:label{
  642.         id="info",
  643.         text="Set offset for " .. cleanName .. " (" .. i .. " of " .. #sprite.layers .. ")"
  644.       }
  645.      
  646.       -- If we have existing data, use it as default
  647.       local existingData = getLayerOffsetData(sprite.layers[i])
  648.       local initialX = defaultOffsetX
  649.       local initialY = defaultOffsetY
  650.      
  651.       if existingData then
  652.         initialX = existingData.offsetX
  653.         initialY = existingData.offsetY
  654.       end
  655.      
  656.       layerDlg:number{
  657.         id="offsetX",
  658.         label="Offset X:",
  659.         text=tostring(initialX),
  660.         decimals=0
  661.       }
  662.      
  663.       layerDlg:number{
  664.         id="offsetY",
  665.         label="Offset Y:",
  666.         text=tostring(initialY),
  667.         decimals=0
  668.       }
  669.      
  670.       local layerResult = false
  671.      
  672.       layerDlg:button{
  673.         id="ok",
  674.         text="OK",
  675.         onclick=function()
  676.           layerResult = true
  677.           layerOffsets[i] = {
  678.             x = layerDlg.data.offsetX,
  679.             y = layerDlg.data.offsetY
  680.           }
  681.           layerDlg:close()
  682.         end
  683.       }
  684.      
  685.       -- Show dialog and wait for result
  686.       layerDlg:show{wait=true}
  687.      
  688.       -- If user cancelled, use defaults or existing data
  689.       if not layerResult then
  690.         if existingData then
  691.           layerOffsets[i] = {
  692.             x = existingData.offsetX,
  693.             y = existingData.offsetY
  694.           }
  695.         else
  696.           layerOffsets[i] = {
  697.             x = defaultOffsetX,
  698.             y = defaultOffsetY
  699.           }
  700.         end
  701.       end
  702.      
  703.       -- Store the value in the layer for future use
  704.       setLayerOffsetData(sprite.layers[i], layerOffsets[i].x, layerOffsets[i].y)
  705.     end
  706.   end
  707.  
  708.   -- Restore all layers to visible
  709.   for _, layer in ipairs(sprite.layers) do
  710.     layer.isVisible = true
  711.   end
  712.  
  713.   -- Now export each layer as a frame - with content-based cropping
  714.   for i, layer in ipairs(sprite.layers) do
  715.     local frameIndex = i - 1 -- Convert to 0-based for the SHP file
  716.     local filepath = string.format("%s%d.png", basePath, frameIndex)
  717.    
  718.     debug("Exporting layer " .. i .. " (" .. layer.name .. ") as frame " .. frameIndex)
  719.    
  720.     -- Create a temporary copy of the sprite to crop
  721.     local tempSprite = Sprite(sprite)
  722.    
  723.     -- Make only the target layer visible in the temp sprite
  724.     for j, otherLayer in ipairs(tempSprite.layers) do
  725.       otherLayer.isVisible = (j == i)
  726.     end
  727.    
  728.     -- Get pre-crop dimensions for offset adjustment
  729.     local originalWidth = tempSprite.width
  730.     local originalHeight = tempSprite.height
  731.     local originalCenterX = originalWidth / 2
  732.     local originalCenterY = originalHeight / 2
  733.    
  734.     -- Crop to visible content
  735.     app.command.CropSprite {
  736.       ui=false,
  737.       bounds="content",
  738.       trim=true
  739.     }
  740.    
  741.     -- Get post-crop dimensions for offset adjustment
  742.     local croppedWidth = tempSprite.width
  743.     local croppedHeight = tempSprite.height
  744.     local croppedCenterX = croppedWidth / 2
  745.     local croppedCenterY = croppedHeight / 2
  746.    
  747.     debug("Cropped from " .. originalWidth .. "x" .. originalHeight .. " to " .. croppedWidth .. "x" .. croppedHeight)
  748.    
  749.     -- Temporarily disable the file format warning
  750.     local originalAlertPref = false
  751.     if app.preferences and app.preferences.save_file then
  752.       originalAlertPref = app.preferences.save_file.show_file_format_doesnt_support_alert or false
  753.       app.preferences.save_file.show_file_format_doesnt_support_alert = false
  754.     end
  755.    
  756.     -- Export the cropped sprite
  757.     tempSprite:saveCopyAs(filepath)
  758.    
  759.     -- Restore original preference
  760.     if app.preferences and app.preferences.save_file then
  761.       app.preferences.save_file.show_file_format_doesnt_support_alert = originalAlertPref
  762.     end
  763.    
  764.     -- Close temp sprite after saving
  765.     tempSprite:close()
  766.    
  767.     -- Verify the file was created
  768.     if not app.fs.isFile(filepath) then
  769.       debug("WARNING: Failed to export frame " .. frameIndex .. " to " .. filepath)
  770.       showError("Failed to export frame " .. frameIndex)
  771.       meta:close()
  772.       return
  773.     else
  774.       debug("Successfully exported cropped frame " .. frameIndex .. " to " .. filepath)
  775.     end
  776.    
  777.     -- Get the offset for this layer
  778.     local offsetX = 0
  779.     local offsetY = 0
  780.    
  781.     -- Get offsets from layerOffsets table
  782.     if layerOffsets[i] then
  783.       offsetX = layerOffsets[i].x or 0
  784.       offsetY = layerOffsets[i].y or 0
  785.     end
  786.    
  787.     -- Adjust offsets based on cropping
  788.     -- Original offsets are relative to sprite center, adjust for the new center
  789.     offsetX = offsetX - (originalCenterX - croppedCenterX)
  790.     offsetY = offsetY - (originalCenterY - croppedCenterY)
  791.    
  792.     debug("Adjusted offsets for layer " .. i .. " after cropping: " .. offsetX .. "," .. offsetY)
  793.    
  794.     meta:write(string.format("frame%d_offset_x=%d\n", frameIndex, offsetX))
  795.     meta:write(string.format("frame%d_offset_y=%d\n", frameIndex, offsetY))
  796.   end
  797.  
  798.   -- Close metadata file
  799.   meta:close()
  800.  
  801.   -- Create and execute the export command
  802.   local cmd = quoteIfNeeded(converterPath) ..
  803.              " export " ..
  804.              quoteIfNeeded(basePath) ..
  805.              " " .. quoteIfNeeded(exportSettings.outFile) ..
  806.              " 0" ..
  807.              " " .. layerOffsets[1].x ..
  808.              " " .. layerOffsets[1].y ..
  809.              " " .. quoteIfNeeded(metaPath)
  810.  
  811.   debug("Executing: " .. cmd)
  812.  
  813.   -- Execute command
  814.   local success = executeHidden(cmd)
  815.   if success then
  816.     app.alert("SHP file exported successfully.")
  817.   else
  818.     showError("Failed to export SHP file.")
  819.   end
  820. end
  821.  
  822. function init(plugin)
  823.   debug("Initializing plugin...")
  824.  
  825.   -- Register file format first
  826.   local formatRegistered = registerSHPFormat()
  827.   debug("SHP format registered: " .. tostring(formatRegistered))
  828.  
  829.   -- Register commands
  830.   plugin:newCommand{
  831.     id="ImportSHP",
  832.     title="Import SHP...",
  833.     group="file_import",
  834.     onclick=function() importSHP() end
  835.   }
  836.  
  837.   plugin:newCommand{
  838.     id="ExportSHP",
  839.     title="Export SHP...",
  840.     group="file_export",
  841.     onclick=exportSHP
  842.   }
  843.  
  844.   -- Register SHP file format for opening
  845.   if formatRegistered then
  846.     plugin:newCommand{
  847.       id="OpenSHP",
  848.       title="Ultima VII SHP",
  849.       group="file_format",
  850.       onclick=importSHP,
  851.       file_format="shp"
  852.     }
  853.   end
  854. end
  855.  
  856. -- Create a global table to store frame offset data across the entire plugin session
  857. if not _G.exultFrameData then
  858.   _G.exultFrameData = {}
  859. end
  860.  
  861.  
  862. return { init=init }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement