Files
PyTaiko/libs/utils.py
2025-05-13 00:42:01 -04:00

414 lines
16 KiB
Python

import os
import tempfile
import time
import tomllib
import zipfile
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
import pyray as ray
#TJA Format creator is unknown. I did not create the format, but I did write the parser though.
def get_zip_filenames(zip_path: Path) -> list[str]:
result = []
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
file_list = zip_ref.namelist()
for file_name in file_list:
result.append(file_name)
return result
def load_image_from_zip(zip_path: Path, filename: str) -> ray.Image:
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
with zip_ref.open(filename) as image_file:
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file:
temp_file.write(image_file.read())
temp_file_path = temp_file.name
image = ray.load_image(temp_file_path)
os.remove(temp_file_path)
return image
def load_texture_from_zip(zip_path: Path, filename: str) -> ray.Texture:
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
with zip_ref.open(filename) as image_file:
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file:
temp_file.write(image_file.read())
temp_file_path = temp_file.name
texture = ray.load_texture(temp_file_path)
os.remove(temp_file_path)
return texture
def load_all_textures_from_zip(zip_path: Path) -> dict[str, list[ray.Texture]]:
result_dict = dict()
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
files = zip_ref.namelist()
for file in files:
with zip_ref.open(file) as image_file:
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file:
temp_file.write(image_file.read())
temp_file_path = temp_file.name
texture = ray.load_texture(temp_file_path)
os.remove(temp_file_path)
true_filename, index = file.split('_img')
index = int(index.split('.')[0])
if true_filename not in result_dict:
result_dict[true_filename] = []
while len(result_dict[true_filename]) <= index:
result_dict[true_filename].append(None)
result_dict[true_filename][index] = texture
return result_dict
def rounded(num: float) -> int:
sign = 1 if (num >= 0) else -1
num = abs(num)
result = int(num)
if (num - result >= 0.5):
result += 1
return sign * result
def get_current_ms() -> int:
return rounded(time.time() * 1000)
def strip_comments(code: str) -> str:
result = ''
index = 0
for line in code.splitlines():
comment_index = line.find('//')
if comment_index == -1:
result += line
elif comment_index != 0 and not line[:comment_index].isspace():
result += line[:comment_index]
index += 1
return result
def get_pixels_per_frame(bpm: float, time_signature: float, distance: float) -> float:
beat_duration = 60 / bpm
total_time = time_signature * beat_duration
total_frames = 60 * total_time
return (distance / total_frames)
def get_config() -> dict[str, Any]:
with open('config.toml', "rb") as f:
config_file = tomllib.load(f)
return config_file
def draw_scaled_texture(texture: ray.Texture, x: int, y: int, scale: float, color: ray.Color) -> None:
src_rect = ray.Rectangle(0, 0, texture.width, texture.height)
dst_rect = ray.Rectangle(x, y, texture.width*scale, texture.height*scale)
ray.draw_texture_pro(texture, src_rect, dst_rect, ray.Vector2(0, 0), 0, color)
@dataclass
class SessionData:
selected_song: str = '' #Path
selected_difficulty: int = 0
song_title: str = ''
result_score: int = 0
result_good: int = 0
result_ok: int = 0
result_bad: int = 0
result_max_combo: int = 0
result_total_drumroll: int = 0
result_gauge_length: int = 0
session_data = SessionData()
def reset_session():
return SessionData()
@dataclass
class GlobalData:
textures: dict[str, list[ray.Texture]] = field(default_factory=lambda: dict())
songs_played: int = 0
global_data = GlobalData()
@dataclass
class OutlinedText:
font: ray.Font
text: str
font_size: int
text_color: ray.Color
outline_color: ray.Color
outline_thickness: int = 2
vertical: bool = False
line_spacing: float = 1.0 # Line spacing for vertical text
lowercase_spacing_factor: float = 0.85 # Adjust spacing for lowercase letters and whitespace
vertical_chars: set = field(default_factory=lambda: {'-', '|', '/', '\\', ''})
no_space_chars: set = field(default_factory=lambda: {
'', '','', '','', '','', '','', '',
'', '','', '','', '','', '','', '',
'', '','','','','','','','','','',
'','','','','','',''
})
def __post_init__(self):
# Cache for rotated characters
self._rotation_cache = {}
# Cache for character measurements
self._char_size_cache = {}
self.texture = self._create_texture()
def _get_char_size(self, char):
"""Cache character size measurements"""
if char not in self._char_size_cache:
if char in self.vertical_chars:
# For vertical chars, width and height are swapped
self._char_size_cache[char] = ray.Vector2(self.font_size, self.font_size)
else:
self._char_size_cache[char] = ray.measure_text_ex(self.font, char, self.font_size, 1.0)
return self._char_size_cache[char]
def _calculate_vertical_spacing(self, current_char, next_char=None):
"""Calculate vertical spacing between characters"""
# Check if current char is lowercase, whitespace or a special character
is_spacing_char = (current_char.islower() or
current_char.isspace() or
current_char in self.no_space_chars)
# Additional check for capitalization transition
if next_char and ((current_char.isupper() and next_char.islower()) or
next_char in self.no_space_chars):
is_spacing_char = True
# Apply spacing factor if it's a spacing character
spacing = self.line_spacing * (self.lowercase_spacing_factor if is_spacing_char else 1.0)
return self.font_size * spacing
def _get_rotated_char(self, char, color):
"""Get or create a rotated character texture from cache"""
cache_key = (char, color[0], color[1], color[2], color[3])
if cache_key in self._rotation_cache:
return self._rotation_cache[cache_key]
char_size = self._get_char_size(char)
# For rotated text, we need extra padding to prevent cutoff
padding = max(int(self.font_size * 0.2), 2) # Add padding proportional to font size
temp_width = int(char_size.y) + padding * 2
temp_height = int(char_size.x) + padding * 2
# Create a temporary image with padding to ensure characters aren't cut off
temp_image = ray.gen_image_color(temp_width, temp_height, ray.Color(0, 0, 0, 0))
# Calculate centering offsets
x_offset = padding
y_offset = padding
# Draw the character centered in the temporary image
ray.image_draw_text_ex(
temp_image,
self.font,
char,
ray.Vector2(x_offset-5, y_offset),
self.font_size,
1.0,
color
)
# Rotate the temporary image 90 degrees counterclockwise
rotated_image = ray.gen_image_color(temp_height, temp_width, ray.Color(0, 0, 0, 0))
for x in range(temp_width):
for y in range(temp_height):
pixel = ray.get_image_color(temp_image, x, temp_height - y - 1)
ray.image_draw_pixel(rotated_image, y, x, pixel)
# Unload temporary image
ray.unload_image(temp_image)
# Cache the rotated image
self._rotation_cache[cache_key] = rotated_image
return rotated_image
def _calculate_dimensions(self):
"""Calculate dimensions based on orientation"""
if not self.vertical:
# Horizontal text
text_size = ray.measure_text_ex(self.font, self.text, self.font_size, 1.0)
# Add extra padding to prevent cutoff
extra_padding = max(int(self.font_size * 0.15), 2)
width = int(text_size.x + self.outline_thickness * 4 + extra_padding * 2)
height = int(text_size.y + self.outline_thickness * 4 + extra_padding * 2)
padding_x = self.outline_thickness * 2 + extra_padding
padding_y = self.outline_thickness * 2 + extra_padding
return width, height, padding_x, padding_y
else:
# For vertical text, pre-calculate all character heights and widths
char_heights = []
char_widths = []
for i, char in enumerate(self.text):
next_char = self.text[i+1] if i+1 < len(self.text) else None
char_heights.append(self._calculate_vertical_spacing(char, next_char))
# For vertical characters, consider rotated dimensions
if char in self.vertical_chars:
# Use padded width for rotated characters
padding = max(int(self.font_size * 0.2), 2) * 2
char_widths.append(self._get_char_size(char).x + padding)
else:
char_widths.append(self._get_char_size(char).x)
max_char_width = max(char_widths) if char_widths else 0
total_height = sum(char_heights) if char_heights else 0
# Add extra padding for vertical text
extra_padding = max(int(self.font_size * 0.15), 2)
width = int(max_char_width + self.outline_thickness * 4 + extra_padding * 2)
height = int(total_height + self.outline_thickness * 4 + extra_padding * 2)
padding_x = self.outline_thickness * 2 + extra_padding
padding_y = self.outline_thickness * 2 + extra_padding
return width, height, padding_x, padding_y
def _draw_horizontal_text(self, image, padding_x, padding_y):
"""Draw horizontal text with outline"""
# Draw outline
for dx in range(-self.outline_thickness, self.outline_thickness + 1):
for dy in range(-self.outline_thickness, self.outline_thickness + 1):
if dx == 0 and dy == 0:
continue
ray.image_draw_text_ex(
image,
self.font,
self.text,
ray.Vector2(padding_x + dx, padding_y + dy),
self.font_size,
1.0,
self.outline_color
)
# Draw main text
ray.image_draw_text_ex(
image,
self.font,
self.text,
ray.Vector2(padding_x, padding_y),
self.font_size,
1.0,
self.text_color
)
def _draw_vertical_text(self, image, width, padding_x, padding_y):
"""Draw vertical text with outline"""
# Precalculate positions and spacings to avoid redundant calculations
positions = []
current_y = padding_y
for i, char in enumerate(self.text):
char_size = self._get_char_size(char)
char_height = self._calculate_vertical_spacing(
char,
self.text[i+1] if i+1 < len(self.text) else None
)
# Calculate center position for each character
if char in self.vertical_chars:
# For vertical characters, we need to use the rotated image dimensions
rotated_img = self._get_rotated_char(char, self.text_color)
char_width = rotated_img.width
center_offset = (width - char_width) // 2
else:
char_width = char_size.x
center_offset = (width - char_width) // 2
positions.append((char, center_offset, current_y, char_height, char in self.vertical_chars))
current_y += char_height
# First draw all outlines
for dx in range(-self.outline_thickness, self.outline_thickness + 1):
for dy in range(-self.outline_thickness, self.outline_thickness + 1):
if dx == 0 and dy == 0:
continue
for char, center_offset, y_pos, _, is_vertical in positions:
if is_vertical:
rotated_img = self._get_rotated_char(char, self.outline_color)
ray.image_draw(
image,
rotated_img,
ray.Rectangle(0, 0, rotated_img.width, rotated_img.height),
ray.Rectangle(
int(center_offset + dx),
int(y_pos + dy),
rotated_img.width,
rotated_img.height
),
ray.WHITE
)
else:
ray.image_draw_text_ex(
image,
self.font,
char,
ray.Vector2(center_offset + dx, y_pos + dy),
self.font_size,
1.0,
self.outline_color
)
# Then draw all main text
for char, center_offset, y_pos, _, is_vertical in positions:
if is_vertical:
rotated_img = self._get_rotated_char(char, self.text_color)
ray.image_draw(
image,
rotated_img,
ray.Rectangle(0, 0, rotated_img.width, rotated_img.height),
ray.Rectangle(
int(center_offset),
int(y_pos),
rotated_img.width,
rotated_img.height
),
ray.WHITE
)
else:
ray.image_draw_text_ex(
image,
self.font,
char,
ray.Vector2(center_offset, y_pos),
self.font_size,
1.0,
self.text_color
)
def _create_texture(self):
"""Create a texture with outlined text"""
# Calculate dimensions
width, height, padding_x, padding_y = self._calculate_dimensions()
# Create transparent image
image = ray.gen_image_color(width, height, ray.Color(0, 0, 0, 0))
# Draw text based on orientation
if not self.vertical:
self._draw_horizontal_text(image, padding_x, padding_y)
else:
self._draw_vertical_text(image, width, padding_x, padding_y)
# Create texture from image
texture = ray.load_texture_from_image(image)
ray.unload_image(image)
return texture
def draw(self, src: ray.Rectangle, dest: ray.Rectangle, origin: ray.Vector2, rotation: float, color: ray.Color):
"""Draw the outlined text"""
ray.draw_texture_pro(self.texture, src, dest, origin, rotation, color)
def unload(self):
"""Clean up resources"""
# Unload all cached rotated images
for img in self._rotation_cache.values():
ray.unload_image(img)
self._rotation_cache.clear()
# Unload texture
ray.unload_texture(self.texture)