#!/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 """ 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 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 ") 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()