6ad76a89fa
Wizard pasos 5-7 ahora funcionales (1-4 ya estaban en Sprint 1).
vmssailor/studio/designer/rule_engine.py
- RuleContext, RuleEngine, EquipmentProposal
- Lee library/rules/*.yaml y aplica reglas heuristicas
- Filtra por vessel_type, vessel_subtype, length_overall_m range
- Selecciona candidato segun condiciones 'when' (loa min/max)
- Genera tag_prefix con sustitucion {side}/{idx}
vmssailor/studio/designer/port_auto_assigner.py
- auto_assign() greedy: 1 bus Modbus RTU + tarjetas dedicadas para motores/gensets
- Tarjeta auxiliar compartida para resto de equipos
- Mapea SignalType -> ChannelType (AI/DI/DO/RPM)
- Genera TagBindings con scaling apropiado por tipo de senal
- Respeta capacidades 10/5/4/1 de AR-NMEA-IO-v1.0
- AssignmentReport con cards + tags + warnings
vmssailor/studio/wizard/step_05_equipment.py
- Tabla con propuestas del rule engine
- Checkboxes accept/reject + edicion inline de columnas
- Boton 'Regenerar' para re-aplicar reglas
vmssailor/studio/wizard/step_06_refinement.py
- Vista resumen de equipos aceptados
vmssailor/studio/wizard/step_07_topology.py
- Llama auto_assign sobre los equipos materializados
- Muestra tabla de tarjetas con uso por canal (DO/DI/AI/RPM)
- Lista warnings de capacidad
vmssailor/studio/editors/equipment_editor.py
- CRUD de Equipment del proyecto activo
- Tabla editable inline (tag_prefix, name, model_ref, system_id, coords, deck)
- Dialog modal para agregar equipos
- Senal projectMutated para refrescar canvas + sidebar
vmssailor/studio/main_window.py
- Layout actualizado: splitter vertical en panel derecho
(canvas arriba + equipment editor abajo)
- _on_project_mutated() re-distribuye al sidebar y canvas
Biblioteca expandida (Sprint 2 brief: 5-7 yates, 10+ motores, gensets, bombas):
- vessels: + azimut_grande_32m, princess_y85, trawler_32m_offshore, patrol_coastal_30m (total: 6)
- engines: + cat_c32_acert, mtu_16v_2000_m96, yanmar_8lv_370 (total: 5)
- gensets: + kohler_28efkozd, onan_qd13500 (total: 3)
- pumps: + jabsco_36800, grundfos_cm10 (NUEVO categoria pumps)
Tests (tests/studio/test_designer.py, 10 nuevos, total 120/120):
- Rule engine: load default, propose engines, candidate picking por LOA
- auto_assign builds topology compatible with Project (Pydantic validation)
- Equipment editor smoke
VesselWizard.build_project() ahora materializa equipment + topology + tags
desde las propuestas y la asignacion automatica del paso 7.
Criterios Sprint 2:
- uv run vms-studio crea proyecto completo desde wizard con equipos + tags + topologia
- vms-validate-library: OK 6 vessels, 10 equipment, 1 rules
- 120/120 pytest verde, ruff clean
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
180 lines
6.5 KiB
Python
180 lines
6.5 KiB
Python
"""QWizard contenedor + builder de Project a partir del estado del wizard.
|
|
|
|
Sprint 1: pasos 1-4 funcionales, 5-7 placeholders, 8 confirm.
|
|
Sprint 2: pasos 5-7 funcionales con rule engine + auto-assigner.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from PySide6.QtWidgets import QWidget, QWizard
|
|
|
|
from vmssailor.core import (
|
|
Equipment,
|
|
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_05_equipment import Step05Equipment
|
|
from vmssailor.studio.wizard.step_06_refinement import Step06Refinement
|
|
from vmssailor.studio.wizard.step_07_topology import Step07Topology
|
|
from vmssailor.studio.wizard.step_08_confirm import Step08Confirm
|
|
|
|
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"
|
|
F_PROPOSALS = "equipment.proposals"
|
|
F_ASSIGNMENT = "topology.assignment"
|
|
|
|
|
|
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(Step05Equipment())
|
|
self.addPage(Step06Refinement())
|
|
self.addPage(Step07Topology())
|
|
self.addPage(Step08Confirm())
|
|
|
|
# Style
|
|
self.setStyleSheet(
|
|
f"""
|
|
QWizard {{ background: {C_ABYSS}; }}
|
|
QWidget {{ background: {C_ABYSS}; color: {C_SAND}; }}
|
|
QWizard QPushButton {{ min-width: 100px; }}
|
|
"""
|
|
)
|
|
|
|
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]
|
|
|
|
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),
|
|
]
|
|
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",
|
|
)
|
|
|
|
try:
|
|
systems_enabled = [SystemId(s) for s in systems_raw]
|
|
except ValueError:
|
|
systems_enabled = [SystemId.MAIN_ENGINE]
|
|
|
|
# Sprint 2: materializar Equipment desde las propuestas aceptadas
|
|
proposals = self.field(F_PROPOSALS) or []
|
|
equipment_list: list[Equipment] = []
|
|
for idx, p in enumerate(proposals):
|
|
equipment_list.append(
|
|
Equipment(
|
|
id=f"eq_{idx:03d}_{p.tag_prefix.lower()}",
|
|
model_ref=p.model_ref,
|
|
tag_prefix=p.tag_prefix,
|
|
display_name=p.display_name,
|
|
location=p.to_ship_coord(),
|
|
system_id=p.system_id,
|
|
)
|
|
)
|
|
|
|
# Topología + tags del auto-assigner
|
|
assignment = self.field(F_ASSIGNMENT)
|
|
topology = assignment.topology() if assignment is not None else None
|
|
tags = list(assignment.tags) if assignment is not None else []
|
|
|
|
project_id = make_project_id(customer or "cliente", vessel.id)
|
|
|
|
if topology is not None:
|
|
return Project(
|
|
id=project_id,
|
|
name=project_name,
|
|
customer=customer,
|
|
vessel=vessel,
|
|
systems_enabled=systems_enabled,
|
|
equipment=equipment_list,
|
|
tags=tags,
|
|
topology=topology,
|
|
notes="Creado desde wizard (Sprint 2 — rule engine + auto-assigner).",
|
|
)
|
|
return Project(
|
|
id=project_id,
|
|
name=project_name,
|
|
customer=customer,
|
|
vessel=vessel,
|
|
systems_enabled=systems_enabled,
|
|
equipment=equipment_list,
|
|
tags=tags,
|
|
notes="Creado desde wizard (Sprint 2 — rule engine + auto-assigner).",
|
|
)
|