mirror of
https://forge.apps.education.fr/blender-edutech/jumeaux-numeriques.git
synced 2024-01-27 06:56:18 +01:00
Configuration du plot statique
This commit is contained in:
parent
72dbc3f914
commit
5243d26bd4
@ -28,26 +28,26 @@ scene = bge.logic.getCurrentScene()
|
||||
|
||||
public_vars = {
|
||||
't' : [['System','time'], [], []],
|
||||
'bp_ext' : [['Bp cote rue','activated'], ['pin', 'd','i'], ['o','-', 'green', 1]],
|
||||
'bp_ext_r' : [['Bp cote rue','activated_real'], [], ['o','--', 'green', 1]],
|
||||
'bp_int' : [['Bp cote cour','activated'], ['pin', 'd','i'], ['o','-', 'darkgreen', 1]],
|
||||
'bp_int_r' : [['Bp cote cour','activated_real'], [], ['o','--', 'darkgreen', 1]],
|
||||
'fdc_o' : [['Microrupteur fdc ouvert','activated'], ['pin', 'd','i'], ['o','-', 'orange', 1]],
|
||||
'fdc_o_r' : [['Microrupteur fdc ouvert','activated_real'], [], ['o','--', 'orange', 1]],
|
||||
'fdc_f' : [['Microrupteur fdc ferme','activated'], ['pin', 'd','i'], ['o','-', 'darkorange', 1]],
|
||||
'fdc_f_r' : [['Microrupteur fdc ferme','activated_real'], [], ['o','--', 'darkorange', 1]],
|
||||
'mot_o' : [['Moteur','open'], ['pin_open', 'd','o'], ['o','-', 'violet', 1]],
|
||||
'mot_f' : [['Moteur','close'], ['pin_close', 'd','o'], ['o','-', 'darkviolet', 1]],
|
||||
'mot_angle' : [['Moteur','alpha'], [], ['o','-', 'blue', 1]],
|
||||
'mot_vitesse' : [['Moteur','speed'], [], ['o','-', 'darkblue', 1]],
|
||||
'bp_ext' : [['Bp cote rue','activated'], ['pin', 'd','i'], ['.','-', 'green', 1]],
|
||||
'bp_ext_r' : [['Bp cote rue','activated_real'], [], ['.','--', 'green', 1]],
|
||||
'bp_int' : [['Bp cote cour','activated'], ['pin', 'd','i'], ['.','-', 'darkgreen', 1]],
|
||||
'bp_int_r' : [['Bp cote cour','activated_real'], [], ['.','--', 'darkgreen', 1]],
|
||||
'fdc_o' : [['Microrupteur fdc ouvert','activated'], ['pin', 'd','i'], ['.','-', 'orange', 1]],
|
||||
'fdc_o_r' : [['Microrupteur fdc ouvert','activated_real'], [], ['.','--', 'orange', 1]],
|
||||
'fdc_f' : [['Microrupteur fdc ferme','activated'], ['pin', 'd','i'], ['.','-', 'darkorange', 1]],
|
||||
'fdc_f_r' : [['Microrupteur fdc ferme','activated_real'], [], ['.','--', 'darkorange', 1]],
|
||||
'mot_o' : [['Moteur','open'], ['pin_open', 'd','o'], ['.','-', 'violet', 1]],
|
||||
'mot_f' : [['Moteur','close'], ['pin_close', 'd','o'], ['.','-', 'darkviolet', 1]],
|
||||
'mot_angle' : [['Moteur','alpha'], [], ['.','-', 'blue', 1]],
|
||||
'mot_vitesse' : [['Moteur','speed'], [], ['.','-', 'darkblue', 1]],
|
||||
'mot_pas' : [['Moteur','step'], [], []],
|
||||
'portail_x' : [['Portail','x'], [], ['o','-', 'turquoise', 1]],
|
||||
'portail_vitesse' : [['Portail','speed'], [], ['o','-', 'darkturquoise', 1]],
|
||||
'portail_x' : [['Portail','x'], [], ['.','-', 'turquoise', 1]],
|
||||
'portail_vitesse' : [['Portail','speed'], [], ['.','-', 'darkturquoise', 1]],
|
||||
'portail_pas' : [['Portail','step'], [], []],
|
||||
'gyr' : [['Led','activated'], ['pin', 'd','o'], ['o','-', 'gold', 1]],
|
||||
'ir_emet' : [['Emetteur IR', 'activated'], ['pin', 'd','o'],['o','-', 'red', 1]],
|
||||
'ir_recep' : [['Recepteur IR','activated'], ['pin', 'd','i'],['o','-', 'darkred', 1]],
|
||||
'ir_recep_r' : [['Recepteur IR','activated_real'], [],['o','--', 'darkred', 1]]}
|
||||
'gyr' : [['Led','activated'], ['pin', 'd','.'], ['.','-', 'gold', 1]],
|
||||
'ir_emet' : [['Emetteur IR', 'activated'], ['pin', 'd','o'],['.','-', 'red', 1]],
|
||||
'ir_recep' : [['Recepteur IR','activated'], ['pin', 'd','i'],['.','-', 'darkred', 1]],
|
||||
'ir_recep_r' : [['Recepteur IR','activated_real'], [],['.','--', 'darkred', 1]]}
|
||||
|
||||
# Couleurs
|
||||
color_passive = (0.800, 0.005, 0.315,1) # bouton non activable : magenta
|
||||
|
@ -47,7 +47,9 @@ def commandes():
|
||||
gyr(False)
|
||||
print ("")
|
||||
|
||||
daq(['mot_angle', 'mot_vitesse', 'portail_x', 'portail_vitesse'])
|
||||
# daq(['mot_angle', 'mot_vitesse', 'portail_x', 'portail_vitesse'])
|
||||
daq(['bp_ext', 'bp_ext_r', 'bp_int', 'bp_int_r', 'fdc_o', 'fdc_o_r', 'fdc_f', 'fdc_f_r', 'mot_o', 'mot_f', 'mot_angle', 'mot_vitesse', 'portail_x', 'portail_vitesse', 'gyr', 'ir_emet', 'ir_recep', 'ir_recep_r'])
|
||||
# daq(['bp_ext', 'bp_ext_r', 'bp_int', 'bp_int_r', 'fdc_o', 'fdc_o_r', 'fdc_f', 'fdc_f_r', 'mot_o', 'mot_f', 'gyr'])
|
||||
reset_t()
|
||||
|
||||
# Fermeture
|
||||
@ -88,15 +90,16 @@ def commandes():
|
||||
# " rad - portail_vitesse : " +str(round(portail_vitesse, 3))+" mm/s - moteur_vitesse : " +str(round(mot_vitesse, 3))+
|
||||
# " rad/s - portail_pas : " +str(round(portail_pas, 3))+" mm/impulsion - moteur_pas : " +str(round(mot_pas, 3))+" rad/impulsion")
|
||||
|
||||
plot()
|
||||
plot(['bp_ext', 'bp_ext_r', 'bp_int', 'bp_int_r', 'fdc_o', 'fdc_o_r', 'fdc_f', 'fdc_f_r', 'mot_o', 'mot_f', 'gyr', ['mot_angle', 'mot_vitesse', 'portail_x', 'portail_vitesse']])
|
||||
fin() # A garder
|
||||
|
||||
###############################################################################
|
||||
# En: External call << DONT CHANGE THIS SECTION >>
|
||||
# Fr: Appel externe << NE PAS MODIFIER CETTE SECTION >>
|
||||
###############################################################################
|
||||
###############################################################################
|
||||
# En: External call << DONT CHANGE THIS SECTION >>
|
||||
# Fr: Appel externe << NE PAS MODIFIER CETTE SECTION >>
|
||||
###############################################################################
|
||||
|
||||
if __name__=='start':
|
||||
thread_cmd_start(commandes)
|
||||
if __name__=='stop':
|
||||
stop()
|
||||
|
||||
|
Binary file not shown.
15
twin.py
15
twin.py
@ -33,8 +33,7 @@ system=importlib.import_module(scene.objects['System']['script'][:-4]) # Systèm
|
||||
sys.setrecursionlimit(10**5) # Limite sur la récursivité (valeur par défaut : 1000) -> segfault de Blender
|
||||
|
||||
# Config file
|
||||
twin_config = ET.parse('twin_config.xml')
|
||||
twin_config_tree = twin_config.getroot()
|
||||
twin_config = ET.parse('twin_config.xml').getroot()
|
||||
|
||||
# Couleurs
|
||||
color_cmd = (0.8, 0.8, 0.8, 1) # Blanc
|
||||
@ -73,10 +72,10 @@ def keyboard(cont):
|
||||
# Maj du fichier de config (screen size : data/config/screen/width-> [0][0].text)
|
||||
screen_width = bge.render.getWindowWidth()
|
||||
screen_height = bge.render.getWindowHeight()
|
||||
twin_config_tree[0][0].text=str(screen_width)
|
||||
twin_config_tree[0][1].text=str(screen_height)
|
||||
twin_config_tree[0][2].text=str(scene.objects['About']['quality'])
|
||||
buffer_xml = ET.tostring(twin_config_tree)
|
||||
twin_config[0][0].text=str(screen_width)
|
||||
twin_config[0][1].text=str(screen_height)
|
||||
twin_config[0][2].text=str(scene.objects['About']['quality'])
|
||||
buffer_xml = ET.tostring(twin_config)
|
||||
with open("twin_config.xml", "wb") as f:
|
||||
f.write(buffer_xml)
|
||||
|
||||
@ -128,9 +127,9 @@ def keyboard(cont):
|
||||
def cmd_init():
|
||||
|
||||
# Fichier de config (screen size : data/config/screen/width-> [0][0].text, height-> [0][1].text)
|
||||
bge.render.setWindowSize(int(twin_config_tree[0][0].text),int(twin_config_tree[0][1].text))
|
||||
bge.render.setWindowSize(int(twin_config[0][0].text),int(twin_config[0][1].text))
|
||||
quality_eevee=('NOSMAA', 'LOW', 'MEDIUM','HIGH','ULTRA')
|
||||
scene.objects['About']['quality'] = int(twin_config_tree[0][2].text)
|
||||
scene.objects['About']['quality'] = int(twin_config[0][2].text)
|
||||
if quality_eevee[scene.objects['About']['quality']] == 'NOSMAA':
|
||||
eevee.smaa_quality= 'LOW'
|
||||
eevee.use_eevee_smaa = False
|
||||
|
@ -217,7 +217,7 @@ def quality_up(cont):
|
||||
eevee.use_eevee_smaa = False
|
||||
else:
|
||||
eevee.use_eevee_smaa = True
|
||||
print (sys.platform)
|
||||
# print (sys.platform)
|
||||
if sys.platform=="linux": # Plantage sur Windows
|
||||
eevee.smaa_quality= quality_eevee[scene.objects['About']['quality']]
|
||||
|
||||
|
@ -1,7 +1,10 @@
|
||||
<data>
|
||||
<screen>
|
||||
<width>1609</width>
|
||||
<height>905</height>
|
||||
<quality>1</quality>
|
||||
<width>792</width>
|
||||
<height>445</height>
|
||||
<quality>4</quality>
|
||||
</screen>
|
||||
<plot>
|
||||
<config>True</config>
|
||||
</plot>
|
||||
</data>
|
89
twin_daq.py
89
twin_daq.py
@ -5,6 +5,7 @@ import importlib
|
||||
import subprocess # Multiprocessus
|
||||
import time
|
||||
import csv
|
||||
import xml.etree.ElementTree as ET # Creating/parsing XML file
|
||||
|
||||
###############################################################################
|
||||
# twin_daq.py
|
||||
@ -44,24 +45,30 @@ def get(data):
|
||||
# Enregistrement des données
|
||||
###############################################################################
|
||||
|
||||
##
|
||||
# Activation du mode DAQ
|
||||
##
|
||||
|
||||
def daq(data):
|
||||
daq_data.clear()
|
||||
scene.objects['System']['daq_var'] =[]
|
||||
data_line = ['t']
|
||||
row = ['t']
|
||||
for var in data:
|
||||
scene.objects['System']['daq_var'].append([daq_config[var][0][0], daq_config[var][0][1]])
|
||||
data_line.append(var)
|
||||
print ("Acquisition des données :", scene.objects['System']['daq_var'])
|
||||
daq_data.append(data_line)
|
||||
row.append(var)
|
||||
# print ("Acquisition des données :", scene.objects['System']['daq_var'])
|
||||
daq_data.append(row)
|
||||
scene.objects['System']['daq']=True
|
||||
|
||||
##
|
||||
# Ajout des données (fps = 60)
|
||||
##
|
||||
|
||||
def daq_add(cont):
|
||||
data_line = [round(scene.objects['System']['time'], 3)]
|
||||
row = [round(scene.objects['System']['time'], 3)]
|
||||
for var in scene.objects['System']['daq_var']:
|
||||
data_line.append(round(scene.objects[var[0]][var[1]], 3))
|
||||
daq_data.append(data_line)
|
||||
row.append(round(scene.objects[var[0]][var[1]], 3))
|
||||
daq_data.append(row)
|
||||
|
||||
###############################################################################
|
||||
# Tableau CSV
|
||||
@ -74,11 +81,13 @@ def localize_floats(row):
|
||||
for el in row
|
||||
]
|
||||
|
||||
##
|
||||
# Génération du fichier
|
||||
##
|
||||
|
||||
def csv_generate():
|
||||
scene.objects['System']['daq']=False
|
||||
fichier_csv=scene.objects['System']['system']+'.csv'
|
||||
print ("Génération du fichier :", fichier_csv)
|
||||
scene.objects['Cmd-text']['modal']= True
|
||||
scene.objects['Cmd-text']['Text']= "Génération du fichier : "+fichier_csv
|
||||
scene.objects['Cmd-text'].setVisible(True,False)
|
||||
@ -91,17 +100,70 @@ def csv_generate():
|
||||
# Graphique statique
|
||||
###############################################################################
|
||||
|
||||
##
|
||||
# Création du fichier twin_plot.xml (configuration du graphique)
|
||||
##
|
||||
|
||||
def plot_config_generate(data_groups):
|
||||
|
||||
# Création du XML
|
||||
daq_config_list=list(daq_config)
|
||||
xml_data = ET.Element('data')
|
||||
xml_figure = ET.SubElement(xml_data, 'figure')
|
||||
xml_plot = ET.SubElement(xml_figure, 'plot')
|
||||
for var in daq_config_list:
|
||||
xml_var = ET.SubElement(xml_plot, 'var')
|
||||
xml_var.text=var
|
||||
if len(daq_config[var][2]) >=4:
|
||||
xml_mark = ET.SubElement(xml_var, 'marker')
|
||||
xml_mark.text=daq_config[var][2][0]
|
||||
xml_line = ET.SubElement(xml_var, 'linestyle')
|
||||
xml_line.text=daq_config[var][2][1]
|
||||
xml_color = ET.SubElement(xml_var, 'color')
|
||||
xml_color.text=daq_config[var][2][2]
|
||||
xml_size = ET.SubElement(xml_var, 'linewidth')
|
||||
xml_size.text=str(daq_config[var][2][3])
|
||||
|
||||
# Détection de groupe de graphique
|
||||
data_group_i=0
|
||||
xml_group = ET.SubElement(xml_var, 'group')
|
||||
xml_group.text="-1" # Pas de groupe par défaut
|
||||
for data_group in data_groups:
|
||||
if isinstance(data_group, list):
|
||||
data_group_i+=1
|
||||
for data_subgroup in data_group: # Scan du sous-groupe
|
||||
if data_subgroup == var:
|
||||
xml_group.text=str(data_group_i)
|
||||
else:
|
||||
if data_group == var: # Pas de groupe
|
||||
xml_group.text="0"
|
||||
|
||||
# Ecriture fichier
|
||||
plot_config = ET.ElementTree(xml_data)
|
||||
with open("plot_config.xml", "wb") as f: # XML généré plat (not pretty print)
|
||||
plot_config.write(f)
|
||||
|
||||
##
|
||||
# Activation du mode Plot
|
||||
def plot():
|
||||
##
|
||||
|
||||
def plot(data_groups):
|
||||
plot_config_generate(data_groups)
|
||||
scene.objects['System']['plot']=True
|
||||
|
||||
##
|
||||
# Fermer le processus du graphique
|
||||
##
|
||||
|
||||
def plot_close():
|
||||
if scene.objects['System']['plot_proc'] is not None:
|
||||
if scene.objects['System']['plot_proc'].poll()==None:
|
||||
scene.objects['System']['plot_proc'].terminate()
|
||||
|
||||
##
|
||||
# Génération du graphique
|
||||
##
|
||||
|
||||
def plot_generate():
|
||||
plot_close()
|
||||
fichier_csv=scene.objects['System']['system']+'.csv'
|
||||
@ -113,12 +175,18 @@ def plot_generate():
|
||||
# FIXME : ne marche pas
|
||||
###############################################################################
|
||||
|
||||
##
|
||||
# Mise en forme de décimaux
|
||||
##
|
||||
|
||||
def truncate(n, decimals=0):
|
||||
multiplier = 10**decimals
|
||||
return int(n* multiplier)/multiplier
|
||||
|
||||
##
|
||||
# Création du graphique interactif
|
||||
##
|
||||
|
||||
def plot_start(data):
|
||||
# subprocess.run([sys.executable, os.path.join(os.getcwd(), "twin_plot.py")], , stdin=subprocess.PIPE) # Process bloquant
|
||||
|
||||
@ -141,7 +209,10 @@ def plot_start(data):
|
||||
# # print("Blender stout : ", stout)
|
||||
scene.objects['System']['plot']=True
|
||||
|
||||
##
|
||||
# Ajout des données (fps = 60)
|
||||
##
|
||||
|
||||
def plot_add(cont):
|
||||
if cont.sensors['Plot'].positive :
|
||||
|
||||
|
@ -1,6 +1,10 @@
|
||||
import sys
|
||||
import random
|
||||
import importlib
|
||||
import matplotlib
|
||||
import csv
|
||||
import xml.etree.ElementTree as ET # Creating/parsing XML file
|
||||
|
||||
matplotlib.use('Qt5Agg')
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar
|
||||
@ -16,12 +20,22 @@ from matplotlib.figure import Figure
|
||||
# @license: GNU GPL
|
||||
###############################################################################
|
||||
|
||||
# UPBGE scene
|
||||
# scene = bge.logic.getCurrentScene()
|
||||
# Lecture des configurations
|
||||
twin_config = ET.parse('twin_config.xml').getroot()
|
||||
plot_config_tree = ET.parse('plot_config.xml').getroot()
|
||||
plot_config={}
|
||||
|
||||
# Récupérer le brochage du jumeau réel
|
||||
# system=importlib.import_module(scene.objects['Doc']['system']) # Système
|
||||
# pin_config = system.get_pin_config()
|
||||
###############################################################################
|
||||
# Configuration des plots
|
||||
###############################################################################
|
||||
|
||||
def plot_config_dict():
|
||||
for var in plot_config_tree[0][0]:
|
||||
var_dict={}
|
||||
for i in range (len(var)):
|
||||
if var[i] is not None:
|
||||
var_dict.update({var[i].tag : var[i].text})
|
||||
plot_config.update({var.text: var_dict})
|
||||
|
||||
###############################################################################
|
||||
# Zone de dessin
|
||||
@ -98,11 +112,6 @@ class DynamicPlot(QtWidgets.QMainWindow):
|
||||
# self.xdata = self.xdata + [float(msg_list[0])]
|
||||
# self.ydata = self.ydata + [float(msg_list[1])]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# # Lecture du Pipe
|
||||
# # i=0
|
||||
# # lines = sys.stdin.readlines()
|
||||
@ -132,11 +141,6 @@ class DynamicPlot(QtWidgets.QMainWindow):
|
||||
# self.ydata = self.ydata + [float(msg_list[1])]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# for line in msg_lines:
|
||||
# msg_list=line.split(',')
|
||||
# # print(msg_list)
|
||||
@ -144,7 +148,6 @@ class DynamicPlot(QtWidgets.QMainWindow):
|
||||
# self.xdata = self.xdata + [float(msg_list[0])]
|
||||
# self.ydata = self.ydata + [float(msg_list[1])]
|
||||
|
||||
|
||||
# for line in sys.stdin:
|
||||
# msg_list=msg.split(',')
|
||||
# print(msg_list)
|
||||
@ -152,7 +155,6 @@ class DynamicPlot(QtWidgets.QMainWindow):
|
||||
# self.xdata = self.xdata + [float(msg_list[0])]
|
||||
# self.ydata = self.ydata + [float(msg_list[1])]
|
||||
|
||||
|
||||
# self.ydata = self.ydata + [random.randint(0, 10)]
|
||||
# Drop off the first y element, append a new one.
|
||||
# self.ydata = self.ydata[1:] + [random.randint(0, 10)]
|
||||
@ -176,9 +178,10 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
super(MainWindow, self).__init__(*args, **kwargs)
|
||||
self.canvas = MplCanvas(self, width=5, height=4, dpi=100)
|
||||
toolbar = NavigationToolbar(self.canvas, self)
|
||||
plt = self.canvas.subplot
|
||||
# plt = self.canvas.subplot
|
||||
# plt.cla()
|
||||
|
||||
# Implantation
|
||||
# Implantation de la fenêtre
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(toolbar)
|
||||
layout.addWidget(self.canvas)
|
||||
@ -186,15 +189,56 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
widget.setLayout(layout)
|
||||
self.setCentralWidget(widget)
|
||||
|
||||
# Lecture des données
|
||||
print ("Qt : fichier de données :", sys.argv[1])
|
||||
n_data = 50
|
||||
self.xdata = list(range(n_data))
|
||||
self.ydata = [random.randint(0, 10) for i in range(n_data)]
|
||||
# Lecture fichier CSV
|
||||
fields = []
|
||||
rows = []
|
||||
with open(sys.argv[1], newline='') as csvfile:
|
||||
csvreader = csv.reader(csvfile, delimiter=';')
|
||||
fields = next(csvreader)
|
||||
for row in csvreader:
|
||||
rows.append(row)
|
||||
|
||||
# Plot
|
||||
# Mise en tableau à deux colonnes (xdata,ydata)
|
||||
xdata=[]
|
||||
ydata=[]
|
||||
i=0
|
||||
for field in fields:
|
||||
xdata_row=[]
|
||||
ydata_row=[]
|
||||
for row in rows:
|
||||
xdata_row.append(float(row[0].replace(',', '.'))) # Revenir au format US des décimaux
|
||||
ydata_row.append(float(row[i].replace(',', '.'))) # Revenir au format US des décimaux
|
||||
xdata.append(xdata_row)
|
||||
ydata.append(ydata_row)
|
||||
i+=1
|
||||
|
||||
# Plots
|
||||
plot_config_dict() # Configuration des plots
|
||||
# FIXME : multiplot à faire
|
||||
# plts = []
|
||||
# plt_i=0
|
||||
# for i in range(len(fields)):
|
||||
# if i != 0 or plot_config[fields[i]]['group']=="-1" : # i=0 -> Ne pas afficher car c'est temps (variable t)
|
||||
# if plot_config[fields[i]]['group']=="0" : # Pas de groupe : nouveau plot
|
||||
# plts.append(self.canvas.subplot)
|
||||
|
||||
plt = self.canvas.subplot
|
||||
plt.cla()
|
||||
plt.plot(self.xdata, self.ydata, 'b')
|
||||
for i in range(len(fields)):
|
||||
if i != 0 : # i=0 -> Ne pas afficher car c'est temps (variable t)
|
||||
if twin_config[1][0].text == "True": # Configuration des plots activée
|
||||
plt.plot(xdata[i], ydata[i], label=fields[i], color=plot_config[fields[i]]['color'],
|
||||
linewidth=plot_config[fields[i]]['linewidth'], linestyle=plot_config[fields[i]]['linestyle'], marker=plot_config[fields[i]]['marker'])
|
||||
else:
|
||||
plt.plot(xdata[i], ydata[i], '.-', label=fields[i])
|
||||
|
||||
# Décoration
|
||||
plt.set_xlabel("Temps (s)")
|
||||
plt.set_ylabel("Valeurs")
|
||||
plt.set_title(sys.argv[1])
|
||||
plt.axhline(linewidth=1, color='k')
|
||||
plt.grid(True, linestyle='--')
|
||||
plt.legend()
|
||||
|
||||
# Redraw
|
||||
self.canvas.draw()
|
||||
|
Loading…
Reference in New Issue
Block a user