def createPositionerForm(self): """Creates the form inputs for the detector positioner""" title = FormTitle('Detector Position') self.main_layout.addWidget(title) self.position_form_group = FormGroup(FormGroup.Layout.Grid) for index, link in enumerate(self.detector.positioner.links): if link.type == Link.Type.Revolute: unit = 'degrees' offset = math.degrees(link.set_point) lower_limit = math.degrees(link.lower_limit) upper_limit = math.degrees(link.upper_limit) else: unit = 'mm' offset = link.set_point lower_limit = link.lower_limit upper_limit = link.upper_limit pretty_label = link.name.replace('_', ' ').title() control = FormControl(pretty_label, offset, desc=unit, required=True, number=True) control.form_lineedit.setDisabled(link.locked) if not link.ignore_limits: control.range(lower_limit, upper_limit) limits_button = create_tool_button( tooltip='Disable Joint Limits', style_name='MidToolButton', icon_path=path_for('limit.png'), checkable=True, status_tip=f'Ignore joint limits of {pretty_label}', checked=link.ignore_limits) limits_button.clicked.connect(self.ignoreJointLimits) limits_button.setProperty('link_index', index) control.extra = [limits_button] self.position_form_group.addControl(control) self.position_form_group.group_validation.connect( self.formValidation) self.main_layout.addWidget(self.position_form_group) button_layout = QtWidgets.QHBoxLayout() self.move_detector_button = QtWidgets.QPushButton('Move Detector') self.move_detector_button.clicked.connect( self.moveDetectorsButtonClicked) button_layout.addWidget(self.move_detector_button) button_layout.addStretch(1) self.main_layout.addLayout(button_layout)
def createApertureForm(self): """Creates the form inputs for changing the aperture size""" title = FormTitle(f'{self.instrument.jaws.name} Aperture Size') self.main_layout.addWidget(title) aperture = self.instrument.jaws.aperture upper_limit = self.instrument.jaws.aperture_upper_limit lower_limit = self.instrument.jaws.aperture_lower_limit self.aperture_form_group = FormGroup(FormGroup.Layout.Grid) control = FormControl('Horizontal Aperture Size', aperture[0], desc='mm', required=True, number=True) control.range(lower_limit[0], upper_limit[0]) self.aperture_form_group.addControl(control) control = FormControl('Vertical Aperture Size', aperture[1], desc='mm', required=True, number=True) control.range(lower_limit[1], upper_limit[1]) self.aperture_form_group.addControl(control) self.main_layout.addWidget(self.aperture_form_group) self.aperture_form_group.group_validation.connect(self.formValidation) self.main_layout.addSpacing(10) button_layout = QtWidgets.QHBoxLayout() self.change_aperture_button = QtWidgets.QPushButton( 'Change Aperture Size') self.change_aperture_button.clicked.connect( self.changeApertureButtonClicked) button_layout.addWidget(self.change_aperture_button) button_layout.addStretch(1) self.main_layout.addLayout(button_layout)
def createPositionerForm(self): """Creates the form inputs for the jaw positioner""" title = FormTitle(f'{self.instrument.jaws.name} Position') self.main_layout.addWidget(title) self.position_form_group = FormGroup(FormGroup.Layout.Grid) for index in self.instrument.jaws.positioner.order: link = self.instrument.jaws.positioner.links[index] if link.type == Link.Type.Revolute: unit = 'degrees' offset = math.degrees(link.set_point) lower_limit = math.degrees(link.lower_limit) upper_limit = math.degrees(link.upper_limit) else: unit = 'mm' offset = link.set_point lower_limit = link.lower_limit upper_limit = link.upper_limit control = FormControl(link.name.title(), offset, desc=unit, required=True, number=True) control.form_lineedit.setDisabled(link.locked) if not link.ignore_limits: control.range(lower_limit, upper_limit) limits_button = create_tool_button( tooltip='Disable Joint Limits', style_name='MidToolButton', icon_path=path_for('limit.png'), checkable=True, status_tip=f'Ignore joint limits of {link.name}', checked=link.ignore_limits) limits_button.clicked.connect(self.ignoreJointLimits) limits_button.setProperty('link_index', index) control.extra = [limits_button] self.position_form_group.addControl(control) self.position_form_group.group_validation.connect( self.formValidation) self.main_layout.addWidget(self.position_form_group) button_layout = QtWidgets.QHBoxLayout() self.move_jaws_button = QtWidgets.QPushButton('Move Jaws') self.move_jaws_button.clicked.connect(self.moveJawsButtonClicked) button_layout.addWidget(self.move_jaws_button) button_layout.addStretch(1) self.main_layout.addLayout(button_layout)
def __init__(self, sample, parent): super().__init__() self.parent = parent self.main_layout = QtWidgets.QVBoxLayout() self.main_layout.setContentsMargins(0, 0, 0, 0) unit = 'mm' self.form_group = FormGroup() self.x_position = FormControl('X', 0.0, required=True, desc=unit, number=True) self.y_position = FormControl('Y', 0.0, required=True, desc=unit, number=True) self.z_position = FormControl('Z', 0.0, required=True, desc=unit, number=True) self.form_group.addControl(self.x_position) self.form_group.addControl(self.y_position) self.form_group.addControl(self.z_position) self.form_group.group_validation.connect(self.formValidation) button_layout = QtWidgets.QHBoxLayout() self.execute_button = QtWidgets.QPushButton( TransformType.Translate.value) self.execute_button.clicked.connect(self.executeButtonClicked) button_layout.addWidget(self.execute_button) button_layout.addStretch(1) self.main_layout.addWidget(self.form_group) self.main_layout.addLayout(button_layout) self.main_layout.addStretch(1) self.setLayout(self.main_layout) self.valid_sample = False self.selected_sample = sample
def createCustomPlaneBox(self): self.custom_plane_widget = QtWidgets.QWidget(self) layout = QtWidgets.QVBoxLayout() self.form_group = FormGroup(FormGroup.Layout.Horizontal) self.x_axis = FormControl('X', 1.0, required=True, number=True) self.x_axis.range(-1.0, 1.0) self.y_axis = FormControl('Y', 0.0, required=True, number=True) self.y_axis.range(-1.0, 1.0) self.z_axis = FormControl('Z', 0.0, required=True, number=True) self.z_axis.range(-1.0, 1.0) self.form_group.addControl(self.x_axis) self.form_group.addControl(self.y_axis) self.form_group.addControl(self.z_axis) self.form_group.group_validation.connect(self.setCustomPlane) layout.addWidget(self.form_group) self.custom_plane_widget.setLayout(layout) self.custom_plane_widget.setVisible(False) self.main_layout.addWidget(self.custom_plane_widget)
def createPositionerWidget(self, positioner, add_base_button=False): """Creates the form widget for the given positioner :param positioner: serial manipulator :type positioner: SerialManipulator :param add_base_button: indicates base button should be shown :type add_base_button: bool :return: positioner widget :rtype: QtWidgets.QWidget """ widget = QtWidgets.QWidget() layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) title = FormTitle(positioner.name) layout.addWidget(title) if add_base_button: base_button = create_tool_button(tooltip='Change Base Matrix', style_name='MidToolButton') import_base_action = QtWidgets.QAction('Import Base Matrix', self) import_base_action.setStatusTip( f'Import base transformation matrix for {positioner.name}') import_base_action.triggered.connect( lambda ignore, n=positioner.name: self.importPositionerBase(n)) compute_base_action = QtWidgets.QAction('Calculate Base Matrix', self) compute_base_action.setStatusTip( f'Calculate base transformation matrix for {positioner.name}') compute_base_action.triggered.connect( lambda ignore, n=positioner.name: self.computePositionerBase(n )) export_base_action = QtWidgets.QAction('Export Base Matrix', self) export_base_action.setStatusTip( f'Export base transformation matrix for {positioner.name}') export_base_action.triggered.connect( lambda ignore, n=positioner.name: self.exportPositionerBase(n)) base_button.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) base_button.addActions( [import_base_action, compute_base_action, export_base_action]) base_button.setDefaultAction(import_base_action) base_button.setIcon(QtGui.QIcon(path_for('base.png'))) title.addHeaderControl(base_button) reset_button = create_tool_button( tooltip='Reset Base Matrix', style_name='MidToolButton', status_tip= f'Reset base transformation matrix of {positioner.name}', icon_path=path_for('refresh.png'), hide=True) reset_button.clicked.connect( lambda ignore, n=positioner.name: self.resetPositionerBase(n)) title.addHeaderControl(reset_button) reset_button.setVisible( positioner.base is not positioner.default_base) self.base_reset_buttons[positioner.name] = reset_button form_group = FormGroup(FormGroup.Layout.Grid) order_offset = len(self.positioner_form_controls) for index in positioner.order: link = positioner.links[index] if link.type == Link.Type.Revolute: unit = 'degrees' offset = math.degrees(link.set_point) lower_limit = math.degrees(link.lower_limit) upper_limit = math.degrees(link.upper_limit) else: unit = 'mm' offset = link.set_point lower_limit = link.lower_limit upper_limit = link.upper_limit control = FormControl(link.name.title(), offset, desc=unit, required=True, number=True) control.form_lineedit.setDisabled(link.locked) if not link.ignore_limits: control.range(lower_limit, upper_limit) lock_button = create_tool_button( tooltip='Lock Joint', style_name='MidToolButton', status_tip=f'Lock {link.name} joint in {positioner.name}', icon_path=path_for('lock.png'), checkable=True, checked=link.locked) lock_button.clicked.connect(self.lockJoint) lock_button.setProperty('link_index', index + order_offset) limits_button = create_tool_button( tooltip='Disable Joint Limits', style_name='MidToolButton', status_tip=f'Ignore joint limits of {link.name}', icon_path=path_for('limit.png'), checkable=True, checked=link.ignore_limits) limits_button.clicked.connect(self.ignoreJointLimits) limits_button.setProperty('link_index', index + order_offset) control.extra = [lock_button, limits_button] form_group.addControl(control) form_group.group_validation.connect(self.formValidation) self.positioner_form_controls.append(control) layout.addWidget(form_group) widget.setLayout(layout) self.positioner_forms.append(form_group) return widget
class PlaneAlignmentTool(QtWidgets.QWidget): """Creates a UI to rotate a selected plane on the sample so that it is aligned with an arbitrary plane or a plane formed by any 2 axes of the coordinate system. The plane on the sample is specified by picking 3 or more points on the surface of the sample. :param sample: name of sample :type sample: str :param parent: main window instance :type parent: MainWindow """ def __init__(self, sample, parent): super().__init__() self.parent = parent self.main_layout = QtWidgets.QVBoxLayout() self.main_layout.setContentsMargins(0, 0, 0, 0) self.vertices = None self.initial_plane = None self.final_plane_normal = None layout = QtWidgets.QHBoxLayout() self.table_widget = QtWidgets.QTableWidget() self.table_widget.setShowGrid(False) self.table_widget.setAlternatingRowColors(True) self.table_widget.setEditTriggers( QtWidgets.QAbstractItemView.NoEditTriggers) self.table_widget.setColumnCount(3) self.table_widget.setFixedHeight(150) self.table_widget.verticalHeader().setVisible(False) self.table_widget.setHorizontalHeaderLabels(['X', 'Y', 'Z']) self.table_widget.setSelectionBehavior( QtWidgets.QAbstractItemView.SelectRows) self.table_widget.setSelectionMode( QtWidgets.QAbstractItemView.ExtendedSelection) self.table_widget.selectionModel().selectionChanged.connect( self.selection) self.table_widget.horizontalHeader().setSectionResizeMode( QtWidgets.QHeaderView.Stretch) layout.addWidget(self.table_widget) button_layout = QtWidgets.QVBoxLayout() self.select_button = create_tool_button( icon_path=path_for('select.png'), checkable=True, checked=True, status_tip='Normal scene manipulation with the mouse', tooltip='Normal Mode', style_name='ToolButton') self.select_button.clicked.connect(lambda: self.togglePicking(False)) button_layout.addWidget(self.select_button) self.pick_button = create_tool_button( icon_path=path_for('point.png'), checkable=True, status_tip='Select 3D points that define the plane', tooltip='Pick Point Mode', style_name='ToolButton') self.pick_button.clicked.connect(lambda: self.togglePicking(True)) button_layout.addWidget(self.pick_button) self.delete_button = create_tool_button( icon_path=path_for('cross.png'), style_name='ToolButton', status_tip='Remove selected points from the scene', tooltip='Delete Points') self.delete_button.clicked.connect(self.removePicks) button_layout.addWidget(self.delete_button) layout.addSpacing(10) layout.addLayout(button_layout) self.main_layout.addLayout(layout) self.main_layout.addSpacing(10) self.main_layout.addWidget(QtWidgets.QLabel('Select Final Plane:')) self.plane_combobox = QtWidgets.QComboBox() self.plane_combobox.setView(QtWidgets.QListView()) self.plane_combobox.addItems([p.value for p in PlaneOptions]) self.plane_combobox.currentTextChanged.connect(self.setPlane) self.main_layout.addWidget(self.plane_combobox) self.createCustomPlaneBox() self.setPlane(self.plane_combobox.currentText()) button_layout = QtWidgets.QHBoxLayout() self.execute_button = QtWidgets.QPushButton('Align Planes') self.execute_button.clicked.connect(self.executeButtonClicked) button_layout.addWidget(self.execute_button) button_layout.addStretch(1) self.main_layout.addLayout(button_layout) self.main_layout.addStretch(1) self.setLayout(self.main_layout) self.parent.gl_widget.pick_added.connect(self.addPicks) self.selected_sample = sample def togglePicking(self, value): """Toggles between point picking and scene manipulation in the graphics widget :param value: indicates if picking is enabled :type value: bool """ self.parent.gl_widget.picking = value if value: self.select_button.setChecked(False) self.pick_button.setChecked(True) else: self.select_button.setChecked(True) self.pick_button.setChecked(False) def createCustomPlaneBox(self): self.custom_plane_widget = QtWidgets.QWidget(self) layout = QtWidgets.QVBoxLayout() self.form_group = FormGroup(FormGroup.Layout.Horizontal) self.x_axis = FormControl('X', 1.0, required=True, number=True) self.x_axis.range(-1.0, 1.0) self.y_axis = FormControl('Y', 0.0, required=True, number=True) self.y_axis.range(-1.0, 1.0) self.z_axis = FormControl('Z', 0.0, required=True, number=True) self.z_axis.range(-1.0, 1.0) self.form_group.addControl(self.x_axis) self.form_group.addControl(self.y_axis) self.form_group.addControl(self.z_axis) self.form_group.group_validation.connect(self.setCustomPlane) layout.addWidget(self.form_group) self.custom_plane_widget.setLayout(layout) self.custom_plane_widget.setVisible(False) self.main_layout.addWidget(self.custom_plane_widget) def setCustomPlane(self, is_valid): """Sets custom destination/final plane normal. Error is shown if normal is invalid :param is_valid: indicate if the custom plane normal is valid :type is_valid: bool """ if is_valid: normal = Vector3( [self.x_axis.value, self.y_axis.value, self.z_axis.value]) length = normal.length if length > 1e-5: self.final_plane_normal = normal / length self.x_axis.validation_label.setText('') return else: self.x_axis.validation_label.setText('Bad Normal') self.final_plane_normal = None def setPlane(self, selected_text): """Sets destination/final plane normal or shows inputs for custom normal :param selected_text: PlaneOptions :type selected_text: str """ if selected_text == PlaneOptions.Custom.value: self.custom_plane_widget.setVisible(True) return elif selected_text == PlaneOptions.XY.value: self.final_plane_normal = Vector3([0., 0., 1.]) elif selected_text == PlaneOptions.XZ.value: self.final_plane_normal = Vector3([0., 1., 0.]) else: self.final_plane_normal = Vector3([1., 0., 0.]) self.custom_plane_widget.setVisible(False) @property def selected_sample(self): """Gets and sets the selected sample key :returns: selected sample key :rtype: str """ return self._selected_sample @selected_sample.setter def selected_sample(self, value): self._selected_sample = value sample = self.parent.presenter.model.sample if not sample: self.vertices = None self.execute_button.setDisabled(True) self.clearPicks() return if value is None: mesh = None for s in sample.values(): if mesh is None: mesh = s.copy() else: mesh.append(s) else: mesh = self.parent.presenter.model.sample[value] self.vertices = mesh.vertices[mesh.indices].reshape(-1, 9) self.plane_size = mesh.bounding_box.radius self.sample_center = mesh.bounding_box.center self.execute_button.setEnabled(True) def selection(self): """Highlights selected picks in the graphics widget""" picks = self.parent.gl_widget.picks sm = self.table_widget.selectionModel() for i in range(self.table_widget.rowCount()): picks[i][1] = sm.isRowSelected(i, QtCore.QModelIndex()) self.parent.gl_widget.update() def addPicks(self, start, end): """Computes pick coordinates and adds pick to table and graphics widgets. Pick coordinates are computed as the first intersection between start and end coordinates :param start: start point of pick line segment :type start: Vector3 :param end: end point of pick line segment :type end: Vector3 """ points = point_selection(start, end, self.vertices) if points.size == 0: return point = points[0] last_index = self.table_widget.rowCount() self.table_widget.insertRow(last_index) for i in range(3): item = QtWidgets.QTableWidgetItem(f'{point[i]:.3f}') item.setTextAlignment(QtCore.Qt.AlignCenter) self.table_widget.setItem(last_index, i, item) self.parent.gl_widget.picks.append([list(point), False]) self.updateInitialPlane() def removePicks(self): """Removes picks selected in the table widget and updates the initial plane""" model_index = [ m.row() for m in self.table_widget.selectionModel().selectedRows() ] model_index.sort(reverse=True) self.table_widget.selectionModel().reset() for index in model_index: del self.parent.gl_widget.picks[index] self.table_widget.removeRow(index) self.updateInitialPlane() def updateInitialPlane(self): """Computes the initial plane when 3 or more points have been picked""" if len(self.parent.gl_widget.picks) > 2: points = list(zip(*self.parent.gl_widget.picks))[0] self.initial_plane = Plane.fromBestFit(points) d = self.initial_plane.normal.dot(self.initial_plane.point - self.sample_center) self.initial_plane.point = self.sample_center + self.initial_plane.normal * d self.parent.scenes.drawPlane(self.initial_plane, 2 * self.plane_size, 2 * self.plane_size) else: self.initial_plane = None self.parent.scenes.removePlane() def closeEvent(self, event): self.parent.gl_widget.picking = False self.clearPicks() event.accept() def executeButtonClicked(self): if self.final_plane_normal is None or self.initial_plane is None: return matrix = Matrix44.identity() matrix[0:3, 0:3] = rotation_btw_vectors(self.initial_plane.normal, self.final_plane_normal) if not is_close(matrix, Matrix44.identity()): self.parent.presenter.transformSample(matrix, self.selected_sample, TransformType.Custom) self.clearPicks() def clearPicks(self): """Clears picks from tool and graphics widget""" self.table_widget.clear() self.initial_plane = None self.parent.gl_widget.picks.clear() self.parent.scenes.removePlane()
class RotateTool(QtWidgets.QWidget): """Creates a UI for applying simple rotation around the 3 principal axes :param sample: name of sample :type sample: str :param parent: main window instance :type parent: MainWindow """ def __init__(self, sample, parent): super().__init__() self.parent = parent self.main_layout = QtWidgets.QVBoxLayout() self.main_layout.setContentsMargins(0, 0, 0, 0) unit = 'degrees' self.form_group = FormGroup() self.x_rotation = FormControl('X', 0.0, required=True, desc=unit, number=True) self.x_rotation.range(-360.0, 360.0) self.y_rotation = FormControl('Y', 0.0, required=True, desc=unit, number=True) self.y_rotation.range(-360.0, 360.0) self.z_rotation = FormControl('Z', 0.0, required=True, desc=unit, number=True) self.z_rotation.range(-360.0, 360.0) self.form_group.addControl(self.x_rotation) self.form_group.addControl(self.y_rotation) self.form_group.addControl(self.z_rotation) self.form_group.group_validation.connect(self.formValidation) button_layout = QtWidgets.QHBoxLayout() self.execute_button = QtWidgets.QPushButton(TransformType.Rotate.value) self.execute_button.clicked.connect(self.executeButtonClicked) button_layout.addWidget(self.execute_button) button_layout.addStretch(1) self.main_layout.addWidget(self.form_group) self.main_layout.addLayout(button_layout) self.main_layout.addStretch(1) self.setLayout(self.main_layout) self.valid_sample = False self.selected_sample = sample @property def selected_sample(self): """Gets and sets the selected sample key :returns: selected sample key :rtype: str """ return self._selected_sample @selected_sample.setter def selected_sample(self, value): self._selected_sample = value self.valid_sample = True if self.parent.presenter.model.sample else False self.form_group.validateGroup() def formValidation(self, is_valid): if is_valid and self.valid_sample: self.execute_button.setEnabled(True) else: self.execute_button.setDisabled(True) def executeButtonClicked(self): angles = [ self.z_rotation.value, self.y_rotation.value, self.x_rotation.value ] if not is_close(angles, [0.0, 0.0, 0.0]): self.parent.presenter.transformSample(angles, self.selected_sample, TransformType.Rotate)