diff --git a/generate_machine_box.py b/generate_machine_box.py
new file mode 100644
index 0000000000..47584c57b7
--- /dev/null
+++ b/generate_machine_box.py
@@ -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()
\ No newline at end of file
diff --git a/src/Mod/CAM/Gui/Resources/preferences/PathJob.ui b/src/Mod/CAM/Gui/Resources/preferences/PathJob.ui
index 73482d95e4..d4646a2db1 100644
--- a/src/Mod/CAM/Gui/Resources/preferences/PathJob.ui
+++ b/src/Mod/CAM/Gui/Resources/preferences/PathJob.ui
@@ -15,20 +15,12 @@
-
-
+
0
-
-
-
- 0
- 0
- 695
- 308
-
-
-
+
+
General
@@ -118,16 +110,8 @@ If left empty no template will be preselected.
-
-
-
- 0
- 0
- 695
- 480
-
-
-
+
+
Post processor
@@ -334,16 +318,8 @@ See the file save policy below on how to deal with name conflicts.
-
-
-
- 0
- 0
- 674
- 619
-
-
-
+
+
Setup
diff --git a/src/Mod/CAM/Path/Dressup/Gui/Preferences.py b/src/Mod/CAM/Path/Dressup/Gui/Preferences.py
index 0d42921161..eb3e6630dc 100644
--- a/src/Mod/CAM/Path/Dressup/Gui/Preferences.py
+++ b/src/Mod/CAM/Path/Dressup/Gui/Preferences.py
@@ -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
diff --git a/src/Mod/CAM/Path/Machine/ui/editor/machine_editor.py b/src/Mod/CAM/Path/Machine/ui/editor/machine_editor.py
index 95bf2d8bda..a9f171f415 100644
--- a/src/Mod/CAM/Path/Machine/ui/editor/machine_editor.py
+++ b/src/Mod/CAM/Path/Machine/ui/editor/machine_editor.py
@@ -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)
diff --git a/src/Mod/CAM/Path/Main/Gui/PreferencesJob.py b/src/Mod/CAM/Path/Main/Gui/PreferencesJob.py
index 6900846bd6..4678029d40 100644
--- a/src/Mod/CAM/Path/Main/Gui/PreferencesJob.py
+++ b/src/Mod/CAM/Path/Main/Gui/PreferencesJob.py
@@ -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()
diff --git a/src/Mod/CAM/Path/Tool/assets/ui/preferences.py b/src/Mod/CAM/Path/Tool/assets/ui/preferences.py
index 5f1e763171..252de63165 100644
--- a/src/Mod/CAM/Path/Tool/assets/ui/preferences.py
+++ b/src/Mod/CAM/Path/Tool/assets/ui/preferences.py
@@ -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)