diff --git a/Asteroids.ico b/Asteroids.ico deleted file mode 100755 index ce40682..0000000 Binary files a/Asteroids.ico and /dev/null differ diff --git a/CMakeLists.txt b/CMakeLists.txt index a8e9125..eef6346 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,7 @@ configure_file(${CMAKE_SOURCE_DIR}/source/project.h.in ${CMAKE_BINARY_DIR}/proje set(APP_SOURCES source/main.cpp source/core/system/director.cpp + source/core/system/global_events.cpp source/core/rendering/sdl_manager.cpp source/core/rendering/line_renderer.cpp source/core/rendering/color_oscillator.cpp diff --git a/Makefile b/Makefile index bd4f719..5522d38 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,7 @@ endif APP_SOURCES := \ source/main.cpp \ source/core/system/director.cpp \ + source/core/system/global_events.cpp \ source/core/rendering/sdl_manager.cpp \ source/core/rendering/line_renderer.cpp \ source/core/rendering/color_oscillator.cpp \ diff --git a/asteroids b/asteroids deleted file mode 100755 index 9f0079b..0000000 Binary files a/asteroids and /dev/null differ diff --git a/data/shapes/letra_a.shp b/data/shapes/letra_a.shp new file mode 100644 index 0000000..2f6c379 --- /dev/null +++ b/data/shapes/letra_a.shp @@ -0,0 +1,9 @@ +# letra_a.shp +# Generado automáticamente desde jailgames.svg +# Dimensiones: 71.43 x 100.00 px + +name: letra_a +scale: 1.0 +center: 35.71, 50.00 + +polyline: 28.57,71.43 28.57,100.00 0.00,100.00 0.00,14.29 14.29,14.29 14.29,0.00 57.14,0.00 57.14,14.29 71.43,14.29 71.43,100.00 42.86,100.00 42.86,71.43 28.57,14.29 28.57,57.14 42.86,57.14 42.86,14.29 diff --git a/data/shapes/letra_e.shp b/data/shapes/letra_e.shp new file mode 100644 index 0000000..d0fd9f6 --- /dev/null +++ b/data/shapes/letra_e.shp @@ -0,0 +1,9 @@ +# letra_e.shp +# Generado automáticamente desde jailgames.svg +# Dimensiones: 71.43 x 100.00 px + +name: letra_e +scale: 1.0 +center: 35.71, 50.00 + +polyline: 57.14,28.57 57.14,42.86 28.57,42.86 28.57,85.71 71.43,85.71 71.43,100.00 0.00,100.00 0.00,0.00 71.43,0.00 71.43,14.29 28.57,14.29 28.57,28.57 diff --git a/data/shapes/letra_g.shp b/data/shapes/letra_g.shp new file mode 100644 index 0000000..c3940eb --- /dev/null +++ b/data/shapes/letra_g.shp @@ -0,0 +1,9 @@ +# letra_g.shp +# Generado automáticamente desde jailgames.svg +# Dimensiones: 71.43 x 100.00 px + +name: letra_g +scale: 1.0 +center: 35.71, 50.00 + +polyline: 14.29,0.00 57.14,0.00 57.14,14.29 28.57,14.29 28.57,85.71 42.86,85.71 42.86,42.86 71.43,42.86 71.43,100.00 14.29,100.00 14.29,85.71 0.00,85.71 0.00,14.29 14.29,14.29 diff --git a/data/shapes/letra_i.shp b/data/shapes/letra_i.shp new file mode 100644 index 0000000..453939f --- /dev/null +++ b/data/shapes/letra_i.shp @@ -0,0 +1,9 @@ +# letra_i.shp +# Generado automáticamente desde jailgames.svg +# Dimensiones: 28.57 x 100.00 px + +name: letra_i +scale: 1.0 +center: 14.29, 50.00 + +polyline: 0.00,0.00 28.57,0.00 28.57,100.00 0.00,100.00 0.00,0.00 diff --git a/data/shapes/letra_j.shp b/data/shapes/letra_j.shp new file mode 100644 index 0000000..1e81cf8 --- /dev/null +++ b/data/shapes/letra_j.shp @@ -0,0 +1,9 @@ +# letra_j.shp +# Generado automáticamente desde jailgames.svg +# Dimensiones: 57.14 x 100.00 px + +name: letra_j +scale: 1.0 +center: 28.57, 50.00 + +polyline: 0.00,100.00 0.00,85.71 28.57,85.71 28.57,0.00 57.14,0.00 57.14,85.71 42.86,85.71 42.86,100.00 diff --git a/data/shapes/letra_l.shp b/data/shapes/letra_l.shp new file mode 100644 index 0000000..ab0a35c --- /dev/null +++ b/data/shapes/letra_l.shp @@ -0,0 +1,9 @@ +# letra_l.shp +# Generado automáticamente desde jailgames.svg +# Dimensiones: 57.14 x 100.00 px + +name: letra_l +scale: 1.0 +center: 28.57, 50.00 + +polyline: 0.00,0.00 28.57,0.00 28.57,85.71 57.14,85.71 57.14,100.00 0.00,100.00 diff --git a/data/shapes/letra_m.shp b/data/shapes/letra_m.shp new file mode 100644 index 0000000..93ff5c3 --- /dev/null +++ b/data/shapes/letra_m.shp @@ -0,0 +1,9 @@ +# letra_m.shp +# Generado automáticamente desde jailgames.svg +# Dimensiones: 100.00 x 100.00 px + +name: letra_m +scale: 1.0 +center: 50.00, 50.00 + +polyline: 71.43,57.14 57.14,57.14 57.14,71.43 42.86,71.43 42.86,57.14 28.57,57.14 28.57,100.00 0.00,100.00 0.00,0.00 14.29,0.00 14.29,14.29 28.57,14.29 28.57,28.57 42.86,28.57 42.86,42.86 57.14,42.86 57.14,28.57 71.43,28.57 71.43,14.29 85.71,14.29 85.71,0.00 100.00,0.00 100.00,100.00 71.43,100.00 diff --git a/data/shapes/letra_s.shp b/data/shapes/letra_s.shp new file mode 100644 index 0000000..e210945 --- /dev/null +++ b/data/shapes/letra_s.shp @@ -0,0 +1,9 @@ +# letra_s.shp +# Generado automáticamente desde jailgames.svg +# Dimensiones: 57.14 x 100.00 px + +name: letra_s +scale: 1.0 +center: 28.57, 50.00 + +polyline: 0.00,85.71 28.57,85.71 28.57,57.14 14.29,57.14 14.29,42.86 0.00,42.86 0.00,14.29 14.29,14.29 14.29,0.00 57.14,0.00 57.14,14.29 28.57,14.29 28.57,42.86 42.86,42.86 42.86,57.14 57.14,57.14 57.14,85.71 42.86,85.71 42.86,100.00 0.00,100.00 diff --git a/jailgames.svg b/jailgames.svg new file mode 100644 index 0000000..8b69eb2 --- /dev/null +++ b/jailgames.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/source/game/escenes/escena_joc.cpp b/source/game/escenes/escena_joc.cpp index 41122ab..0344688 100644 --- a/source/game/escenes/escena_joc.cpp +++ b/source/game/escenes/escena_joc.cpp @@ -4,6 +4,7 @@ #include "escena_joc.hpp" #include "../../core/system/gestor_escenes.hpp" +#include "../../core/system/global_events.hpp" #include #include #include @@ -49,29 +50,13 @@ void EscenaJoc::executar() { continue; } - // Tecles globals de finestra (F1/F2/F3) - if (event.type == SDL_EVENT_KEY_DOWN) { - switch (event.key.key) { - case SDLK_F1: - sdl_.decreaseWindowSize(); - continue; - case SDLK_F2: - sdl_.increaseWindowSize(); - continue; - case SDLK_F3: - sdl_.toggleFullscreen(); - continue; - } + // Events globals (F1/F2/F3/ESC/QUIT) + if (GlobalEvents::handle(event, sdl_)) { + continue; } - // Processament normal del joc + // Processament específic del joc (SPACE per disparar) processar_input(event); - - // Detectar tancament de finestra o ESC - if (event.type == SDL_EVENT_QUIT || - (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE)) { - GestorEscenes::actual = GestorEscenes::Escena::EIXIR; - } } // Actualitzar física del joc amb delta_time real diff --git a/source/game/escenes/escena_logo.cpp b/source/game/escenes/escena_logo.cpp index 1757791..5c22b1c 100644 --- a/source/game/escenes/escena_logo.cpp +++ b/source/game/escenes/escena_logo.cpp @@ -3,11 +3,17 @@ #include "escena_logo.hpp" #include "../../core/system/gestor_escenes.hpp" +#include "../../core/system/global_events.hpp" +#include "../../core/graphics/shape_loader.hpp" +#include "../../core/rendering/shape_renderer.hpp" #include +#include +#include EscenaLogo::EscenaLogo(SDLManager& sdl) : sdl_(sdl), temps_acumulat_(0.0f) { std::cout << "Escena Logo: Inicialitzant...\n"; + inicialitzar_lletres(); } void EscenaLogo::executar() { @@ -32,34 +38,21 @@ void EscenaLogo::executar() { continue; } - // Tecles globals de finestra (F1/F2/F3) - if (event.type == SDL_EVENT_KEY_DOWN) { - switch (event.key.key) { - case SDLK_F1: - sdl_.decreaseWindowSize(); - continue; - case SDLK_F2: - sdl_.increaseWindowSize(); - continue; - case SDLK_F3: - sdl_.toggleFullscreen(); - continue; - } + // Events globals (F1/F2/F3/ESC/QUIT) + if (GlobalEvents::handle(event, sdl_)) { + continue; } - // Processar events de l'escena + // Processar events de l'escena (qualsevol tecla/clic salta al joc) processar_events(event); - - // ESC o tancar finestra = eixir - if (event.type == SDL_EVENT_QUIT || - (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE)) { - GestorEscenes::actual = GestorEscenes::Escena::EIXIR; - } } // Actualitzar lògica actualitzar(delta_time); + // Actualitzar colors oscil·lats (efecte verd global) + sdl_.updateColors(delta_time); + // Dibuixar dibuixar(); } @@ -67,18 +60,117 @@ void EscenaLogo::executar() { std::cout << "Escena Logo: Finalitzant...\n"; } +void EscenaLogo::inicialitzar_lletres() { + using namespace Graphics; + + // Llista de fitxers .shp (A repetida per a les dues A's) + std::vector fitxers = { + "letra_j.shp", "letra_a.shp", "letra_i.shp", "letra_l.shp", + "letra_g.shp", "letra_a.shp", "letra_m.shp", "letra_e.shp", "letra_s.shp" + }; + + // Pas 1: Carregar totes les formes i calcular amplades + float ancho_total = 0.0f; + + for (const auto& fitxer : fitxers) { + auto forma = ShapeLoader::load(fitxer); + if (!forma || !forma->es_valida()) { + std::cerr << "[EscenaLogo] Error carregant " << fitxer << std::endl; + continue; + } + + // Calcular bounding box de la forma (trobar ancho) + float min_x = FLT_MAX; + float max_x = -FLT_MAX; + + for (const auto& prim : forma->get_primitives()) { + for (const auto& punt : prim.points) { + min_x = std::min(min_x, punt.x); + max_x = std::max(max_x, punt.x); + } + } + + float ancho = max_x - min_x; + + lletres_.push_back({ + forma, + {0.0f, 0.0f}, // Posició es calcularà després + ancho + }); + + ancho_total += ancho; + } + + // Pas 2: Afegir espaiat entre lletres + ancho_total += ESPAI_ENTRE_LLETRES * (lletres_.size() - 1); + + // Pas 3: Calcular posició inicial (centrat horitzontal) + constexpr float PANTALLA_ANCHO = 640.0f; + constexpr float PANTALLA_ALTO = 480.0f; + + float x_inicial = (PANTALLA_ANCHO - ancho_total) / 2.0f; + float y_centre = PANTALLA_ALTO / 2.0f; + + // Pas 4: Assignar posicions a cada lletra + float x_actual = x_inicial; + + for (auto& lletra : lletres_) { + // Centre de la lletra en pantalla + lletra.posicio.x = x_actual + lletra.ancho / 2.0f; + lletra.posicio.y = y_centre; + + // Avançar per a següent lletra + x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES; + } + + std::cout << "[EscenaLogo] " << lletres_.size() + << " lletres carregades, ancho total: " << ancho_total << " px\n"; +} + +float EscenaLogo::calcular_escala_zoom(float temps) const { + if (temps >= DURACIO_ZOOM) { + return ESCALA_FINAL; // Animació acabada + } + + // Progrés normalitzat (0.0 a 1.0) + float t = temps / DURACIO_ZOOM; + + // Ease-out quadràtic: y = 1 - (1-t)^2 + // Comença ràpid, acaba suau + float factor = 1.0f - (1.0f - t) * (1.0f - t); + + // Interpolar entre escala inicial i final + return ESCALA_INICIAL + (ESCALA_FINAL - ESCALA_INICIAL) * factor; +} + void EscenaLogo::actualitzar(float delta_time) { temps_acumulat_ += delta_time; - // Després de 2 segons, saltar al joc - if (temps_acumulat_ >= 2.0f) { + // Després de DURACIO_TOTAL segons, saltar al joc + if (temps_acumulat_ >= DURACIO_TOTAL) { GestorEscenes::actual = GestorEscenes::Escena::JOC; } } void EscenaLogo::dibuixar() { - // Pantalla negra + // Fons negre sdl_.neteja(0, 0, 0); + + // Calcular escala actual del zoom + float escala = calcular_escala_zoom(temps_acumulat_); + + // Dibuixar cada lletra amb l'escala animada + for (const auto& lletra : lletres_) { + Rendering::render_shape( + sdl_.obte_renderer(), + lletra.forma, + lletra.posicio, // Posició en pantalla + 0.0f, // Sense rotació + escala, // Escala animada + true // Dibuixar + ); + } + sdl_.presenta(); } diff --git a/source/game/escenes/escena_logo.hpp b/source/game/escenes/escena_logo.hpp index 8f8a1c9..5a7edf9 100644 --- a/source/game/escenes/escena_logo.hpp +++ b/source/game/escenes/escena_logo.hpp @@ -1,11 +1,15 @@ // escena_logo.hpp - Pantalla d'inici del joc -// Mostra pantalla negra durant 2 segons i salta al joc +// Mostra logo JAILGAMES animat amb zoom i salta al joc // © 2025 Port a C++20 #pragma once #include "../../core/rendering/sdl_manager.hpp" +#include "../../core/graphics/shape.hpp" +#include "../../core/types.hpp" #include +#include +#include class EscenaLogo { public: @@ -16,6 +20,25 @@ private: SDLManager& sdl_; float temps_acumulat_; + // Estructura per a cada lletra del logo + struct LetraLogo { + std::shared_ptr forma; + Punt posicio; // Posició final en pantalla + float ancho; // Ancho per a càlcul de centrat + }; + + std::vector lletres_; // 9 lletres: J-A-I-L-G-A-M-E-S + + // Constants d'animació + static constexpr float DURACIO_ZOOM = 1.5f; // Duració del zoom (segons) + static constexpr float DURACIO_TOTAL = 3.0f; // Duració total abans d'anar al joc + static constexpr float ESCALA_INICIAL = 0.1f; // Escala inicial (10%) + static constexpr float ESCALA_FINAL = 1.0f; // Escala final (100%) + static constexpr float ESPAI_ENTRE_LLETRES = 10.0f; // Espaiat entre lletres + + // Mètodes privats + void inicialitzar_lletres(); + float calcular_escala_zoom(float temps) const; void actualitzar(float delta_time); void dibuixar(); void processar_events(const SDL_Event& event); diff --git a/tools/svg_to_shp.py b/tools/svg_to_shp.py new file mode 100755 index 0000000..7404e7c --- /dev/null +++ b/tools/svg_to_shp.py @@ -0,0 +1,340 @@ +#!/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 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 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 ") + 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()