Krita/krita/pykrita/KritaToSpine/SpineExport.py

214 lines
8.1 KiB
Python
Raw Normal View History

2025-03-07 08:03:18 +01:00
import os, re, json, pathlib
import krita
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QMessageBox)
class SpineExport(object):
def __init__(self, parent=None):
self.msgBox = None
self.fileFormat = 'png'
self.bonePattern = re.compile("\(bone\)|\[bone\]", re.IGNORECASE)
self.mergePattern = re.compile("\(merge\)|\[merge\]", re.IGNORECASE)
self.slotPattern = re.compile("\(slot\)|\[slot\]", re.IGNORECASE)
self.skinPattern = re.compile("\(skin(:.+)?\)|\[skin(:.+)?\]", re.IGNORECASE)
self.folderPattern = re.compile("\(folder(:.+)?\)|\[folder(:.+)?\]", re.IGNORECASE)
def exportDocument(self, document, directory, boneLength, includeHidden):
if document is not None:
self.json = {
"skeleton": {"images": directory},
"bones": [{"name": "root"}],
"slots": [],
"skins": {"default": {}},
"animations": {}
}
self.spineBones = self.json['bones']
self.spineSlots = self.json['slots']
self.spineSkins = self.json['skins']
self.boneLength = boneLength
self.includeHidden = includeHidden
horGuides = document.horizontalGuides()
verGuides = document.verticalGuides()
xOrigin = 0
yOrigin = 0
if len(horGuides) == 1 and len(verGuides) == 1:
xOrigin = verGuides[0]
yOrigin = -horGuides[0] + 1
else:
pass
# self._alert(f"Not exactly 1 each of {horGuides} horizontal and {verGuides} vertical guides; not using origin")
krita.Krita.instance().setBatchmode(True)
self.document = document
self._export(document.rootNode(), directory, "root", xOrigin, yOrigin)
krita.Krita.instance().setBatchmode(False)
with open(f'{directory}/spine.json', 'w') as outfile:
json.dump(self.json, outfile, indent=2)
else:
self._alert("Please select a Document")
@staticmethod
def quote(value):
return '"' + value + '"'
@staticmethod
# Returns None if the name doesn't contain the tag. Empty string if the tag is present but has no value, otherwise returns the tag's value
def getTagValue(name, tag):
regex = "\[{0}:?([^\]]+)?\]".format(tag)
matches = re.findall(regex, name, flags=re.IGNORECASE)
if len(matches) > 0:
return matches[0].strip()
else:
return None
def _alert(self, message):
print(f"Showing alert: {message}")
self.msgBox = self.msgBox if self.msgBox else QMessageBox()
self.msgBox.setText(message)
self.msgBox.exec()
def _getSlot(self, name):
return next((x for x in self.spineSlots if x['name'] == name), None)
def _export(self, node, directory, bone="root", xOffset=0, yOffset=0, slot=None, currentSkinName="default"):
for child in node.childNodes():
if "selectionmask" in child.type():
continue
if not self.includeHidden and not child.visible():
continue
if '[ignore]' in child.name():
continue
# Special "fake" Krita layer - maybe used for showing guides?
if child.name() == "decorations-wrapper-layer":
continue;
# Save a copy so other children don't affect us
skinName = currentSkinName
childDir = directory
if "grouplayer" in child.type():
child.setPassThroughMode(False)
if child.childNodes() and not self.mergePattern.search(child.name()):
newBone = bone
newSlot = slot
newX = xOffset
newY = yOffset
# Found a bone
if self.bonePattern.search(child.name()):
newBone = self.bonePattern.sub('', child.name()).strip()
rect = child.bounds()
newX = rect.left() + rect.width() / 2 - xOffset
newY = (- rect.bottom() + rect.height() / 2) - yOffset
self.spineBones.append({
'name': newBone,
'parent': bone,
'length': self.boneLength,
'x': newX,
'y': newY
})
newX = xOffset + newX
newY = yOffset + newY
# Found a slot
if self.slotPattern.search(child.name()):
newSlotName = self.slotPattern.sub('', child.name()).strip()
newSlot = self._getSlot(newSlotName)
if (newSlot == None):
newSlot = {
'name': newSlotName,
'bone': bone,
'attachment': None,
'blend': 'normal',
}
self.spineSlots.append(newSlot)
# Found a skin
newSkinName = self.getTagValue(child.name(), "skin")
print(newSkinName, skinName)
if newSkinName is not None:
skinName = newSkinName
childDir = childDir + "/" + skinName
pathlib.Path(childDir).mkdir(parents=True, exist_ok=True)
self._export(child, directory, newBone, newX, newY, newSlot, skinName)
continue
newSkinName = self.getTagValue(child.name(), "skin")
if newSkinName is not None:
skinName = newSkinName
childDir = directory + "/" + skinName
pathlib.Path(childDir).mkdir(parents=True, exist_ok=True)
name = self.mergePattern.sub('', child.name()).strip()
name = self.skinPattern.sub('', name).strip()
fileName = name if skinName == "default" else "{0}/{1}".format(skinName, name)
outPath = '{0}/{1}.{2}'.format(directory, fileName, self.fileFormat)
print("outpath: " + outPath)
## Note there is an optional bounds setting here
child.save(outPath, 96, 96, krita.InfoObject())
newSlot = slot
if not newSlot:
newSlot = self._getSlot(name)
if newSlot == None:
newSlot = {
'name': name,
'bone': bone,
'attachment': name,
'blend': 'normal',
}
self.spineSlots.append(newSlot)
else:
if not newSlot['attachment']:
newSlot['attachment'] = name
# Found a blend mode
if child.blendingMode() == 'multiply':
newSlot['blend'] = 'multiply'
if child.blendingMode() == 'add':
newSlot['blend'] = 'additive'
if child.blendingMode() == 'screen':
newSlot['blend'] = 'screen'
rect = child.bounds()
slotName = newSlot['name']
if skinName not in self.spineSkins:
self.spineSkins[skinName] = {}
skinDict = self.spineSkins[skinName]
if slotName not in skinDict:
skinDict[slotName] = {}
slotDict = skinDict[slotName]
slotDict[name] = {
'x': round(rect.left() + rect.width() / 2 - xOffset, 2),
'y': round((- rect.bottom() + rect.height() / 2) - yOffset, 2),
'width': rect.width(),
'height': rect.height(),
}
if name != fileName:
slotDict[name]["name"] = fileName