Developer's Notes: FreeCAD Automation & TechDraw Scripting
Document Metadata
🗓️ Last Modified: 2026-09-29
👤 Author / Owner:@JakubDedik&@Jules
🏷️ Status: Active Reference
🔗 Linked System Component: Headless FreeCAD GUI Runner (runner.py), TechDraw HLR Engine, PySide Window Manager
This document summarizes the architectural insights, troubleshooting steps, failures, and final successful implementation discovered while building a headless parametric CAD generation and drawing export pipeline using FreeCAD on an Ubuntu server.
1. Core Findings & FreeCAD Architecture
FreeCAD has a dual-layer architecture:
* App Layer (Database): Handles 3D geometric modeling, spreadsheet values, and feature trees. This layer operates purely on coordinates and mathematical shapes (Part::TopoShape).
* Gui Layer (Representation): Handles Qt-based UI widgets, 3D viewport rendering (via Coin3D), and 2D drawing sheets (via the TechDraw HLR projection engine).
The Caching Bug (Why standard recomputes fail in scripts)
When modifying a spreadsheet cell via Python (sheet.set()) and calling doc.recompute(), the C++ shape definition updates perfectly. However, the TechDraw 2D projection views (DrawViewPart) remain stuck at the original template dimensions.
This happens because the Hidden Line Removal (HLR) projection engine is part of the Gui layer. In command-line scripting mode (freecad -c or freecad script.py), FreeCAD bypasses the Qt window event loops. Because these event loops are inactive:
1. The Gui ViewProviders for the 3D shapes do not update.
2. The HLR engine reads stale geometry from the visual cache instead of the live database.
3. The drawing exports show the original default template shapes, even if text labels are overridden.
2. Our Failures (What did NOT work)
Before arriving at the final solution, we tested several logical but unsuccessful approaches:
A. Headless xvfb-run without a window manager
- Attempt: Running
xvfb-run -a freecad script.pyand calling.touch()/.recompute()on the TechDraw views. - Result: Failed. Without a window manager mapping the windows, Qt paints nothing. The views remained completely un-rendered.
B. Programmatic .recompute() or ForceUpdate in-memory
- Attempt: Touching view objects programmatically, setting
page.ViewObject.ForceUpdate = True, and looping overGui.updateGui()withtime.sleep(). - Result: Failed. The HLR cache was locked. FreeCAD's C++ dependency graph did not register the change from Spreadsheet -> Box -> TechDraw View because the UI event bindings were not instantiated in-memory.
C. Undocumented C++ API calls (TechDrawGui.redrawPage)
- Attempt: Invoking
TechDrawGui.redrawPage(page)to force a redraw. - Result: Failed with
AttributeError: module 'TechDrawGui' has no attribute 'redrawPage'. This method is not exposed in FreeCAD's Python bindings for this version.
3. Our Successes (What DID work)
We achieved success by matching the manual user GUI workflow exactly inside our script, combined with a persistent virtual desktop environment.
A. The "Save, Close, and Re-open" Lifecycle
Instead of trying to update the cached geometry in-memory, we forced FreeCAD to reload the document database from scratch:
1. Modify: Set the spreadsheet cell values (sheet.set("B1", "250")).
2. Save: Save the document to a temporary file (temp_update.fcstd). This writes the spreadsheet updates directly to the ZIP-based XML structure on disk.
3. Close & Re-open: Close the document and immediately re-open the temporary file. This forces the GUI ViewProviders to instantiate themselves fresh using the updated spreadsheet values.
4. MDI Tab Focus: Programmatically switch the active tab in the Qt window manager to the Drawing sheet tab using PySide:
python
mdi_area = mw.findChild(QtWidgets.QMdiArea)
mdi_area.setActiveSubWindow(drawing_subwindow)
5. Redraw GUI Command: Trigger the official GUI-level redraw command:
python
Gui.runCommand("TechDraw_RedrawPage")
This mimics the user clicking the "Redraw Page" button in the toolbar, forcing a clean, correctly-scaled HLR recalculation.
B. Graphical Autologin & Display Access (DISPLAY=:0)
- Instead of virtual X framebuffers (which restrict manual monitor view) or virtual VNC sessions, we configured LightDM to auto-login
admin-sshinto a physical desktop GUI session (DISPLAY=:0) on boot. - We modified the systemd worker service (
coveroq-cad-worker.service) to run withEnvironment=DISPLAY=:0 XAUTHORITY=/home/admin-ssh/.Xauthority. - This grants the background worker daemon authorized access to launch the FreeCAD GUI window directly on your monitor screen, making the desktop viewable and fully capable of drawing sheet generation.
4. Key Lessons for Developers
- GUI commands require focus: FreeCAD GUI Commands (like
Gui.runCommand("TechDraw_RedrawPage")) execute against the currently active window/tab in the Qt MDI workspace. You must focus the Drawing tab first via PySide before running the command. - Conda vs. System Libraries: When running FreeCAD in a Conda environment, Qt6 may complain about missing platform libraries (like
libxcb-cursor0). Symlinking the system library directly into the Conda environment'slib/directory resolves this cleanly:bash ln -s /usr/lib/x86_64-linux-gnu/libxcb-cursor.so.0 ~/miniforge3/envs/cad-env/lib/ - Stale Subprocesses in SSH: Stopping a parent command in SSH does not automatically kill background GUI processes spawned by it. Always ensure proper cleanup (
killall -9 freecad) to prevent zombie processes from eating up system RAM and triggering the Linux Out-Of-Memory (OOM) Killer. - Test Suite Validation: Watch out for strict regex matches on dimensions. FreeCAD exports drawing dimensions as integers (
130) instead of decimals (130.0) depending on the template precision. Use word-boundary checks like\b[0-9]+(?:\.[0-9]+)?\brather than expecting floating points.
5. Telemetry Log Sentinel & Automated Jules Dispatch
To ensure production health without requiring continuous manual inspection of /api/logs, the Hourly Telemetry Log Sentinel operates autonomously via Coveroq.Cad.Orchestrator (dotnet run --project backend-dotnet/src/Coveroq.Cad.Orchestrator -- audit-logs).
Key Capabilities
- Log Ingestion: Downloads the latest 500 log events via
GET /api/log?limit=500with admin authentication. - Anomaly Classification: Filters out routine operational traffic (health checks, keepalives) and isolates:
- Uncaught exceptions & HTTP 500 responses.
- CAD worker heartbeat timeouts & disconnections.
- FreeCAD subprocess / script execution failures.
- Security authentication alerts.
- Deduplication & Task Dispatch: Searches existing open issues and creates strongly typed tasks linked to parent stories under Project 1 with
jules-queuedandbuglabels.
Cron Setup on Station pc-jakub
To run the sentinel hourly on station pc-jakub, add the following crontab entry:
0 * * * * cd /home/admin-ssh/3D-platforma && dotnet run --project backend-dotnet/src/Coveroq.Cad.Orchestrator -- audit-logs --dispatch-jules >> /home/admin-ssh/audit_logs.log 2>&1
Alternatively, GitHub Actions executes the sentinel on a schedule via .github/workflows/telemetry-log-sentinel.yml.
6. Daily Codebase Hygiene Sentinel
To maintain repository cleanliness and prevent drift or orphaned files across development cycles, the Daily Codebase Hygiene Sentinel operates via Coveroq.Cad.Orchestrator (dotnet run --project backend-dotnet/src/Coveroq.Cad.Orchestrator -- audit-hygiene).
Audited Categories
- Orphaned / Unreferenced Files: Identifies scripts in
cad/scripts/that are not imported or referenced in runner/options or tests, as well as modules inwebapp/src/not imported or referenced in web pages, modules, HTML files, or test suites. - Deploy & Infrastructure Discrepancies: Checks for missing local file references in
Dockerfile,docker-compose.yml, andTaskfile.yml. - Unmaintained & Stale Modules: Flags files with
@arch-generation: v1metadata that have not been modified for over 60 days, as well as modules explicitly marked as@completeness: Deprecated.
Automated Execution
The hygiene sentinel is scheduled to run daily at 02:00 UTC via .github/workflows/codebase-hygiene-sentinel.yml on pc-jakub runner. When discrepancies are found, it can generate an aggregated GitHub Issue using --create-issue.
7. Hourly Telemetry Log Coverage Sentinel
To guarantee complete observability across the entire platform and enforce strict trace standards aligned with InMemoryLogSink.cs, the Hourly Telemetry Log Coverage Sentinel operates via Coveroq.Cad.Orchestrator (dotnet run --project backend-dotnet/src/Coveroq.Cad.Orchestrator -- audit-coverage).
Key Capabilities
- Backend Coverage: Verifies that all Minimal API endpoints in
Coveroq.Cad.Apiand command handlers inCoveroq.Cad.Applicationemit entry traces and error/exit logs with designatedLogSourcecontext. - Frontend Coverage: Inspects all TypeScript page orchestrators (
webapp/src/pages/) and core services (webapp/src/services/) to ensure user interactions and async calls emit structured logs viatelemetryService.sendLog(LogLevel, message, LogSource). - Automated Jules Task Dispatch: When log coverage falls below the required threshold (85%), the sentinel automatically creates a targeted refactoring task in the
jules-queuedbacklog with the exact list of untraced functions.
Cron Setup on Station pc-jakub
To run the coverage sentinel hourly (offset at minute 30), add the following crontab entry:
30 * * * * cd /home/admin-ssh/3D-platforma && dotnet run --project backend-dotnet/src/Coveroq.Cad.Orchestrator -- audit-coverage --dispatch-jules >> /home/admin-ssh/audit_coverage.log 2>&1
Alternatively, GitHub Actions executes the sentinel hourly via .github/workflows/telemetry-coverage-sentinel.yml.