import bge # Blender Game Engine (UPBGE) import bpy # Blender import sys import math import time import xml.etree.ElementTree as ET # Creating/parsing XML file import importlib import runpy # Exécution de script Python légère (sans import) from pylint import epylint as lint # Mesure de la qualité d'un code Python import twin_doc # Documentation import twin_about # About ############################################################################### # twin.py # @title: Bibliothèque générale de l'environnement 3D pour le développement de jumeau numérique # @project: Blender-EduTech # @lang: fr # @authors: Philippe Roy # @copyright: Copyright (C) 2020-2023 Philippe Roy # @license: GNU GPL # # Cet environnement 3D est programmable en Python. Il est destiné à la découverte de la programmation de système pluritechnologique. # ############################################################################### # UPBGE scene scene = bge.logic.getCurrentScene() eevee = bpy.context.scene.eevee system=importlib.import_module(scene.objects['System']['script'][:-4]) # Système # Memory 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() # Couleurs color_cmd = (0.8, 0.8, 0.8, 1) # Blanc color_cmd_hl = (0.8, 0.619, 0.021, 1) # Jaune color_passive = (0.800, 0.005, 0.315,1) # bouton non activable : magenta color_active = (0.799, 0.130, 0.063,1) # bouton activable : orange color_hl = (0.8, 0.8, 0.8, 1) # bouton focus : blanc color_activated = (0.8, 0.619, 0.021, 1) # bouton activé numériquement uniquement : jaune color_activated_real = (0.799, 0.031, 0.038, 1) # élément activé physiquement uniquement : rouge (hors clic) color_activated_dbl = (0.246, 0.687, 0.078, 1) # élément activé physiquement et numériquement : vert clair color_blue = (0.007, 0.111, 0.638, 1) # Couleur Blue (Actionneur type moteur) color_blue_dark = (0.004, 0.054, 0.296, 1) # Couleur Blue (Actionneur type moteur) color_grey = (0.285, 0.285, 0.285, 1) # Couleur Blue (Actionneur type moteur) color_led_yellow = (0.799, 0.617, 0.021, 1) # Couleur Led Jaune # Constantes JUST_ACTIVATED = bge.logic.KX_INPUT_JUST_ACTIVATED JUST_RELEASED = bge.logic.KX_INPUT_JUST_RELEASED ACTIVATE = bge.logic.KX_INPUT_ACTIVE # JUST_DEACTIVATED = bge.logic.KX_SENSOR_JUST_DEACTIVATED ############################################################################### # Gestion du clavier ############################################################################### # Mode : Orbit(0) par défaut, Pan(1) avec Shift, Zoom (2) avec Ctrl, Eclaté (1) avec Shift def keyboard(cont): obj = scene.objects['System'] keyboard = bge.logic.keyboard # Touche ESC if JUST_ACTIVATED in keyboard.inputs[bge.events.ESCKEY].queue: # 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) with open("twin_config.xml", "wb") as f: f.write(buffer_xml) # Sortir bge.logic.endGame() # Shift -> mode 1 : Pan (clic milieu) ou Eclaté (clic gauche) if JUST_ACTIVATED in keyboard.inputs[bge.events.LEFTSHIFTKEY].queue: obj['manip_mode']=1 if JUST_ACTIVATED in keyboard.inputs[bge.events.RIGHTSHIFTKEY].queue: obj['manip_mode']=1 # Ctrl -> mode 2 : Zoom (clic milieu) if JUST_ACTIVATED in keyboard.inputs[bge.events.LEFTCTRLKEY].queue: obj['manip_mode']=2 if JUST_ACTIVATED in keyboard.inputs[bge.events.RIGHTCTRLKEY].queue: obj['manip_mode']=2 # Pas de modificateur -> mode 0 : Orbit (clic milieu) if JUST_RELEASED in keyboard.inputs[bge.events.LEFTSHIFTKEY].queue: obj['manip_mode']=0 if JUST_RELEASED in keyboard.inputs[bge.events.RIGHTSHIFTKEY].queue: obj['manip_mode']=0 if JUST_RELEASED in keyboard.inputs[bge.events.LEFTCTRLKEY].queue: obj['manip_mode']=0 if JUST_RELEASED in keyboard.inputs[bge.events.RIGHTCTRLKEY].queue: obj['manip_mode']=0 # Touche Home -> Reset de la vue if JUST_ACTIVATED in keyboard.inputs[bge.events.HOMEKEY].queue: manip_reset() # Touche F5 -> Run et Pause if JUST_ACTIVATED in keyboard.inputs[bge.events.F5KEY].queue: cycle_run () # Touche F6 -> Stop / Init if JUST_ACTIVATED in keyboard.inputs[bge.events.F6KEY].queue: cycle_stop () ############################################################################### # Commandes ############################################################################### ## # Initialisation des commandes ## 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)) quality_eevee=('NOSMAA', 'LOW', 'MEDIUM','HIGH','ULTRA') scene.objects['About']['quality'] = int(twin_config_tree[0][2].text) if quality_eevee[scene.objects['About']['quality']] == 'NOSMAA': eevee.smaa_quality= 'LOW' eevee.use_eevee_smaa = False else: eevee.use_eevee_smaa = True eevee.smaa_quality= quality_eevee[scene.objects['About']['quality']] # Ajout du Hud # scene.active_camera = scene.objects["Camera"] # scene.objects['Sun'].setVisible(True,True) # scene.addOverlayCollection(scene.cameras['Camera-Hud'], bpy.data.collections['Hud']) # UI : Commands scene.objects['Run-Hl'].setVisible(False,False) # scene.objects['Pause-Hl'].setVisible(False,False) scene.objects['ResetView-Hl'].setVisible(False,False) scene.objects['Doc-cmd-Hl'].setVisible(False,False) scene.objects['About-cmd-Hl'].setVisible(False,False) # UI : Text, ... scene.objects['Cmd-text']['Text']="" scene.objects['Cmd-text'].setVisible(True,False) scene.objects['Twins-text']['Text']="Connection fermée." # Windows windows=("Doc", "Doc_chap-general", "Doc_chap-system", "Doc_chap-python", "About") for window in windows: scene.objects[window].setVisible(False,True) # Jumeau scene.objects['System']['twins'] = False ## # Highlight des commandes ## def cmd_hl(cont): obj = cont.owner # Activation if cont.sensors['MO'].status == JUST_ACTIVATED and scene.objects['System']['manip_mode']==0: if obj.name!="Run" and obj.name!="Pause" and obj.name!="Stop" and obj.name!="Doc-cmd-colbox": # if obj.name!="Run" and obj.name!="Pause" and obj.name!="Run-Hl" and obj.name!="Pause-Hl" and obj.name!="Pause-Hl": obj.setVisible(False,True) scene.objects[obj.name+'-Hl'].setVisible(True,True) # Run et pause if obj.name=="Pause" or obj.name=="Run": if scene.objects['System']['run'] == True: pass # scene.objects['Pause'].setVisible(False,False) FIXME pause pas implémenté # scene.objects['Pause-Hl'].setVisible(True,False) FIXME pause pas implémenté else: scene.objects['Run'].setVisible(False,False) scene.objects['Run-Hl'].setVisible(True,False) # Stop if obj.name=="Stop": scene.objects['Stop'].setVisible(False,False) scene.objects['Stop-Hl'].setVisible(True,False) # Doc if obj.name=="Doc-cmd-colbox": scene.objects['Doc-cmd'].setVisible(False,False) scene.objects['Doc-cmd-Hl'].setVisible(True,False) # Text text_hl ={"Run":"Exécuter (F5)", "Stop":"Stop et initialisation (F6)", # "Pause":"Pause (F5)", "Doc-cmd-colbox":"Documentation", "ResetView": "Reset de la vue (Touche Début)", "About-cmd": "A propos"} scene.objects['Cmd-text']['modal']= False scene.objects['Cmd-text']['Text']= text_hl[obj.name] if scene.objects['Doc']['page_chap']== "" and obj.name =="Doc-cmd-colbox" : scene.objects['Cmd-text']['Text']= "Chargement de la documentation ..." if scene.objects['Doc']['page_chap']!= "" and obj.name =="Doc-cmd-colbox" : bpy.context.view_layer.update() # Bug de mise à jour scene.objects['Cmd-text'].setVisible(True,False) # Désactivation if cont.sensors['MO'].status == JUST_RELEASED and (scene.objects['System']['manip_mode']==0 or scene.objects['System']['manip_mode']==9): if scene.objects['Cmd-text']['modal'] != True: scene.objects['Cmd-text']['Text']= "" scene.objects['Cmd-text'].setVisible(False,False) if obj.name!="Run" and obj.name!="Pause" and obj.name!="Stop" and obj.name!="Doc-cmd-colbox": scene.objects[obj.name+'-Hl'].setVisible(False,True) obj.setVisible(True,True) # Run et pause if obj.name=="Pause" or obj.name=="Run": if scene.objects['System']['run'] == True: pass # scene.objects['Pause-Hl'].setVisible(False,False) FIXME pause pas implémenté # scene.objects['Pause'].setVisible(True,False) FIXME pause pas implémenté else: scene.objects['Run-Hl'].setVisible(False,False) scene.objects['Run'].setVisible(True,False) # Stop if obj.name=="Stop": scene.objects['Stop-Hl'].setVisible(False,False) scene.objects['Stop'].setVisible(True,False) # Doc if obj.name=="Doc-cmd-colbox": scene.objects['Doc-cmd-Hl'].setVisible(False,False) scene.objects['Doc-cmd'].setVisible(True,False) ## # Click sur les commandes ## def cmd_click(cont): obj = cont.owner if cont.sensors['Click'].status == JUST_ACTIVATED and cont.sensors['MO'].positive and scene.objects['System']['manip_mode']==0: # Play et pause if obj.name=="Pause" or obj.name=="Run": cycle_run () # Stop if obj.name=="Stop": cycle_stop () # Reset-view if obj.name=="ResetView" : manip_reset() # About if obj.name=="About-cmd" : twin_about.open() # Aide if obj.name=="Doc-cmd-colbox" : twin_doc.open() ############################################################################### # Manipulation du mécanisme ############################################################################### ## # Dessiner une cercle (bas niveau) ## def circle (center, radius, color): ang = 0 ang_step = 0.1 while ang< 2 * math.pi: x0 = center[0]+float(radius*math.cos(ang)*0.95) y0 = center[1] z0 = center[2]+float(radius*math.sin(ang)) x1 = center[0]+float(radius*math.cos(ang+ang_step)*0.95) y1 = center[1] z1 = center[2]+float(radius*math.sin(ang+ang_step)) bge.render.drawLine([x0,y0,z0],[x1,y1,z1],color) ang += ang_step ## # Initialisation de la vue 3D ## # def manip_init(cont): def manip_init(): # Ajout du Hud scene.active_camera = scene.objects["Camera"] scene.objects['Sun'].setVisible(True,True) scene.addOverlayCollection(scene.cameras['Camera-Hud'], bpy.data.collections['Hud']) # Fenêtres scene.objects['About'].setVisible(False,True) scene.objects['Doc'].setVisible(False,True) # Mémorisation de la position de la caméra scene.objects['Camera']['init_lx']=scene.objects['Camera'].worldPosition.x scene.objects['Camera']['init_ly']=scene.objects['Camera'].worldPosition.y scene.objects['Camera']['init_lz']=scene.objects['Camera'].worldPosition.z # Mémorisation de la position du modèle scene.objects['System']['init_lx']=scene.objects['System'].worldPosition.x scene.objects['System']['init_ly']=scene.objects['System'].worldPosition.y scene.objects['System']['init_lz']=scene.objects['System'].worldPosition.z scene.objects['System']['init_rx']=scene.objects['System'].worldOrientation.to_euler().x scene.objects['System']['init_ry']=scene.objects['System'].worldOrientation.to_euler().y scene.objects['System']['init_rz']=scene.objects['System'].worldOrientation.to_euler().z ## # Atteindre une orientation (bas niveau) ## def applyRotationTo(obj, rx=None, ry=None, rz=None): rres=0.001 # resolution rotation # x if rx is not None: while (abs(rx-obj.worldOrientation.to_euler().x) > rres) : if obj.worldOrientation.to_euler().x-rx > rres: obj.applyRotation((-rres, 0, 0), True) if rx-obj.worldOrientation.to_euler().x > rres: obj.applyRotation((rres, 0, 0), True) # print ("delta x ",rx-obj.worldOrientation.to_euler().x) # y if ry is not None: while (abs(ry-obj.worldOrientation.to_euler().y) > rres) : if obj.worldOrientation.to_euler().y-ry > rres: obj.applyRotation((0, -rres, 0), True) if ry-obj.worldOrientation.to_euler().y > rres: obj.applyRotation((0, rres, 0), True) # print ("delta y ",ry-obj.worldOrientation.to_euler().y) # z if rz is not None: while (abs(rz-obj.worldOrientation.to_euler().z) > rres) : if obj.worldOrientation.to_euler().z-rz > rres: obj.applyRotation((0, 0, -rres), True) if rz-obj.worldOrientation.to_euler().z > rres: obj.applyRotation((0, 0, rres), True) # print ("delta z ",rz-obj.worldOrientation.to_euler().z) ## # Reset de la manipulation de la vue ## def manip_reset(): scene.objects['Camera'].worldPosition.x = scene.objects['Camera']['init_lx'] scene.objects['Camera'].worldPosition.y = scene.objects['Camera']['init_ly'] scene.objects['Camera'].worldPosition.z = scene.objects['Camera']['init_lz'] applyRotationTo(scene.objects['System'], 0, 0, 0) scene.objects['System'].worldPosition.x = scene.objects['System']['init_lx'] scene.objects['System'].worldPosition.y = scene.objects['System']['init_ly'] scene.objects['System'].worldPosition.z = scene.objects['System']['init_lz'] # for objet in scene.objects['System']['objects'] : # scene.objects[objet].setVisible(True,False) # scene.objects[objet].restorePhysics() # if objet+"_Lines.GP" in scene.objects: # scene.objects[objet+"_Lines.GP"].setVisible(True,False) ## # Position de départ pour la manipulation de la vue ## def manip_start(cont): obj = cont.owner obj['click_x']=cont.sensors['ClickM'].position[0] obj['click_y']=cont.sensors['ClickM'].position[1] ## # Manipulation du modèle ou de la caméra ## def manip(cont): obj = cont.owner sensibilite_orbit=0.0005 sensibilite_pan=0.005 sensibilite_zoom=0.01 delta_x=cont.sensors['DownM'].position[0]-obj['click_x'] delta_y=cont.sensors['DownM'].position[1]-obj['click_y'] # Orbit if obj['manip_mode']==0: x0 = scene.objects['Orbit-Hud'].worldPosition.x y0 =scene.objects['Orbit-Hud'].worldPosition.y z0 =scene.objects['Orbit-Hud'].worldPosition.z width = bge.render.getWindowWidth() height = bge.render.getWindowHeight() dist_orbit_y_base=200 # Pour 1280 x 720 # radius_orbit_y_base=5 radius_orbit_y_base=0.875 diag_base= math.sqrt(1280**2+720**2) diag= math.sqrt(width**2+height**2) dist_orbit_y = dist_orbit_y_base*(diag/diag_base) radius_orbit_y=radius_orbit_y_base*(diag/diag_base) dist_orbit = math.sqrt(((width/2)-obj['click_x'])**2+((height/2)-obj['click_y'])**2) if obj['click_y']> height/2 : dist_orbit = dist_orbit+((math.sqrt(((height/2)-obj['click_y'])**2)/175)*25) # Correction des Y sous le centre ? if dist_orbit= abs(delta_y): scene.objects['System'].applyRotation((0, delta_x*sensibilite_orbit, 0), False) else: scene.objects['System'].applyRotation((0, delta_y*sensibilite_orbit, 0), False) # Pan if obj['manip_mode']==1: # Shift scene.objects['System'].applyMovement((delta_x*sensibilite_pan, -delta_y*sensibilite_pan, -delta_y*sensibilite_pan), False) # scene.objects['Camera'].applyMovement((delta_x*-sensibilite_pan, delta_y*sensibilite_pan, 0), True) # Zoom if obj['manip_mode']==2: # Ctrl scene.objects['Camera'].applyMovement((0, 0, (delta_x+delta_y)*sensibilite_zoom), True) scene.objects['Camera'].applyMovement((0, 0, (delta_x+delta_y)*sensibilite_zoom), True) ## # Zoom ## def manip_wheel(cont): obj = cont.owner sensibilite_wheel = 20 if cont.sensors['WheelUp'].positive: scene.objects['Camera'].applyMovement((0, 0, -sensibilite_wheel), True) if cont.sensors['WheelDown'].positive: scene.objects['Camera'].applyMovement((0, 0, sensibilite_wheel), True) ############################################################################### # Cycle ############################################################################### ## # Validation du code Python ## def python_validation(file): (pylint_stdout, pylint_stderr) = lint.py_run(file+' --disable=C --disable=W --disable=R', return_std=True) stdout = pylint_stdout.read() stderr = pylint_stderr.read() if " error (" in stdout: # Présence d'erreur # UI : information scene.objects['Cmd-text']['modal']= True scene.objects['Cmd-text']['Text']= "Erreur dans le script ... " scene.objects['Cmd-text'].setVisible(True,False) print(stdout) # Affichage console return False else: scene.objects['Cmd-text']['modal']= False scene.objects['Cmd-text']['Text']= "" scene.objects['Cmd-text'].setVisible(False,False) return True ## # Mise en route et pause du cycle ## def cycle_run (): # Pause if scene.objects['System']['run'] == True: scene.objects['System']['run']=False scene.objects['Pause'].setVisible(False,False) scene.objects['Pause'].suspendPhysics() scene.objects['Pause-Hl'].setVisible(False,False) scene.objects['Run'].restorePhysics() scene.objects['Run-Hl'].setVisible(True,False) # Run else : scene.objects['System']['run']=True scene.objects['Run'].setVisible(False,False) scene.objects['Run'].suspendPhysics() scene.objects['Run-Hl'].setVisible(False,False) # scene.objects['Pause']. restorePhysics() # FIXME pause pas implémentée # scene.objects['Pause'].setVisible(True,False) # FIXME pause pas implémentée # Démarrage du cycle if scene.objects['System']['thread_cmd']==False: time.sleep(0.125) scene.objects['System']['thread_cmd']=True system.system_reset() time.sleep(0.125) scene.objects['System']['time']=0 if python_validation(scene.objects['System']['script']+'.py'): runpy.run_module(scene.objects['System']['script'], run_name='start') # Execution du script utilisateur # Arrêt de la pause else: # FIXME : Relancer la maquette pass ## # Arrêt et réinitialisation du cycle (forçage) ## def cycle_stop (): scene.objects['System']['thread_cmd']=False system.system_reset() ## # Fin naturelle du cycle ## def cycle_end (cont): if cont.sensors['End cycle'].positive: scene.objects['System']['run']=False if python_validation(scene.objects['System']['script']+'.py'): runpy.run_module(scene.objects['System']['script'], run_name='stop') # Fin du script utilisateur # Commandes scene.objects['Pause'].setVisible(False,False) scene.objects['Pause'].suspendPhysics() scene.objects['Pause-Hl'].setVisible(False,False) scene.objects['Run'].setVisible(True,False) scene.objects['Run'].restorePhysics() ## # Highlight sur les éléments cliquable du systèmes ## def cycle_hl(cont): if "-colbox" in cont.owner.name: obj= scene.objects[cont.owner.name[:-7]] else: obj = cont.owner # Activation if cont.sensors['MO'].status == JUST_ACTIVATED: obj['mo'] = True # Composant avec un goupe de couleur (type actionneur) if "color_group" in obj.getPropertyNames(): for obj_name in obj['color_group']: scene.objects[obj_name].color = color_hl else: obj.color = color_hl # Description if obj['description'] is not None: scene.objects['Cmd-text']['Text']=obj['description'] scene.objects['Cmd-text'].setVisible(True,True) # Désactivation if cont.sensors['MO'].status == JUST_RELEASED : obj['mo'] = False scene.objects['Cmd-text']['Text']="" # Composant passif if "active" in obj.getPropertyNames(): if obj['active'] == False: # Composant passif obj.color = color_passive return # Composant avec un goupe de couleur (type actionneur) if "color_group" in obj.getPropertyNames(): for obj_name in obj['color_group']: cycle_actuator_color(obj_name) return # Composant simple obj.color = color_active ## # Click sur les éléments cliquables du système (activation numérique) ## def cycle_click(cont): obj = cont.owner if scene.objects['System']['run'] ==False: return # Passif if "active" in obj.getPropertyNames(): if obj['active'] == False: obj.color = color_passive return # Activation if cont.sensors['Click'].status == JUST_ACTIVATED and cont.sensors['MO'].positive and scene.objects['System']['manip_mode']==0: obj.color = color_activated obj['activated'] = True obj['click'] = True # Modele 3d -> Arduino : FIXME # Désactivation if cont.sensors['Click'].status == JUST_RELEASED: obj['activated'] = False obj['click'] = False # Modele 3d -> Arduino : FIXME if cont.sensors['MO'].positive: obj.color = color_hl else: obj.color = color_active ## # Couleurs sur les éléments sensibles du système ## def cycle_sensitive_color(obj): # Mouse over if obj['mo'] == True and obj['click'] == False and obj.color !=color_hl: obj.color =color_hl # Jumeau elif scene.objects['System']['twins']: if obj['activated_real'] ==False and obj['activated']==False and obj.color !=color_active: obj.color =color_active elif obj['activated_real'] and obj['activated'] and obj.color !=color_activated_dbl: obj.color =color_activated_dbl elif obj['activated_real'] and obj['activated']==False and obj.color !=color_activated_real: obj.color =color_activated_real elif obj['activated_real'] ==False and obj['activated'] and obj.color !=color_activated: obj.color =color_activated elif obj['activated_real'] ==False and obj['activated']==False and obj.color !=color_activated: obj.color =color_active # Uniquement maquette 3D else: if obj['activated'] == True and obj.color !=color_activated: obj.color =color_activated elif obj['activated'] == False and obj.color !=color_active: obj.color =color_active ## # Couleurs des actionneurs ## def cycle_actuator_color(name): obj = scene.objects[name] if obj['color']== "blue": obj.color = color_blue elif obj['color']== "blue-dark": obj.color = color_blue_dark elif obj['color']== "grey": obj.color = color_grey elif obj['color']== "led_yellow": obj.color = color_led_yellow ## # Comportement standard des boutons ## def cycle_bp(cont): obj = cont.owner # Arduino -> Modele 3D if scene.objects['System']['twins']: if obj['pin'] is not None: if obj['pin'].read()==True and obj['activated_real'] == False : obj['activated_real'] = True if obj['pin'].read()==False and obj['activated_real'] == True : obj['activated_real'] = False # Couleurs cycle_sensitive_color(obj) ## # Comportement standard des voyants ## def cycle_voy(cont): obj = cont.owner obj_on=scene.objects[obj.name+"-on"] # Mouse over if obj['mo'] == True and obj.color !=color_hl: obj.color =color_hl elif scene.objects['System']['run']: # Activation if obj['activated'] and obj_on.visible == False: obj_on.setVisible(True,False) obj.setVisible(False,False) # Modele 3D -> Arduino if scene.objects['System']['twins']: if obj['pin'] is not None: obj['pin'].write(1) # Désactivation if obj['activated']==False and obj_on.visible == True: obj.setVisible(True,False) obj_on.setVisible(False,False) # Modele 3D -> Arduino if scene.objects['System']['twins']: if obj['pin'] is not None: obj['pin'].write(0) ## # Mise en couleur des actionneurs ## def cycle_def_focusgroup(focusgroup, description): group=[] for obj in focusgroup: group.append(obj[0]) for obj in focusgroup: scene.objects[obj[0]]['color_group'] = group scene.objects[obj[0]]['color'] = obj[1] scene.objects[obj[0]]['description']=description cycle_actuator_color(obj[0])