# BITÁCORA DE DESARROLLO — AR-ShipDesign > **Propósito:** Registro técnico vivo de cada módulo funcional de la app. > A diferencia de `CHANGELOG.md` (que registra versiones), esta bitácora documenta > el estado interno de cada módulo: qué funciona, qué se corrigió, por qué se > tomaron ciertas decisiones y qué queda pendiente. > > **Actualizar** al final de cada sesión de trabajo o al completar un feature. --- ## Convenciones de estado | Símbolo | Significado | |---------|-------------| | ✅ | Implementado y verificado (tests + visual) | | 🔧 | Implementado, pendiente verificación visual en la app | | 🐛 | Bug conocido, no resuelto aún | | 📋 | Planificado, no iniciado | | ❌ | Descartado o revertido (con explicación) | | ⚠️ | Restricción crítica — no romper | --- ## Reglas Inquebrantables (leer SIEMPRE antes de editar los visores) ### ⚠️ REGLA DE EJES — NUNCA VIOLAR ``` Vista Perfil (ProfileViewer) → nodos en EJE X (longitudinal) + EJE Z (vertical) Vista Planta (PlanViewer) → nodos en EJE X (longitudinal) + EJE Y (transversal) Vista Frontal (BodyPlanViewer) → nodos en EJE Y (transversal) + EJE Z (vertical) ``` Nunca bloquear nodos en ninguna vista. Nunca añadir restricciones `_hit_test` a nodos normales. Si un cambio rompe el movimiento en alguno de estos ejes → **REVERTIR INMEDIATAMENTE**. ### ⚠️ REGLA DE SNAP El snap de nodos de contorno (`snap_boundary_nodes_to_contours`) **solo** se ejecuta en `_on_new_project` (wizard de creación). Nunca en `_on_offsets_edited_from_viewer` ni en `Hull.from_dict()`. Los `x_offsets` son datos del usuario y se restauran tal cual. --- ## Sentinels de nodo especial (`viewer_lines.py`) ```python _KEEL_IDX = -1 # nodo de quilla (keel_z[i] por estación) _SHEER_IDX = -2 # nodo de cubierta (sheer_z[i] por estación) _STEM_IDX = -10 # punto de control de roda _TRANS_IDX = -20 # punto de control de espejo de popa ``` El índice `j` en `(i, j)` siendo negativo indica nodo especial, no columna de `data[i,j]`. --- ## Módulo 1 — Geometría del Casco **Archivo clave:** `arshipdesign/core/hull.py`, `arshipdesign/core/offsets.py` ### Estructura de datos ``` Hull ├── offsets: OffsetsTable │ ├── x_stations[n_sta] — posición X de cada estación [m] │ ├── data[n_sta, n_wl] — semi-manga Y por (estación, LdA) [m] │ ├── keel_z[n_sta] — Z de la quilla por estación [m] │ ├── z_waterlines[n_wl] — Z absoluta de cada LdA [m] │ ├── z_offsets[n_sta, n_wl] — ajuste Z local por nodo [m] │ └── x_offsets[n_sta, n_wl] — ajuste X visual del nodo en los visores 2D [m] ├── sheer_z[n_sta] — Z de la cubierta (arrufo) por estación [m] ├── stem_ctrl[k, 2] — polígono de control de la roda (B-spline) ├── transom_ctrl[k, 2] — polígono de control del espejo de popa └── corner_nodes: list[[i,j]] — nodos marcados como esquina (rompen suavidad) ``` ### Estado ✅ - **Serialización** (`to_dict` / `from_dict`): guarda todos los arrays sin recalcular. Al cargar, los `x_offsets` se restauran exactamente como el usuario los dejó. - **Inserción de estaciones** (`insert_station`): interpola Y, keel_z, sheer_z y offsets. - **Inserción de líneas de agua** (`insert_waterline`): interpola semi-mangas. - **B-Spline de sección** (`_section_yz` en `to_mesh`): muestrea el perfil Y-Z desde quilla → LdA de control → cubierta con grado mín(3, n-1). - **Malla 3D** (`to_mesh`): grilla estructurada n_u × n_v interpolada entre estaciones, triangulada para PyVista. Genera ambas bandas (estribor + babor). - **Lazy cache** (`station_planes`, `get_sheer_z`): no recalcula si los datos no cambian. ### Bug conocido 🐛 **"Tabla en quilla"** — Si se mueve `keel_z[i]` de una sola estación muy lejos de las vecinas, la malla 3D muestra una depresión abrupta (tabla/aleta) porque: - Las LdA permanecen en sus Z fijos absolutas. - La interpolación entre estaciones crea una concavidad estrecha en esa estación. - **Workaround:** mover la quilla en varias estaciones sucesivas para distribuir el cambio. - **Fix definitivo:** wizard de redistribución de LdA + más puntos de control de quilla. --- ## Módulo 2 — Visores 2D Interactivos **Archivo clave:** `arshipdesign/ui/widgets/viewer_lines.py` ### Clases principales ``` _BaseViewer — zoom, paneo, drag de nodos, hit-test, HUD, fairness, selección de curva ├── BodyPlanViewer — secciones transversales Y-Z (cuadernas) ├── ProfileViewer — vista lateral X-Z (quilla, cubierta, roda, espejo) └── PlanViewer — vista de planta X-Y (líneas de agua desde arriba) ``` ### Estado 🔧 - **Drag de nodos**: todos los nodos arrastrables, sin restricciones (respeta Regla de Ejes). - **Selección de nodo** (clic): nodo se vuelve dorado; panel `NodeInfoPanel` muestra X/Y/Z y checkbox de esquina. Enter aplica el valor editado manualmente. - **Selección de curva** (Shift+clic): detecta arista de la malla NURBS más cercana. La curva completa se resalta en verde menta `#00FFB0` con 2.5 px. - Body Plan: Shift+clic → sección completa keel→LdA→sheer (estación i) - Perfil: Shift+clic → quilla o cubierta (curva longitudinal) - Planta: Shift+clic → línea de agua j completa - **Peines de curvatura** `[C]`: pelos perpendiculares a la curva. Normalizados por max|κ| → siempre visibles aunque la curva sea casi recta. Solo en la curva seleccionada (Shift+clic) o en todas si no hay selección. Pelo invertido al lado opuesto = inflexión (cambio de signo de curvatura). - **Coloreo de equidad** `[F]`: nodos coloreados verde→amarillo→rojo por |d²Y/dX²|. - **Suavizado local** `[S]`: Laplaciano 1 paso en el nodo seleccionado. - **Zoom**: rueda del ratón. Doble clic: fit-to-view. - **Paneo**: botón medio o derecho + arrastrar. - **HUD** (esquina inferior derecha): estado de [C]/[F]/[S] y nombre de la curva activa. - **Sincronización entre vistas** (en vivo): `offsets_dragging` durante el drag, `offsets_edited` al soltar. - **Menú contextual** (clic derecho): insertar LdA, estación, roda, espejo, esquina. ### Historial de correcciones | Fecha | Problema | Causa raíz | Fix aplicado | |-------|----------|------------|--------------| | 2026-05-28 | Nodos de borde no arrastrables en X | `_hit_test` de ProfileViewer excluía i=0 e i=n-1 para LdA normales | Revertido: loop incluye todos los nodos sin excepción | | 2026-05-28 | Peines de curvatura invisibles | `scale = beam × 0.20` → κ≈0.02 → pelo de 2cm, invisible a escala normal | Normalizado: todos los κ ÷ max\|κ\| antes de escalar | | 2026-05-28 | `self._selected` no existe | Nombre incorrecto del atributo | Corregido a `self._selected_idx` | ### Pendiente 📋 - Peines de curvatura en keel/sheer desde el ProfileViewer (actualmente solo en quilla/cubierta como curvas, no como Z). - Suavizado 2D (Laplaciano transversal dentro de la cuaderna). - Tests automatizados para fairness coloring y suavizado. --- ## Módulo 3 — Visor 3D **Archivo clave:** `arshipdesign/ui/widgets/viewer_3d.py` ### Estado 🔧 - **Motor**: PyVista + pyvistaqt (`QtInteractor` embebido). - **Degradación sin PyVista**: muestra `QLabel` en lugar de crashear (permite que CI pase). - **Carga diferida**: `QtInteractor` se crea 500 ms después del arranque (evita conflicto OpenGL). - **Tema oscuro**: fondo `#1a1d30`, casco `#3a6080`, aristas `#4da8ff`, plano de flotación `#4da8ff` al 15%. - **Toggle mallas** (botón `⬡ Mallas` en barra superior del visor): apagado por defecto. Llama `GetProperty().EdgeVisibilityOn/Off()` sobre el actor VTK → sin re-render. ### Historial de correcciones | Fecha | Problema | Fix | |-------|----------|-----| | 2026-05-29 | Mallas siempre visibles, sin forma de apagarlas | Añadido botón toggle + `_show_edges=False` por defecto | ### Pendiente 📋 - Caras invertidas: detectar y colorear diferente (rojo/azul), comando flip. - Capas de visualización: buttocks, waterlines, sections como actores independientes. - Cierre de malla en AP para transom stern. --- ## Módulo 4 — Guardado y Cargado de Proyectos **Archivos:** `arshipdesign/core/project.py`, `arshipdesign/core/hull.py` ### Formato `.arsd` Archivo ZIP que contiene `hull.json` con formato `hull_v1`. Incluye todos los arrays de offsets, control curves, y metadatos del buque. ### Estado ✅ - **Persistencia exacta**: todos los arrays se guardan y restauran fielmente. - **Sin snap en carga**: `from_dict` no llama `snap_boundary_nodes_to_contours`. ### Historial de correcciones | Fecha | Problema | Causa | Fix | |-------|----------|-------|-----| | 2026-05-28 | Forma diferente al recargar | `snap_boundary_nodes_to_contours` en `from_dict` recalculaba `x_offsets` | Eliminado de `from_dict` | | 2026-05-28 | Nodos saltaban al soltar | `snap` en `_on_offsets_edited_from_viewer` sobreescribía la posición del usuario | Eliminado del handler | --- ## Módulo 5 — Hidrostáticos **Archivos:** `arshipdesign/core/hydrostatics.py` ### Estado ✅ - Cálculo en tiempo real al modificar cualquier nodo. - Métricas: Δ, LCB, TCB, KB, BM, GM, Cb, Cm, Cp, Cw, AWP. - Validado contra casco analítico Wigley (IACS Rec.34 §4). Tests: 315/315 ✅ --- ## Módulo 6 — Estabilidad **Archivo:** `arshipdesign/core/stability.py` ### Estado ✅ - Curva GZ por planos de inclinación. - Criterios IMO IS Code 2008 verificados. --- ## Módulo 7 — Generadores Paramétricos **Archivos:** `arshipdesign/parametric/wizard_*.py` ### Familias disponibles ✅ | Familia | Archivo | Estado | |---------|---------|--------| | Workboat (buque de trabajo) | `wizard_workboat.py` | ✅ | | Velero | `wizard_sailing.py` | ✅ | | Lancha rápida | `wizard_fast.py` | ✅ | | Remolcador | `wizard_tug.py` | ✅ | | Ferry / pasaje | `wizard_ferry.py` | ✅ | - **Arrufo parabólico**: `sheer_z[i] = sheer_base + camber × (1 − (2x/L − 1)²)` - Snap de nodos de contorno se aplica **una sola vez** al crear el proyecto. ### Pendiente 📋 - Opción transom stern en el wizard (`has_transom: bool`, `transom_angle: float`). - Wizard de estaciones/LdA/buttocks: definir manualmente posiciones antes de generar la malla. --- ## Módulo 8 — UI / Layout / Ribbon **Archivos:** `arshipdesign/ui/main_window.py`, widgets varios ### Estado 🔧 - **Layout 4 viewports**: QSplitters anidados. Arriba: 3D+Perfil. Abajo: FrontalI+Planta. - **Maximizar viewport** (botón `⬜`/`❎` o doble clic en barra de título): oculta viewport compañero y fila opuesta. Restaurar vuelve a 50/50. - **Ribbon**: tabs Geometría, Hidrostáticos, Estabilidad, Estructural. Grupo "Suavizado" con botones Curvatura, Equidad, Suavizar. - **NodeInfoPanel**: flotante, coordenadas X/Y/Z editables + checkbox esquina. ### Historial de correcciones | Fecha | Problema | Fix | |-------|----------|-----| | 2026-05-28 | Enter en NodeInfoPanel no aplicaba cambio | Señal `coord_edited` no conectada | Conectada en `__init__` | | 2026-05-29 | `QPushButton` no importado | Faltaba en bloque de imports | Añadido | --- ## Módulo 9 — Herramientas de Fairness (Equidad) **Funciones en** `viewer_lines.py`: `_fairness_color`, `_smooth_selected_node`, `_draw_curvature_comb`, `_curvature_comb_data`, `_dist_to_segment` ### Peines de curvatura ``` κᵢ = 2 × cross(t₁, t₂) / (l₁ + l₂) — curvatura discreta firmada κ_normalizada = κᵢ / max|κ| — rango [-1, 1] pelo_longitud = κ_normalizada × scale — en unidades de mundo ``` - Pelo al lado contrario de la curva = curvatura positiva (convexa). - Pelo al mismo lado = curvatura negativa (cóncava / inflexión). - Spine = línea que une las puntas → revela continuidad de curvatura. ### Coloreo de equidad ``` roughness = |Y[i+1] - 2·Y[i] + Y[i-1]| / (Δx²) ``` - Verde `#22cc66`: roughness < 0.005 m⁻¹ - Rojo `#e03030`: roughness > 0.150 m⁻¹ ### Suavizado Laplaciano 1-paso ``` Y_new[i] = (Y[i-1] + Y[i] + Y[i+1]) / 3 ``` Solo nodos interiores. Aplica a Y breadths, keel_z y sheer_z. --- ## Módulo 10 — Deshacer / Rehacer (Ctrl+Z / Ctrl+Y) **Archivo:** `arshipdesign/ui/main_window.py` ### Estado 🔧 - **Mecanismo**: stack de snapshots `hull.to_dict()` — cada estado es una copia completa del casco serializado (arreglos numpy → listas, muy pequeño en memoria). - **Capacidad**: 50 pasos de deshacer (`_MAX_UNDO = 50`). - **Ctrl+Z** (`Editar → Deshacer`): restaura el estado anterior al último drag/edición. - **Ctrl+Y** (`Editar → Rehacer`): rehace el cambio deshecho. - Cada nueva edición **limpia el stack de redo** (rama nueva invalida el futuro). - Al crear o abrir un proyecto, ambos stacks se limpian (`_reset_undo_history`). - Las acciones del menú se habilitan/deshabilitan según haya pasos disponibles. ### Cómo funciona internamente ``` _last_hull_state = snapshot del hull ANTES del último edit _undo_stack = [estado_0, estado_1, ..., estado_n] ← el más reciente al final _redo_stack = estados deshechados disponibles Al recibir offsets_edited: 1. push _last_hull_state → _undo_stack 2. clear _redo_stack 3. _last_hull_state = hull.to_dict() (nuevo estado actual) Al hacer Ctrl+Z: 1. push hull.to_dict() → _redo_stack 2. hull = Hull.from_dict(_undo_stack.pop()) 3. _load_hull_viewers(hull) — refresca todos los visores + hidrostáticos ``` ### Qué operaciones son deshaciibles | Operación | ¿Deshacible? | |-----------|-------------| | Arrastrar nodo | ✅ | | Suavizar con [S] | ✅ (si emite offsets_edited) | | Editar coordenada en panel | ✅ | | Insertar estación/LdA desde menú contextual | ✅ | | Crear nuevo proyecto | ❌ (limpia el historial) | | Abrir proyecto | ❌ (limpia el historial) | --- ## Módulo 11 — Iconos de Ribbon (arshipdesign/ui/icons.py) **Estado:** 🔧 Implementado — pendiente verificación visual ### Qué hace Nuevo módulo `arshipdesign/ui/icons.py` con **50 iconos programáticos** únicos, uno por cada botón del ribbon. Antes todos compartían el mismo icono genérico del sistema (`SP_FileDialogDetailedView`). ### Diseño técnico - Cada icono se dibuja con `QPainter` sobre un `QPixmap(24×24)` transparente. - Paleta coherente con el tema oscuro: - `#c8d8e8` trazo principal - `#4da8ff` cyan / agua - `#00ffb0` verde mint (selección / OK) - `#ffd060` amarillo / dorado (energía, controles) - `#ff5555` rojo (daño, alerta) - Función pública: `icon("clave") → QIcon` con caché `_CACHE` dict. - Importado en `main_window.py` como `from arshipdesign.ui.icons import icon as _ico`. ### Iconos implementados por grupo | Grupo | Claves | |-------|--------| | HOME / Vistas | `4views`, `lines_plan` | | Geometría / Nuevo | `wizard`, `hull_nurbs`, `appendage` | | Geometría / Edición NURBS | `ctrl_pts`, `extrude`, `mirror`, `lackenby` | | Geometría / Importar | `import_offsets`, `import_dxf` | | Geometría / Exportar | `export_iges`, `export_step`, `export_dxf` | | Geometría / Suavizado | `smooth`, `combs`, `fairness` | | Análisis / Hidrostática | `hydro_calc`, `hydro_curves`, `export_csv` | | Análisis / Estabilidad | `gz_curve`, `imo`, `damage` | | Análisis / Resistencia | `holtrop`, `savitsky`, `vpp` | | Análisis / Seakeeping | `stf`, `spectrum` | | Análisis / Estructura | `iso12215` | | Tanques | `new_tank`, `model_tank`, `load_case`, `sounding`, `calc_kg` | | Sistemas / Eléctrico | `epla` | | Sistemas / Fluidos | `fuel`, `freshwater`, `bilge`, `firefight` | | Sistemas / Routing 3D | `pipes`, `cables` | | Sistemas / Clima | `hvac`, `steering` | | Fabricación / CNC | `materials`, `nesting`, `gcode`, `postproc` | | Fabricación / Moldes FRP | `lofting`, `laminate`, `resin`, `bom` | ### Decisiones - Se mantienen los iconos estándar del sistema para: Nuevo, Abrir, Guardar (Archivo), Deshacer/Rehacer (flechas del sistema), Offsets (vista de lista). - El módulo NO importa Qt en el nivel de módulo — los QIcon solo se crean cuando se llaman, así la importación de `icons.py` es segura antes de que exista `QApplication`. ### Corrección — Rediseño v2 (2026-05-30 sesión 2) **Problema detectado:** La primera versión usaba trazos claros (`#c8d8e8`) sobre fondo transparente. El ribbon de PySide6 tiene fondo blanco → los iconos eran prácticamente invisibles (se veía solo el borde del botón). **Solución:** Rediseño completo con estilo "flat icon": - Relleno sólido de color por categoría + contorno oscuro `#1a2535` - Visible en fondos claros Y oscuros - Colores por categoría: | Categoría | Color | |-----------|-------| | Geometría / Casco | Azul océano `#2a7fc8` | | Edición NURBS | Índigo `#5548d0` | | Suavizado | Verde vivo `#20a860` | | Peines | Púrpura `#7040c8` + verde mint | | Fairness | Gradiente rojo→verde | | Análisis hidro | Teal `#1898a8` | | Estabilidad | Azul `#2068c0` | | Resistencia | Naranja `#d07020` | | Tanques | Cyan `#18a0c0` | | Sistemas eléctrico | Amarillo sobre negro | | Fabricación | Violeta `#8838b8` | **Estado tras rediseño:** 🔧 Pendiente verificar visualmente (requiere `python main.py`) --- ## Módulo 12 — Peines de Curvatura Mejorados **Estado:** 🔧 Implementado — pendiente verificación visual ### Cambios en `viewer_lines.py` **Problema:** Los peines se dibujaban usando los ~10-20 puntos crudos de la tabla de offsets. Resultado: pelos escasos, ángulos bruscos, spine anguloso. **Solución:** Nueva función `_resample_curve_smooth(xs, ys, n=80)`: - Parametriza la curva por longitud de arco acumulada - Remuestrea a **80 puntos equidistantes** usando `scipy.interpolate.CubicSpline` - Fallback a `np.interp` (lineal) si scipy no está disponible - Llamada al inicio de `_draw_curvature_comb` antes de calcular κ **Resultado esperado:** 80 pelos por curva en lugar de ~10-20, spine suave. ### Regla No aumentar más de 80 muestras sin medir impacto en FPS — la función se llama en cada `paintEvent` (puede ser frecuente al arrastrar nodos). --- ## Módulo 13 — Visor 3D Colores Sólidos **Estado:** 🔧 Implementado — pendiente verificación visual ### Cambios en `viewer_3d.py` — `_render_hull_mesh` | Parámetro | Antes | Ahora | Por qué | |-----------|-------|-------|---------| | `smooth_shading` | `True` | `False` | Facetas planas = aspecto sólido, sin blur | | `opacity` | `0.92` | `1.0` | Totalmente opaco = color pleno | | `ambient` | (default ~0.2) | `0.40` | Reduce sombras duras, color más uniforme | | `diffuse` | (default ~0.8) | `0.60` | Equilibrio iluminación | | `specular` | (default ~0.1) | `0.05` | Sin brillos que difuminen | | `color` | `#3a6080` | `#4a8ab0` | Tono más vivo y legible | | `line_width` | `0.3` | `0.6` | Aristas más visibles al activar mallas | ### Nota Si en el futuro se quiere smooth shading selectivo (solo en alta resolución), usar `mesh.compute_normals()` primero y luego `smooth_shading=True`. --- ## Módulo 14 — Fix Freeze Curva GZ (QThread) **Estado:** 🔧 Implementado — pendiente verificación ### Problema `_on_show_stability` → `_compute_and_show_gz` → `compute_gz_wall_sided` → `compute_upright` (integración hidrostática pesada) → **bloqueaba el UI thread de Qt** indefinidamente ("se trabó el programa"). ### Solución Nueva clase `_GZWorker(QObject)` con señales `finished` / `error`. El cálculo se mueve a un `QThread`: ``` [UI thread] botón → _compute_and_show_gz() ↓ lanza QThread [Hilo GZ] _GZWorker.run() ↓ emite finished(gz_curve, imo_result) [UI thread] _on_gz_done() → actualiza widget + statusBar ``` ### Guarda doble Si el usuario hace clic dos veces seguidas, el segundo clic se ignora mientras el hilo anterior sigue corriendo (`if self._gz_thread.isRunning(): return`). ### Archivos modificados | Archivo | Cambio | |---------|--------| | `main_window.py` | Import `QThread, QObject`; clase `_GZWorker`; `_compute_and_show_gz` refactorizado; nuevo slot `_on_gz_done` | --- ## Roadmap Global | Prioridad | Feature | Módulo | Estado | |-----------|---------|--------|--------| | 🔴 Alta | Wizard de estaciones / LdA / buttocks | Geometría | 📋 | | 🔴 Alta | Transom stern (popa espejo) | Geometría + 3D | 📋 | | 🟡 Media | Verificar iconos ribbon visualmente | UI | 🔧 | | 🟡 Media | Verificar peines densidad visual | Fairness | 🔧 | | 🟡 Media | Verificar colores sólidos 3D visual | Visor 3D | 🔧 | | 🟡 Media | Caras invertidas 3D + flip | Visor 3D | 📋 | | 🟡 Media | Peines de curvatura en keel/sheer (Z) | Fairness | 📋 | | 🟡 Media | Suavizado 2D (Laplaciano transversal) | Fairness | 📋 | | 🟢 Baja | Tests de fairness automatizados | Tests | 📋 | | 🟢 Baja | Exportar DXF / offsets CSV | Exportación | 📋 | | 🟢 Baja | Importar offsets desde tabla manual | Importación | 📋 | --- ## Tests y Entorno ```bash # Ejecutar suite completa cd "D:\Proyectos Software\AR-Shipdesign" python -m pytest tests/ -x -q # Lanzar la aplicación python main.py ``` **Estado de tests:** 315/315 ✅ — Última verificación: 2026-05-30 ⚠️ Tests no actualizados para GZWorker (QThread) — agregar en próxima sesión. --- *Última actualización: 2026-05-30 (sesión 2)* *Mantener este archivo actualizado al final de cada sesión de trabajo.*