· 6 months ago · Mar 19, 2025, 01:15 PM
1-- Exult SHP Format Extension for Aseprite
2
3local pluginName = "exult-shp"
4local pluginDir = app.fs.joinPath(app.fs.userConfigPath, "extensions", pluginName)
5local converterPath = app.fs.joinPath(pluginDir, "exult_shp")
6
7-- Debug system with toggle
8local debugEnabled = true -- toggle debug messages
9
10local function debug(message)
11 if debugEnabled then
12 print("[Exult SHP] " .. message)
13 end
14end
15
16local function logError(message)
17 -- Always print errors regardless of debug setting
18 print("[Exult SHP ERROR] " .. message)
19end
20
21-- Global utility function for quoting paths with spaces
22function quoteIfNeeded(path)
23 if path:find(" ") then
24 return '"' .. path .. '"'
25 else
26 return path
27 end
28end
29
30-- Helper to run commands with hidden output
31function 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
77end
78
79debug("Plugin initializing...")
80debug("System Information:")
81debug("OS: " .. (app.fs.pathSeparator == "/" and "Unix-like" or "Windows"))
82debug("Temp path: " .. app.fs.tempPath)
83debug("User config path: " .. app.fs.userConfigPath)
84
85debug("Converter expected at: " .. converterPath)
86
87-- Check if converterPath exists with OS-specific extension
88if 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
97end
98
99-- Verify converter exists at startup
100local converterExists = app.fs.isFile(converterPath)
101debug("Converter exists: " .. tostring(converterExists))
102
103-- Make the converter executable once at startup if needed
104if converterExists and app.fs.pathSeparator == "/" then
105 local chmodCmd = "chmod +x " .. quoteIfNeeded(converterPath)
106 debug("Executing chmod at plugin initialization: " .. chmodCmd)
107 executeHidden(chmodCmd)
108end
109
110-- Error display helper
111function showError(message)
112 logError(message)
113 app.alert{
114 title="Exult SHP Error",
115 text=message
116 }
117end
118
119-- Don't detect Animation sequences when opening files
120function 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
127end
128
129function 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
134end
135
136-- File format registration function
137function 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
144end
145
146function 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)
224end
225
226-- Global table to store layer offset data
227if not _G.exultLayerOffsets then
228 _G.exultLayerOffsets = {}
229end
230
231-- Enhance the getLayerOffsetData function to parse from layer names when not in global table:
232function 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
266end
267
268-- Also update setLayerOffsetData to store with clean name:
269function 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 .. "]"
288end
289
290-- Modify the processImport function to use layers instead of frames
291function 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
471end
472
473-- Add this helper function to check for offset tags
474function 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
484end
485
486-- Replace the exportSHP function with this improved version:
487function 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
820end
821
822function 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
854end
855
856-- Create a global table to store frame offset data across the entire plugin session
857if not _G.exultFrameData then
858 _G.exultFrameData = {}
859end
860
861
862return { init=init }