Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/codegen.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions

name: Preferences code generation

on:
pull_request:
branches: ["master"]
workflow_dispatch:

env:
FILE: freecad/gridfinity_workbench/preferences/auto.py
SCRIPT: codegen_preferences.py

jobs:
codegen:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install .[dev]
- name: Setup for code generation
run: |
mv ${FILE} ${FILE}.bak
- name: Run code generation
run: |
python ${SCRIPT}
- name: Check for differences
run: |
diff ${FILE}.bak ${FILE}
155 changes: 155 additions & 0 deletions codegen_preferences.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""A script to generate python bindings for FreeCAD preferences.

This script parses `.ui` files (which are just XML), and generates a typed python functions for
getting them.

It also runs some checks for the preferences:
- Check if they belong to 'Mod/Gridfinity' group
- Check if there is only a single setter widget for each preference variable
- They have a default value set. This is more needed for code generation itself, as it isn't
incorrect for the XML to ommit them - default ones are used then.
"""

# ruff: noqa: D103, T201, S314

from __future__ import annotations

import re
import sys
import xml.etree.ElementTree as ET
from pathlib import Path

_CLASSES = {
"float": {
"Gui::PrefDoubleSpinBox",
},
"int": {
"Gui::PrefSpinBox",
},
"bool": {
"Gui::PrefCheckBox",
},
}

_WORKBENCH_DIR = Path(__file__).parent / "freecad" / "gridfinity_workbench"
_UI_DIR = _WORKBENCH_DIR / "ui"
_OUTPUT_FILE = _WORKBENCH_DIR / "preferences" / "auto.py"

_EXPECTED_PATH = "Mod/Gridfinity"


def get_value(widget: ET.Element, t: str) -> str | None:
if t == "float":
prop = widget.find("./property[@name='value']/double")
elif t == "int":
prop = widget.find("./property[@name='value']/number")
elif t == "bool":
prop = widget.find("./property[@name='checked']/bool")
return None if prop is None else prop.text.capitalize()
else:
raise AssertionError(f"Unrecognized type {t!r}")
return None if prop is None else prop.text


preferences = {}

for file in _UI_DIR.glob("*.ui"):
for widget in ET.parse(file).getroot().findall(".//widget"):
clazz = widget.attrib["class"]
if not clazz.startswith("Gui::Pref"):
continue

for t, clazzes in _CLASSES.items(): # noqa: B007
if clazz in clazzes:
break
else:
print(f"Unrecognized class {clazz}")
sys.exit(1)

name = widget.find("./property[@name='prefEntry']/cstring").text
path = widget.find("./property[@name='prefPath']/cstring").text

if path != _EXPECTED_PATH:
print(f"Preferences for {name} have path {path!r} instead of {_EXPECTED_PATH!r}")
sys.exit(1)

if name in preferences:
print(f"Multiple preference widgets for entry {name}")
other_file = preferences[name][0]
if file == other_file:
print(f"Both defined in {file.relative_to(_WORKBENCH_DIR)}")
else:
print(
f"Defined in {other_file.relative_to(_WORKBENCH_DIR)} and"
f"{file.relative_to(_WORKBENCH_DIR)}.",
)
sys.exit(1)

value = get_value(widget, t)
if value is None:
print(
f"No default value for property {name!r} of type {t!r} in file "
f"{file.relative_to(_WORKBENCH_DIR)}",
)
print(
"Try changing it to something different, saving the file, changing it back to"
"desired value and saving again.",
)
sys.exit(1)

preferences[name] = (file, t, value)


_TO_SNAKE_CASE_PATERN = re.compile(r"(?<!^)(?=[A-Z])")


def to_snake_case(name: str) -> str:
return _TO_SNAKE_CASE_PATERN.sub("_", name).lower()


_GETTER_CODE = """
def {snake_case}() -> {type}:
# from {file}
return _PARAMS.Get{Type}("{pascal_case}", {default})
"""


def codegen(name: str, file: Path, t: str, default: str) -> str:
return _GETTER_CODE.format(
type=t,
Type=t.capitalize(),
pascal_case=name,
snake_case=to_snake_case(name),
default=default,
file=file.relative_to(_UI_DIR),
)


getters = [codegen(name, *args) for name, args in preferences.items()]
getters.sort()

_FILE_PREFIX = R'''"""This file was auto generated, do not edit it directly!

If you want to:
- change a default value: do it in a `.ui` file.
- change a paramter name: do it in a `.ui` file.
- customize a function behaviour: override it in `__init__.py` in this directory

If you make changes to the `.ui` files, run `codegen_preferences.py` again to update this file.
"""
# fmt: off

import FreeCAD

_PARAMS = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Gridfinity")
'''

_FILE_SUFFIX = R"""
# fmt: on
"""

with Path.open(_OUTPUT_FILE, "w") as f:
f.write(_FILE_PREFIX)
for getter in getters:
f.write(getter)
f.write(_FILE_SUFFIX)
44 changes: 22 additions & 22 deletions freecad/gridfinity_workbench/baseplate_feature_construction.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import FreeCAD as fc # noqa: N813
import Part

from . import const, utils
from . import const, preferences, utils

GridfinityLayout = list[list[bool]]

Expand Down Expand Up @@ -145,28 +145,28 @@ def __init__(self, obj: fc.DocumentObject) -> None:
"<br> <br> Hex is alternative press fit style, inscribed diameter<br> <br>"
"<br> <br> default = 6.2 mm"
),
).MagnetHoleDiameter = const.MAGNET_HOLE_DIAMETER
).MagnetHoleDiameter = preferences.magnet_hole_diameter()

obj.addProperty(
"App::PropertyLength",
"MagnetHoleDepth",
"NonStandard",
"Depth of Magnet Holes <br> <br> default = 2.4 mm",
).MagnetHoleDepth = const.MAGNET_HOLE_DEPTH
).MagnetHoleDepth = preferences.magnet_hole_depth()

obj.addProperty(
"App::PropertyLength",
"MagnetEdgeThickness",
"NonStandard",
"Thickness of edge around magnets <br> <br> default = 1.2 mm",
).MagnetEdgeThickness = const.MAGNET_EDGE_THICKNESS
).MagnetEdgeThickness = preferences.magnet_baseplate_edge_thickness()

obj.addProperty(
"App::PropertyLength",
"MagnetBase",
"NonStandard",
"Thickness of base under the magnets <br> <br> default = 0.4 mm",
).MagnetBase = const.MAGNET_BASE
).MagnetBase = preferences.magnet_baseplate_magnet_base()

obj.addProperty(
"App::PropertyLength",
Expand All @@ -175,14 +175,14 @@ def __init__(self, obj: fc.DocumentObject) -> None:
"Diameter of the hole at the bottom of the magnet cutout"
"<br> Set to zero to make disapear"
"<br> <br> default = 3 mm",
).MagnetBaseHole = const.MAGNET_BASE_HOLE
).MagnetBaseHole = preferences.magnet_baseplate_magnet_base_hole()

obj.addProperty(
"App::PropertyLength",
"MagnetChamfer",
"NonStandard",
"Chamfer at top of magnet hole <br> <br> default = 0.4 mm",
).MagnetChamfer = const.MAGNET_CHAMFER
).MagnetChamfer = preferences.magnet_baseplate_magnet_chamfer()

## Gridfinity Expert Only Parameters
obj.addProperty(
Expand All @@ -191,23 +191,23 @@ def __init__(self, obj: fc.DocumentObject) -> None:
"zzExpertOnly",
"Distance of the magnet holes from bin edge <br> <br> default = 8.0 mm",
1,
).MagnetHoleDistanceFromEdge = const.MAGNET_HOLE_DISTANCE_FROM_EDGE
).MagnetHoleDistanceFromEdge = preferences.magnet_hole_distance_from_edge()

## Gridfinity Hidden Properties
obj.addProperty(
"App::PropertyLength",
"BaseThickness",
"Hidden",
"Thickness of base under the normal baseplate profile <br> <br> default = 6.4 mm",
).BaseThickness = const.BASE_THICKNESS
).BaseThickness = preferences.screw_baseplate_base_thickness()
obj.setEditorMode("BaseThickness", 2)

obj.addProperty(
"App::PropertyBool",
"MagnetHoles",
"ShouldBeHidden",
"MagnetHoles",
).MagnetHoles = const.MAGNET_HOLES
).MagnetHoles = True
obj.setEditorMode("MagnetHoles", 2)

def make(self, obj: fc.DocumentObject, layout: GridfinityLayout) -> Part.Shape:
Expand Down Expand Up @@ -284,7 +284,7 @@ def __init__(self, obj: fc.DocumentObject) -> None:
"ScrewHoleDiameter",
"NonStandard",
"Diameter of screw holes inside magnet holes <br> <br> default = 3 mm",
).ScrewHoleDiameter = const.SCREW_HOLE_DIAMETER
).ScrewHoleDiameter = preferences.screw_hole_diameter()

## Gridfinity Expert Only Parameters
obj.addProperty(
Expand All @@ -293,7 +293,7 @@ def __init__(self, obj: fc.DocumentObject) -> None:
"zzExpertOnly",
"Chamfer of screwholes on the bottom of the baseplate, allows the use of countersuck"
"m3 screws in the bottom up to a bin <br> <br> default = 3 mm",
).MagnetBottomChamfer = const.MAGNET_BOTTOM_CHAMFER
).MagnetBottomChamfer = preferences.screw_baseplate_magnet_bottom_chamfer()

def make(self, obj: fc.DocumentObject, layout: GridfinityLayout) -> Part.Shape:
"""Create screw chamfer for a baseplate.
Expand Down Expand Up @@ -372,7 +372,7 @@ def __init__(self, obj: fc.DocumentObject) -> None:
"ConnectionHoleDiameter",
"NonStandard",
"Holes on the sides to connect multiple baseplates together <br> <br> default = 3.2 mm",
).ConnectionHoleDiameter = const.CONNECTION_HOLE_DIAMETER
).ConnectionHoleDiameter = preferences.screw_baseplate_connection_hole_diameter()

def make(self, obj: fc.DocumentObject) -> Part.Shape:
"""Create connection holes for a baseplate.
Expand Down Expand Up @@ -562,7 +562,7 @@ def __init__(self, obj: fc.DocumentObject) -> None:
"SmallFillet",
"NonStandard",
"Fillets of the main cutout in each grid of the baseplate <br> <br> default = 1 mm",
).SmallFillet = const.BASEPLATE_SMALL_FILLET
).SmallFillet = preferences.magnet_baseplate_small_fillet()

def make(self, obj: fc.DocumentObject, layout: GridfinityLayout) -> Part.Shape:
"""Create baseplate center cutout.
Expand Down Expand Up @@ -632,62 +632,62 @@ def __init__(self, obj: fc.DocumentObject) -> None:
"zzExpertOnly",
"height of chamfer in bottom of bin base profile <br> <br> default = 0.8 mm",
1,
).BaseProfileBottomChamfer = const.BASEPLATE_BOTTOM_CHAMFER
).BaseProfileBottomChamfer = preferences.baseplate_bottom_chamfer()

obj.addProperty(
"App::PropertyLength",
"BaseProfileVerticalSection",
"zzExpertOnly",
"Height of the vertical section in bin base profile",
1,
).BaseProfileVerticalSection = const.BASEPLATE_VERTICAL_SECTION
).BaseProfileVerticalSection = preferences.baseplate_vertical_section()

obj.addProperty(
"App::PropertyLength",
"BaseProfileTopChamfer",
"zzExpertOnly",
"Height of the top chamfer in the bin base profile",
1,
).BaseProfileTopChamfer = const.BASEPLATE_TOP_CHAMFER
).BaseProfileTopChamfer = preferences.baseplate_top_chamfer()

obj.addProperty(
"App::PropertyLength",
"BinOuterRadius",
"zzExpertOnly",
"Outer radius of the bin",
1,
).BinOuterRadius = const.BASEPLATE_OUTER_RADIUS
).BinOuterRadius = preferences.baseplate_outer_radius()

obj.addProperty(
"App::PropertyLength",
"BinVerticalRadius",
"zzExpertOnly",
"Radius of the base profile Vertical section",
1,
).BinVerticalRadius = const.BASEPLATE_VERTICAL_RADIUS
).BinVerticalRadius = preferences.baseplate_vertical_radius()

obj.addProperty(
"App::PropertyLength",
"BinBottomRadius",
"zzExpertOnly",
"bottom of bin corner radius",
1,
).BinBottomRadius = const.BASEPLATE_BOTTOM_RADIUS
).BinBottomRadius = preferences.baseplate_bottom_chamfer()

obj.addProperty(
"App::PropertyLength",
"Clearance",
"zzExpertOnly",
("The Clearance between bin and baseplate <br> <br>default = 0.25 mm"),
).Clearance = const.CLEARANCE
).Clearance = preferences.clearance()

obj.addProperty(
"App::PropertyLength",
"BaseplateTopLedgeWidth",
"zzExpertOnly",
"Top ledge of baseplate, doubled between grids <br> <br> default = 0.4 mm",
1,
).BaseplateTopLedgeWidth = const.BASEPLATE_TOP_LEDGE_WIDTH
).BaseplateTopLedgeWidth = preferences.baseplate_top_ledge_width()

def make(self, obj: fc.DocumentObject) -> None:
"""Generate Rectanble layout and calculate relevant parameters.
Expand Down
Loading