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>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
"""QWizard contenedor + builder de Project a partir del estado del wizard."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from PySide6.QtWidgets import QWidget, QWizard
|
||||
|
||||
from vmssailor.core import (
|
||||
Project,
|
||||
SystemId,
|
||||
Vessel,
|
||||
VesselSubtype,
|
||||
VesselType,
|
||||
)
|
||||
from vmssailor.core.vessel import Bulkhead, Deck
|
||||
from vmssailor.shared.ids import make_project_id
|
||||
from vmssailor.studio.theme import (
|
||||
C_ABYSS,
|
||||
C_SAND,
|
||||
)
|
||||
from vmssailor.studio.wizard.step_01_vessel_type import Step01VesselType
|
||||
from vmssailor.studio.wizard.step_02_template import Step02Template
|
||||
from vmssailor.studio.wizard.step_03_dimensions import Step03Dimensions
|
||||
from vmssailor.studio.wizard.step_04_systems import Step04Systems
|
||||
from vmssailor.studio.wizard.step_08_confirm import Step08Confirm
|
||||
from vmssailor.studio.wizard.step_57_placeholder import Step57Placeholder
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Wizard field keys used across steps
|
||||
F_PROJECT_NAME = "project.name"
|
||||
F_CUSTOMER = "project.customer"
|
||||
F_VESSEL_TYPE = "vessel.type"
|
||||
F_VESSEL_SUBTYPE = "vessel.subtype"
|
||||
F_TEMPLATE_ID = "vessel.template_id"
|
||||
F_VESSEL_NAME = "vessel.name"
|
||||
F_LOA = "vessel.length_overall_m"
|
||||
F_BEAM = "vessel.beam_max_m"
|
||||
F_DRAFT = "vessel.draft_m"
|
||||
F_BULKHEAD_FWD = "vessel.bulkhead_fwd_m"
|
||||
F_BULKHEAD_AFT = "vessel.bulkhead_aft_m"
|
||||
F_SYSTEMS = "systems.enabled"
|
||||
|
||||
|
||||
class VesselWizard(QWizard):
|
||||
"""Wizard 8 pasos para crear un nuevo Project desde cero."""
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("VMS-Sailor · Nuevo proyecto")
|
||||
self.setMinimumSize(900, 640)
|
||||
self.setWizardStyle(QWizard.ModernStyle)
|
||||
self.setOption(QWizard.NoBackButtonOnStartPage, True)
|
||||
self.setOption(QWizard.HaveHelpButton, False)
|
||||
self.setOption(QWizard.IndependentPages, False)
|
||||
|
||||
# Add pages
|
||||
self.addPage(Step01VesselType())
|
||||
self.addPage(Step02Template())
|
||||
self.addPage(Step03Dimensions())
|
||||
self.addPage(Step04Systems())
|
||||
self.addPage(Step57Placeholder(index=5, title="Equipos sugeridos (Sprint 2)"))
|
||||
self.addPage(Step57Placeholder(index=6, title="Refinamiento manual (Sprint 2)"))
|
||||
self.addPage(Step57Placeholder(index=7, title="Topología I/O AR-NMEA-IO (Sprint 2)"))
|
||||
self.addPage(Step08Confirm())
|
||||
|
||||
# Style
|
||||
self.setStyleSheet(
|
||||
f"""
|
||||
QWizard {{ background: {C_ABYSS}; }}
|
||||
QWidget {{ background: {C_ABYSS}; color: {C_SAND}; }}
|
||||
QWizard QPushButton {{ min-width: 100px; }}
|
||||
"""
|
||||
)
|
||||
|
||||
# Localización de botones (QWizard usa textos del sistema)
|
||||
self.setButtonText(QWizard.NextButton, "Siguiente >")
|
||||
self.setButtonText(QWizard.BackButton, "< Atrás")
|
||||
self.setButtonText(QWizard.FinishButton, "Crear proyecto")
|
||||
self.setButtonText(QWizard.CancelButton, "Cancelar")
|
||||
|
||||
# ----- Project construction -----------------------------------------
|
||||
|
||||
def build_project(self) -> Project:
|
||||
"""Construye un `Project` a partir de los datos capturados por el wizard."""
|
||||
project_name = self.field(F_PROJECT_NAME) or "Proyecto sin nombre"
|
||||
customer = self.field(F_CUSTOMER) or ""
|
||||
vessel_name = self.field(F_VESSEL_NAME) or project_name
|
||||
v_type = self.field(F_VESSEL_TYPE) or VesselType.YACHT_MOTOR.value
|
||||
v_sub = self.field(F_VESSEL_SUBTYPE) or VesselSubtype.PLANING.value
|
||||
loa = float(self.field(F_LOA) or 24.0)
|
||||
beam = float(self.field(F_BEAM) or 5.5)
|
||||
draft = float(self.field(F_DRAFT) or 1.8)
|
||||
bh_fwd = float(self.field(F_BULKHEAD_FWD) or (loa * 0.30))
|
||||
bh_aft = float(self.field(F_BULKHEAD_AFT) or (loa * 0.15))
|
||||
systems_raw = self.field(F_SYSTEMS) or [SystemId.MAIN_ENGINE.value]
|
||||
|
||||
# Build vessel
|
||||
decks = [
|
||||
Deck(id="lower", name="Cubierta inferior", z_bl_bottom=0.4, z_bl_top=draft + 1.0),
|
||||
Deck(
|
||||
id="main",
|
||||
name="Cubierta principal",
|
||||
z_bl_bottom=draft + 1.0,
|
||||
z_bl_top=draft + 3.0,
|
||||
),
|
||||
]
|
||||
bulkheads = [
|
||||
Bulkhead(id="er_aft", name="Mamparo popa SM", x_pp=bh_aft),
|
||||
Bulkhead(id="er_fwd", name="Mamparo proa SM", x_pp=bh_fwd),
|
||||
]
|
||||
try:
|
||||
vessel = Vessel(
|
||||
id=f"wizard_{int(loa * 10)}m",
|
||||
name=vessel_name,
|
||||
type=VesselType(v_type),
|
||||
subtype=VesselSubtype(v_sub),
|
||||
length_overall_m=loa,
|
||||
beam_max_m=beam,
|
||||
draft_m=draft,
|
||||
decks=decks,
|
||||
bulkheads=bulkheads,
|
||||
data_source="user_input",
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error("Vessel build failed: %s", exc)
|
||||
raise
|
||||
|
||||
# Equipment + tags llegan en Sprint 2 (rule engine). Por ahora, vacío.
|
||||
try:
|
||||
systems_enabled = [SystemId(s) for s in systems_raw]
|
||||
except ValueError:
|
||||
systems_enabled = [SystemId.MAIN_ENGINE]
|
||||
|
||||
project_id = make_project_id(customer or "cliente", vessel.id)
|
||||
return Project(
|
||||
id=project_id,
|
||||
name=project_name,
|
||||
customer=customer,
|
||||
vessel=vessel,
|
||||
systems_enabled=systems_enabled,
|
||||
equipment=[],
|
||||
tags=[],
|
||||
notes="Creado desde wizard Sprint 1. Sprint 2 agregará equipos sugeridos.",
|
||||
)
|
||||
Reference in New Issue
Block a user