Skip to content

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.py and 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 over Gui.updateGui() with time.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-ssh into a physical desktop GUI session (DISPLAY=:0) on boot.
  • We modified the systemd worker service (coveroq-cad-worker.service) to run with Environment=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's lib/ 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]+)?\b rather 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=500 with 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-queued and bug labels.

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 in webapp/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, and Taskfile.yml.
  • Unmaintained & Stale Modules: Flags files with @arch-generation: v1 metadata 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.Api and command handlers in Coveroq.Cad.Application emit entry traces and error/exit logs with designated LogSource context.
  • 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 via telemetryService.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-queued backlog 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.