Imported from Winklaa22/GeodeticSketchGenerator (
AGENTS.md). Install upstream withnpx skills add Winklaa22/GeodeticSketchGenerator. Copyright stays with the author.
AGENTS.md
Guidance for coding agents working in this repository.
What this is
Geodetic Sketch Generator — a PyQt6 desktop app that turns geodetic survey point files (TXT) into DXF drawings: point/line/pipe/height/cable-mark plotting from coordinate data, a CAD-like DXF viewer/editor with undo/redo and an AutoCAD-style command line, multi-sheet layout with a configurable title-block table, and PDF export. The repo name (AutocadScriptGenerator) predates the current scope.
Commands
- Run the app:
python main.py - Run tests:
python -m pytest -q - Run a single test:
python -m pytest tests/test_commands.py::test_add_point_command_execute_and_undo -q - Install deps:
pip install -r requirements.txt - Pre-commit hooks (trailing-whitespace/EOF/yaml/toml checks + a full pytest run):
pre-commit run --all-files(not installed into.git/hooksin a fresh checkout — run manually orpre-commit installfirst)
Tests cover only core/ and models/ (command execute/undo, parsing, geometry/pattern math,
plot math, project and table-template JSON round-tripping). There's no pytest-qt; the ui/ PyQt6
layer isn't exercised by the suite.
Architecture
Three layers, dependencies flow one way: models → core → ui. models and core never
import ui or PyQt6 — that boundary is what keeps core/ testable headlessly.
models/ — Point, plain data.
core/ — all business logic:
parser.py—PointFileParserreads a geodetic point TXT file (space/tab-delimited, auto-detected, optional header row) intoDict[int, Point].dxf_document.py—DXFDocumentwraps anezdxfDrawing; every DXF mutation (entities, layers, colors) goes through it rather than touchingezdxfdirectly.commands/— undo/redo Command pattern.base.Commandis theexecute(doc)/undo(doc)protocol;history.CommandHistoryholds the undo/redo stacks (capped atDEFAULT_MAX_DEPTH = 200);composite.CompositeCommandgroups several commands into one undo step. Concrete commands:draw.py(add point/line/circle/text/polyline),edit.py(move/rotate/scale/delete/duplicate),layers.py,text.py(entity text/color edits),survey.py(a registry of point-set-to-drawing builders keyed byDrawMode, looked up viaget_survey_builder; the four label-bearing builders — points/heights/cable-marks/measurements — stageLabelRequests instead of computing their own offsets, seelabel_placement.pybelow).survey_draw_service.py—SurveyDrawService.build_command()(single mode) andbuild_commands()(several modes at once, needed so label classes get solved together) are the entry points from parsed points +GenerationConfig(s) toCommand(s), dispatching throughcommands/survey.py::build_survey_commands.label_placement.py—solve_label_positions(labels, obstacles, marker_radius)is the label coordinate solver: given every pendingLabelRequest(across all label classes) plusObstacles(marker circles, route segments) it returns a centre point and a hard-collision count per label. Ring/direction candidates around each anchor, an iterated-conditional-modes sweep, and a Voronoi-cell constraint (never closer to another label's anchor than to your own) keep it from just drifting outward to "solve" collisions. Pure geometry — noezdxf, noDXFDocument, noDrawModeawareness.commands/survey.pyis the only caller.geometry.py/patterns.py— direction/angle math between consecutive survey points and routed-path recognition (boxes/wedges) used by the survey builders.draw_modes.py/config.py— theDrawModeenum and the per-mode*Optionsdataclasses (PointsOptions,HeightsOptions,CableOptions, ...) that feed the survey builders.session.py—EditorSessionholds the loaded point file and its parse result;AppState(EMPTY/READY/APPLIED/ERROR) is derived from it, not stored separately.project.py—ProjectState(plus nested*Statedataclasses, one per mode/tab) is the full serializable app state;save_project/load_projectread/write.gsgprojJSON.open_any()also accepts a bare.dxf/.txtfor import.table_template.py— the title-block table model (TableTemplate: columns/rows/cells/fields)..gsgtablefiles save/load one independently of a project;project.pymirrors the same shape asTableTemplateStatefor embedding in.gsgproj.sheets.py,plot.py— sheet/layout state (SheetSet) and print/plot math (page sizing, scale, stroke width) that feeds PDF export.validation.py,exceptions.py—AppErrorsubclasses raised bycore/; the UI catches these at its boundary and shows the message instead of letting anything raw surface.
ui/ — PyQt6, built around two top-level windows swapped via window_router.py::WindowRouter
(closes the old window, shows the new one — never both open at once):
start_screen.py— recent-projects launcher (new/import/open).editor/window.py::MainWindow— the main editor. It composes controllers rather than doing the work itself:DocumentController(point/DXF file I/O),ProjectController(.gsgprojsave/load/rename),LayoutController(sheets),TableTemplateController. The left-side option tabs (editor/tabs/) are driven byeditor/mode_registry.py::MODE_SPECS— one entry perDrawMode, mapping a tab widget ↔ itsproject.pystate dataclass ↔ itsconfig.pyoptions dataclass. Add a new draw mode there rather than hand-wiring a tab intoMainWindow.dxf/viewer.py::DxfViewer— the CAD canvas. Owns the liveDXFDocument+CommandHistory, renders it into aQGraphicsScenevia ezdxf's drawing add-on through a custombackend.py::QtSceneBackend, and hosts the AutoCAD-style command line (dxf/command_line.py+dxf/interpreter.py::DxfCommandInterpreter, e.g.LINE,MOVE,ROTATE,ZOOM) alongside click-driven tool sessions (dxf/tools/) for the same operations.dxf/pdf_export.py— renders sheets (page frame + title-block table) to PDF using the plot options fromcore/plot.py.theme/— design tokens (tokens.py), a qtawesome-basedIconManager, and the app stylesheet; pull spacing/color from here rather than hardcoding values in widget code.
Conventions
- New code is comment-free and docstring-free — an intentional, already-applied project-wide style. Match it; don't add comments or docstrings to new code.
- Every module starts with
from __future__ import annotations. - User-facing failures are raised as
AppErrorsubclasses (core/exceptions.py), not generic exceptions, so the UI boundary can catch and display them by type.
