Update, docs
This commit is contained in:
		
							parent
							
								
									3328393f1d
								
							
						
					
					
						commit
						4b8760c3e3
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,2 +1 @@ | |||||||
| assets | assets | ||||||
| js/main.js |  | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								Makefile
									
									
									
									
									
								
							| @ -1,10 +1,9 @@ | |||||||
| all: js | all: js | ||||||
| 
 | 
 | ||||||
| OUTPUT=js/main.js | OUTPUT=js/obja.js | ||||||
| 
 | 
 | ||||||
| js: src/* | js: src/* | ||||||
| 	rm -f ${OUTPUT} | 	rm -f ${OUTPUT} | ||||||
| 	cat src/Loader.js >> ${OUTPUT} && echo >> ${OUTPUT} | 	cat src/Loader.js >> ${OUTPUT} && echo >> ${OUTPUT} | ||||||
| 	cat src/Model.js >> ${OUTPUT} && echo >> ${OUTPUT} | 	cat src/Model.js >> ${OUTPUT} && echo >> ${OUTPUT} | ||||||
| 	cat src/main.js >> ${OUTPUT} && echo >> ${OUTPUT} |  | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										127
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,127 @@ | |||||||
|  | # OBJ augmenté | ||||||
|  | 
 | ||||||
|  | WaveFront OBJ est un format permettant d'encoder des modèles 3D de manière | ||||||
|  | simple. Cependant, il n'est pas adapté aux représentations progressives. Pour | ||||||
|  | cela, nous avons augmenté OBJ de nouvelles commandes qui permettent de modifier | ||||||
|  | le contenu préalablement déclaré. | ||||||
|  | 
 | ||||||
|  | ## Commandes | ||||||
|  | ###### Ajout d'un sommet | ||||||
|  | 
 | ||||||
|  | Comme dans le OBJ standard, pour ajouter un sommet, il suffit d'utiliser le | ||||||
|  | caractère `v` suivi des coordonnées du sommet. Par exemple : | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | v 1.0 2.0 3.0 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ###### Ajout d'une face | ||||||
|  | 
 | ||||||
|  | Comme dans le OBJ standard, pour ajouter une face, il suffit d'utiliser le | ||||||
|  | caractère `f` suivi des indices des sommets de la face. Par exemple : | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | v 0.0 0.0 0.0 | ||||||
|  | v 1.0 0.0 0.0 | ||||||
|  | v 1.0 1.0 0.0 | ||||||
|  | f 1 2 3 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | **Attention :** en OBJ, les indices commencent à partir de 1 | ||||||
|  | 
 | ||||||
|  | **Attention :** dans notre logiciel, seule les faces triangulaires sont supportées. | ||||||
|  | 
 | ||||||
|  | ###### Edition d'un sommet | ||||||
|  | 
 | ||||||
|  | Notre format OBJ permet la modification d'un ancien sommet. Pour modifier un | ||||||
|  | sommet, il suffit d'utiliser les caractères `ev` suivis de l'indice du sommet à | ||||||
|  | modifier puis de ses nouvelles coordonées. Par exemple : | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | v 0.0 0.0 0.0 | ||||||
|  | ev 1 1.0 1.0 1.0 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ###### Edition d'une face | ||||||
|  | 
 | ||||||
|  | Notre format OBJ permet la modification d'une ancienne face. Pour modifier une | ||||||
|  | face, il suffit d'utiliser les caractères `ef` suivis de l'indice de la face à | ||||||
|  | modifier puis des indices de ses nouveaux sommets. Par exemple : | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | v 0.0 0.0 0.0 | ||||||
|  | v 1.0 0.0 0.0 | ||||||
|  | v 1.0 1.0 0.0 | ||||||
|  | v 1.0 1.0 1.0 | ||||||
|  | f 1 2 3 | ||||||
|  | ef 1 1 2 4 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ###### Suppression d'une face | ||||||
|  | Notre format OBJ permet la suppression d'une ancienne face. Pour supprimer une | ||||||
|  | face, il suffit d'utiliser les caracètres `df` suivis de l'indice de la face à | ||||||
|  | supprimer. Par exemple : | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | v 0.0 0.0 0.0 | ||||||
|  | v 1.0 0.0 0.0 | ||||||
|  | v 1.0 1.0 0.0 | ||||||
|  | v 1.0 1.0 1.0 | ||||||
|  | f 1 2 3 | ||||||
|  | df 1 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | **Attention :** les indices des faces ne sont pas changés après la suppression | ||||||
|  | d'une ancienne face. | ||||||
|  | 
 | ||||||
|  | ###### Triangle strips et triangle fans | ||||||
|  | Pour la compression de contenu 3D, on utilise souvent des [Triangle | ||||||
|  | Strips](https://en.wikipedia.org/wiki/Triangle_strip) et des [Triangle | ||||||
|  | Fans](https://en.wikipedia.org/wiki/Triangle_fan). | ||||||
|  | 
 | ||||||
|  | Notre format OBJ augmenté permet la déclaration de strips et de fans en | ||||||
|  | utilisant respectivement les caractères `ts` et `tf` suivis des indices des | ||||||
|  | sommets. Par exemple : | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | v -1.0 0.0 0.0 | ||||||
|  | v -0.5 1.0 0.0 | ||||||
|  | v 0.0 0.0 0.0 | ||||||
|  | v 0.5 1.0 0.0 | ||||||
|  | v 1.0 0.0 0.0 | ||||||
|  | ts 1 2 3 4 5 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ou bien | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | v 0.0 0.0 0.0 | ||||||
|  | v -1.0 0.0 0.0 | ||||||
|  | v -0.707 0.707 0.0 | ||||||
|  | v 0.0 1.0 0.0 | ||||||
|  | v 0.707 0.707 0.0 | ||||||
|  | v 1.0 0.0 0.0 | ||||||
|  | tf 1 2 3 4 5 6 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Utilisation | ||||||
|  | 
 | ||||||
|  | Vous pouvez récupérer les sources de cette application en lançant la commande | ||||||
|  | ``` | ||||||
|  | git clone https://gitea.tforgione.fr/tforgione/obja | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | À la racine de ce projet, le script `server.py` vous permet de démarrer un | ||||||
|  | server de streaming. Vous pouvez l'exécuter en lançant `./server.py`. Une fois | ||||||
|  | cela fait, vous pouvez allez sur [localhost:8000](http://localhost:8000) pour | ||||||
|  | lancer le streaming. Le navigateur télécharge progressivement les données et | ||||||
|  | les affiche. | ||||||
|  | 
 | ||||||
|  | Les modèles doivent être sauvegardés dans le dossiers `assets`, et peuvent être | ||||||
|  | visualisés en ajouter `?nom_du_modele.obj` à la fin de l'url. Par exemple, | ||||||
|  | [localhost:8000/?bunny.obj](http://localhost:8000/?bunny.obj) chargera le | ||||||
|  | modèle `bunny.obj` du dossier `assets`. Ce modèle est un modèle d'exemple, il | ||||||
|  | commence par encoder la version basse résolution du [Stanford | ||||||
|  | bunny](https://graphics.stanford.edu/data/3Dscanrep/), translate tous ses | ||||||
|  | sommets, les retranslate vers leurs positions d'origines puis supprime toutes | ||||||
|  | les faces. | ||||||
| @ -37,6 +37,7 @@ | |||||||
|         </div> |         </div> | ||||||
|         <script src="js/three.min.js"></script> |         <script src="js/three.min.js"></script> | ||||||
|         <script src="js/OrbitControls.js"></script> |         <script src="js/OrbitControls.js"></script> | ||||||
|  |         <script src="js/obja.js"></script> | ||||||
|         <script src="js/main.js"></script> |         <script src="js/main.js"></script> | ||||||
|     </body> |     </body> | ||||||
| </html> | </html> | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ animate(); | |||||||
| 
 | 
 | ||||||
| function init() { | function init() { | ||||||
| 
 | 
 | ||||||
|     let url = 'assets/' + (document.URL.split('?')[1] || "bunny_remove.obj"); |     let url = 'assets/' + (document.URL.split('?')[1] || "bunny.obj"); | ||||||
| 
 | 
 | ||||||
|     loader = new Loader(url, 1024, 20); |     loader = new Loader(url, 1024, 20); | ||||||
|     loader.start(function(elements) { |     loader.start(function(elements) { | ||||||
							
								
								
									
										313
									
								
								js/obja.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								js/obja.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,313 @@ | |||||||
|  | function fetchDataLength(path, callback) { | ||||||
|  |     let xhr = new XMLHttpRequest(); | ||||||
|  | 
 | ||||||
|  |     xhr.open('HEAD', path, true); | ||||||
|  |     xhr.onreadystatechange = function() { | ||||||
|  |         if (xhr.readyState === 4) { | ||||||
|  |             if (xhr.status === 200) { | ||||||
|  |                 callback(xhr.getResponseHeader('Content-Length')); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |     xhr.send(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function fetchData(path, start, end, callback) { | ||||||
|  |     let xhr = new XMLHttpRequest(); | ||||||
|  | 
 | ||||||
|  |     xhr.open('GET', path, true); | ||||||
|  |     xhr.setRequestHeader('Range', 'bytes=' + start + "-" + (end - 1)); | ||||||
|  |     xhr.onreadystatechange = function() { | ||||||
|  |         if (xhr.readyState === 4) { | ||||||
|  |             if (xhr.status === 206) { | ||||||
|  |                 callback(xhr.responseText); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |     xhr.send(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function parseLine(line) { | ||||||
|  |     let element = {}; | ||||||
|  |     let split = line.split(/[ \t]+/); | ||||||
|  | 
 | ||||||
|  |     if (split.length === 0) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     switch (split[0]) { | ||||||
|  |         case "v": | ||||||
|  |             element.type = Element.AddVertex; | ||||||
|  |             element.value = new THREE.Vector3( | ||||||
|  |                 parseFloat(split[1]), | ||||||
|  |                 parseFloat(split[2]), | ||||||
|  |                 parseFloat(split[3]), | ||||||
|  |             ); | ||||||
|  |             return element; | ||||||
|  | 
 | ||||||
|  |         case "f": | ||||||
|  |             element.type = Element.AddFace; | ||||||
|  |             element.value = new THREE.Face3( | ||||||
|  |                 parseInt(split[1], 10) - 1, | ||||||
|  |                 parseInt(split[2], 10) - 1, | ||||||
|  |                 parseInt(split[3], 10) - 1, | ||||||
|  |             ); | ||||||
|  |             return element; | ||||||
|  | 
 | ||||||
|  |         case "ts": | ||||||
|  |             element.type = Element.AddTriangleStrip; | ||||||
|  |             element.value = []; | ||||||
|  |             for (let i = 1; i < split.length - 2; i++) { | ||||||
|  |                 element.value.push(new THREE.Face3( | ||||||
|  |                     parseInt(split[i]  , 10) - 1, | ||||||
|  |                     i % 2 === 1 ? parseInt(split[i+1], 10) - 1 : parseInt(split[i+2], 10) - 1, | ||||||
|  |                     i % 2 === 1 ? parseInt(split[i+2], 10) - 1 : parseInt(split[i+1], 10) - 1, | ||||||
|  |                 )); | ||||||
|  |             } | ||||||
|  |             return element; | ||||||
|  | 
 | ||||||
|  |         case "tf": | ||||||
|  |             element.type = Element.AddTriangleFan; | ||||||
|  |             element.value = []; | ||||||
|  |             for (let i = 1; i < split.length - 2; i++) { | ||||||
|  |                 element.value.push(new THREE.Face3( | ||||||
|  |                     parseInt(split[1]  , 10) - 1, | ||||||
|  |                     parseInt(split[i+1], 10) - 1, | ||||||
|  |                     parseInt(split[i+2], 10) - 1, | ||||||
|  |                 )); | ||||||
|  |             } | ||||||
|  |             return element; | ||||||
|  | 
 | ||||||
|  |         case "ev": | ||||||
|  |             element.type = Element.EditVertex; | ||||||
|  |             element.id = parseInt(split[1], 10) - 1; | ||||||
|  |             element.value = new THREE.Vector3( | ||||||
|  |                 parseFloat(split[2]), | ||||||
|  |                 parseFloat(split[3]), | ||||||
|  |                 parseFloat(split[4]), | ||||||
|  |             ); | ||||||
|  |             return element; | ||||||
|  | 
 | ||||||
|  |         case "ef": | ||||||
|  |             element.type = Element.EditFace; | ||||||
|  |             element.id = parseInt(split[1], 10) - 1; | ||||||
|  |             element.value = new THREE.Face3( | ||||||
|  |                 parseInt(split[2], 10) - 1, | ||||||
|  |                 parseInt(split[3], 10) - 1, | ||||||
|  |                 parseInt(split[4], 10) - 1, | ||||||
|  |             ); | ||||||
|  |             return element; | ||||||
|  | 
 | ||||||
|  |         case "df": | ||||||
|  |             element.type = Element.DeleteFace; | ||||||
|  |             element.id = parseInt(split[1], 10) - 1; | ||||||
|  |             return element; | ||||||
|  | 
 | ||||||
|  |         case "": | ||||||
|  |         case "#": | ||||||
|  |             return; | ||||||
|  | 
 | ||||||
|  |         default: | ||||||
|  |             throw new Error(split[0] + " is not a defined macro"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const Element = {}; | ||||||
|  | Element.AddVertex = "AddVertex"; | ||||||
|  | Element.AddFace = "AddFace"; | ||||||
|  | Element.AddTriangleStrip = "AddTriangleStrip"; | ||||||
|  | Element.AddTriangleFan = "AddTriangleFan"; | ||||||
|  | Element.EditVertex = "EditVertex"; | ||||||
|  | Element.EditFace = "EditFace"; | ||||||
|  | Element.DeleteFace = "DeleteFace"; | ||||||
|  | 
 | ||||||
|  | class Loader { | ||||||
|  |     constructor(path, chunkSize = 1024, timeout = 20) { | ||||||
|  |         this.path = path; | ||||||
|  |         this.chunkSize = chunkSize; | ||||||
|  |         this.timeout = timeout; | ||||||
|  |         this.currentByte = 0; | ||||||
|  |         this.remainder = ""; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     start(callback) { | ||||||
|  |         fetchDataLength(this.path, (length) => { | ||||||
|  |             this.dataLength = length; | ||||||
|  |             this.next(callback); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     percentage() { | ||||||
|  |         return 100 * this.currentByte / this.dataLength; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     next(callback) { | ||||||
|  |         this.downloadAndParseNextChunk((data) => { | ||||||
|  |             callback(data); | ||||||
|  |             setTimeout(() => { | ||||||
|  |                 this.next(callback); | ||||||
|  |             }, this.timeout); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     downloadAndParseNextChunk(callback) { | ||||||
|  | 
 | ||||||
|  |         let upperBound = Math.min(this.currentByte + this.chunkSize, this.dataLength); | ||||||
|  | 
 | ||||||
|  |         if (upperBound <= this.currentByte) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         fetchData(this.path, this.currentByte, upperBound, (data) => { | ||||||
|  | 
 | ||||||
|  |             this.currentByte = upperBound; | ||||||
|  | 
 | ||||||
|  |             let elements = []; | ||||||
|  |             let split = data.split('\n'); | ||||||
|  |             split[0] = this.remainder + split[0]; | ||||||
|  |             this.remainder = split.pop(); | ||||||
|  | 
 | ||||||
|  |             for (let line of split) { | ||||||
|  |                 elements.push(parseLine(line)); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             callback(elements); | ||||||
|  | 
 | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class Model extends THREE.Mesh { | ||||||
|  |     constructor(path) { | ||||||
|  |         let geometry = new THREE.Geometry(); | ||||||
|  |         let materials = [ | ||||||
|  |             new THREE.MeshLambertMaterial( { color: 0xffffff, side: THREE.DoubleSide } ), | ||||||
|  |             new THREE.MeshBasicMaterial( { transparent: true, opacity: 0 } ) | ||||||
|  |         ]; | ||||||
|  |         super(geometry, materials); | ||||||
|  |         this.frustumCulled = false; | ||||||
|  |         this.path = path; | ||||||
|  |         this.vertices = []; | ||||||
|  |         this.currentLine = 1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     throwError(message) { | ||||||
|  |         let e = new Error("In " + this.path + ":L" + this.currentLine + " " + message); | ||||||
|  |         e.type = "custom"; | ||||||
|  |         throw e; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     checkVertex(id) { | ||||||
|  |         if (this.geometry.vertices[id] === undefined) { | ||||||
|  |             this.throwError("EditVertex requires vertex " + (id + 1) + " but there is no such vertex"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     checkFaceId(id) { | ||||||
|  |         if (this.geometry.faces[id] === undefined) { | ||||||
|  |             this.throwError("EditFace requires face " + (id + 1) + " but there is no such face"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     checkFace(f) { | ||||||
|  |         let vertices = this.geometry.vertices; | ||||||
|  | 
 | ||||||
|  |         if (vertices[f.a] === undefined) { | ||||||
|  |             this.throwError("Face requires vertex " + (f.a + 1) + " but there is no such vertex"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (vertices[f.b] === undefined) { | ||||||
|  |             this.throwError("Face requires vertex " + (f.b + 1) + " but there is no such vertex"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (vertices[f.c] === undefined) { | ||||||
|  |             this.throwError("Face requires vertex " + (f.c + 1) + " but there is no such vertex"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     manageElement(element) { | ||||||
|  | 
 | ||||||
|  |         let vertices = this.geometry.vertices; | ||||||
|  |         let f, normal; | ||||||
|  | 
 | ||||||
|  |         switch (element.type) { | ||||||
|  |             case Element.AddVertex: | ||||||
|  |                 this.geometry.vertices.push(element.value); | ||||||
|  |                 this.geometry.verticesNeedUpdate = true; | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             case Element.EditVertex: | ||||||
|  |                 this.checkVertex(element.id); | ||||||
|  |                 this.geometry.vertices[element.id].copy(element.value); | ||||||
|  |                 this.geometry.verticesNeedUpdate = true; | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             case Element.AddFace: | ||||||
|  | 
 | ||||||
|  |                 f = element.value; | ||||||
|  |                 this.checkFace(f); | ||||||
|  |                 normal = | ||||||
|  |                     vertices[f.b].clone().sub(vertices[f.a]) | ||||||
|  |                         .cross(vertices[f.c].clone().sub(vertices[f.a])); | ||||||
|  |                 normal.normalize(); | ||||||
|  | 
 | ||||||
|  |                 f.normal = normal; | ||||||
|  |                 f.materialIndex = 0; | ||||||
|  |                 this.geometry.faces.push(f); | ||||||
|  |                 this.geometry.elementsNeedUpdate = true; | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             case Element.AddTriangleStrip: | ||||||
|  |             case Element.AddTriangleFan: | ||||||
|  | 
 | ||||||
|  |                 for (let f of element.value) { | ||||||
|  | 
 | ||||||
|  |                     this.checkFace(f); | ||||||
|  |                     let normal = | ||||||
|  |                         vertices[f.b].clone().sub(vertices[f.a]) | ||||||
|  |                         .cross(vertices[f.c].clone().sub(vertices[f.a])); | ||||||
|  |                     normal.normalize(); | ||||||
|  | 
 | ||||||
|  |                     f.normal = normal; | ||||||
|  |                     f.materialIndex = 0; | ||||||
|  |                     this.geometry.faces.push(f); | ||||||
|  | 
 | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 this.geometry.elementsNeedUpdate = true; | ||||||
|  | 
 | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             case Element.EditFace: | ||||||
|  | 
 | ||||||
|  |                 f = element.value; | ||||||
|  |                 this.checkFaceId(element.id); | ||||||
|  |                 this.checkFace(f); | ||||||
|  |                 normal = | ||||||
|  |                     vertices[f.b].clone().sub(vertices[f.a]) | ||||||
|  |                         .cross(vertices[f.c].clone().sub(vertices[f.a])); | ||||||
|  |                 normal.normalize(); | ||||||
|  | 
 | ||||||
|  |                 f.normal = normal; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |                 this.geometry.faces[element.id] = f; | ||||||
|  |                 this.geometry.elementsNeedUpdate = true; | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             case Element.DeleteFace: | ||||||
|  |                 this.geometry.faces[element.id].materialIndex = 1; | ||||||
|  |                 this.geometry.elementsNeedUpdate = true; | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             default: | ||||||
|  |                 throw new Error("unknown element type: " + element.type); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.currentLine++; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user