Last active
December 15, 2020 01:29
-
-
Save NoodleSushi/3d5c1fceb049b64340f5b94bbfc67fa6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import io | |
import re | |
import os as OS | |
import json as Json | |
import pyperclip | |
import math | |
from rectpack import newPacker | |
from rectpack.packer import Packer | |
from PIL import Image | |
from types import FunctionType | |
from types import LambdaType | |
from typing import Union | |
from typing import List | |
MODULE_COMMENT = r""" | |
Source code generated from: | |
_ ___ _ _ _ | |
_ __ (_) __ _ _ ___ / __|| |_ _ _ __| |(_) ___ | |
| ' \ | |/ _|| '_|/ _ \\__ \| _|| || |/ _` || |/ _ \ | |
|_|_|_||_|\__||_| \___/|___/ \__| \_,_|\__,_||_|\___/ | |
__ __ _ __ _ _ ___ | |
| \/ | __ _ | |__ ___ / _|(_)| | ___ __ __|_ ) | |
| |\/| |/ _` || / // -_)| _|| || |/ -_) \ V / / / | |
|_| |_|\__,_||_\_\\___||_| |_||_|\___| \_/ /___| | |
by NoodleSushi | |
https://gist.github.com/NoodleSushi/3d5c1fceb049b64340f5b94bbfc67fa6 | |
""".replace("\n", "\n// ") | |
class DIR(): | |
OUTPUT: str = "_OUTPUT" | |
VSCODE: str = ".vscode" | |
class FILE(): | |
CONFIG: str = OS.path.join(DIR.OUTPUT, "config.json") | |
MAIN: str = "main.lua" | |
SOURCE: str = OS.path.join(DIR.OUTPUT, "source.ms") | |
class ConfigHandler: | |
DEFAULT_CONFIG: dict = { | |
"sprite_packer.enable": True, | |
"sprite_packer.dir_key": True, | |
"sprite_packer.dir_lookup": "", | |
"sprite_packer.rel_dir_key": False | |
} | |
config: dict = DEFAULT_CONFIG.copy() | |
@staticmethod | |
def load_config_info() -> dict: | |
loaded_json: dict = Json.loads(FileHandler.read_file(FILE.CONFIG)) | |
for key in loaded_json.keys(): | |
ConfigHandler.config[key] = loaded_json[key] | |
@staticmethod | |
def make_config_info() -> None: | |
default_config_json: str = Json.dumps(ConfigHandler.DEFAULT_CONFIG, indent=2) | |
FileHandler.write_file(FILE.CONFIG, default_config_json) | |
@staticmethod | |
def get_value(key: str) -> Union[int, bool, str]: | |
return ConfigHandler.config[key] | |
DEPENENCIES = """ | |
graphics = class extends screen | |
_translation = object | |
x = 0 | |
y = 0 | |
end | |
_anchor = object | |
x = 0 | |
y = 0 | |
end | |
_scale = object | |
x = 0 | |
y = 0 | |
end | |
_rotation = 0 | |
fillRect = function(x,y,w,h,c) | |
super(x,-y,w,h,c) | |
end | |
setLinearGradient = function(x1, y1, x2, x2, y2, color1, color2) | |
super(x1, -y1, x2, -y2, color1, color2) | |
end | |
setRadialGradient = function(x, y, radius, color1, color2) | |
super(x, -y, radius, color1, color2) | |
end | |
fillRect = function(x, y, width, height, color) | |
super(x, -y, width, height, color) | |
end | |
fillRoundRect = function(x, y, width, height, radius, color) | |
super(x, -y, width, height, radius, color) | |
end | |
drawSprite = function(sprite, x, y, width = -1, height = -1) | |
if sprite.startsWith("@") then | |
local lookup = sprite_lookup[sprite.substring(1, sprite.length)] | |
local _w = if width == -1 then lookup[3] else width end | |
local _h = if height == -1 then _w/lookup[3]*lookup[4] else height end | |
local old_rot = _rotation | |
local old_anchor_x = _anchor.x | |
local old_anchor_y = _anchor.y | |
local old_scale_x = _scale.x | |
local old_scale_y = _scale.y | |
if lookup[5] then | |
setDrawRotation(old_rot-90) | |
setDrawAnchor(-old_anchor_y, -old_anchor_x) | |
setDrawScale(old_scale_y, old_scale_x) | |
end | |
drawSpritePart( | |
lookup[0], | |
lookup[1], | |
lookup[2], | |
lookup[3], | |
lookup[4], | |
x, | |
y, | |
_w, | |
_h | |
) | |
setDrawRotation(old_rot) | |
setDrawAnchor(old_anchor_x, -old_anchor_y) | |
setDrawScale(old_scale_x, old_scale_y) | |
return | |
end | |
super(sprite, x, -y, width, height) | |
end | |
drawSpritePart = function(sprite, part_x, part_y, part_width, part_height, x, y, width, height) | |
super(sprite, part_x, part_y, part_width, part_height, x, -y, width, height) | |
end | |
drawMap = function(map, x, y, width, height) | |
super(map, x, -y, width, height) | |
end | |
drawText = function(text, x, y, size, color) | |
super(text, x, -y, size, color) | |
end | |
drawLine = function(x1, y1, x2, y2, color) | |
super(x1, -y1, x2, -y2, color) | |
end | |
drawPolygon = function(_points, color) | |
local points = _points.concat([]) | |
for i=1 to points.length-1 by 2 | |
points[i] = -points[i] | |
end | |
super(points, color) | |
end | |
fillPolygon = function(_points, color) | |
local points = _points.concat([]) | |
for i=1 to points.length-1 by 2 | |
points[i] = -points[i] | |
end | |
super(points, color) | |
end | |
setTranslation = function(x,y) | |
_translation.x = x-width/2 | |
_translation.y = -y+height/2 | |
super(_translation.x, _translation.y) | |
end | |
setDrawAnchor = function(x,y) | |
_anchor.x = x | |
_anchor.y = -y | |
super(_anchor.x,_anchor.y) | |
end | |
setDrawScale = function(x, y) | |
_scale.x = x | |
_scale.y = y | |
super(x, y) | |
end | |
setDrawRotation = function(ang) | |
_rotation = ang | |
super(ang) | |
end | |
end | |
graphics.setDrawAnchor(-1, -1) | |
graphics.setTranslation(0, 0) | |
graphics.setDrawScale(1, 1) | |
""" | |
def iterate_listdir(scanned_dir: str = None) -> List[str]: | |
out: List[str] = [] | |
for file_key in OS.listdir(scanned_dir or None): | |
file_ext: str = OS.path.splitext(file_key)[-1] | |
file_dir: str = OS.path.normpath(OS.path.join(scanned_dir or "", file_key)) | |
if file_ext == "": | |
out += iterate_listdir(file_dir) | |
else: | |
out.append(file_dir) | |
return out | |
def make_var_name_safe(s: str) -> str: | |
#https://stackoverflow.com/questions/3303312/how-do-i-convert-a-string-to-a-valid-variable-name-in-python | |
# Remove invalid characters | |
s = re.sub('[^0-9a-zA-Z_]', '', s) | |
# Remove leading characters until we find a letter or underscore | |
s = re.sub('^[^a-zA-Z_]+', '', s) | |
return s | |
def generate_commentbar(text: str) -> str: | |
return "//"+"="*78 + "\n// "+text + "\n//"+"-"*78 + "\n" | |
class FileHandler: | |
@staticmethod | |
def read_file(filename: str) -> str: | |
filehandle: io.TextIOWrapper = open(filename) | |
out: str = filehandle.read() | |
filehandle.close() | |
return out | |
@staticmethod | |
def write_file(filename: str, data: str) -> None: | |
filehandle: io.TextIOWrapper = open(filename, "w") | |
filehandle.write(data) | |
filehandle.close() | |
class FileConverters: | |
@staticmethod | |
def Microscript(data: str, file_name: str) -> str: | |
return f"{data}\n" | |
@staticmethod | |
def Json(data: Union[str, dict, list], file_name = "", is_first_call: bool = True, tabs: int = 0) -> str: | |
tabber: LambdaType = lambda x: "\t"*x | |
out: str = "" | |
_tabs: int = tabs | |
if isinstance(data, str) and is_first_call: | |
var_name: str = make_var_name_safe(file_name) | |
msobj_from_json: str = FileConverters.Json(Json.loads(data), "", False) | |
return f"{var_name} = {msobj_from_json}" | |
elif isinstance(data, dict): | |
out += "object\n" | |
_tabs += 1 | |
for key in data: | |
converted_value: str = FileConverters.Json(data[key], is_first_call = False, tabs = _tabs) | |
out += f"{tabber(_tabs)}\"{key}\" = {converted_value}" | |
_tabs -= 1 | |
out += f"{tabber(_tabs)}end\n" | |
elif isinstance(data, list): | |
out += "[\n" | |
_tabs += 1 | |
for value in data: | |
converted_value: str = re.sub(r'^\t+', '', FileConverters.Json(value, is_first_call = False, tabs = _tabs)[:-1]) | |
out += f"{tabber(_tabs)}{converted_value},\n" | |
_tabs -= 1 | |
out += f"{tabber(_tabs)}]\n" | |
elif isinstance(data, str): | |
if str(data)[0] == "@": | |
out += f"{data[1:]}\n" | |
else: | |
out += f"\"{data}\"\n" | |
elif isinstance(data, bool): | |
out += f"{str(data).lower()}\n" | |
else: | |
out += f"{data}\n" | |
return out | |
extensions = [".lua", ".json"] | |
switcher = { | |
".lua": Microscript.__func__, | |
".json": Json.__func__ | |
} | |
def compile_sprites(rel_dir: str) -> dict: | |
packer: Packer = newPacker() | |
png_dir_list: List[str] = [] | |
png_w_list: List[int] = [] | |
png_shrink_list: List[float] = [] | |
bin_img_list: List[Image] = [] | |
sprite_lookup: dict = {} | |
png_dir_list = list(filter( | |
lambda x: OS.path.splitext(x)[-1] == ".png" and x.split(OS.path.altsep)[0] != DIR.OUTPUT, | |
iterate_listdir(rel_dir) | |
)) | |
if len(png_dir_list) == 0: | |
return | |
for idx, file_dir in enumerate(png_dir_list): | |
img: Image = Image.open(file_dir) | |
shrink: int = max(1, max(*img.size)/256) | |
#assert(max(*img.size) <= 256) | |
img = img.resize((int(img.size[0]/shrink), int(img.size[1]/shrink))) | |
png_w_list.append(img.size[0]) | |
png_shrink_list.append(shrink) | |
packer.add_rect(*img.size, idx) | |
packer.add_bin(256, 256, count = float("inf")) | |
packer.pack() | |
for _i in range(len(packer)): | |
bin_img: Image = Image.new('RGBA', (256, 256), (0, 0, 0, 0)) | |
bin_img_list.append(bin_img) | |
for rect in packer.rect_list(): | |
b, x, y, w, h, rid = rect | |
is_rotated: bool = (w != png_w_list[rid]) | |
shrink: int = png_shrink_list[rid] | |
rect_img: Image = Image.open(png_dir_list[rid]) | |
if is_rotated: | |
rect_img = rect_img.transpose(Image.ROTATE_90) | |
rect_img = rect_img.resize((w, h)) | |
bin_img_list[b].paste(rect_img, (x, y)) | |
spr_name: str = OS.path.splitext(png_dir_list[rid])[-2] | |
if ConfigHandler.get_value("sprite_packer.rel_dir_key"): | |
spr_name = OS.path.relpath(spr_name, rel_dir) | |
if not ConfigHandler.get_value("sprite_packer.dir_key"): | |
spr_name = spr_name.split(OS.path.altsep)[-1] | |
sprite_lookup[spr_name] = [f"spritesheet_{b}", x, y, w, h, is_rotated] | |
for bin_img_idx, bin_img in enumerate(bin_img_list): | |
bin_img.save(OS.path.join(DIR.OUTPUT, f"spritesheet_{bin_img_idx}.png")) | |
return sprite_lookup | |
def main() -> None: | |
current_directory: str | |
source_output_data: str | |
OS.path.altsep = "\\" | |
OS.makedirs(DIR.OUTPUT, exist_ok=True) | |
current_directory = OS.path.dirname(OS.path.abspath(__file__)) | |
OS.chdir(current_directory) | |
try: | |
ConfigHandler.load_config_info() | |
except FileNotFoundError: | |
ConfigHandler.make_config_info() | |
ConfigHandler.load_config_info() | |
source_output_data = "" | |
source_output_data += MODULE_COMMENT + "\n\n" | |
for file_dir in iterate_listdir(): | |
file_ext: str = OS.path.splitext(file_dir)[-1] | |
is_ext_supported: bool = (file_ext in FileConverters.extensions) | |
excluded_directory_list: List[str] = [value for value in DIR.__dict__.values()] | |
is_directory_included: bool = (not file_dir.split(OS.path.altsep)[0] in excluded_directory_list) | |
if is_ext_supported and is_directory_included: | |
converter_func: FunctionType = FileConverters.switcher.get(file_ext) | |
file_name: str = OS.path.splitext(file_dir)[-2] | |
file_data: str = FileHandler.read_file(file_dir) | |
file_converted_data: str = converter_func(file_data, file_name) | |
source_output_data += generate_commentbar(file_dir) | |
source_output_data += f"{file_converted_data}\n" | |
if ConfigHandler.get_value("sprite_packer.enable"): | |
dir_lookup: str = ConfigHandler.get_value("sprite_packer.dir_lookup") | |
if not OS.path.isdir(dir_lookup): | |
dir_lookup = "" | |
sprite_lookup_data: str = FileConverters.Json(compile_sprites(dir_lookup)) | |
if sprite_lookup_data != "None\n": | |
source_output_data += generate_commentbar("AUTO-GENERATED SPRITE LOOKUP DATA") | |
source_output_data += f"sprite_lookup = {sprite_lookup_data}" | |
source_output_data += generate_commentbar("DEPENDENCIES") | |
source_output_data += DEPENENCIES | |
FileHandler.write_file(FILE.SOURCE, source_output_data) | |
pyperclip.copy(source_output_data) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment