diff --git a/.claude/skills/plano-de-ataque/SKILL.md b/.claude/skills/plano-de-ataque/SKILL.md new file mode 100644 index 0000000..8bb3ef7 --- /dev/null +++ b/.claude/skills/plano-de-ataque/SKILL.md @@ -0,0 +1,163 @@ +--- +name: plano-de-ataque +description: Gera ou atualiza o PPTX "Plano de Ataque" (progresso por quinzena de Geotecnia/Estaqueamento, Infraestrutura/Bloco e Superestrutura/Pilar) deste repositório. Use quando o usuário pedir para regenerar o plano de ataque, atualizar quinzenas com uma planilha nova, mudar as cores/legenda do PPTX, ajustar o mapeamento de pixels de uma planta nova, ou reempacotar os pacotes standalone (dist-linux/dist-windows). Escopo: repositório bethania_porto_santos (raiz do projeto). +--- + +# Plano de Ataque — PPTX de progresso por quinzena + +Este skill resume o que foi aprendido construindo `gerar_plano.py`: um gerador +que pega um PPTX-modelo (1 slide de "uma quinzena") e uma planilha de +planejamento, e produz um PPTX com 1 slide por quinzena, pintando cada +quadrante da planta conforme o status de cada estrutura naquela data. + +## Onde as coisas estão + +- `gerar_plano.py` (raiz) — script-fonte, único arquivo, sem módulos próprios + além de `openpyxl`, `python-pptx`, `lxml`. +- `cores.json` (raiz) — cores/opacidades/parâmetros de quinzena, editável sem + tocar no código (ver seção "Cores" abaixo). +- `dist-linux/` — pacote pronto (executável standalone via PyInstaller) + + modelo `.pptx` + planilha `.xlsx` de exemplo + `LEIA-ME.txt`. +- `dist-windows/` — pacote pronto (Python "embeddable" do python.org com as + libs já instaladas em `python/Lib/site-packages`, sem precisar instalar + nada) + `PlanoDeAtaque.bat` + mesmos exemplos + `LEIA-ME.txt` + um `.zip` + com tudo empacotado. + +## Fluxo normal de uso (regenerar o PPTX) + +1. Reunir em uma pasta: 1 `.json` de cores, 1 `.pptx` modelo (slide 2 = layout + de uma quinzena), 1 `.xlsx` de planejamento. +2. `python3 gerar_plano.py` dentro dessa pasta (ou rodar o executável/`.bat` + em `dist-linux`/`dist-windows`). +3. O script AUTO-DETECTA os 3 arquivos pela extensão na própria pasta (não + usa nomes fixos) e gera `_ATUALIZADO.pptx`. +4. Se o usuário mandar uma planilha/modelo NOVO por upload, é só copiar para + `.uploads/` (ou onde estiver rodando), gerar, e comparar visualmente + (ver "Como validar" abaixo) antes de entregar. + +## Modelo de dados da planilha + +Aba **"Planejamento"**, dados a partir da linha 4: +- Coluna B: nome do quadrante, formato `"-/-"`, + ex. `"10-11/A1-B"` (colunas são números 1-48; linhas são rótulos entre + `D, C, B1, B, A1, A`). Regex usado: `^(\d+)-(\d+)/([A-Z0-9]+)-([A-Z0-9]+)$`. +- Colunas C/D: Geotecnia (Início/Término) +- Colunas E/F: Infraestrutura (Início/Término) +- Colunas G/H: Superestrutura (Início/Término) + +Status por quadrante/estrutura numa data de corte `p_end`: +- `None` (não pintar) se Início vazio ou Início > p_end +- `"concluido"` se Término preenchido e Término <= p_end +- `"andamento"` caso contrário + +## Quinzenas + +Períodos de **14 dias** começando em `2026-12-14 = Quinzena 02` (a planilha +não tem atividade antes dessa data — por isso a numeração já começa em 02, +não em 01). Esses três parâmetros ficam em `cores.json → quinzena` e dão +para editar sem mexer no código: `duracao_dias`, `data_inicio`, +`numero_inicial`. + +## Cores (`cores.json`) + +Uma seção por estrutura (`geotecnia`, `superestrutura`, `infraestrutura`), +cada uma com `andamento` e `concluido`, cada um com `preenchimento` (hex sem +`#`), `borda` (hex sem `#`) e `alpha_preenchimento` (0-100). A legenda do +slide sempre usa 100% de opacidade (fiel à cor pedida); o desenho usa o +`alpha_preenchimento` configurado — mais baixo na geotecnia (cobre o +quadrante inteiro, não pode esconder a planta) e mais alto nos marcadores de +pilar/bloco (são pequenos, não atrapalham). + +**Regra de ouro ao pedir "mude a cor X pra Y"**: sempre pedir/usar um print +em ALTA resolução da legenda e fazer amostragem de pixel (não confiar em +screenshot comprimido/pequeno — já erramos assim uma vez nesta conversa). +Amostrar tanto o preenchimento quanto a BORDA (são cores diferentes, e a +legenda de referência sempre tem as duas). + +## Como as camadas são desenhadas (importante para não perder fidelidade) + +- **Geotecnia/Estaqueamento**: retângulo arredondado cobrindo o quadrante + INTEIRO (não um recorte menor). +- **Superestrutura/Pilar e Infraestrutura/Bloco**: NÃO são um retângulo + proporcional ao quadrante — são marcadores pequenos (quadrado arredondado, + `marcador_fracao` do espaçamento entre colunas) plantados nos 4 VÉRTICES + (colunaxlinha) de cada quadrante ativo. Como vértices são compartilhados + entre quadrantes vizinhos, dedupe por ponto usando o status mais avançado + (`concluido` > `andamento`) para não desenhar marcadores duplicados/ + sobrepostos no mesmo lugar. + +## Mapeamento pixel → EMU (a parte mais delicada) + +As posições de colunas/linhas da planta (`IM1_COLPX`, `IM2_COLPX`, +`IM1_ROWPX`, `IM2_ROWPX` no script) são constantes medidas a dedo nas imagens +do modelo, usando detecção de blobs por soma de pixels escuros por +coluna/linha (círculos dos eixos, ver histórico da conversa para o script de +medição). Se a planta do modelo mudar, essas constantes têm que ser +remedidas — **sempre medir direto na imagem que está DE FATO embutida no +`.pptx`** (`ppt/media/imageN.png` dentro do zip), não em uma cópia enviada +separadamente por upload: já tivemos um bug de desalinhamento porque as duas +não eram pixel-idênticas. + +**Gotcha crítico**: se a imagem no slide tiver um corte aplicado no +PowerPoint (`` para + baixar as wheels certas (mesmo rodando de um Linux — `pip download` só + baixa o arquivo, não executa nada) e extrair (unzip) cada wheel dentro de + `python/Lib/site-packages`. Lançador é um `.bat` simples chamando + `python\python.exe gerar_plano.py`. Depois de montar, gerar de novo o + `PlanoDeAtaque_windows.zip` (cuidado: gerar o zip DENTRO da própria pasta + que está sendo zipada faz o zip se autoincluir — gerar em `/tmp` e mover + para dentro depois). + +## Como validar antes de entregar + +Como não há LibreOffice/PowerPoint disponível neste ambiente para renderizar +o `.pptx`, a validação usada foi: +1. Integridade: `zipfile.testzip()` + `lxml.etree.fromstring` em cada + `.xml`/`.rels` do pacote. +2. Reconstrução visual manual: ler `off/ext` (EMU) de cada `` e forma + colorida do slide via `python-pptx`, converter para pixels e compor com + Pillow (`Image.alpha_composite`) para gerar um PNG de conferência — é + assim que os desalinhamentos e cores erradas foram detectados nesta + conversa. + +## Git neste repositório + +Não existe identidade de git configurada globalmente por padrão aqui. Em vez +de rodar `git config --global` (proibido pelas regras de segurança do +ambiente), usar override só para o comando do commit: +`git -c user.name="..." -c user.email="..." commit -m "..."`. +`.uploads/` está no `.gitignore` (são anexos de conversa, não arquivos do +projeto) — os artefatos finais que importam ficam em `dist-linux/` e +`dist-windows/`. diff --git a/.claude/skills/pptx-planta-progresso/SKILL.md b/.claude/skills/pptx-planta-progresso/SKILL.md new file mode 100644 index 0000000..c17df4b --- /dev/null +++ b/.claude/skills/pptx-planta-progresso/SKILL.md @@ -0,0 +1,241 @@ +--- +name: pptx-planta-progresso +description: Metodologia para construir um gerador de PPTX de progresso de obra (ou qualquer overlay colorido sobre uma planta/imagem técnica) a partir de uma imagem de planta com grelha de eixos, uma planilha de cronograma e um PPTX-modelo. Use quando o usuário pedir para pintar/marcar status (andamento/concluído) sobre uma planta em um PowerPoint, gerar um slide por período (quinzena/semana/mês) a partir de um cronograma, ou reproduzir esse tipo de relatório visual em um projeto novo (edificação, planta, cronograma diferentes dos já usados). +--- + +# Overlay de progresso sobre planta técnica em PPTX + +Metodologia genérica extraída de um caso real (planta de fundações dividida +em quadrantes, cronograma por Geotecnia/Infraestrutura/Superestrutura, +1 slide por quinzena). Os nomes aqui são genéricos de propósito — adapte aos +nomes reais do projeto em mãos. + +Esta skill é sobre **método**, não sobre um script fixo: os detalhes (nomes +de coluna, quantidade de eixos, cores) sempre vão mudar de projeto para +projeto; o que se repete é a sequência de passos e as pegadinhas abaixo. + +## Visão geral do pipeline + +``` +imagem(ns) da planta ──┐ + ├─→ medir pixels dos eixos → mapear quadrante→retângulo +planilha de cronograma ─┘ │ + ▼ +PPTX-modelo (1 slide-tipo) ──→ duplicar N vezes (1 por período) ──→ pintar + cada + quadrante + conforme + status +``` + +## Passo 1 — Medir os eixos da planta em pixels + +Se a planta tem uma grelha de eixos numerados/rotulados (comum em plantas de +estrutura/fundação: colunas numéricas, linhas em letras), NÃO estime a +posição visualmente — meça programaticamente: + +1. Carregar a imagem em escala de cinza (`PIL` + `numpy`). +2. Binarizar (`pixel < limiar`) e restringir a análise a uma FAIXA estreita + onde só existem os círculos/marcas de eixo (topo da imagem para colunas, + margem esquerda para linhas) — se usar a imagem inteira, hachuras e texto + de dentro do desenho viram ruído e atrapalham a detecção. +3. Somar pixels escuros por coluna (ou por linha) dentro dessa faixa. +4. Agrupar posições consecutivas com valor > 0 (tolerância de alguns pixels + de "buraco") em clusters; o centro de cada cluster (média ponderada pela + intensidade) é a posição do eixo. +5. Filtrar clusters espúrios por LARGURA — um círculo de eixo tem uma largura + característica (~mesma para todos); qualquer cluster muito mais estreito + ou muito mais largo é ruído (seta, cota, texto cruzando a faixa) e deve + ser descartado. Conferir sempre que o número de clusters batidos é igual + ao número de eixos esperado. +6. Fazer o mesmo para linhas, cuidado com textos de cota grandes/rotacionados + que caem na mesma faixa (ex. uma cota vertical entre dois eixos pode gerar + um cluster fantasma — filtrar por largura resolve isso também). + +Sempre medir na imagem **de fato usada no PPTX final** (extraia +`ppt/media/imageN.png` de dentro do `.zip` do `.pptx`, não confie numa cópia +enviada separadamente — arquivos "iguais" podem ter poucos pixels de +diferença de crop/exportação e isso já causou desalinhamento em produção). + +## Passo 2 — Mapear "quadrante" (ou elemento) → retângulo/ponto em pixels + +Se o cronograma se refere a células/trechos da planta por um código (ex. +`"10-11/A1-B"` = colunas 10 a 11, linhas A1 a B), parseie esse código com +regex e resolva os 2 pares de coordenadas usando os dicionários do passo 1. +Se a planta estiver dividida em mais de uma imagem (ex. planta muito larga +exportada em 2 metades), identifique de qual imagem cada elemento faz parte +(geralmente por faixa de coluna) — mas confirme que a granularidade do +cronograma nunca faz um elemento "atravessar" duas imagens; se atravessar, +esse caso precisa de tratamento especial. + +## Passo 3 — Pixel → coordenada do PPTX (EMU) + +Cada imagem colocada num slide tem uma posição/tamanho (`off`/`ext`, em EMU) +no XML do PPTX. A conversão ingênua é interpolação linear: +`emu = off + (pixel / tamanho_da_imagem_em_px) * ext`. + +**Gotcha crítico**: se a imagem tiver um corte aplicado no PowerPoint +(elemento `` dentro do ``), +a interpolação tem que ser feita sobre a região VISÍVEL da imagem, não sobre +a imagem inteira — do contrário tudo fica deslocado (geralmente para baixo +ou para o lado que foi cortado). Sempre inspecionar o XML de cada `` +do slide-modelo antes de assumir que pixel 0 = canto exibido: + +```python +import re +xml = open('slide_extraido.xml').read() +for m in re.finditer(r'.*?', xml, re.S): + print(re.search(r'name="([^"]+)"', m.group(0)).group(1), + re.search(r'', m.group(0))) +``` + +Se houver corte, a fórmula fica: +``` +vis0 = corte_inicio_fraction * tamanho_px +vis1 = (1 - corte_fim_fraction) * tamanho_px +escala = ext / (vis1 - vis0) +emu = off + (pixel - vis0) * escala +``` + +Se a planta estiver em mais de uma imagem lado a lado/empilhada, ainda dá +para conferir que a escala px→EMU bate entre elas (compare a distância entre +dois eixos conhecidos em cada imagem, convertida para EMU) — se bater, dá +para usar um offset constante para alinhar uma imagem à outra visualmente +mesmo que tenham sido exportadas com recortes/margens diferentes. + +## Passo 4 — Decidir COMO pintar cada tipo de status + +Nem todo "status" deve ser um retângulo do tamanho do quadrante inteiro. +Dois padrões comuns, que podem coexistir na mesma planta: + +- **Status de área** (ex. escavação/estaqueamento, algo que ocupa a área + toda do trecho): retângulo cobrindo o quadrante inteiro, com transparência + (alpha < 100%) para a planta de baixo continuar legível. +- **Status de ponto** (ex. pilar, bloco de fundação — elementos que existem + em pontos discretos, normalmente nos vértices/interseções da grelha, não + espalhados pela área): marcador pequeno (quadrado/círculo) centrado em + cada vértice relevante, não um retângulo proporcional ao quadrante. Se um + vértice é compartilhado por elementos vizinhos, faça DEDUPE por ponto + (usar o status mais avançado entre os que compartilham aquele vértice — + ex. "concluído" prevalece sobre "andamento") em vez de desenhar + marcadores sobrepostos. + +Peça sempre uma imagem de referência de como o cliente já visualiza isso +(ou já visualizou em outra ferramenta) antes de assumir qual dos dois +padrões usar — a diferença visual é grande e already causou retrabalho. + +## Passo 5 — Fidelidade de cor + +Se o usuário fornecer uma legenda de referência (imagem), AMOSTRE OS PIXELS +em vez de "chutar" a cor no olho — e use a imagem em maior resolução +disponível (uma captura de tela pequena/comprimida pode alterar o tom o +suficiente pra ficar "quase certo, mas não fiel", que foi motivo de reclamação +real numa entrega). Amostre separadamente o preenchimento (centro do ícone) e +a borda (poucos pixels para dentro da borda visível) — normalmente são cores +diferentes. + +```python +import numpy as np +from PIL import Image +arr = np.array(Image.open('legenda.png').convert('RGB')) +patch = arr[y-5:y+5, x-5:x+5].reshape(-1, 3) +hexcolor = '#%02X%02X%02X' % tuple(patch.mean(axis=0).round().astype(int)) +``` + +Exponha as cores (preenchimento, borda, opacidade) num **arquivo de +configuração separado do código** (JSON simples) para o cliente poder ajustar +sem depender de você re-editar script. + +## Passo 6 — Duplicar o slide-modelo N vezes (1 por período) + +`python-pptx` não tem API pública para duplicar slide. Receita: + +```python +def duplicate_slide(prs, source_index): + source = prs.slides[source_index] + dest = prs.slides.add_slide(source.slide_layout) + for shp in list(dest.shapes): + shp._element.getparent().remove(shp._element) # limpa placeholders da layout + + rid_map = {} + for rId, rel in source.part.rels.items(): + if rel.reltype == RT.SLIDE_LAYOUT or rel.is_external: + continue + rid_map[rId] = dest.part.relate_to(rel.target_part, rel.reltype) + + for shp in source.shapes: + new_el = copy.deepcopy(shp._element) + # IMPORTANTE: reescrever r:embed/r:link direto no atributo XML, + # nunca com replace de string — se o remapeamento de rIds for uma + # permutação (ex. rId2 vira rId3 e rId3 vira rId2), um replace + # sequencial de texto corrompe as duas referências. + for el in new_el.iter(): + for attr in (qn('r:embed'), qn('r:link')): + old = el.get(attr) + if old in rid_map: + el.set(attr, rid_map[old]) + dest.shapes._spTree.append(new_el) + return dest +``` + +Depois de duplicar, reordenar slides manipulando `prs.slides._sldIdLst` +diretamente (mover/remover elementos ``) já que também não há API +pública para isso. + +## Passo 7 — Validar sem PowerPoint/LibreOffice instalado + +Quando não há como renderizar o `.pptx` de verdade: + +1. **Integridade estrutural**: `zipfile.ZipFile(path).testzip()` (sem + corrupção) + `lxml.etree.fromstring` em cada `.xml`/`.rels` do pacote + (sem erro de parse). +2. **Reconstrução visual manual**: ler `left/top/width/height` (EMU) de cada + `` e forma via `python-pptx`, converter para pixels, montar um PNG + com Pillow (`Image.alpha_composite`) reproduzindo o slide. É assim que se + detectam desalinhamentos e cores erradas ANTES de entregar — não confie + só na leitura do XML. + +## Passo 8 — Empacotar como "sem dependências" para o usuário final + +- **Linux**: `pyinstaller --onefile --name NomeDoApp script.py` — direto. +- **Windows, sem acesso a uma máquina/VM Windows nem Wine**: não dá para + compilar um `.exe` de verdade. Alternativa que funciona: montar um Python + "portátil" — + 1. Baixar o zip *embeddable* oficial do python.org + (`python--embed-amd64.zip`). + 2. Editar o `python._pth` de dentro do zip para incluir + `Lib\site-packages` e descomentar `import site` (por padrão o + embeddable vem sem suporte a pacotes de terceiros). + 3. Baixar as wheels certas **mesmo rodando de Linux**, sem precisar de + Wine, com `pip download --platform win_amd64 --python-version + --implementation cp --abi cp --only-binary=:all: ` + (`pip download` só baixa o arquivo, não executa nada — funciona + cross-platform). + 4. Extrair (unzip) cada `.whl` dentro de `Lib\site-packages`. + 5. Lançador: um `.bat` de uma linha chamando + `python\python.exe script.py`, com `pause` no final pra não fechar a + janela sozinho. + - Deixe claro pro usuário que isso é uma alternativa válida a um `.exe` + "de verdade" — funciona igual (zero instalação), mas é uma pasta em vez + de um arquivo único. + - Se for gerar um `.zip` desse pacote, gere em um diretório TEMPORÁRIO + fora da pasta que está sendo zipada, e só depois mova o `.zip` para + dentro — gerar direto dentro da própria pasta faz o zip se + autoincluir (bug real, já aconteceu). + +## Passo 9 — Descoberta automática de arquivos de entrada + +Se o programa final vai rodar numa pasta com 2-3 arquivos de entrada +(config, modelo, dados), não force nomes fixos — detecte por extensão +(`glob.glob('*.xlsx')` etc.), e falhe com mensagem clara se encontrar 0 ou +mais de 1 candidato ("deixe só um arquivo .xlsx na pasta"). Isso deixa o +programa reutilizável em outro projeto só trocando os arquivos de entrada, +sem precisar editar nada. + +## Nota sobre git neste tipo de ambiente sandbox + +Ambientes de execução isolados (containers efêmeros) às vezes não têm +identidade de git configurada e não se deve rodar `git config --global` +(pode ser bloqueado por política, e além disso não é uma boa prática mexer +em config global do usuário). Use override só no comando do commit: +`git -c user.name="..." -c user.email="..." commit -m "..."`.