Merge pull request #26533 from Connor9220/Machine
CAM: Add Machine Library and Editor
This commit is contained in:
164
generate_machine_box.py
Normal file
164
generate_machine_box.py
Normal file
@@ -0,0 +1,164 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
FreeCAD Macro: Generate Machine Boundary Box
|
||||
|
||||
This macro creates a wireframe box representing the working envelope
|
||||
of a CNC machine based on its configuration.
|
||||
|
||||
COORDINATE SYSTEM:
|
||||
- Uses MACHINE coordinates (absolute travel limits of the machine)
|
||||
- Not work coordinates (relative to workpiece)
|
||||
- Shows the full extent the machine can move in X, Y, Z directions
|
||||
|
||||
Author: Generated for FreeCAD CAM
|
||||
"""
|
||||
|
||||
import FreeCAD
|
||||
import Part
|
||||
import Path
|
||||
from Path.Machine.models.machine import MachineFactory
|
||||
import os
|
||||
|
||||
def get_machine_file():
|
||||
"""Prompt user to select a machine configuration file."""
|
||||
# Get available machine files
|
||||
machines = MachineFactory.list_configuration_files()
|
||||
machine_names = [name for name, path in machines if path is not None]
|
||||
|
||||
if not machine_names:
|
||||
FreeCAD.Console.PrintError("No machine configuration files found.\n")
|
||||
return None
|
||||
|
||||
# For now, use the first machine. In a real macro, you'd use a dialog
|
||||
# to let the user choose
|
||||
selected_name = machine_names[0] # Default to first
|
||||
selected_path = None
|
||||
for name, path in machines:
|
||||
if name == selected_name and path:
|
||||
selected_path = path
|
||||
break
|
||||
|
||||
if not selected_path:
|
||||
FreeCAD.Console.PrintError("Could not find selected machine file.\n")
|
||||
return None
|
||||
|
||||
return selected_path
|
||||
|
||||
def create_machine_boundary_box(machine_file, color=(1.0, 0.0, 0.0), line_width=2.0, draw_style="Dashed"):
|
||||
"""Create a wireframe box showing machine boundaries.
|
||||
|
||||
Args:
|
||||
machine_file: Path to the machine configuration file
|
||||
color: RGB tuple for wire color (default: red)
|
||||
line_width: Width of the wires (default: 2.0)
|
||||
draw_style: "Solid", "Dashed", or "Dotted" (default: "Dashed")
|
||||
"""
|
||||
|
||||
try:
|
||||
# Load the machine configuration
|
||||
machine = MachineFactory.load_configuration(machine_file)
|
||||
FreeCAD.Console.PrintMessage(f"Loaded machine: {machine.name}\n")
|
||||
|
||||
# Get axis limits
|
||||
x_min = y_min = z_min = float('inf')
|
||||
x_max = y_max = z_max = float('-inf')
|
||||
|
||||
# Find min/max for linear axes
|
||||
for axis_name, axis_obj in machine.linear_axes.items():
|
||||
if axis_name.upper() == 'X':
|
||||
x_min = min(x_min, axis_obj.min_limit)
|
||||
x_max = max(x_max, axis_obj.max_limit)
|
||||
elif axis_name.upper() == 'Y':
|
||||
y_min = min(y_min, axis_obj.min_limit)
|
||||
y_max = max(y_max, axis_obj.max_limit)
|
||||
elif axis_name.upper() == 'Z':
|
||||
z_min = min(z_min, axis_obj.min_limit)
|
||||
z_max = max(z_max, axis_obj.max_limit)
|
||||
|
||||
# Check if we have valid limits
|
||||
if x_min == float('inf') or y_min == float('inf') or z_min == float('inf'):
|
||||
FreeCAD.Console.PrintError("Machine does not have X, Y, Z linear axes defined.\n")
|
||||
return None
|
||||
|
||||
FreeCAD.Console.PrintMessage(f"Machine boundaries: X({x_min:.3f}, {x_max:.3f}), Y({y_min:.3f}, {y_max:.3f}), Z({z_min:.3f}, {z_max:.3f})\n")
|
||||
FreeCAD.Console.PrintMessage("Note: These are MACHINE coordinates showing the absolute travel limits.\n")
|
||||
FreeCAD.Console.PrintMessage("Work coordinates would be relative to the workpiece origin.\n")
|
||||
|
||||
# Create the 8 corner points of the box
|
||||
p1 = FreeCAD.Vector(x_min, y_min, z_min)
|
||||
p2 = FreeCAD.Vector(x_max, y_min, z_min)
|
||||
p3 = FreeCAD.Vector(x_max, y_max, z_min)
|
||||
p4 = FreeCAD.Vector(x_min, y_max, z_min)
|
||||
p5 = FreeCAD.Vector(x_min, y_min, z_max)
|
||||
p6 = FreeCAD.Vector(x_max, y_min, z_max)
|
||||
p7 = FreeCAD.Vector(x_max, y_max, z_max)
|
||||
p8 = FreeCAD.Vector(x_min, y_max, z_max)
|
||||
|
||||
# Create edges (12 edges for wireframe box)
|
||||
edges = [
|
||||
Part.makeLine(p1, p2), # bottom face
|
||||
Part.makeLine(p2, p3),
|
||||
Part.makeLine(p3, p4),
|
||||
Part.makeLine(p4, p1),
|
||||
Part.makeLine(p5, p6), # top face
|
||||
Part.makeLine(p6, p7),
|
||||
Part.makeLine(p7, p8),
|
||||
Part.makeLine(p8, p5),
|
||||
Part.makeLine(p1, p5), # vertical edges
|
||||
Part.makeLine(p2, p6),
|
||||
Part.makeLine(p3, p7),
|
||||
Part.makeLine(p4, p8),
|
||||
]
|
||||
|
||||
# Create a compound of all edges (wireframe)
|
||||
compound = Part.makeCompound(edges)
|
||||
|
||||
# Create a new document if none exists
|
||||
if not FreeCAD.ActiveDocument:
|
||||
FreeCAD.newDocument("MachineBoundary")
|
||||
|
||||
# Create the shape in the document
|
||||
obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"MachineBoundary_{machine.name.replace(' ', '_')}")
|
||||
obj.Shape = compound
|
||||
obj.Label = f"Machine Boundary: {machine.name}"
|
||||
|
||||
# Set visual properties
|
||||
obj.ViewObject.ShapeColor = color
|
||||
obj.ViewObject.LineWidth = line_width
|
||||
obj.ViewObject.DrawStyle = draw_style
|
||||
|
||||
FreeCAD.ActiveDocument.recompute()
|
||||
|
||||
FreeCAD.Console.PrintMessage(f"Created machine boundary box for {machine.name}\n")
|
||||
return obj
|
||||
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintError(f"Error creating machine boundary box: {str(e)}\n")
|
||||
return None
|
||||
|
||||
def main():
|
||||
"""Main macro function."""
|
||||
FreeCAD.Console.PrintMessage("FreeCAD Macro: Generate Machine Boundary Box\n")
|
||||
|
||||
# Get machine file
|
||||
machine_file = get_machine_file()
|
||||
if not machine_file:
|
||||
return
|
||||
|
||||
# Create the boundary box with customizable appearance
|
||||
# You can change these parameters:
|
||||
# color: (R, G, B) tuple, e.g., (1.0, 0.0, 0.0) for red, (0.0, 1.0, 0.0) for green
|
||||
# line_width: thickness of the wires
|
||||
# draw_style: "Solid", "Dashed", or "Dotted"
|
||||
obj = create_machine_boundary_box(machine_file,
|
||||
color=(1.0, 0.0, 0.0), # Red
|
||||
line_width=2.0,
|
||||
draw_style="Dashed") # Broken/dashed lines
|
||||
if obj:
|
||||
FreeCAD.Console.PrintMessage("Macro completed successfully.\n")
|
||||
else:
|
||||
FreeCAD.Console.PrintError("Macro failed.\n")
|
||||
|
||||
# Run the macro
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
410
src/Mod/CAM/CAMTests/TestMachine.py
Normal file
410
src/Mod/CAM/CAMTests/TestMachine.py
Normal file
@@ -0,0 +1,410 @@
|
||||
# -*- 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. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import tempfile
|
||||
import pathlib
|
||||
import CAMTests.PathTestUtils as PathTestUtils
|
||||
from Path.Machine.models.machine import (
|
||||
Machine,
|
||||
Spindle,
|
||||
OutputOptions,
|
||||
GCodeBlocks,
|
||||
ProcessingOptions,
|
||||
MachineFactory,
|
||||
)
|
||||
|
||||
|
||||
class TestMachineDataclass(PathTestUtils.PathTestBase):
|
||||
"""Test the unified Machine dataclass"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.default_machine = Machine()
|
||||
|
||||
def test_default_initialization(self):
|
||||
"""Test that Machine initializes with sensible defaults"""
|
||||
machine = Machine()
|
||||
|
||||
# Basic identification
|
||||
self.assertEqual(machine.name, "Default Machine")
|
||||
self.assertEqual(machine.manufacturer, "")
|
||||
self.assertEqual(machine.description, "")
|
||||
|
||||
# Machine type is derived from axes configuration
|
||||
self.assertEqual(machine.machine_type, "custom") # No axes configured yet
|
||||
|
||||
# Add axes and verify machine type updates
|
||||
machine.add_linear_axis("X", FreeCAD.Vector(1, 0, 0))
|
||||
machine.add_linear_axis("Y", FreeCAD.Vector(0, 1, 0))
|
||||
self.assertEqual(machine.machine_type, "custom") # Still missing Z axis
|
||||
|
||||
machine.add_linear_axis("Z", FreeCAD.Vector(0, 0, 1))
|
||||
self.assertEqual(machine.machine_type, "xyz") # Now has XYZ axes
|
||||
|
||||
# Add rotary axes and verify machine type updates
|
||||
machine.add_rotary_axis("A", FreeCAD.Vector(1, 0, 0), -120, 120)
|
||||
self.assertEqual(machine.machine_type, "xyza")
|
||||
|
||||
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)
|
||||
self.assertIsNotNone(machine.freecad_version)
|
||||
|
||||
# Post-processor defaults
|
||||
self.assertIsInstance(machine.output, OutputOptions)
|
||||
self.assertIsInstance(machine.blocks, GCodeBlocks)
|
||||
self.assertIsInstance(machine.processing, ProcessingOptions)
|
||||
|
||||
def test_custom_initialization(self):
|
||||
"""Test Machine initialization with custom values and verify machine_type is derived"""
|
||||
# Create a 5-axis machine (XYZAC)
|
||||
machine = Machine(
|
||||
name="Test Mill",
|
||||
manufacturer="ACME Corp",
|
||||
description="5-axis mill",
|
||||
configuration_units="imperial",
|
||||
)
|
||||
|
||||
# Add axes to make it a 5-axis machine
|
||||
machine.add_linear_axis("X", FreeCAD.Vector(1, 0, 0))
|
||||
machine.add_linear_axis("Y", FreeCAD.Vector(0, 1, 0))
|
||||
machine.add_linear_axis("Z", FreeCAD.Vector(0, 0, 1))
|
||||
machine.add_rotary_axis("A", FreeCAD.Vector(1, 0, 0), -120, 120)
|
||||
machine.add_rotary_axis("C", FreeCAD.Vector(0, 0, 1), -360, 360)
|
||||
|
||||
self.assertEqual(machine.name, "Test Mill")
|
||||
self.assertEqual(machine.manufacturer, "ACME Corp")
|
||||
self.assertEqual(machine.description, "5-axis mill")
|
||||
self.assertEqual(machine.machine_type, "xyzac")
|
||||
self.assertEqual(machine.configuration_units, "imperial")
|
||||
|
||||
def test_configuration_units_property(self):
|
||||
"""Test configuration_units property returns correct values"""
|
||||
metric_machine = Machine(configuration_units="metric")
|
||||
self.assertEqual(metric_machine.configuration_units, "metric")
|
||||
|
||||
imperial_machine = Machine(configuration_units="imperial")
|
||||
self.assertEqual(imperial_machine.configuration_units, "imperial")
|
||||
|
||||
|
||||
class TestOutputOptions(PathTestUtils.PathTestBase):
|
||||
"""Test OutputOptions dataclass"""
|
||||
|
||||
def test_default_initialization(self):
|
||||
"""Test OutputOptions initialization with defaults"""
|
||||
opts = OutputOptions()
|
||||
|
||||
# Default values
|
||||
self.assertTrue(opts.comments)
|
||||
self.assertTrue(opts.blank_lines)
|
||||
self.assertTrue(opts.header)
|
||||
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)
|
||||
|
||||
def test_custom_initialization(self):
|
||||
"""Test OutputOptions initialization with custom values"""
|
||||
opts = OutputOptions(
|
||||
comments=False,
|
||||
blank_lines=False,
|
||||
header=False,
|
||||
line_numbers=True,
|
||||
bcnc_blocks=True,
|
||||
path_labels=True,
|
||||
machine_name=True,
|
||||
tool_change=False,
|
||||
doubles=False,
|
||||
adaptive=True,
|
||||
)
|
||||
|
||||
# Verify custom values
|
||||
self.assertFalse(opts.comments)
|
||||
self.assertFalse(opts.blank_lines)
|
||||
self.assertFalse(opts.header)
|
||||
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)
|
||||
|
||||
def test_equality(self):
|
||||
"""Test OutputOptions equality comparison"""
|
||||
opts1 = OutputOptions()
|
||||
opts2 = OutputOptions()
|
||||
self.assertEqual(opts1, opts2)
|
||||
|
||||
opts2.comments = False
|
||||
self.assertNotEqual(opts1, opts2)
|
||||
|
||||
|
||||
class TestSpindle(PathTestUtils.PathTestBase):
|
||||
"""Test Spindle dataclass"""
|
||||
|
||||
def test_spindle_initialization(self):
|
||||
"""Test Spindle initialization with defaults"""
|
||||
spindle = Spindle(
|
||||
name="Main Spindle",
|
||||
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))
|
||||
|
||||
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):
|
||||
"""Test to_dict and from_dict"""
|
||||
spindle = Spindle(
|
||||
name="Test Spindle",
|
||||
id="spindle-001",
|
||||
max_power_kw=3.0,
|
||||
max_rpm=18000,
|
||||
min_rpm=500,
|
||||
tool_change="manual",
|
||||
tool_axis=FreeCAD.Vector(0, 1, 0),
|
||||
)
|
||||
|
||||
data = spindle.to_dict()
|
||||
self.assertEqual(data["name"], "Test Spindle")
|
||||
self.assertEqual(data["id"], "spindle-001")
|
||||
self.assertEqual(data["max_power_kw"], 3.0)
|
||||
self.assertEqual(data["tool_axis"], [0, 1, 0])
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class TestMachineFactory(PathTestUtils.PathTestBase):
|
||||
"""Test MachineFactory class for loading/saving configurations"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures with temporary directory"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.temp_path = pathlib.Path(self.temp_dir)
|
||||
MachineFactory.set_config_directory(self.temp_dir)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up temporary directory"""
|
||||
import shutil
|
||||
|
||||
if self.temp_path.exists():
|
||||
shutil.rmtree(self.temp_path)
|
||||
|
||||
def test_set_and_get_config_directory(self):
|
||||
"""Test setting and getting configuration directory"""
|
||||
test_dir = self.temp_path / "test_configs"
|
||||
MachineFactory.set_config_directory(test_dir)
|
||||
|
||||
config_dir = MachineFactory.get_config_directory()
|
||||
self.assertEqual(config_dir, test_dir)
|
||||
self.assertTrue(config_dir.exists())
|
||||
|
||||
def test_save_and_load_configuration(self):
|
||||
"""Test saving and loading a machine configuration"""
|
||||
# Create a test machine
|
||||
machine = Machine(
|
||||
name="Test Machine",
|
||||
manufacturer="Test Corp",
|
||||
description="Test description",
|
||||
configuration_units="metric",
|
||||
)
|
||||
|
||||
# Add axes to make it an XYZ machine
|
||||
machine.add_linear_axis("X", FreeCAD.Vector(1, 0, 0))
|
||||
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",
|
||||
max_power_kw=5.5,
|
||||
max_rpm=24000,
|
||||
min_rpm=1000,
|
||||
)
|
||||
machine.spindles.append(spindle)
|
||||
|
||||
# Save configuration
|
||||
filepath = MachineFactory.save_configuration(machine, "test_machine.fcm")
|
||||
self.assertTrue(filepath.exists())
|
||||
|
||||
# Load configuration
|
||||
loaded_machine = MachineFactory.load_configuration("test_machine.fcm")
|
||||
|
||||
# Verify loaded data
|
||||
self.assertEqual(loaded_machine.name, "Test Machine")
|
||||
self.assertEqual(loaded_machine.manufacturer, "Test Corp")
|
||||
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")
|
||||
|
||||
def test_save_configuration_auto_filename(self):
|
||||
"""Test saving with automatic filename generation"""
|
||||
machine = Machine(name="My Test Machine")
|
||||
|
||||
filepath = MachineFactory.save_configuration(machine)
|
||||
|
||||
# Should create file with sanitized name
|
||||
self.assertTrue(filepath.exists())
|
||||
self.assertEqual(filepath.name, "My_Test_Machine.fcm")
|
||||
|
||||
def test_load_nonexistent_file(self):
|
||||
"""Test loading a file that doesn't exist"""
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
MachineFactory.load_configuration("nonexistent.fcm")
|
||||
|
||||
def test_create_default_machine_data(self):
|
||||
"""Test creating default machine data dictionary"""
|
||||
data = MachineFactory.create_default_machine_data()
|
||||
|
||||
self.assertIsInstance(data, dict)
|
||||
# The data structure has nested "machine" key
|
||||
self.assertIn("machine", data)
|
||||
self.assertEqual(data["machine"]["name"], "New Machine")
|
||||
self.assertIn("spindles", data["machine"])
|
||||
|
||||
def test_list_configuration_files(self):
|
||||
"""Test listing available configuration files"""
|
||||
# Create some test configurations
|
||||
machine1 = Machine(name="Machine 1")
|
||||
machine2 = Machine(name="Machine 2")
|
||||
|
||||
MachineFactory.save_configuration(machine1, "machine1.fcm")
|
||||
MachineFactory.save_configuration(machine2, "machine2.fcm")
|
||||
|
||||
# List configurations
|
||||
configs = MachineFactory.list_configuration_files()
|
||||
|
||||
# Should include <any> plus our two machines
|
||||
self.assertGreaterEqual(len(configs), 3)
|
||||
self.assertEqual(configs[0][0], "<any>")
|
||||
|
||||
# Check that our machines are in the list (by display name, not filename)
|
||||
names = [name for name, path in configs]
|
||||
self.assertIn("Machine 1", names)
|
||||
self.assertIn("Machine 2", names)
|
||||
|
||||
def test_list_configurations(self):
|
||||
"""Test listing configuration names"""
|
||||
machine = Machine(name="Test Machine")
|
||||
MachineFactory.save_configuration(machine, "test.fcm")
|
||||
|
||||
configs = MachineFactory.list_configurations()
|
||||
|
||||
self.assertIsInstance(configs, list)
|
||||
self.assertIn("<any>", configs)
|
||||
# Returns display name from JSON, not filename
|
||||
self.assertIn("Test Machine", configs)
|
||||
|
||||
def test_delete_configuration(self):
|
||||
"""Test deleting a configuration file"""
|
||||
machine = Machine(name="To Delete")
|
||||
filepath = MachineFactory.save_configuration(machine, "delete_me.fcm")
|
||||
|
||||
self.assertTrue(filepath.exists())
|
||||
|
||||
# Delete the configuration
|
||||
result = MachineFactory.delete_configuration("delete_me.fcm")
|
||||
self.assertTrue(result)
|
||||
self.assertFalse(filepath.exists())
|
||||
|
||||
# Try deleting again (should return False)
|
||||
result = MachineFactory.delete_configuration("delete_me.fcm")
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_get_builtin_config(self):
|
||||
"""Test getting built-in machine configurations"""
|
||||
# Test each built-in config type
|
||||
config_types = ["XYZ", "XYZAC", "XYZBC", "XYZA", "XYZB"]
|
||||
|
||||
for config_type in config_types:
|
||||
machine = MachineFactory.get_builtin_config(config_type)
|
||||
self.assertIsInstance(machine, Machine)
|
||||
self.assertIsNotNone(machine.name)
|
||||
|
||||
def test_get_builtin_config_invalid_type(self):
|
||||
"""Test getting built-in config with invalid type"""
|
||||
with self.assertRaises(ValueError):
|
||||
MachineFactory.get_builtin_config("INVALID")
|
||||
|
||||
def test_serialization_roundtrip(self):
|
||||
"""Test full serialization roundtrip with complex machine"""
|
||||
# Create a complex machine with all components
|
||||
machine = Machine(
|
||||
name="Complex Machine",
|
||||
manufacturer="Test Mfg",
|
||||
description="Full featured machine",
|
||||
configuration_units="metric",
|
||||
)
|
||||
|
||||
# Add spindle
|
||||
machine.spindles.append(
|
||||
Spindle(
|
||||
name="Main",
|
||||
max_power_kw=7.5,
|
||||
max_rpm=30000,
|
||||
)
|
||||
)
|
||||
|
||||
# Configure post-processor settings
|
||||
machine.output.output_comments = False
|
||||
machine.output.axis_precision = 4
|
||||
machine.output.line_increment = 5
|
||||
|
||||
# line_increment is set to default 10 in OutputOptions
|
||||
|
||||
# Save and load
|
||||
filepath = MachineFactory.save_configuration(machine, "complex.fcm")
|
||||
loaded = MachineFactory.load_configuration(filepath)
|
||||
|
||||
# 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)
|
||||
@@ -1,208 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
import unittest
|
||||
import FreeCAD
|
||||
from Path.Tool.machine.models.machine import Machine
|
||||
|
||||
|
||||
class TestPathToolMachine(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.default_machine = Machine()
|
||||
|
||||
def test_initialization_defaults(self):
|
||||
self.assertEqual(self.default_machine.label, "Machine")
|
||||
self.assertAlmostEqual(self.default_machine.max_power.getValueAs("W").Value, 2000)
|
||||
self.assertAlmostEqual(self.default_machine.get_min_rpm_value(), 3000)
|
||||
self.assertAlmostEqual(self.default_machine.get_max_rpm_value(), 60000)
|
||||
self.assertAlmostEqual(self.default_machine.min_feed.getValueAs("mm/min").Value, 1)
|
||||
self.assertAlmostEqual(self.default_machine.max_feed.getValueAs("mm/min").Value, 2000)
|
||||
expected_peak_torque_rpm = 60000 / 3
|
||||
self.assertAlmostEqual(
|
||||
self.default_machine.get_peak_torque_rpm_value(),
|
||||
expected_peak_torque_rpm,
|
||||
)
|
||||
expected_max_torque_nm = 2000 * 9.5488 / expected_peak_torque_rpm
|
||||
self.assertAlmostEqual(
|
||||
self.default_machine.max_torque.getValueAs("Nm").Value,
|
||||
expected_max_torque_nm,
|
||||
)
|
||||
self.assertIsNotNone(self.default_machine.id)
|
||||
|
||||
def test_initialization_custom_values(self):
|
||||
custom_machine = Machine(
|
||||
label="Custom Machine",
|
||||
max_power=5,
|
||||
min_rpm=1000,
|
||||
max_rpm=20000,
|
||||
max_torque=50,
|
||||
peak_torque_rpm=15000,
|
||||
min_feed=10,
|
||||
max_feed=5000,
|
||||
id="custom-id",
|
||||
)
|
||||
self.assertEqual(custom_machine.label, "Custom Machine")
|
||||
self.assertAlmostEqual(custom_machine.max_power.getValueAs("W").Value, 5000)
|
||||
self.assertAlmostEqual(custom_machine.get_min_rpm_value(), 1000)
|
||||
self.assertAlmostEqual(custom_machine.get_max_rpm_value(), 20000)
|
||||
self.assertAlmostEqual(custom_machine.max_torque.getValueAs("Nm").Value, 50)
|
||||
self.assertAlmostEqual(custom_machine.get_peak_torque_rpm_value(), 15000)
|
||||
self.assertAlmostEqual(custom_machine.min_feed.getValueAs("mm/min").Value, 10)
|
||||
self.assertAlmostEqual(custom_machine.max_feed.getValueAs("mm/min").Value, 5000)
|
||||
self.assertEqual(custom_machine.id, "custom-id")
|
||||
|
||||
def test_initialization_custom_torque_quantity(self):
|
||||
custom_torque_machine = Machine(max_torque=FreeCAD.Units.Quantity(100, "Nm"))
|
||||
self.assertAlmostEqual(custom_torque_machine.max_torque.getValueAs("Nm").Value, 100)
|
||||
|
||||
def test_validate_valid(self):
|
||||
try:
|
||||
self.default_machine.validate()
|
||||
except AttributeError as e:
|
||||
self.fail(f"Validation failed unexpectedly: {e}")
|
||||
|
||||
def test_validate_missing_label(self):
|
||||
self.default_machine.label = ""
|
||||
with self.assertRaisesRegex(AttributeError, "Machine name is required"):
|
||||
self.default_machine.validate()
|
||||
|
||||
def test_validate_peak_torque_rpm_greater_than_max_rpm(self):
|
||||
self.default_machine.set_peak_torque_rpm(70000)
|
||||
with self.assertRaisesRegex(AttributeError, "Peak Torque RPM.*must be less than max RPM"):
|
||||
self.default_machine.validate()
|
||||
|
||||
def test_validate_max_rpm_less_than_min_rpm(self):
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_min_rpm(4000) # min_rpm = 4000 RPM
|
||||
self.default_machine.set_peak_torque_rpm(1000) # peak_torque_rpm = 1000 RPM
|
||||
self.default_machine._max_rpm = 2000 / 60.0 # max_rpm = 2000 RPM (33.33 1/s)
|
||||
self.assertLess(
|
||||
self.default_machine.get_max_rpm_value(),
|
||||
self.default_machine.get_min_rpm_value(),
|
||||
)
|
||||
with self.assertRaisesRegex(AttributeError, "Max RPM must be larger than min RPM"):
|
||||
self.default_machine.validate()
|
||||
|
||||
def test_validate_max_feed_less_than_min_feed(self):
|
||||
self.default_machine.set_min_feed(1000)
|
||||
self.default_machine._max_feed = 500
|
||||
with self.assertRaisesRegex(AttributeError, "Max feed must be larger than min feed"):
|
||||
self.default_machine.validate()
|
||||
|
||||
def test_get_torque_at_rpm(self):
|
||||
torque_below_peak = self.default_machine.get_torque_at_rpm(10000)
|
||||
expected_peak_torque_rpm = 60000 / 3
|
||||
expected_max_torque_nm = 2000 * 9.5488 / expected_peak_torque_rpm
|
||||
expected_torque_below_peak = expected_max_torque_nm / expected_peak_torque_rpm * 10000
|
||||
self.assertAlmostEqual(torque_below_peak, expected_torque_below_peak)
|
||||
|
||||
torque_at_peak = self.default_machine.get_torque_at_rpm(
|
||||
self.default_machine.get_peak_torque_rpm_value()
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
torque_at_peak,
|
||||
self.default_machine.max_torque.getValueAs("Nm").Value,
|
||||
)
|
||||
|
||||
torque_above_peak = self.default_machine.get_torque_at_rpm(50000)
|
||||
expected_torque_above_peak = 2000 * 9.5488 / 50000
|
||||
self.assertAlmostEqual(torque_above_peak, expected_torque_above_peak)
|
||||
|
||||
def test_set_label(self):
|
||||
self.default_machine.label = "New Label"
|
||||
self.assertEqual(self.default_machine.label, "New Label")
|
||||
|
||||
def test_set_max_power(self):
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_max_power(5, "hp")
|
||||
self.assertAlmostEqual(
|
||||
self.default_machine.max_power.getValueAs("W").Value,
|
||||
5 * 745.7,
|
||||
places=4,
|
||||
)
|
||||
with self.assertRaisesRegex(AttributeError, "Max power must be positive"):
|
||||
self.default_machine.set_max_power(0)
|
||||
|
||||
def test_set_min_rpm(self):
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_min_rpm(5000)
|
||||
self.assertAlmostEqual(self.default_machine.get_min_rpm_value(), 5000)
|
||||
with self.assertRaisesRegex(AttributeError, "Min RPM cannot be negative"):
|
||||
self.default_machine.set_min_rpm(-100)
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_min_rpm(70000)
|
||||
self.assertAlmostEqual(self.default_machine.get_min_rpm_value(), 70000)
|
||||
self.assertAlmostEqual(self.default_machine.get_max_rpm_value(), 70001)
|
||||
|
||||
def test_set_max_rpm(self):
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_max_rpm(50000)
|
||||
self.assertAlmostEqual(self.default_machine.get_max_rpm_value(), 50000)
|
||||
with self.assertRaisesRegex(AttributeError, "Max RPM must be positive"):
|
||||
self.default_machine.set_max_rpm(0)
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_max_rpm(2000)
|
||||
self.assertAlmostEqual(self.default_machine.get_max_rpm_value(), 2000)
|
||||
self.assertAlmostEqual(self.default_machine.get_min_rpm_value(), 1999)
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_max_rpm(0.5)
|
||||
self.assertAlmostEqual(self.default_machine.get_max_rpm_value(), 0.5)
|
||||
self.assertAlmostEqual(self.default_machine.get_min_rpm_value(), 0)
|
||||
|
||||
def test_set_min_feed(self):
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_min_feed(500, "inch/min")
|
||||
self.assertAlmostEqual(
|
||||
self.default_machine.min_feed.getValueAs("mm/min").Value,
|
||||
500 * 25.4,
|
||||
places=4,
|
||||
)
|
||||
with self.assertRaisesRegex(AttributeError, "Min feed cannot be negative"):
|
||||
self.default_machine.set_min_feed(-10)
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_min_feed(3000)
|
||||
self.assertAlmostEqual(self.default_machine.min_feed.getValueAs("mm/min").Value, 3000)
|
||||
self.assertAlmostEqual(self.default_machine.max_feed.getValueAs("mm/min").Value, 3001)
|
||||
|
||||
def test_set_max_feed(self):
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_max_feed(3000)
|
||||
self.assertAlmostEqual(self.default_machine.max_feed.getValueAs("mm/min").Value, 3000)
|
||||
with self.assertRaisesRegex(AttributeError, "Max feed must be positive"):
|
||||
self.default_machine.set_max_feed(0)
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_min_feed(600)
|
||||
self.default_machine.set_max_feed(500)
|
||||
self.assertAlmostEqual(self.default_machine.max_feed.getValueAs("mm/min").Value, 500)
|
||||
self.assertAlmostEqual(self.default_machine.min_feed.getValueAs("mm/min").Value, 499)
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_max_feed(0.5)
|
||||
self.assertAlmostEqual(self.default_machine.max_feed.getValueAs("mm/min").Value, 0.5)
|
||||
self.assertAlmostEqual(self.default_machine.min_feed.getValueAs("mm/min").Value, 0)
|
||||
|
||||
def test_set_peak_torque_rpm(self):
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_peak_torque_rpm(40000)
|
||||
self.assertAlmostEqual(self.default_machine.get_peak_torque_rpm_value(), 40000)
|
||||
with self.assertRaisesRegex(AttributeError, "Peak torque RPM cannot be negative"):
|
||||
self.default_machine.set_peak_torque_rpm(-100)
|
||||
|
||||
def test_set_max_torque(self):
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_max_torque(200, "in-lbf")
|
||||
self.assertAlmostEqual(
|
||||
self.default_machine.max_torque.getValueAs("Nm").Value,
|
||||
200 * 0.112985,
|
||||
places=4,
|
||||
)
|
||||
with self.assertRaisesRegex(AttributeError, "Max torque must be positive"):
|
||||
self.default_machine.set_max_torque(0)
|
||||
|
||||
def test_dump(self):
|
||||
try:
|
||||
self.default_machine.dump(False)
|
||||
except Exception as e:
|
||||
self.fail(f"dump() method failed unexpectedly: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -170,6 +170,16 @@ SET(PathPythonToolsGui_SRCS
|
||||
Path/Tool/Gui/Controller.py
|
||||
)
|
||||
|
||||
SET(PathPythonMachineUiEditor_SRCS
|
||||
Path/Machine/ui/editor/machine_editor.py
|
||||
Path/Machine/ui/editor/__init__.py
|
||||
)
|
||||
|
||||
SET(PathPythonMachineModels_SRCS
|
||||
Path/Machine/models/__init__.py
|
||||
Path/Machine/models/machine.py
|
||||
)
|
||||
|
||||
SET(PathPythonToolsToolBit_SRCS
|
||||
Path/Tool/toolbit/__init__.py
|
||||
Path/Tool/toolbit/util.py
|
||||
@@ -248,15 +258,6 @@ SET(PathPythonToolsLibraryUi_SRCS
|
||||
Path/Tool/library/ui/properties.py
|
||||
)
|
||||
|
||||
SET(PathPythonToolsMachine_SRCS
|
||||
Path/Tool/machine/__init__.py
|
||||
)
|
||||
|
||||
SET(PathPythonToolsMachineModels_SRCS
|
||||
Path/Tool/machine/models/__init__.py
|
||||
Path/Tool/machine/models/machine.py
|
||||
)
|
||||
|
||||
SET(PathPythonToolsShape_SRCS
|
||||
Path/Tool/shape/__init__.py
|
||||
Path/Tool/shape/util.py
|
||||
@@ -508,6 +509,7 @@ SET(Tests_SRCS
|
||||
CAMTests/TestGrblLegacyPost.py
|
||||
CAMTests/TestLinuxCNCPost.py
|
||||
CAMTests/TestLinkingGenerator.py
|
||||
CAMTests/TestMachine.py
|
||||
CAMTests/TestMach3Mach4Post.py
|
||||
CAMTests/TestMach3Mach4LegacyPost.py
|
||||
CAMTests/TestMassoG3Post.py
|
||||
@@ -559,7 +561,6 @@ SET(Tests_SRCS
|
||||
CAMTests/TestPathToolShapeIcon.py
|
||||
CAMTests/TestPathToolLibrary.py
|
||||
CAMTests/TestPathToolLibrarySerializer.py
|
||||
CAMTests/TestPathToolMachine.py
|
||||
CAMTests/TestPathToolController.py
|
||||
CAMTests/TestPathUtil.py
|
||||
CAMTests/TestPathVcarve.py
|
||||
@@ -639,8 +640,8 @@ SET(all_files
|
||||
${PathPythonToolsLibraryModels_SRCS}
|
||||
${PathPythonToolsLibrarySerializers_SRCS}
|
||||
${PathPythonToolsLibraryUi_SRCS}
|
||||
${PathPythonToolsMachine_SRCS}
|
||||
${PathPythonToolsMachineModels_SRCS}
|
||||
${PathPythonMachineModels_SRCS}
|
||||
${PathPythonMachineUiEditor_SRCS}
|
||||
${PathPythonGui_SRCS}
|
||||
${Tools_SRCS}
|
||||
${Tools_Bit_SRCS}
|
||||
@@ -916,6 +917,20 @@ INSTALL(
|
||||
Mod/CAM/Path/Tool/machine
|
||||
)
|
||||
|
||||
INSTALL(
|
||||
FILES
|
||||
${PathPythonMachineModels_SRCS}
|
||||
DESTINATION
|
||||
Mod/CAM/Path/Machine/models
|
||||
)
|
||||
|
||||
INSTALL(
|
||||
FILES
|
||||
${PathPythonToolsMachineUiEditor_SRCS}
|
||||
DESTINATION
|
||||
Mod/CAM/Path/Machine/ui/editor
|
||||
)
|
||||
|
||||
INSTALL(
|
||||
FILES
|
||||
${PathPythonToolsMachineModels_SRCS}
|
||||
|
||||
@@ -15,20 +15,12 @@
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QToolBox" name="toolBox">
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="page">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>695</width>
|
||||
<height>308</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
<widget class="QWidget" name="tab">
|
||||
<attribute name="title">
|
||||
<string>General</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
@@ -118,16 +110,8 @@ If left empty no template will be preselected.</string>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_2">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>695</width>
|
||||
<height>480</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
<widget class="QWidget" name="tab_2">
|
||||
<attribute name="title">
|
||||
<string>Post processor</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
@@ -334,16 +318,8 @@ See the file save policy below on how to deal with name conflicts.</string>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_3">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>674</width>
|
||||
<height>619</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
<widget class="QWidget" name="tab_3">
|
||||
<attribute name="title">
|
||||
<string>Setup</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
|
||||
@@ -36,15 +36,14 @@ def RegisterDressup(dressup):
|
||||
|
||||
class DressupPreferencesPage:
|
||||
def __init__(self, parent=None):
|
||||
self.form = QtGui.QToolBox()
|
||||
self.form = QtGui.QWidget()
|
||||
self.form.setWindowTitle(translate("CAM_PreferencesPathDressup", "Dressups"))
|
||||
|
||||
layout = QtGui.QVBoxLayout(self.form)
|
||||
pages = []
|
||||
for dressup in _dressups:
|
||||
page = dressup.preferencesPage()
|
||||
if hasattr(page, "icon") and page.icon:
|
||||
self.form.addItem(page.form, page.icon, page.label)
|
||||
else:
|
||||
self.form.addItem(page.form, page.label)
|
||||
layout.addWidget(page.form)
|
||||
pages.append(page)
|
||||
self.pages = pages
|
||||
|
||||
|
||||
48
src/Mod/CAM/Path/Machine/models/__init__.py
Normal file
48
src/Mod/CAM/Path/Machine/models/__init__.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2025 Billy Huddleston <billy@ivdc.com> *
|
||||
# * *
|
||||
# * 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",
|
||||
]
|
||||
1398
src/Mod/CAM/Path/Machine/models/machine.py
Normal file
1398
src/Mod/CAM/Path/Machine/models/machine.py
Normal file
File diff suppressed because it is too large
Load Diff
26
src/Mod/CAM/Path/Machine/ui/editor/__init__.py
Normal file
26
src/Mod/CAM/Path/Machine/ui/editor/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2025 Billy Huddleston <billy@ivdc.com> *
|
||||
# * *
|
||||
# * 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"]
|
||||
1570
src/Mod/CAM/Path/Machine/ui/editor/machine_editor.py
Normal file
1570
src/Mod/CAM/Path/Machine/ui/editor/machine_editor.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -39,7 +39,7 @@ class JobPreferencesPage:
|
||||
import FreeCADGui
|
||||
|
||||
self.form = FreeCADGui.PySideUic.loadUi(":preferences/PathJob.ui")
|
||||
self.form.toolBox.setCurrentIndex(0) # Take that qt designer!
|
||||
self.form.tabWidget.setCurrentIndex(0) # Take that qt designer!
|
||||
|
||||
self.postProcessorDefaultTooltip = self.form.defaultPostProcessor.toolTip()
|
||||
self.postProcessorArgsDefaultTooltip = self.form.defaultPostProcessorArgs.toolTip()
|
||||
|
||||
@@ -146,7 +146,7 @@ class PostProcessor:
|
||||
else:
|
||||
# get all operations from 'Operations' group
|
||||
self._job = job
|
||||
self._operations = getattr(job.Operations, "Group", [])
|
||||
self._operations = getattr(job.Operations, "Group", []) if job is not None else []
|
||||
|
||||
@classmethod
|
||||
def exists(cls, processor):
|
||||
|
||||
@@ -10,7 +10,6 @@ from .library.serializers import FCTLSerializer
|
||||
from .toolbit import ToolBit
|
||||
from .toolbit.serializers import FCTBSerializer
|
||||
from .shape import ToolBitShape, ToolBitShapePngIcon, ToolBitShapeSvgIcon
|
||||
from .machine import Machine
|
||||
|
||||
# Register asset classes and serializers.
|
||||
cam_assets.register_asset(Library, FCTLSerializer)
|
||||
@@ -18,7 +17,6 @@ cam_assets.register_asset(ToolBit, FCTBSerializer)
|
||||
cam_assets.register_asset(ToolBitShape, DummyAssetSerializer)
|
||||
cam_assets.register_asset(ToolBitShapePngIcon, DummyAssetSerializer)
|
||||
cam_assets.register_asset(ToolBitShapeSvgIcon, DummyAssetSerializer)
|
||||
cam_assets.register_asset(Machine, DummyAssetSerializer)
|
||||
|
||||
# For backward compatibility with files saved before the toolbit rename
|
||||
# This makes the Path.Tool.toolbit.base module available as Path.Tool.Bit.
|
||||
|
||||
@@ -26,6 +26,8 @@ import tempfile
|
||||
import FreeCAD
|
||||
import Path
|
||||
from PySide import QtGui, QtCore
|
||||
from ....Machine.ui.editor import MachineEditorDialog
|
||||
from ....Machine.models import MachineFactory
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
@@ -46,20 +48,21 @@ def _is_writable_dir(path: pathlib.Path) -> bool:
|
||||
|
||||
class AssetPreferencesPage:
|
||||
def __init__(self, parent=None):
|
||||
self.form = QtGui.QToolBox()
|
||||
self.form = QtGui.QWidget()
|
||||
self.form.setWindowTitle(translate("CAM_PreferencesAssets", "Assets"))
|
||||
|
||||
asset_path_widget = QtGui.QWidget()
|
||||
main_layout = QtGui.QHBoxLayout(asset_path_widget)
|
||||
# Set up main layout directly on the form
|
||||
self.main_layout = QtGui.QVBoxLayout(self.form)
|
||||
|
||||
# Create widgets
|
||||
self.asset_path_label = QtGui.QLabel(translate("CAM_PreferencesAssets", "Asset Directory:"))
|
||||
self.assets_group = QtGui.QGroupBox(translate("CAM_PreferencesAssets", "Asset Location"))
|
||||
self.asset_path_label = QtGui.QLabel(translate("CAM_PreferencesAssets", "Default path"))
|
||||
self.asset_path_edit = QtGui.QLineEdit()
|
||||
self.asset_path_note_label = QtGui.QLabel(
|
||||
translate(
|
||||
"CAM_PreferencesAssets",
|
||||
"Note: Select the directory that will contain the "
|
||||
"Tool folder with Bit/, Shape/, and Library/ subfolders.",
|
||||
"Tools folder with Bit/, Shape/, and Library/ subfolders and the Machines/ folder.",
|
||||
)
|
||||
)
|
||||
self.asset_path_note_label.setWordWrap(True)
|
||||
@@ -74,24 +77,67 @@ class AssetPreferencesPage:
|
||||
font.setItalic(True)
|
||||
self.asset_path_note_label.setFont(font)
|
||||
|
||||
# Layout for asset path section
|
||||
edit_button_layout = QtGui.QGridLayout()
|
||||
edit_button_layout.addWidget(self.asset_path_label, 0, 0, QtCore.Qt.AlignVCenter)
|
||||
edit_button_layout.addWidget(self.asset_path_edit, 0, 1, QtCore.Qt.AlignVCenter)
|
||||
edit_button_layout.addWidget(self.select_path_button, 0, 2, QtCore.Qt.AlignVCenter)
|
||||
edit_button_layout.addWidget(self.reset_path_button, 0, 3, QtCore.Qt.AlignVCenter)
|
||||
edit_button_layout.addWidget(self.asset_path_note_label, 1, 1, 1, 1, QtCore.Qt.AlignTop)
|
||||
edit_button_layout.addItem(
|
||||
QtGui.QSpacerItem(0, 0, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding),
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
4,
|
||||
# Assets group box
|
||||
self.assets_layout = QtGui.QGridLayout(self.assets_group)
|
||||
self.assets_layout.addWidget(self.asset_path_label, 0, 0, QtCore.Qt.AlignVCenter)
|
||||
self.assets_layout.addWidget(self.asset_path_edit, 0, 1, QtCore.Qt.AlignVCenter)
|
||||
self.assets_layout.addWidget(self.select_path_button, 0, 2, QtCore.Qt.AlignVCenter)
|
||||
self.assets_layout.addWidget(self.reset_path_button, 0, 3, QtCore.Qt.AlignVCenter)
|
||||
self.assets_layout.addWidget(self.asset_path_note_label, 1, 1, 1, 1, QtCore.Qt.AlignTop)
|
||||
self.main_layout.addWidget(self.assets_group)
|
||||
|
||||
# Machines group box
|
||||
self.machines_group = QtGui.QGroupBox(translate("CAM_PreferencesAssets", "Machines"))
|
||||
self.machines_layout = QtGui.QVBoxLayout(self.machines_group)
|
||||
|
||||
self.warning_label = QtGui.QLabel(
|
||||
translate(
|
||||
"CAM_PreferencesAssets",
|
||||
"Warning: Machine definition is an experimental feature. Changes "
|
||||
"made here will not affect any CAM functionality",
|
||||
)
|
||||
)
|
||||
self.warning_label.setWordWrap(True)
|
||||
warning_font = self.warning_label.font()
|
||||
warning_font.setItalic(True)
|
||||
self.warning_label.setFont(warning_font)
|
||||
self.warning_label.setContentsMargins(0, 0, 0, 10)
|
||||
self.machines_layout.addWidget(self.warning_label)
|
||||
|
||||
main_layout.addLayout(edit_button_layout, QtCore.Qt.AlignTop)
|
||||
self.machines_label = QtGui.QLabel(translate("CAM_PreferencesAssets", "Machines"))
|
||||
self.machines_layout.addWidget(self.machines_label)
|
||||
|
||||
self.form.addItem(asset_path_widget, translate("CAM_PreferencesAssets", "Assets"))
|
||||
self.machines_list = QtGui.QListWidget()
|
||||
self.machines_layout.addWidget(self.machines_list)
|
||||
|
||||
# Buttons: Add / Edit / Delete
|
||||
self.btn_layout = QtGui.QHBoxLayout()
|
||||
self.add_machine_btn = QtGui.QPushButton(translate("CAM_PreferencesAssets", "Add"))
|
||||
self.edit_machine_btn = QtGui.QPushButton(translate("CAM_PreferencesAssets", "Edit"))
|
||||
self.delete_machine_btn = QtGui.QPushButton(translate("CAM_PreferencesAssets", "Delete"))
|
||||
self.btn_layout.addWidget(self.add_machine_btn)
|
||||
self.btn_layout.addWidget(self.edit_machine_btn)
|
||||
self.btn_layout.addWidget(self.delete_machine_btn)
|
||||
self.machines_layout.addLayout(self.btn_layout)
|
||||
|
||||
self.machines_layout.addStretch() # Prevent the list from stretching
|
||||
|
||||
self.main_layout.addWidget(self.machines_group)
|
||||
|
||||
# Wire up buttons
|
||||
self.add_machine_btn.clicked.connect(self.add_machine)
|
||||
self.edit_machine_btn.clicked.connect(self.edit_machine)
|
||||
self.delete_machine_btn.clicked.connect(self.delete_machine)
|
||||
|
||||
# Connect double-click to edit
|
||||
self.machines_list.itemDoubleClicked.connect(self.edit_machine)
|
||||
|
||||
for name, filename in MachineFactory.list_configuration_files():
|
||||
if name == "<any>" or filename is None:
|
||||
continue
|
||||
item = QtGui.QListWidgetItem(name)
|
||||
item.setData(QtCore.Qt.UserRole, filename)
|
||||
self.machines_list.addItem(item)
|
||||
|
||||
def selectAssetPath(self):
|
||||
# Implement directory selection dialog
|
||||
@@ -131,3 +177,61 @@ class AssetPreferencesPage:
|
||||
if not asset_path:
|
||||
asset_path = str(Path.Preferences.getDefaultAssetPath())
|
||||
self.asset_path_edit.setText(asset_path)
|
||||
|
||||
def add_machine(self):
|
||||
# Create a new machine JSON file in the user's machine asset folder
|
||||
try:
|
||||
# Open editor for new machine, filename will be generated on save
|
||||
editor = MachineEditorDialog()
|
||||
if editor.exec_() == QtGui.QDialog.Accepted:
|
||||
# add to list
|
||||
filename = editor.filename
|
||||
display_name = MachineFactory.get_machine_display_name(filename)
|
||||
item = QtGui.QListWidgetItem(display_name)
|
||||
item.setData(QtCore.Qt.UserRole, filename) # Store filename only
|
||||
self.machines_list.addItem(item)
|
||||
except Exception as e:
|
||||
Path.Log.error(f"Failed to create machine file: {e}")
|
||||
|
||||
def edit_machine(self):
|
||||
try:
|
||||
item = self.machines_list.currentItem()
|
||||
if not item:
|
||||
return
|
||||
filename = item.data(QtCore.Qt.UserRole)
|
||||
if not filename:
|
||||
return
|
||||
dlg = MachineEditorDialog(filename)
|
||||
if dlg.exec_() == QtGui.QDialog.Accepted:
|
||||
# Reload display name from file after save
|
||||
display = MachineFactory.get_machine_display_name(filename)
|
||||
if display:
|
||||
item.setText(display)
|
||||
except Exception as e:
|
||||
Path.Log.error(f"Failed to open machine editor: {e}")
|
||||
|
||||
def delete_machine(self):
|
||||
try:
|
||||
item = self.machines_list.currentItem()
|
||||
if not item:
|
||||
return
|
||||
filename = item.data(QtCore.Qt.UserRole)
|
||||
if not filename:
|
||||
return
|
||||
# Confirm delete
|
||||
resp = QtGui.QMessageBox.question(
|
||||
self.form,
|
||||
translate("CAM_PreferencesAssets", "Delete Machine"),
|
||||
translate(
|
||||
"CAM_PreferencesAssets", "Are you sure you want to delete this machine file?"
|
||||
),
|
||||
QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
|
||||
)
|
||||
if resp != QtGui.QMessageBox.Yes:
|
||||
return
|
||||
if MachineFactory.delete_configuration(filename):
|
||||
self.machines_list.takeItem(self.machines_list.currentRow())
|
||||
else:
|
||||
Path.Log.error("Failed to delete machine file.")
|
||||
except Exception as e:
|
||||
Path.Log.error(f"Failed to delete machine: {e}")
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
from .models.machine import Machine
|
||||
|
||||
__all__ = [
|
||||
"Machine",
|
||||
]
|
||||
@@ -1 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
@@ -1,435 +0,0 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@gmail.com> *
|
||||
# * *
|
||||
# * 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 uuid
|
||||
import json
|
||||
import FreeCAD
|
||||
from FreeCAD import Base
|
||||
from typing import Optional, Union, Mapping, List
|
||||
from ...assets import Asset, AssetUri, AssetSerializer
|
||||
|
||||
|
||||
class Machine(Asset):
|
||||
"""Represents a machine with various operational parameters."""
|
||||
|
||||
asset_type: str = "machine"
|
||||
API_VERSION = 1
|
||||
|
||||
UNIT_CONVERSIONS = {
|
||||
"hp": 745.7, # hp to W
|
||||
"in-lbf": 0.112985, # in-lbf to N*m
|
||||
"inch/min": 25.4, # inch/min to mm/min
|
||||
"rpm": 1.0 / 60.0, # rpm to 1/s
|
||||
"kW": 1000.0, # kW to W
|
||||
"Nm": 1.0, # Nm to N*m
|
||||
"mm/min": 1.0, # mm/min to mm/min
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
label: str = "Machine",
|
||||
max_power: Union[int, float, FreeCAD.Units.Quantity] = 2,
|
||||
min_rpm: Union[int, float, FreeCAD.Units.Quantity] = 3000,
|
||||
max_rpm: Union[int, float, FreeCAD.Units.Quantity] = 60000,
|
||||
max_torque: Optional[Union[int, float, FreeCAD.Units.Quantity]] = None,
|
||||
peak_torque_rpm: Optional[Union[int, float, FreeCAD.Units.Quantity]] = None,
|
||||
min_feed: Union[int, float, FreeCAD.Units.Quantity] = 1,
|
||||
max_feed: Union[int, float, FreeCAD.Units.Quantity] = 2000,
|
||||
id: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initializes a Machine object.
|
||||
|
||||
Args:
|
||||
label: The label of the machine.
|
||||
max_power: The maximum power of the machine (kW or Quantity).
|
||||
min_rpm: The minimum RPM of the machine (RPM or Quantity).
|
||||
max_rpm: The maximum RPM of the machine (RPM or Quantity).
|
||||
max_torque: The maximum torque of the machine (Nm or Quantity).
|
||||
peak_torque_rpm: The RPM at which peak torque is achieved
|
||||
(RPM or Quantity).
|
||||
min_feed: The minimum feed rate of the machine
|
||||
(mm/min or Quantity).
|
||||
max_feed: The maximum feed rate of the machine
|
||||
(mm/min or Quantity).
|
||||
id: The unique identifier of the machine.
|
||||
"""
|
||||
self.id = id or str(uuid.uuid1())
|
||||
self._label = label
|
||||
|
||||
# Initialize max_power (W)
|
||||
if isinstance(max_power, FreeCAD.Units.Quantity):
|
||||
self._max_power = max_power.getValueAs("W").Value
|
||||
elif isinstance(max_power, (int, float)):
|
||||
self._max_power = max_power * self.UNIT_CONVERSIONS["kW"]
|
||||
else:
|
||||
self._max_power = 2000.0
|
||||
|
||||
# Initialize min_rpm (1/s)
|
||||
if isinstance(min_rpm, FreeCAD.Units.Quantity):
|
||||
try:
|
||||
self._min_rpm = min_rpm.getValueAs("1/s").Value
|
||||
except (Base.ParserError, ValueError):
|
||||
self._min_rpm = min_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
|
||||
elif isinstance(min_rpm, (int, float)):
|
||||
self._min_rpm = min_rpm * self.UNIT_CONVERSIONS["rpm"]
|
||||
else:
|
||||
self._min_rpm = 3000 * self.UNIT_CONVERSIONS["rpm"]
|
||||
|
||||
# Initialize max_rpm (1/s)
|
||||
if isinstance(max_rpm, FreeCAD.Units.Quantity):
|
||||
try:
|
||||
self._max_rpm = max_rpm.getValueAs("1/s").Value
|
||||
except (Base.ParserError, ValueError):
|
||||
self._max_rpm = max_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
|
||||
elif isinstance(max_rpm, (int, float)):
|
||||
self._max_rpm = max_rpm * self.UNIT_CONVERSIONS["rpm"]
|
||||
else:
|
||||
self._max_rpm = 60000 * self.UNIT_CONVERSIONS["rpm"]
|
||||
|
||||
# Initialize min_feed (mm/min)
|
||||
if isinstance(min_feed, FreeCAD.Units.Quantity):
|
||||
self._min_feed = min_feed.getValueAs("mm/min").Value
|
||||
elif isinstance(min_feed, (int, float)):
|
||||
self._min_feed = min_feed
|
||||
else:
|
||||
self._min_feed = 1.0
|
||||
|
||||
# Initialize max_feed (mm/min)
|
||||
if isinstance(max_feed, FreeCAD.Units.Quantity):
|
||||
self._max_feed = max_feed.getValueAs("mm/min").Value
|
||||
elif isinstance(max_feed, (int, float)):
|
||||
self._max_feed = max_feed
|
||||
else:
|
||||
self._max_feed = 2000.0
|
||||
|
||||
# Initialize peak_torque_rpm (1/s)
|
||||
if isinstance(peak_torque_rpm, FreeCAD.Units.Quantity):
|
||||
try:
|
||||
self._peak_torque_rpm = peak_torque_rpm.getValueAs("1/s").Value
|
||||
except (Base.ParserError, ValueError):
|
||||
self._peak_torque_rpm = peak_torque_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
|
||||
elif isinstance(peak_torque_rpm, (int, float)):
|
||||
self._peak_torque_rpm = peak_torque_rpm * self.UNIT_CONVERSIONS["rpm"]
|
||||
else:
|
||||
self._peak_torque_rpm = self._max_rpm / 3
|
||||
|
||||
# Initialize max_torque (N*m)
|
||||
if isinstance(max_torque, FreeCAD.Units.Quantity):
|
||||
self._max_torque = max_torque.getValueAs("Nm").Value
|
||||
elif isinstance(max_torque, (int, float)):
|
||||
self._max_torque = max_torque
|
||||
else:
|
||||
# Convert 1/s to rpm
|
||||
peak_rpm_for_calc = self._peak_torque_rpm * 60
|
||||
self._max_torque = (
|
||||
self._max_power * 9.5488 / peak_rpm_for_calc if peak_rpm_for_calc else float("inf")
|
||||
)
|
||||
|
||||
def get_id(self) -> str:
|
||||
"""Returns the unique identifier for the Machine instance."""
|
||||
return self.id
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Returns a dictionary representation of the Machine."""
|
||||
return {
|
||||
"version": self.API_VERSION,
|
||||
"id": self.id,
|
||||
"label": self.label,
|
||||
"max_power": self._max_power, # W
|
||||
"min_rpm": self._min_rpm, # 1/s
|
||||
"max_rpm": self._max_rpm, # 1/s
|
||||
"max_torque": self._max_torque, # Nm
|
||||
"peak_torque_rpm": self._peak_torque_rpm, # 1/s
|
||||
"min_feed": self._min_feed, # mm/min
|
||||
"max_feed": self._max_feed, # mm/min
|
||||
}
|
||||
|
||||
def to_bytes(self, serializer: AssetSerializer) -> bytes:
|
||||
"""Serializes the Machine object to bytes using to_dict."""
|
||||
data_dict = self.to_dict()
|
||||
json_str = json.dumps(data_dict)
|
||||
return json_str.encode("utf-8")
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data_dict: dict, id: str) -> "Machine":
|
||||
"""Creates a Machine instance from a dictionary."""
|
||||
machine = cls(
|
||||
label=data_dict.get("label", "Machine"),
|
||||
max_power=data_dict.get("max_power", 2000.0), # W
|
||||
min_rpm=data_dict.get("min_rpm", 3000 * cls.UNIT_CONVERSIONS["rpm"]), # 1/s
|
||||
max_rpm=data_dict.get("max_rpm", 60000 * cls.UNIT_CONVERSIONS["rpm"]), # 1/s
|
||||
max_torque=data_dict.get("max_torque", None), # Nm
|
||||
peak_torque_rpm=data_dict.get("peak_torque_rpm", None), # 1/s
|
||||
min_feed=data_dict.get("min_feed", 1.0), # mm/min
|
||||
max_feed=data_dict.get("max_feed", 2000.0), # mm/min
|
||||
id=id,
|
||||
)
|
||||
return machine
|
||||
|
||||
@classmethod
|
||||
def from_bytes(
|
||||
cls,
|
||||
data: bytes,
|
||||
id: str,
|
||||
dependencies: Optional[Mapping[AssetUri, Asset]],
|
||||
) -> "Machine":
|
||||
"""
|
||||
Deserializes bytes into a Machine instance using from_dict.
|
||||
"""
|
||||
# If dependencies is None, it's fine as Machine doesn't use it.
|
||||
data_dict = json.loads(data.decode("utf-8"))
|
||||
return cls.from_dict(data_dict, id)
|
||||
|
||||
@classmethod
|
||||
def dependencies(cls, data: bytes) -> List[AssetUri]:
|
||||
"""Returns a list of AssetUri dependencies parsed from the serialized data."""
|
||||
return [] # Machine has no dependencies
|
||||
|
||||
@property
|
||||
def max_power(self) -> FreeCAD.Units.Quantity:
|
||||
return FreeCAD.Units.Quantity(self._max_power, "W")
|
||||
|
||||
@property
|
||||
def min_rpm(self) -> FreeCAD.Units.Quantity:
|
||||
return FreeCAD.Units.Quantity(self._min_rpm, "1/s")
|
||||
|
||||
@property
|
||||
def max_rpm(self) -> FreeCAD.Units.Quantity:
|
||||
return FreeCAD.Units.Quantity(self._max_rpm, "1/s")
|
||||
|
||||
@property
|
||||
def max_torque(self) -> FreeCAD.Units.Quantity:
|
||||
return FreeCAD.Units.Quantity(self._max_torque, "Nm")
|
||||
|
||||
@property
|
||||
def peak_torque_rpm(self) -> FreeCAD.Units.Quantity:
|
||||
return FreeCAD.Units.Quantity(self._peak_torque_rpm, "1/s")
|
||||
|
||||
@property
|
||||
def min_feed(self) -> FreeCAD.Units.Quantity:
|
||||
return FreeCAD.Units.Quantity(self._min_feed, "mm/min")
|
||||
|
||||
@property
|
||||
def max_feed(self) -> FreeCAD.Units.Quantity:
|
||||
return FreeCAD.Units.Quantity(self._max_feed, "mm/min")
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return self._label
|
||||
|
||||
@label.setter
|
||||
def label(self, label: str) -> None:
|
||||
self._label = label
|
||||
|
||||
def get_min_rpm_value(self) -> float:
|
||||
"""Helper method to get minimum RPM value for display/testing."""
|
||||
return self._min_rpm * 60
|
||||
|
||||
def get_max_rpm_value(self) -> float:
|
||||
"""Helper method to get maximum RPM value for display/testing."""
|
||||
return self._max_rpm * 60
|
||||
|
||||
def get_peak_torque_rpm_value(self) -> float:
|
||||
"""Helper method to get peak torque RPM value for display/testing."""
|
||||
return self._peak_torque_rpm * 60
|
||||
|
||||
def validate(self) -> None:
|
||||
"""Validates the machine parameters."""
|
||||
if not self.label:
|
||||
raise AttributeError("Machine name is required")
|
||||
if self._peak_torque_rpm > self._max_rpm:
|
||||
err = ("Peak Torque RPM {ptrpm:.2f} must be less than max RPM " "{max_rpm:.2f}").format(
|
||||
ptrpm=self._peak_torque_rpm * 60, max_rpm=self._max_rpm * 60
|
||||
)
|
||||
raise AttributeError(err)
|
||||
if self._max_rpm <= self._min_rpm:
|
||||
raise AttributeError("Max RPM must be larger than min RPM")
|
||||
if self._max_feed <= self._min_feed:
|
||||
raise AttributeError("Max feed must be larger than min feed")
|
||||
|
||||
def get_torque_at_rpm(self, rpm: Union[int, float, FreeCAD.Units.Quantity]) -> float:
|
||||
"""
|
||||
Calculates the torque at a given RPM.
|
||||
|
||||
Args:
|
||||
rpm: The RPM value (int, float, or Quantity).
|
||||
|
||||
Returns:
|
||||
The torque at the given RPM in Nm.
|
||||
"""
|
||||
if isinstance(rpm, FreeCAD.Units.Quantity):
|
||||
try:
|
||||
rpm_hz = rpm.getValueAs("1/s").Value
|
||||
except (Base.ParserError, ValueError):
|
||||
rpm_hz = rpm.Value * self.UNIT_CONVERSIONS["rpm"]
|
||||
else:
|
||||
rpm_hz = rpm * self.UNIT_CONVERSIONS["rpm"]
|
||||
max_torque_nm = self._max_torque
|
||||
peak_torque_rpm_hz = self._peak_torque_rpm
|
||||
peak_rpm_for_calc = peak_torque_rpm_hz * 60
|
||||
rpm_for_calc = rpm_hz * 60
|
||||
torque_at_current_rpm = (
|
||||
self._max_power * 9.5488 / rpm_for_calc if rpm_for_calc else float("inf")
|
||||
)
|
||||
if rpm_for_calc <= peak_rpm_for_calc:
|
||||
torque_at_current_rpm = (
|
||||
max_torque_nm / peak_rpm_for_calc * rpm_for_calc
|
||||
if peak_rpm_for_calc
|
||||
else float("inf")
|
||||
)
|
||||
return min(max_torque_nm, torque_at_current_rpm)
|
||||
|
||||
def set_max_power(self, power: Union[int, float], unit: Optional[str] = None) -> None:
|
||||
"""Sets the maximum power of the machine."""
|
||||
unit = unit or "kW"
|
||||
if unit in self.UNIT_CONVERSIONS:
|
||||
power_value = power * self.UNIT_CONVERSIONS[unit]
|
||||
else:
|
||||
power_value = FreeCAD.Units.Quantity(power, unit).getValueAs("W").Value
|
||||
self._max_power = power_value
|
||||
if self._max_power <= 0:
|
||||
raise AttributeError("Max power must be positive")
|
||||
|
||||
def set_min_rpm(self, min_rpm: Union[int, float, FreeCAD.Units.Quantity]) -> None:
|
||||
"""Sets the minimum RPM of the machine."""
|
||||
if isinstance(min_rpm, FreeCAD.Units.Quantity):
|
||||
try:
|
||||
min_rpm_value = min_rpm.getValueAs("1/s").Value
|
||||
except (Base.ParserError, ValueError):
|
||||
min_rpm_value = min_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
|
||||
else:
|
||||
min_rpm_value = min_rpm * self.UNIT_CONVERSIONS["rpm"]
|
||||
self._min_rpm = min_rpm_value
|
||||
if self._min_rpm < 0:
|
||||
raise AttributeError("Min RPM cannot be negative")
|
||||
if self._min_rpm >= self._max_rpm:
|
||||
self._max_rpm = min_rpm_value + 1.0 / 60.0
|
||||
|
||||
def set_max_rpm(self, max_rpm: Union[int, float, FreeCAD.Units.Quantity]) -> None:
|
||||
"""Sets the maximum RPM of the machine."""
|
||||
if isinstance(max_rpm, FreeCAD.Units.Quantity):
|
||||
try:
|
||||
max_rpm_value = max_rpm.getValueAs("1/s").Value
|
||||
except (Base.ParserError, ValueError):
|
||||
max_rpm_value = max_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
|
||||
else:
|
||||
max_rpm_value = max_rpm * self.UNIT_CONVERSIONS["rpm"]
|
||||
self._max_rpm = max_rpm_value
|
||||
if self._max_rpm <= 0:
|
||||
raise AttributeError("Max RPM must be positive")
|
||||
if self._max_rpm <= self._min_rpm:
|
||||
self._min_rpm = max(0, max_rpm_value - 1.0 / 60.0)
|
||||
|
||||
def set_min_feed(
|
||||
self,
|
||||
min_feed: Union[int, float, FreeCAD.Units.Quantity],
|
||||
unit: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Sets the minimum feed rate of the machine."""
|
||||
unit = unit or "mm/min"
|
||||
if unit in self.UNIT_CONVERSIONS:
|
||||
min_feed_value = min_feed * self.UNIT_CONVERSIONS[unit]
|
||||
else:
|
||||
min_feed_value = FreeCAD.Units.Quantity(min_feed, unit).getValueAs("mm/min").Value
|
||||
self._min_feed = min_feed_value
|
||||
if self._min_feed < 0:
|
||||
raise AttributeError("Min feed cannot be negative")
|
||||
if self._min_feed >= self._max_feed:
|
||||
self._max_feed = min_feed_value + 1.0
|
||||
|
||||
def set_max_feed(
|
||||
self,
|
||||
max_feed: Union[int, float, FreeCAD.Units.Quantity],
|
||||
unit: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Sets the maximum feed rate of the machine."""
|
||||
unit = unit or "mm/min"
|
||||
if unit in self.UNIT_CONVERSIONS:
|
||||
max_feed_value = max_feed * self.UNIT_CONVERSIONS[unit]
|
||||
else:
|
||||
max_feed_value = FreeCAD.Units.Quantity(max_feed, unit).getValueAs("mm/min").Value
|
||||
self._max_feed = max_feed_value
|
||||
if self._max_feed <= 0:
|
||||
raise AttributeError("Max feed must be positive")
|
||||
if self._max_feed <= self._min_feed:
|
||||
self._min_feed = max(0, max_feed_value - 1.0)
|
||||
|
||||
def set_peak_torque_rpm(
|
||||
self, peak_torque_rpm: Union[int, float, FreeCAD.Units.Quantity]
|
||||
) -> None:
|
||||
"""Sets the peak torque RPM of the machine."""
|
||||
if isinstance(peak_torque_rpm, FreeCAD.Units.Quantity):
|
||||
try:
|
||||
peak_torque_rpm_value = peak_torque_rpm.getValueAs("1/s").Value
|
||||
except (Base.ParserError, ValueError):
|
||||
peak_torque_rpm_value = peak_torque_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
|
||||
else:
|
||||
peak_torque_rpm_value = peak_torque_rpm * self.UNIT_CONVERSIONS["rpm"]
|
||||
self._peak_torque_rpm = peak_torque_rpm_value
|
||||
if self._peak_torque_rpm < 0:
|
||||
raise AttributeError("Peak torque RPM cannot be negative")
|
||||
|
||||
def set_max_torque(
|
||||
self,
|
||||
max_torque: Union[int, float, FreeCAD.Units.Quantity],
|
||||
unit: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Sets the maximum torque of the machine."""
|
||||
unit = unit or "Nm"
|
||||
if unit in self.UNIT_CONVERSIONS:
|
||||
max_torque_value = max_torque * self.UNIT_CONVERSIONS[unit]
|
||||
else:
|
||||
max_torque_value = FreeCAD.Units.Quantity(max_torque, unit).getValueAs("Nm").Value
|
||||
self._max_torque = max_torque_value
|
||||
if self._max_torque <= 0:
|
||||
raise AttributeError("Max torque must be positive")
|
||||
|
||||
def dump(self, do_print: bool = True) -> Optional[str]:
|
||||
"""
|
||||
Dumps machine information to console or returns it as a string.
|
||||
|
||||
Args:
|
||||
do_print: If True, prints the information to the console.
|
||||
If False, returns the information as a string.
|
||||
|
||||
Returns:
|
||||
A formatted string containing machine information if do_print is
|
||||
False, otherwise None.
|
||||
"""
|
||||
min_rpm_value = self._min_rpm * 60
|
||||
max_rpm_value = self._max_rpm * 60
|
||||
peak_torque_rpm_value = self._peak_torque_rpm * 60
|
||||
|
||||
output = ""
|
||||
output += f"Machine {self.label}:\n"
|
||||
output += f" Max power: {self._max_power:.2f} W\n"
|
||||
output += f" RPM: {min_rpm_value:.2f} RPM - {max_rpm_value:.2f} RPM\n"
|
||||
output += f" Feed: {self.min_feed.UserString} - " f"{self.max_feed.UserString}\n"
|
||||
output += (
|
||||
f" Peak torque: {self._max_torque:.2f} Nm at " f"{peak_torque_rpm_value:.2f} RPM\n"
|
||||
)
|
||||
output += f" Max_torque: {self._max_torque} Nm\n"
|
||||
|
||||
if do_print:
|
||||
print(output)
|
||||
return output
|
||||
@@ -26,6 +26,11 @@ import TestApp
|
||||
|
||||
from CAMTests.TestCAMSanity import TestCAMSanity
|
||||
from CAMTests.TestLinkingGenerator import TestGetLinkingMoves
|
||||
from CAMTests.TestMachine import (
|
||||
TestMachineDataclass,
|
||||
TestMachineFactory,
|
||||
TestSpindle,
|
||||
)
|
||||
from CAMTests.TestPathProfile import TestPathProfile
|
||||
|
||||
from CAMTests.TestPathAdaptive import TestPathAdaptive
|
||||
@@ -94,7 +99,6 @@ from CAMTests.TestPathToolLibrarySerializer import (
|
||||
)
|
||||
from CAMTests.TestPathToolChangeGenerator import TestPathToolChangeGenerator
|
||||
from CAMTests.TestPathToolController import TestPathToolController
|
||||
from CAMTests.TestPathToolMachine import TestPathToolMachine
|
||||
from CAMTests.TestPathUtil import TestPathUtil
|
||||
from CAMTests.TestPathVcarve import TestPathVcarve
|
||||
from CAMTests.TestPathVoronoi import TestPathVoronoi
|
||||
|
||||
Reference in New Issue
Block a user