CAM: Refactor Machine Editor UI, replace QToolBox with tabs
Major refactor of the Machine Editor to use QTabWidget for section navigation. Added tabbed spindle management with add/remove functionality, split machine configuration into Output Options, G-Code Blocks, and Processing Options tabs. Updated preferences UI to use tabs instead of QToolBox. src/Mod/CAM/Gui/Resources/preferences/PathJob.ui: - Replace QToolBox with QTabWidget for preferences tabs src/Mod/CAM/Path/Dressup/Gui/Preferences.py: - Use QWidget with vertical layout instead of QToolBox for dressup preferences src/Mod/CAM/Path/Machine/ui/editor/machine_editor.py: - Refactor to use QTabWidget for editor sections - Implement tabbed spindle management with add/remove - Split configuration into Output Options, G-Code Blocks, and Processing Options tabs - Update post processor selection logic src/Mod/CAM/Path/Main/Gui/PreferencesJob.py: - Update to use tabWidget instead of toolBox src/Mod/CAM/Path/Tool/assets/ui/preferences.py: - Use QWidget and direct layout instead of QToolBox for asset preferences
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()
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -302,13 +302,12 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
|
||||
def __init__(self, machine_filename: Optional[str] = None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(translate("CAM_MachineEditor", "Machine Editor"))
|
||||
self.setMinimumSize(700, 900)
|
||||
self.resize(700, 900)
|
||||
|
||||
self.current_units = "metric"
|
||||
|
||||
# Initialize machine object first (needed by setup_post_tab)
|
||||
# Initialize machine object first (needed by setup methods)
|
||||
self.filename = machine_filename
|
||||
self.machine = None # Store the Machine object
|
||||
|
||||
@@ -317,6 +316,16 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
else:
|
||||
self.machine = Machine(name="New Machine")
|
||||
|
||||
# Set window title with machine name
|
||||
title = translate("CAM_MachineEditor", "Machine Editor")
|
||||
if self.machine and self.machine.name:
|
||||
title += f" - {self.machine.name}"
|
||||
self.setWindowTitle(title)
|
||||
|
||||
# Initialize widget and processor caches
|
||||
self.post_widgets = {}
|
||||
self.processor = {}
|
||||
|
||||
self.layout = QtGui.QVBoxLayout(self)
|
||||
|
||||
# Tab widget for sections
|
||||
@@ -328,15 +337,33 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
self.tabs.addTab(self.machine_tab, translate("CAM_MachineEditor", "Machine"))
|
||||
self.setup_machine_tab()
|
||||
|
||||
# Post tab
|
||||
self.post_tab = QtGui.QWidget()
|
||||
self.tabs.addTab(self.post_tab, translate("CAM_MachineEditor", "Post Processor"))
|
||||
self.setup_post_tab()
|
||||
# Output Options tab
|
||||
self.output_tab = QtGui.QWidget()
|
||||
self.tabs.addTab(self.output_tab, translate("CAM_MachineEditor", "Output Options"))
|
||||
self.setup_output_tab()
|
||||
|
||||
# G-Code Blocks tab
|
||||
self.blocks_tab = QtGui.QWidget()
|
||||
self.tabs.addTab(self.blocks_tab, translate("CAM_MachineEditor", "G-Code Blocks"))
|
||||
self.setup_blocks_tab()
|
||||
|
||||
# Processing Options tab
|
||||
self.processing_tab = QtGui.QWidget()
|
||||
self.tabs.addTab(self.processing_tab, translate("CAM_MachineEditor", "Processing Options"))
|
||||
self.setup_processing_tab()
|
||||
|
||||
# Check experimental flag for machine post processor
|
||||
param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/CAM")
|
||||
self.enable_machine_postprocessor = param.GetBool("EnableMachinePostprocessor", True)
|
||||
self.tabs.setTabVisible(self.tabs.indexOf(self.post_tab), self.enable_machine_postprocessor)
|
||||
self.tabs.setTabVisible(
|
||||
self.tabs.indexOf(self.output_tab), self.enable_machine_postprocessor
|
||||
)
|
||||
self.tabs.setTabVisible(
|
||||
self.tabs.indexOf(self.blocks_tab), self.enable_machine_postprocessor
|
||||
)
|
||||
self.tabs.setTabVisible(
|
||||
self.tabs.indexOf(self.processing_tab), self.enable_machine_postprocessor
|
||||
)
|
||||
# Text editor (initially hidden)
|
||||
self.text_editor = CodeEditor()
|
||||
|
||||
@@ -376,6 +403,9 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
# Populate GUI from machine object
|
||||
self.populate_from_machine(self.machine)
|
||||
|
||||
# Update spindle button state
|
||||
self._update_spindle_button_state()
|
||||
|
||||
# Set focus and select the name field for new machines
|
||||
if not machine_filename:
|
||||
self.name_edit.setFocus()
|
||||
@@ -496,6 +526,11 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
"""Update machine name when text changes."""
|
||||
if self.machine:
|
||||
self.machine.name = text
|
||||
# Update window title with new name
|
||||
title = translate("CAM_MachineEditor", "Machine Editor")
|
||||
if self.machine.name:
|
||||
title += f" - {self.machine.name}"
|
||||
self.setWindowTitle(title)
|
||||
|
||||
def _on_rotary_sequence_changed(self, axis_name, value):
|
||||
"""Update rotary axis sequence."""
|
||||
@@ -525,6 +560,52 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
spindle = self.machine.spindles[spindle_index]
|
||||
setattr(spindle, field_name, value)
|
||||
|
||||
def _add_spindle(self):
|
||||
"""Add a new spindle to the machine."""
|
||||
if self.machine and len(self.machine.spindles) < 9:
|
||||
new_index = len(self.machine.spindles) + 1
|
||||
new_spindle = Spindle(
|
||||
name=f"Spindle {new_index}",
|
||||
id=f"spindle{new_index}",
|
||||
max_power_kw=3.0,
|
||||
max_rpm=24000,
|
||||
min_rpm=6000,
|
||||
tool_change="manual",
|
||||
)
|
||||
self.machine.spindles.append(new_spindle)
|
||||
self.update_spindles()
|
||||
self._update_spindle_button_state()
|
||||
# Set focus to the new tab
|
||||
self.spindles_tabs.setCurrentIndex(len(self.machine.spindles) - 1)
|
||||
|
||||
def _remove_spindle(self, index):
|
||||
"""Remove a spindle from the machine with confirmation.
|
||||
|
||||
Args:
|
||||
index: Index of the tab/spindle to remove
|
||||
"""
|
||||
if not self.machine or len(self.machine.spindles) <= 1:
|
||||
return # Don't allow removing the last spindle
|
||||
|
||||
spindle = self.machine.spindles[index]
|
||||
reply = QtGui.QMessageBox.question(
|
||||
self,
|
||||
translate("CAM_MachineEditor", "Remove Spindle"),
|
||||
translate("CAM_MachineEditor", f"Remove '{spindle.name}'? This cannot be undone."),
|
||||
QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
|
||||
QtGui.QMessageBox.No,
|
||||
)
|
||||
|
||||
if reply == QtGui.QMessageBox.Yes:
|
||||
self.machine.spindles.pop(index)
|
||||
self.update_spindles()
|
||||
self._update_spindle_button_state()
|
||||
|
||||
def _update_spindle_button_state(self):
|
||||
"""Enable/disable the add spindle button based on count."""
|
||||
if self.machine:
|
||||
self.add_spindle_button.setEnabled(len(self.machine.spindles) < 9)
|
||||
|
||||
def _on_manufacturer_changed(self, text):
|
||||
"""Update manufacturer when text changes."""
|
||||
if self.machine:
|
||||
@@ -639,13 +720,30 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
self.type_combo.currentIndexChanged.connect(self._on_type_changed)
|
||||
layout.addRow(translate("CAM_MachineEditor", "Type"), self.type_combo)
|
||||
|
||||
self.spindle_count_combo = QtGui.QComboBox()
|
||||
for i in range(1, 10): # 1 to 9 spindles
|
||||
self.spindle_count_combo.addItem(str(i), i)
|
||||
self.spindle_count_combo.currentIndexChanged.connect(self.update_spindles)
|
||||
layout.addRow(
|
||||
translate("CAM_MachineEditor", "Number of spindles"), self.spindle_count_combo
|
||||
# Post Processor Selection
|
||||
self.post_processor_combo = QtGui.QComboBox()
|
||||
postProcessors = Path.Preferences.allEnabledPostProcessors([""])
|
||||
for post in postProcessors:
|
||||
self.post_processor_combo.addItem(post)
|
||||
self.post_processor_combo.currentIndexChanged.connect(self.updatePostProcessorTooltip)
|
||||
self.post_processor_combo.currentIndexChanged.connect(
|
||||
lambda: self._update_machine_field(
|
||||
"postprocessor_file_name", self.post_processor_combo.currentText()
|
||||
)
|
||||
)
|
||||
self.postProcessorDefaultTooltip = translate("CAM_MachineEditor", "Select a post processor")
|
||||
self.post_processor_combo.setToolTip(self.postProcessorDefaultTooltip)
|
||||
layout.addRow(translate("CAM_MachineEditor", "Post Processor"), self.post_processor_combo)
|
||||
|
||||
# self.post_processor_args_edit = QtGui.QLineEdit()
|
||||
# self.post_processor_args_edit.textChanged.connect(
|
||||
# lambda text: self._update_machine_field("postprocessor_args", text)
|
||||
# )
|
||||
# self.postProcessorArgsDefaultTooltip = translate(
|
||||
# "CAM_MachineEditor", "Additional arguments"
|
||||
# )
|
||||
# self.post_processor_args_edit.setToolTip(self.postProcessorArgsDefaultTooltip)
|
||||
# layout.addRow(translate("CAM_MachineEditor", "Arguments"), self.post_processor_args_edit)
|
||||
|
||||
# Axes group
|
||||
self.axes_group = QtGui.QGroupBox(translate("CAM_MachineEditor", "Axes"))
|
||||
@@ -656,7 +754,25 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
# Spindles group
|
||||
self.spindles_group = QtGui.QGroupBox(translate("CAM_MachineEditor", "Spindles"))
|
||||
spindles_layout = QtGui.QVBoxLayout(self.spindles_group)
|
||||
|
||||
self.spindles_tabs = QtGui.QTabWidget()
|
||||
self.spindles_tabs.setTabsClosable(True)
|
||||
self.spindles_tabs.tabCloseRequested.connect(self._remove_spindle)
|
||||
|
||||
# Add + button to the tab bar corner, vertically centered
|
||||
corner_container = QtGui.QWidget()
|
||||
corner_container_layout = QtGui.QVBoxLayout(corner_container)
|
||||
corner_container_layout.setContentsMargins(0, 0, 0, 0)
|
||||
corner_container_layout.setSpacing(0)
|
||||
corner_container_layout.addStretch()
|
||||
self.add_spindle_button = QtGui.QPushButton("+")
|
||||
self.add_spindle_button.setToolTip(translate("CAM_MachineEditor", "Add Spindle"))
|
||||
self.add_spindle_button.clicked.connect(self._add_spindle)
|
||||
self.add_spindle_button.setSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
|
||||
corner_container_layout.addWidget(self.add_spindle_button, 0, QtCore.Qt.AlignCenter)
|
||||
corner_container_layout.addStretch()
|
||||
self.spindles_tabs.setCornerWidget(corner_container, QtCore.Qt.TopRightCorner)
|
||||
|
||||
spindles_layout.addWidget(self.spindles_tabs)
|
||||
layout.addRow(self.spindles_group)
|
||||
|
||||
@@ -701,8 +817,6 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
self.axes_group.setVisible(False)
|
||||
return
|
||||
|
||||
axes_form = QtGui.QFormLayout()
|
||||
|
||||
# Get axes directly from machine object
|
||||
linear_axes = list(self.machine.linear_axes.keys()) if self.machine else []
|
||||
rotary_axes = list(self.machine.rotary_axes.keys()) if self.machine else []
|
||||
@@ -852,7 +966,10 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
axis_grid.addWidget(QtGui.QLabel("Prefer+"), 1, 4, QtCore.Qt.AlignRight)
|
||||
axis_grid.addWidget(prefer_positive, 1, 5)
|
||||
|
||||
rotary_layout.addRow(f"{axis}", axis_grid)
|
||||
axis_label = QtGui.QLabel(f"{axis}")
|
||||
axis_label.setMinimumWidth(30) # Prevent layout shift when axis names change
|
||||
rotary_layout.addRow(axis_label, axis_grid)
|
||||
|
||||
self.axis_edits[axis] = {
|
||||
"min": min_edit,
|
||||
"max": max_edit,
|
||||
@@ -874,26 +991,6 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
input fields for name, ID, power, speed, and tool holder.
|
||||
Updates Machine.spindles directly.
|
||||
"""
|
||||
# Update machine spindles with current edits before rebuilding UI
|
||||
if hasattr(self, "spindle_edits") and self.machine:
|
||||
# Resize spindles list to match current edits
|
||||
while len(self.machine.spindles) < len(self.spindle_edits):
|
||||
self.machine.spindles.append(Spindle(name=""))
|
||||
while len(self.machine.spindles) > len(self.spindle_edits):
|
||||
self.machine.spindles.pop()
|
||||
|
||||
# Update each spindle from UI
|
||||
for i, edits in enumerate(self.spindle_edits):
|
||||
spindle = self.machine.spindles[i]
|
||||
spindle.name = edits["name"].text()
|
||||
spindle.id = edits["id"].text()
|
||||
spindle.max_power_kw = edits["max_power_kw"].value()
|
||||
spindle.max_rpm = edits["max_rpm"].value()
|
||||
spindle.min_rpm = edits["min_rpm"].value()
|
||||
spindle.tool_change = edits["tool_change"].itemData(
|
||||
edits["tool_change"].currentIndex()
|
||||
)
|
||||
|
||||
# Clear existing spindle tabs - this properly disconnects signals
|
||||
while self.spindles_tabs.count() > 0:
|
||||
tab = self.spindles_tabs.widget(0)
|
||||
@@ -903,9 +1000,9 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
self.spindles_tabs.removeTab(0)
|
||||
|
||||
self.spindle_edits = []
|
||||
count = self.spindle_count_combo.itemData(self.spindle_count_combo.currentIndex())
|
||||
count = len(self.machine.spindles) if self.machine else 1
|
||||
|
||||
# Ensure machine has enough spindles
|
||||
# Ensure machine has at least 1 spindle
|
||||
if self.machine:
|
||||
while len(self.machine.spindles) < count:
|
||||
self.machine.spindles.append(
|
||||
@@ -996,8 +1093,8 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
}
|
||||
)
|
||||
|
||||
def setup_post_tab(self):
|
||||
"""Set up the post processor configuration tab dynamically from Machine dataclass."""
|
||||
def setup_output_tab(self):
|
||||
"""Set up the output options configuration tab."""
|
||||
# Use scroll area for all the options
|
||||
scroll = QtGui.QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
@@ -1005,61 +1102,55 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
layout = QtGui.QVBoxLayout(scroll_widget)
|
||||
scroll.setWidget(scroll_widget)
|
||||
|
||||
main_layout = QtGui.QVBoxLayout(self.post_tab)
|
||||
main_layout = QtGui.QVBoxLayout(self.output_tab)
|
||||
main_layout.addWidget(scroll)
|
||||
|
||||
# Store widgets for later population
|
||||
self.post_widgets = {}
|
||||
|
||||
# === Post Processor Selection (special handling for combo box) ===
|
||||
pp_group = QtGui.QGroupBox("Post Processor Selection")
|
||||
pp_layout = QtGui.QFormLayout(pp_group)
|
||||
|
||||
self.post_processor_combo = QtGui.QComboBox()
|
||||
postProcessors = Path.Preferences.allEnabledPostProcessors([""])
|
||||
for post in postProcessors:
|
||||
self.post_processor_combo.addItem(post)
|
||||
self.post_processor_combo.currentIndexChanged.connect(self.updatePostProcessorTooltip)
|
||||
self.post_processor_combo.currentIndexChanged.connect(
|
||||
lambda: self._update_machine_field(
|
||||
"postprocessor_file_name", self.post_processor_combo.currentText()
|
||||
)
|
||||
)
|
||||
self.postProcessorDefaultTooltip = translate("CAM_MachineEditor", "Select a post processor")
|
||||
self.post_processor_combo.setToolTip(self.postProcessorDefaultTooltip)
|
||||
pp_layout.addRow("Post Processor", self.post_processor_combo)
|
||||
self.post_widgets["postprocessor_file_name"] = self.post_processor_combo
|
||||
|
||||
self.post_processor_args_edit = QtGui.QLineEdit()
|
||||
self.post_processor_args_edit.textChanged.connect(
|
||||
lambda text: self._update_machine_field("postprocessor_args", text)
|
||||
)
|
||||
self.postProcessorArgsDefaultTooltip = translate(
|
||||
"CAM_MachineEditor", "Additional arguments"
|
||||
)
|
||||
self.post_processor_args_edit.setToolTip(self.postProcessorArgsDefaultTooltip)
|
||||
pp_layout.addRow("Arguments", self.post_processor_args_edit)
|
||||
self.post_widgets["postprocessor_args"] = self.post_processor_args_edit
|
||||
|
||||
layout.addWidget(pp_group)
|
||||
|
||||
# === Dynamically generate groups for nested dataclasses ===
|
||||
# === Output Options ===
|
||||
if self.machine:
|
||||
# Output Options
|
||||
output_group, output_widgets = DataclassGUIGenerator.create_group_for_dataclass(
|
||||
self.machine.output, "Output Options"
|
||||
)
|
||||
layout.addWidget(output_group)
|
||||
self._connect_widgets_to_machine(output_widgets, "output")
|
||||
|
||||
# G-Code Blocks
|
||||
layout.addStretch()
|
||||
|
||||
def setup_blocks_tab(self):
|
||||
"""Set up the G-Code blocks configuration tab."""
|
||||
# Use scroll area for all the options
|
||||
scroll = QtGui.QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll_widget = QtGui.QWidget()
|
||||
layout = QtGui.QVBoxLayout(scroll_widget)
|
||||
scroll.setWidget(scroll_widget)
|
||||
|
||||
main_layout = QtGui.QVBoxLayout(self.blocks_tab)
|
||||
main_layout.addWidget(scroll)
|
||||
|
||||
# === G-Code Blocks ===
|
||||
if self.machine:
|
||||
blocks_group, blocks_widgets = DataclassGUIGenerator.create_group_for_dataclass(
|
||||
self.machine.blocks, "G-Code Blocks"
|
||||
)
|
||||
layout.addWidget(blocks_group)
|
||||
self._connect_widgets_to_machine(blocks_widgets, "blocks")
|
||||
|
||||
# Processing Options
|
||||
layout.addStretch()
|
||||
|
||||
def setup_processing_tab(self):
|
||||
"""Set up the processing options configuration tab."""
|
||||
# Use scroll area for all the options
|
||||
scroll = QtGui.QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll_widget = QtGui.QWidget()
|
||||
layout = QtGui.QVBoxLayout(scroll_widget)
|
||||
scroll.setWidget(scroll_widget)
|
||||
|
||||
main_layout = QtGui.QVBoxLayout(self.processing_tab)
|
||||
main_layout.addWidget(scroll)
|
||||
|
||||
# === Processing Options ===
|
||||
if self.machine:
|
||||
processing_group, processing_widgets = DataclassGUIGenerator.create_group_for_dataclass(
|
||||
self.machine.processing, "Processing Options"
|
||||
)
|
||||
@@ -1068,9 +1159,6 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
# Cache for post processors
|
||||
self.processor = {}
|
||||
|
||||
def _connect_widgets_to_machine(self, widgets: Dict[str, QtGui.QWidget], parent_path: str):
|
||||
"""Connect widgets to update Machine object fields.
|
||||
|
||||
@@ -1267,13 +1355,13 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
self.post_processor_combo, name, self.postProcessorDefaultTooltip
|
||||
)
|
||||
processor = self.getPostProcessor(name)
|
||||
if processor.tooltipArgs:
|
||||
self.post_processor_args_edit.setToolTip(processor.tooltipArgs)
|
||||
else:
|
||||
self.post_processor_args_edit.setToolTip(self.postProcessorArgsDefaultTooltip)
|
||||
# if processor.tooltipArgs:
|
||||
# self.post_processor_args_edit.setToolTip(processor.tooltipArgs)
|
||||
# else:
|
||||
# self.post_processor_args_edit.setToolTip(self.postProcessorArgsDefaultTooltip)
|
||||
else:
|
||||
self.post_processor_combo.setToolTip(self.postProcessorDefaultTooltip)
|
||||
self.post_processor_args_edit.setToolTip(self.postProcessorArgsDefaultTooltip)
|
||||
# self.post_processor_args_edit.setToolTip(self.postProcessorArgsDefaultTooltip)
|
||||
|
||||
def populate_from_machine(self, machine: Machine):
|
||||
"""Populate UI fields from Machine object.
|
||||
@@ -1285,14 +1373,31 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
self.manufacturer_edit.setText(machine.manufacturer)
|
||||
self.description_edit.setText(machine.description)
|
||||
units = machine.configuration_units
|
||||
self.units_combo.blockSignals(True)
|
||||
index = self.units_combo.findData(units)
|
||||
if index >= 0:
|
||||
self.units_combo.setCurrentIndex(index)
|
||||
self.units_combo.blockSignals(False)
|
||||
self.current_units = units
|
||||
machine_type = machine.machine_type
|
||||
self.type_combo.blockSignals(True)
|
||||
index = self.type_combo.findData(machine_type)
|
||||
if index >= 0:
|
||||
self.type_combo.setCurrentIndex(index)
|
||||
self.type_combo.blockSignals(False)
|
||||
|
||||
# Post processor selection
|
||||
if self.enable_machine_postprocessor:
|
||||
post_processor = machine.postprocessor_file_name
|
||||
index = self.post_processor_combo.findText(post_processor, QtCore.Qt.MatchFixedString)
|
||||
if index >= 0:
|
||||
self.post_processor_combo.setCurrentIndex(index)
|
||||
else:
|
||||
self.post_processor_combo.setCurrentIndex(0)
|
||||
self.updatePostProcessorTooltip()
|
||||
|
||||
# Post processor arguments
|
||||
# self.post_processor_args_edit.setText(machine.postprocessor_args)
|
||||
|
||||
# Get units for suffixes in populate
|
||||
units = self.units_combo.itemData(self.units_combo.currentIndex())
|
||||
@@ -1300,28 +1405,23 @@ class MachineEditorDialog(QtGui.QDialog):
|
||||
# Update axes UI after loading machine data
|
||||
self.update_axes()
|
||||
|
||||
spindles = machine.spindles
|
||||
spindle_count = len(spindles)
|
||||
if spindle_count == 0:
|
||||
spindle_count = 1 # Default to 1 if none
|
||||
spindle_count = min(spindle_count, 9) # Cap at 9
|
||||
self.spindle_count_combo.setCurrentText(str(spindle_count))
|
||||
self.update_spindles() # Update spindles after setting count (will populate from machine.spindles)
|
||||
# Ensure at least 1 spindle
|
||||
if len(machine.spindles) == 0:
|
||||
machine.spindles.append(
|
||||
Spindle(
|
||||
name="Spindle 1",
|
||||
id="spindle1",
|
||||
max_power_kw=3.0,
|
||||
max_rpm=24000,
|
||||
min_rpm=6000,
|
||||
tool_change="manual",
|
||||
)
|
||||
)
|
||||
self.update_spindles() # Update spindles UI
|
||||
self._update_spindle_button_state()
|
||||
|
||||
# Post processor configuration - populate dynamically generated widgets
|
||||
if self.enable_machine_postprocessor and hasattr(self, "post_widgets"):
|
||||
# Post processor selection
|
||||
post_processor = machine.postprocessor_file_name
|
||||
index = self.post_processor_combo.findText(post_processor, QtCore.Qt.MatchFixedString)
|
||||
if index >= 0:
|
||||
self.post_processor_combo.setCurrentIndex(index)
|
||||
else:
|
||||
self.post_processor_combo.setCurrentIndex(0)
|
||||
|
||||
# Post processor arguments
|
||||
self.post_processor_args_edit.setText(machine.postprocessor_args)
|
||||
self.updatePostProcessorTooltip()
|
||||
|
||||
# Update all post-processor widgets from machine object
|
||||
self._populate_post_widgets_from_machine(machine)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -48,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)
|
||||
@@ -76,39 +77,52 @@ 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.setRowStretch(3, 1)
|
||||
# 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)
|
||||
|
||||
main_layout.addLayout(edit_button_layout, QtCore.Qt.AlignTop)
|
||||
# Machines group box
|
||||
self.machines_group = QtGui.QGroupBox(translate("CAM_PreferencesAssets", "Machines"))
|
||||
self.machines_layout = QtGui.QVBoxLayout(self.machines_group)
|
||||
|
||||
self.form.addItem(asset_path_widget, translate("CAM_PreferencesAssets", "Assets"))
|
||||
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)
|
||||
|
||||
# Integrate machines list into the Assets panel
|
||||
machines_list_layout = QtGui.QVBoxLayout()
|
||||
machines_label = QtGui.QLabel(translate("CAM_PreferencesAssets", "Machines"))
|
||||
machines_list_layout.addWidget(machines_label)
|
||||
self.machines_label = QtGui.QLabel(translate("CAM_PreferencesAssets", "Machines"))
|
||||
self.machines_layout.addWidget(self.machines_label)
|
||||
|
||||
self.machines_list = QtGui.QListWidget()
|
||||
machines_list_layout.addWidget(self.machines_list)
|
||||
self.machines_layout.addWidget(self.machines_list)
|
||||
|
||||
# Buttons: Add / Edit / Delete
|
||||
btn_layout = QtGui.QHBoxLayout()
|
||||
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"))
|
||||
btn_layout.addWidget(self.add_machine_btn)
|
||||
btn_layout.addWidget(self.edit_machine_btn)
|
||||
btn_layout.addWidget(self.delete_machine_btn)
|
||||
machines_list_layout.addLayout(btn_layout)
|
||||
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)
|
||||
|
||||
# Insert the machines list directly under the path controls
|
||||
edit_button_layout.addLayout(machines_list_layout, 2, 0, 1, 4)
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user