# API de extensões — Physics Lab v4

Uma extensão é um arquivo **.js** que chama `PhysicsLab.registerExtension({...})`. Ela pode criar novos tipos de objeto (com desenho e física próprios), categorias, ferramentas e experimentos. Instale pelo menu **Extensões › Gerenciar extensões**: abrindo um arquivo do seu dispositivo ou colando o código (sempre com confirmação). A extensão fica salva no navegador e é recarregada automaticamente.

**Segurança:** extensões rodam 100% no navegador e **nunca são baixadas da internet** pelo app (instalação por URL/GitHub foi removida). Nenhum código roda em servidor. Instale apenas código em que você confia.

**Idiomas:** qualquer texto (nome, rótulos, títulos, teoria) pode ser uma string simples ou um objeto `{ en, pt, es }`. O app mostra o idioma escolhido pelo usuário (inglês é o padrão). Use `api.tx(obj)` para escolher o texto no idioma atual e `api.lang` para saber qual é.

**Ícones:** use nomes do [Google Material Symbols](https://fonts.google.com/icons) (ex.: `'mode_fan'`, `'bolt'`, `'science'`). O app traz um subconjunto offline com os ícones mais comuns; nomes fora dele aparecem como o ícone padrão de extensão. Para incluir novos ícones, rode `python3 tools/build.py` (ele lê os nomes usados nas extensões oficiais).

## Estrutura mínima

```js
PhysicsLab.registerExtension({
    id: 'meu.exemplo',            // único; reinstalar com o mesmo id atualiza
    name: { en: 'My example', pt: 'Meu exemplo', es: 'Mi ejemplo' },
    version: '1.0.0',
    author: 'Seu nome',
    description: 'O que ela faz',
    setup(api) {
        // registre aqui tipos, ferramentas, experimentos e eventos
    },
    teardown(api) { /* opcional: ao desativar/remover */ }
});
```

## Unidades

- Posições e tamanhos estão em **pixels do mundo**: 1 m = `api.SCALE` px (50).
- O ângulo `o.a` está em **radianos**, positivo no sentido horário da tela (o eixo y aponta para baixo).
- Velocidades `o.vx`, `o.vy` em px/s; `o.av` (velocidade angular) em rad/s.
- `api.physics.applyForce(o, Fx, Fy, px, py)` recebe forças em **Newtons**; com o ponto (px, py) gera torque.
- A massa `o.mass` está em kg; carga `o.charge` em μC.

## Registrar um tipo de objeto

```js
api.registerType('meuTipo', {
    label: { en: 'My object', pt: 'Meu objeto', es: 'Mi objeto' }, icon: 'hexagon', cat: 'minhaCategoria',
    dims: 'box',            // 'box' (w,h) | 'circle' (r) | 'point'
    body: false,            // true = corpo rígido do motor de física
    layer: 1,               // 0 = região de fundo, 1 = normal, 2 = anotação
    defaults: { w: 80, h: 40, potencia: 10 },
    create: 'click',        // 'click' | 'drag-box' | 'drag-circle' | 'drag-line'
    shape(o) { },           // só para body:true → { kind:'circle', r } ou { kind:'poly', verts:[{x,y}...] }
    draw(ctx, o, R) { },    // desenhe em coordenadas do mundo; R.local(ctx,o) aplica posição/rotação
    drawFront(ctx, o, R) {},// opcional: desenhado por cima dos corpos
    force(o, W, h) { },     // a cada passo da física (~240×/s): aplique forças
    update(o, dt, W) { },   // a cada quadro com a simulação rodando (instrumentos)
    reset(o) { },           // ao reiniciar a simulação (limpe estados internos _xxx)
    colliders(o) { },       // opcional: retorna polígonos convexos estáticos [[{x,y}...]]
    optics(o, prims, media){},// opcional: participa do traçado de raios
    props: [ /* controles do painel */ ],
    readout: o => [['Rótulo', 'valor']]  // medições ao vivo
});
```

Campos cujo nome começa com `_` (ex.: `o._estado`) **não são salvos** no projeto — use-os para estados temporários.

### Controles do painel (`props`)

| Tipo | Campos |
| ---- | ------ |
| `number` | `key`, `label`, `unit`, `min`, `max`, `step` |
| `range` | igual a number, com controle deslizante |
| `select` | `options: [[valor, 'Rótulo'], ...]` |
| `check` | caixa de seleção (booleano) |
| `color` | seletor de cor |
| `text` / `textarea` | texto |
| `button` | `onClick(o, api)` |
| `info` | `html` (texto ou função) |

Use `get(o)` e `set(o, valor)` para converter unidades e `show(o)` para exibir condicionalmente. Ex.: `{ label: 'Raio', unit: 'm', type: 'number', get: o => o.r / 50, set: (o, v) => { o.r = v * 50; } }`.

### O mundo `W` recebido em `force()`

- `W.bodies` — lista de corpos: `{ o, dynamic, sh, aabb, m }` (use `b.o` para o objeto)
- `W.applyForce(o, Fx, Fy, px?, py?)` — força em N
- `W.applyImpulse(o, Jx, Jy)` — impulso em N·s
- `W.settings` — gravidade, ar etc.; `W.time` — tempo de simulação

## Categorias, ferramentas e experimentos

```js
const L = (en, pt, es) => ({ en, pt, es });
api.registerCategory({ id: 'minhaCategoria', label: L('Mine', 'Minha', 'Mía'), icon: 'cyclone' });
api.registerTool('minhaCategoria', { label: L('Place object', 'Colocar objeto', 'Colocar objeto'), icon: 'hexagon', create: 'meuTipo', props: { potencia: 20 } });
api.registerTool('minhaCategoria', { label: L('Action', 'Ação', 'Acción'), icon: 'bolt', run(api) { /* ... */ } });
api.registerExperiment({
    id: 'meu-exp', icon: 'science',
    field: 'Mecânica',   // 'Mecânica' | 'Ondas' | 'Eletromagnetismo' | 'Óptica' | 'Termodinâmica' | 'Fluidos' | 'Gravitação' | 'Física Moderna' | 'Extensões'
    title: L('My experiment', 'Meu experimento', 'Mi experimento'),
    summary: L('Short summary', 'Resumo curto', 'Resumen corto'),
    info: {
        en: { theory: 'Text with $LaTeX$', observe: ['...'], tryit: ['...'] },
        pt: { theory: 'Texto com $LaTeX$', observe: ['...'], tryit: ['...'] },
        es: { theory: 'Texto con $LaTeX$', observe: ['...'], tryit: ['...'] }
    },
    settings: { gravity: 9.8 },
    build(api) { api.add('meuTipo', { x: 600, y: 350 }); }
});
```

## Funções da `api`

| Função | Descrição |
| ------ | --------- |
| `api.add(tipo, props)` | cria e adiciona um objeto; retorna o objeto |
| `api.remove(ids)` · `api.get(id)` · `api.objects()` · `api.find(pred)` | manipular objetos |
| `api.joint(tipo, a, pa, b, pb, extra)` | conexões: 'rod', 'rope', 'spring', 'pin', 'pulley', 'force' |
| `api.select(ids)` · `api.selected()` | seleção |
| `api.commit()` | grava no histórico (desfazer) e salva |
| `api.scene.set({ gravity, airDrag, theme, ... })` | configurações da cena |
| `api.sim.play()` · `pause()` · `reset()` · `time` · `playing` | controle da simulação |
| `api.physics.applyForce(...)` · `applyImpulse(...)` · `bodies()` | física |
| `api.view.fit()` · `frame(x0,y0,x1,y1)` | câmera |
| `api.on(evento, fn)` | 'step', 'frame', 'play', 'pause', 'reset', 'select', 'render', 'overlay', 'load' |
| `api.toast(msg, tipo)` · `api.modal({...})` · `api.confirm(msg)` · `api.prompt(msg)` | interface |
| `api.util` · `api.draw` · `api.MATERIALS` · `api.Lab` | utilitários e acesso avançado |
| `api.t(texto, vars)` · `api.tx({en,pt,es})` · `api.lang` | tradução e idioma atual |
| `api.mi(nome, {size,color,fill})` · `api.drawIcon(ctx, nome, x, y, tam, cor)` | ícones Material (HTML e canvas) |

## Boas práticas

- Erros dentro de `draw`, `force`, `update` e eventos são capturados e não travam o laboratório, mas aparecem no console (F12).
- Se uma extensão impedir o uso, abra a página com `?safe=1` no endereço e remova-a no gerenciador.
- Plugins **.plugin.json** da versão 2 continuam podendo ser instalados (modo compatibilidade, somente objetos; ações em texto não são mais executadas).
- Experimentos marcados como Premium pelo administrador também valem para os experimentos das extensões (pelo `id`).

Veja o exemplo completo e comentado em `extensions/exemplo-ventilador.physlab.js`, e as extensões oficiais em `extensions/` (circuitos, fotoporta, corpo negro, ressonância) — todas podem ser baixadas pelo gerenciador para estudo.
