5.6.3 Calcul de la taille du pas et de la position initiale
Trop souvant en
expérimentant nos L-systèmes, nous avons eu des problèmes avec la taille du
dessin – soit qu’il dépassait le canevas et on ne le voyait qu’en partie, soit qu’il
était trop petit et trop de détails étaient cachés. De même avec la position et
l’orientation initiale de la tortue – le dessin pouvait être mal placé sur le canevas,
ou il se dessinait seulement partialement ou pas du tout dans le canevas.
Attaquons nous alors au problème de trouver automatiquement une bonne
longueur pour l’avancement de la tortue ainsi qu’une bonne position et
orientation de départ.
Comment faire? Puisque nous avons, avant de lancer le dessin, une description
complète du dessin, sous forme d’une chaîne de caractères donnant toutes les
instruction Logo pour le dessiner, nous pouvons faire précéder le dessin par une
simulation de l’activité de dessiner la courbe. Cette simulation nous servira
à
- calculer le rectangle englobant le dessin, la boîte englobante, ou encore
– en anglais cette fois – sa bounding box;
- cette bounding box nous servira, à la fois pour déterminer la longueur
du trait qu’un simple F doit dessiner, pour que la totalité du dessin
tienne dans le canevas, et pour determiner la position initiale de la
tortue.
Pour calculer la boîte englobante, nous allons commencer par un rectangle de
taille très petite, disons de longueur 0 et de hauteur 0. Ce rectangle sera à coup
sûr trop petit pour contenir un dessin quelconque! Ensuite, nous allons supposer
que la tortue se trouve à la seule et unique position que ce rectangle permet: la
position horizentale et verticale égale à 0. La tortue aura l’orientation initiale
correspondant à l’angle 0.
Enfin, nous allons lire la chaîne de description de la courbe, caractère par
caractère, et si nous rencontrons
-
- un caractère “+”, nous allons ajouter l’angle à l’orientation de la tortue.
-
- un caractère “-” nous allons soustraire l’angle de l’orientation de la tortue.
-
- un caractère F, la position horizontale de la tortue sera augmentée du sinus
de l’angle et sa position verticale sera augmentée du cosinus de ce
même angle.25
| FIG. 5.27: | Un rectangle de point d’origine α@β |
|
|
Pour faciliter notre discours, supposons que notre boîte englobante soit définie
avec un point d’origine aux coordonnées ((α,β)
) et le point diagonalement opposé,
le point extent, aux coordonnées (γ, δ),
comme montré dans la figure 5.27. Nous
savons qu’au début
α, β γ et δ
valent tous 0.
Si, après un mouvement simulé de la tortue, sa nouvelle position horizontale
est plus petite que α, la coordonnée horizontale de l’origine de notre rectangle
englobant, alors cette coordonnée α prendra comme nouvelle valeur la
position horizontale de la tortue. Si la position horizontale de la tortue
est supérieure à γ, la coordonnée horizontale du point extent, alors γ
prendra comme nouvelle valeur la position horizontale de la tortue. Nous
procéderons d’une manière analogue pour les coordonnées verticales, β et
δ.
Si ce processus se répète pour chacun des déplacements simulés de la tortue, il
est évident que le rectangle sera à chaque instant exactement de la taille du dessin
simulé jusqu’à cet instant. Bien entendu, à la fin de la simulation, le rectangle
aura la taille du dessin final – si le dessin est dessiné avec une longueur de pas
égal à 1.
Revenons alors vers l’implémentation: pour simuler les mouvements de la
tortue nous avons besoin d’une tortue simulée. Celle-ci se caractérise par une
position horizontale et verticale et une orientation. Nous avons le choix
soit:
-
- de creer une nouvelle classe pour représenter une telle tortue avec ses trois
variables d’instances, une pour chacune des charactéristiques,
-
- de mettre ces variables d’instances directement dans notre classe Lsystem.
Optons pour la deuxième solution et ajoutons trois variables d’instance
supplémentaires à notre classe. Ce seront la variable sh et sv, pour garder la
Situation Horizontale et V erticale de la tortue, ainsi que la variable sdir,
donnant la direection de la tortue simulée. Il nous faut également une variable
d’instance pour la boîte englobante, la bounding box, que nous appelerons
box.
Ce qui nous donne la nouvelle définition de la classe Lsystem que
voici:26
DessinsLogo subclass: #Lsystem
instanceVariableNames: ’niveau reglesDerivation
axiome angle longueur
box sh sv sdir’
classVariableNames: ’’
poolDictionaries: ’’
category: ’Cours-Smalltalk’
|
|
Ensuite, nous avons besoin de calculer la boîte englobante. Ceci doit être
fait après avoir généré la chaîne de caractères représentant la courbe et,
naturellement, avant l’affichage de cette courbe. Ce qui, sous l’hypothèse que la
méthode boiteMin: calcule la boîte englobante, nous oblige à modifier notre
méthode lanceLsystem pour obtenir la version ci-dessous:
Lsystem>>lanceLsystem
| tmp |
self initImage.
pen place: 200@390.
tmp := self remplace: axiome times: niveau.
self boiteMine: tmp.
self automate: tmp.
self afficheImage
|
|
Pour calculer la bounding box, donc pour simuler les mouvements de la tortue,
nous pouvons prendre une approche analogue à celle que nous avons prise pour
faire le dessin: un parcours de la représentation de la courbe et l’activation d’une
méthode specifique pour chacun des caractères correspondant à une activité
de la tortue. Il suffit d’adapter la méthode automate: à nos nouveaux
besoins.
Dans l’adaptation ci-dessous nous supposons connues les trois méthodes
avanceS, plusS et moinsS, qui correspondent, pour notre simulation, aux
methodes avance, plus et moins de notre vraie tortue de dessin.
Voici alors notre méthode boiteMin::
Lsystem>>boiteMin: aString
sh := sv := 0.
sdir := 0.
box := Rectangle origin: 0 @ 0 corner: 0 @ 0.
aString
do: [:ele | (’F+-’ indexOf: ele)
> 0
ifTrue: [self
perform: (#(#avanceS #plusS #moinsS)
at: (’F+-’ indexOf: ele))]]
|
|
Cette méthode se charge également de l’initialisation des variables d’instances
nécessaires pour la simulation: les trois variables représentant l’état de la tortue simulée
sont toutes initialisées à 0: la tortue commence au point 0@0 dans la direction 0. La
variable box, représentant la boîte englobante, est initialisée à une instance de la classe
Rectangle.27
C’est justement le message origin:corner:, transmis à la classe Rectangle, qui
crée cette instance. L’argument origin:, le point 0@0, correspond au point α@β
de la figure 5.27, page 417. L’agument corner:, qui est ici également le point 0@0,
correspond au point γ@δ de cette même figure. C’est donc bien un rectangle
réduit à sa taille minimale.
Voici alors les trois méthodes de simulation des mouvements de la
tortue.
Lsystem>>plusS
sdir := sdir + angle
|
|
Lsystem>>moinsS
sdir := sdir - angle
|
|
Lsystem>>avancefS
sh := sh + sdir degreeSin.
sv := sv + sdir degreeCos.
self boxUpdate
|
|
Bien sûr, la simulation de + et - doit juste changer l’orientation de la
tortue, par une simple addition ou soustraction. Par contre, la simulation
de F doit à la fois calculer le déplacement, c’est le calcul de sinus et
cosinus,28
et mettre à jour les dimensions de notre boîte englobante, ce qui est fait par la
méthode boxUpdate ci-dessous:
Lsystem>>boxUpdate
sh < box origin x
ifTrue: [box := box withLeft: sh].
sh > box corner x
ifTrue: [box := box withRight: sh].
sv < box origin y
ifTrue: [box := Rectangle origin: box origin x @ sv
corner: box corner].
sv > box corner y
ifTrue: [box := box withBottom: sv]
|
|
Sachant que la méthode withLeft: change, dans notre dessin d’un rectangle
page 417, la valeur de
, que la méthode withRight: change
et que la méthode
withBottom: change
, cette méthode boxUpdate se comporte exactement comme
nous l’avons décrit ci-dessus, page 417.
Ayant maintenant la boîte englobante à notre disposition, nous pouvons –
enfin – calculer la position initiale de la tortue ainsi que la longueur d’un pas
(d’une instruction F) pour que le dessin tienne à l’intérieur de notre canevas et
qu’il le remplisse le mieux possible. C’est ce calcul qui est fait par la méthode
normalise ci-dessous:
Lsystem>>normalise
1 | stepX stepY step |
2 stepX := box corner x - box origin x.
3 stepY := box corner y - box origin y.
4 step := (form extent x - 20 / stepX)
5 min: (form extent y - 20 / stepY).
6 "rectangle:
7 origin: point de départ de la tortue
8 corner: longueur d’un pas @ 0"
9 ^ Rectangle origin: form extent x -
10 (step *
11 (box origin x + box corner x))
12 / 2
13 @ (form extent y +
14 (step *
15 (box origin y + box corner y))
16 / 2)
17 corner: step @ 0
|
|
Commentaire:
-
- Ligne 1: déclaration de trois variables temporaires: stepX contiendra la
taille horizontale de la boîte englobante, stepY sa taille verticale et
step contiendra la valeur que longueur devra prendre pour que le
dessin de la courbe tienne dans le canevas de dessin. Supposons, comme
tout le long de cette section, que notre boîte englobante ait l’origine au
point α@β et que l’extension (l’extent) soit donnée par le point γ@δ.
Notons que tel que nous avons écrit le programme jusqu’à maintenant,
α ∈ [−∞,γ],
β ∈ [−∞,δ], γ ∈ [0,+∞] et
δ ∈ [0,+∞].
-
- Ligne 2: stepX prendra comme valeur −α + δ,
correspondant bien à la
taille horizontale de la boîte englobante, à la distance de la coordonnée
horizontale α et γ.
-
- Ligne 3: stepY prendra comme valeur la taille verticale de la boîte
englobante, donc la distance entre la coordonnée verticale de β et δ.
-
- Lignes 4 et 5: Jusqu’à maintenant nous avons dans stepX et stepY la
taille d’un F pour un canevas d’unité, un canevas dont la taille est
déterminée de manière telle qu’un F correspond à un mouvement de la
tortue de longueur 1. Nous devons adapter cette longueur à la taille de
notre canevas réel, à la taille de notre form. C’est cela que nous faisons
dans ces deux lignes, où nous calculons la taille d’un F dans le dessin
couvrant le canevas, tout simplement en prenant la taille minimum de
la taille verticale et horizontale.29
Une telle taille est simplement le résultat de la division de la longueur
(ou hauteur) du canevas réel (moins une petite constante pour éviter
des débordements des bordures dûs aux erreurs d’arrondi) et de stepX
(ou stepY).
-
Lignes 9 à 17: Puisque nous voulons retourner deux valeurs: la
taille du pas et la position initiale de la tortue, nous retournons
ces valeurs dans la structure d’un rectangle qui a comme origine
la position initiale de la tortue et comme extension la taille du
pas.30
step * (box origin x + box corner x) / 2
|
|
La coordonnée x de la position de la tortue sera alors le milieu (d’où la
division par 2 dans l’expression ci-dessus) des coordonnées réelles (prenant
en compte les valeurs négatives éventuelles) de α et β multiplié par la taille
du pas, step. La coordonnée y est calculée de manière analogue avec les
coordonnées β et δ.
Si nous faisons maintenant les adaptations pour prendre en compte ces calculs de
placement et de pas dans le reste du programme, nous pouvons, sans nous
préoccuper ni de la longueur, ni de la position initiale de la tortue, dessiner des
systèmes de Lindenmayer dans des fenêtres de dimensions quelconques. La figure
5.28 montre une courbe de Koch quadratique dans un canevas rectangulaire
automatiquement mise aux “bonnes” dimensions et placée correctement au milieu
du rectangle.
Ces adaptations sont très simples. D’abord nous devons lancer le calcul de la
boîte englobante, initialer la variable d’instance longueur et positionner la tortue
aux coordonées initiales calculées. Ces activités doivent se situer entre la
génération de la chaîne de caractères correspondant à la description de la courbe
et son interprétation géométrique. Ceci nous donne la nouvelle version de la
méthode lanceLsystem que voici:
Lsystem>>lanceLsystem
| tmp tmp1 |
self initImage.
tmp := self remplace: axiome times: niveau.
self boiteMin: tmp.
tmp1 := self normalise.
pen place: tmp1 origin.
longueur := tmp1 corner x.
self automate: tmp.
self afficheImage
|
|
Ensuite, nous devons éliminer dans la méthode lsystem la lecture interactive
d’une valeur pour la variable longueur: nous n’en avons plus besoin maintenant,
elle est calculée automatiquement par normalise. Pour simplifier l’interaction
avec l’utilisateur, changeons aussi la lecture des règles de dérivation de manière
à l’initialiser avec une règle lors de la lecture de la première règle, et
de l’initialiser à la chaîne vide lors des lectures suivantes. Ce qui nous
donne:
Lsystem>>lSystem
| tmp |
axiome := FillInTheBlank request: ’axiome?’
initialAnswer: ’F’.
reglesDerivation := Dictionary new.
tmp := FillInTheBlank request: ’derivation 1 ?’
initialAnswer: ’F:F+F--F+F’.
tmp := tmp copyWithout: $ .
tmp = ’’
ifFalse: [reglesDerivation
at: (tmp at: 1)
put: (tmp copyFrom: 3 to: tmp size).
self
getDerivations: (tmp = ’’
ifTrue: [1]
ifFalse: [2])].
angle := (FillInTheBlank request: ’angle?’
initialAnswer: ’60’) asNumber.
niveau := 5.
self encore
|
|
Voilà. Nous avons enlevé la première limitation, enoncée sur la page 412, de
notre interprète des systèmes de Lindenmayer. Attaquons nous alors à la
deuxième: permettre à nos règles de réécriture de contenir aussi des caractères
correpondant à des commandes concernant non pas le tracé graphique mais
l’interprète même.