Simulador de diagramas de blocos estilo Simulink, rodando 100% no browser (SPA React, deploy em Vercel: https://tinysim.vercel.app). O usuário arrasta blocos para um canvas, conecta-os com curvas Bezier, configura parâmetros e executa uma simulação de tempo contínuo. O resultado pode ser exportado como código C compilável.
- React 18.3.1 (JSX puro, sem TypeScript)
- Vite 6 + @vitejs/plugin-react-swc
@projectstorm/react-diagramsv7 — engine de canvas/nodes/linkschart.js— blocos de plot/histogramacontrol-systems-js,jszip,dompurify,react-toastify- Sem gerenciador de estado global (Redux/Zustand) —
useStatelocal + alguns singletons ad-hoc (Simulation,useModal,sessionStorage) - Vitest (
npm test/npm run test:watch), ambientehappy-dom(necessário porque@projectstorm/react-diagramsespera globais de browser mesmo só sendo importado) — adicionado nesta fase de refatoração, era zero antes.
src/
App.jsx, main.jsx # raiz da aplicação
components/ # UI: menubar, sidebar, librarybar, modal, zoom...
elements/ # ~55 blocos (Constant, Add, Integrator, PID, Plot...)
complements/ # widgets de visualização (LineChart, Gauge, Histogram)
nodes/ # motor de diagrama
nodeModel.jsx # classe base de nó
nodes/simNodeModel.jsx # SimNodeModel (toda lógica de bloco herda daqui)
ports/ # portas Bezier
links/bezierLinks.jsx # roteamento ortogonal (estilo Simulink) dos links;
# nome "Bezier" é histórico, não desenha mais curvas
selection/mouse.jsx # seleção múltipla
stack/stack.jsx # undo/redo (existe, mas DESATIVADO)
simulation/core.jsx # engine de simulação (loop de tempo, integrationMethods.jsx)
codeGeneration/ # gera projeto C a partir do diagrama
cmodels/ # um cmodel_*.jsx por tipo de bloco (espelha elements/)
template/ # templates .c/.h
public/samples/ # exemplos .tsim prontos
SimulationEngine(src/simulation/core.jsx) mantémcurrentTime,stepSize,stopTimee itera os nós marcadosisTerminalBlock(saídas), que resolvem recursivamente suas entradas vianode.solve()→solution().- Métodos de integração disponíveis: Euler (RK1), Heun (RK2) e Runge-Kutta 4,
implementados em
src/simulation/integrationMethods.jsx(integrateLinearODE) e usados pelos blocos com estado (Integrator, FirstOrder). - Cache por passo (
lastStepSolved) evita recálculo, desativável viastatelessMode. - Cada bloco em
src/elements/*.jsxestendeSimNodeModele implementa sua própriasolution(), ícone e painel de configuração — não há abstração compartilhada para os padrões repetidos (ex: operações binárias).
Desenvolvimento ativo, mas em estágio "beta funcional, sem rede de segurança".
Commits recentes corrigiram bugs visuais nas conexões Bezier (fix bezier port, fix bug nodes is scapping) — sinal de que a camada de
nodes/links/ports ainda é frágil.
- Zero testes — qualquer refator é às escuras. Maior risco do projeto, já que é um simulador (resultado numérico errado é silencioso).
- Sem TypeScript / tipagem —
SimNodeModel, portas e o engine de simulação têm contratos implícitos (quais campos um bloco "tem que" ter) que só existem na cabeça de quem escreveu. simulation/core.jsxmonolítico (224 linhas) — mistura setup, loop de tempo, modo realtime e reset numa única classe.- Duplicação massiva em
elements/— ~20 blocos matemáticos binários (add/sub/multiply/divide/pow/mod...) repetem o mesmo boilerplate de criação de portas e estrutura. Mesmo padrão duplicado emcodeGeneration/cmodels/(um arquivo por bloco, pouca reutilização). - Undo/redo desativado (
nodes/stack/stack.jsxexiste mas foi desligado no commit00871ea) — feature incompleta largada pela metade. - Estado global desorganizado — mistura de singleton de classe
(
Simulation), objeto global (useModal),sessionStorage(clipboard) euseStateespalhado pelos componentes, sem padrão único. - Validação fraca — checagem de ciclos algébricos feita na Fase 4
(
algebraicLoop.jsx); divisão por zero ainda não tratada, import de CSV ainda sem validação de tipos/schema. - Subsistemas/hierarquia implementados na Fase 4 (
SubsystemModel); ainda sem geração de código C para eles e sem detecção de loop algébrico atravessando a fronteira do subsistema (ver detalhes na Fase 4).
- Extrair uma classe genérica para os blocos matemáticos variádicos
(add/sub/multiply/divide/mod): criada
VariadicMathModel(src/elements/variadicMathModel.jsx). Ela concentra criação de portas, o loop de redução (combine()) e a UI de "Add port"; cada bloco agora só declara metadata (identity,seedFromFirstInput,combine(), ícone). Os 5 arquivos emelements/caíram de ~50-70 linhas cada para ~25-30. - Mesma consolidação no code generation: criada
makeArrayReduceCModel(src/codeGeneration/cmodels/_arrayReduceCModel.jsx), uma factory para cmodels que compilam para uma função C(const double* array, int size).cmodel_add/sub/multiply/divide/mod.jsxagora só passam nome da função e corpo em C; o boilerplate de registrar libs/vars/steps ficou centralizado.cmodel_modfoi convertido do formato pairwise antigo (mod_operation(a,b)) para o mesmo formato array/size dos demais — sem mudança de comportamento (mesma matemática, NaN em divisão por zero), apenas C gerado mais consistente entre blocos.- Ainda não cobertos por essa abstração: pow/log/exp/sqrt são
aridade fixa (1-2 entradas, sem "Add port") — padrão diferente, menor
ganho de duplicação. Candidatos a uma segunda abstração
(
FixedArityMathModel) numa próxima rodada, mas não bloqueiam o resto.
- Ainda não cobertos por essa abstração: pow/log/exp/sqrt são
aridade fixa (1-2 entradas, sem "Add port") — padrão diferente, menor
ganho de duplicação. Candidatos a uma segunda abstração
(
- Bug pequeno encontrado e corrigido durante a Fase 1:
getCurrentTime()estava duplicado emsimulation/core.jsx(warning de build), e havia um terceiro métodogetTime()idêntico e não usado em nenhum lugar do código. Os dois métodos redundantes foram removidos. - Configurado Vitest (
npm test/npm run test:watch) — primeira ferramenta de teste do projeto. Foi necessário subirvitede 6.0.3 para 6.4.3 (mesmo range^6dopackage.json, sem mudar a versão major) porque vitest 4.1.9 não inicializava contra ovite@6.0.3exato (erro interno noModuleRunner). - Escrita uma suíte de regressão para
SimulationEngine(src/simulation/core.test.jsx, 9 testes) antes de tocar na estrutura interna — cobrerunSetup,runStep(caminho feliz e quando um nó lança erro),runStandard,resetSimulatione os cálculos degetTotalTimeArray/getTimeArray. Serviu de rede de segurança para o item abaixo. - Quebrado
simulation/core.jsxem módulos menores, com a API pública da classeSimulationEnginepreservada (mesmos métodos, mesma assinatura, mesmo singleton exportado por padrão — a classe agora também é exportada nomeada para permitir testes com instâncias isoladas): -setup.jsx—setupSimulation(model): valida o modelo e extrai os nós. -runStep.jsx—solveTerminalNodes(...): resolve os blocos terminais de um passo. -runModes.jsx—runStandardLoop/runRealTimeLoop: os dois laços de execução. -reset.jsx—resetNodes(...): reseta os blocos com estado interno. -timeArrays.jsx—buildTotalTimeArray/buildTimeArray: funções puras já usadas pelos testes.core.jsxficou como um orquestrador fino que mantém o estado (currentStep,currentTime, flags) e delega a lógica para esses módulos. - Definir contrato explícito de bloco (interface que todo
SimNodeModeldeve cumprir) e documentar em um único lugar. - Unificar gerenciamento de estado global (escolher um padrão: Context, Zustand, ou ao menos consolidar os singletons existentes em um só lugar coerente).
- Avaliar migração incremental para TypeScript (ao menos
nodeModel,simNodeModelesimulation/coreprimeiro, por serem o núcleo). - Trocado, nos 48 arquivos restantes de
src/elements/*.jsx, o import{ SimNodeModel } from '../nodes/nodeModel'pelo import direto de../nodes/nodes/simNodeModel(troca mecânica, mesma import shape em todos os arquivos). Isso elimina o ciclonodeModel.jsx → elements/index.jsx → <qualquer bloco> → nodeModel.jsxpara todo o diretórioelements/— qualquer bloco agora pode ser importado isoladamente (testes, Storybook, etc.) sem depender da ordem de carregamento.App.jsxecomponents/dropElement.jsxcontinuam importando denodeModel.jsxnormalmente, pois eles de fato precisam doEngine/Model, não só doSimNodeModel.- Teste de regressão dedicado em
src/elements/isolatedImport.test.jsx: importa 5 blocos (Average,Clock,Comparator,Text,Switch) como os primeiros imports do arquivo, reproduzindo exatamente o cenário que quebrava antes (módulo cego, semApp.jsxcarregado primeiro).
- Teste de regressão dedicado em
- Regressão encontrada e corrigida: a correção acima não cobria uma
segunda borda do mesmo ciclo —
bezierLinks.jsxeselection/mouse.jsximportavam{ Engine } from '../nodeModel', enodeModel.jsximportaelements/index.jsx(todos os blocos), que por sua vez chega emsimNodeModel.jsx→bezierPortModel.jsx→bezierLinks.jsx→ de volta anodeModel.jsx, ainda no meio da definição da classeSimNodeModel. Isso quebrava 5 suítes de teste (gain,integrator,PIDcontroller,variadicMathModel,isolatedImport— 0 testes executados,TypeError: Class extends value undefined) sempre que um bloco era importado sem passar primeiro porApp.jsx. Corrigido extraindo a criação doEnginepara um módulo-folha novo,src/nodes/engine.jsx(sem dependência deelements/index.jsx);nodeModel.jsx,bezierLinks.jsxeselection/mouse.jsxagora importamEnginedesse módulo.npm testvoltou a 39/39 (7/7 suítes) enpm run buildcontinua limpo.
Validação na época:
npm test(41/41) enpm run buildpassavam limpos (único warning restante é de uma dependência de terceiros,@projectstorm/react-diagrams-routing, não relacionado ao código do projeto). Os testes doSimulationEngineforam escritos contra o comportamento antigo antes do split em módulos, e continuaram passando sem alteração depois.Atualização (2026-06-24): falha intermitente investigada — causa raiz identificada, não é bug de código.
npm testchegou a falhar (todas as suítes) comTypeError: Class extends value undefined is not a constructor or nullemsrc/elements/constant.jsx. Repetindonpm testem loop comnode_modules/.viteapagado a cada vez (15+ execuções), a falha reproduziu numa janela específica que coincidiu com uma edição em disco desrc/nodes/nodes/simNodeModel.jsx(arquivo que define a classe baseSimNodeModel, herdada por todo bloco) ocorrendo durante a leitura do Vite/Vitest. Conclusão: é uma corrida entre o test runner lendo o arquivo e o editor/linter salvando-o no mesmo instante — o Vite lê uma versão momentaneamente incompleta do módulo, a classe saiundefined, e qualquer bloco que estendaSimNodeModelfalha noextends. Não é um ciclo de import nem regressão no código;src/nodes/engine.jsxe o corte do ciclonodeModel.jsx → elements/index.jsx → bloco → nodeModel.jsxcontinuam corretos. Se voltar a acontecer, é esperado se houver edição de arquivo fonte (especialmentesimNodeModel.jsx) durante a execução donpm test— não indica bug.Também nesta data:
bezierLinks.jsxfoi reescrito por completo — trocou o roteamento por curvas Bezier (comcomputeCurvature, descrito no parágrafo de Fase 2 abaixo) por roteamento ortogonal estilo Simulink (computeOrthogonalRoute, com desvio de obstáculos).computeCurvaturenão existe mais no código; o histórico abaixo é mantido como registro, não como descrição do estado atual.
- Configurar test runner (Vitest —
npm test). - Testes unitários para
SimulationEngine(Euler step, reset, stop condition) — feito em paralelo com o split docore.jsx(ver Fase 1). - Testes para
solution()dos blocos mais usados:Add/Sub/Multiply/Divide/Mod(viaVariadicMathModel),Integrator(Euler + guarda de loop algébrico + reset),GainePIDController(termos P/I/D isolados + guarda de loop algébrico + reset). Total: 36 testes (npm test). Ambiente do Vitest precisou ser trocado denodeparahappy-dom, porque@projectstorm/react-diagramsassume globais de browser (self) mesmo só sendo importado, não renderizado. - Testes de regressão para o cálculo de curvatura Bezier
(
bezierLinks.jsx→computeCurvature).- Bug real encontrado e corrigido durante a escrita desses testes:
computeCurvaturesempre retornava0.5, para qualquer ângulo. Causa:Array.prototype.findnum array ordenado ascendente por ângulo sempre batia no primeiro item ({angle:5, curvature:0}) sempre que o ângulo real era maior que 5°; e como esse item temcurvature: 0(falsy), o fallback?.curvature || 0.5sempre vencia. Ou seja, a "curvatura adaptativa" por ângulo nunca funcionou — todo link sempre usou 0.5. Corrigido para uma busca por "menor breakpoint que ainda cobre o ângulo" (ângulos pequenos → quase reto, perto de 90° → curva máxima, voltando a quase reto perto de 180°), confirmado com o usuário antes de aplicar por mudar a aparência visual de todos os links existentes. - Bug colateral encontrado e corrigido ao testar
VariadicMathModel: importaradd.jsx(ou qualquer um dos 5 blocos refatorados) de forma isolada — como um teste faz, sem passar primeiro porApp.jsx/nodeModel.jsx— quebrava comClass extends value undefined is not a constructor. Causa:variadicMathModel.jsximportavaSimNodeModelde../nodes/nodeModel, que reexporta oEngine/Modele por isso arrastaelements/index.jsx(todos os blocos) de volta — um ciclo de import que só "dava sorte" de resolver na ordem certa quando a entrada era sempreApp.jsx. Corrigido importando direto do módulo-fonte (../nodes/nodes/simNodeModel), que não depende deelements/index.jsx. Esse mesmo padrão de import (from '../nodes/nodeModel') ainda existe em praticamente todos os outros arquivos deelements/— funciona hoje só por ordem de carregamento favorável; é uma dívida arquitetural a ter em mente ao adicionar testes para mais blocos.
- Bug real encontrado e corrigido durante a escrita desses testes:
- Reativado undo/redo (
nodes/stack/stack.jsx), com correções no design original: o listener antigo (eventDidFirenoDiagramModel) capturava tambémzoomUpdated/offsetUpdated/gridUpdated(pan/zoom entrando como "ações" desfazíveis) e nunca capturavapositionChanged(que dispara no nó, não no model) — ou seja, mover um bloco, a edição mais comum, nunca era gravada.updateModel()também trocava oDiagramModelinteiro viaEngine.setModel(new DiagramModel()), o que descartava o próprio listener registrado no construtor após o primeiro undo/redo. Corrigido: filtra porevent.function(sónodesUpdated/linksUpdated/positionChanged, debounced parapositionChangedpara não empilhar um estado por frame de arrasto); restaura desserializando no mesmomodel(mesmo padrão desamples.jsx/menubar.jsx) em vez de trocar o objeto; reanexa listeners de posição aos nós recriados pela desserialização. Também passou a capturar edição de parâmetros de bloco (ex.:gainValue), que muda campos direto sem disparar evento nenhum — capturado no fechamento do modal de configurações (useModal.close()), único ponto confiável pra saber que um bloco terminou de ser editado. Atalhos:Ctrl+Z(undo) /Ctrl+Shift+Z(redo).- A instância do
Stacké criada sob demanda (getStackManager()), não no carregamento do módulo: o módulo é importado pormodal.jsx, que por sua vez é importado por praticamente todo bloco emelements/, então instanciar no load-time quebrava qualquer teste que importa um bloco isoladamente (Engine.getModel()ainda énullnesse cenário, antes denodeModel.jsxrodarEngine.setModel(...)).
- A instância do
- Implementar métodos de integração além de Euler — feito
(
src/simulation/integrationMethods.jsx: Euler/Heun/RK4). - Sincronizado o modo "tempo real" com a geração de código C. O loop
runRealTimeLoop(src/simulation/runModes.jsx) já existia no React (setTimeout(stepSize*1000)entre passos) e o codegen já propagava o flagrealTimeModeaté a struct C (model.simulation.mode, 0 = simulado, 1 = tempo real), mas omain.c.templategerado ignorava esse campo — o loop C sempre rodava o mais rápido possível, independente do modo. Corrigido adicionandosim_real_time_sleep(double seconds)(libs.h.template/libs.c.template,usleepno POSIX/Sleepno Windows via#ifdef _WIN32), chamada no fim de cada iteração domain.c.templatequandomodel.simulation.mode == 1, dormindo porsampling_timesegundos — mesma cadência que orunRealTimeLoopdo React, só que no C gerado. Validado manualmente compilando comgccummodel.c/main.cde exemplo (stepSize=0.2,stopTime=1,mode=1): 6 passos, ~1.2s de tempo real decorrido (medido comtime), confirmando a sincronização. - Adicionados 4 blocos de fonte clássicos do Simulink, que faltavam por
completo: Step (
src/elements/step.jsx), Ramp (src/elements/ramp.jsx), Sine Wave (src/elements/sineWave.jsx) e Pulse Generator (src/elements/pulseGenerator.jsx) — cada um com bloco React, cmodel C correspondente (src/codeGeneration/cmodels/cmodel_{step,ramp,sineWave,pulseGenerator}.jsx) e testes (*.test.jsx, 16 casos novos). Seguem o mesmo padrão declock.jsx/random.jsx: sem porta de entrada, leemSimulation.getCurrentTime()emsolution(). - Adicionados 2 exemplos novos em
public/samples/: Signal Sources (signalsources.tsim) — os 4 blocos novos, cada um plotado isoladamente — e Closed-Loop Step Response (stepresponse.tsim) — Step → PIDController (setpoint) com FirstOrder como planta na realimentação, plotando setpoint vs. resposta da planta. Gerados programaticamente (instanciando os nós, ligando as portas viaPortModel/LinkModele chamandoDiagramModel.serialize()) em vez de à mão, para garantir ummodel.jsonválido no mesmo formato que o app produz ao salvar — o script usado foi descartado após a geração, não faz parte do repositório.
- Subsistemas/blocos aninhados. Três blocos novos:
SubsystemModel(src/elements/subsystem.jsx),SubsystemInputModeleSubsystemOutputModel(subsystemInput.jsx/subsystemOutput.jsx), registrados emindex.jsx. - Modelo:SubsystemModelguarda seu própriointernalModel(DiagramModel) — um diagrama independente, vivo o tempo todo em memória, com seus próprios nós/links. Dentro dele, blocosSubsystemInputModel/SubsystemOutputModelmarcam onde os sinais entram/saem; o botão "Sync Ports" no modal doSubsystemModelrecalcula as portas externas a partir desses marcadores (não há sincronia automática em tempo real nesta rodada — decisão deliberada para não precisar de listeners no diagrama interno). - Navegação: "View Inside" troca omodeldoEngineglobal pelointernalModeldo subsistema, no lugar (mesmo padrão que o undo/redo usa para restaurar estado — não cria um segundo Engine). Novo singletonsrc/nodes/subsystemNavigation.jsx(estilo ad-hoc igual aouseModal) mantém uma pilha de navegação e notificaApp.jsx, que renderiza um breadcrumb com "‹ Back"/"Top". Lacuna conhecida: os listeners doStackde undo/redo (nodes/stack/stack.jsx) são presos ao model que existia quando oStackfoi instanciado: undo/redo não acompanha a navegação para dentro/fora de um subsistema. - Simulação:SubsystemModel.solution()resolve todas as saídas de uma vez (não só a docalledPortchamado), porque o cache por passo emSimNodeModel.solve()guarda o retorno de uma única chamada desolution()— se o bloco resolvesse só uma porta por vez, consultar uma segunda saída no mesmo passo devolveriaundefinedem vez de recalcular. CadaSubsystemInputModeldelega paraparentSubsystem.getOuterInputValue(portIndex), que chamagetNodeByInputna própria porta externa doSubsystemModel— ou seja, o subsistema é "transparente" para o restante do motor de simulação, sem precisar de um segundoSimulationEngine.reset()propaga para todos os nós dointernalModel. - Análise de frequência: também suportado —linearize()noSubsystemModelusathis.calledPort.options.label(out1,out2...) pra achar o marcador de saída certo e delega; loops algébricos e blocos não suportados dentro do subsistema propagam o mesmo erro que propagariam fora dele. - Bug real encontrado e corrigido durante os testes:syncPorts()chamavathis.removePort(port)iterando direto sobrethis.getOutPorts()— masgetOutPorts()/getInPorts()noDefaultNodeModelda lib retornam o array vivo interno (portsIn/portsOut), eremovePortfazsplicenesse mesmo array. Mutar um array enquanto oforEachitera sobre ele pula elementos (índices deslocam depois dosplice) — com 2 portas para remover, só 1 saía. Corrigido iterando sobre uma cópia ([...this.getOutPorts()]). - Não coberto nesta rodada (limitações deliberadas, não bugs): detecção de loop algébrico não atravessa a fronteira de um subsistema (só vê suas portas externas como uma caixa preta); não há geração de código C paraSubsystemModel/marcadores — chegar a um deles emcodeGeneration/modelActions.jsxlança o mesmo erro "no C model registered" que qualquer outro bloco semcmodel, em vez de gerar algo (falha clara, não silenciosa); sem suporte a subsistemas recursivos com referência a si mesmo (não testado, risco de recursão infinita se alguém arrastar um Subsystem dentro de si mesmo). - Detecção de loop algébrico / validação de diagrama antes de simular.
Criado
src/simulation/algebraicLoop.jsx(detectAlgebraicLoop(nodes)): faz DFS sobre o grafo de dependências (getNodeByInput) procurando ciclo entre blocos puramente combinacionais. A chave do design é a nova flagbreaksAlgebraicLoopemSimNodeModel(defaultfalse), marcadatruenos 7 blocos que já guardamlastStepSolved/previousStepantes de recursar nas entradas —Integrator,Derivator,Memory,ZOH,ZeroOrder,FirstOrder,PIDController. Esse guard pré-existente é o que faz esses blocos devolverem um valor cacheado do passo anterior em vez de recursar de novo no próprio input quando reentrados no mesmo passo — ou seja, eles já eram, na prática, os "quebra-ciclo" do motor; a detecção estática só formaliza essa mesma regra antes de rodar, em vez de descobrir via estouro de pilha. Um ciclo que não passa por nenhum desses blocos (ex.:Addligado de volta nele mesmo viaGain) é reportado como erro antes da simulação começar.setupSimulation()(src/simulation/setup.jsx) agora roda essa checagem e retorna{ok: false, error: 'algebraic-loop', cycleLabels};SimulationEngine.runSetup()(src/simulation/core.jsx) trata esse caso com um toast nomeando os blocos do ciclo (A → B → A) e sugerindo adicionar um bloco com estado, em vez de travar com call stack overflow sem explicação. Testes novos:algebraicLoop.test.jsx(6 casos, incluindo self-loop e ciclo quebrado por bloco com estado),setup.test.jsx(4 casos) e um caso adicional emcore.test.jsx. - Scope de frequência / análise mais avançada, usando
control-systems-js(até então dependência sem uso real no código). Novo blocoFrequencyScopeModel(src/elements/frequencyScope.jsx): não participa do loop de tempo (isTerminalBlock = false), é uma análise estática sob demanda — botão "Analyze" no modal de configuração chamaanalyze(), que linealiza a cadeia de blocos conectada à sua entrada e plota o diagrama de Bode (magnitude/fase x frequência, escala log) comBodeChart(elements/complements/BodeChart.jsx, dois canvases chart.js empilhados). - Mecanismo: novo métodolinearize()emSimNodeModel(nodes/nodes/simNodeModel.jsx), análogo aosolution()mas retornando{numerator, denominator}(Laplace) em vez de um valor numérico. Default: bloco sem porta de entrada (fonte — Constant, Clock, Step...) é tratado como referência unitária ({[1],[1]}); bloco com entrada que não sobrescrevelinearize()lançaLinearizationError(bloco não-linear ou sem representação conhecida). Overrides adicionados emGainModel(ganho puro),IntegratorModel(1/s),FirstOrderModel(1/(s+a)),ZeroOrderModel(s+a),PIDControllerModel(só quandokd=0e a portasetpointestá desconectada — ver limitações abaixo), e genericamente emVariadicMathModelvia uma flagisLinearCombination(truesó emAddModel/SubModel;Multiply/Divide/Modcaem no default e lançam erro, por serem não-lineares). - Álgebra de polinômios isolada emsrc/simulation/transferFunctionMath.jsx(seriesTFpara blocos em cascata,addTFpara somas/subtrações sobre um denominador comum,trimLeadingZeros,logSpace,LinearizationError) — só na borda final (FrequencyScopeModel.analyze()) os coeficientes acumulados são passados protransferFunction()da lib externa. - Limitação real descoberta na lib, não só deste projeto:control-systems-jsexigenumerator.length <= denominator.length(sistema "estritamente próprio" — physically realizable). Isso bloqueia umDerivatorpuro (s/1, numerador de ordem maior) e um PID completo comkd≠0(numerador de ordem 2 > denominador de ordem 1) — por isso só PI (kd=0) é suportado na análise de frequência. - Bug da lib contornado, não só evitado:tf.bode()sem argumentos usa um range de frequência "default" calculado a partir dos polos/zeros: para qualquer sistema com polo na origem (ex.: umIntegratorsozinho — o caso mais comum no app), esse cálculo retornaInfinity/NaNem todos os pontos (confirmado isolando o problema via Node antes de implementar). Por isso o bloco sempre gera seu próprio array de frequências log-espaçadas (logSpace(minFrequency, maxFrequency, numPoints), configurável no modal) em vez de confiar no default da lib. - Não suportado nesta rodada (lança erro claro nomeando o bloco, em vez de silenciosamente dar um resultado errado):Multiply,Divide,Mod,Derivator, PID comkd≠0,Saturation,Switch, comparadores,Memory/ZOH(delay/sample discreto) e qualquer outro bloco que não sobrescrevalinearize().
- Bug real encontrado e corrigido:
src/elements/index.jsximportavaPIDControllerModel,DFlipFlopModel,TFlipFlopModel,JKFlipFlopModeleSRFlipFlopModel, mas nenhum dos 5 estava no bloco deexport— ou seja, 5 blocos completos (com cmodel C e o PID com testes próprios) ficavam invisíveis na barra de elementos, sem nenhum erro visível. Corrigido adicionando os 5 aoexport. - Removidos comentários de código morto sem valor explicativo:
import comentado de
RightAnglePortModele comentário inline desatualizado emsimNodeModel.jsx, bloco de serialização de portas comentado (feature nunca concluída) também emsimNodeModel.jsx, e implementação antiga comentada emcmodel_constantPI.jsx. - Avaliado e mantido (não é código morto, é débito documentado/planejado):
nodes/stack/stack.jsx(undo/redo desativado, mas listado na Fase 3 para reativar) e a dependênciacontrol-systems-jsnopackage.json(reservada para a Fase 4 — análise de frequência).
- Maior gap estrutural com o Simulink identificado: todo sinal hoje é
escalar (
solution()retorna{label: Number}); blocos como Add/Gain/Integrator fazem aritmética direta no valor lido, sem checagem de tipo. Isso bloqueava qualquer bloco MIMO/vetorial. Decisão explícita de não reescrever os ~70 blocos de uma vez (risco alto demais dado o débito técnico do projeto); esta fase entrega o núcleo Simulink-like com blast radius controlado. - Sinal vetorial representado como array JS comum (
number[]), fluindo pela mesma porta/link que hoje carrega umNumber— sem classe wrapper nova, mantendo a filosofia "valor opaco" do motor de simulação (solve()/cache/getNodeByInputcontinuam sem saber nada sobre o tipo do dado). - Guard contra corrupção silenciosa: novo módulo
src/simulation/vectorSignal.jsx(isVectorSignal,VectorSignalError,assertScalar, mesmo padrão deLinearizationErroremtransferFunctionMath.jsx). Aplicado nos pontos de maior tráfego —variadicMathModel.jsx(cobre Add/Sub/Multiply/Divide/Mod de uma vez),gain.jsx,integrator.jsx,derivator.jsx,firstOrder.jsx,memory.jsx— para que alimentar esses blocos com um vetor lance um erro claro em vez de corromper silenciosamente (concatenação de string em vez de soma, por exemplo).- Gap conhecido, deixado de propósito: os outros ~19 blocos que também
fazem aritmética/comparação direta (
comparator,average,pow/log/exp/sqrt/trigonometric,lookupTable,saturation,switch,CSVExport, etc.) não receberam o guard nesta rodada — não é regressão (hoje também não validam nada), mas alimentá-los com vetor ainda falha silenciosamente. Candidato a uma Fase 1.1.
- Gap conhecido, deixado de propósito: os outros ~19 blocos que também
fazem aritmética/comparação direta (
- Blocos novos
Mux/Demux(src/elements/mux.jsx/demux.jsx), registrados emindex.jsx.Mux: N portas de entrada escalares (mesmo padrão "Add port" deVariadicMathModel) → 1 saída vetorial[in1, in2, ...].Demux: 1 entrada vetorial → N saídas escalares, largura configurável no painel de settings (recria as portas de saída viasyncOutputPorts(), mesmo padrão deSubsystemModel.syncPorts()).- Cuidado de serialização:
Demux.deserialize()não chamasyncOutputPorts()— as portas (e os links que carregam) já são restauradas porsuper.deserialize()a partir do próprio JSON salvo; recriá-las ali descartaria esses links. Só o campowidthé restaurado.
- Cuidado de serialização:
-
SimNodeModel.createPort(label, isInput, options)ganhou um 3º parâmetro opcional repassado aoBezierPortModel— usado por Mux para marcar sua porta de saída comvectorWidth: N. Blocos existentes continuam chamando com 2 argumentos, sem alteração de comportamento.- Limitação conhecida:
vectorWidthnão sobrevive a um save/load do diagrama —BezierPortModelnão temserialize()/deserialize()próprios, então esse metadado (puramente cosmético, usado só pra engrossar o traço do link) se perde ao recarregar um.tsimsalvo. Os links/portas em si (e a simulação) não são afetados, só o indicador visual.
- Limitação conhecida:
- Visualização de vetor:
plot.jsxdetecta quando um valor de porta é um array (buildPlotDatasets(), extraída como função pura e testada isoladamente) e expande automaticamente em N sub-datasets no gráfico (label[0],label[1]...) em vez de quebrar contra o Chart.js, que esperanumber[]plano.display.jsxformata um valor vetorial como[1.00, 2.00, 3.00]em vez dotoString()padrão do React. Comportamento escalar existente preservado 100% em ambos. - Indicador visual cosmético em
bezierLinks.jsx: link cuja porta de origem temvectorWidth > 1é desenhado com traço mais espesso (7 em vez de 4). Não afeta a simulação. - Fora de escopo desta fase (mesmo padrão de documentação usado para
Subsystem/PID kd≠0): geração de código C para Mux/Demux (cai no mesmo erro
genérico "no C model registered");
linearize()/análise de frequência através de Mux/Demux; sinais vetoriais cruzando fronteira de Subsystem.
npm test(113/113, 19 suítes) enpm run buildpassaram limpos após esta fase (mesmo warning pré-existente de terceiros em@projectstorm/react-diagrams-routing).
- Fechado o gap documentado no fim da Fase 6: aplicado
assertScalar(src/simulation/vectorSignal.jsx) nos ~32 blocos restantes que liaminpt.solve()cru e faziam aritmética/comparação direta —comparator,comparatorConstante,average,standardDeviation,pow,log,exp,sqrt,trigonometric,round,saturation,lookupTable,min,max,isEven,isOdd,CSVExport,histogram,gauge,PIDcontroller(entrada e setpoint),zo,zoh, os 7 gates lógicos (AND/OR/NOT/NOR/NAND/XOR/XNOR) e os 4 flip-flops (D/T/JK/SR). Mesmo padrão de erro claro (VectorSignalError) em vez de corrupção silenciosa (concatenação de string, comparação sempre verdadeira por array ser truthy,NaN).- Decisão de escopo dentro do Switch: só a porta de condição
(
in2, usada como booleano) recebeu o guard. As portas selecionadas (in1/in3, repassadas direto pro output) ficaram sem guard de propósito — um Switch repassando um vetor inteiro (seleção de um de N sinais vetoriais) é uma operação Simulink-like legítima, não corrupção. - Decisão de escopo no Subsystem:
subsystem.jsx/subsystemOutput.jsxcontinuam sem guard — são passthrough transparente de fronteira, e "sinais vetoriais cruzando fronteira de Subsystem" já está listado como fora de escopo (ver Fase 6); guardar ali bloquearia essa extensão futura sem nenhum bug atual a prevenir.
- Decisão de escopo dentro do Switch: só a porta de condição
(
- Testes de regressão dedicados (
src/elements/vectorSignalGuard.test.jsx) cobrindo um representante de cada padrão de leitura (comparador de 2 portas, redução variádica booleana, condição do Switch, blocos de aridade fixa 1 e 2 entradas, blocos de visualização) — não os 32 arquivos individualmente, só os padrões.
npm test(121/121, 20 suítes) enpm run buildpassaram limpos.
- Fechado o último gap documentado nas Fases 4 e 6:
SubsystemModel,MuxModeleDemuxModelagora geram C em vez de cair no erro genérico "no C model registered". Implementado reaproveitando peças já existentes noModelActions, sem mecanismo novo:node.calledPort.options.label(já usado porcmodel_dFlipFlop.jsxpara blocos com múltiplas saídas),node.isvisited(dedupe de código gerado) e o fato de todo cmodel já poder devolver uma expressão C, não só um identificador simples (precedente:_arrayReduceCModel.jsxmontadouble param[] = {...}inline). -
cmodel_mux.jsx: declaradouble var_..._mux[N];(N = portas de entrada conectadas) e preenche cada posição no step. Retorna o nome do array. -
cmodel_demux.jsx: não declara variável própria — devolve uma expressão de indexação (array[i]) no array C da entrada, usandocalledPortpara saber qualoutNfoi pedido. -
cmodel_subsystem.jsx(3 exports:SubsystemModel,SubsystemOutputModel,SubsystemInputModel): delega para as mesmas pontes já usadas pela simulação (getNodeByInput,parentSubsystem,portIndex,getOutputMarkers()— versrc/elements/subsystem.jsx, zero alteração nesse arquivo). Nenhum dos três marcadores declara variável própria, só repassa a expressão C do nó que resolveu — subsistemas aninhados funcionam por recursão sem caso especial. - Decisão de design deliberada: nenhuma validação em JS de "Mux
alimentando bloco escalar" antes de gerar C. Um array decai pra
pointer em C; se um bloco escalar tentar usar esse retorno em
aritmética direta (
pointer * double), o C gerado não compila — mesma filosofia "falha clara, não corrupção silenciosa" já usada no guard de simulação (Fase 6/6.1), só que resolvida pelo próprio compilador C em vez de um guard em JS. - Testes novos (
cmodel_mux.test.jsx,cmodel_demux.test.jsx,cmodel_subsystem.test.jsx) chamando as funções de cmodel diretamente com umthismockado (addModelC__vars/addModelC__step/getNodecomovi.fn()) — não havia precedente de teste de cmodel no repo antes desta fase.
npm test(131/131, 23 suítes) enpm run buildpassaram limpos. Fora de escopo, documentado: subsistema recursivo referenciando a si mesmo (risco não testado já citado na Fase 4, herdado pelo codegen sem ser regressão nova).
Auditoria do pipeline de codegen encontrou 3 bugs reais (não só gaps) e 1 gap de usabilidade; todos corrigidos nesta fase.
- Bug real corrigido:
cmodel_histogram.jsxchamavanode.getNodes()— método que não existe no blocoHistogram(é método deDiagramModel, não de um node). Qualquer diagrama com um bloco Histogram travava a geração de código comTypeError: node.getNodes is not a function, mesmo o próprio painel de settings do bloco já avisando "Not compatible with Code Generation" (a intenção era um erro claro; o que existia era um crash não tratado). Corrigido para seguir o mesmo padrão decmodel_gauge.jsx/cmodel_display.jsx— expõe cada entrada conectada como uma porta GET (sem desenhar gráfico em C, já que não há essa capacidade no C gerado), agora suportando múltiplos datasets corretamente (a versão antiga, mesmo se não travasse, só teria propagado o valor da última entrada pra uma única porta compartilhada). Texto do painel atualizado pra refletir a nova realidade. - Bug real corrigido:
cmodel_csvImport.jsxtinha o nome do arquivo hardcoded como"data.csv"literal para toda instância do bloco — duas instâncias de CSV Import no mesmo diagrama colidiam no mesmo arquivo. Corrigido paradata_${node.CGenUID}.csv, mesmo padrão quecmodel_csvExport.jsxjá usava (a inconsistência era só no Import). - Bug real corrigido: o C gerado pelo CSV Import esperava um arquivo
data.csvque nunca era exportado no zip — o usuário precisava criar esse arquivo manualmente com o cabeçalho exato. Se o arquivo não existisse,load_csvfalhava silenciosamente (*table = NULL) elookup_csvdesreferenciava esse pointer nulo no primeiro passo (segfault). Corrigido em duas partes: (1) guarda deNULL/*size == 0nolookup_csvgerado, retornando 0.0 em vez de desreferenciar; (2) o CSV real (carregado no browser, emnode.mapValues/columnNames) agora é reconstruído e empacotado no zip via um mecanismo novo —ModelActions.addExtraFile(filename, content)(dedupido por nome, mesmo espírito do#addUniqueReplacementjá usado pelos outrosaddX), comcstruct.extraFilesmesclado emoutputFilesantes docreateZip()emcodeGen.jsx. Esse mecanismo é genérico — qualquer cmodel futuro que precise empacotar um arquivo extra (não só template C) pode reusá-lo. - UX corrigida:
menubar.jsxmostrava sempre'Failed to generate code.'no toast de erro, descartando a mensagem específica que os cmodels já lançam (incluindo as da Fase 7: "saída 2 não existe (rode Sync Ports)" etc.). Trocado paratoast.promise(..., {error: {render ({data}) => ...}}), mostrandodata.message— o usuário agora vê qual bloco/motivo causou a falha. - Item de usabilidade: zip gerado não tinha instruções de build —
só os
.c/.h. AdicionadosMakefile.template(regragcc ... -lm, flag necessária porque vários blocos usam<math.h>e o comentário antigo emmain.cnão mencionava isso) eREADME.template(build com e sem make, nota sobre os arquivosdata_<id>.csvdo CSV Import), ambos registrados emcodeGen.jsxjunto aos demais templates. - Testes novos:
cmodel_histogram.test.jsx(regressão do crash + caso sem portas),cmodel_csvImport.test.jsx(filename único, guarda null no C gerado, conteúdo do CSV empacotado),modelActions.test.jsx(dedupe doaddExtraFile).
npm test(138/138, 26 suítes) enpm run buildpassaram limpos.
A Fase 1 deve vir antes da Fase 2 ser totalmente eficaz: escrever testes contra a duplicação atual significa testar 20 variações do mesmo bug em potencial. Mas a Fase 2 (mesmo que parcial — só o engine de simulação) deveria começar em paralelo, antes de qualquer refator grande, para servir de rede de segurança contra regressões durante a Fase 1.