Files
AR-VMS-Seaman/vmssailor/studio/wizard/step_02_template.py
T
alro65 813476c8db sprint-1: Studio shell PySide6 + wizard 8 pasos
Sprint 1 entrega el shell del Studio operativo. Para correrlo:
    uv run vms-studio

Componentes:

vmssailor/studio/theme.py
- Aplica design tokens del Sprint 0 (paleta Deep Ocean) a PySide6
- QSS global completo + QPalette + fuentes Inter/Space Grotesk/JetBrains Mono

vmssailor/studio/app.py
- StudioApp (QApplication) con tema, logo, version
- run_studio() entry point

vmssailor/studio/main_window.py
- Layout: topbar / sidebar / canvas central / statusbar
- Menus: Proyecto (Nuevo wizard, Abrir, Guardar, Guardar como, Salir),
  Edicion/Vista (stubs Sprint 2), Ayuda
- Operaciones funcionales: New from wizard, Open .vmsproj, Save, Save As,
  Validate (cross-entity), Compile (placeholder)
- Reloj live + statusbar con stats del proyecto

vmssailor/studio/widgets/system_sidebar.py
- Sidebar dinamico que muestra wizard steps + sistemas habilitados + disponibles
- Lee catalogo maestro y proyecto activo
- Senial systemActivated(SystemId) al doble-click

vmssailor/studio/widgets/vessel_canvas.py
- QGraphicsView central con grilla naval (1m por celda)
- Renderiza silueta del buque en planta + mamparos + equipos
- ship_to_scene() transformacion canonica naval -> escena
- Centerline + Pp axis marcados
- Ruler de eslora con marcas cada 5m
- Zoom con wheel + scroll-pan, label de zoom% en header

vmssailor/studio/wizard/ - QWizard 8 pasos
- step_01_vessel_type: tipo + subtipo + nombre proyecto + cliente
- step_02_template: selector con biblioteca curada (Sunseeker, Ferretti, blank)
- step_03_dimensions: LOA/manga/calado/mamparos con pre-fill de plantilla
- step_04_systems: checkboxes agrupados por categoria con pre-select por default_for
- step_57_placeholder: stubs visuales para Sprint 2 (pasos 5, 6, 7)
- step_08_confirm: resumen HTML completo del proyecto a crear
- VesselWizard.build_project() construye un Project valido

Tests (tests/studio/, 11 nuevos, total 110/110):
- pytest-qt offscreen
- Smoke tests del MainWindow, wizard, canvas, sidebar
- test_ship_to_scene_mapping (transformacion naval->escena)

Stack agregado:
- PySide6 6.11.1
- pytest-qt 4.5.0

Decisiones autonomas:
- QFont.setWeight requiere QFont.Weight enum en PySide6 6.11 (no int)
- QFrame.Shape.NoFrame (no QListWidget.NoFrame) para PySide6 6.11
- Pasos 5-7 quedan placeholders explicitos: Sprint 2 implementa rule engine
- Wizard crea Project sin equipment todavia (Sprint 2 los agrega)

Criterios de aceptacion Sprint 1:
- uv run vms-studio: abre ventana operativa
- 110/110 pytest verde
- ruff clean
- Smoke offscreen: MainWindow + Wizard + Canvas + Sidebar OK

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 07:52:31 -04:00

168 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Paso 2: selección de plantilla de biblioteca curada."""
from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QHBoxLayout,
QLabel,
QLineEdit,
QListWidget,
QListWidgetItem,
QVBoxLayout,
QWidget,
QWizardPage,
)
from vmssailor.library import load_library
from vmssailor.studio.theme import C_FOG, mono_font, ui_font
class Step02Template(QWizardPage):
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.setTitle("Paso 2 · Plantilla de buque")
self.setSubTitle(
"Elige una plantilla de la biblioteca curada. Las dimensiones del paso 3 "
"se pre-rellenarán pero puedes ajustar después."
)
outer = QHBoxLayout(self)
outer.setSpacing(20)
# Left: list of templates filtered by type
self._list = QListWidget()
self._list.setMinimumWidth(280)
outer.addWidget(self._list, 1)
# Right: details
right = QVBoxLayout()
outer.addLayout(right, 2)
self._name_label = QLabel("(Selecciona una plantilla)")
self._name_label.setObjectName("title")
self._name_label.setFont(ui_font(13))
right.addWidget(self._name_label)
self._meta_label = QLabel("")
self._meta_label.setFont(mono_font(10))
self._meta_label.setStyleSheet(f"color: {C_FOG};")
right.addWidget(self._meta_label)
self._desc_label = QLabel("")
self._desc_label.setWordWrap(True)
right.addWidget(self._desc_label)
right.addStretch(1)
# Hidden vessel_name field for the next step
self._vessel_name = QLineEdit()
self._vessel_name.hide()
from vmssailor.studio.wizard.wizard import F_TEMPLATE_ID, F_VESSEL_NAME
self.registerField(F_TEMPLATE_ID, self, "selected_template_id", "templateChanged")
self.registerField(F_VESSEL_NAME, self._vessel_name)
self._templates: dict[str, dict] = {}
self._selected_id: str | None = None
self._load_templates()
self._list.currentItemChanged.connect(self._on_selection_changed)
# ----- Property API (for QWizard field) -------------------------------
from PySide6.QtCore import Property, Signal # type: ignore
templateChanged = Signal()
def get_selected_template_id(self) -> str:
return self._selected_id or ""
def set_selected_template_id(self, value: str) -> None:
self._selected_id = value
self.templateChanged.emit()
selected_template_id = Property(
str, fget=get_selected_template_id, fset=set_selected_template_id, notify=templateChanged
)
# ----- Internals ------------------------------------------------------
def initializePage(self) -> None:
from vmssailor.studio.wizard.wizard import F_VESSEL_TYPE
type_str = self.field(F_VESSEL_TYPE)
self._refresh_list(type_str)
def _load_templates(self) -> None:
try:
result = load_library()
for v in result.vessels:
self._templates[v.id] = {
"id": v.id,
"name": v.name,
"type": v.type.value,
"length_overall_m": v.length_overall_m,
"beam_max_m": v.beam_max_m,
"draft_m": v.draft_m,
"description": v.description,
}
except Exception:
# Si la biblioteca falla, no rompemos el wizard.
pass
# Also add a "blank" template
self._templates["__blank__"] = {
"id": "__blank__",
"name": "Plantilla en blanco",
"type": "*",
"length_overall_m": 24.0,
"beam_max_m": 5.5,
"draft_m": 1.8,
"description": "Empezar desde cero, sin plantilla de buque. Defines todas las dimensiones manualmente.",
}
def _refresh_list(self, type_filter: str | None) -> None:
self._list.clear()
for tid, t in self._templates.items():
if type_filter and t["type"] not in (type_filter, "*"):
continue
label = (
f"{t['name']}\n"
f" {t['length_overall_m']:.1f} × {t['beam_max_m']:.1f} m"
if tid != "__blank__"
else t["name"]
)
item = QListWidgetItem(label)
item.setData(Qt.UserRole, tid)
self._list.addItem(item)
if self._list.count() > 0:
self._list.setCurrentRow(0)
def _on_selection_changed(self, current: QListWidgetItem | None) -> None:
if not current:
return
tid = current.data(Qt.UserRole)
t = self._templates.get(tid)
if not t:
return
self._selected_id = tid
self._vessel_name.setText(t["name"])
self._name_label.setText(t["name"])
self._meta_label.setText(
f"id: {t['id']} LOA {t['length_overall_m']:.1f} m · "
f"manga {t['beam_max_m']:.1f} m · calado {t['draft_m']:.1f} m"
)
self._desc_label.setText(t.get("description", "") or "(sin descripción)")
self.templateChanged.emit()
self.completeChanged.emit()
def isComplete(self) -> bool:
return self._selected_id is not None
def get_template(self) -> dict | None:
if self._selected_id:
return self._templates.get(self._selected_id)
return None