Skip to content

Developer Guide

Marius Stanciu edited this page Oct 29, 2026 · 1 revision

This page is for people who change the source code of FlatCAM Evo. It describes the structure of the code, how to run it from source, how the tests work, and how to write a plugin, a Tcl command, or a preprocessor.

Where to start

  • The entry point is flatcam.py. It starts the App class in appMain.py. The architecture is described in the sections below.
  • Most changes touch one of these folders: appPlugins/ (tools), appEditors/ (editors), appObjects/ (project objects), appParsers/ (file formats), appGUI/ (widgets and preferences), tclCommands/ (scripting), preprocessors/ (G-code output).

Architecture

Core flow

flatcam.py (entry) -> App (appMain.py) -> MainGUI + Objects + Editors + Plugins + Parsers
  • App (appMain.py) is the central class. It holds the GUI, the object collection, the editors, the tools, the parsers, the handlers, and the preferences. Each component gets an app reference.
  • camlib.py holds the geometry and CNC library: the base classes Gerber, Excellon, Geometry, and CNCjob, and the geometry helpers.

Object model

FlatCAMObj (appObjects/AppObjectTemplate.py)  - base of all project objects
  GerberObject, ExcellonObject, GeometryObject, CNCJobObject, ScriptObject, DocumentObject
  • Objects are created by the new_object() factory. They are registered in ObjectCollection (the project tree) and drawn on the VisPy canvas.
  • File import is in appHandlers/appIO.py. It calls the parsers in appParsers/ and creates the objects.

Plugins

  • Every tool inherits AppTool (appTool.py).
  • The lifecycle is: __init__(app), then install() (adds the menu entry and the shortcut), then run() (opens the tool), then connect_signals() or set_tool_ui().
  • Connect and disconnect the widgets with ui_connect() and ui_disconnect(), so that a signal is not connected twice.
  • Two layouts exist in appPlugins/:
    • Single-file plugins: one file with the tool class and the UI class (for example appPlugins/ToolDrilling/Drilling.py).
    • Split plugins: one folder with the controller, the UI, the model, and the generator. The file names carry the tool name. ToolNCC and ToolPaint use Ncc.py, NccUI.py, NccModel.py, and NccGen.py (and Paint.py, PaintUI.py, PaintModel.py, PaintGen.py).
  • The plugins are imported in appPlugins/__init__.py. appMain.py creates each tool (for example self.ncclear_tool = ToolNcc(self)) and calls install().

Editors

  • Three editors with their own plugin folders: appEditors/appGerberEditor.py with grb_plugins/, appGeoEditor.py with geo_plugins/, and appExcEditor.py with exc_plugins/.
  • Editor plugins inherit AppToolEditor and use the EditorTool and EditorUI pattern.

Preprocessors

  • The files in preprocessors/ are loaded by appPreProcessor.py at start-up. Each class registers itself by its class name. See Preprocessors.

Tcl commands

  • tclCommands/ has one class for each command, for example TclCommandDelete. The base class is TclCommand (tclCommands/TclCommand.py). TclCommandSignaled adds asynchronous execution with a signal that blocks until the work is done.
  • See Tcl Scripting for the use of the commands.

Preferences

  • defaults.py holds the factory values (AppDefaults). AppOptions holds the values in use.
  • The preferences UI is in appGUI/preferences/, one folder for each section. PreferencesUIManager.py links the widgets to the option keys and saves them.
  • A tool syncs its own widgets with the preferences (storage_to_form() and form_to_storage()).

Tools database

  • The Tools Database is appDatabase.py (the UI) and appCommon/ToolDatabase.py (the SQLite storage). See Tools Database.

Run from source

Requirements

  • Python 3.11 or newer.
  • PyQt6, VisPy, Shapely, NumPy, rtree, ezdxf, lxml, svglib, reportlab, and the other packages in requirements.txt.
  • Optional: requirements-optional.txt (OR-Tools for the path optimization, and other extras).
  • A conda or mamba environment is also possible: environment.yml.

Steps

Windows (PowerShell, in the repository folder):

python -m venv .venv
.venv\Scripts\python.exe -m pip install --upgrade pip
.venv\Scripts\python.exe -m pip install -r requirements.txt
.venv\Scripts\python.exe flatcam.py

Linux or macOS:

python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python flatcam.py
  • On Linux, GDAL may need the system library. The README gives the exact steps.
  • A file can be given on the command line: python flatcam.py board.FlatPrj.
  • Headless mode runs without a window: python flatcam.py --headless --command="..." (see Command Line and Headless).
  • The user data (preferences, the Tools Database, the logs, and your own preprocessors) is in the data folder. See Preferences Overview.

Tests

The tests are in tests/ and use pytest. They run without a display. Never start the GUI for a test run.

Commands

Run these from the repository root, with the .venv interpreter (PowerShell on Windows).

When Command Time
While you work on an area .venv\Scripts\python.exe -m pytest tests\test_<area>.py -q -p no:cacheprovider seconds
Before each commit .venv\Scripts\python.exe tests\gates\run_core.py about 35 to 60 s
Before each push .venv\Scripts\python.exe tests\gates\run_gate.py about 1 to 2 minutes
After a change of test files .venv\Scripts\python.exe tests\gates\check_gate.py seconds
  • run_core.py runs every test file except the slow ones listed in tests/gates/slow_files.txt. It compares the outcomes with tests/gates/outcome_baseline.json. A new test file is in the core by default.
  • run_gate.py runs the whole suite and compares it with the baseline.
  • --workers 0 runs one serial process. Use it to find a test that leaves a global state for the next test.
  • The test budget is in tests/gates/test_budget.json. Above the budget the gate prints a warning and does not fail.
  • The README of the tests folder is tests/README_gates.md.

Markers and options

Marker or variable Effect
@pytest.mark.hardware and FLATCAM_TEST_HARDWARE=1 Tests that need a real machine. Off by default.
@pytest.mark.timing and FLATCAM_TEST_TIMING=1 Tests that measure time on the GUI thread. Off by default.
@pytest.mark.diptrace and FLATCAM_TEST_DIPTRACE=1 Slow DipTrace importer tests. Off by default.

Writing tests

  • Put a test in the topical file of its area (tests/test_<area>.py). Do not create a new file for each ticket.
  • Test what the user sees: the wrong output, the wrong geometry, a lost undo step, a corrupt file, an unsafe machine move.
  • Do not test private attributes, the order of calls, the source text, or the exact text of a log.
  • Wait for a state in a GUI test (a loop with qWait and a limit), not for a fixed time.

Writing a plugin

  1. Create a folder in appPlugins/, or one file (for a small tool). Use the tool name as the prefix of the files.
  2. Write the controller class. Inherit AppTool:
from appTool import AppTool

class ToolMyTool(AppTool):
    def __init__(self, app):
        AppTool.__init__(self, app)
        self.app = app

    def install(self, icon=None, separator=None, **kwargs):
        # adds the menu entry; see appTool.py for the arguments (icon, separator, shortcut, pos)
        AppTool.install(self, icon=icon, separator=separator, **kwargs)

    def run(self, toggle=True):
        AppTool.run(self)
        self.set_tool_ui()
        self.connect_signals()

    def connect_signals(self):
        self.ui_connect()

    def ui_connect(self):
        # connect the widget signals here; pair each one with a disconnect in ui_disconnect()
        pass

    def ui_disconnect(self):
        pass

    def set_tool_ui(self):
        # build or refresh the widgets of the tool
        pass

The arguments and the base behavior of each method are in appTool.py. Look at appPlugins/ToolNCC/Ncc.py for a complete example.

  1. Write the UI class (the widgets, with _() around each text that the user reads).
  2. Write the generator (the algorithm) in a separate file with no widgets. It can then be tested without the GUI.
  3. Import the plugin in appPlugins/__init__.py.
  4. Create it in appMain.py next to the other tools, and call install().
  5. Add the preferences in defaults.py and in the preferences UI.
  6. Add the tests (see above).

Rules from the repository:

  • Use ui_connect() and ui_disconnect() pairs.
  • Do not use mixins.
  • Use quad_segs = steps // 4 for the circle segments.
  • Use history_of(app) for an undo step.

Writing a Tcl command

  1. Create tclCommands/TclCommandMyCommand.py. The module name must start with TclCommand and the class name must be the same as the module name.
  2. Inherit TclCommand (or TclCommandSignaled for a long task).
  3. Set the attributes and the execute method:
import collections
from tclCommands.TclCommand import TclCommand

class TclCommandMyCommand(TclCommand):
    aliases = ['my_command']
    description = '%s %s' % ("--", "Does something with the object.")

    arg_names = collections.OrderedDict([
        ('name', str),
    ])

    option_types = collections.OrderedDict([
        ('outname', str),
    ])

    required = ['name']

    def execute(self, args, unnamed_args):
        # the object is found by its name in the project; see tclCommands/TclCommandDelete.py for a full example
        return 'OK'
  • aliases are the command names. arg_names are the positional arguments. option_types are the options (-name value). required lists the mandatory names.
  • tclCommands/__init__.py loads every module of the folder with pkgutil, so a new file is found without an import line.
  • The Tcl commands do not plot. Keep the work in the object, not in the command.
  • Add a test in the tcl test file of the area.

Writing a preprocessor

See Preprocessors for the methods, the variables, and the folders. A preprocessor is a Python class that inherits PreProc (milling) or AppPreProcTools (solder paste) and implements all abstract methods.

Contributing

  • Report bugs and ask for features in the GitHub issues. Attach the Gerber, Excellon, or project file, and the log.txt from the data folder.
  • Pull requests are welcome. Make them against the development branch 1.0, and add tests for the changes.
  • Run run_core.py before each commit and run_gate.py before a push.
  • Keep the changes small and focused. One commit for one change.
  • Translations are in the locale folder. See Translating.

See also

Clone this wiki locally