feat(web): migra Movimento com homem-morto e trava de hardware

- Controle: opt-in de movimento fisico, botao "segure para mover"
  (mouse, toque e teclado), sliders das articulacoes tambem so movem
  enquanto pressionados; velocidade e interpolacao ficam no servidor
- sequencias prontas e posturas salvas: previa so na tela ou execucao
  segurando o botao ate o fim; soltar interrompe na hora
- posturas com nome inline (sem prompt), exclusao confirmada inline,
  exportar/importar; posturas aparecem na paleta (Ctrl+K)
- Calibracao: testes de fim de curso por canal, marcar limites e tabela
  editavel com validacao antes de salvar
- Diagnostico (antigo Servos): ao vivo limitado a 10/s, bloqueado quando
  o braco esta com os motores
- desenho esquematico do braco em SVG com previa animada
- servidor: evento 'motores' tambem ao ligar canais; renovar no alvo
  devolve movendo=false para a sequencia avancar
- aba escondida ou E-Stop soltam o braco e desativam o movimento

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-05 19:56:48 -03:00
co-authored by Claude Opus 5.5
parent 947c0a3127
commit 673dea62e8
10 changed files with 1153 additions and 9 deletions
@@ -0,0 +1,96 @@
<script lang="ts">
// Vista lateral esquemática: cada articulação é um <g> aninhado que gira no próprio eixo.
// Ordem dos ângulos: 0 garra, 1 punho 1, 2 punho 2, 3 braço superior, 4 braço inferior, 5 antebraço, 6 base.
let { pose, fantasma = null }: { pose: number[]; fantasma?: number[] | null } = $props();
const r = (v: number) => v - 90;
</script>
{#snippet braco(p: number[], classe: string)}
<g class={classe}>
<g transform="translate(120 236) rotate({r(p[4])})">
<line x1="0" y1="0" x2="0" y2="-78" class="seg" />
<g transform="translate(0 -78) rotate({r(p[3])})">
<line x1="0" y1="0" x2="0" y2="-66" class="seg" />
<g transform="translate(0 -66) rotate({r(p[5])})">
<line x1="0" y1="0" x2="0" y2="-40" class="seg fino" />
<g transform="translate(0 -40) rotate({r(p[1])})">
<line x1="0" y1="0" x2="0" y2="-18" class="seg fino" />
<ellipse cx="0" cy="-18" rx={7 * Math.abs(Math.cos(((p[2] - 90) * Math.PI) / 180)) + 2} ry="4" class="punho" />
<g transform="translate(0 -20)">
<path d="M0 0 L-4 -16 L-1 -24" class="dedo" transform="rotate({-8 - (p[0] - 90) * 0.25})" />
<path d="M0 0 L4 -16 L1 -24" class="dedo" transform="rotate({8 + (p[0] - 90) * 0.25})" />
</g>
<circle r="4" class="junta" />
</g>
<circle r="5" class="junta" />
</g>
<circle r="6" class="junta" />
</g>
<circle r="7" class="junta" />
</g>
</g>
{/snippet}
<svg viewBox="0 0 240 290" role="img" aria-label="Postura do braço">
<!-- base: o arco mostra a rotação (vista de cima) -->
<rect x="80" y="240" width="80" height="16" rx="5" class="base" />
<path d="M85 274 A40 14 0 0 0 155 274" class="trilho" />
<circle
cx={120 + 35 * Math.cos(Math.PI - (pose[6] * Math.PI) / 180)}
cy={274 + 12 * Math.sin((pose[6] * Math.PI) / 180) * 0.4}
r="4"
class="marcador"
/>
{#if fantasma}{@render braco(fantasma, 'fantasma')}{/if}
{@render braco(pose, 'real')}
</svg>
<style>
svg {
width: 100%;
height: 100%;
display: block;
}
.seg {
stroke: var(--destaque);
stroke-width: 12;
stroke-linecap: round;
}
.fino {
stroke-width: 8;
}
.junta {
fill: var(--superficie);
stroke: var(--texto-2);
stroke-width: 2;
}
.punho {
fill: var(--texto-2);
}
.dedo {
fill: none;
stroke: var(--texto);
stroke-width: 4;
stroke-linecap: round;
stroke-linejoin: round;
}
.base {
fill: var(--superficie-3);
stroke: var(--borda-forte);
}
.trilho {
fill: none;
stroke: var(--borda-forte);
stroke-width: 2;
stroke-dasharray: 3 4;
}
.marcador {
fill: var(--aviso);
}
.fantasma {
opacity: 0.25;
}
.fantasma .seg {
stroke: var(--texto-3);
}
</style>