Files
orni_attack/tools/svg_to_shp.py
2025-11-28 21:07:36 +01:00

341 lines
9.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""
svg_to_shp.py - Conversor de jailgames.svg a archivos .shp individuales
Extrae letras del SVG y las convierte al formato .shp del juego Orni Attack
Uso: python3 svg_to_shp.py <input.svg> <output_dir>
"""
import sys
import os
import re
import xml.etree.ElementTree as ET
def parse_transform_matrix(transform_str):
"""
Parsea string "matrix(a,b,c,d,e,f)" → tupla (a,b,c,d,e,f)
"""
match = re.search(r'matrix\(([\d\.\-,\s]+)\)', transform_str)
if not match:
return (1, 0, 0, 1, 0, 0) # Identidad por defecto
values = re.split(r'[,\s]+', match.group(1).strip())
values = [float(v) for v in values if v]
if len(values) == 6:
return tuple(values)
return (1, 0, 0, 1, 0, 0)
def apply_transform(punto, matrix):
"""
Aplica transformación matricial 2D a un punto (x, y)
matrix = (a, b, c, d, e, f)
x' = a*x + c*y + e
y' = b*x + d*y + f
"""
x, y = punto
a, b, c, d, e, f = matrix
x_new = a * x + c * y + e
y_new = b * x + d * y + f
return (x_new, y_new)
def parse_svg_path(d_attr):
"""
Convierte comandos SVG path (M y L) a lista de puntos [(x, y), ...]
Ejemplo: "M896,1693L896,1531.23L1219.53,1531.23..." → [(896, 1693), (896, 1531.23), ...]
"""
# Reemplazar comas por espacios para facilitar parsing
d_attr = d_attr.replace(',', ' ')
# Split por comandos M y L
# Añadir marcador antes de cada comando
d_attr = re.sub(r'([ML])', r'|\1', d_attr)
commands = [c.strip() for c in d_attr.split('|') if c.strip()]
points = []
for cmd in commands:
if not cmd:
continue
# Extraer letra de comando y coordenadas
cmd_letter = cmd[0]
coords_str = cmd[1:].strip()
if not coords_str:
continue
# Parsear pares de coordenadas
coords = coords_str.split()
# Procesar en pares (x, y)
i = 0
while i < len(coords) - 1:
try:
x = float(coords[i])
y = float(coords[i + 1])
points.append((x, y))
i += 2
except (ValueError, IndexError):
i += 1
return points
def rect_to_points(rect_elem):
"""
Convierte elemento <rect> a lista de puntos (rectángulo cerrado)
"""
x = float(rect_elem.get('x', 0))
y = float(rect_elem.get('y', 0))
width = float(rect_elem.get('width', 0))
height = float(rect_elem.get('height', 0))
# Rectángulo cerrado: 5 puntos (último = primero)
return [
(x, y),
(x + width, y),
(x + width, y + height),
(x, y + height),
(x, y) # Cerrar
]
def calc_bounding_box(puntos):
"""
Calcula bounding box de una lista de puntos
Retorna: (min_x, max_x, min_y, max_y, ancho, alto)
"""
if not puntos:
return (0, 0, 0, 0, 0, 0)
xs = [p[0] for p in puntos]
ys = [p[1] for p in puntos]
min_x = min(xs)
max_x = max(xs)
min_y = min(ys)
max_y = max(ys)
ancho = max_x - min_x
alto = max_y - min_y
return (min_x, max_x, min_y, max_y, ancho, alto)
def normalizar_letra(nombre, puntos, altura_objetivo=100.0):
"""
Escala y traslada letra para que tenga altura_objetivo pixels
y esté centrada en origen (0, 0) en esquina superior izquierda
Retorna: dict con puntos normalizados, centro, ancho, alto
"""
if not puntos:
return None
min_x, max_x, min_y, max_y, ancho, alto = calc_bounding_box(puntos)
if alto == 0:
print(f" [WARN] Letra {nombre}: altura cero")
return None
# Factor de escala basado en altura
escala = altura_objetivo / alto
# Normalizar puntos:
# 1. Trasladar a origen (restar min_x, min_y)
# 2. Aplicar escala
puntos_norm = []
for x, y in puntos:
x_norm = (x - min_x) * escala
y_norm = (y - min_y) * escala
puntos_norm.append((x_norm, y_norm))
# Calcular dimensiones finales
ancho_norm = ancho * escala
alto_norm = alto * escala
# Centro de la letra
centro = (ancho_norm / 2.0, alto_norm / 2.0)
return {
'nombre': nombre,
'puntos': puntos_norm,
'centro': centro,
'ancho': ancho_norm,
'alto': alto_norm
}
def generar_shp(letra_norm, output_dir):
"""
Genera archivo .shp con formato del juego Orni Attack
Formato:
name: letra_x
scale: 1.0
center: cx, cy
polyline: x1,y1 x2,y2 x3,y3 ...
"""
if not letra_norm:
return
nombre_archivo = f"letra_{letra_norm['nombre'].lower()}.shp"
filepath = os.path.join(output_dir, nombre_archivo)
with open(filepath, 'w', encoding='utf-8') as f:
# Header con comentarios
f.write(f"# {nombre_archivo}\n")
f.write(f"# Generado automáticamente desde jailgames.svg\n")
f.write(f"# Dimensiones: {letra_norm['ancho']:.2f} x {letra_norm['alto']:.2f} px\n")
f.write(f"\n")
# Metadatos
f.write(f"name: letra_{letra_norm['nombre'].lower()}\n")
f.write(f"scale: 1.0\n")
f.write(f"center: {letra_norm['centro'][0]:.2f}, {letra_norm['centro'][1]:.2f}\n")
f.write(f"\n")
# Polyline con todos los puntos
f.write("polyline: ")
puntos_str = " ".join([f"{x:.2f},{y:.2f}" for x, y in letra_norm['puntos']])
f.write(puntos_str)
f.write("\n")
print(f"{nombre_archivo:20} ({len(letra_norm['puntos']):3} puntos, "
f"{letra_norm['ancho']:6.2f} x {letra_norm['alto']:6.2f} px)")
def parse_svg(filepath):
"""
Parsea jailgames.svg y extrae las 9 letras con sus puntos transformados
Retorna: lista de dicts con {nombre, puntos}
"""
tree = ET.parse(filepath)
root = tree.getroot()
# Namespace SVG
ns = {'svg': 'http://www.w3.org/2000/svg'}
# Buscar grupo con transform
groups = root.findall('.//svg:g[@transform]', ns)
if not groups:
print("[ERROR] No se encontró grupo con transform")
return []
group = groups[0]
transform_str = group.get('transform', '')
transform_matrix = parse_transform_matrix(transform_str)
print(f"[INFO] Transform matrix: {transform_matrix}")
# Extraer paths y rects
paths = group.findall('svg:path', ns)
rects = group.findall('svg:rect', ns)
print(f"[INFO] Encontrados {len(paths)} paths y {len(rects)} rects")
# Nombres de las letras para paths (sin I que es un rect)
nombres_paths = ['J', 'A', 'L', 'G', 'A', 'M', 'E', 'S']
letras = []
# Procesar paths
for i, path in enumerate(paths):
if i >= len(nombres_paths):
break
d_attr = path.get('d')
if not d_attr:
continue
# Parsear puntos del path
puntos = parse_svg_path(d_attr)
# Aplicar transformación
puntos = [apply_transform(p, transform_matrix) for p in puntos]
letras.append({
'nombre': nombres_paths[i],
'puntos': puntos
})
# Procesar rects (la letra I es un rect)
for rect in rects:
puntos = rect_to_points(rect)
# Aplicar transformación
puntos = [apply_transform(p, transform_matrix) for p in puntos]
letras.append({
'nombre': 'I',
'puntos': puntos
})
return letras
def main():
if len(sys.argv) != 3:
print("Uso: python3 svg_to_shp.py <input.svg> <output_dir>")
print("Ejemplo: python3 svg_to_shp.py jailgames.svg data/shapes/")
sys.exit(1)
svg_path = sys.argv[1]
output_dir = sys.argv[2]
# Verificar que el SVG existe
if not os.path.exists(svg_path):
print(f"[ERROR] No se encuentra el archivo: {svg_path}")
sys.exit(1)
# Crear directorio de salida si no existe
os.makedirs(output_dir, exist_ok=True)
print(f"\n{'='*70}")
print(f" SVG → .shp Converter para Orni Attack")
print(f"{'='*70}\n")
print(f"Input: {svg_path}")
print(f"Output: {output_dir}/\n")
# Parsear SVG
print("[1/3] Parseando SVG...")
letras = parse_svg(svg_path)
if not letras:
print("[ERROR] No se pudieron extraer letras del SVG")
sys.exit(1)
print(f" ✓ Extraídas {len(letras)} letras\n")
# Filtrar duplicados (solo una 'A')
print("[2/3] Filtrando duplicados...")
letras_unicas = {}
for letra in letras:
nombre = letra['nombre']
if nombre not in letras_unicas:
letras_unicas[nombre] = letra
print(f"{len(letras_unicas)} letras únicas: {', '.join(sorted(letras_unicas.keys()))}\n")
# Normalizar y generar .shp
print("[3/3] Generando archivos .shp (altura objetivo: 100px)...\n")
for nombre in sorted(letras_unicas.keys()):
letra = letras_unicas[nombre]
letra_norm = normalizar_letra(nombre, letra['puntos'], altura_objetivo=100.0)
if letra_norm:
generar_shp(letra_norm, output_dir)
print(f"\n{'='*70}")
print(f" ✓ Conversión completada: {len(letras_unicas)} archivos .shp generados")
print(f"{'='*70}\n")
if __name__ == '__main__':
main()