Une minuterie paramètrable

Ce didacticiel suppose que vous connaissiez la définitions des différents noeuds, events et spécifications de base du VRML97.
Vous pourrez trouver toutes ces infos (en français) sur le site de 3Dnet.
N'oubliez pas que tous les ROUTE doivent se trouver tout à la fin du fichier.

But :

Nous allons créer une minuterie dont nous règlerons la durée en tournant un bouton. Nous afficherons dans la scène la durée de cette minuterie. Un interrupteur servira à allumer une lumière. Une fois le temps de la minuterie écoulé, la lumière s'éteindra.

En pratique :

Pour le bouton de réglage de la minuterie, nous utiliserons un cylindre.

Shape {
           geometry Cylinder {
           }           
 }

Nous parentons ce cylindre à un noeud transform que nous nommons "reglage".

DEF reglage Transform {
	children [
		Shape {
			geometry Cylinder {
			}
		}
	]
}

Nous associons un capteur CylinderSensor au cylindre. Nous l'appelons "captRG".

DEF reglage Transform {
	children [
		Shape {
			geometry Cylinder {
			}
		}
		DEF	captRG CylinderSensor {}
	]
}

Nous routons la rotation detectée (rotation_changed) par ce capteur vers la rotation du noeud reglage, ce qui permet de faire tourner le cylindre.

ROUTE captRG.rotation_changed TO reglage.rotation  

Limitons l'amplitude de ce CylinderSensor de façon qu'il ne tourne pas à l'infini. Laissons lui une marge de manoeuvre de 6 rad, ce qui fait 343.77°. Le maxAngle sera donc à 6 et le minAngle à 0, ce qui est sa valeur par défaut. Donc, pas la peine de préciser le minAngle.

DEF capteur TouchSensor {
	maxAngle 6
}

Voir la scène Voir le source


La durée de la minuterie variera donc en fonction de cette angle. 1 rad d'angle correspondant à une seconde. Donc notre minuterie va de 0 à 6 secondes. Par défaut, l'angle de rotation de notre cylindre est de 0 rad, ce qui fait une durée de minuterie de 0 seconde. Nous la mettrons à 1 seconde par défaut.
Pour celà, il faut tourner notre cylindre de 1 rad.

DEF reglage Transform {
	rotation 0 1 0 1
	children [
		Shape {
			geometry Cylinder {
			}
		}
		DEF	captRG CylinderSensor {}
	]
}

Il faut aussi que nous ajoutions un offset au CylinderSensor, sinon, dès que nous cliquerons dessus, il mettra la durée à 0 seconde.

DEF reglage Transform {
	children [
		Shape {
			geometry Cylinder {
			}
		}
		DEF	captRG CylinderSensor {
			offset 1
		}
	]
}

Nous allons utiliser un noeud texte nommé "affichage" qui nous permettra de visualiser la durée de la minuterie. Nous le parentons à un noeud Transform pour pouvoir le placer 1.5 m au-dessus du cylindre.
Par défaut; la rotation du bouton de minuterie est de 1 rad. L'affichage doit donc indiqué 1 par défaut.

Transform {
	translation 0 1.5 0	
	children [
		Shape {
			geometry DEF affichage Text { string "1"
			}
		}
	] 
} 

L'affichage doit afficher la valeur de l'angle de la rotation détectée par le capteur captRG. Cette rotation est un eventOut de type SFRotation. Nous ne pouvons pas le router directement vers la string de notre affichage. C'est un noeud script qui récupèrera la valeur de l'angle et la convertira en texte que nous routerons vers la string d'affichage. Appelons ce script "convert".

DEF convert Script {
}

Le script convert reçoit un eventIn de type SFRotation que nous appelons "rot".

DEF convert Script {
	eventIn SFRotation rot
}

Nous routons la rotation_changed de captRG vers le script convert.

ROUTE captRG.rotation_changed TO convert.rot

Il emet un eventOut de type MFString que nous appelons "duree".

DEF convert Script {
	eventIn SFRotation rot
	eventOut SFString duree
}

Nous routons cet eventOut duree vers la string de "affichage".

ROUTE convert.duree	TO affichage.string

Nous définissons une fonction rot() qui s'exécutera à chaque changement de la valeurs de l'angle de la rotation détectée par captRG.

url "javascript: function rot(){
}
"

La fonction rot() travaille implicitement avec deux valeurs :
- la valeur de l'évènement qui l'active
- l'instant ou celle-ci est activée.
Il faut préciser dans la définition de la fonction la valeur que nous voulons utiliser. En l'occurence, la valeur de l'évènement.

url "javascript: function rot(val){
}
"

Maintenant que nous avons accès à cette valeur, il faut récupérer sa composante angle et l'affecter à l'eventOut "duree". Attention, il y a un petit piège ! L'eventOut "duree" étant un MFString, ll faut affecter la composante angle à la première composante de "duree".

url	"javascript: function rot(val){
		duree[0] = val[3] ;
	}
	"

Note : Vous remarquerez que la conversion de val[3] qui est un SFFloat vers duree[0] qui est un SFString est faite automatiquement par le javascript.

Voir la scène

La détection de la rotation étant très précise, nous obtenons pour l'affichage un nombre bien trop important de décimale. 2 serait bien suffisant. Pour ce faire, nous multiplions la composante angle par 100, nous l'arrondissons et finalement la redivisons par 100.

duree[0] = Math.floor(100*val[3])/100 ;

Voir la scène Voir le source


La lumière "lamp" est un noeud PointLight de couleur blanche. Nous la plaçons devant le cylindre, légèrement sur la droite et en haut afin qu'elle fasse un joli reflet. Par défaut, elle est éteinte.

DEF lamp PointLight {
	on FALSE
	color 1 1 1
	location 4 1 10
}

Afin que l'on voit bien si elle est allumé ou éteinte, précisons dans les infos de navigation que nous éteignons la torche du navigateur.

NavigationInfo {
	headlight FALSE
}

Créons un interrupteur qui allumera la lampe et déclenchera l'écoulement de la minuterie. Pour l'interrupteur, nous utiliserons une boite de 20 x 20 x 20 cms.

Shape {
         geometry Box { size .2 .2 .2
         }
}           

Nous lui associons un TouchSensor dans un noeud Transform. Nommons le TouchSensor "interupteur" et déplaçons la boite afin qu'elle ne chevauche pas le cylindre.

Transform {
	translation 2 0 0
	children [
	Shape {
		geometry Box { size .2 .2 .2
		}
	}
	DEF interupteur TouchSensor {
	}
	]
} 

Passons maintenant à la minuterie. Nous utilisons un TimeSensor que nous nommons "timer". Par défaut, la durée est à 1 seconde. Inutile de la préciser donc.

DEF Timer TimeSensor {
}

Nous utiliserons le TouchTime de "interupteur" comme StartTime de "Timer".

ROUTE interupteur.touchTime TO Timer.startTime

Du coup, nous utilisons la propriété isActive de "Timer" pour allumer la lampe et l'y maintenir tant que la minuterie ne s'est pas écoulé.

ROUTE Timer.isActive TO lamp.on

Si "timer" est actif, "lamp" aussi. Et donc si "Timer" est inactif (ce qui se produit à la fin de son cycleInterval vu qu'il n'est pas en boucle), la lumière aussi.
Voilà le script presque complet...

DEF convert Script {
	eventIn	SFRotation rot	 
	eventIn SFTime touch  
	field SFBool etat FALSE	 
	eventOut MFString duree	 
	eventOut SFBool	active	 
	url	"javascript:
	function rot(val){
		duree[0] = Math.floor(100*val[3])/100 ;
	}
"
}

Voir la scène Voir le source


Vous avez remarqué ?... On a oublié un truc :
La valeur de l'angle doit être routée vers la durée du cycle du TimeSensor. Nous nous servons du script "convert" pour cela. Rajoutons lui un eventOut de type SFTime que nous nommons "temps". La valeur de cette eventOut est égal à la valeur de la string duree.

eventOut SFTime temps
...
temps = duree[0] = Math.floor(100*val[3])/100 ;

Note : vous remarquerez que une fois encore, le javascript se charge d'attribuer correctement les "unités" de chaque eventOut :
- temps est une SFTime
- duree[0] est un SFString issu d'un MFString.

Nous devons router cet eventOut "temps" vers le cycleInterval de "Timer".

ROUTE convert.temps TO Timer.cycleInterval

Voir la scène Voir le source

Pour aller plus loin :

Essayez de mettre en place un petit système qui permette d'afficher le temps qui passe, histoire de vérifier que la durée programmé est bien celle qui s'écoule.
En vous inspirant de la technique employée pour l'exercice sur l'interrupteur à 2 états, changez la couleur de l'interrupteur : vert quand la lumière est allumée et rouge quand elle est éteinte.

Valid XHTML 1.0 Strict Valid CSS!