Draft: Introduce 1-axis mode for ortho array (#21602)

* Draft: Introduce 1-axis mode for ortho array

As the title says - the 1 axis mode allows to switch between all of the
axises mode and allows to modify only 1 axis at the time that user can
select with the checkbox.

* Draft: Rename to Linear Mode and remove redundant comments

* Draft: Display only one interval for the selected axis

* Draft: Cache selected variables in user.cfg - axis mode, intervals, etc...

* Draft: Make sure the checkboxes in OrthoArray are exclusively selected

* Draft: Apply review comments

Changed a couple of things according to review:
* linear mode is now being used as default during first startup (it
  wasn't before)
* applied Roy's comments about coding style, etc.
* grouped everything into separate QGroupBox which is dedicated for
  Linear Mode and has it's own labels, although spinboxes are shared so
  during mode switch we reparent them now
* removed Orthogonal Array's icon
* in the QGroup applied naming suggested in the review, X Intervals ->
  interval, etc.
* changed to radio buttons since we want exclusivity in selection
This commit is contained in:
tetektoza
2025-06-09 17:33:51 +02:00
committed by GitHub
parent a38689b6fb
commit 29ca81b64f
3 changed files with 293 additions and 16 deletions

View File

@@ -39,10 +39,59 @@
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="label_icon">
<property name="text">
<string notr="true">(Placeholder for the icon)</string>
<widget class="QGroupBox" name="group_axis_mode">
<property name="toolTip">
<string>Toggle between Orthogonal mode and Linear mode. In Linear mode, you can select which axis to use.</string>
</property>
<property name="title">
<string>Axis mode</string>
</property>
<layout class="QGridLayout" name="gridLayout_axis_mode">
<item row="0" column="0">
<widget class="QPushButton" name="button_linear_mode">
<property name="text">
<string>Switch to linear mode</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<layout class="QGridLayout" name="grid_axis_select">
<item row="0" column="0">
<widget class="QRadioButton" name="radiobutton_x_axis">
<property name="text">
<string>X Axis</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QRadioButton" name="radiobutton_y_axis">
<property name="text">
<string>Y Axis</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QRadioButton" name="radiobutton_z_axis">
<property name="text">
<string>Z Axis</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
@@ -122,6 +171,18 @@ The number must be at least 1 in each direction.</string>
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QGroupBox" name="group_linearmode">
<property name="toolTip">
<string>Currently selected axis</string>
</property>
<property name="title">
<string></string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
</layout>
</widget>
</item>
<item row="4" column="0">
<widget class="QGroupBox" name="group_X">
<property name="toolTip">

View File

@@ -29,7 +29,7 @@
# @{
import PySide.QtGui as QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
from PySide import QtWidgets
import FreeCAD as App
import FreeCADGui as Gui
import Draft_rc # include resources, icons, ui files
@@ -93,19 +93,18 @@ class TaskPanelOrthoArray:
self.form.setWindowIcon(icon)
self.form.setWindowTitle(translate("draft","Orthogonal array"))
self.form.label_icon.setPixmap(pix.scaled(32, 32))
# -------------------------------------------------------------------
# Default values for the internal function,
# and for the task panel interface
start_x = U.Quantity(100.0, App.Units.Length)
start_y = start_x
start_z = start_x
length_unit = start_x.getUserPreferred()[2]
start_x = params.get_param("XInterval", "Mod/Draft/OrthoArrayLinearMode")
start_y = params.get_param("YInterval", "Mod/Draft/OrthoArrayLinearMode")
start_z = params.get_param("ZInterval", "Mod/Draft/OrthoArrayLinearMode")
self.v_x = App.Vector(start_x.Value, 0, 0)
self.v_y = App.Vector(0, start_y.Value, 0)
self.v_z = App.Vector(0, 0, start_z.Value)
length_unit = App.Units.Quantity(0.0, App.Units.Length).getUserPreferred()[2]
self.v_x = App.Vector(start_x, 0, 0)
self.v_y = App.Vector(0, start_y, 0)
self.v_z = App.Vector(0, 0, start_z)
self.form.input_X_x.setProperty('rawValue', self.v_x.x)
self.form.input_X_x.setProperty('unit', length_unit)
@@ -128,9 +127,9 @@ class TaskPanelOrthoArray:
self.form.input_Z_z.setProperty('rawValue', self.v_z.z)
self.form.input_Z_z.setProperty('unit', length_unit)
self.n_x = 2
self.n_y = 2
self.n_z = 1
self.n_x = params.get_param("XNumOfElements", "Mod/Draft/OrthoArrayLinearMode")
self.n_y = params.get_param("YNumOfElements", "Mod/Draft/OrthoArrayLinearMode")
self.n_z = params.get_param("ZNumOfElements", "Mod/Draft/OrthoArrayLinearMode")
self.form.spinbox_n_X.setValue(self.n_x)
self.form.spinbox_n_Y.setValue(self.n_y)
@@ -143,6 +142,19 @@ class TaskPanelOrthoArray:
self.form.checkbox_link.setChecked(self.use_link)
# -------------------------------------------------------------------
# Initialize Linear mode variables
self.linear_mode = params.get_param("LinearModeOn", "Mod/Draft/OrthoArrayLinearMode")
self.active_axis = params.get_param("AxisSelected", "Mod/Draft/OrthoArrayLinearMode")
# Hide the axis radiobuttons initially (they're only visible in Linear mode)
self.toggle_axis_radiobuttons(False)
if self.linear_mode:
self.form.button_linear_mode.setChecked(True)
self.toggle_linear_mode()
else:
self.form.group_linearmode.hide()
# -------------------------------------------------------------------
# Some objects need to be selected before we can execute the function.
self.selection = None
@@ -166,6 +178,12 @@ class TaskPanelOrthoArray:
self.form.checkbox_fuse.stateChanged.connect(self.set_fuse)
self.form.checkbox_link.stateChanged.connect(self.set_link)
# Linear mode callbacks - only set up if the UI elements exist
self.form.button_linear_mode.clicked.connect(self.toggle_linear_mode)
self.form.radiobutton_x_axis.clicked.connect(lambda: self.set_active_axis("X"))
self.form.radiobutton_y_axis.clicked.connect(lambda: self.set_active_axis("Y"))
self.form.radiobutton_z_axis.clicked.connect(lambda: self.set_active_axis("Z"))
def accept(self):
"""Execute when clicking the OK button or Enter key."""
@@ -183,6 +201,20 @@ class TaskPanelOrthoArray:
self.v_x, self.v_y, self.v_z,
self.n_x, self.n_y, self.n_z)
if self.valid_input:
axis_values = {
'X': (self.v_x.x, self.n_x),
'Y': (self.v_y.y, self.n_y),
'Z': (self.v_z.z, self.n_z),
}
values = axis_values.get(self.active_axis)
if values is not None:
interval, num_elements = values
# Set interval
params.set_param(f"{self.active_axis}Interval", interval, "Mod/Draft/OrthoArrayLinearMode")
# Set number of elements
params.set_param(f"{self.active_axis}NumOfElements", num_elements, "Mod/Draft/OrthoArrayLinearMode")
self.create_object()
# The internal function already displays messages
self.finish()
@@ -211,6 +243,14 @@ class TaskPanelOrthoArray:
_err(translate("draft","Object:") + " {0} ({1})".format(obj.Label, obj.TypeId))
return False
# we should not ever do this but maybe a sanity check here?
if self.linear_mode:
if not (self.form.radiobutton_x_axis.isChecked() or
self.form.radiobutton_y_axis.isChecked() or
self.form.radiobutton_z_axis.isChecked()):
_err(translate("draft","In Linear mode, at least one axis must be selected."))
return False
# The other arguments are not tested but they should be present.
if v_x and v_y and v_z:
pass
@@ -234,6 +274,20 @@ class TaskPanelOrthoArray:
# make a compound and then use it as input for the array function.
sel_obj = self.selection[0]
# clear this stuff out to be sure we don't preserve something
# from 3-axis mode
if self.linear_mode:
start_val = App.Units.Quantity(100.0, App.Units.Length).Value
if not self.form.radiobutton_x_axis.isChecked():
self.n_x = 1
self.v_x = App.Vector(start_val, 0, 0)
if not self.form.radiobutton_y_axis.isChecked():
self.n_y = 1
self.v_y = App.Vector(0, start_val, 0)
if not self.form.radiobutton_z_axis.isChecked():
self.n_z = 1
self.v_z = App.Vector(0, 0, start_val)
# This creates the object immediately
# obj = Draft.make_ortho_array(sel_obj,
# self.v_x, self.v_y, self.v_z,
@@ -357,6 +411,7 @@ class TaskPanelOrthoArray:
# make a compound and then use it as input for the array function.
sel_obj = self.selection[0]
_msg(translate("draft","Object:") + " {}".format(sel_obj.Label))
_msg(translate("draft","Number of X elements:") + " {}".format(self.n_x))
_msg(translate("draft","Interval X:")
+ " ({0}, {1}, {2})".format(self.v_x.x,
@@ -379,6 +434,155 @@ class TaskPanelOrthoArray:
"""Execute when clicking the Cancel button or pressing Escape."""
self.finish()
def toggle_linear_mode(self):
"""Toggle between Linear mode and Orthogonal mode."""
self.linear_mode = self.form.button_linear_mode.isChecked()
self.toggle_axis_radiobuttons(self.linear_mode)
params.set_param("LinearModeOn" , self.linear_mode, "Mod/Draft/OrthoArrayLinearMode")
if self.linear_mode:
self.form.button_linear_mode.setText(translate("draft", "Switch to ortho mode"))
# check radiobutton based on current cfg
self.update_axis_ui()
# For linear mode we're hiding all group boxes for X, Y, Z axis and the one
# with number of elements as we will reparent those spinboxes under newly
# created group
self._set_orthomode_groups_visibility(hide=True)
self._reparent_groups(mode="Linear")
# Set the appropriate title for the group (we flip it back and forth after changing mode)
# and show the group
self.form.group_linearmode.show()
self.form.group_linearmode.setTitle(f"{self.active_axis} Axis")
else: # ortho mode
self.form.button_linear_mode.setText(translate("draft", "Switch to linear mode"))
# For ortho mode we're showing back default groupboxes and we reparent everything
# back to them, as we reuse spinboxes in both modes
self._set_orthomode_groups_visibility(hide=False)
self._reparent_groups(mode="Ortho")
self.form.group_linearmode.hide()
def toggle_axis_radiobuttons(self, show):
"""Show or hide the axis radio buttons."""
for radiobutton in [self.form.radiobutton_x_axis,
self.form.radiobutton_y_axis,
self.form.radiobutton_z_axis]:
radiobutton.setVisible(show)
def set_active_axis(self, axis):
"""Set the active axis when a radio button is changed."""
if self.linear_mode:
# get current radiobutton that was supposed to have state changed
radiobutton = getattr(self.form, f"radiobutton_{axis.lower()}_axis")
# If we're checking a different radio button than the current active axis
if radiobutton.isChecked() and axis != self.active_axis:
self.active_axis = axis
params.set_param("AxisSelected", self.active_axis, "Mod/Draft/OrthoArrayLinearMode")
self._setup_linear_mode_layout()
self.form.group_linearmode.setTitle(f"{self.active_axis} Axis")
def update_axis_ui(self):
"""Update the UI to reflect the current axis selection."""
# Make sure only one axis is selected
self.form.radiobutton_x_axis.setChecked(self.active_axis == "X")
self.form.radiobutton_y_axis.setChecked(self.active_axis == "Y")
self.form.radiobutton_z_axis.setChecked(self.active_axis == "Z")
def _get_axis_widgets(self, axis):
"""Get all widgets for a specific axis."""
return {
'spinbox_elements': getattr(self.form, f"spinbox_n_{axis}", None),
'spinbox_interval': getattr(self.form, f"input_{axis}_{axis.lower()}", None),
'button_reset': getattr(self.form, f"button_reset_{axis}", None)
}
def _clear_linear_mode_layout(self):
"""Clear all widgets from the linear mode layout."""
group_layout = self.form.group_linearmode.layout()
# Remove all items from the layout
while group_layout.count():
item = group_layout.takeAt(0)
if item.widget():
item.widget().setParent(None)
def _setup_linear_mode_layout(self):
"""Set up the Linear mode layout with widgets for the active axis."""
# Clear existing layout first
self._clear_linear_mode_layout()
group_layout = self.form.group_linearmode.layout()
# Create labels
label_elements = QtWidgets.QLabel(translate("draft", "Number of elements"))
label_interval = QtWidgets.QLabel(translate("draft", "Interval"))
# Get widgets for active axis
widgets = self._get_axis_widgets(self.active_axis)
# Add widgets to layout
widget_pairs = [
(label_elements, widgets['spinbox_elements']),
(label_interval, widgets['spinbox_interval'])
]
for row_index, (label, widget) in enumerate(widget_pairs):
label.setParent(self.form.group_linearmode)
widget.setParent(self.form.group_linearmode)
group_layout.addWidget(label, row_index, 0)
group_layout.addWidget(widget, row_index, 1)
# Add reset button spanning both columns
widgets['button_reset'].setParent(self.form.group_linearmode)
group_layout.addWidget(widgets['button_reset'], 2, 0, 1, 2)
def _restore_axis_to_ortho_layout(self, axis, row_index):
"""Restore widgets for a specific axis back to their original Ortho layout positions."""
widgets = self._get_axis_widgets(axis)
# Restore spinbox elements to grid layout
grid_number_layout = self.form.findChild(QtWidgets.QGridLayout, "grid_number")
grid_number_layout.addWidget(widgets['spinbox_elements'], row_index, 1)
# Restore interval input to its axis-specific inner grid layout
inner_grid_layout = self.form.findChild(QtWidgets.QGridLayout, f"grid_{axis}")
inner_grid_layout.addWidget(widgets['spinbox_interval'], row_index, 1)
# Restore reset button to its axis group
group_box = self.form.findChild(QtWidgets.QGroupBox, f"group_{axis}")
group_axis_layout = group_box.layout()
group_axis_layout.addWidget(widgets['button_reset'], 1, 0)
def _setup_ortho_mode_layout(self):
"""Restore all widgets back to their original Ortho mode layout positions."""
for row_index, axis in enumerate(["X", "Y", "Z"]):
self._restore_axis_to_ortho_layout(axis, row_index)
def _reparent_groups(self, mode="Linear"):
"""Reparent widgets between Linear and Ortho mode layouts."""
if mode == "Linear":
self._setup_linear_mode_layout()
else:
self._setup_ortho_mode_layout()
def _set_orthomode_groups_visibility(self, hide=True):
"""Change visibility of ortho mode groups"""
for axis in ["X", "Y", "Z"]:
group_name = f"group_{axis}"
group_box = self.form.findChild(QtWidgets.QGroupBox, group_name)
if hide:
group_box.hide()
self.form.group_copies.hide()
else:
group_box.show()
self.form.group_copies.show()
def finish(self):
"""Finish the command, after accept or reject.

View File

@@ -506,6 +506,18 @@ def _get_param_dictionary():
"Wall": ("bool", False),
}
start_val = App.Units.Quantity(100.0, App.Units.Length).Value
param_dict["Mod/Draft/OrthoArrayLinearMode"] = {
"LinearModeOn": ("bool", True),
"AxisSelected": ("string", "X"),
"XInterval": ("float", start_val),
"YInterval": ("float", start_val),
"ZInterval": ("float", start_val),
"XNumOfElements": ("int", 2),
"YNumOfElements": ("int", 2),
"ZNumOfElements": ("int", 2)
}
# Arch parameters that are not in the preferences:
param_dict["Mod/Arch"] = {
"applyConstructionStyle": ("bool", True),