zxmarcos/engecad

★ 0Forks 0PythonGitHub ↗Compare

README

EngeCAD

CAD livre para mapeamento e plantas cadastrais: desenhe sobre ortofoto georreferenciada, com sistema de coordenadas de verdade, precisão topográfica na entrada por teclado e automação em Python.

Formato de trabalho DXF e leitura nativa de DWG AC1018/AC1021/AC1024/AC1027/AC1032. O leitor DWG é implementado no próprio projeto, seguindo o contêiner binário, compressão, mapas de páginas, índices de handles e fluxos de entidades da especificação ODA; a edição aberta é materializada no modelo geométrico interno.

Versão 0.2.0 · licença GPL-3.0 · Python 3.10–3.13

Instalação

O PySide6 ainda não publica wheels para o Python 3.14, então o venv precisa ser criado explicitamente com o 3.13 (ou 3.12):

py -3.13 -m venv .venv
.venv\Scripts\activate
pip install -e ".[dev]"

Rodar:

python -m engecad
python -m engecad planta.dxf     # abre um desenho direto
python -m engecad planta.dwg     # abre um DWG diretamente

O que já funciona

Interface Ribbon escura em 5 guias, com todos os comandos, ícones vetoriais, acesso rápido e modo compacto (Ctrl+F1)
Vista pan (botão do meio), zoom ancorado no cursor, grade adaptativa, escala 1:N
Coordenadas CRS via PROJ/pyproj, SIRGAS 2000 / UTM pré-configurado, E/N ao vivo na barra de status
Imagem de fundo GeoTIFF/COG, JP2, ECW (ver abaixo), reprojeção on-the-fly, leitura decimada por overviews
Desenho linha, polilinha, retângulo, círculo, arco por 3 pontos, texto
Cotas DXF linear, alinhada, rotacionada, angular, raio, diâmetro, arco e ordenada; associativas e com DIMSTYLE métrico
Hachuras DXF sólido, 172 padrões métricos/ANSI e .PAT; ilhas, escala, ângulo, cor, transparência, área e limites associativos
Carimbos blocos A0–A4 em paisagem/retrato, escala de plotagem e 13 campos editáveis persistidos como atributos DXF
Blocos BLOCK, INSERT, WBLOCK, explosão recursiva, atributos editáveis e parâmetros dinâmicos simplificados
Símbolos biblioteca topográfica/cadastral com 14 símbolos nativos e tamanho anotativo conforme a escala
Seleção clique, janela (→) e captura (←), Shift para somar/tirar, realce ao passar o cursor
Grips esticar vértice, mover entidade, mudar raio e ângulo — arrastando, sem comando
Edição mover, copiar, girar, espelhar, escalar, paralela, aparar, estender, apagar
Snap extremidade, ponto médio, centro, quadrante, interseção, próximo, grade
Camadas visibilidade, cor ACI, camada corrente
Medição DIST (distância + azimute em GMS), AREA (área + perímetro + hectares)
Arquivo abrir/salvar DXF R2018 + abrir DWG nativo + sidecar .emap.json
Importar shapefile (.shp, com reprojeção via .prj e separação em camadas por atributo)
Automação console Python embutido (F9) e execução de arquivos .py

Entrada de coordenada — o que dá precisão topográfica

Com uma ferramenta ativa, digite na linha de comando:

500123.45,7412987.12    absoluto no CRS do projeto
@10,0                   relativo ao último ponto
@10<90                  polar CAD (anti-horário a partir do leste)
@10<<45                 por AZIMUTE (horário a partir do norte)
@100<<45d30'20"         azimute em grau/minuto/segundo

O << para azimute é a forma direta de lançar um memorial descritivo.

Digitar com o foco no desenho já cai na linha de comando — não precisa clicar lá embaixo antes de cada comando.

A Ribbon organiza as ferramentas em Início, Inserir, Anotar, Vista e Gerenciar. Dê dois cliques numa guia ou use Ctrl+F1 para recolhê-la e ganhar área de desenho; os atalhos e os comandos digitados continuam os mesmos.

Comandos

Desenhar Modificar Vista / geral
LINE L MOVE M ZE enquadrar tudo
PLINE PL COPY CO ZOOM Z
RECT REC ROTATE RO ESCALA 500 → 1:500
CIRCLE C MIRROR MI PAN P
ARC A SCALE SC GRADE F7
TEXT T OFFSET O OSNAP F3
HATCH H HATCHEDIT HE CARIMBO / CARIMBOEDIT
BLOCK B EXPLODE X INSERT I · WBLOCK W
SIMBOLO ATTEDIT DYNEDIT · ESCALAANOTATIVA
DIMLINEAR DLI
DIMALIGNED DAL
DIMANGULAR DAN
DIMRADIUS DRA
DIMDIAMETER DDI
DIMARC DAR
DIMORDINATE DOR
DIMREASSOCIATE DRE
DIMDISASSOCIATE DDA
TRIM TR CAMADA LA
EXTEND EX U / REDO
ERASE E / Del AJUDA F1
SELTUDO Ctrl+A DIST · AREA

Dentro de TRIM e EXTEND, digitar U desfaz o último corte sem sair da ferramenta. Enter no prompt vazio repete o último comando.

As cotas criadas sobre pontos de OSNAP ficam associadas às entidades de origem e acompanham alterações de vértices, raios, MOVE, ROTATE e SCALE. Pontos digitados permanecem fixos. Se uma origem for apagada, a cota fica órfã sem perder seu último valor; DIMREASSOCIATE refaz os vínculos e DIMDISASSOCIATE os remove deliberadamente.

HATCH usa os contornos fechados que estiverem selecionados; sem seleção, solicita um ponto interno e encontra automaticamente a menor região fechada. As hachuras acompanham MOVE, ROTATE, SCALE e edição por grips dos limites. O diálogo também carrega padrões AutoCAD .PAT. CARIMBO insere um bloco nativo na escala corrente da vista, com formato A0–A4, orientação, folha, revisão, responsável, CREA/CAU, imóvel, matrícula, CRS e demais campos editáveis.

BLOCK transforma a seleção em uma definição DXF reutilizável; INSERT abre as definições e a biblioteca incorporada; WBLOCK grava uma definição ou seleção como DXF independente; EXPLODE decompõe inclusive blocos aninhados e converte atributos em textos. ATTEDIT edita os ATTRIB, e DYNEDIT oferece largura, altura, rotação, espelhamento e estados de visibilidade sem criar entidades proprietárias. Os símbolos são anotativos: o tamanho informado em milímetros de papel é recalculado por ESCALAANOTATIVA ou ESCALA.

Em Projeto › Dados do projeto, os dados cadastrais ficam centralizados no sidecar e alimentam novos carimbos; carimbos existentes são sincronizados ao salvar as alterações.


Sobre o ECW — leia antes de reclamar que não abre

ECW é formato proprietário da Hexagon. A leitura é gratuita no desktop, mas a biblioteca não é redistribuível — é por isso que nem o QGIS a embute, e por isso que o GDAL que vem nos wheels do rasterio não traz o driver.

Consequência: com um pip install puro não dá para ler nem converter ECW. Precisa existir na máquina um GDAL com o driver.

O EngeCAD tenta, nesta ordem:

  1. abrir direto com o GDAL embutido;
  2. procurar um GDAL externo com ECW (%ENGECAD_GDAL_BIN%, C:\OSGeo4W\bin, C:\Program Files\QGIS*\bin) e oferecer conversão para COG, feita uma única vez — o COG depois navega mais rápido que o ECW original;
  3. se não achar nada, explicar como instalar.

Para habilitar ECW: instale o OSGeo4W e marque o pacote gdal-ecw. Depois reabra o EngeCAD.

Para saber o que a sua máquina suporta: Ajuda › Diagnóstico de raster (ECW).

O GDAL externo é chamado por subprocess, nunca importado. Carregar duas cópias de GDAL/PROJ no mesmo processo Python é causa clássica de crash silencioso no Windows.


Sobre o DWG

DWG é formato proprietário da Autodesk — ler/escrever nativamente exigiria licenciar código da Autodesk. O caminho livre é o ODA File Converter, gratuito, distribuído pela Open Design Alliance: converte DWG↔DXF localmente, sem internet depois de instalado. O DXF continua sendo o formato nativo do EngeCAD; o DWG é só entrada/saída para interoperar com AutoCAD e afins.

Para habilitar: baixe o ODA File Converter, instale (fica em C:\Program Files\ODA File Converter <versão>) e reabra o EngeCAD — ele encontra o executável automaticamente. Para apontar para uma instalação fora do padrão, use a variável de ambiente ENGECAD_ODA_BIN.

  • Arquivo › Abrir DWG... converte para DXF em memória e abre como um DXF de terceiro (mantém o CRS do projeto corrente se não houver sidecar).
  • Arquivo › Exportar DWG... salva o desenho corrente como .dwg, sem alterar o .dxf nativo nem o caminho salvo do documento.

Para saber o que a sua máquina suporta: Ajuda › Diagnóstico de DWG.

Assim como o GDAL externo, o ODA File Converter é chamado por subprocess, nunca importado.


Console Python

F9 abre o console. A API já está no escopo, sem import:

add_line((500000, 7400000), (500050, 7400000))
add_polyline([(0,0), (10,0), (10,10)], closed=True, layer="LIMITE")

e = add_line((0,0), (10,0))
move(e, 0, 25)                # dx, dy
copy(e, 12, 0)                # devolve as cópias
rotate(e, (0,0), 90)          # graus, anti-horário
scale(e, (0,0), 2.0)
offset(e, 3.0)                # paralela; >0 = à esquerda do trajeto

add_linear_dimension((0,0), (10,0), (0,2))
add_aligned_dimension((0,0), (10,5), (5,8))
add_radius_dimension((20,0), 5, (27,2))

create_block("MARCO", selected(), base=(500000, 7400000))
insert_block("MARCO", (500010, 7400010), attributes={"ID": "M-01"})
insert_symbol("PONTO_TOPOGRAFICO", (500020, 7400020), {"PONTO": "101"})
wblock("biblioteca/marco.dxf", block_name="MARCO")

select_all(); selected(); deselect()
area([(0,0), (10,0), (10,10), (0,10)])       # 100.0
azimuth((0,0), (10,0))                        # 90.0
to_wgs84(500000, 7400000)                     # (-51.0, -23.5101947)
command("LINE")                               # o mesmo comando da barra

Cada execução vira um único item de desfazer. Um script que cria 500 entidades some com um Ctrl+Z; um script que levanta exceção no meio não deixa metade do trabalho no desenho. Veja examples/.


Arquitetura — as decisões que sustentam o resto

1. Precisão: nenhuma coordenada UTM chega ao Qt. Uma coordenada UTM tem magnitude ~7,4×10⁶. Entregue crua ao rasterizador do Qt (via QTransform ou QGraphicsView), ela é processada internamente em precisão simples e o desenho passa a tremer na casa do meio metro. Por isso render/viewport.py faz toda a transformação em float64 no Python e entrega ao painter apenas coordenadas de tela, que são números pequenos. É também a razão de o canvas ser um QWidget próprio, e não QGraphicsView.

2. Registro de comandos único. Linha de comando, console Python e (v0.4) AutoLISP despacham todos por core/registry.py. Construir esse funil desde o começo é o que fará o interpretador LISP ser um plugue, e não uma reescrita.

3. Undo por instantâneo, não por matriz inversa. Desfazer um MOVE aplicando a matriz inversa parece elegante, mas não devolve o valor idêntico: medido em ezdxf, mover 500010.0 e voltar dá 500009.99999999907. São nanômetros — irrelevantes em campo, mas num CAD o desfazer tem de devolver exatamente o número que estava lá, e a deriva acumula. core/snapshot.py guarda os valores crus, o que de quebra serve para edições que não são transformações afins (aparar, estender, arrastar grip), onde não existe matriz inversa nenhuma.

4. Apagar não destrói. unlink_entity tira a entidade do layout mas a mantém viva, para o desfazer poder religá-la. Consequência sutil: is_alive não serve para saber se algo foi apagado — o critério certo é a entidade ainda estar no índice do documento (core/selection.py).

engecad/
  core/        geometria, documento, CRS, undo, snapshot, seleção, picking,
               grips, offset, aparar, índice espacial, registro     (SEM Qt)
  render/      viewport, canvas, camada raster, tema
  io/          DXF, DWG nativo, sidecar .emap.json, importação de raster
               (cadeia ECW) e de shapefile
  dwg/         contêiner AC18/R2007+, bitstream, índice de objetos e entidades
  tools/       ferramentas interativas (máquinas de estado)
  snap/        motor de osnap
  ui/          janela, linha de comando, painel de camadas, diálogo de CRS
  scripting/   API pública e console

core/ não importa Qt — geometria, CRS, undo e toda a matemática de edição são testáveis sem abrir janela.


Testes

pytest -q

319 testes. Os que mais importam:

  • ida e volta mundo → tela → mundo com coordenada UTM real (exigido < 1 mm; medido: 0);
  • zoom ancorado 60× sem deriva do ponto sob o cursor;
  • mover e desfazer devolvendo a coordenada exata, e 50 ciclos undo/redo sem deriva;
  • aparar terminando exatamente sobre o círculo (interseção analítica, não achatada);
  • round-trip DXF: criar → salvar → reabrir → geometria idêntica;
  • cotas associativas acompanhando MOVE, SCALE, grips, interseções e alteração de raio, inclusive depois de salvar/reabrir e em ciclos de undo/redo;
  • snap escolhendo o candidato certo por prioridade e por raio em pixels;
  • script no console desfazendo num passo, e script que falha não deixando resíduo;
  • desenhar sobre a ortofoto e conferir que o DXF salvo tem a coordenada do mundo real;
  • importar shapefile em CRS diferente do projeto e conferir a reprojeção via .prj.

Os testes de interface sobem a aplicação inteira em modo offscreen, simulam cliques e arrastos com QMouseEvent de verdade e chegam a verificar os pixels pintados.


Roteiro

Versão Entrega
0.1 ver, desenhar, salvar, console Python
0.2 (atual) seleção, grips, edição completa, arco/círculo/retângulo/texto
0.3 plantas cadastrais: hachura, blocos, quadro de áreas, memorial descritivo
0.4 AutoLISP: interpretador próprio + command, entmake, ssget, carga de .lsp
0.5 espaço papel, escala de plotagem, carimbo, PDF georreferenciado
0.6 SHP / GeoPackage / KML, tiles XYZ e WMS
0.7 instalador Windows, sistema de plugins

Construído sobre

PySide6 (LGPL) · ezdxf (MIT) · pyproj (MIT) · rasterio (BSD) · NumPy · Shapely

Contributors

zxmarcosweltonlopes

Issues