✨ feat(data_processing.py): remove deprecated get_discrete_vertices2 method and rename get_discrete_vertices3 to get_discrete_vertices. Simplify get_discrete_vertices method by removing the selection of the discretisation method from the settings and always using the Z0-Zi < DeltaZ method. This improves code readability and maintainability. ✨ feat(input.py): remove deprecated result_file_path and bruteforce_discretization_result attributes from ScannedObject class. Add old_delta, old_discrete, and old_discrete_type attributes to cache the results of the get_discrete_vertices method. This improves performance by avoiding unnecessary recomputations of the discretized vertices. ✨ feat(MainWindow.py): add support for selecting a layer to display discrete graphs for. Add two new graph types: "Coupe de la couche" and "Difference entre le rayon de chaque points 🔧 fix(ui): change tab index to display the correct tab on startup ✨ feat(ui): add label and combobox to select layer to display 🔧 fix(worker): add discretisation_value parameter to PreProcessWorker constructor ✨ feat(math): add get_true_teta_from_x_y, get_difference_from_mean_value, and get_distance_between_two_vertices functions The UI fix changes the tab index to display the correct tab on startup. The new label and combobox allow the user to select the layer to display. The worker fix adds a discretisation_value parameter to the PreProcessWorker constructor. The new math functions are get_true_teta_from_x_y, get_difference_from_mean_value, and get_distance_between_two_vertices. These functions are useful for calculating teta, differences from mean values, and distances between vertices.
470 lines
20 KiB
Python
470 lines
20 KiB
Python
"""
|
|
Created on Mon Apr 24 2023
|
|
@name: MainWindow.py
|
|
@desc: Main window of the application
|
|
@auth: Djalim Simaila
|
|
@e-mail: djalim.simaila@inrae.fr
|
|
"""
|
|
import os
|
|
from PyQt5 import QtWidgets
|
|
from PyQt5.QtCore import QThread
|
|
from PyQt5.QtWidgets import QFileDialog, QWidget
|
|
from utils.files.input import ScannedObject
|
|
from utils.gui.pyqt.settings.Settings import Settings
|
|
from utils.gui.pyqt.about.AboutThis import AboutThis
|
|
from utils.math.data_extraction import get_radius_from_x_y, get_true_teta_from_x_y
|
|
from utils.settings.SettingManager import SettingManager
|
|
from utils.graph2D.visplot_render import cross_section, render2D
|
|
from utils.graph3D.visplot_render import render3D
|
|
from utils.gui.pyqt.main_window.UI_MainWindow import Ui_MainWindow
|
|
from utils.gui.pyqt.main_window.Workers.DiscreteDataWorker import DiscreteDataProcessWorker
|
|
from utils.gui.pyqt.main_window.Workers.PreProcessWorker import PreProcessWorker
|
|
from utils.gui.pyqt.main_window.Workers.RawDataWorker import RawDataProcessWorker
|
|
from utils.gui.pyqt.error_popup.ErrorPopup import ErrorPopup
|
|
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|
"""
|
|
Main window of the application, it contains all the UI elements
|
|
|
|
"""
|
|
|
|
def __init__(self, parent=None):
|
|
super(MainWindow, self).__init__(parent)
|
|
# Retrieve the UI
|
|
self.setupUi(self)
|
|
# Setup buttons listeners
|
|
self.start_analyse_button.clicked.connect(self.start_preprocess)
|
|
self.input_file_choose_btn.clicked.connect(self.select_file)
|
|
self.output_folder_choose_btn.clicked.connect(self.select_folder)
|
|
self.show_graph_checkbox.stateChanged.connect(self.toggle_graphs)
|
|
self.actionSauvegarder_le_model_redress.triggered.connect(self.save_model)
|
|
self.actionPr_f_rennces.triggered.connect(self.show_settings)
|
|
self.actionQuitter.triggered.connect(self.close)
|
|
self.actionQ_propos_de_ce_logiciel.triggered.connect(self.show_about)
|
|
|
|
self.layer_ComboBox.addItems(['Aucune couche'])
|
|
self.layer_ComboBox.currentIndexChanged.connect(self.layer_changed)
|
|
|
|
self.graphType = [
|
|
"Aucun",
|
|
"Mesh3D",
|
|
"Coupe XZ",
|
|
"Coupe YZ",
|
|
"Evolution du rayon moyen",
|
|
"Coupe de la couche",
|
|
"Difference entre le rayon de chaque points et le rayon moyen de la couche"
|
|
]
|
|
|
|
self.obj = None
|
|
self.raw_data= None
|
|
self.discrete_data = None
|
|
|
|
self.completed_tasks = 0
|
|
self.total_tasks = 2
|
|
|
|
self.combo_boxes = [
|
|
self.slot0ComboBox,
|
|
self.slot1ComboBox,
|
|
self.slot2ComboBox,
|
|
self.slot3ComboBox,
|
|
self.slot4ComboBox,
|
|
self.slot5ComboBox,
|
|
self.slot6ComboBox,
|
|
self.slot7ComboBox,
|
|
self.slot8ComboBox,
|
|
self.slot9ComboBox,
|
|
self.slot10ComboBox
|
|
]
|
|
|
|
for combo_box in self.combo_boxes:
|
|
combo_box.addItems(self.graphType)
|
|
combo_box.currentIndexChanged.connect(self.graph_type_changed)
|
|
|
|
self.slots = [
|
|
[self.slot0,"Aucun"],
|
|
[self.slot1,"Aucun"],
|
|
[self.slot2,"Aucun"],
|
|
[self.slot3,"Aucun"],
|
|
[self.slot4,"Aucun"],
|
|
[self.slot5,"Aucun"],
|
|
[self.slot6,"Aucun"],
|
|
[self.slot7,"Aucun"],
|
|
[self.slot8,"Aucun"],
|
|
[self.slot9,"Aucun"],
|
|
[self.slot10,"Aucun"]
|
|
]
|
|
|
|
for slot_nb,slot in enumerate(self.slots):
|
|
slot[1] = SettingManager.get_instance().get_last_graph(slot_nb)
|
|
self.combo_boxes[slot_nb].setCurrentText(slot[1])
|
|
|
|
self.graph_nb =0
|
|
self.graph_type_changed()
|
|
|
|
self.settings_window = Settings()
|
|
self.about_window = AboutThis()
|
|
|
|
self.has_changed = True
|
|
self.old_discretisation_value = None
|
|
|
|
###############################################################################
|
|
# #
|
|
# #
|
|
# Input/Setting Management #
|
|
# #
|
|
# #
|
|
###############################################################################
|
|
def select_file(self):
|
|
"""
|
|
Open a file dialog to select the input file
|
|
"""
|
|
file = QFileDialog.getOpenFileName()[0]
|
|
self.input_file_path.setPlainText(file)
|
|
self.output_file_prefix.setText(os.path.splitext(os.path.basename(file))[0])
|
|
if self.output_folder_path.toPlainText() is None or self.output_folder_path.toPlainText() == "":
|
|
self.output_folder_path.setPlainText(os.path.dirname(file))
|
|
self.has_changed = True
|
|
|
|
def select_folder(self):
|
|
"""
|
|
Open a file dialog to select the output folder
|
|
"""
|
|
self.output_folder_path.setPlainText(QFileDialog.getExistingDirectory())
|
|
self.has_changed = True
|
|
|
|
def check_input_file(self):
|
|
"""
|
|
Check if the input file is valid
|
|
"""
|
|
if not os.path.isfile(self.input_file_path.toPlainText()):
|
|
ErrorPopup("Fichier d'entrée invalide",button_label="Choisir un fichier d'entrée",button_callback=self.select_file).show_popup()
|
|
return False
|
|
return True
|
|
|
|
def check_output_folder(self):
|
|
"""
|
|
Check if the output folder is valid
|
|
"""
|
|
if not os.path.isdir(self.output_folder_path.toPlainText()):
|
|
ErrorPopup("Dossier de sortie invalide",button_label="Choisir un dossier de sortie",button_callback=self.select_folder).show_popup()
|
|
return False
|
|
return True
|
|
|
|
###############################################################################
|
|
# #
|
|
# #
|
|
# Data Processing #
|
|
# #
|
|
# #
|
|
###############################################################################
|
|
|
|
def start_preprocess(self):
|
|
"""
|
|
Start the analyse, create the thread and connect the signals.
|
|
"""
|
|
if not self.check_input_file():
|
|
return
|
|
if not self.check_output_folder():
|
|
return
|
|
|
|
self.clear_graphs()
|
|
self.completed_tasks = 0
|
|
if not self.has_changed and self.old_discretisation_value == self.discretisation_value_selector.value():
|
|
self.completed_tasks = self.total_tasks -1
|
|
self.finish_analyse()
|
|
return
|
|
self.has_changed = False
|
|
self.old_discretisation_value = self.discretisation_value_selector.value()
|
|
|
|
# Create the thread to run the analyse
|
|
self.preprocess_thread = QThread()
|
|
self.preprocess_worker = PreProcessWorker("PreProcessWorker",
|
|
self.input_file_path.toPlainText(),
|
|
self.discretisation_value_selector.value())
|
|
self.preprocess_worker.moveToThread(self.preprocess_thread)
|
|
|
|
# Connect the signals
|
|
# Start
|
|
self.preprocess_thread.started.connect(self.preprocess_worker.run)
|
|
# Progress
|
|
self.preprocess_worker.status.connect(self.set_status)
|
|
self.preprocess_worker.progress.connect(self.update_progress_bar)
|
|
self.preprocess_worker.processed_obj.connect(self.set_obj)
|
|
self.preprocess_worker.processed_obj.connect(self.process_raw_data)
|
|
self.preprocess_worker.processed_obj.connect(self.process_discrete_data)
|
|
# Finished
|
|
self.preprocess_worker.finished.connect(self.preprocess_thread.quit)
|
|
self.preprocess_worker.finished.connect(self.preprocess_worker.deleteLater)
|
|
self.preprocess_thread.finished.connect(self.preprocess_thread.deleteLater)
|
|
|
|
# Start the thread
|
|
self.preprocess_thread.start()
|
|
self.start_analyse_button.setEnabled(False)
|
|
|
|
def process_raw_data(self, obj:ScannedObject):
|
|
"""
|
|
Start the analyse, create the thread and connect the signals.
|
|
"""
|
|
self.processrawdata_thread = QThread()
|
|
self.processraw_worker = RawDataProcessWorker("RawDataProcessWorker",
|
|
obj,
|
|
self.output_folder_path.toPlainText(),
|
|
self.output_file_prefix.text(),
|
|
self.discretisation_value_selector.value())
|
|
self.processraw_worker.moveToThread(self.processrawdata_thread)
|
|
# Connect the signals
|
|
# Start
|
|
self.processrawdata_thread.started.connect(self.processraw_worker.run)
|
|
# Progress
|
|
self.processraw_worker.status.connect(self.set_status)
|
|
self.processraw_worker.progress.connect(self.update_progress_bar)
|
|
self.processraw_worker.processedData.connect(self.set_raw_data)
|
|
# Finished
|
|
self.processraw_worker.finished.connect(self.finish_analyse)
|
|
self.processraw_worker.finished.connect(self.processrawdata_thread.quit)
|
|
self.processraw_worker.finished.connect(self.processraw_worker.deleteLater)
|
|
self.processrawdata_thread.finished.connect(self.processrawdata_thread.deleteLater)
|
|
|
|
# Start the thread
|
|
self.processrawdata_thread.start()
|
|
|
|
def process_discrete_data(self, obj:ScannedObject):
|
|
"""
|
|
Start the analyse, create the thread and connect the signals.
|
|
"""
|
|
self.processdiscrete_thread = QThread()
|
|
self.processdiscrete_worker = DiscreteDataProcessWorker("DiscreteDataProcessWorker",
|
|
obj,
|
|
self.output_folder_path.toPlainText(),
|
|
self.output_file_prefix.text(),
|
|
self.discretisation_value_selector.value())
|
|
self.processdiscrete_worker.moveToThread(self.processdiscrete_thread)
|
|
# Connect the signals
|
|
# Start
|
|
self.processdiscrete_thread.started.connect(self.processdiscrete_worker.run)
|
|
# Progress
|
|
self.processdiscrete_worker.status.connect(self.set_status)
|
|
self.processdiscrete_worker.progress.connect(self.update_progress_bar)
|
|
self.processdiscrete_worker.processedData.connect(self.set_discrete_data)
|
|
# Finished
|
|
self.processdiscrete_worker.finished.connect(self.finish_analyse)
|
|
self.processdiscrete_worker.finished.connect(self.processdiscrete_thread.quit)
|
|
self.processdiscrete_worker.finished.connect(self.processdiscrete_worker.deleteLater)
|
|
self.processdiscrete_thread.finished.connect(self.processdiscrete_thread.deleteLater)
|
|
|
|
# Start the thread
|
|
self.processdiscrete_thread.start()
|
|
|
|
def set_obj(self,obj:ScannedObject):
|
|
"""
|
|
Set the obj to the main window
|
|
"""
|
|
self.obj = obj
|
|
|
|
def set_discrete_data(self,discrete_data:dict):
|
|
"""
|
|
Set the discrete data to the main window
|
|
"""
|
|
self.discrete_data = discrete_data
|
|
layer = [str(i) for i in range(len(discrete_data["X moy (en mm)"]))]
|
|
layer.insert(0,"Aucune couche")
|
|
self.layer_ComboBox.clear()
|
|
self.layer_ComboBox.addItems(layer)
|
|
|
|
|
|
|
|
def set_raw_data(self,raw_data:dict):
|
|
"""
|
|
Set the raw data to the main window
|
|
"""
|
|
self.raw_data = raw_data
|
|
|
|
def save_model(self):
|
|
"""
|
|
Save the model to a file
|
|
"""
|
|
if self.obj is None:
|
|
ErrorPopup("Aucune analyse effectuée. Aucun modèle à sauvegarder").show_popup()
|
|
return
|
|
file_path = QFileDialog.getSaveFileName(self,
|
|
"Sauvegarder le modèle",
|
|
"./",
|
|
"Fichier OBJ (*.obj)")
|
|
self.obj.export_obj(file_path[0])
|
|
|
|
###############################################################################
|
|
# #
|
|
# #
|
|
# Graphs management #
|
|
# #
|
|
# #
|
|
###############################################################################
|
|
|
|
def toggle_graphs(self):
|
|
"""
|
|
Show or hide the graphs
|
|
"""
|
|
if self.show_graph_checkbox.isChecked():
|
|
self.Graphs.show()
|
|
else:
|
|
self.Graphs.hide()
|
|
|
|
def renderGraphs(self,obj:ScannedObject,raw_data:dict,discrete_data:dict):
|
|
"""
|
|
Render the graphs
|
|
|
|
:param obj: The scanned object
|
|
:param raw_data: The raw data
|
|
:param discrete_data: The discrete data
|
|
"""
|
|
if not self.show_graph_checkbox.isChecked():
|
|
return
|
|
self.set_status("Rendering graphs... this may take a moment")
|
|
for slot in self.slots:
|
|
current_slot = slot[0]
|
|
graph_type = slot[1]
|
|
if graph_type == "Mesh3D":
|
|
current_slot.addWidget(render3D(obj,False).native)
|
|
if graph_type == "Coupe XZ":
|
|
current_slot.addWidget(cross_section(obj.get_x(),
|
|
obj.get_z(),
|
|
"Coupe XZ",
|
|
"X (en mm)",
|
|
"Z (en mm)",
|
|
False).native)
|
|
if graph_type == "Coupe YZ":
|
|
current_slot.addWidget(cross_section(obj.get_y(),
|
|
obj.get_z(),
|
|
"Coupe YZ",
|
|
"Y (en mm)",
|
|
"Z (en mm)",
|
|
False).native)
|
|
if graph_type == "Evolution du rayon moyen":
|
|
current_slot.addWidget(render2D(list(zip(discrete_data['Z moy (en mm)'],discrete_data['Rayon moyen (en mm)'])),
|
|
"Evolution du rayon moyen en fonction de Z",
|
|
"Z (en mm)",
|
|
"Rayon moyen (en mm)\n",
|
|
False).native)
|
|
self.set_status("Graphs rendered!")
|
|
|
|
def renderDiscreteGraphs(self,obj:ScannedObject,raw_data:dict,discrete_data:dict):
|
|
"""
|
|
"""
|
|
self.set_status("Renderingc discretes graphs... this may take a moment")
|
|
if self.layer_ComboBox.currentText() == 'Aucune couche':
|
|
return
|
|
layer_nb = int(self.layer_ComboBox.currentText())
|
|
discretisation_value = self.discretisation_value_selector.value()
|
|
for slot in self.slots:
|
|
current_slot = slot[0]
|
|
graph_type = slot[1]
|
|
if graph_type == "Coupe de la couche":
|
|
self.clear_slot(current_slot)
|
|
vertices = obj.get_discrete_vertices(discretisation_value)[layer_nb]
|
|
current_slot.addWidget(cross_section([x for x,y,z in vertices],
|
|
[y for x,y,z in vertices],
|
|
"Coupe de la couche",
|
|
"X en mm",
|
|
"Y en mm",
|
|
False).native)
|
|
if graph_type == "Difference entre le rayon de chaque points et le rayon moyen de la couche":
|
|
self.clear_slot(current_slot)
|
|
vertices = obj.get_discrete_vertices(discretisation_value)[layer_nb]
|
|
x_mean = discrete_data["X moy (en mm)"][layer_nb]
|
|
y_mean = discrete_data["Y moy (en mm)"][layer_nb]
|
|
r_mean = discrete_data["Rayon moyen (en mm)"][layer_nb]
|
|
rs = [get_radius_from_x_y(x,y,x_mean,y_mean) for x,y,z in vertices]
|
|
θs = [get_true_teta_from_x_y(x,y,x_mean,y_mean) for x,y,z in vertices]
|
|
min_θ = min(θs)
|
|
current_slot.addWidget(cross_section([θ-min_θ for θ in θs],
|
|
[r-r_mean for r in rs],
|
|
"Difference entre le rayon de chaque points et le rayon moyen de la couche",
|
|
"Theta en rad",
|
|
"R - <R>",
|
|
False).native)
|
|
self.set_status("Graphs rendered!")
|
|
|
|
|
|
def clear_slot(self,slot):
|
|
for i in reversed(range(slot.count())):
|
|
slot.itemAt(i).widget().setParent(None)
|
|
|
|
def clear_graphs(self):
|
|
"""
|
|
Clear the graphs
|
|
"""
|
|
if not self.show_graph_checkbox.isChecked():
|
|
return
|
|
for slot,_ in self.slots:
|
|
self.clear_slot(slot)
|
|
|
|
###############################################################################
|
|
# #
|
|
# #
|
|
# User interface updates #
|
|
# #
|
|
# #
|
|
###############################################################################
|
|
|
|
def finish_analyse(self):
|
|
"""
|
|
Finish the analyse
|
|
"""
|
|
self.completed_tasks += 1
|
|
if self.completed_tasks == self.total_tasks:
|
|
self.status_text.setText("Done")
|
|
self.analyse_progress_bar.setValue(100)
|
|
self.renderGraphs(self.obj,self.raw_data,self.discrete_data)
|
|
self.renderDiscreteGraphs(self.obj,self.raw_data,self.discrete_data)
|
|
self.start_analyse_button.setEnabled(True)
|
|
|
|
def update_progress_bar(self, value):
|
|
"""
|
|
Update the progress bar
|
|
"""
|
|
self.analyse_progress_bar.setValue(value)
|
|
|
|
def set_status(self, status:str):
|
|
"""
|
|
Set the status of the analyse
|
|
"""
|
|
self.status_text.setText(status)
|
|
|
|
def show_settings(self):
|
|
"""
|
|
Show the settings window
|
|
"""
|
|
self.settings_window.show()
|
|
|
|
def show_about(self):
|
|
"""
|
|
Show the about window
|
|
"""
|
|
self.about_window.show()
|
|
|
|
def graph_type_changed(self):
|
|
"""
|
|
Update the number of graphs
|
|
"""
|
|
self.graph_nb = 0
|
|
for combo_box in self.combo_boxes:
|
|
if combo_box.currentIndex() != 0:
|
|
self.graph_nb += 1
|
|
self.graph_nb_spinbox.setValue(self.graph_nb)
|
|
|
|
settings = SettingManager.get_instance()
|
|
for count,_ in enumerate(self.slots):
|
|
self.slots[count][1] = self.combo_boxes[count].currentText()
|
|
settings.set_last_graph(count,self.slots[count][1])
|
|
|
|
if self.obj is None or self.discrete_data is None or self.raw_data is None:
|
|
return
|
|
else:
|
|
self.clear_graphs()
|
|
self.renderGraphs(self.obj,self.raw_data,self.discrete_data)
|
|
self.renderDiscreteGraphs(self.obj,self.raw_data,self.discrete_data)
|
|
|
|
def layer_changed(self):
|
|
if self.obj is None or self.discrete_data is None or self.raw_data is None:
|
|
return
|
|
else:
|
|
self.renderDiscreteGraphs(self.obj,self.raw_data,self.discrete_data) |