Arquitectura de Kmila-9s

Aplicación Multiplataforma para el Aprendizaje y Depuración de Código VHDL sin Requerimiento de Hardware Físico


flowchart TB
    Usuario([Usuario])

    subgraph App["Aplicación Multiplataforma (MAUI / Blazor)"]
        UI[Interfaz de Usuario<br/>Editor Monaco, Visualizador, i18n]
        ProgramBuilder[ProgramBuilder<br/>Orquestador de Construcción]
    end

    subgraph Core["Motor de Simulación"]
        Debugger[Debugger<br/>Orquestador de Simulación]
        Parser[Parser<br/>Análisis Léxico y Sintáctico]
        Interpreter[Intérprete<br/>Ejecución y Ciclos Delta]
        TimeMachine[TimeMachine<br/>Control de Tiempo Discreto]

        subgraph IServ["Servicios del Intérprete (singletons por proceso)"]
            EntityReg[(EntityRegistry)]
            PackageReg[(PackageRegistry)]
            FunctionReg[(FunctionRegistry<br/>v1.15)]
            SkipLog[/SkipDiagnosticLog<br/>v1.15/]
            IEEELoader[/"IEEELibraryLoader<br/>v1.15 #20"/]
        end
    end

    subgraph Lib["LibraryCompiler (módulo hermano, v1.16 #22-#26)"]
        LCFacade[LibraryCompiler<br/>Facade + ProjectContext]
        Resolver[LibraryResolver<br/>topo-sort de dependencias]
        Builder[LibraryBuilder<br/>una librería → CompiledLibrary]
        Orch[BuildOrchestrator<br/>Linear / Parallel +<br/>OnDemand/Cached/Persistent]
        IBlobStore[/IBlobStore<br/>contrato; concretos en host/]
        IEEEStubs[(Builtins/IEEE/<br/>std_logic_1164.vhd<br/>numeric_std.vhd<br/>embedded resources)]
    end

    LCFacade --> Resolver
    LCFacade --> Orch
    Orch --> Builder
    Builder -.-> IEEEStubs
    Orch <-.-> IBlobStore
    IEEELoader -->|delega v1.16| LCFacade
    LCFacade -->|seed CompiledLibrary| FunctionReg

    subgraph Data["Capa de Datos"]
        SQLite[(SQLite<br/>Proyectos y Configuración)]
        FPGA_JSON[(JSON<br/>Especificaciones FPGA)]
    end

    subgraph Output["Resultados"]
        Signals[Estados de Señales<br/>y Formas de Onda]
        Resources[Estimación de<br/>Recursos FPGA]
    end

    Usuario --> UI
    UI --> ProgramBuilder
    ProgramBuilder --> Debugger
    Debugger --> Parser
    Parser -->|AST| Interpreter
    Debugger --> Interpreter
    Interpreter <--> TimeMachine
    Interpreter --> Signals
    Interpreter --> Resources
    Signals --> UI
    Resources --> UI
    UI --> SQLite
    UI --> FPGA_JSON

    style Usuario fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
    style UI fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
    style ProgramBuilder fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
    style Debugger fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    style Parser fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    style Interpreter fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    style TimeMachine fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    style SQLite fill:#fce4ec,stroke:#c62828,stroke-width:2px
    style FPGA_JSON fill:#fce4ec,stroke:#c62828,stroke-width:2px
    style Signals fill:#fff3e0,stroke:#e65100,stroke-width:2px
    style Resources fill:#fff3e0,stroke:#e65100,stroke-width:2px

Módulo Descripción
App Interfaz multiplataforma (MAUI/Blazor) con editor Monaco, gestión de proyectos, i18n (7 idiomas), y validación de recursos FPGA
Debugger Capa de pre-procesamiento (no orquestador). Lee un .vhdl, corre un lint (RunDiagnosticsVHD-MISSING-SEMICOLON, VHD-BLOCK-BALANCE) y un pase estructural que hidrata tipos Interpreter (Entity/Architecture). No ejecuta la simulación ni llama a TimeMachine; el flujo de simulación en vivo lo maneja Kmila.Shared.Services contra el Interpreter. Propaga índices de línea al Tokenizer (TokenizeLineAt) y publica Diagnostics aun cuando el parse estructural lance excepción. (Correcto desde v1.14.1: revisiones previas describían un rol de "orquestador central / State Manager / Time Travel" que nunca existió — ver ../Debugger/README.md.)
Parser Analiza el código VHDL: análisis léxico (Tokenizer), sintáctico, y generación del Árbol de Sintaxis Abstracta (AST). En v1.15 acepta declaraciones de procedure en el encabezado de la arquitectura, instanciación directa de entidad (label : entity work.X(arch)), y expresiones aritméticas como valores por defecto de generic.
Interpreter Ejecuta la lógica VHDL con algoritmo Shunting-yard, ciclos delta, y gestión de estados de señales. v1.15 introduce FunctionRegistry (firmas de funciones y procedimientos como cajas negras), SkipDiagnosticLog (códigos VHD-* para todo construct silenciosamente omitido), e IEEELibraryLoader que sembra los nombres de IEEE.std_logic_1164 y IEEE.numeric_std (delegado al módulo LibraryCompiler desde v1.16 #22-#26). Soporta case <slice>, <sig>'range en for-loops, y alias con tamaño efectivo. Motor Tier 4.1–4.3 (cerrado 2026-07-28): atributos de señal 'event/'last_value/'stable[(T)]/'active (SignalAttributeHandler + DeferredAttribute); case?/is? (matching-case VHDL-2008); body-synthesis MVP de funciones/procedimientos (inlining single-return + cascada if/elsif/else → MUX, con frames ProcedureFrame/LoopFrame/BranchFrame/IProcessFrame y fallback UserFunctionBlackBox). Dos runners intercambiables vía ISimulationRunner: SimulationRunner (Visual/Slow) y FastSimulationRunner (Fast, salta ticks ociosos, byte-idéntico).
TimeMachine Controla el tiempo de simulación discreto (10 ns/tick), ciclos de reloj, y proporciona control de ejecución (inicio/pausa/parada). v1.15 sustituye el REPL interactivo de Program.cs por un test-runner automatizado de 23 casos (Clock, TimeClock, Timer + 3 stress).

Plataformas: Windows, macOS, Linux, Android, iOS | Stack: .NET 9, MAUI, Blazor | Base de Datos: SQLite

Perfil de memoria (baseline v1.15, RSS pico mediano — no re-medido desde entonces): Parser 94 MB · Interpreter 101 MB · Interpreter --test-delta 56 MB · Debugger 98 MB · TimeMachine 37 MB. Todos los .csproj ejecutables declaran ServerGarbageCollection=false para favorecer Workstation GC en dispositivos de bajo RAM. (Cifras de referencia v1.15; el motor creció con Tier 4.1–4.3, así que trátalas como orden de magnitud, no como valores actuales.)

Notas de versión: los CHANGELOG_v1.15.mdCHANGELOG_v1.19.md y los CHANGELOG_*_2026-*.md por-tema ahora viven en _archive/changelogs/ y están fusionados en el CHANGELOG.md consolidado (fuente más fresca). El trabajo posterior a julio se resume en STATUS.md §0.

Sistema de botones — slot grammar (2026-06-28). Tres familias de botones (.kmila-btn Family 11 · .kmila-btn-icon Family 14 · .kmila-btn-tab Family 15) viven en Kmila.Shared/wwwroot/css/buttons.css. Encima de ellas se apoya una gramática de slots que dicta dónde va cada botón, qué tamaño lleva para su rol, y cómo se agrupa con sus vecinos:

  • .kmila-slot-input-trailing — input absorbe ancho, botón comparte la fila a align-items: stretch; nunca cae debajo.
  • .kmila-slot-banner — texto + acción al final del banner; texto envuelve, botón nunca envuelve.
  • .kmila-slot-modal-footer — fila de acciones alineada a la derecha al pie del modal; ≤520 px apila invertido para alcance del pulgar.
  • .kmila-slot-header-tools (+ __filter, __filter-divider, __actions) — filter shelf (search ± select integrados en un contenedor de 44 px) + cluster de acciones separado. Reflow a dos filas en ≤768 px.
  • .kmila-slot-segmented — pill background unifica una fila de .kmila-btn-tab para que se lea como un solo segmented control.
  • .kmila-slot-toolbar-divider / .waveform-viewer__divider — separador vertical fino entre clusters dentro de la misma toolbar.

Reglas no codificadas como clases (variant rationalization, swap Family 11 → 14 en filas de lista, retirar .btn-lg huérfano, single-family-per-toolbar) se documentan en CHANGELOG_button_placement_2026-06-28.md y en la propuesta congelada en Documentacion/UI_AUDIT_2026-06-28-button-placement/SUMMARY.md.


Mini-currículo de Conceptos (/concepts, 2026-05-17)

Pista paralela a /learn y /practice, dedicada al modelo de ejecución detrás de VHDL: secuencial dentro de un proceso, concurrente en el cuerpo de la arquitectura, y por qué el paralelismo real solo existe en hardware. Documentación canónica en ROADMAP_concurrent_execution_module.md; detalles de implementación en CHANGELOG_concepts_module_2026-05-17.md.

flowchart LR
    Catalog["Pages/Concepts.razor<br/>/concepts"] --> Lesson["Pages/ConceptView.razor<br/>/concepts/{module}/{lesson}"]
    Catalog --> Exercise["Pages/ConceptExerciseView.razor<br/>/concepts/exercise/{id}"]

    Lesson --> Player["Components/ExecutionPlayer.razor<br/>Monaco read-only + tape transport"]
    Lesson --> Quiz["Components/QuizletPanel.razor<br/>radio + check + explicación"]

    Exercise --> Editor["Components/BlockEditor.razor<br/>kmila-blocks workspace"]
    Exercise -->|fingerprint match| Player

    Player --> Runner["Services/TimelineRunner<br/>state machine, no DI"]
    Editor --> BP["Services/BlockProgram<br/>fingerprint(workspaceJson)"]

    Catalog -.-> Cat["Services/ConceptCatalog<br/>manifest + bodies + timelines + payloads"]
    Lesson -.-> Cat
    Exercise -.-> Cat
    Lesson -.-> Prog["Services/ConceptProgressStore<br/>concept.done.* en AppSettings"]
    Exercise -.-> Prog

    style Catalog fill:#e3f2fd,stroke:#1565c0
    style Lesson fill:#e3f2fd,stroke:#1565c0
    style Exercise fill:#e3f2fd,stroke:#1565c0
    style Player fill:#fff3e0,stroke:#e65100
    style Editor fill:#fff3e0,stroke:#e65100
    style Quiz fill:#fff3e0,stroke:#e65100
    style Runner fill:#e8f5e9,stroke:#2e7d32
    style BP fill:#e8f5e9,stroke:#2e7d32
    style Cat fill:#f3e5f5,stroke:#7b1fa2
    style Prog fill:#f3e5f5,stroke:#7b1fa2

Decisiones de diseño clave:

Decisión Razón
Catálogo y store clonados de LessonCatalog/LessonProgressStore en lugar de parametrizar Isolation: la pista de Conceptos puede evolucionar su esquema sin riesgo de regresión en /learn. Mismo costo de código, mucho menor blast radius.
Timelines pre-grabados en JSON (Phase 1) en vez de instrumentar el simulador Permite que cada paso lleve una nota didáctica y se controle el ritmo narrativo; el simulador no necesita cambios. Phase 2 del roadmap puede sustituir con emisión en vivo sin tocar el ExecutionPlayer.
kmila-blocks (JS engine bespoke, wwwroot/js/kmila-blocks*.js) reemplaza Blockly desde v1.20 Un solo kmila-blocks.js (~36 KB) sustituyó el bundle Blockly (~1 043 KB). Los nombres de interop kmilaBlockly* se preservan por compatibilidad histórica; el engine es kmila-blocks, no Blockly. Ver [[project_blockly_replacement_plan]].
Fingerprint canónico (workspace JSON → string ordenado) en lugar de comparación estructural Permite catálogos pre-armados pequeños y un mensaje amigable "esta combinación aún no está preparada" cuando el alumno construye algo fuera del set. Es ordenamiento-sensible — variantes conmutativas requieren entradas adicionales en el catálogo.
Quizlets como [Parameter] opcional en Concept Tres lecciones (1, 3, 5) llevan validación gate; el resto sale al footer directamente. Cero costo cuando no hay quiz.

Persistencia: una sola tabla AppSettings ya existente, con prefijos distintos: learn.done.* (existente), concept.done.{module}.{lesson} y concept.done.exercises.{id} (nuevos). No hubo migración de esquema.

Idiomas: EN y ES totalmente autorados (10 markdown bodies + 11 timelines + 4 exercises briefs + ~66 keys de diccionario). FR/DE/CH/JP/AR caen al EN con el banner estándar lesson-fallback-banner según la convención de feedback_translation_pending.


Módulos fuera del alcance de TT1

Las siguientes carpetas existen en el repositorio pero no son parte del alcance de TT1 y no están documentadas en los diagramas de tesis. Se mantienen como placeholders para posibles desarrollos en TT2.

Módulo Estado Notas
Simulator/ Placeholder URP-3D vacío Plantilla Unity 6 estándar. Único contenido propio: SampleScene.unity (cámara + luz direccional) y los scripts Readme.cs/ReadmeEditor.cs del template. Sin integración con el resto de Kmila (sin IPC, sin loaders compartidos, sin referencias en App/Kmila). Reservado para una futura visualización 3D (FPGA / circuito).
KmilaFactorySim/ Placeholder URP-2D vacío Sin scripts C# propios. Único contenido: SampleScene.unity (cámara ortográfica + Global Light 2D). Sin integración. La intención original (visualización tipo "factory" de utilización de recursos) queda como exploración futura.

Recomendación práctica: antes de poblar cualquiera de los dos proyectos Unity, definir explícitamente el contrato de comunicación con App/Kmila (formato JSON sobre stdio, WebSocket local, archivos compartidos, etc.) y agregar un diagrama C4 de despliegue extendido. Mientras tanto, ambos proyectos pueden ignorarse para efectos de auditoría, build y documentación.


Auditoría de diagramas (2026-05-09)

Una auditoría cruzada de código y diagramas se realizó el 2026-05-09. Resultado:

  • 8 diagramas reescritos para reflejar el código actual.
  • 3 diagramas con actualizaciones superficiales (renombre de método/clase).
  • 19 diagramas nuevos en Documentacion/TT1/diagrams/ (extendiendo las series 5.5.x, 5.8.x, 5.10.x, 5.11.x, 5.12.x, 5.13.x).
  • Captions actualizadas en chapters/capitulo4.tex y chapters/capitulo5.tex.
  • Anexo con las llamadas \diagramfig{} para los 19 nuevos en chapters/anexo_diagramas_audit_2026-05-09.tex, listo para integrar en el lugar narrativo apropiado de capítulo 5.

Detalle completo: AUDIT_2026-05-09.md y CHANGELOG_diagrams_2026-05-09.md.


Editor step-replay (`/editor/

Después de cada simulación en el editor real, la nueva pestaña Replay del dock reproduce el run capturado a través del mismo ExecutionPlayer que usa la pista /concepts — Monaco de solo lectura, transporte de cinta, tabla de señales en vivo.

flowchart LR
    Run(["▶ Run"]) --> Builder["ProgramBuilder<br/>RunSimulationAsync"]
    Builder --> Engine["Interpreter<br/>DeltaCycleEngine"]
    Engine --> SH["SignalHistory"]
    SH --> WD["WaveformData<br/>FromSignalHistory"]
    WD --> ReplayB["Services/ReplayBuilder<br/>(static, ≤200 frames)"]
    ReplayB --> Doc["ExecutionTimelineDoc"]
    Doc --> ReplayState["Services/ReplayState<br/>(scoped)"]
    ReplayState -->|OnChange| Panel["Components/StepReplayPanel.razor"]
    Panel --> Player["ExecutionPlayer.razor<br/>(reused from Concepts)"]

    style Builder fill:#f3e5f5,stroke:#7b1fa2
    style Engine fill:#e8f5e9,stroke:#2e7d32
    style ReplayB fill:#e8f5e9,stroke:#2e7d32
    style ReplayState fill:#f3e5f5,stroke:#7b1fa2
    style Panel fill:#e3f2fd,stroke:#1565c0
    style Player fill:#fff3e0,stroke:#e65100

Decisiones de diseño clave:

Decisión Razón
Reutilizar ExecutionPlayer en lugar de un nuevo componente Ya tiene reproductor + tape + tabla de señales + animación de cambios. Cero UI nueva.
ReplayBuilder puro estático en lugar de servicio DI Convierte WaveformDataExecutionTimelineDoc sin estado.
Cap de 200 frames con downsampling Mantiene el tape ágil incluso para runs largos. Preserva inicio + fin, los frames intermedios se estridan uniformemente.
ReplayState scoped en lugar de event-bus global Por-circuito: dos proyectos abiertos en dos pestañas no se pisan.
HighlightLines siempre vacío en Phase A Mapeo AST→línea-fuente requiere instrumentar Interpreter/DeltaCycleEngine. Phase B; ver memoria project_editor_live_simulation_playback.

Persistencia: ninguna en Phase A — el replay vive en memoria scoped, perdido al refresh. Las snapshots de simulación en SQLite podrían usarse en una Phase futura para reconstruir replays a demanda.

Depuración en vivo (backlog #3/#4/#5, 2026-05-27): sobre el mismo ReplayState + el DeltaCycleEngine en vivo (AttachLive/DetachLive):

Pieza Qué hace
Aviso de breakpoint no rastreable (#4) RefreshLineMap(source) reconstruye el mapa señal→línea (y el conjunto de líneas-condición) desde el código actual; IsLineTracked/TrackedLines clasifican cada breakpoint. Editor.RepaintBreakpointsAsync() llama al JS extendido kmilaSetMonacoBreakpoints(all, untracked, warnMsg) → los breakpoints sin transición de señal se pintan como glifo gris hueco punteado + tooltip ⚠. StepReplayPanel también los marca en su lista.
Step-through en el esquemático (#3) TrackActiveLines + LiveActiveLines/LiveActiveSignals + evento OnLiveStepChanged (throttle ~10 Hz, inmediato al pausar/step). SchematicViewer en modo Debug pulsa la compuerta cuyo Cell.SourceLine coincide con el delta actual (is-active-step) y enciende los wires de señales cambiadas; barra de control Pause/Resume/Step vía Replay.Live*.
Breakpoints en condiciones (#5) El parser captura la línea del keyword en IfBranch.ConditionLine / CaseWhenBranch.ConditionLine; DeltaCycleEngine.OnConditionEvaluated(line) dispara cuando una rama if/elsif se toma o un arm case-when coincide; ReplayState.OnLiveConditionEvaluated pausa si hay un breakpoint en esa línea — el camino de transición-de-señal no puede atraparlas porque la condición no cambia ninguna señal.
Reattach a mitad de run (#11) Editor.HydrateWaveformFromLiveEngine() reconstruye _waveformData desde Replay.LiveEngine.History al volver a un sim en vuelo. Abrir un run guardado durante el sim activo activa _liveMergeSuspended (los merges de progreso/fin no pisan la vista guardada); un banner "Volver a la corrida en vivo" rehidrata desde el engine.

Esto cierra el HighlightLines vacío de la fila anterior: ahora el engine sí emite las líneas activas por delta. Ver memoria [kmila-backlog-2026-05-20] items #3/#4/#5/#11.

Detalle completo del polish pass 2026-05-17 (6 tareas, todas con sweep verde de 281+ tests): CHANGELOG_concepts_module_2026-05-17.md.


WaveformViewer — pipeline y arquitectura interactiva (2026-05-22)

La pestaña Waveforms del dock del editor renderiza, por cada señal de la simulación, una traza tipo step-function en SVG inline. El componente cubre tres caminos: visualización en vivo durante un run, snapshot estático tras completar, y modo compare superponiendo múltiples snapshots con paletas distintas. El detalle completo del rediseño 2026-05-22 (cierre de los items #8, #9, #10 del backlog) vive en CHANGELOG_waveform_overhaul_2026-05-22.md.

flowchart TB
    Sim["Interpreter<br/>DeltaCycleEngine"] --> SH["SignalHistory<br/>(transiciones por señal)"]
    SH --> WD["WaveformData<br/>Signals + TotalTicks + TimescaleNs"]
    WD --> Editor["Pages/Editor.razor<br/>WaveformsContent slot"]
    Snap["SimulationSnapshotStore<br/>(SQLite)"] -.snapshots.-> Editor
    Editor --> WV["Components/WaveformViewer.razor"]

    subgraph WV["Components/WaveformViewer.razor"]
        State["Estado<br/>_zoomWidth, _hiddenSignals, _showClock,<br/>_compareTracks, _hoverId"]
        Render["Render branches<br/>single / compare"]
        Build["BuildTraceSvg / BuildCompareTraceSvg / BuildRulerSvg"]
        ExportSvg["HandleExportSubmitAsync → WaveformExporter.BuildSvg"]
        ExportVcd["BuildVcd<br/>(VCD format)"]
        ExportPng["BuildSvg → kmilaSvgToPngBase64 (canvas)"]
        ExportUml["(no aplica — solo Flow tiene UML)"]
        Reset["ZoomReset async<br/>→ kmilaWaveformFitWidth"]
    end

    State --> Render
    Render --> Build
    Build --> SVG["Inline SVG strips<br/>(step / diamond / sticky ruler)"]

    SVG --> Body[".waveform-viewer__body<br/>overflow:auto, position:relative"]

    Body -. data-view-start-ns,<br/>data-view-end-ns,<br/>data-label-width .-> JS["JS: app.js<br/>kmilaAttachWaveformHover<br/>kmilaWaveformFitWidth"]
    JS -. line + chip .-> Body
    JS -. fit width .-> Reset

    Render --> ExportSvg
    Render --> ExportVcd
    Render --> ExportPng
    Render --> ChipBar["Chip-bar de señales ocultas<br/>(sobre el body)"]

    style WV fill:#f3e5f5,stroke:#7b1fa2
    style Body fill:#fff3e0,stroke:#e65100
    style JS fill:#e8f5e9,stroke:#2e7d32
    style Sim fill:#e8f5e9,stroke:#2e7d32
    style WD fill:#e8f5e9,stroke:#2e7d32
    style Snap fill:#fce4ec,stroke:#c62828

Decisiones de diseño clave (post-2026-05-22):

Decisión Razón
Inline SVG en lugar de Chart.js o canvas Cada transición de la SignalHistory produce un paso real; el zoom solo redimensiona el ancho del SVG sin reinicializar pipeline de canvas. Migración heredada de cycles anteriores; documentada en el header del componente.
Pan-zoom DETACHED del waveform body El cuerpo ya tiene overflow:auto. Mantener pan-zoom causaba dos mecanismos de pan simultáneos y, tras agregar el ocultado por señal (#9), las cotas cacheadas de clampPan quedaban obsoletas y el usuario podía arrastrar la traza fuera de pantalla. Schematic y Flow lo siguen usando porque son canvases libres. Ver [project_waveform_no_panzoom].
Sticky chrome bidireccional Columna izquierda con nombres (position: sticky; left: 0) + sombra de profundidad para hacer evidente el pinning; fila inferior del ruler (position: sticky; bottom: 0) para que el eje de tiempo siga visible al scrollear muchas señales.
Click-to-hide en la etiqueta de cada fila Sin chrome extra; ícono de ojo aparece en hover. Una chip-bar arriba del body lista los ocultos para restaurar individualmente o en bulk. Aplica también a exports VCD/SVG/PNG.
Hover cursor JS en lugar de @onmousemove Razor @onmousemove ruta por SignalR en Blazor Server; un cursor que actualiza 60 fps colapsa el circuito. JS lee data-view-* del body en cada frame, sin viajes a C#.
Reset = fit-to-width, no constante 1200 kmilaWaveformFitWidth mide body.clientWidth - 162 y lo asigna a _zoomWidth (clamp 200..2.4M). En paneles anchos coincide con el default; en estrechos efectivamente cabe la traza completa. No auto-fit en mount: probado y revertido — comprime trazas densas a un bloque sólido ilegible.
Techo de zoom 2 400 000 px Tras cuatro pasadas de afinamiento (24k → 96k → 200k → 600k → 2.4M) según feedback del usuario. Permite ~24 px por ciclo en el peor caso (100 MHz × 1 ms = 100k ciclos). Riesgo de stutter en iOS Safari al rendering de SVGs de tamaño extremo — el seguimiento correcto si surge como queja es virtualización de path (emitir solo el rango visible), no bajar el techo.
Panel .kmila-dock__panel--waveforms con overflow:hidden Mismo idiom que el Schematic. Sin esta override, el panel del dock añadía su propio scrollbar vertical apilándose con el del body (el "tercer scrollbar" reportado por el usuario).
Filtrado en export Los tres formatos (VCD, SVG, PNG) honran _hiddenSignals. Para SVG/PNG, se construye un WaveformData clonado superficialmente (los Transitions siguen compartidos por referencia — no hay copia de datos pesados).

Componentes JS asociados (Kmila.Shared/wwwroot/js/app.js):

Función Propósito Notas
kmilaAttachWaveformHover(bodySelector) Inyecta <div class="waveform-hover__line"> + <div class="waveform-hover__chip"> en el body; listeners mousemove / mouseleave / scroll. Lee data-view-start-ns, data-view-end-ns, data-label-width del body en cada update(). Devuelve un id para detach.
kmilaDetachWaveformHover(id) Remueve listeners + nodos DOM creados. Llamado en Dispose() del componente.
kmilaWaveformFitWidth(bodySelector, labelW) Devuelve body.clientWidth - labelW - 2, o -1 si no es medible. Usado por ZoomReset.
kmilaSvgToPngBase64(svgString, scale) Rasteriza un SVG a base64 PNG vía <canvas> + drawImage. Compartido con el resto de exporters; sufrió el fix de tainted-canvas / font-import documentado en CHANGELOG_v1.19.md (item flow PNG).
kmilaInlineSvgStyles(svgEl) Antes de exportar: inline-iza estilos computados y elimina @import, <foreignObject>, xlink:href externos. Mitiga el bug de tainted canvas.
kmilaAttachPanZoom(rootSel, opts) NO usado por WaveformViewer. Sí por SchematicViewer + FlowDiagramViewer. Documentación in-code del recipe contain: inline-size + min-width: 0.

Limitaciones reconocidas y follow-ups:

  1. Performance a 2.4 M px — desktop OK, iOS Safari potencialmente lento. Fix correcto: emisión virtualizada del SVG path al rango visible (viewStartNs..viewEndNs), no bajar el techo.
  2. Quick-filter IN/OUT/INOUT del backlog #9 — diferido; el click-por-fila + chip-bar cubre el uso típico.
  3. Pinch-zoom en touch — sin equivalente al re-adjuntar pan-zoom. Si surge necesidad real (workflow tablet-first), agregar un handler pinch-only dedicado que actualice _zoomWidth.
  4. Time-range zoom (drag-select sobre la traza para zoom directo a un rango) — sería el siguiente paso natural y obviaría la necesidad de techos elevados.

Actualización 2026-09-13 — escalabilidad en corridas largas + lectura de valores

Motivación: en corridas de orden de segundos (p.ej. un display de 7 segmentos que actualiza cada ~50k ciclos) el visor congelaba la app entera. La causa no era el DOM (el dibujo ya estaba ventaneado), sino el costo algorítmico por render sobre el hilo único del circuito Blazor. Cambios (solo visualización; la lógica de simulación queda intacta y la salida VCD/CSV es byte-idéntica en corridas sin overflow):

  • Acceso ventaneado + decimado (Fase 1/2). WaveformSignal.GetWindow(startNs, endNs, maxPoints) (en Models/WaveformData.cs) hace búsqueda binaria (IndexAtOrBefore) del rango visible y devuelve a lo sumo ~1 punto por pixel, preservando primer/último y min/max de cada bucket para que pulsos rápidos (reloj) no desaparezcan. BuildTraceSvg consume esa ventana. Se eliminó el hot-spot que copiaba la lista completa de transiciones (Transitions.ToArray() + Array.IndexOf) por señal-bus en cada render, y el escaneo lineal desde índice 0. Esto cierra el follow-up #1 (emisión virtualizada del path también en el escaneo de datos, no solo el dibujo).
  • Memoización de trazas. TraceSvgCached cachea el SVG por señal salvo cambio de ventana/zoom/ancho/color/#transiciones. Un movimiento de cursor NO reconstruye las trazas — solo los chips de valor —, evitando el render-storm.
  • MergeFrom O(1) por señal. Índice Dictionary<string, WaveformSignal> en vez del FirstOrDefault por nombre (era O(señales²) cada tick de 200 ms).
  • Columna de valor + toggle por señal. Cada fila muestra el valor de la señal en el cursor (WaveformData.ValueAt, binary search, last-write-wins — helper compartido con SchematicViewer en modo Debug) y un toggle onda↔valor; las señales numéricas (integer / bus) arrancan en modo valor. Clases CSS waveform-signal-value, waveform-row__mode-toggle, waveform-row__value-track, waveform-value-readout.
  • Cursor por hover. kmilaAttachWaveformHover(body, dotNetRef) ahora publica el tiempo bajo el puntero (throttled ~60 ms) a WaveformViewer.OnCursorHoverSignalCursorBus. SetCursorTick (antes definido pero sin llamadores). El click en la regla sigue creando breakpoints de tiempo. Claves i18n: WAVEFORM_VALUE_AT_CURSOR, WAVEFORM_SHOW_AS_VALUE, WAVEFORM_SHOW_AS_WAVE.
  • Timestamp long en SignalHistory (Fase 3a). El encode tick*1000+delta desbordaba int pasado ~2.1 M ticks (~21 ms a 10 ns/tick), corrompiendo tiempos de onda/VCD en corridas largas. Se amplió a long solo en la ruta de registro/almacenamiento; la ruta de atributos (SignalAttributeHandler.OnDeltaCommit, 'stable(T)/'last_value) conserva su int — la semántica de simulación no cambia. 167/167 tests verdes.
  • Descartado: volcado a disco de SignalHistory. Un spill transparente exigiría reescribir todas las rutas de lectura/export para ser segment-aware (riesgo a la garantía byte-idéntica de VCD/CSV y al replay) y choca con el watermark append-only de ReadSince. El overflow ya está resuelto con long; el único caso de RAM ilimitada (señal que cambia cada tick por segundos) ya tiene mitigaciones (KMILA_SIM_NO_HISTORY=1, ocultar reloj). No se implementó por no justificar el riesgo.

Actualización 2026-09-13 — buses expandibles por bit + etiqueta legible

Dos mejoras solo de visualización (la lógica de generación queda intacta y la salida VCD/CSV es byte-idéntica; ver project_waveform_viz_only):

  • std_logic_vector colapsable → una onda por bit. El bus se sigue mostrando como una única fila agregada (lane GTKWave con etiqueta hex); un chevron (/) en su etiqueta lo expande en una sub-fila por bit (MSB arriba, etiqueta nombre[n] en convención downto), cada una dibujada reutilizando la rama std_logic de BuildTraceSvg sobre señales sintetizadas ligeras (BitSignalsFor + BitTraceSvgCached, cacheadas por nombre#bit). Para conservar los estados U/Z/X por bit —que el valor numérico agregado colapsa— se añadió un campo display-only WaveformTransition.Bits (string crudo MSB-first) poblado solo para buses en FromSignalHistory (estático) y MergeFrom (vivo). Ese campo no lo lee ninguna ruta de export: Value/Label y el VCD no cambian. Estado de expansión en _expandedVectors (patrón espejo de _hiddenSignals). Bits U/Z/X se renderizan como la traza discontinua "desconocido" ya existente, no como 0. Clases CSS waveform-row__expand-toggle, waveform-row__expand-spacer, waveform-row--bit, waveform-row__bit-name. Claves i18n WAVEFORM_EXPAND_BITS, WAVEFORM_COLLAPSE_BITS.
  • Etiqueta nombre↔tipo sin colisión. El chip de dirección (IN/OUT/INOUT/INTERNAL) iba a 4px del nombre mono, así que un nombre con "in"/"out"/"internal" se leía como parte del tipo. Se añadió un divisor (border-left + padding) a la izquierda del nombre y title con el nombre completo (el nombre se trunca con ellipsis). El modo compare ahora también envuelve chip+nombre+ancho en .waveform-row__label-main para heredar el mismo divisor y la contención de ellipsis (antes carecía de ambos). Solo CSS + markup.

Otros viewers (Schematic, Flow) y la utilidad kmilaAttachPanZoom

Para contexto comparativo:

flowchart LR
    Synth["Interpreter.Synthesis<br/>Netlist + LayeredLayout"] --> SV["Components/SchematicViewer.razor"]
    Flow["Mermaid.js<br/>flowchart compositor"] --> FV["Components/FlowDiagramViewer.razor"]
    WaveData["WaveformData"] --> WVV["Components/WaveformViewer.razor"]

    SV  -. usa .-> PZ["js: kmilaAttachPanZoom<br/>(transform-pan + pinch)"]
    FV  -. usa .-> PZ
    WVV -. NO usa .-> PZ
    WVV -. solo .-> Native["overflow:auto<br/>(scroll nativo)"]

    style SV fill:#f3e5f5,stroke:#7b1fa2
    style FV fill:#f3e5f5,stroke:#7b1fa2
    style WVV fill:#f3e5f5,stroke:#7b1fa2
    style PZ fill:#e8f5e9,stroke:#2e7d32
    style Native fill:#fff3e0,stroke:#e65100
Viewer Estructura Gestos Razón
SchematicViewer Free-form (gates + wires en posiciones X/Y arbitrarias) Pan + pinch (kmilaAttachPanZoom) Sin un layout rectilíneo, scroll nativo no es suficiente: el usuario quiere centrar la vista, hacer zoom contextual, perseguir un wire largo.
FlowDiagramViewer Free-form (Mermaid flowchart) Pan + pinch (kmilaAttachPanZoom) Misma razón — el grafo emitido por Mermaid puede ser arbitrariamente ancho/alto y no se beneficia de scroll lineal.
WaveformViewer Row-structured (filas alineadas por tiempo) Scroll nativo + botones toolbar Cada fila tiene la misma altura y comparte el eje X. Pan 2D libre no aporta sobre scroll; toolbar Zoom-In/Out/Reset cubre el cambio de relación de aspecto horizontal.

Diagramas detallados por módulo (2026-05-22)

Esta sección expande el diagrama de alto nivel mostrando los servicios principales de cada módulo y el flujo de datos entre ellos a nivel de clase. Pensada para auditoría — los detalles más granulares (cada handler, helper, etc.) viven en los README.md de cada módulo.

Parser

flowchart LR
    Source["Fuente VHDL<br/>(string)"] --> Tk["Tokenizer<br/>+ TokenizeLineAt (v1.15)"]
    Tk --> Tokens["Token[]<br/>(con índice de línea original)"]
    Tokens --> PP["PortsParser<br/>(v1.17: Levenshtein typo-suggest;<br/>v1.15: catch missing semicolons)"]
    Tokens --> EP["EntityParser"]
    Tokens --> AP["ArchitectureParser<br/>(v1.15: procedure decls,<br/>direct entity instantiation,<br/>arithmetic generic defaults)"]
    Tokens --> ImpP["ImportParser<br/>(library / use)"]
    AP --> AST["AST<br/>(Architecture, Process,<br/>ConcurrentStatement, ...)"]
    PP --> AST
    EP --> AST
    ImpP --> AST
    PP -.diagnostics.-> DL["SkipDiagnosticLog<br/>(códigos VHD-*)"]
    AP -.diagnostics.-> DL
    EP -.diagnostics.-> DL

    style Tk fill:#e8f5e9,stroke:#2e7d32
    style PP fill:#e8f5e9,stroke:#2e7d32
    style EP fill:#e8f5e9,stroke:#2e7d32
    style AP fill:#e8f5e9,stroke:#2e7d32
    style ImpP fill:#e8f5e9,stroke:#2e7d32
    style AST fill:#fff3e0,stroke:#e65100
    style DL fill:#fce4ec,stroke:#c62828

Surfaces: Parser/Models/AST.cs, Parser/Services/Tokenizer.cs, Parser/Services/PortsParser.cs, Parser/Services/ArchitectureParser.cs, Parser/Services/EntityParser.cs, Parser/Services/ImportParser.cs. Parser/Program.cs corre la suite de 40 tests y reporta exit-code para CI.

Interpreter — runtime + delta-cycle

flowchart TB
    AST["AST del Parser"] --> Ar["Architecture<br/>(handler central)"]
    Pkg["PackageRegistry<br/>(singleton)"] --> Ar
    Ent["EntityRegistry<br/>(singleton)"] --> Ar
    Fn["FunctionRegistry<br/>(v1.15)"] --> Ar
    IEEEL["IEEELibraryLoader<br/>(v1.15 #20, delegated to LibraryCompiler en v1.16 #22-#26)"] --> Pkg

    Ar --> SR["SimulationRunner<br/>(coordinador)"]
    SR --> DCE["DeltaCycleEngine<br/>(IDisposable)"]
    DCE --> PS["ProcessScheduler<br/>(activación por sensitivity list)"]
    DCE --> SS["SignalScheduler<br/>(after clause, signal queue)"]
    DCE --> SAH["SignalAttributeHandler<br/>('event / 'last_value /<br/>'stable / 'active)"]
    DCE --> SH["SignalHistory<br/>(transiciones por señal)"]
    DCE --> SDL["SkipDiagnosticLog<br/>(VHD-* per omisión)"]

    SH --> WD["WaveformData.FromSignalHistory"]
    SH --> VCD["VCD / CSV / Text export<br/>(en Interpreter; el frontend wrapper<br/>está en WaveformViewer.BuildVcd)"]

    DCE --> EV["Eventos live-replay<br/>OnFrameEmitted (delta + líneas)<br/>OnConditionEvaluated (if/elsif/when, #5)<br/>Pause/Resume/StepOneDelta"]
    EV --> RS["Kmila.Shared/ReplayState<br/>(breakpoints, schematic step-through)"]

    style Ar fill:#e8f5e9,stroke:#2e7d32
    style DCE fill:#e8f5e9,stroke:#2e7d32
    style PS fill:#e8f5e9,stroke:#2e7d32
    style SS fill:#e8f5e9,stroke:#2e7d32
    style SAH fill:#e8f5e9,stroke:#2e7d32
    style SH fill:#fff3e0,stroke:#e65100
    style WD fill:#fff3e0,stroke:#e65100

Tests: el Interpreter tiene una suite xUnit en Interpreter/Tests/ (correr con dotnet test), que al cierre del arco Tier 4.3 (2026-07-30) estaba en ~167 casos. El subconjunto de la pipeline IEEE/síntesis lo forman DeltaCycleEngineTests + SynthesisTests + IEEELibraryLoaderTests + LibraryCompilerTests + IEEEPrimitivesTests + IEEELoweringEndToEndTests + NestedIEEECallTests. Además existe el corpus de smoke test*.vhdl (54 fixtures) que corre Program.cs. Detalle y estado por superficie en ../Interpreter/README.md §7. (La cifra "82 cases / --test-delta" de revisiones previas era el subconjunto IEEE de v1.19.)

Modos de ejecución: Visual vs Fast + fix de timestamp >21 ms (2026-09-13)

Motivación: corridas de segundos de tiempo simulado (≥1 s a 10 ns/tick = 10⁸ ticks) tardaban demasiado en el Editor. Se añadió un selector Fast/Visual en el Run-bar (SimulationParameters.FastMode, toggle RunBar → leído por Editor.OnRunRequested):

  • Visual (por defecto): el SimulationRunner de referencia (bucle tick++, visita cada tick) + forma de onda en vivo (timer de 200 ms en ProgramBuilder) + replay por delta (ReplayState.AttachLive → breakpoints/step/línea-activa). Sin cambios de comportamiento.

  • Fast: dos aceleraciones combinadas, solo visualización/orquestación (la salida de simulación es idéntica):

    1. Menos efectos visuales: ProgramBuilder.RunSimulationAsync(fastRun:true) omite el timer de 200 ms (no hay merge de historial ni redibujo de SVG por-frame) y Editor omite Replay.AttachLive. Queda una barra de progreso barata (OnProgressChanged); la forma de onda completa se construye una vez al final (OnSimulationDataReadyFromSignalHistory). Pausa/Stop siguen funcionando (van por el runner, no por ReplayState); breakpoints/step no.
    2. Motor "duplicado" dirigido por eventos: nuevo Interpreter/Services/FastSimulationRunner.cs — hermano del SimulationRunner que conduce el mismo DeltaCycleEngine, pero salta los ticks ociosos: avanza a min( próximo flanco de reloj, próximo estímulo, SignalScheduler.NextScheduledTick, DeltaCycleEngine.NextProcessWakeTick, sello final ). Es correcto porque ambas colas del motor drenan todo lo vencido en <= currentTick (SignalScheduler.AdvanceTime, DrainReadyProcessWakesAsync), y un tick ocioso no produce delta → ni cambio de historial ni de señal. El motor y SimulationRunner quedan intactos; la única adición al motor es el getter de solo-lectura DeltaCycleEngine.NextProcessWakeTick (espejo de NextScheduledTick, sin comportamiento). Ambos runners implementan ISimulationRunner para que ProgramBuilder elija uno en runtime.

    Equivalencia byte-idéntica verificada por FastRunnerEquivalenceTests (VCD de Fast == VCD de referencia para reloj, flip-flop con flanco, y wait for). Sonda de velocidad: 50 000 ticks con reloj lento (1 MHz @ 10 ns) → referencia ~97 ms vs Fast ~7 ms (~14×), salida idéntica. Nota: si el reloj conmuta cada tick (p.ej. 50 MHz @ 10 ns), no hay ticks ociosos que saltar y la ganancia de Fast es solo la de "menos efectos visuales"; para bajar el piso de 10⁸ ticks ahí, la palanca es una resolución de tick más gruesa (cambia granularidad, aparte). Pendiente menor: persistir el modo entre sesiones (hoy es estado de sesión).

  • Fix de timestamp intlong (>21 ms), correctness — autorizado por el usuario. El sello de tiempo del camino de atributos seguía en int ((int)(_currentTick*1000)), que desborda Int32.MaxValue pasado tick ≈ 2.147×10⁶ (~21.5 ms a 10 ns/tick), corrompiendo 'stable/'last_value/'event/'delayed y los timestamps de transición de clock/estímulo. Se ensanchó a long: SignalAttributeHandler._lastChangeMoments / _currentTimeMoment / OnDeltaCommit / StampStimulusChanges / GetStable, el timeMoment en DeltaCycleEngine.RunDeltaCycleAsync, y los dos casts (int)(tick*1000) en SimulationRunner. Cambia la salida solo para corridas >21 ms (de incorrecta a correcta); <21 ms queda idéntica. Suite: 170/170 verde (167 previos + 3 de equivalencia).

  • ETA de ejecución + log de rendimiento (2026-09-13). SimulationParameters estima el tiempo restante en vivo (RemainingSeconds/ElapsedSeconds) desde el ritmo observado de progreso (remaining = elapsedActivo × (100−%)/%, excluyendo el tiempo en pausa; se abstiene hasta ≥1 % y ≥0.3 s para no mostrar números disparatados). El Run-bar lo muestra bajo la barra de progreso ("~5s restante" / "estimando…" / "en pausa"). No hay predicción previa fiable (el costo por tick varía por diseño), así que se calibra sobre la marcha. ProgramBuilder emite además una línea [Sim.Perf] a /logs (mode, elapsed, totalTicks, y en Fast visitedTicks

    • % de ticks ejecutados) para diagnosticar por qué una corrida fue lenta. Tests en SimulationEtaTests; suite 172/172.

Interpreter — synthesis path (modo "FPGA visualization")

flowchart LR
    AST["AST del Parser"] --> Synth["Synthesizer<br/>(orquestador)"]
    Synth --> CAS["ConcurrentAssignSynthesizer<br/>(v1.19: nested DF recursion)"]
    Synth --> PrS["ProcessSynthesizer"]
    Synth --> IPL["IEEEPrimitives.TryLower<br/>(unsigned/signed/to_integer/<br/>resize/conv_std_logic_vector → cell)"]

    CAS --> NL["Netlist<br/>(cells + nets + ports)"]
    PrS --> NL
    IPL --> NL

    NL --> LL["LayeredLayout<br/>(posiciona cells)"]
    LL --> SVTab[".kmila-dock--schematic<br/>(Components/SchematicViewer.razor)"]

    style Synth fill:#e8f5e9,stroke:#2e7d32
    style CAS fill:#e8f5e9,stroke:#2e7d32
    style PrS fill:#e8f5e9,stroke:#2e7d32
    style IPL fill:#e8f5e9,stroke:#2e7d32
    style NL fill:#fff3e0,stroke:#e65100
    style LL fill:#fff3e0,stroke:#e65100
    style SVTab fill:#f3e5f5,stroke:#7b1fa2

ConcurrentAssignSynthesizer.ResolveDeferredArgs gana en v1.19 (#29) el case DeferredFunction nested que recursivamente baja llamadas IEEE anidadas (to_integer(unsigned(addr))) a un grafo de cells correctamente cableado.

Debugger

flowchart LR
    UI["UI (Editor.razor)<br/>RunBar → ProgramBuilder"] --> Dbg["Debugger.Run<br/>(orquestador)"]
    Dbg --> P["Parser.ParseAsync"]
    P --> Dbg
    Dbg --> Diag["RunDiagnostics<br/>(propaga aún si el parse falla)"]
    Diag --> SDL2["SkipDiagnosticLog<br/>+ Diagnostics<br/>(devueltos al UI)"]
    Dbg --> I["Interpreter.SimulationRunner"]
    I --> SH2["SignalHistory + WaveformData"]
    SH2 --> Dbg
    Dbg --> UI2["UI (Waveforms, Schematic,<br/>Output, Replay)"]

    style Dbg fill:#e8f5e9,stroke:#2e7d32
    style Diag fill:#e8f5e9,stroke:#2e7d32
    style UI fill:#e3f2fd,stroke:#1565c0
    style UI2 fill:#e3f2fd,stroke:#1565c0

Debugger.RunDiagnostics es el contrato que hace que la pestaña Output del dock siempre tenga contenido útil, aún cuando un Parser exception tumbó el árbol estructural.

LibraryCompiler

flowchart TB
    UI["UI / Editor"] --> LCF["LibraryCompiler<br/>(Facade)"]
    LCF --> PCx["ProjectContext"]
    LCF --> Res["LibraryResolver<br/>(topo-sort de deps)"]
    LCF --> Orch["BuildOrchestrator"]

    Orch --> Lin["LinearScheduler"]
    Orch --> Par["ParallelScheduler"]
    Orch --> CacheStrat{"Strategy:<br/>OnDemand /<br/>Cached /<br/>Persistent"}

    Orch --> Bld["LibraryBuilder<br/>(una librería)"]
    Bld --> CL["CompiledLibrary"]
    Bld -.usa.-> Stubs["Builtins/IEEE/<br/>std_logic_1164.vhd<br/>numeric_std.vhd<br/>(embedded)"]

    CacheStrat -. persiste .-> IBS["IBlobStore<br/>(contrato; concretos en host)"]
    CL --> FR["FunctionRegistry<br/>(sembrado por LCF)"]

    style LCF fill:#e8f5e9,stroke:#2e7d32
    style Orch fill:#e8f5e9,stroke:#2e7d32
    style Bld fill:#e8f5e9,stroke:#2e7d32
    style CL fill:#fff3e0,stroke:#e65100
    style FR fill:#fff3e0,stroke:#e65100

Detalles: IEEE_PIPELINE_GUIDE.md y CHANGELOG_v1.16.md.

Export pipeline (cross-cutting)

flowchart LR
    Sim["WaveformData /<br/>Netlist /<br/>FlowAST"] --> Exporters{Tipo de export}

    Exporters --> Vcd["BuildVcd<br/>(en WaveformViewer)<br/>→ texto VCD"]
    Exporters --> WaveSvg["WaveformExporter.BuildSvg<br/>+ kmilaSvgToPngBase64<br/>→ SVG / PNG"]
    Exporters --> SchSvg["SchematicViewer.BuildSvg<br/>+ canvas raster<br/>→ SVG / PNG"]
    Exporters --> FlowSvg["FlowDiagramViewer.BuildSvg<br/>+ kmilaInlineSvgStyles<br/>+ canvas raster<br/>→ SVG / PNG"]
    Exporters --> FlowUml["PlantUmlFlowEmitter<br/>(v1.19, backlog #1)<br/>→ .puml texto"]

    Vcd --> Save["IFileExportService.SaveTextAsync /<br/>SaveBytesAsync"]
    WaveSvg --> Save
    SchSvg --> Save
    FlowSvg --> Save
    FlowUml --> Save
    Save --> Disk["Documents/Kmila/<br/>SVG/ PNG/ VCD/ UML/"]

    style Exporters fill:#fff3e0,stroke:#e65100
    style Save fill:#e3f2fd,stroke:#1565c0
    style Disk fill:#fce4ec,stroke:#c62828

Notas operativas:

  • El 64 MB cap del SignalR de Blazor Server (vide [project_blazor_signalr_message_size]) limita el tamaño de los bytes que pueden viajar JSInterop → C# en un solo round-trip. Diagramas extremadamente grandes (10k+ nodos de Mermaid Flow) podrían requerir un trigger directo en JS (Blob+anchor) en vez del round-trip actual.
  • kmilaInlineSvgStyles es el cierre del bug de tainted-canvas reportado en v1.19 (CHANGELOG entry "Flow PNG / SVG export broken"). Sanitiza @import, <foreignObject>, <image> externo, xlink:href antes del drawImage que rasteriza.

UI surface — pestañas del Editor dock

flowchart LR
    Editor["Pages/Editor.razor"] --> ED["EditorDock.razor"]
    ED --> Tabs{"DockState.Active"}

    Tabs -- Entities --> Tab1["Lista de entidades parseadas"]
    Tabs -- Schematic --> Tab2["SchematicViewer<br/>(pan-zoom JS)"]
    Tabs -- Flow --> Tab3["FlowDiagramViewer<br/>(pan-zoom JS)"]
    Tabs -- Ports --> Tab4["PortsPanel<br/>(estímulos por puerto)"]
    Tabs -- Waveforms --> Tab5["WaveformViewer<br/>(scroll nativo + hover)"]
    Tabs -- Runs --> Tab6["RunsHistoryPanel<br/>(SQLite snapshots)"]
    Tabs -- Files --> Tab7["FilesCard"]
    Tabs -- Output --> Tab8["BuildLog + diagnostics"]
    Tabs -- Replay --> Tab9["StepReplayPanel<br/>→ ExecutionPlayer"]

    style Tab2 fill:#f3e5f5,stroke:#7b1fa2
    style Tab3 fill:#f3e5f5,stroke:#7b1fa2
    style Tab5 fill:#f3e5f5,stroke:#7b1fa2
    style Tab9 fill:#f3e5f5,stroke:#7b1fa2

Cada pestaña es un RenderFragment named param de EditorDock. La pestaña activa se persiste por proyecto en EditorDockState + localStorage. La pestaña Replay apareció en el polish pass 2026-05-17; Flow apareció con el export UML 2026-05-21 (vide [project_flow_uml_export]).


Visual Runtime — canvas-driven infinite-run mode (Phase 0, 2026-06-24; Phase 3+ post-2026-06-26)

State as of 2026-07-24: los bloques de Fase 0–2 documentados abajo son la base; el runtime hoy tiene el conjunto completo de 12 widgets + bit-slice bindings + bounce + 4 example gallery cases (Fase 3), plus canvas record + export + locator + pan-wall (Fase 3+). Ver VISUAL_MODE.md §10-12 para el estado actual de los widgets y CHANGELOG.md "Visual Runtime — full set + slices …" para el detalle de shipping.

Modo de simulación complementario al bounded run: vincula los puertos del diseño a componentes virtuales (LED, botón) en un canvas y deja al estudiante interactuar. Reutiliza al 100% el motor de simulación existente — no hay un segundo DeltaCycleEngine.

flowchart LR
    UI["Pages/Visual.razor"] --> Coord["VisualRunCoordinator"]
    Coord --> Engine["DeltaCycleEngine<br/>(reused)"]
    Engine --> Frame["OnFrameEmitted"]
    Frame --> Ring["FrameRing<br/>(2000 ticks, bounded)"]
    Frame --> Coord

    Btn["ButtonWidget"] -- pointerdown/up --> Map["InputStateMap"]
    Coord -- "drain between ticks" --> Map
    Map -- "Variable.Value=" --> Engine
    Engine -- "Variable.OnValueChange" --> Handler["SignalAttributeHandler"]
    Handler -- "_activeFlags[name]=true" --> Engine

    Coord --> Ring
    Ring --> Led["LedWidget<br/>(reads latest port value)"]

    Persist["KvizSerializer (.kviz JSON)"] <--> UI

    style Engine fill:#fff3e0,stroke:#e65100
    style Handler fill:#fff3e0,stroke:#e65100
    style Coord fill:#e3f2fd,stroke:#0d47a1
    style Map fill:#e3f2fd,stroke:#0d47a1
    style Ring fill:#e3f2fd,stroke:#0d47a1

Piezas en naranja = preexistentes en el simulator core (no se tocan). Piezas en azul = nuevas en Kmila.Shared/Services/VisualRun/ y Kmila.Shared/Components/VisualRun/.

El seam crítico es el ciclo coordinator.LoopAsync → escribe puertos → engine.AdvanceTime(tick)engine.RunTimeStepAsync() → snapshot. Para detalles del contrato de cada componente, los puertos soportados, el schema .kviz y el roadmap fase 1+, ver VISUAL_MODE.md. El plan original y las decisiones bloqueadas viven en HANDOFF_visual_runtime_2026-06-24.md.

Fase 1A/B (2026-06-25): dos componentes nuevos (SwitchWidget, LedArrayWidget) y un auto-clock dentro del coordinador. Cualquier puerto IN cuyo nombre lea como reloj (clk, clock, aclk, …) se toggea cada CLOCK_HALF_PERIOD_TICKS = 8 ticks — salvo que el usuario haya cableado un Switch/Button a ese puerto. Visual.razor recolecta los puertos manejados por el usuario y los pasa como userDrivenPorts al constructor del coordinador, que los excluye de su lista interna _clockPorts. Esto permite que la counter4 fixture cuente sin estímulo externo, mientras que un diseño con clk manual sigue funcionando.

Fase 2 (2026-06-25): se cierran los cinco entregables de HANDOFF §8 en una sola sesión:

  1. Speed slider + custom ms/tick — el coordinador acepta un override CustomTickDelayMs: int?; cuando es null cae al preset Speed.TickDelayMs(). La página mapea un <input type="range"> 0..3 al enum VisualSpeed y un <input type="number"> al override.
  2. Pause / Continuecoordinator.Pause() / Resume() con un loop que duerme 20 ms entre chequeos mientras IsPaused.
  3. Tick breakpointsTickBreakpoints: HashSet<ulong> consultado al inicio de cada tick; al match el loop entra en pausa, dispara OnBreakpointHit(tick) y se arma el sentinel _lastBreakTick para que Resume() no vuelva a romper en el mismo tick.
  4. Frame scrubbingFrameRing.Nearest(tick) busca el frame más cercano dentro de la ventana del anillo. Mientras está en pausa, Pages/Visual.razor.ApplyScrubFrame() reescribe port.Value para cada puerto del entity desde frame.PortValues; los widgets re-leen y redibujan. Resume() deja que el siguiente tick sobrescriba todo.
  5. StepperWidget — cuarto widget interactivo. Decodifica 4 pines phaseA/B/Ān/B̄n contra la tabla canónica de 8 patrones Gray; calcula el delta WrappedDelta(prevIdx, newIdx) para acumular pasos con wrap-around correcto. El rotor (transform: rotate(...)) gira 360 / stepsPerRev por paso.

A nivel de persistencia se agrega el setting KMILA_VISUAL_RING_BUFFER (256..16384, default 2000) en AppSettings. Visual.razor lo lee en OnInitialized y toma Math.Max(perDoc, settings) para dimensionar el FrameRing del coordinador.

7-seg — modos de decodificación + polaridad (2026-09-14): el banco de 7 segmentos (SevenSegmentBankWidget) gana el mismo mode que el dígito suelto — raw (bus empacado de segmentos, ancho digits×7), hex y dec (bus empacado de nibbles, ancho digits×4, decodificado internamente). Ambos widgets ganan además una config polarity: cathode (default, activo-alto, comportamiento previo) o anode (activo-bajo, invierte el bit leído). La polaridad aplica solo en modo raw; en modos decodificados el widget elige qué segmentos encienden. La tabla de patrones y el decode se extraen a Services/VisualRun/SevenSegPatterns.cs (reuso entre ambos widgets). Es un cambio solo de visualización: no toca el motor ni la salida de simulación, y los .kviz existentes se renderizan igual (defaults raw/cathode). Sin cambios de schema (Config es un diccionario abierto) ni de pines (KvizWidgetCatalog). Contratos por widget en VISUAL_MODE.md §5.

Buzzer — corte de audio al detener (2026-09-14): el BuzzerWidget emitía el tono desde OnAfterRenderAsync según los valores de puerto, que se congelan al detener o pausar la simulación (incluida la auto-pausa por cambio de pestaña) — el oscilador de Web Audio seguía sonando tras parar. Corregido atando el estado audible al ciclo de ejecución: IsActive ahora exige Coordinator { IsRunning: true, IsPaused: false } (CoordinatorActive), de modo que el siguiente render tras StopRun/Pause empuja silencio. Además VisualPanel llama a kmilaAudio.stopAll() (nuevo en kmila-audio.js) al detener y al ocultar la pestaña, como corte inmediato. Solo visualización/audio; sin cambios en el motor.

7-seg bank — modo multiplexado (2026-09-14): el banco gana un cuarto mode = "mux" que modela un display multiplexado real: todos los dígitos comparten las líneas de segmento (a..g) y cada dígito tiene su propia línea común / de selección (ánodo/cátodo) que el diseño estrobea de a uno. El wiring (config, solo mux) ofrece ambos estilos de cableado pedidos por el usuario: packed (dos vectores seg + sel, orden asumido, compacto) o explicit (pines individuales a..g + an0..an{digits-1}, cada uno cableable por separado). selectActiveLow (default true) fija si el dígito se habilita con 0 (convención an de las tarjetas) o con 1. Como a velocidad de simulación solo un dígito está seleccionado por tick, con persistence (default true) el widget retiene por dígito el último patrón visto mientras su selección estuvo activa, recorriendo hacia atrás el FrameRing (VisualRunCoordinator.Frames) — sin estado nuevo en el motor ni cambios en DeltaCycleEngine. polarity ahora aplica también a los segmentos en mux. KvizWidgetCatalog.PinsOf se vuelve dependiente de modo/wiring para el banco, y OnSevenSegBankModeChange/DigitsChange/WiringChange llaman a Sanitize para podar bindings obsoletos al cambiar el conjunto de pines. Sigue siendo solo visualización; los .kviz previos se renderizan igual (default raw).


Última actualización: 2 de mayo de 2026 (datos de módulos); 9 de mayo de 2026 (auditoría de diagramas); 17 de mayo de 2026 (polish pass + step-replay); 22 de mayo de 2026 (overhaul de WaveformViewer + diagramas detallados por módulo); 24 de junio de 2026 (Visual Runtime Phase 0 seam); 25 de junio de 2026 (Phase 1A/B Switch+LED array + Phase 2 speed/scrub/bp/stepper); 13 de septiembre de 2026 (WaveformViewer: ventaneo+decimación por zoom, columna de valor + toggle por señal, cursor por hover, timestamp long en SignalHistory); 14 de septiembre de 2026 (7-seg bank: modos raw/hex/dec + polaridad cátodo/ánodo en ambos widgets de 7 segmentos; modo multiplexado mux con cableado packed/explicit, nivel de selección y persistencia de visión; fix del buzzer que seguía sonando tras detener/ocultar la pestaña).