Add the new anim_stitcher tool
154
.gitignore
vendored
@ -14,4 +14,156 @@ Cargo.lock
|
||||
*.pdb
|
||||
|
||||
# MdBook generated files
|
||||
/book
|
||||
/book
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
8
.vscode/settings.json
vendored
@ -10,5 +10,13 @@
|
||||
"**/node_modules/*/**": true,
|
||||
"**/.hg/store/**": true,
|
||||
"**/target/**": true,
|
||||
},
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.svn": true,
|
||||
"**/.hg": true,
|
||||
"**/CVS": true,
|
||||
"**/.DS_Store": true,
|
||||
"**/__pycache__": true,
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
27
automation/anim_stitcher/__main__.py
Normal file
@ -0,0 +1,27 @@
|
||||
import argparse
|
||||
import sys
|
||||
import logging
|
||||
from qt_common.qt_app_wrapper import QtAppWrapper
|
||||
from . import ui
|
||||
|
||||
def main() -> int:
|
||||
# Handle program arguments
|
||||
ap = argparse.ArgumentParser(
|
||||
prog='anim_stitcher', description='A tool for stitching PNG sequences into sprite sheets')
|
||||
|
||||
args = ap.parse_args()
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
# Run the application
|
||||
with QtAppWrapper():
|
||||
# Create and show the window
|
||||
w = ui.AnimStitcherWindow()
|
||||
w.show()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
85
automation/anim_stitcher/stitcher.py
Normal file
@ -0,0 +1,85 @@
|
||||
"""This file contains the actual stitcher logic."""
|
||||
|
||||
import os
|
||||
from PIL import Image
|
||||
import json
|
||||
import logging
|
||||
from typing import List
|
||||
from project_root import get_project_root
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_sprite_exists(sprite_type: str, sprite_name: str) -> bool:
|
||||
"""Checks if a sprite directory exists for the given sprite type and name.
|
||||
|
||||
Args:
|
||||
sprite_type (str): Sprite type (short name)
|
||||
sprite_name (str): Sprite name
|
||||
|
||||
Returns:
|
||||
bool: Does it exist?
|
||||
"""
|
||||
|
||||
# Get the project root
|
||||
project_root = get_project_root()
|
||||
logger.debug(f"Project root: {project_root}")
|
||||
|
||||
# Build the path the sprite should exist in
|
||||
sprite_path = os.path.join(
|
||||
project_root, "game", "dist", "anm", sprite_type, f"{sprite_type}_{sprite_name}")
|
||||
|
||||
return os.path.isdir(sprite_path)
|
||||
|
||||
|
||||
def stitch_images_and_write_to_disk(sprite_type: str, sprite_name: str, images: List[str]) -> None:
|
||||
|
||||
# Load all the images
|
||||
images_to_stitch = []
|
||||
for image_path in images:
|
||||
images_to_stitch.append(Image.open(image_path))
|
||||
|
||||
# Collect the total width and maximum height of the images while building a list of the sizes
|
||||
total_width = 0
|
||||
max_height = 0
|
||||
for image in images_to_stitch:
|
||||
total_width += image.size[0]
|
||||
max_height = max(max_height, image.size[1])
|
||||
|
||||
# Create a new image with the total width and maximum height
|
||||
new_image = Image.new("RGBA", (total_width, max_height))
|
||||
|
||||
# Paste each image into the new image
|
||||
x_offset = 0
|
||||
for image in images_to_stitch:
|
||||
new_image.paste(image, (x_offset, 0))
|
||||
x_offset += image.size[0]
|
||||
|
||||
# Save the new image
|
||||
project_root = get_project_root()
|
||||
logger.debug(f"Project root: {project_root}")
|
||||
new_image = new_image.quantize(method=2)
|
||||
new_image.save(os.path.join(project_root, "game", "dist", "anm", sprite_type,
|
||||
f"{sprite_type}_{sprite_name}", f"{sprite_type}_{sprite_name}.png"))
|
||||
|
||||
# Build some JSON metadata
|
||||
metadata = {
|
||||
"sheet_height": max_height,
|
||||
"sheet_width": total_width,
|
||||
"frames": []
|
||||
}
|
||||
|
||||
# Add the metadata for each image
|
||||
x_offset = 0
|
||||
for image in images_to_stitch:
|
||||
metadata["frames"].append({
|
||||
"x": x_offset,
|
||||
"y": 0,
|
||||
"width": image.size[0],
|
||||
"height": image.size[1]
|
||||
})
|
||||
x_offset += image.size[0]
|
||||
|
||||
# Write the metadata to disk
|
||||
with open(os.path.join(project_root, "game", "dist", "anm", sprite_type,
|
||||
f"{sprite_type}_{sprite_name}", f"{sprite_type}_{sprite_name}.anim_meta.json"), "w") as f:
|
||||
json.dump(metadata, f, indent=4)
|
163
automation/anim_stitcher/ui.py
Normal file
@ -0,0 +1,163 @@
|
||||
"""anim_stitcher GUI"""
|
||||
|
||||
from PySide2 import QtWidgets
|
||||
from PySide2.QtCore import Qt
|
||||
import pkgutil
|
||||
from qt_common import qt_window_center, qt_lines, qt_dialog_style
|
||||
import sprite_types
|
||||
import re
|
||||
import os
|
||||
from . import stitcher
|
||||
from project_root import get_project_root
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger("anim_stitcher.ui")
|
||||
|
||||
SPRITE_NAMING_VALIDATION = r"^[a-z][a-zA-Z\d]+$"
|
||||
|
||||
|
||||
class AnimStitcherWindow(QtWidgets.QWidget):
|
||||
|
||||
selected_files = None
|
||||
|
||||
def __init__(self):
|
||||
super(AnimStitcherWindow, self).__init__()
|
||||
|
||||
# Configure the window
|
||||
self.setWindowFlags(
|
||||
self.windowFlags() ^ Qt.Window)
|
||||
self.setWindowTitle("Anim Stitcher")
|
||||
self.resize(280, 200)
|
||||
qt_window_center.center_window(self)
|
||||
|
||||
# Set the root of the application to be a vertical list
|
||||
self.setLayout(QtWidgets.QVBoxLayout())
|
||||
|
||||
# Load the stylesheet for this app
|
||||
self.setStyleSheet(qt_dialog_style.STYLESHEET)
|
||||
|
||||
# Configure the title at the top of the window
|
||||
self.label = QtWidgets.QLabel("Anim Stitcher")
|
||||
self.label.setProperty('labelClass', 'label-title')
|
||||
self.layout().addWidget(self.label)
|
||||
self.description = QtWidgets.QLabel(
|
||||
"Stitch PNG sequences into a sprite sheet")
|
||||
self.description.setProperty('labelClass', 'label-description')
|
||||
self.layout().addWidget(self.description)
|
||||
self.layout().addWidget(qt_lines.QHLine())
|
||||
|
||||
# Add an import button
|
||||
self.import_button = QtWidgets.QPushButton("Select PNGs")
|
||||
self.import_button.clicked.connect(self.load_png_dialog)
|
||||
self.layout().addWidget(self.import_button)
|
||||
self.layout().addWidget(qt_lines.QHLine())
|
||||
|
||||
# Add a selection option for the sprite type
|
||||
known_sprite_types = sprite_types.get_known_sprite_types().values()
|
||||
self.sprite_type_layout = QtWidgets.QHBoxLayout()
|
||||
self.sprite_type_label = QtWidgets.QLabel("Sprite Type")
|
||||
self.sprite_type_layout.addWidget(self.sprite_type_label)
|
||||
self.sprite_type_dropdown = QtWidgets.QComboBox()
|
||||
for ty in known_sprite_types:
|
||||
self.sprite_type_dropdown.addItem(ty)
|
||||
self.sprite_type_dropdown.setEnabled(False)
|
||||
self.sprite_type_layout.addWidget(self.sprite_type_dropdown)
|
||||
self.layout().addLayout(self.sprite_type_layout)
|
||||
|
||||
# Add a box to accept a sprite name
|
||||
self.sprite_name_layout = QtWidgets.QHBoxLayout()
|
||||
self.sprite_name_label = QtWidgets.QLabel("Sprite Name")
|
||||
self.sprite_name_layout.addWidget(self.sprite_name_label)
|
||||
self.sprite_name_input = QtWidgets.QLineEdit()
|
||||
self.sprite_name_input.setText("unnamedSprite")
|
||||
self.sprite_name_input.setEnabled(False)
|
||||
self.sprite_name_layout.addWidget(self.sprite_name_input)
|
||||
self.layout().addLayout(self.sprite_name_layout)
|
||||
|
||||
# Add a seperator
|
||||
self.layout().addWidget(qt_lines.QHLine())
|
||||
|
||||
# Add a button to start the stitching process
|
||||
self.finishing_layout = QtWidgets.QHBoxLayout()
|
||||
self.close_button = QtWidgets.QPushButton("Cancel")
|
||||
self.close_button.clicked.connect(self.close)
|
||||
self.finishing_layout.addWidget(self.close_button)
|
||||
self.stitch_button = QtWidgets.QPushButton("Stitch")
|
||||
self.stitch_button.clicked.connect(self.stitch_images)
|
||||
self.stitch_button.setEnabled(False)
|
||||
self.finishing_layout.addWidget(self.stitch_button)
|
||||
self.layout().addLayout(self.finishing_layout)
|
||||
|
||||
# Add space at the bottom in case window size is wrong
|
||||
self.layout().addStretch()
|
||||
|
||||
def load_png_dialog(self):
|
||||
|
||||
# Open a file picker to search for the desired image
|
||||
file_dialog = QtWidgets.QFileDialog()
|
||||
file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
|
||||
file_dialog.setNameFilter("Image Files (*.png *.jpg *.jpeg)")
|
||||
file_dialog.setViewMode(QtWidgets.QFileDialog.Detail)
|
||||
file_dialog.setLabelText(QtWidgets.QFileDialog.Accept, "Import")
|
||||
file_dialog.setLabelText(QtWidgets.QFileDialog.Reject, "Cancel")
|
||||
file_dialog.setWindowTitle("Import PNG Sequence")
|
||||
file_dialog.setAcceptMode(QtWidgets.QFileDialog.AcceptOpen)
|
||||
file_dialog.setDirectory(os.path.join(get_project_root(), "assets"))
|
||||
|
||||
# If the user selected an image, import it
|
||||
if file_dialog.exec_():
|
||||
# Enable all the disabled fields
|
||||
self.sprite_type_dropdown.setEnabled(True)
|
||||
self.sprite_name_input.setEnabled(True)
|
||||
self.stitch_button.setEnabled(True)
|
||||
|
||||
# Save the selected files
|
||||
self.selected_files = file_dialog.selectedFiles()
|
||||
|
||||
else:
|
||||
logger.warning("No image selected")
|
||||
return
|
||||
|
||||
def stitch_images(self):
|
||||
|
||||
# Check the naming convention
|
||||
if not re.match(SPRITE_NAMING_VALIDATION, self.sprite_name_input.text()):
|
||||
|
||||
# Pop up a warning
|
||||
warning_dialog = QtWidgets.QMessageBox()
|
||||
warning_dialog.setIcon(QtWidgets.QMessageBox.Warning)
|
||||
warning_dialog.setText("Invalid Sprite Name")
|
||||
warning_dialog.setInformativeText(
|
||||
"The sprite name must be lower camel case\nExample: myShinySprite")
|
||||
warning_dialog.setWindowTitle("Invalid Sprite Name")
|
||||
warning_dialog.exec_()
|
||||
|
||||
return
|
||||
|
||||
# Check if we are about to overwrite an existing sprite
|
||||
known_sprite_types = sprite_types.get_known_sprite_types()
|
||||
ty_long_to_short_map = dict(map(reversed, known_sprite_types.items()))
|
||||
sprite_type = ty_long_to_short_map[self.sprite_type_dropdown.currentText(
|
||||
)]
|
||||
sprite_name = self.sprite_name_input.text()
|
||||
if stitcher.check_sprite_exists(sprite_type, sprite_name):
|
||||
|
||||
# Pop up confirmation box
|
||||
warning_dialog = QtWidgets.QMessageBox()
|
||||
warning_dialog.setIcon(QtWidgets.QMessageBox.Warning)
|
||||
warning_dialog.setText("Overwrite Sprite?")
|
||||
warning_dialog.setInformativeText(
|
||||
"A sprite with the name {}_{} already exists.\nDo you want to overwrite it?".format(sprite_type, sprite_name))
|
||||
warning_dialog.setWindowTitle("Overwrite Sprite?")
|
||||
warning_dialog.setStandardButtons(
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
warning_dialog.setDefaultButton(QtWidgets.QMessageBox.No)
|
||||
if warning_dialog.exec_() == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
|
||||
# Perform the actual stitching action
|
||||
stitcher.stitch_images_and_write_to_disk(
|
||||
sprite_type, sprite_name, self.selected_files)
|
||||
|
||||
# Close the window
|
||||
self.close()
|
11
automation/project_root.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""Module to fetch the project root directory."""
|
||||
|
||||
import os
|
||||
|
||||
def get_project_root() -> str:
|
||||
"""Gets the project root directory.
|
||||
|
||||
Returns:
|
||||
str: The project root directory.
|
||||
"""
|
||||
return os.environ.get("LD50_PROJECT_ROOT")
|
2
automation/qt_common/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
"""This module is used to provide common functionality for Qt applications."""
|
||||
|
25
automation/qt_common/dialog.css
Normal file
@ -0,0 +1,25 @@
|
||||
QWidget {
|
||||
background-color: #444444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
QLabel[labelClass="label-title"] {
|
||||
color: #58a5cc;
|
||||
font-weight: bold;
|
||||
font-size:25px;
|
||||
}
|
||||
|
||||
QComboBox:disabled {
|
||||
background-color:#272727;
|
||||
color: #444444;
|
||||
}
|
||||
|
||||
QCheckBox:disabled {
|
||||
background-color:#272727;
|
||||
color: #444444;
|
||||
}
|
||||
|
||||
QPushButton:disabled {
|
||||
background-color:#272727;
|
||||
color: #444444;
|
||||
}
|
37
automation/qt_common/qt_app_wrapper.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""A wrapper that handles bootstrapping QT applications in environments that do not have QT support."""
|
||||
|
||||
# Load the logging system
|
||||
import logging
|
||||
logger = logging.getLogger("qt_common.utils")
|
||||
|
||||
# We need to have a global to keep track of the QApplication
|
||||
_qt_app = None
|
||||
|
||||
class QtAppWrapper:
|
||||
|
||||
def __init__(self, parent=None):
|
||||
self.parent = parent
|
||||
|
||||
def __enter__(self):
|
||||
global _qt_app
|
||||
|
||||
# If there is no parent, we must make a QApplication
|
||||
if self.parent is None:
|
||||
logger.info("No parent specified. Creating QApplication")
|
||||
from PySide2 import QtWidgets
|
||||
try:
|
||||
if not _qt_app:
|
||||
_qt_app = QtWidgets.QApplication([])
|
||||
except RuntimeError:
|
||||
logger.error(
|
||||
"Could not create QApplication. Is it already running?")
|
||||
raise
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
global _qt_app
|
||||
|
||||
# If there is no parent, we must run the QApplication ourselves
|
||||
if self.parent is None:
|
||||
logger.info("Running QApplication")
|
||||
if _qt_app:
|
||||
_qt_app.exec_()
|
5
automation/qt_common/qt_dialog_style.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""This module has an embedded CSS file defining the style of the dialogs."""
|
||||
|
||||
import pkgutil
|
||||
|
||||
STYLESHEET = pkgutil.get_data(__name__, "./dialog.css").decode("utf-8")
|
17
automation/qt_common/qt_lines.py
Normal file
@ -0,0 +1,17 @@
|
||||
"""Some utility classes for making lines in QT panels"""
|
||||
|
||||
from PySide2 import QtWidgets
|
||||
|
||||
|
||||
class QHLine(QtWidgets.QFrame):
|
||||
def __init__(self):
|
||||
super(QHLine, self).__init__()
|
||||
self.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
self.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
|
||||
|
||||
class QVLine(QtWidgets.QFrame):
|
||||
def __init__(self):
|
||||
super(QVLine, self).__init__()
|
||||
self.setFrameShape(QtWidgets.QFrame.VLine)
|
||||
self.setFrameShadow(QtWidgets.QFrame.Sunken)
|
8
automation/qt_common/qt_window_center.py
Normal file
@ -0,0 +1,8 @@
|
||||
from PySide2 import QtWidgets
|
||||
|
||||
|
||||
def center_window(w):
|
||||
qr = w.frameGeometry()
|
||||
cp = QtWidgets.QDesktopWidget().availableGeometry().center()
|
||||
qr.moveCenter(cp)
|
||||
w.move(qr.topLeft())
|
30
automation/sprite_types.py
Normal file
@ -0,0 +1,30 @@
|
||||
"""This file contains functions for reading known sprite types."""
|
||||
|
||||
from typing import Dict
|
||||
import json
|
||||
import os
|
||||
from project_root import get_project_root
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_known_sprite_types() -> Dict[str, str]:
|
||||
"""Gets a dictionary of known sprite types as a mapping from short name to friendly name
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: Short name -> Friendly name
|
||||
"""
|
||||
|
||||
# Load our JSON file containing known sprite types
|
||||
project_root = get_project_root()
|
||||
logger.debug(f"Project root: {project_root}")
|
||||
with open(os.path.join(project_root, "game", "dist", "known-sprite-types.json"), "r") as f:
|
||||
known_sprite_types = json.load(f)
|
||||
|
||||
# We need to re-shape the data
|
||||
sprite_types = {}
|
||||
for item in known_sprite_types:
|
||||
sprite_types[item["short"]] = item["friendly"]
|
||||
|
||||
|
||||
return sprite_types
|
@ -1 +1,2 @@
|
||||
# Artist Information
|
||||
|
||||
|
@ -1,9 +0,0 @@
|
||||
# The Auto-Stitch directory
|
||||
|
||||
This is a bit of a *magic* directory. Anything put in here will automatically be turned into a spritesheet at compile time.
|
||||
|
||||
## File organization
|
||||
|
||||
In this directory, framesets are expected to be stored in subdirectories. The names of these are important as they will translate into the names of the spritesheets.
|
||||
|
||||
For example, if you put a set of frames in `auto_stitch/chr_testFox`, you will get a spritesheet generated to `dist/gen/anm/chr/chr_testFox.png`.
|
1
game/dist/.gitignore
vendored
@ -1 +0,0 @@
|
||||
/gen/*
|
6
launch_anim_stitcher.sh
Normal file
@ -0,0 +1,6 @@
|
||||
#! /bin/bash
|
||||
set -ex
|
||||
|
||||
export PYTHONPATH=$(pwd)/automation:$PYTHONPATH
|
||||
export LD50_PROJECT_ROOT=$(pwd)
|
||||
python3 -m anim_stitcher
|
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
pillow
|
||||
PySide2
|