389 lines
11 KiB
Python
Executable File
389 lines
11 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 polylines separadas.
|
|
Cada comando M inicia una nueva polyline.
|
|
Retorna: lista de listas de puntos [[(x,y), ...], [(x,y), ...], ...]
|
|
"""
|
|
# 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()]
|
|
|
|
polylines = [] # Lista de polylines
|
|
current_polyline = [] # Polyline actual
|
|
|
|
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()
|
|
|
|
# Si es comando M (MoveTo), empezar nueva polyline
|
|
if cmd_letter == 'M':
|
|
# Guardar polyline anterior si tiene puntos
|
|
if current_polyline:
|
|
polylines.append(current_polyline)
|
|
current_polyline = []
|
|
|
|
# Procesar en pares (x, y)
|
|
i = 0
|
|
while i < len(coords) - 1:
|
|
try:
|
|
x = float(coords[i])
|
|
y = float(coords[i + 1])
|
|
current_polyline.append((x, y))
|
|
i += 2
|
|
except (ValueError, IndexError):
|
|
i += 1
|
|
|
|
# No olvidar la última polyline
|
|
if current_polyline:
|
|
polylines.append(current_polyline)
|
|
|
|
return polylines
|
|
|
|
|
|
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(polylines):
|
|
"""
|
|
Calcula bounding box de una o varias polylines
|
|
polylines puede ser:
|
|
- lista de puntos [(x,y), ...]
|
|
- lista de polylines [[(x,y), ...], [(x,y), ...]]
|
|
Retorna: (min_x, max_x, min_y, max_y, ancho, alto)
|
|
"""
|
|
# Aplanar si es lista de polylines
|
|
puntos = []
|
|
if polylines and isinstance(polylines[0], list):
|
|
for polyline in polylines:
|
|
puntos.extend(polyline)
|
|
else:
|
|
puntos = polylines
|
|
|
|
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, polylines, 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
|
|
|
|
polylines: lista de polylines [[(x,y), ...], [(x,y), ...]]
|
|
Retorna: dict con polylines normalizadas, centro, ancho, alto
|
|
"""
|
|
if not polylines:
|
|
return None
|
|
|
|
min_x, max_x, min_y, max_y, ancho, alto = calc_bounding_box(polylines)
|
|
|
|
if alto == 0:
|
|
print(f" [WARN] Letra {nombre}: altura cero")
|
|
return None
|
|
|
|
# Factor de escala basado en altura
|
|
escala = altura_objetivo / alto
|
|
|
|
# Normalizar cada polyline:
|
|
# 1. Trasladar a origen (restar min_x, min_y)
|
|
# 2. Aplicar escala
|
|
# 3. Cerrar polyline (último punto = primer punto)
|
|
polylines_norm = []
|
|
total_puntos = 0
|
|
|
|
for polyline in polylines:
|
|
polyline_norm = []
|
|
for x, y in polyline:
|
|
x_norm = (x - min_x) * escala
|
|
y_norm = (y - min_y) * escala
|
|
polyline_norm.append((x_norm, y_norm))
|
|
|
|
# Cerrar polyline si no está cerrada
|
|
if polyline_norm and polyline_norm[0] != polyline_norm[-1]:
|
|
polyline_norm.append(polyline_norm[0])
|
|
|
|
polylines_norm.append(polyline_norm)
|
|
total_puntos += len(polyline_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,
|
|
'polylines': polylines_norm,
|
|
'centro': centro,
|
|
'ancho': ancho_norm,
|
|
'alto': alto_norm,
|
|
'total_puntos': total_puntos
|
|
}
|
|
|
|
|
|
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 ...
|
|
polyline: x1,y1 x2,y2 x3,y3 ... (si hay múltiples formas)
|
|
"""
|
|
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")
|
|
|
|
# Generar una línea polyline por cada forma
|
|
for polyline in letra_norm['polylines']:
|
|
puntos_str = " ".join([f"{x:.2f},{y:.2f}" for x, y in polyline])
|
|
f.write(f"polyline: {puntos_str}\n")
|
|
|
|
print(f" ✓ {nombre_archivo:20} ({len(letra_norm['polylines'])} formas, "
|
|
f"{letra_norm['total_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 (buscar recursivamente en todos los descendientes)
|
|
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 "ORNI ATTACK!"
|
|
# Grupo 1 (top): O, R, N, I (3 paths + 1 rect)
|
|
# Grupo 2 (bottom): A, T, T, A, C, K, ! (6 paths + 1 path para !)
|
|
# Total: 9 paths + 1 rect
|
|
# Asumiendo orden de aparición en SVG:
|
|
nombres_paths = ['O', 'R', 'N', 'A', 'T', 'T', 'A', 'C', 'K', 'EXCLAMACION']
|
|
|
|
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 polylines del path (ahora retorna lista de polylines)
|
|
polylines = parse_svg_path(d_attr)
|
|
|
|
# Aplicar transformación a cada polyline
|
|
polylines_transformed = []
|
|
for polyline in polylines:
|
|
polyline_transformed = [apply_transform(p, transform_matrix) for p in polyline]
|
|
polylines_transformed.append(polyline_transformed)
|
|
|
|
letras.append({
|
|
'nombre': nombres_paths[i],
|
|
'polylines': polylines_transformed
|
|
})
|
|
|
|
# Procesar rects (la letra I es un rect)
|
|
for rect in rects:
|
|
puntos = rect_to_points(rect)
|
|
|
|
# Aplicar transformación
|
|
puntos_transformed = [apply_transform(p, transform_matrix) for p in puntos]
|
|
|
|
# Rect es una sola polyline
|
|
letras.append({
|
|
'nombre': 'I',
|
|
'polylines': [puntos_transformed]
|
|
})
|
|
|
|
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['polylines'], 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()
|