Add the new anim_stitcher tool
154
.gitignore
vendored
@ -14,4 +14,156 @@ Cargo.lock
|
|||||||
*.pdb
|
*.pdb
|
||||||
|
|
||||||
# MdBook generated files
|
# 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,
|
"**/node_modules/*/**": true,
|
||||||
"**/.hg/store/**": true,
|
"**/.hg/store/**": true,
|
||||||
"**/target/**": 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
|
# 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
|