CAM: Improvements to machine (#28011)

* CAM:  Improvements to machine

* CAM: SPDX

* lint and translation string problems

* fixing headers and missing toolhead tests

---------

Co-authored-by: tarman3 <[email protected]>
This commit is contained in:
sliptonic
2026-03-09 14:02:31 -04:00
committed by GitHub
co-authored by tarman3
parent a34bad7da4
commit cdd9d685fe
17 changed files with 5434 additions and 3202 deletions
+196 -103
View File
@@ -1,24 +1,32 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2025 Brad Collette *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# ***************************************************************************
# SPDX-License-Identifier: LGPL-2.1-or-later
# SPDX-FileCopyrightText: 2025 Brad Collette
# SPDX-FileNotice: Part of the FreeCAD project.
################################################################################
# #
# FreeCAD is free software: you can redistribute it and/or modify #
# it under the terms of the GNU Lesser General Public License as #
# published by the Free Software Foundation, either version 2.1 #
# of the License, or (at your option) any later version. #
# #
# FreeCAD is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty #
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. #
# See the GNU Lesser General Public License for more details. #
# #
# You should have received a copy of the GNU Lesser General Public #
# License along with FreeCAD. If not, see https://www.gnu.org/licenses #
# #
################################################################################
import FreeCAD
import tempfile
import pathlib
import CAMTests.PathTestUtils as PathTestUtils
from Path.Machine.models.machine import (
from Machine.models.machine import (
Machine,
Spindle,
Toolhead,
OutputOptions,
GCodeBlocks,
ProcessingOptions,
MachineFactory,
)
@@ -58,12 +66,6 @@ class TestMachineDataclass(PathTestUtils.PathTestBase):
machine.add_rotary_axis("C", FreeCAD.Vector(0, 0, 1), -360, 360)
self.assertEqual(machine.machine_type, "xyzac")
# Coordinate system defaults
self.assertEqual(machine.reference_system["X"], FreeCAD.Vector(1, 0, 0))
self.assertEqual(machine.reference_system["Y"], FreeCAD.Vector(0, 1, 0))
self.assertEqual(machine.reference_system["Z"], FreeCAD.Vector(0, 0, 1))
self.assertEqual(machine.tool_axis, FreeCAD.Vector(0, 0, -1))
# Units and versioning
self.assertEqual(machine.configuration_units, "metric")
self.assertEqual(machine.version, 1)
@@ -71,8 +73,9 @@ class TestMachineDataclass(PathTestUtils.PathTestBase):
# Post-processor defaults
self.assertIsInstance(machine.output, OutputOptions)
self.assertIsInstance(machine.blocks, GCodeBlocks)
self.assertIsInstance(machine.processing, ProcessingOptions)
self.assertEqual(machine.postprocessor_file_name, "")
self.assertIsInstance(machine.postprocessor_properties, dict)
def test_custom_initialization(self):
"""Test Machine initialization with custom values and verify machine_type is derived"""
@@ -113,44 +116,99 @@ class TestOutputOptions(PathTestUtils.PathTestBase):
"""Test OutputOptions initialization with defaults"""
opts = OutputOptions()
# Default values
self.assertTrue(opts.comments)
self.assertTrue(opts.blank_lines)
self.assertTrue(opts.header)
# Default values - using current field names
from Machine.models.machine import OutputUnits
self.assertEqual(opts.output_units, OutputUnits.METRIC)
self.assertEqual(opts.command_space, " ")
self.assertEqual(opts.comment_symbol, "(")
self.assertEqual(opts.end_of_line_chars, "\n")
self.assertEqual(opts.line_increment, 10)
self.assertEqual(opts.line_number_start, 100)
self.assertFalse(opts.line_numbers)
self.assertFalse(opts.bcnc_blocks)
self.assertFalse(opts.path_labels)
self.assertFalse(opts.machine_name)
self.assertTrue(opts.tool_change)
self.assertTrue(opts.doubles)
self.assertFalse(opts.adaptive)
self.assertEqual(opts.line_number_prefix, "N")
self.assertTrue(opts.output_comments)
self.assertTrue(opts.output_blank_lines)
self.assertTrue(opts.output_bcnc_comments)
self.assertTrue(opts.output_header)
self.assertFalse(opts.output_labels)
self.assertTrue(opts.output_operation_labels)
self.assertFalse(opts.list_tools_in_header)
self.assertTrue(opts.list_fixtures_in_header)
self.assertFalse(opts.machine_name_in_header)
self.assertTrue(opts.description_in_header)
self.assertTrue(opts.project_file_in_header)
self.assertTrue(opts.output_units_in_header)
self.assertTrue(opts.date_in_header)
self.assertTrue(opts.document_name_in_header)
self.assertTrue(opts.output_duplicate_parameters)
self.assertTrue(opts.output_duplicate_commands)
self.assertEqual(opts.axis_precision, 3)
self.assertEqual(opts.feed_precision, 3)
self.assertEqual(opts.spindle_precision, 0)
def test_custom_initialization(self):
"""Test OutputOptions initialization with custom values"""
from Machine.models.machine import OutputUnits
opts = OutputOptions(
comments=False,
blank_lines=False,
header=False,
output_units=OutputUnits.IMPERIAL,
command_space="",
comment_symbol=";",
end_of_line_chars="\r\n",
line_increment=5,
line_number_start=10,
line_numbers=True,
bcnc_blocks=True,
path_labels=True,
machine_name=True,
tool_change=False,
doubles=False,
adaptive=True,
line_number_prefix="L",
output_comments=False,
output_blank_lines=False,
output_bcnc_comments=False,
output_header=False,
output_labels=True,
output_operation_labels=False,
list_tools_in_header=True,
list_fixtures_in_header=False,
machine_name_in_header=True,
description_in_header=False,
project_file_in_header=False,
output_units_in_header=False,
date_in_header=False,
document_name_in_header=False,
output_duplicate_parameters=False,
output_duplicate_commands=False,
axis_precision=4,
feed_precision=2,
spindle_precision=1,
)
# Verify custom values
self.assertFalse(opts.comments)
self.assertFalse(opts.blank_lines)
self.assertFalse(opts.header)
self.assertEqual(opts.output_units, OutputUnits.IMPERIAL)
self.assertEqual(opts.command_space, "")
self.assertEqual(opts.comment_symbol, ";")
self.assertEqual(opts.end_of_line_chars, "\r\n")
self.assertEqual(opts.line_increment, 5)
self.assertEqual(opts.line_number_start, 10)
self.assertTrue(opts.line_numbers)
self.assertTrue(opts.bcnc_blocks)
self.assertTrue(opts.path_labels)
self.assertTrue(opts.machine_name)
self.assertFalse(opts.tool_change)
self.assertFalse(opts.doubles)
self.assertTrue(opts.adaptive)
self.assertEqual(opts.line_number_prefix, "L")
self.assertFalse(opts.output_comments)
self.assertFalse(opts.output_blank_lines)
self.assertFalse(opts.output_bcnc_comments)
self.assertFalse(opts.output_header)
self.assertTrue(opts.output_labels)
self.assertFalse(opts.output_operation_labels)
self.assertTrue(opts.list_tools_in_header)
self.assertFalse(opts.list_fixtures_in_header)
self.assertTrue(opts.machine_name_in_header)
self.assertFalse(opts.description_in_header)
self.assertFalse(opts.project_file_in_header)
self.assertFalse(opts.output_units_in_header)
self.assertFalse(opts.date_in_header)
self.assertFalse(opts.document_name_in_header)
self.assertFalse(opts.output_duplicate_parameters)
self.assertFalse(opts.output_duplicate_commands)
self.assertEqual(opts.axis_precision, 4)
self.assertEqual(opts.feed_precision, 2)
self.assertEqual(opts.spindle_precision, 1)
def test_equality(self):
"""Test OutputOptions equality comparison"""
@@ -158,63 +216,98 @@ class TestOutputOptions(PathTestUtils.PathTestBase):
opts2 = OutputOptions()
self.assertEqual(opts1, opts2)
opts2.comments = False
opts2.output_comments = False
self.assertNotEqual(opts1, opts2)
class TestSpindle(PathTestUtils.PathTestBase):
"""Test Spindle dataclass"""
class TestProcessingOptions(PathTestUtils.PathTestBase):
"""Test ProcessingOptions dataclass"""
def test_spindle_initialization(self):
"""Test Spindle initialization with defaults"""
spindle = Spindle(
name="Main Spindle",
def test_default_initialization(self):
"""Test ProcessingOptions initialization with defaults"""
opts = ProcessingOptions()
# Default values
self.assertFalse(opts.early_tool_prep)
self.assertFalse(opts.filter_inefficient_moves)
self.assertFalse(opts.split_arcs)
self.assertTrue(opts.tool_change)
self.assertFalse(opts.translate_rapid_moves)
self.assertIsNone(opts.return_to)
def test_custom_initialization(self):
"""Test ProcessingOptions initialization with custom values"""
opts = ProcessingOptions(
early_tool_prep=True,
filter_inefficient_moves=True,
split_arcs=True,
tool_change=False,
translate_rapid_moves=True,
return_to=(10.0, 20.0, 30.0),
)
# Verify custom values
self.assertTrue(opts.early_tool_prep)
self.assertTrue(opts.filter_inefficient_moves)
self.assertTrue(opts.split_arcs)
self.assertFalse(opts.tool_change)
self.assertTrue(opts.translate_rapid_moves)
self.assertEqual(opts.return_to, (10.0, 20.0, 30.0))
def test_equality(self):
"""Test ProcessingOptions equality comparison"""
opts1 = ProcessingOptions()
opts2 = ProcessingOptions()
self.assertEqual(opts1, opts2)
opts2.filter_inefficient_moves = True
self.assertNotEqual(opts1, opts2)
class TestToolhead(PathTestUtils.PathTestBase):
"""Test Toolhead dataclass"""
def test_toolhead_initialization(self):
"""Test Toolhead initialization with defaults"""
toolhead = Toolhead(
name="Main Toolhead",
max_power_kw=5.5,
max_rpm=24000,
min_rpm=1000,
tool_change="automatic",
)
self.assertEqual(spindle.name, "Main Spindle")
self.assertEqual(spindle.max_power_kw, 5.5)
self.assertEqual(spindle.max_rpm, 24000)
self.assertEqual(spindle.min_rpm, 1000)
self.assertEqual(spindle.tool_change, "automatic")
# Default tool axis should be set
self.assertEqual(spindle.tool_axis, FreeCAD.Vector(0, 0, -1))
self.assertEqual(toolhead.name, "Main Toolhead")
self.assertEqual(toolhead.max_power_kw, 5.5)
self.assertEqual(toolhead.max_rpm, 24000)
self.assertEqual(toolhead.min_rpm, 1000)
self.assertEqual(toolhead.tool_change, "automatic")
# Default toolhead_wait should be 0.0
self.assertEqual(toolhead.toolhead_wait, 0.0)
def test_spindle_custom_tool_axis(self):
"""Test Spindle with custom tool axis"""
spindle = Spindle(
name="Side Spindle",
tool_axis=FreeCAD.Vector(1, 0, 0),
)
self.assertEqual(spindle.tool_axis, FreeCAD.Vector(1, 0, 0))
def test_spindle_serialization(self):
def test_toolhead_serialization(self):
"""Test to_dict and from_dict"""
spindle = Spindle(
name="Test Spindle",
id="spindle-001",
toolhead = Toolhead(
name="Test Toolhead",
id="toolhead-001",
max_power_kw=3.0,
max_rpm=18000,
min_rpm=500,
tool_change="manual",
tool_axis=FreeCAD.Vector(0, 1, 0),
toolhead_wait=1.5,
)
data = spindle.to_dict()
self.assertEqual(data["name"], "Test Spindle")
self.assertEqual(data["id"], "spindle-001")
data = toolhead.to_dict()
self.assertEqual(data["name"], "Test Toolhead")
self.assertEqual(data["id"], "toolhead-001")
self.assertEqual(data["max_power_kw"], 3.0)
self.assertEqual(data["tool_axis"], [0, 1, 0])
self.assertEqual(data["toolhead_wait"], 1.5)
restored = Spindle.from_dict(data)
self.assertEqual(restored.name, spindle.name)
self.assertEqual(restored.id, spindle.id)
self.assertEqual(restored.max_power_kw, spindle.max_power_kw)
self.assertEqual(restored.tool_axis, spindle.tool_axis)
restored = Toolhead.from_dict(data)
self.assertEqual(restored.name, toolhead.name)
self.assertEqual(restored.id, toolhead.id)
self.assertEqual(restored.max_power_kw, toolhead.max_power_kw)
self.assertEqual(restored.toolhead_wait, toolhead.toolhead_wait)
class TestMachineFactory(PathTestUtils.PathTestBase):
@@ -257,14 +350,14 @@ class TestMachineFactory(PathTestUtils.PathTestBase):
machine.add_linear_axis("Y", FreeCAD.Vector(0, 1, 0))
machine.add_linear_axis("Z", FreeCAD.Vector(0, 0, 1))
# Add a spindle
spindle = Spindle(
name="Main Spindle",
# Add a toolhead
toolhead = Toolhead(
name="Main Toolhead",
max_power_kw=5.5,
max_rpm=24000,
min_rpm=1000,
)
machine.spindles.append(spindle)
machine.toolheads.append(toolhead)
# Save configuration
filepath = MachineFactory.save_configuration(machine, "test_machine.fcm")
@@ -279,8 +372,8 @@ class TestMachineFactory(PathTestUtils.PathTestBase):
self.assertEqual(loaded_machine.description, "Test description")
self.assertEqual(loaded_machine.machine_type, "xyz")
self.assertEqual(loaded_machine.configuration_units, "metric")
self.assertEqual(len(loaded_machine.spindles), 1)
self.assertEqual(loaded_machine.spindles[0].name, "Main Spindle")
self.assertEqual(len(loaded_machine.toolheads), 1)
self.assertEqual(loaded_machine.toolheads[0].name, "Main Toolhead")
def test_save_configuration_auto_filename(self):
"""Test saving with automatic filename generation"""
@@ -305,7 +398,7 @@ class TestMachineFactory(PathTestUtils.PathTestBase):
# The data structure has nested "machine" key
self.assertIn("machine", data)
self.assertEqual(data["machine"]["name"], "New Machine")
self.assertIn("spindles", data["machine"])
self.assertIn("toolheads", data["machine"])
def test_list_configuration_files(self):
"""Test listing available configuration files"""
@@ -381,9 +474,9 @@ class TestMachineFactory(PathTestUtils.PathTestBase):
configuration_units="metric",
)
# Add spindle
machine.spindles.append(
Spindle(
# Add toolhead
machine.toolheads.append(
Toolhead(
name="Main",
max_power_kw=7.5,
max_rpm=30000,
@@ -391,9 +484,9 @@ class TestMachineFactory(PathTestUtils.PathTestBase):
)
# Configure post-processor settings
machine.output.output_comments = False
machine.output.axis_precision = 4
machine.output.line_increment = 5
machine.output.comments.enabled = False
machine.output.precision.axis = 4
machine.output.formatting.line_increment = 5
# line_increment is set to default 10 in OutputOptions
@@ -404,7 +497,7 @@ class TestMachineFactory(PathTestUtils.PathTestBase):
# Verify all components
self.assertEqual(loaded.name, machine.name)
self.assertEqual(loaded.manufacturer, machine.manufacturer)
self.assertEqual(len(loaded.spindles), 1)
self.assertFalse(loaded.output.output_comments)
self.assertEqual(loaded.output.axis_precision, 4)
self.assertEqual(loaded.output.line_increment, 5)
self.assertEqual(len(loaded.toolheads), 1)
self.assertFalse(loaded.output.comments.enabled)
self.assertEqual(loaded.output.precision.axis, 4)
self.assertEqual(loaded.output.formatting.line_increment, 5)
+8 -8
View File
@@ -171,17 +171,17 @@ SET(PathPythonToolsGui_SRCS
)
SET(PathPythonMachineUi_SRCS
Path/Machine/ui/__init__.py
Machine/ui/__init__.py
)
SET(PathPythonMachineUiEditor_SRCS
Path/Machine/ui/editor/machine_editor.py
Path/Machine/ui/editor/__init__.py
Machine/ui/editor/machine_editor.py
Machine/ui/editor/__init__.py
)
SET(PathPythonMachineModels_SRCS
Path/Machine/models/__init__.py
Path/Machine/models/machine.py
Machine/models/__init__.py
Machine/models/machine.py
)
SET(PathPythonToolsToolBit_SRCS
@@ -927,21 +927,21 @@ INSTALL(
FILES
${PathPythonMachineModels_SRCS}
DESTINATION
Mod/CAM/Path/Machine/models
Mod/CAM/Machine/models
)
INSTALL(
FILES
${PathPythonMachineUi_SRCS}
DESTINATION
Mod/CAM/Path/Machine/ui
Mod/CAM/Machine/ui
)
INSTALL(
FILES
${PathPythonMachineUiEditor_SRCS}
DESTINATION
Mod/CAM/Path/Machine/ui/editor
Mod/CAM/Machine/ui/editor
)
INSTALL(
@@ -0,0 +1,156 @@
{
"freecad_version": "1.2.0",
"machine": {
"axes": {
"X": {
"joint": [
[
1.0,
0.0,
0.0
],
[
0,
0,
0
]
],
"max": 1000,
"max_velocity": 10000,
"min": 0.0,
"sequence": 0,
"type": "linear"
},
"Y": {
"joint": [
[
0.0,
1.0,
0.0
],
[
0,
0,
0
]
],
"max": 1000,
"max_velocity": 10000,
"min": 0,
"sequence": 0,
"type": "linear"
},
"Z": {
"joint": [
[
0.0,
0.0,
1.0
],
[
0,
0,
0
]
],
"max": 1000,
"max_velocity": 10000,
"min": 0,
"sequence": 0,
"type": "linear"
}
},
"description": "3 axis mill using Grbl controller",
"manufacturer": "",
"name": "Generic Grbl",
"spindles": [
{
"id": "spindle1",
"max_power_kw": 1.5,
"max_rpm": 24000,
"min_rpm": 0,
"name": "Spindle 1",
"tool_axis": [
0.0,
0.0,
-1.0
],
"tool_change": "manual",
"coolant_flood": false,
"coolant_mist": false,
"coolant_delay": 0.0,
"spindle_wait": 0.0
}
],
"units": "metric"
},
"output": {
"units": "metric",
"output_tool_length_offset": true,
"output_header": true,
"header": {
"include_date": true,
"include_description": true,
"include_document_name": true,
"include_machine_name": false,
"include_project_file": true,
"include_units": true,
"include_tool_list": false,
"include_fixture_list": true
},
"comments": {
"enabled": true,
"symbol": "(",
"include_operation_labels": true,
"include_blank_lines": true,
"output_bcnc_comments": false
},
"formatting": {
"line_numbers": false,
"line_number_start": 100,
"line_number_prefix": "N",
"line_increment": 10,
"command_space": " ",
"end_of_line_chars": "\n"
},
"precision": {
"axis": 3,
"feed": 3,
"spindle": 0
},
"duplicates": {
"commands": true,
"parameters": true
}
},
"postprocessor": {
"file_name": "generic",
"properties": {
"supports_tool_radius_compensation": false,
"supported_commands": "G0\nG00\nG1\nG01\nG2\nG02\nG3\nG03\nG73\nG74\nG81\nG82\nG83\nG84\nG38.2\nG54\nG55\nG56\nG57\nG58\nG59\nG59.1\nG59.2\nG59.3\nG59.4\nG59.5\nG59.6\nG59.7\nG59.8\nG59.9\nM0\nM00\nM1\nM01\nM3\nM03\nM4\nM04\nM6\nM06",
"drill_cycles_to_translate": "G73\nG81\nG82\nG83",
"preamble": "G17 G90",
"postamble": "M5\nG17 G90\nM2",
"safetyblock": "",
"pre_operation": "",
"post_operation": "",
"pre_tool_change": "",
"post_tool_change": "",
"pre_job": "",
"post_job": "",
"pre_fixture_change": "",
"post_fixture_change": "",
"pre_rotary_move": "",
"post_rotary_move": "",
"tool_return": ""
}
},
"processing": {
"early_tool_prep": false,
"filter_inefficient_moves": false,
"split_arcs": false,
"tool_change": false,
"translate_rapid_moves": true
},
"version": 1
}
@@ -0,0 +1,135 @@
{
"freecad_version": "1.2.0",
"machine": {
"axes": {
"X": {
"type": "linear",
"role": "table_linear",
"parent": null,
"sequence": 0,
"joint": {
"origin": [0, 0, 0],
"axis": [1, 0, 0]
},
"limits": {
"min": 0,
"max": 1000
},
"max_velocity": 10000
},
"Y": {
"type": "linear",
"role": "table_linear",
"parent": null,
"sequence": 0,
"joint": {
"origin": [0, 0, 0],
"axis": [0, 1, 0]
},
"limits": {
"min": 0,
"max": 1000
},
"max_velocity": 10000
},
"Z": {
"type": "linear",
"role": "head_linear",
"parent": null,
"sequence": 0,
"joint": {
"origin": [0, 0, 0],
"axis": [0, 0, 1]
},
"limits": {
"min": 0,
"max": 1000
},
"max_velocity": 10000
}
},
"description": "Basic LinuxCNC configuration",
"manufacturer": "",
"name": "Generic LinuxCNC",
"toolheads": [
{
"id": "toolhead1",
"name": "Toolhead 1",
"toolhead_type": "rotary",
"tool_change": "manual",
"max_power_kw": 3.0,
"min_rpm": 6000,
"max_rpm": 24000,
"coolant_flood": false,
"coolant_mist": false,
"coolant_delay": 0.0,
"toolhead_wait": 0.0
}
],
"units": "metric"
},
"output": {
"units": "metric",
"output_tool_length_offset": true,
"output_header": true,
"header": {
"include_date": true,
"include_description": true,
"include_document_name": true,
"include_machine_name": false,
"include_project_file": true,
"include_units": true,
"include_tool_list": false,
"include_fixture_list": true
},
"comments": {
"enabled": true,
"symbol": "(",
"include_operation_labels": true,
"include_blank_lines": true,
"output_bcnc_comments": true
},
"formatting": {
"line_numbers": false,
"line_number_start": 100,
"line_number_prefix": "N",
"line_increment": 10,
"command_space": " ",
"end_of_line_chars": "\n"
},
"precision": {
"axis": 3,
"feed": 3,
"spindle": 0
},
"duplicates": {
"commands": true,
"parameters": true
}
},
"postprocessor": {
"file_name": "linuxcnc",
"properties": {
"supports_tool_radius_compensation": true,
"supported_commands": "G0\nG00\nG1\nG01\nG2\nG02\nG3\nG03\nG73\nG74\nG81\nG82\nG83\nG84\nG38.2\nG54\nG55\nG56\nG57\nG58\nG59\nG59.1\nG59.2\nG59.3\nG59.4\nG59.5\nG59.6\nG59.7\nG59.8\nG59.9\nM0\nM00\nM1\nM01\nM3\nM03\nM4\nM04\nM6\nM06",
"drill_cycles_to_translate": "",
"preamble": "G17 G54 G40 G49 G80 G90",
"postamble": "M05\nG17 G54 G90 G80 G40\nM2",
"safetyblock": "G40 G49 G80",
"pre_operation": "",
"post_operation": "",
"pre_tool_change": "",
"post_tool_change": "",
"blend_mode": "BLEND",
"blend_tolerance": 0.0
}
},
"processing": {
"early_tool_prep": false,
"filter_inefficient_moves": false,
"split_arcs": false,
"tool_change": true,
"translate_rapid_moves": false
},
"version": 1
}
@@ -0,0 +1,150 @@
{
"blocks": {
"postamble": "M05\nG17 G90\nM2",
"preamble": "G17 G90"
},
"freecad_version": "1.2.0",
"machine": {
"axes": {
"X": {
"joint": [
[
1.0,
0.0,
0.0
],
[
0,
0,
0
]
],
"max": 500,
"max_velocity": 10000,
"min": 0,
"sequence": 0,
"type": "linear"
},
"Y": {
"joint": [
[
0.0,
1.0,
0.0
],
[
0,
0,
0
]
],
"max": 300,
"max_velocity": 10000,
"min": 0,
"sequence": 0,
"type": "linear"
},
"Z": {
"joint": [
[
0.0,
0.0,
1.0
],
[
0,
0,
0
]
],
"max": 300,
"max_velocity": 10000,
"min": 0,
"sequence": 0,
"type": "linear"
}
},
"description": "3 axis CNC using SmoothieBoard controller",
"manufacturer": "Smoothieware",
"name": "Generic SmoothieBoard",
"spindles": [
{
"id": "spindle1",
"max_power_kw": 2.2,
"max_rpm": 24000,
"min_rpm": 0,
"name": "Spindle 1",
"tool_axis": [
0.0,
0.0,
-1.0
],
"tool_change": "manual",
"coolant_flood": false,
"coolant_mist": false,
"coolant_delay": 0.0
}
],
"units": "metric"
},
"output": {
"units": "metric",
"output_tool_length_offset": true,
"output_header": true,
"header": {
"include_date": true,
"include_description": true,
"include_document_name": true,
"include_machine_name": false,
"include_project_file": true,
"include_units": true,
"include_tool_list": false,
"include_fixture_list": true
},
"comments": {
"enabled": true,
"symbol": "(",
"include_operation_labels": true,
"include_blank_lines": true,
"output_bcnc_comments": true
},
"formatting": {
"line_numbers": false,
"line_number_start": 100,
"line_number_prefix": "N",
"line_increment": 10,
"command_space": " ",
"end_of_line_chars": "\n"
},
"precision": {
"axis": 3,
"feed": 3,
"spindle": 0
},
"duplicates": {
"commands": true,
"parameters": true
}
},
"postprocessor": {
"args": "",
"file_name": "smoothie"
},
"processing": {
"drill_cycles_to_translate": [
"G73",
"G81",
"G82",
"G83"
],
"early_tool_prep": false,
"filter_inefficient_moves": false,
"spindle_wait": 0.0,
"split_arcs": false,
"suppress_commands": [],
"tool_change": true,
"translate_drill_cycles": false,
"translate_rapid_moves": false
},
"version": 1
}
+52
View File
@@ -0,0 +1,52 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# SPDX-FileCopyrightText: 2025 Billy Huddleston <[email protected]>
# SPDX-FileNotice: Part of the FreeCAD project.
################################################################################
# #
# FreeCAD is free software: you can redistribute it and/or modify #
# it under the terms of the GNU Lesser General Public License as #
# published by the Free Software Foundation, either version 2.1 #
# of the License, or (at your option) any later version. #
# #
# FreeCAD is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty #
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. #
# See the GNU Lesser General Public License for more details. #
# #
# You should have received a copy of the GNU Lesser General Public #
# License along with FreeCAD. If not, see https://www.gnu.org/licenses #
# #
################################################################################
from .machine import (
Machine,
MachineFactory,
LinearAxis,
RotaryAxis,
Toolhead,
ToolheadType,
MachineUnits,
OutputUnits,
OutputOptions,
ProcessingOptions,
)
# Legacy compatibility aliases
Spindle = Toolhead
SpindleType = ToolheadType
__all__ = [
"Machine",
"MachineFactory",
"LinearAxis",
"RotaryAxis",
"Toolhead",
"ToolheadType",
"Spindle", # Legacy alias
"SpindleType", # Legacy alias
"MachineUnits",
"OutputUnits",
"OutputOptions",
"ProcessingOptions",
]
File diff suppressed because it is too large Load Diff
@@ -1,10 +1,10 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# SPDX-FileCopyrightText: 2025 Billy Huddleston <[email protected]>
# SPDX-FileNotice: Part of the FreeCAD project.
################################################################################
# #
# © 2026 Billy Huddleston <[email protected]> #
# #
# FreeCAD is free software: you can redistribute it and/or modify #
# it under the terms of the GNU Lesser General Public License as #
# published by the Free Software Foundation, either version 2.1 #
+25
View File
@@ -0,0 +1,25 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# SPDX-FileCopyrightText: 2025 Billy Huddleston <[email protected]>
# SPDX-FileNotice: Part of the FreeCAD project.
################################################################################
# #
# FreeCAD is free software: you can redistribute it and/or modify #
# it under the terms of the GNU Lesser General Public License as #
# published by the Free Software Foundation, either version 2.1 #
# of the License, or (at your option) any later version. #
# #
# FreeCAD is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty #
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. #
# See the GNU Lesser General Public License for more details. #
# #
# You should have received a copy of the GNU Lesser General Public #
# License along with FreeCAD. If not, see https://www.gnu.org/licenses #
# #
################################################################################
"""Machine editor package."""
from .machine_editor import MachineEditorDialog
__all__ = ["MachineEditorDialog"]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,155 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# SPDX-FileCopyrightText: 2025 Billy Huddleston <[email protected]>
# SPDX-FileNotice: Part of the FreeCAD project.
################################################################################
# #
# FreeCAD is free software: you can redistribute it and/or modify #
# it under the terms of the GNU Lesser General Public License as #
# published by the Free Software Foundation, either version 2.1 #
# of the License, or (at your option) any later version. #
# #
# FreeCAD is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty #
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. #
# See the GNU Lesser General Public License for more details. #
# #
# You should have received a copy of the GNU Lesser General Public #
# License along with FreeCAD. If not, see https://www.gnu.org/licenses #
# #
################################################################################
"""
Helper module for managing postprocessor properties in the machine editor.
"""
from PySide import QtGui
from typing import Dict, Any
class PostProcessorPropertyManager:
"""Manages dynamic creation and handling of postprocessor property widgets."""
@staticmethod
def create_property_widget(prop: Dict[str, Any], current_value: Any) -> QtGui.QWidget:
"""Create a widget for a postprocessor property based on its type.
Args:
prop: Property schema dictionary with keys: name, type, label, default, help, etc.
current_value: Current value of the property
Returns:
QWidget configured for the property type, with a value_getter() method
"""
prop_type = prop.get("type")
if prop_type == "bool":
widget = QtGui.QCheckBox()
widget.setChecked(current_value if current_value is not None else False)
widget.value_getter = lambda: widget.isChecked()
return widget
elif prop_type == "int":
widget = QtGui.QSpinBox()
widget.setRange(prop.get("min", -999999), prop.get("max", 999999))
widget.setValue(current_value if current_value is not None else 0)
widget.value_getter = lambda: widget.value()
return widget
elif prop_type == "float":
widget = QtGui.QDoubleSpinBox()
widget.setRange(prop.get("min", -999999.0), prop.get("max", 999999.0))
widget.setDecimals(prop.get("decimals", 4))
widget.setValue(current_value if current_value is not None else 0.0)
widget.value_getter = lambda: widget.value()
return widget
elif prop_type == "choice":
widget = QtGui.QComboBox()
choices = prop.get("choices", [])
for choice in choices:
widget.addItem(str(choice), choice)
if current_value:
index = widget.findData(current_value)
if index >= 0:
widget.setCurrentIndex(index)
widget.value_getter = lambda: widget.itemData(widget.currentIndex())
return widget
elif prop_type == "text":
widget = QtGui.QPlainTextEdit()
widget.setMaximumHeight(100)
widget.setPlainText(current_value if current_value else "")
widget.value_getter = lambda: widget.toPlainText()
return widget
elif prop_type == "file":
# File picker widget
container = QtGui.QWidget()
layout = QtGui.QHBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
line_edit = QtGui.QLineEdit()
line_edit.setText(current_value if current_value else "")
browse_button = QtGui.QPushButton("...")
browse_button.setMaximumWidth(30)
def browse_file():
filters = prop.get("filters", "All Files (*)")
filename, _ = QtGui.QFileDialog.getOpenFileName(
container, "Select File", "", filters
)
if filename:
line_edit.setText(filename)
browse_button.clicked.connect(browse_file)
layout.addWidget(line_edit)
layout.addWidget(browse_button)
container.value_getter = lambda: line_edit.text()
return container
else: # Default to string
widget = QtGui.QLineEdit()
widget.setText(str(current_value) if current_value else "")
widget.value_getter = lambda: widget.text()
return widget
@staticmethod
def connect_property_widget(
widget: QtGui.QWidget, prop_name: str, prop_type: str, machine, callback=None
):
"""Connect a property widget to update the machine configuration.
Args:
widget: The widget to connect
prop_name: Name of the property
prop_type: Type of the property
machine: Machine object to update
callback: Optional callback function to call after update
"""
def update_property():
if hasattr(widget, "value_getter"):
value = widget.value_getter()
machine.postprocessor_properties[prop_name] = value
if callback:
callback(prop_name, value)
# Connect appropriate signal based on widget type
if isinstance(widget, QtGui.QCheckBox):
widget.stateChanged.connect(update_property)
elif isinstance(widget, QtGui.QSpinBox) or isinstance(widget, QtGui.QDoubleSpinBox):
widget.valueChanged.connect(update_property)
elif isinstance(widget, QtGui.QComboBox):
widget.currentIndexChanged.connect(update_property)
elif isinstance(widget, QtGui.QPlainTextEdit):
widget.textChanged.connect(update_property)
elif isinstance(widget, QtGui.QLineEdit):
widget.textChanged.connect(update_property)
elif isinstance(widget, QtGui.QWidget):
# For composite widgets (like file picker), find the line edit
line_edit = widget.findChild(QtGui.QLineEdit)
if line_edit:
line_edit.textChanged.connect(update_property)
@@ -1,48 +0,0 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * *
# * Copyright (c) 2025 Billy Huddleston <[email protected]> *
# * *
# * This file is part of FreeCAD. *
# * *
# * FreeCAD is free software: you can redistribute it and/or modify it *
# * under the terms of the GNU Lesser General Public License as *
# * published by the Free Software Foundation, either version 2.1 of the *
# * License, or (at your option) any later version. *
# * *
# * FreeCAD is distributed in the hope that it will be useful, but *
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
# * Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Lesser General Public *
# * License along with FreeCAD. If not, see *
# * <https://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
from .machine import (
Machine,
MachineFactory,
LinearAxis,
RotaryAxis,
Spindle,
MachineUnits,
OutputUnits,
OutputOptions,
GCodeBlocks,
ProcessingOptions,
)
__all__ = [
"Machine",
"MachineFactory",
"LinearAxis",
"RotaryAxis",
"Spindle",
"MachineUnits",
"OutputUnits",
"OutputOptions",
"GCodeBlocks",
"ProcessingOptions",
]
File diff suppressed because it is too large Load Diff
@@ -1,26 +0,0 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * *
# * Copyright (c) 2025 Billy Huddleston <[email protected]> *
# * *
# * This file is part of FreeCAD. *
# * *
# * FreeCAD is free software: you can redistribute it and/or modify it *
# * under the terms of the GNU Lesser General Public License as *
# * published by the Free Software Foundation, either version 2.1 of the *
# * License, or (at your option) any later version. *
# * *
# * FreeCAD is distributed in the hope that it will be useful, but *
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
# * Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Lesser General Public *
# * License along with FreeCAD. If not, see *
# * <https://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
"""Machine editor package."""
from .machine_editor import MachineEditorDialog
__all__ = ["MachineEditorDialog"]
File diff suppressed because it is too large Load Diff
+20 -22
View File
@@ -1,33 +1,31 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# SPDX-FileCopyrightText: 2025 Samuel Abels <[email protected]>
# SPDX-FileNotice: Part of the FreeCAD project.
# ***************************************************************************
# * Copyright (c) 2025 Samuel Abels <[email protected]> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
################################################################################
# #
# FreeCAD is free software: you can redistribute it and/or modify #
# it under the terms of the GNU Lesser General Public License as #
# published by the Free Software Foundation, either version 2.1 #
# of the License, or (at your option) any later version. #
# #
# FreeCAD is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty #
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. #
# See the GNU Lesser General Public License for more details. #
# #
# You should have received a copy of the GNU Lesser General Public #
# License along with FreeCAD. If not, see https://www.gnu.org/licenses #
# #
################################################################################
import pathlib
import tempfile
import FreeCAD
import Path
from PySide import QtGui, QtCore
from ....Machine.ui.editor import MachineEditorDialog
from ....Machine.models import MachineFactory
from Machine.ui.editor import MachineEditorDialog
from Machine.models.machine import MachineFactory
translate = FreeCAD.Qt.translate
+20 -25
View File
@@ -1,36 +1,31 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# SPDX-FileCopyrightText: 2025 sliptonic <[email protected]>
# SPDX-FileNotice: Part of the FreeCAD project.
################################################################################
# #
# FreeCAD is free software: you can redistribute it and/or modify #
# it under the terms of the GNU Lesser General Public License as #
# published by the Free Software Foundation, either version 2.1 #
# of the License, or (at your option) any later version. #
# #
# FreeCAD is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty #
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. #
# See the GNU Lesser General Public License for more details. #
# #
# You should have received a copy of the GNU Lesser General Public #
# License along with FreeCAD. If not, see https://www.gnu.org/licenses #
# #
################################################################################
# flake8: noqa import
# ***************************************************************************
# * Copyright (c) 2016 sliptonic <[email protected]> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import TestApp
from CAMTests.TestCAMSanity import TestCAMSanity
from CAMTests.TestLinkingGenerator import TestGetLinkingMoves
from CAMTests.TestMachine import (
TestMachineDataclass,
TestMachineFactory,
TestSpindle,
)
from CAMTests.TestMachine import TestMachineDataclass, TestMachineFactory, TestToolhead
from CAMTests.TestPathProfile import TestPathProfile
from CAMTests.TestPathAdaptive import TestPathAdaptive