File

src/app/models/cabri-integration-jeujustepoint.ts

Properties

positionBottomY
positionBottomY: number
positionLeftX
positionLeftX: number
positionRightX
positionRightX: number
positionTopY
positionTopY: number
import { CabriIntegration } from "./cabri-integration";
import { CabriDataService } from "../services/cabri-data.service";
import { GlobalService } from "../services/global.service";
import { NgZone, Renderer2 } from "@angular/core";
import { JeuJustePointPage, justePointActivity, operation } from "../page/jeu-juste-point/jeu-juste-point.page";
import {
	BoundingBox,
	DynamicTexture,
	Mesh,
	PickingInfo,
	StandardMaterial,
	Vector3,
	SubMesh,
	AssetsManager,
	AssetContainer,
	AbstractAssetTask,
	MeshAssetTask,
	ImageAssetTask,
	TextureAssetTask,
	AbstractMesh,
	Texture,
	MultiMaterial,
	Observer,
	PointerInfo,
	Animatable,
	Color3,
	Material,
	HighlightLayer,
	EventState,
	Animation,
	Scene,
	MeshBuilder
} from "babylon4.1";
import { AppUtils, Fraction } from "../app-utils";
import { BabylonJsConfetti, ParticleAnimName } from "./babylonjs-confetti";
// import { JeuJustePointParticles } from "mathjax-angular/types";
import { Subject } from "rxjs";
import { Button, Vector2WithInfo } from "@babylonjs/gui";
import { Control } from "gui4.1";

declare var window: {
	store: any;
	document: any;
	innerWidth: any;
	innerHeight: any;
	outerWidth: any;
	outerHeight: any;
	URL: any;
	btoa: any;
	devicePixelRatio: number;
};

export class CollisionPosition {
	positionTopY: number;
	positionBottomY: number;
	positionLeftX: number;
	positionRightX: number;
}

export class TargetMesh extends Mesh {
	value: number | Fraction;
	rangeValue: any;
	draggable: boolean;
	index: number;
	color: string;
	remediation: boolean;
	initialPosition: Vector3;
	visible: boolean;
	diffuseTexture: any;
}

export class TargetSubMesh extends SubMesh {
	value: number | Fraction;
	visible: boolean;
}
export enum DistanceUnit {
	Mm = "Mm",
	Dm = "Dm",
	Cm = "Cm",
	M = "M",
	Dam = "Dam",
	Hm = "Hm",
	Km = "km"
}

declare var MathJax: {
	startup: any;
	svg: any;
	tex: any;
	tex2svg: any;
	tex2svgPromise: any;
	loader: any;
	Hub: any;
	config: any;
	tex2mml: any;
	defsGlobalCaching: any;
};

declare var BABYLON;

export interface UserAnswer {
	success: boolean;
	response: number | Fraction | ResultOnTheRoad;
	reversed?: boolean;
}

export class ResultOnTheRoad {
	constructor(error: { color: string; correctValue: number | Fraction }[]) {
		this.carsError = error;
	}
	carsError: { color: string; correctValue: number | Fraction }[];
}
export class CabriIntegrationJeuJustePoint extends CabriIntegration {
	generatedMeshElement: Mesh[] = [];
	stepXRange: number;
	bigScaleBars: TargetMesh[] = [];
	smallScaleBars: TargetMesh[] = [];
	tubesMesh: Mesh[] = [];
	targetMeshBrokenBasket: TargetMesh[] = [];
	targetMeshHoop: TargetMesh[] = [];
	background: Mesh;
	targetSubMesh: TargetSubMesh[] = [];
	targetMesh: TargetMesh[] = [];
	linesXPos: number;
	nbBasketItemsRounded: number;
	basketTotalRange: number;
	KPosttargetMesh: TargetMesh[] = [];
	draggablePosition: Vector3;
	basketBallMesh: TargetMesh;
	defaultBallPosition: Vector3;
	defaultBallRotation: Vector3;
	defaultBallScaling: Vector3;
	assetsManagerOnTheRoad: AssetsManager;
	assetsContainerOnTheRoad: AssetContainer;
	assetsManagerBasket: AssetsManager;
	assetsContainerBasket: AssetContainer;
	images: Map<string, HTMLImageElement>;
	textures: Map<string, Texture>;
	materials: Map<string, StandardMaterial>;
	meshs: Map<string, AbstractMesh>;
	pointerObs: Observer<PointerInfo>;
	numberOfElementsDisplayed = new Array();
	currentMesh: Mesh;
	fractionResult: string;
	linePositionY: number;
	hl: HighlightLayer;
	basketRatioElement: number;
	remediationTargettedIndex: number;
	brokenbasket: Mesh;
	hoop: Mesh;
	basketUnitStepPrecision: number;
	hoopNetObservable: Observer<Scene>;

	public waitUserAnswer: Subject<UserAnswer>;

	private babylonLoaded: any;
	advancedTexture;
	bubble;
	textBlock;
	consigne: string[];
	buttonValider: Button;
	color: { color: Color3; name: string }[];
	alpha: number;
	remediationPlanes: Mesh[];
	initialMaterials: Material[];
	mascotteRotation: Vector3;
	consigneScenariosArray: string[];
	lastCalled: number;

	constructor(
		public cabriService: CabriDataService,
		public globalService: GlobalService,
		public _ngZone: NgZone,
		public page: JeuJustePointPage,
		public renderer: Renderer2
	) {
		super(cabriService, globalService, _ngZone, page, renderer);
		this.page = page;
		this.cabriRenderStarted = true;
		// (window as any).justePoint = this;
	}

	onBabylonReady(): Promise<void> {
		return new Promise<void>(async (resolve, reject) => {
			await super.onBabylonReady();
			await this.waitCamera();
			this.customLoadingScreen();
			await this.importMascotteMeshes(1.14, 1.2).then(() => {
				const mascotteXUnit = 0.1;
				this.mascotteRotation = new Vector3(
					this.initialCameraPosition.beta + mascotteXUnit - this.initialCameraPosition.beta,
					0,
					0
				);
			});
			this.setOrthographicCamera();
			this.addEventPointerOnCanvas();
			await this.updateCabriPosition(this.cabriService.holoMode);
			// this.scene.debugLayer.show({ embedMode: true });
			this.enableDragAndDrop();
			this.saveInitialSceneMaterial();
			await this.launchActivity();
			resolve();
		});
	}

	saveInitialSceneMaterial() {
		this.initialMaterials = this.scene.materials.slice();
		this.initialMaterials = this.initialMaterials.concat(this.scene.multiMaterials.slice());
	}

	addEventPointerOnCanvas() {
		super.addEventPointerOnCanvas();
		(window.store.getters.cabri.Canvas as HTMLCanvasElement).eventListeners("wheel").forEach(eventListener => {
			(window.store.getters.cabri.Canvas as HTMLCanvasElement).removeEventListener("wheel", eventListener);
		});
	}

	async launchActivity() {
		if (this.page.variable.activity === justePointActivity.basket) {
			await this.buildElementActivityBasket();
		} else {
			await this.buildElementActivityOnTheRoad();
		}
	}

	getBackground(): Mesh {
		if (!this.background) {
			window.store.getters.cabri.Scene.meshes.forEach(async m => {
				if (m.id === "background") {
					this.background = m;
				}
			});
		}
		return this.background;
	}

	getCurrentOperation(): string {
		let text: string;
		if (!this.page.firstTime) {
			switch (this.page.justePointExercices.exoType) {
				case justePointActivity.basket:
					const pasApproximation = this.page.variable.chooseExactNumber ? "Oui" : "Non";
					text =
						"Activité Juste point -Basket- Difficulté : " +
						this.page.variable.difficulty +
						", Pas d'approximation : " +
						pasApproximation +
						",  Borne Min : " +
						this.page.variable.nbMin.toString() +
						", Borne Max : " +
						this.page.variable.nbMax.toString() +
						", Pas : " +
						this.page.variable.unitStep.toString() +
						", Resultat : ";
					if (Array.isArray(this.page.variable.expectedResult)) {
						this.page.variable.expectedResult.forEach(resultat => {
							text += "'" + resultat.toString() + "'";
						});
					} else {
						text += "'" + this.page.variable.expectedResult.toString() + "'";
					}
					break;
				case justePointActivity.onTheRoad:
					text =
						"Activité Juste point -Route- Difficulté : " +
						this.page.variable.difficulty +
						", Pas de ligne centrale : " +
						this.page.variable.noRoadHelp +
						",  Borne Min : " +
						this.page.variable.nbMin.toString() +
						", Borne Max : " +
						this.page.variable.nbMax.toString() +
						", Pas : " +
						this.page.variable.unitStep.toString() +
						" Resultat : ";
					if (Array.isArray(this.page.variable.expectedResult)) {
						this.page.variable.expectedResult.forEach(resultat => {
							text += "'" + resultat.toString() + "'";
						});
					} else {
						text += "'" + this.page.variable.expectedResult.toString() + "'";
					}
					break;
				default:
					break;
			}
		}
		return text;
	}

	buildElementActivityBasket(): Promise<void> {
		return new Promise(resolve => {
			this.importAssetBasket().then(async () => {
				this.brokenbasket = this.meshs.get("Backboard") as Mesh;
				this.hoop = this.meshs.get("Hoop") as Mesh;
				this.page.expectedResultIsFractionHTML = this.page.variable?.expectedResult instanceof Fraction;
				// this.basketUnitStepPrecision = this.getStepPrecision();
				// console.log("this.basketUnitStepPrecision", this.basketUnitStepPrecision);
				this.generateBasketBall();
				await this.generateStraightLine(22);
				this.generateBrokenBasket();
				this.assetsContainerBasket.addAllToScene();
				resolve();
			});
		});
	}

	importAssetBasket(): Promise<void> {
		this.assetsManagerBasket = new BABYLON.AssetsManager(this.scene);
		this.assetsManagerBasket.useDefaultLoadingScreen = false;
		this.assetsContainerBasket = new BABYLON.AssetContainer(this.scene);
		this.importMeshBasket();
		this.importTexturesBasket();
		this.assetsManagerBasket.onFinish = this.importFinishBasket.bind(this);
		return this.assetsManagerBasket.loadAsync();
	}

	importTexturesBasket() {
		const textureMap = [
			{ name: "brokenBasket", url: "assets/gabarits/jeu_juste_point/backboard.png" },
			{ name: "brokenBasketCurrent", url: "assets/gabarits/jeu_juste_point/backboardCurrent.png" }
		];

		textureMap.forEach(texture => {
			this.assetsManagerBasket.addTextureTask(texture.name, texture.url);
		});
	}

	importMeshBasket() {
		const mMeshs = [
			{ name: "basket_ball", url: "/assets/gabarits/jeu_juste_point/", filename: "basket_ball.babylon" },
			{
				name: "hoop",
				url: "/assets/gabarits/jeu_juste_point/",
				filename: "hoop" + (this.globalService.lowPerformanceMode ? "_ios" : "") + ".babylon"
			}
		];

		mMeshs.forEach(mesh => {
			this.assetsManagerBasket.addMeshTask(mesh.name, null, mesh.url, mesh.filename);
		});
	}

	importFinishBasket(tasks: AbstractAssetTask[]) {
		this.textures = new Map<string, Texture>();
		this.images = new Map<string, HTMLImageElement>();
		this.meshs = new Map<string, AbstractMesh>();
		tasks.forEach(task => {
			if (task.isCompleted) {
				if (task instanceof BABYLON.MeshAssetTask) {
					const lTask = task as MeshAssetTask;
					this.assetsContainerBasket.meshes = this.assetsContainerBasket.meshes.concat(lTask.loadedMeshes);
					lTask.loadedMeshes.forEach(mesh => {
						this.meshs.set(mesh.name, mesh);
						if (mesh.material) {
							if (mesh.material instanceof BABYLON.MultiMaterial) {
								this.assetsContainerBasket.multiMaterials.push(mesh.material as MultiMaterial);
							} else {
								this.assetsContainerBasket.materials.push(mesh.material);
								this.assetsContainerBasket.textures = this.assetsContainerBasket.textures.concat(
									mesh.material.getActiveTextures()
								);
							}
						}
					});
				} else if (task instanceof BABYLON.ImageAssetTask) {
					const lTask = task as ImageAssetTask;
					this.images.set(task.name, lTask.image);
				} else if (task instanceof BABYLON.TextureAssetTask) {
					const lTask = task as TextureAssetTask;
					this.assetsContainerBasket.textures.push(lTask.texture);
					this.textures.set(lTask.name, lTask.texture);
				} else {
					console.error("Task Not Handle", task);
				}
			} else {
				console.error(task.errorObject);
			}
		});
	}

	/**
	 * RESIZE CABRI (from CM) -> Update DOM Cabri elements
	 * @param holoMode Holo mode
	 * @param platform Platform
	 */
	async updateCabriPosition(holoMode: string = null): Promise<void> {
		if (this.page.variable?.activity === justePointActivity.basket) {
			this.cabriService.forceBackgroundTexture =
				"/assets/gabarits/jeu_juste_point/background_basket" + (this.globalService.lowPerformanceMode ? "_ios" : "") + ".jpg";
		} else {
			this.cabriService.forceBackgroundTexture = null;
		}
		return await super.updateCabriPosition(holoMode, "jeuJustePoint").then(()=>this.setBubbleTopPosition());
	}
	async updateCabriBackground(renderCanvas, condition) {
		window.store.getters.cabri.Scene.meshes.forEach(async m => {
			if (m.id === "background") {
				this.background = m;

				this.checkFlatHoloMode(m);
				await this.switchBackgroundLauncher(m, renderCanvas.clientHeight, renderCanvas.clientWidth);
				console.log("cabri background resized");
				this.setBackgroundPosition();
			}
		});
	}

	/**
	 * Remediation meshes to be visible
	 */
	keepRemediationElements() {
		if (this.page.variable.activity === justePointActivity.basket) {
			this.assetsContainerBasket.meshes.forEach((currentMesh: TargetMesh) => {
				currentMesh.isVisible = currentMesh.remediation ? true : false;
			});
		}
	}

	/**
	 * Show or not meshes according to remediation/newQuestion/scenario/mascotte anim
	 */
	displayMeshes(show: boolean) {
		if (this.page.variable.activity === justePointActivity.basket) {
			if (this.assetsContainerBasket) {
				this.assetsContainerBasket.meshes.forEach((currentMesh: TargetMesh) => {
					currentMesh.isVisible = show;
				});
			}
		} else {
			if (this.assetsContainerOnTheRoad) {
				this.assetsContainerOnTheRoad.meshes.forEach((currentMesh: TargetMesh) => {
					currentMesh.isVisible = show;
					currentMesh.getChildMeshes(false).forEach(element => {
						element.isVisible = show;
					});
				});
				if (this.advancedTexture) {
					this.advancedTexture.rootContainer.isVisible = show;
				}
			}
		}
	}

	checkActivityChange(): boolean {
		return true;
	}

	saveDom() {
		super.saveDom();
		this.cabriService.currentSavedDom = "jeuJustePoint";
	}

	restoreDom() {
		super.restoreDom();
		this.startRender();
	}

	clearResponse(mode: string, value: any): Promise<any> {
		return;
	}

	setResponse(mode: string, value: any) {
		// display response
	}

	onScenereadyInit() {
		this.scene = window.store.getters.cabri.Scene;
		this.engine = window.store.getters.cabri.Engine;
	}

	/**
	 * Get background mesh picked point infos when mouse is over the mesh
	 */
	getGroundPosition(): Vector3 {
		const pickinfo = this.scene.pick(
			this.scene.pointerX,
			this.scene.pointerY,
			this.page.variable.activity === justePointActivity.onTheRoad ? this.isRoadGroundMesh : null,
			true,
			this.page.variable.activity === justePointActivity.onTheRoad ? this.camera : this.cameraHud
		);
		if (pickinfo.hit) {
			return pickinfo.pickedPoint;
		}

		return null;
	}

	isRoadGroundMesh(mesh: AbstractMesh) {
		const elligibleBackgroundMesh = ["leftRoad", "rightRoad", "upperGround", "lowerGround", "road"];
		return elligibleBackgroundMesh.includes(mesh.name);
	}

	pointerDown(mesh: Mesh) {
		this.currentMesh = mesh;
		this.draggablePosition = this.getGroundPosition();
	}

	pointerMove() {
		if (this.currentMesh && this.draggablePosition) {
			const current = this.getGroundPosition();

			if (current) {
				const diff = current.subtract(this.draggablePosition);

				this.currentMesh.position.addInPlace(diff);

				this.draggablePosition = current;
			}
			if (this.page.variable.activity === justePointActivity.basket) {
				this.checkHoopCollisionResult(false);
			}
		} else if (this.currentMesh) {
			const isValidPosition = this.getGroundPosition();
			if (isValidPosition) {
				const diff = isValidPosition.subtract(this.currentMesh.position);
				if (Math.abs(diff.y) > 0) {
					diff.z = +diff.y;
					diff.y = 0;
				}
				this.currentMesh.position.addInPlace(diff);
				this.draggablePosition = isValidPosition;
			}
		}
	}

	async pointerUp() {
		this.draggablePosition = null;
		if (this.currentMesh) {
			if (this.page.variable.activity === justePointActivity.basket) {	
				this.checkHoopCollisionResult(true);
				if (this.page.variable.chooseExactNumber) {
					this.checkBigBarsCollisionResult(false, true);
				} else {
					this.checkSmallBarsCollisionResult();
				}
			} else {
				// Todo make road collision
				this.checkRoadCollision();
			}
			this.currentMesh = null;
		}
	}

	/**
	 * Collision and define of correct answer
	 */
	checkHoopCollisionResult(validateAnswer: boolean) {
		// const sourceMeshPosition = this.getBallPosition();
		const souceMesh = this.getBallPosition(true);

		for (const mesh of this.targetMeshBrokenBasket) {
			const sizeMesh = this.getMeshSizeInfos(mesh);
			const targetMesh = {
				positionTopY: mesh.position.y + (sizeMesh.extendSizeWorld.y * 2) / 1.2,
				positionBottomY: mesh.position.y,
				positionLeftX: mesh.position.x - sizeMesh.extendSizeWorld.x,
				positionRightX: mesh.position.x + sizeMesh.extendSizeWorld.x
			};
			const collision = this.checkResponse(targetMesh, souceMesh);
			if (collision) {
				if ((mesh.material as any).diffuseTexture.name !== this.textures.get("brokenBasketCurrent").url) {
					mesh.material = this.swapBrokenBasketMaterial("brokenBasketCurrent");
				}
				if (validateAnswer) {
					mesh.material = this.swapBrokenBasketMaterial("brokenBasket");
					this.manageBallAnimation(mesh);
				}
				break;
			} else {
				// Remove all materials
				for (const currMesh of this.targetMeshBrokenBasket) {
					if ((currMesh.material as any).diffuseTexture.name !== this.textures.get("brokenBasket").url) {
						currMesh.material = this.swapBrokenBasketMaterial("brokenBasket");
					}
				}
			}
		}
	}

	checkSmallBarsCollisionResult() {
		const touchedMesh = this.checkBigBarsCollisionResult(true, false);

		if (touchedMesh) {
			this.manageBallAnimation(touchedMesh, true);
		} else {
			const souceMesh = this.getBallPosition(false);
			for (const mesh of this.smallScaleBars) {
				const sizeMesh = this.getMeshSizeInfos(mesh);
				const targetMeshPosition = {
					positionTopY: mesh.position.y + sizeMesh.extendSizeWorld.y,
					positionBottomY: mesh.position.y - sizeMesh.extendSizeWorld.y,
					positionLeftX: mesh.position.x - sizeMesh.extendSizeWorld.x,
					positionRightX: mesh.position.x + sizeMesh.extendSizeWorld.x / 2 // half distance from right avoid overlap two meshes
				};
				const responded = this.checkResponse(targetMeshPosition, souceMesh, mesh);
				if (responded) {
					this.manageBallAnimation(mesh);
					break;
				}
			}
		}
	}

	checkBigBarsCollisionResult(ballCenterPosition: boolean, launchAnim: boolean) {
		const souceMesh = this.getBallPosition(false);
		if (ballCenterPosition) {
			const targetSize = this.getMeshSizeInfos(this.bigScaleBars[0]);
			souceMesh.positionLeftX = this.basketBallMesh.position.x + targetSize.extendSizeWorld.x;
			souceMesh.positionRightX = this.basketBallMesh.position.x - targetSize.extendSizeWorld.x;
		}

		for (const mesh of this.bigScaleBars) {
			const sizeMesh = this.getMeshSizeInfos(mesh);
			const targetMeshPosition = {
				positionTopY: mesh.position.y + sizeMesh.extendSizeWorld.y,
				positionBottomY: mesh.position.y - sizeMesh.extendSizeWorld.y,
				positionLeftX: mesh.position.x - sizeMesh.extendSizeWorld.x,
				positionRightX: mesh.position.x + sizeMesh.extendSizeWorld.x
			};

			const responded = this.checkResponse(targetMeshPosition, souceMesh, mesh);
			if (responded) {
				if (launchAnim) {
					this.manageBallAnimation(mesh);
					break;
				} else {
					return mesh;
				}
			}
		}
	}

	/**
	 * Collision between ball and hoop
	 */
	checkResponse(targetMesh: CollisionPosition, souceMesh: CollisionPosition, mesh?) {
		if (
			souceMesh.positionTopY >= targetMesh.positionBottomY &&
			souceMesh.positionBottomY <= targetMesh.positionTopY &&
			souceMesh.positionLeftX >= targetMesh.positionLeftX &&
			souceMesh.positionRightX <= targetMesh.positionRightX
		) {
			return true;
		}
	}

	getBallPosition(ballCenterOrigin: boolean): CollisionPosition {
		const currentMesh = this.getMeshSizeInfos(this.basketBallMesh);
		let positionLeftX: number;
		let positionRightX: number;
		if (ballCenterOrigin) {
			positionRightX = positionLeftX = this.basketBallMesh.position.x;
		} else {
			positionLeftX = this.basketBallMesh.position.x + currentMesh.extendSizeWorld.x;
			positionRightX = this.basketBallMesh.position.x - currentMesh.extendSizeWorld.x;
		}
		return {
			positionLeftX,
			positionRightX,
			positionBottomY: this.basketBallMesh.position.y - currentMesh.extendSizeWorld.y,
			positionTopY: this.basketBallMesh.position.y + currentMesh.extendSizeWorld.y
		};
	}

	async manageBallAnimation(mesh: TargetMesh, forceExactValue: boolean = false) {
		const { success, targettedBrokenMesh } = this.checkResponsedMeshValidity(mesh, forceExactValue);
		// remove pointer observable
		this.scene.onPointerObservable.remove(this.pointerObs);
		this.basketBallMesh.position = this.defaultBallPosition.clone();
		this.basketBallMesh.rotation = this.defaultBallRotation.clone();
		this.basketBallMesh.scaling = this.defaultBallScaling.clone();
		this.basketBallMesh.scaling.x = this.basketBallMesh.scaling.y = this.basketBallMesh.scaling.z = 5;
		const resultAnim = this.launchBallAnimation(targettedBrokenMesh, success);

		resultAnim.onAnimationEnd = async () => {
			this.basketBallMesh.animations = new Array();

			if (success) {
				this.basketBallMesh.isVisible = false;
				this.generateConfetti(targettedBrokenMesh.position);
			}
			await AppUtils.timeOut(300);
			if (this.hoopNetObservable) {
				this.scene.onAfterRenderObservable.remove(this.hoopNetObservable);
				this.hoopNetObservable = null;
			}
			this.basketBallMesh.scaling = this.defaultBallScaling.clone();
			this.basketBallMesh.position = this.defaultBallPosition.clone();
			this.basketBallMesh.rotation = this.defaultBallRotation.clone();
			this.enableDragAndDrop();
			let userAnswer;
			if (success) {
				userAnswer = this.page.variable.expectedResult.valueOf();
			} else {
				userAnswer = targettedBrokenMesh.value;
			}
			this.waitUserAnswer.next({ success, response: userAnswer });
			// enable pointer observable
		};
	}

	checkResponsedMeshValidity(mesh: TargetMesh, forceExactValue: boolean) {
		let success = false;
		let targettedBrokenMesh: TargetMesh;

		if (this.page.variable.chooseExactNumber || forceExactValue) {
			if (forceExactValue) {
				const index = this.bigScaleBars.findIndex(currMesh => {
					return currMesh.value === mesh.value;
				});
				const isLastElement = index === this.bigScaleBars.length - 1;

				targettedBrokenMesh = this.targetMeshBrokenBasket.find(currMesh => {
					return mesh.value === (isLastElement ? currMesh.rangeValue.to : currMesh.rangeValue.inital);
				});
			} else {
				targettedBrokenMesh = this.targetMeshBrokenBasket.find(currMesh => {
					return mesh.value === currMesh.value;
				});
			}
			success = Number(Number(mesh.value).toFixed(8)) === Number(Number(this.page.variable.expectedResult.valueOf()).toFixed(8)) ? true : false;
		} else {
			targettedBrokenMesh = this.targetMeshBrokenBasket.find(currMesh => {
				return mesh.rangeValue.from === currMesh.rangeValue.from;
			});
			if (
				mesh.rangeValue.from <= this.page.variable.expectedResult.valueOf() &&
				this.page.variable.expectedResult.valueOf() <= mesh.rangeValue.to
			) {
				success = true;
			}
		}

		return { success, targettedBrokenMesh };
	}

	enableDragAndDrop() {
		let draggableMesh: Mesh;
		let pickResult: PickingInfo;
		this.pointerObs = this.scene.onPointerObservable.add(pointerInfo => {
			if ((pointerInfo.event as any).preventDefault) {
				(pointerInfo.event as any).preventDefault();
			}

			switch (pointerInfo.type) {
				case BABYLON.PointerEventTypes.POINTERDOWN:
					pickResult = this.scene.pick(
						this.scene.pointerX,
						this.scene.pointerY,
						null,
						false,
						this.page.variable.activity === justePointActivity.onTheRoad ? this.camera : this.cameraHud
					);
					if (pickResult.hit) {
						draggableMesh = this.getLastParentCabri(pickResult.pickedMesh as Mesh);
						if ((draggableMesh as TargetMesh)?.draggable) {
							this.pointerDown(draggableMesh);
						}
					}
					break;
				case BABYLON.PointerEventTypes.POINTERUP:
					this.pointerUp();
					break;
				case BABYLON.PointerEventTypes.POINTERMOVE:
					this.pointerMove();
					break;
			}
		});
	}

	/**
	 * Basket - Generate basket ball
	 */
	generateBasketBall() {
		this.basketBallMesh = null;
		this.basketBallMesh = this.meshs.get("BasketballBall") as TargetMesh;
		this.basketBallMesh.renderingGroupId = 1;
		this.basketBallMesh.checkCollisions = true;
		this.basketBallMesh.isPickable = true;
		this.basketBallMesh.layerMask = this.cameraHud.layerMask;
		const scaling = 2;
		const ballPasitionY = this.globalService.isMobile ? -1 : -2;

		this.basketBallMesh.scaling.x = this.basketBallMesh.scaling.y = this.basketBallMesh.scaling.z = scaling;
		this.basketBallMesh.position = new BABYLON.Vector3(0, ballPasitionY, -10);
		this.defaultBallPosition = this.basketBallMesh.position.clone();
		this.defaultBallRotation = this.basketBallMesh.rotation.clone();
		this.defaultBallScaling = this.basketBallMesh.scaling.clone();
		this.basketBallMesh.draggable = true;
		this.assetsContainerBasket.meshes.push(this.basketBallMesh);
	}

	/**
	 * Basket - Generate two separated meshes broken basket and hoop(resize ball to hoop size during anim).
	 */
	generateBrokenBasket() {
		this.targetMeshBrokenBasket = new Array();
		this.targetMeshHoop = new Array();

		this.brokenbasket.scaling = new Vector3(0.2, 0.32, 0.15);
		// Material for inserting image for broken basket
		this.brokenbasket.material = null;
		this.brokenbasket.material = this.swapBrokenBasketMaterial("brokenBasket");
		this.brokenbasket.rotation = new Vector3(0, Math.PI / 2, -0.36);
		this.hoop.rotation = this.brokenbasket.rotation = new Vector3(0, Math.PI / 2, -0.36);
		const brokenBasketY = this.globalService.isMobile ? 2.5 : 2;
		let numberElementsToTaken = new Array();

		// Determine how many broken baskets needs to be created based of the exact mod
		if (this.page.variable.chooseExactNumber) {
			numberElementsToTaken = this.bigScaleBars;
		} else {
			numberElementsToTaken = this.smallScaleBars;
		}
		const currentSize = this.getMeshSizeInfos(this.brokenbasket);
		let middleHalfPosY = this.brokenbasket.scaling.x + this.brokenbasket.scaling.y;
		// createion of hoop and broken basket
		numberElementsToTaken.forEach((currentMesh, index) => {
			const clonedBrokenBasket = this.brokenbasket.clone(`brokenbasket${index}`) as TargetMesh;
			const hoopCloned = this.hoop.clone(`hoopbasket${index}`) as TargetMesh;
			if (this.page.variable.chooseExactNumber) {
				hoopCloned.value = clonedBrokenBasket.value = this.displayedNumber(index);
			} else {
				hoopCloned.rangeValue = clonedBrokenBasket.rangeValue = currentMesh.rangeValue;
			}
			hoopCloned.morphTargetManager = this.hoop.morphTargetManager.clone();
			clonedBrokenBasket.position = new BABYLON.Vector3(currentMesh.position.x, brokenBasketY, -5);
			hoopCloned.position = new BABYLON.Vector3(currentMesh.position.x, brokenBasketY - middleHalfPosY, -6);

			hoopCloned.layerMask = clonedBrokenBasket.layerMask = this.cameraHud.layerMask;
			this.assetsContainerBasket.meshes.push(hoopCloned, clonedBrokenBasket);
			this.targetMeshBrokenBasket.push(clonedBrokenBasket);
			this.targetMeshHoop.push(hoopCloned);
		});

		// if collision between broken baskets so make them smaller
		if (this.eachBrokenBasketsCollided) {
			const paddingBtwn = 0.01;
			const sizeInital = this.stepXRange / 10 - paddingBtwn;
			this.targetMeshBrokenBasket.forEach((eachMesh, index) => {
				const initialSizeBrokenBasket = eachMesh.scaling;
				const resizeRatio = sizeInital / initialSizeBrokenBasket.x;
				eachMesh.scaling = new Vector3(sizeInital, sizeInital * 2, sizeInital);
				this.targetMeshHoop[index].scaling = new Vector3(
					this.targetMeshHoop[index].scaling.x * resizeRatio + 0.07,
					this.targetMeshHoop[index].scaling.y * resizeRatio + 0.07,
					this.targetMeshHoop[index].scaling.z * resizeRatio + 0.07
				);
				middleHalfPosY = sizeInital + sizeInital * 2;
				this.targetMeshHoop[index].position.y = brokenBasketY - middleHalfPosY;
			});
		}
	}

	swapBrokenBasketMaterial(materialName: string): Material {
		const brokeBasketMaterial = new BABYLON.StandardMaterial("brokenBasketMat", this.scene);
		brokeBasketMaterial.diffuseTexture = new BABYLON.Texture(this.textures.get(materialName).url, this.scene);
		brokeBasketMaterial.diffuseTexture.hasAlpha = true;
		brokeBasketMaterial.alpha = 1;
		brokeBasketMaterial.useAlphaFromDiffuseTexture = true;
		return brokeBasketMaterial;
	}

	/**
	 * Basket - Determine there are collision between broken baskets
	 */
	get eachBrokenBasketsCollided() {
		const brokenbasketSize2 = this.getMeshSizeInfos(this.targetMeshBrokenBasket[0]);
		const firstBrokenBasket = this.targetMeshBrokenBasket[0].position.x + brokenbasketSize2.extendSizeWorld.x;
		const secondBrokenBasket = this.targetMeshBrokenBasket[1].position.x - brokenbasketSize2.extendSizeWorld.x;
		return firstBrokenBasket >= secondBrokenBasket;
	}

	/**
	 *Basket -  Ball initial position before starting of the animation
	 */
	getBallAnimInitalPos(targettedHoop: TargetMesh) {
		let paddingX;
		if (targettedHoop.index === this.targetMeshBrokenBasket.length - 1) {
			paddingX = -1.5;
		} else if (targettedHoop.index === this.targetMeshBrokenBasket[0].index) {
			paddingX = 1.5;
		} else {
			paddingX = Math.random() > 0.5 ? 1.5 : -1.5;
		}
		return paddingX;
	}

	/**
	 *  Basket - Ball animation after response
	 */
	launchBallAnimation(targettedHoop: TargetMesh, success = true): Animatable {
		const paddingX = this.getBallAnimInitalPos(targettedHoop);
		this.basketBallMesh.position.x = targettedHoop.position.x + paddingX;
		const frameRate = 15;
		const animationDistance = 1.5 + frameRate;
		const speed = 14;

		// ball below position from tragetted broken basket and random position (right/left)
		const ballPositionAnim = new BABYLON.Animation(
			"positionAnim",
			"position",
			speed,
			BABYLON.Animation.ANIMATIONTYPE_VECTOR3,
			BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
		);
		const positionKeys = [
			{
				frame: 0,
				value: this.basketBallMesh.position,
				outTangent: new BABYLON.Vector3(0, 1, 0)
			},
			{
				frame: animationDistance,
				value: new BABYLON.Vector3(
					success ? targettedHoop.position.x : targettedHoop.position.x - (paddingX > 0 ? 0.65 : -0.65),
					targettedHoop.position.y - 1
				),
				inTangent: new BABYLON.Vector3(0, -1, 0)
			}
		];

		// Rotation ball random speed
		const ballRotationAnim = new BABYLON.Animation(
			"rotationAnim",
			"rotation",
			speed,
			BABYLON.Animation.ANIMATIONTYPE_VECTOR3,
			BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
		);
		const rotationDirection = Math.floor(Math.random() * 10) + 4;
		const rotationKeys = [
			{
				frame: 0,
				value: new BABYLON.Vector3(0, 0, 0)
			},
			{
				frame: animationDistance,
				value: new BABYLON.Vector3(rotationDirection, rotationDirection, 0)
			}
		];

		// resize ball size to hoop size  to get depth effect
		const hoopTargetted = this.targetMeshHoop.find(mesh => {
			return mesh.value === targettedHoop.value;
		});
		const hoopMeshInfo = this.getMeshSizeInfos(hoopTargetted);
		const hoopMeshTotalSize = hoopMeshInfo.extendSizeWorld.x + 0.12;
		const ballScallingAnim = new BABYLON.Animation(
			"scallingAnim",
			"scaling",
			speed / 1.5,
			BABYLON.Animation.ANIMATIONTYPE_VECTOR3,
			BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
		);
		const scallingKeys = [
			{
				frame: 0,
				value: new BABYLON.Vector3(this.basketBallMesh.scaling.x, this.basketBallMesh.scaling.y, this.basketBallMesh.scaling.z)
			},
			{
				frame: frameRate / 1.8,
				value: new BABYLON.Vector3(hoopMeshTotalSize, hoopMeshTotalSize, hoopMeshTotalSize)
			}
		];

		ballPositionAnim.setKeys(positionKeys);
		ballRotationAnim.setKeys(rotationKeys);
		ballScallingAnim.setKeys(scallingKeys);

		hoopTargetted.morphTargetManager.getTarget(0).influence = 1;
		let morphInfluence = 1;
		let numberNetAnimFrame = 0;

		if (success) {
			this.hoopNetObservable = this.scene.onAfterRenderObservable.add(() => {
				// animate hoop net when ball net collided on render

				const hoopNetBasketCollided =
					this.basketBallMesh.position.y <= hoopTargetted.position.y + hoopMeshInfo.extendSizeWorld.y &&
					this.basketBallMesh.scaling.x <= hoopMeshTotalSize;
				if (hoopNetBasketCollided) {
					// influence value from 0 to 1
					hoopTargetted.morphTargetManager.getTarget(0).influence = morphInfluence;
					if (numberNetAnimFrame === 0) {
						// first frame
						morphInfluence -= 0.15;
						if (morphInfluence === 0 || morphInfluence <= 0) {
							numberNetAnimFrame++;
						}
					} else if (numberNetAnimFrame === 1) {
						// second frame
						morphInfluence += 0.03;
						if (morphInfluence >= 1.2) {
							numberNetAnimFrame++;
						}
					} else {
						this.scene.onAfterRenderObservable.remove(this.hoopNetObservable);
						this.hoopNetObservable = null;
					}
				}
			});
		}

		this.basketBallMesh.animations.push(ballPositionAnim, ballRotationAnim, ballScallingAnim);
		return this.scene.beginAnimation(this.basketBallMesh, 0, 2 * frameRate, false);
	}

	/**
	 * Basket - Confetti coming out from broken basket good answer
	 */
	async generateConfetti(targettedHoopPosition: Vector3) {
		const particlesRightAnswer = new BabylonJsConfetti(this, 1000, 1500, ParticleAnimName.basket, targettedHoopPosition);
		await particlesRightAnswer.runParticles();
	}

	getMeshSizeInfos(mesh): BoundingBox {
		return mesh.getBoundingInfo().boundingBox;
	}

	/**
	 * Creation of straight line
	 */
	async generateStraightLine(width: number) {
		let paddingArround = this.globalService.isMobile ? 2.5 : 2;
		this.bigScaleBars = [];
		this.smallScaleBars = [];
		this.tubesMesh = [];
		this.nbBasketItemsRounded = Math.round(this.numberItems);

		if (document.querySelector("#MJX-SVG-global-cache defs")) {
			MathJax.defsGlobalCaching = document.querySelector("#MJX-SVG-global-cache defs");
		}
		this.basketRatioElement = Math.ceil(this.nbBasketItemsRounded / 11);
		this.basketTotalRange = width - 2 * paddingArround;
		this.stepXRange = this.basketTotalRange / Math.round(this.nbBasketItemsRounded - 1);
		const paddingYTube = 2;
		this.linePositionY = this.basketBallMesh.position.y + paddingYTube;
		this.linesXPos = -this.basketTotalRange / 2;

		// create lines
		for (let index = 0; index < this.nbBasketItemsRounded; index++) {
			await this.createEachLine(index);
		}

		// creation of tube
		this.bigScaleBars.reduce((previousMesh: TargetMesh, currentMesh: TargetMesh, currentIndex: number) => {
			if (previousMesh && currentMesh) {
				const currentPath = [previousMesh.position, currentMesh.position];
				const currentTube = BABYLON.MeshBuilder.CreateTube(
					"tube" + currentIndex,
					{ path: currentPath, radius: 0.1 / this.basketRatioElement },
					this.scene
				) as TargetMesh;
				const tubeMaterial = new BABYLON.StandardMaterial("myMaterial", this.scene) as StandardMaterial;
				tubeMaterial.diffuseColor = new BABYLON.Color3(1, 1, 1);
				currentTube.material = tubeMaterial;
				currentTube.remediation = true;
				currentTube.layerMask = this.cameraHud.layerMask;
				this.tubesMesh.push(currentTube);
				this.assetsContainerBasket.meshes.push(currentTube);
			}
			return currentMesh;
		});
		this.createBackgroundTube();
	}

	createBackgroundTube() {
		// create backgound tube
		const graduatedLineBackgroundMaterial = new BABYLON.StandardMaterial("bckgrGraduatedLine", this.scene) as StandardMaterial;
		graduatedLineBackgroundMaterial.diffuseColor = BABYLON.Color3.FromHexString("#36454F");
		// graduatedLineBackgroundMaterial.emissiveColor = new BABYLON.Color3(0, 0, 0);
		const bSphereStart = BABYLON.MeshBuilder.CreateSphere(
			"bSphereStart",
			{ diameter: 0.8 / this.basketRatioElement },
			this.scene
		) as TargetMesh;
		bSphereStart.remediation = true;
		const bSphereEnd = BABYLON.MeshBuilder.CreateSphere(
			"bSphereEnd",
			{ diameter: 0.8 / this.basketRatioElement },
			this.scene
		) as TargetMesh;
		bSphereEnd.remediation = true;
		bSphereStart.layerMask = this.cameraHud.layerMask;
		bSphereEnd.layerMask = this.cameraHud.layerMask;
		bSphereStart.material = graduatedLineBackgroundMaterial;
		bSphereEnd.material = graduatedLineBackgroundMaterial;
		bSphereStart.position = new BABYLON.Vector3(-this.basketTotalRange / 2, this.linePositionY, -2);
		bSphereEnd.position = new BABYLON.Vector3(this.basketTotalRange / 2, this.linePositionY, -2);
		const path = [bSphereStart.position, bSphereEnd.position];
		const tube = BABYLON.MeshBuilder.CreateTube("bTube", { path, radius: 0.4 / this.basketRatioElement }, this.scene) as TargetMesh;
		tube.layerMask = this.cameraHud.layerMask;
		tube.material = graduatedLineBackgroundMaterial;
		tube.remediation = true;
		this.assetsContainerBasket.meshes.push(tube);
		this.assetsContainerBasket.meshes.push(bSphereStart);
		this.assetsContainerBasket.meshes.push(bSphereEnd);
	}

	/**
	 * Basket -  Create graduated each line (exact and/or not exact mod)
	 */
	async createEachLine(index) {
		let currentXposUnits;
		let initalDistanceValue: number;
		if (!this.page.variable.chooseExactNumber) {
			initalDistanceValue = (this.displayedNumber(1) - this.displayedNumber(0)) / this.page.variable.approximation;
		}
		// create scale bars
		const scaleBarsBig = BABYLON.MeshBuilder.CreateBox("scaleBars", { width: 0.08, height: 0.4 }, this.scene) as TargetMesh;
		const scaleBarsMaterial = new BABYLON.StandardMaterial("scaleBarsMaterial", this.scene);
		scaleBarsMaterial.diffuseColor = BABYLON.Color3.FromHexString("#fa7610");
		scaleBarsBig.layerMask = this.cameraHud.layerMask;
		scaleBarsBig.position = new BABYLON.Vector3(this.linesXPos, this.linePositionY, -5);
		scaleBarsBig.material = scaleBarsMaterial;
		scaleBarsBig.value = this.displayedNumber(index);
		scaleBarsBig.remediation = true;
		this.bigScaleBars.push(scaleBarsBig);
		if (index !== this.nbBasketItemsRounded - 1 && this.page.variable.chooseExactNumber) {
			this.drawSemiCircle(scaleBarsBig.position);
		}

		this.assetsContainerBasket.meshes.push(scaleBarsBig);
		currentXposUnits = this.linesXPos;
		if (!this.page.variable.chooseExactNumber && index < this.nbBasketItemsRounded - 1) {
			for (let indexUnit = 1; indexUnit <= this.page.variable.approximation; indexUnit++) {
				const scaleBarsSmallMesh = BABYLON.MeshBuilder.CreateBox(
					"box",
					{ width: this.stepXRange / this.page.variable.approximation - 0.05, height: 0.2 },
					this.scene
				) as TargetMesh;

				const scaleBarsSmallLineMesh = BABYLON.MeshBuilder.CreateBox("box", { width: 0.06, height: 0.2 }, this.scene) as TargetMesh;
				const smallScaleBarsMaterial = new BABYLON.StandardMaterial("smallScaleBarsMaterial", this.scene);
				smallScaleBarsMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0);
				scaleBarsSmallLineMesh.material = smallScaleBarsMaterial;
				scaleBarsSmallLineMesh.layerMask = this.cameraHud.layerMask;
				scaleBarsSmallMesh.layerMask = this.cameraHud.layerMask;
				const positionXCenterd = currentXposUnits + this.stepXRange / this.page.variable.approximation / 2;
				const positionXRight = currentXposUnits + this.stepXRange / this.page.variable.approximation;
				scaleBarsSmallMesh.position = new BABYLON.Vector3(positionXCenterd, this.linePositionY, -5);
				scaleBarsSmallLineMesh.position = new BABYLON.Vector3(positionXRight, this.linePositionY, -5);
				scaleBarsSmallMesh.remediation = true;
				this.drawSemiCircle(scaleBarsSmallMesh.position);
				let rangeStartValue = 0;
				if (indexUnit > 0) {
					rangeStartValue = 0.001;
				}

				scaleBarsSmallMesh.rangeValue = {
					from: scaleBarsBig.value + initalDistanceValue * (indexUnit - 1) + rangeStartValue,
					to: scaleBarsBig.value + initalDistanceValue * indexUnit,
					inital: scaleBarsBig.value
				};
				scaleBarsSmallLineMesh.remediation = scaleBarsSmallMesh.remediation = true;
				currentXposUnits = currentXposUnits + this.stepXRange / this.page.variable.approximation;
				this.smallScaleBars.push(scaleBarsSmallMesh);
				this.assetsContainerBasket.materials.push(smallScaleBarsMaterial);
				this.assetsContainerBasket.meshes.push(scaleBarsSmallMesh, scaleBarsSmallLineMesh);
			}
		}
		// result.position = scaleBarsBig.position;
		this.linesXPos = this.linesXPos + this.stepXRange;
	}

	/**
	 * Basket - Remediation : Create a semi circle between each line
	 */
	async drawSemiCircle(finalPosition: Vector3): Promise<void> {
		const points = [];
		let distanceFinal;
		if (this.basketRatioElement > 1) {
			distanceFinal = this.stepXRange;
		} else {
			distanceFinal = this.stepXRange / 2;
		}

		const xLength = this.page.variable.chooseExactNumber ? 1 : this.page.variable.approximation;
		const rayon = distanceFinal / xLength / this.basketRatioElement;

		const pas = Math.PI / 20;
		// number of points needed to create the semi circle
		for (let i = 0; i <= 20; i++) {
			points.push(new BABYLON.Vector3(rayon * Math.cos(pas * i), rayon * Math.sin(pas * i), 0));
		}

		// Path3D
		const path3d = new BABYLON.Path3D(points);
		const curve = path3d.getCurve();

		const xPosition = this.page.variable.chooseExactNumber ? rayon : 0;

		// visualisation
		const lineMesh = BABYLON.Mesh.CreateLines("lines", curve, this.scene) as Mesh;
		lineMesh.position = new Vector3(finalPosition.x + xPosition, finalPosition.y, finalPosition.z);
		lineMesh.isVisible = false;
		this.drawUnitStepHelp(lineMesh);

		this.assetsContainerBasket.meshes.push(lineMesh);
	}

	/**
	 * Basket - Remediation : Write unit step for help
	 */
	async drawUnitStepHelp(lineMesh: Mesh): Promise<void> {
		return new Promise<void>(async resolve => {
			const sizeMesh = lineMesh.getBoundingInfo().boundingBox;
			const boxHelp = BABYLON.MeshBuilder.CreateBox("boxHelp", { width: 0.5, height: 0.7 }, this.scene) as TargetMesh;
			boxHelp.position = new Vector3(lineMesh.position.x, lineMesh.position.y + sizeMesh.maximumWorld.y, lineMesh.position.z);
			// boxHelp.layerMask = this.cameraHud.layerMask;
			const dynamicTexture = new BABYLON.DynamicTexture("text", { width: 50, height: 0.7 * 100 }, this.scene);
			dynamicTexture.update();
			if (this.page.variable.unitStep instanceof Fraction) {
				const textureContext = dynamicTexture.getContext();
				const imgSrc = await this.mathJaxSvgToImg(1);
				textureContext.clearRect(0, 0, 100, 100);
				textureContext.fillStyle = "rgb(255, 255, 255)";
				textureContext.fillRect(0, 0, 100, 100);
				textureContext.drawImage(imgSrc, 7, 7, 40, 55);
				textureContext.fill();
				dynamicTexture.update();
			} else {
				dynamicTexture.drawText(
					"+" + this.page.variable.unitStep.valueOf().toString(),
					null,
					null,
					`${dynamicTexture.getBaseSize().width / 2}px solid Arial`,
					"black",
					"white"
				);
			}

			const material = new BABYLON.StandardMaterial("materialHelp", this.scene);

			material.diffuseTexture = dynamicTexture;
			boxHelp.material = material;
			boxHelp.isVisible = false;
			this.assetsContainerBasket.meshes.push(boxHelp);
			this.assetsContainerBasket.materials.push(material);
			resolve();
		});
	}

	/**
	 * Basket - Display units into the box randomly
	 */
	async displayBasketBoxRandomly() {
		this.getRandomBoxIndexesDisplayed();
		await this.allBoxesToBeDisplayed();
	}

	/**
	 * Basket - Boxes to be displayed with their own unit step's. Min and Max are always displayed
	 */
	async allBoxesToBeDisplayed() {
		for (let index = 0; index <= this.bigScaleBars.length - 1; index++) {
			// nb min and nb max are displayed / in not exact mode the closest line box is not displayed
			const displayNbMinOrNbMax =
				(index === 0 || index === this.bigScaleBars.length - 1) &&
				(this.bigScaleBars[index].value !== this.expectedResultByAccuracy ||
					(!this.page.variable.chooseExactNumber && this.page.variable.difficulty !== 3));
			if (this.numberOfElementsDisplayed.includes(index) || displayNbMinOrNbMax) {
				await this.generateBoxesResults(index);
			}
		}
	}

	/**
	 * Basket - Remediation : Move the ball to the line
	 */
	async moveBallAnimation() {
		this.basketBallMesh.isVisible = true;
		return new Promise<void>(resolveProm => {
			const targetIndex = this.smallScaleBars.findIndex((currMesh, meshIndex) => {
				const isLastElement = meshIndex === this.bigScaleBars.length - 1;
				return (
					(isLastElement ? currMesh.rangeValue.to : currMesh.rangeValue.inital) ===
					this.bigScaleBars[this.remediationTargettedIndex].value
				);
			});
			const speed = 800;
			const ballAnimation = new BABYLON.Animation(
				"position",
				"position",
				speed,
				BABYLON.Animation.ANIMATIONTYPE_VECTOR3,
				BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE
			);
			const positionKeys = [
				{
					frame: 0,
					value: new BABYLON.Vector3(
						this.basketBallMesh.position.x,
						this.basketBallMesh.position.y,
						this.basketBallMesh.position.z
					)
				},
				{
					frame: speed,
					value: new BABYLON.Vector3(
						this.smallScaleBars[targetIndex].position.x,
						this.smallScaleBars[targetIndex].position.y,
						this.smallScaleBars[targetIndex].position.z
					)
				}
			];

			ballAnimation.setKeys(positionKeys);
			this.basketBallMesh.animations.push(ballAnimation);

			const anim = this.scene.beginAnimation(this.basketBallMesh, 0, speed, false);
			anim.onAnimationEnd = () => {
				resolveProm();
			};
		});
	}

	/**
	 * Basket - Remediation : Determine which 2 box results to be shown and avoiding to display excepted result
	 */
	async showBasketDistanceGraduation(): Promise<void> {
		return new Promise<void>(async resolveProm => {
			const boxesDisplayedNumbers = this.assetsContainerBasket.meshes.filter(mesh => {
				return mesh.name === "boxResult";
			});
			const allNumbersToBeDisplayed = new Array();
			for (let index = 0; index < this.numberItems; index++) {
				allNumbersToBeDisplayed.push(this.displayedNumber(index));
			}

			let indexOfExpectedResult;
			if (this.page.variable.chooseExactNumber) {
				indexOfExpectedResult = this.bigScaleBars.findIndex(meshes => {
					return meshes.value === this.page.variable.expectedResult.valueOf();
				});
			} else {
				indexOfExpectedResult = this.bigScaleBars.findIndex((currMesh, meshIndex) => {
					const isLastElement = meshIndex === this.bigScaleBars.length - 1;
					return this.smallScaleBars.some((smallMesh, index) => {
						return (isLastElement ? smallMesh.rangeValue.to : smallMesh.rangeValue.inital) === currMesh.value;
					});
				});
			}

			const boxesToNoTBeDisplayed = [0, indexOfExpectedResult, indexOfExpectedResult - 1];
			let boxIndexToDisplay;
			if (indexOfExpectedResult > -1) {
				boxIndexToDisplay = this.bigScaleBars.findIndex((mesh, index) => {
					return !boxesToNoTBeDisplayed.includes(index);
				});

				boxesDisplayedNumbers.forEach(mesh => {
					mesh.isVisible = false;
				});
				this.remediationTargettedIndex = allNumbersToBeDisplayed.findIndex((numbersDisplayed, index) => {
					return index === boxIndexToDisplay;
				});
				if (this.remediationTargettedIndex === -1) {
					this.remediationTargettedIndex = 1;
				} else if (this.remediationTargettedIndex === allNumbersToBeDisplayed.length - 1) {
					this.remediationTargettedIndex = this.remediationTargettedIndex - 1;
				}
				this.generateBoxesResults(this.remediationTargettedIndex);
				await AppUtils.timeOut(500);
				this.generateBoxesResults(this.remediationTargettedIndex + 1);
			}

			const drawedLines = this.assetsContainerBasket.meshes.filter(mesh => {
				return mesh.name === "lines";
			});
			const lineMeshInd = drawedLines.findIndex((lineMesh, index) => {
				return index === this.remediationTargettedIndex;
			});
			this.launchBasketLineRemediationAnim().then(async () => {
				if (this.page.variable.chooseExactNumber) {
					this.showDrawedCircleLine(lineMeshInd);
				}
				resolveProm();
			});
		});
	}

	/**
	 * Basket-remediation : Show drawed circle with units step
	 */
	showDrawedCircleLine(lineMeshInd = null) {
		let curentLineMeshInd: number;

		const drawedLines = this.assetsContainerBasket.meshes.filter(mesh => {
			return mesh.name === "lines";
		});

		if (!lineMeshInd) {
			curentLineMeshInd = drawedLines.findIndex((lineMesh, index) => {
				return index === this.remediationTargettedIndex;
			});
		} else {
			curentLineMeshInd = lineMeshInd;
		}

		const boxHelpes = this.assetsContainerBasket.meshes.filter(mesh => {
			return mesh.name === "boxHelp";
		});

		if (drawedLines[curentLineMeshInd]) {
			drawedLines[curentLineMeshInd].layerMask = this.cameraHud.layerMask;
			drawedLines[curentLineMeshInd].isVisible = true;

			if (boxHelpes[curentLineMeshInd]) {
				boxHelpes[curentLineMeshInd].layerMask = this.cameraHud.layerMask;
				boxHelpes[curentLineMeshInd].isVisible = true;
			}
		}
	}

	/**
	 * Basket - Remediation : Reset meshes position before remediation
	 */
	resetMeshesAfterRemediation() {
		const drawedLines = this.assetsContainerBasket.meshes.filter(mesh => {
			return mesh.name === "lines";
		});

		const boxHelpes = this.assetsContainerBasket.meshes.filter(mesh => {
			return mesh.name === "boxHelp";
		});

		boxHelpes.forEach(currentBox => {
			currentBox.layerMask = 0x0fffffff;
		});

		drawedLines.forEach(lines => {
			lines.layerMask = 0x0fffffff;
		});

		this.basketBallMesh.position = this.defaultBallPosition.clone();
		if (this.tubesMesh[this.remediationTargettedIndex]) {
			(this.tubesMesh[this.remediationTargettedIndex].material as StandardMaterial).diffuseColor = new BABYLON.Color3(1, 1, 1);
		}
	}

	/**
	 * Basket - Remediation
	 */
	async showDrawedCircleLinesByIndexes(): Promise<void> {
		return new Promise<void>(async resolveCurrent => {
			for (let i = 0; i < this.page.variable.approximation; i++) {
				if (!this.page.skipSequence) {
					await AppUtils.timeOut(500);
					this.page.cabri.showDrawedCircleLine(this.page.cabri.remediationTargettedIndex * this.page.variable.approximation + i);
				}
			}
			resolveCurrent();
		});
	}

	/**
	 * Basket - Remediation :  Display or hide boxes results
	 */
	showHideBoxesResults(show: boolean) {
		this.assetsContainerBasket.meshes.forEach(mesh => {
			if (mesh.name === "boxResult") {
				mesh.isVisible = show;
			}
		});
	}

	/**
	 * Basket - Remediation :  Display or hide lines
	 */
	showHideLines(displayTogether: boolean, show: boolean): Promise<void> {
		return new Promise<void>(async resolve => {
			for (const mesh of this.assetsContainerBasket.meshes) {
				if (mesh.name === "scaleBars") {
					if (displayTogether) {
						await AppUtils.timeOut(100);
						mesh.isVisible = true;
					} else {
						mesh.isVisible = show;
					}
				}
			}

			resolve();
		});
	}

	/**
	 * Basket - Remediation display boxes and fill tube mesh progressively
	 */
	async launchBasketLineRemediationAnim(): Promise<void> {
		return new Promise<void>(resolveProm => {
			const speed = 800;
			const colorsPossibilities = [new BABYLON.Color3(1, 0, 0), new BABYLON.Color3(0, 1, 0), new BABYLON.Color3(0, 0, 1)];
			const randomColorIndex = Math.floor(Math.random() * colorsPossibilities.length);
			const materialColorAnim = new BABYLON.Animation(
				"position",
				"material.diffuseColor",
				speed,
				BABYLON.Animation.ANIMATIONTYPE_COLOR3,
				BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE
			);
			const positionKeys = [
				{
					frame: 0,
					value: new BABYLON.Color3(1, 1, 1)
				},
				{
					frame: speed,
					value: colorsPossibilities[randomColorIndex]
				}
			];

			if (this.tubesMesh[this.remediationTargettedIndex]) {
				materialColorAnim.setKeys(positionKeys);
				this.tubesMesh[this.remediationTargettedIndex].animations.push(materialColorAnim);
				const anim = this.scene.beginAnimation(this.tubesMesh[this.remediationTargettedIndex], 0, speed, false);

				anim.onAnimationEnd = async () => {
					resolveProm();
				};
			} else {
				resolveProm();
			}
		});
	}

	/**
	 * Basket - Remediation
	 */
	hideAlmostRemediationElements() {
		this.page.cabri.showHideBoxesResults(false);
		this.page.cabri.showHideLines(false, false);
	}

	async showLineBoxResult(box: boolean, hide: boolean) {
		if (box && !hide) {
			this.generateBoxesResults(this.remediationTargettedIndex);
			await AppUtils.timeOut(500);
			this.generateBoxesResults(this.remediationTargettedIndex + 1);
		} else if (!box && !hide) {
			await this.launchBasketLineRemediationAnim();
		}
	}

	async removeScene() {
		if (this.assetsContainerBasket) {
			this.assetsContainerBasket.removeAllFromScene();
			this.assetsContainerBasket.dispose();
		}
		if (this.assetsContainerOnTheRoad) {
			this.assetsContainerOnTheRoad.removeAllFromScene();
			this.assetsContainerOnTheRoad.dispose();
		}
		const removeList = [];
		this.scene.multiMaterials.forEach(element => {
			if (!this.initialMaterials.includes(element)) {
				removeList.push(element);
			}
		});

		this.scene.materials.forEach(element => {
			if (!this.initialMaterials.includes(element)) {
				removeList.push(element);
			}
		});
		for (let material of removeList) {
			await new Promise<void>(resolve => {
				material.onDisposeObservable.addOnce(() => {
					material.isDisposed = true;
					material = null;
					resolve();
				});
				material.dispose(true, true, false);
			});
		}
		this.scene.materials = this.scene.materials.filter(material => {
			return material !== null && !(material as any).isDisposed;
		});
		// console.error(this.scene.materials.length, this.initialMaterials.length);
	}

	async launchAgainActivity() {
		await this.removeScene();
		await this.launchActivity();
	}

	/**
	 * Convert svg generaterd by mathJax into an image in order to display in the box
	 */
	mathJaxSvgToImg(currIndex: number) {
		return new Promise(resolveFinal => {
			let denominateur: number;
			if (this.page.variable.unitStep instanceof Fraction) {
				denominateur = this.page.variable.unitStep.denominateur;
			} else if (this.page.variable.nbMin instanceof Fraction) {
				denominateur = this.page.variable.nbMin.denominateur;
			} else {
				denominateur = this.page.variable.unitStep.valueOf();
			}
			MathJax.tex2svgPromise(`\\frac{${this.page.variable.nbMin.valueOf() + currIndex}}{${denominateur}}`).then(element => {
				this.page.mathJaxDiv.nativeElement.appendChild(element);
				MathJax.startup.document.clear();

				const allMySvgs = this.page.mathJaxDiv.nativeElement.querySelectorAll("svg");
				const svgsLength = allMySvgs.length - 1;
				allMySvgs[svgsLength].appendChild(MathJax.defsGlobalCaching);
				const url = "data:image/svg+xml;base64," + window.btoa(allMySvgs[svgsLength].outerHTML);
				const img = new Image();
				return new Promise(() => {
					img.onload = () => {
						resolveFinal(img);
					};
					img.onerror = err => {
						console.log("errerr", err);
					};
					img.src = url;
				});
			});
		});
	}

	/**
	 * Basket - Results written into the box according if fraction type or not
	 */
	generateBoxesResults(currIndex: number): Promise<void> {
		return new Promise(async resolve => {
			let values: { dynamicTexture: DynamicTexture; box: TargetMesh; material: StandardMaterial; boxHeight: number };
			if (this.nbMinMaxisNotFraction(currIndex) || this.isWholeNumber(currIndex)) {
				// not fraction
				values = this.generateBoxes(currIndex);
			} else if (this.page.variable.unitStep instanceof Fraction) {
				//  fraction
				values = await this.generateFractionBoxes(currIndex);
			} else {
				if (this.page.variable.nbMin instanceof Fraction && this.page.variable.nbMax instanceof Fraction) {
					if (currIndex === 0 || currIndex === this.bigScaleBars.length - 1) {
						// fraction
						values = await this.generateFractionBoxes(currIndex);
					} else {
						// not fraction
						values = this.generateBoxes(currIndex);
					}
				} else {
					// not fraction
					values = this.generateBoxes(currIndex);
				}
			}
			// common ad in the dynamic texture
			values.material.diffuseTexture = values.dynamicTexture;
			values.box.material = values.material;
			values.box.layerMask = this.cameraHud.layerMask;
			values.box.position = new BABYLON.Vector3(this.bigScaleBars[currIndex].position.x, this.linePositionY - values.boxHeight, -5);
			values.box.remediation = true;
			this.assetsContainerBasket.meshes.push(values.box);
			this.assetsContainerBasket.textures.push(values.dynamicTexture);
			this.assetsContainerBasket.materials.push(values.material);
			resolve();
		});
	}

	/**
	 * If displayed number is a whole number so display as number and not as Fraction
	 */
	isWholeNumber(currIndex: number): boolean {
		return this.bigScaleBars[currIndex].value.valueOf() - Math.floor(this.bigScaleBars[currIndex].value.valueOf()) === 0;
	}

	/**
	 * Basket - Check if nbMin and nbMax is not in the fraction type
	 */
	nbMinMaxisNotFraction(currIndex: number): boolean {
		return (
			(this.bigScaleBars[currIndex] &&
				this.bigScaleBars[currIndex].value === this.page.variable.nbMin.valueOf() &&
				!(this.page.variable.nbMin instanceof Fraction)) ||
			(this.bigScaleBars[currIndex].value === this.page.variable.nbMax.valueOf() && !(this.page.variable.nbMax instanceof Fraction))
		);
	}

	/**
	 * Basket - generate boxes without fraction
	 */
	generateBoxes(currIndex: number): { dynamicTexture; box; material; boxHeight } {
		let box: TargetMesh;
		let material: StandardMaterial;
		let boxHeight: number;
		let dynamicTexture;
		boxHeight = 0.7;
		box = BABYLON.MeshBuilder.CreateBox("boxResult", { width: 0.5, height: boxHeight }, this.scene) as TargetMesh;
		material = new BABYLON.StandardMaterial("redMat", this.scene);
		dynamicTexture = new BABYLON.DynamicTexture("text", { width: 50, height: boxHeight * 100 }, this.scene);
		dynamicTexture.update();

		dynamicTexture.drawText(
			this.displayedNumber(currIndex),
			null,
			null,
			`${dynamicTexture.getBaseSize().width / 2}px solid Arial`,
			"black",
			"white"
		);
		return { dynamicTexture, box, material, boxHeight };
	}

	/**
	 * Basket -generate boxes with fraction by using MathJax
	 */
	async generateFractionBoxes(
		currIndex: number
	): Promise<{ dynamicTexture: DynamicTexture; box: TargetMesh; material: StandardMaterial; boxHeight: number }> {
		return new Promise(async resolve => {
			let box: TargetMesh;
			let material: StandardMaterial;
			let boxHeight: number;
			let dynamicTexture;

			boxHeight = 0.7;
			// convert svg to img
			const imgSrc = await this.mathJaxSvgToImg(currIndex);
			box = BABYLON.MeshBuilder.CreateBox("boxResult", { width: 0.6, height: boxHeight }, this.scene) as TargetMesh;
			material = new BABYLON.StandardMaterial("redMat", this.scene);
			dynamicTexture = new BABYLON.DynamicTexture("textureNumber", { width: 90, height: 100 }, this.scene);
			const textureContext = dynamicTexture.getContext();
			// draw img into the canvas
			textureContext.clearRect(0, 0, 100, 100);
			textureContext.fillStyle = "rgb(255, 255, 255)";
			textureContext.fillRect(0, 0, 100, 100);
			textureContext.drawImage(imgSrc, 20, 10, 55, 80);
			textureContext.fill();
			dynamicTexture.update();
			resolve({ dynamicTexture, box, material, boxHeight });
		});
	}

	/**
	 * Precision determine not apply when the result is 0
	 */
	displayedNumberPrecision(value) {
		if (value.toString().includes(".")) {
			return this.basketUnitStepPrecision;
		} else {
			return 0;
		}
	}

	/**
	 * Basket - Display number of result by it's position
	 */
	displayedNumber(index: number) {
		if (this.page.variable.unitStep instanceof Fraction) {
			return (this.page.variable.nbMin.valueOf() + index) / this.page.variable.unitStep.denominateur;
		} else {
			const value = this.page.variable.nbMin.valueOf() + index * this.page.variable.unitStep;
			return Number(value.toFixed(this.displayedNumberPrecision(value)));
		}
	}

	/**
	 * Basket - Get random indexes to be displayed into the box for all difficulties
	 */
	getRandomBoxIndexesDisplayed() {
		this.numberOfElementsDisplayed = new Array();
		if (this.page.variable.difficulty === 0) {
			this.bigScaleBars.forEach((val, index) => {
				this.numberOfElementsDisplayed.push(index);
			});
		} else {
			let numElement = new Array();
			numElement = new Array(this.displayedNumberAllowed).fill(null);
			const expectedResultIndex = this.bigScaleBars.findIndex(mesh => {
				return this.expectedResultByAccuracy === mesh.value;
			});
			numElement.forEach(() => {
				this.getRandomBoxIndexesDisplayedRecursive(this.bigScaleBars.length - 1, 1, expectedResultIndex);
			});
		}
	}

	/**
	 * Basket - Number results displayed by difficulty
	 */
	get displayedNumberAllowed() {
		let numberItems: number;
		const allIntevralNumbersDisplay = this.numberItems - 2;
		if (this.page.variable.difficulty === 1) {
			numberItems = Math.floor(allIntevralNumbersDisplay / 2);
			if (numberItems === 0) {
				numberItems = 1;
			}
		} else if (this.page.variable.difficulty === 2) {
			if (allIntevralNumbersDisplay > 2) {
				numberItems = 2;
			} else {
				numberItems = 1;
			}
		} else if (this.page.variable.difficulty === 3) {
			numberItems = 0;
		}

		return numberItems;
	}

	get expectedResultByAccuracy() {
		return this.page.variable.chooseExactNumber
			? this.page.variable.expectedResult.valueOf()
			: Math.floor(this.page.variable.expectedResult.valueOf() as any);
	}

	/**
	 * Basket - Get random indexes to be displayed into the box for all difficulties except for first level.
	 * Avoid displaying excepted result box
	 */

	getRandomBoxIndexesDisplayedRecursive(min, max, expectedResultIndex) {
		const num = Math.floor(Math.random() * (max - min + 1)) + min;
		if (this.numberOfElementsDisplayed.includes(num) || num === expectedResultIndex) {
			return this.getRandomBoxIndexesDisplayedRecursive(min, max, expectedResultIndex);
		} else {
			this.numberOfElementsDisplayed.push(num);
			return num;
		}
	}

	/**
	 * Get number elements in the graduated scale
	 */
	get numberItems(): number {
		// step for integer is one
		let nbElement;
		if (this.page.variable.unitStep instanceof Fraction) {
			nbElement =
				(this.page.variable.nbMax.valueOf() - this.page.variable.nbMin.valueOf()) * this.page.variable.unitStep.denominateur + 1;
		} else {
			nbElement = (this.page.variable.nbMax.valueOf() - this.page.variable.nbMin.valueOf()) / this.page.variable.unitStep + 1;
		}
		return nbElement;
	}

	/**********************************************************************************************************************
	 *************************************************** ACTIVITY ON THE ROAD **********************************************
	 ***********************************************************************************************************************/

	async buildElementActivityOnTheRoad(): Promise<void> {
		return new Promise(resolve => {
			this.importAssetOnTheRoad().then(() => {
				this.createGroundTile();
				this.createCars();
				if (!this.globalService.lowPerformanceMode) {
					this.createEnvElement();
				}
				this.createHighLighter();
				[this.consigne, this.page.justePointExercices.consigneTTS] = this.getConsigneExerciceRoad();
				this.createSceneGui(this.consigne);
				this.assetsContainerOnTheRoad.addAllToScene();
				resolve();
			});
		});
	}

	createHighLighter() {
		if (!this.hl) {
			this.hl = new BABYLON.HighlightLayer("hl1", this.scene);
			this.alpha = 0;
			this.hl.renderingGroupId = 0;
			this.scene.registerBeforeRender(() => {
				this.alpha += 0.1;

				this.hl.blurHorizontalSize = 0.3 + Math.cos(this.alpha) * 0.6 + 0.6;
				this.hl.blurVerticalSize = 0.3 + Math.sin(this.alpha / 3) * 0.6 + 0.6;
			});
		}
	}

	importAssetOnTheRoad(): Promise<void> {
		this.assetsManagerOnTheRoad = new BABYLON.AssetsManager(this.scene);
		this.assetsContainerOnTheRoad = new BABYLON.AssetContainer(this.scene);
		// (window as any).containerManager = this.assetsContainerOnTheRoad;
		this.importTextureOnTheRoad();
		this.importImageOnTheRoad();
		this.importMeshOnTheRoad();
		this.assetsManagerOnTheRoad.onFinish = this.importFinishOnTheRoad.bind(this);
		return this.assetsManagerOnTheRoad.loadAsync();
	}

	importTextureOnTheRoad() {
		const textureMap = [
			{ name: "parking", url: "assets/babylon/justePoint/textures/parking.png" },
			{ name: "parkingR", url: "assets/babylon/justePoint/textures/parkingR.png" },
			{
				name: "emptyRoad",
				url: "assets/babylon/justePoint/textures/road_empty" + (this.globalService.lowPerformanceMode ? "_ios" : "") + ".png"
			},
			{
				name: "linedRoad",
				url: "assets/babylon/justePoint/textures/road_lined" + (this.globalService.lowPerformanceMode ? "_ios" : "") + ".png"
			},
			{
				name: "grass",
				url: "assets/babylon/justePoint/textures/Grass_03" + (this.globalService.lowPerformanceMode ? "_ios" : "") + ".png"
			}
		];
		if(!this.globalService.lowPerformanceMode){
			textureMap.push({
				name: "normalGrass",
				url: "assets/babylon/justePoint/textures/NormalMap" + (this.globalService.lowPerformanceMode ? "_ios" : "") + ".png"
			});
		}
		textureMap.forEach(texture => {
			this.assetsManagerOnTheRoad.addTextureTask(texture.name, texture.url);
		});
	}

	importMeshOnTheRoad() {
		const mMeshs = this.globalService.lowPerformanceMode
			? [{ name: "Car", url: "/assets/babylon/justePoint/models/", filename: "Low-Poly-Racing-Car.babylon" }]
			: [
					{ name: "Car", url: "/assets/babylon/justePoint/models/", filename: "Low-Poly-Racing-Car.babylon" },
					{ name: "Tree", url: "/assets/babylon/justePoint/models/", filename: "tree.babylon" },
					{ name: "Pine", url: "/assets/babylon/justePoint/models/", filename: "pine.babylon" },
					{ name: "House", url: "/assets/babylon/justePoint/models/", filename: "house.babylon" }
			  ];

		mMeshs.forEach(mesh => {
			this.assetsManagerOnTheRoad.addMeshTask(mesh.name, null, mesh.url, mesh.filename);
		});
	}

	importImageOnTheRoad() {
		const images = [
			{
				name: "kpost",
				url: "assets/babylon/justePoint/textures/kpost" + (this.globalService.lowPerformanceMode ? "_ios" : "") + ".png"
			}
		];

		images.forEach(image => {
			this.assetsManagerOnTheRoad.addImageTask(image.name, image.url);
		});
	}

	importFinishOnTheRoad(tasks: AbstractAssetTask[]) {
		this.textures = new Map<string, Texture>();
		this.images = new Map<string, HTMLImageElement>();
		this.meshs = new Map<string, AbstractMesh>();
		tasks.forEach(task => {
			if (task.isCompleted) {
				if (task instanceof BABYLON.MeshAssetTask) {
					const lTask = task as MeshAssetTask;
					this.assetsContainerOnTheRoad.meshes = this.assetsContainerOnTheRoad.meshes.concat(lTask.loadedMeshes);
					lTask.loadedMeshes.forEach(mesh => {
						this.meshs.set(mesh.name, mesh);
						if (mesh.material) {
							if (mesh.material instanceof BABYLON.MultiMaterial) {
								this.assetsContainerOnTheRoad.multiMaterials.push(mesh.material as MultiMaterial);
							} else {
								this.assetsContainerOnTheRoad.materials.push(mesh.material);
								this.assetsContainerOnTheRoad.textures = this.assetsContainerOnTheRoad.textures.concat(
									mesh.material.getActiveTextures()
								);
							}
						}
					});
				} else if (task instanceof BABYLON.ImageAssetTask) {
					const lTask = task as ImageAssetTask;
					this.images.set(task.name, lTask.image);
				} else if (task instanceof BABYLON.TextureAssetTask) {
					const lTask = task as TextureAssetTask;
					this.assetsContainerOnTheRoad.textures.push(lTask.texture);
					this.textures.set(lTask.name, lTask.texture);
				} else {
					console.error("Task Not Handle", task);
				}
			} else {
				console.error(task.errorObject);
			}
		});
	}

	setBackgroundPosition() {
		if (this.page.variable.activity === justePointActivity.onTheRoad) {
			this.background.position = new BABYLON.Vector3(0, -12, 40);
			this.background.scaling = new BABYLON.Vector3(2.2, 2.2, 2.2);
		} else {
			this.background.position = new BABYLON.Vector3(0, 0, 0);
			this.background.scaling = new BABYLON.Vector3(1.1, 1, 1.1);
		}
	}

	/**
	 * CheckCollision to detec the result of the exercise call on dpointer up event after drag
	 */
	checkRoadCollision() {
		const allOnRoadGround =
			this.targetMesh.length > 0 &&
			this.targetMesh.every(carMesh => {
				return carMesh.intersectsMesh(this.meshs.get("road"), false, true);
			});
		this.buttonValider.isVisible = allOnRoadGround;
	}

	createEnvElement() {
		const tree = this.meshs.get("Tree1") as Mesh;
		this.freezeMaterialOnMesh(tree);
		tree.layerMask = this.camera.layerMask;
		this.assetsContainerOnTheRoad.meshes.push(tree);
		tree.scaling = new Vector3(0.1, 0.1, 0.1);
		tree.position = new Vector3(6, 1.4, 11);
		const tree2 = tree.clone("tree2");
		tree2.scaling = new Vector3(0.1, 0.1, 0.1);
		tree2.position = new Vector3(8, 1.4, 10);
		tree2.rotation.y = (80 * Math.PI) / 180;
		this.assetsContainerOnTheRoad.meshes.push(tree2);
		const tree3 = tree.clone("tree3");
		tree3.scaling = new Vector3(0.1, 0.1, 0.1);
		tree3.position = new Vector3(10, 1.4, 8);
		tree3.rotation.y = (180 * Math.PI) / 180;
		this.assetsContainerOnTheRoad.meshes.push(tree3);
		const tree4 = tree.clone("tree4");
		tree4.scaling = new Vector3(0.1, 0.1, 0.1);
		tree4.position = new Vector3(11, 1.4, 7);
		tree4.rotation.y = (300 * Math.PI) / 180;
		this.assetsContainerOnTheRoad.meshes.push(tree4);
		// pine
		const pine = this.meshs.get("Tree2") as Mesh;
		this.freezeMaterialOnMesh(pine);
		pine.layerMask = this.camera.layerMask;
		this.assetsContainerOnTheRoad.meshes.push(pine);
		pine.scaling = new Vector3(0.1, 0.1, 0.1);
		pine.position = new Vector3(3, 1.4, 11);
		const pine2 = pine.clone("Pine2");
		pine2.scaling = new Vector3(0.16, 0.16, 0.16);
		pine2.position = new Vector3(9, 1.4, 10);
		pine2.rotation.y = (80 * Math.PI) / 180;
		this.assetsContainerOnTheRoad.meshes.push(pine2);
		const pine3 = pine.clone("Pine3");
		pine3.scaling = new Vector3(0.1, 0.1, 0.1);
		pine3.position = new Vector3(-10, 1.4, 2.5);
		pine3.rotation.y = (180 * Math.PI) / 180;
		this.assetsContainerOnTheRoad.meshes.push(pine3);
		const pine4 = pine.clone("Pine4");
		pine4.scaling = new Vector3(0.17, 0.17, 0.17);
		pine4.position = new Vector3(-11.5, 1.4, 9);
		pine4.rotation.y = (300 * Math.PI) / 180;
		this.assetsContainerOnTheRoad.meshes.push(pine4);
		const house = this.meshs.get("Cubo.001") as Mesh;
		this.freezeMaterialOnMesh(house);
		house.getChildMeshes().forEach(element => {
			element.layerMask = this.camera.layerMask;
		});

		house.layerMask = this.camera.layerMask;
		this.assetsContainerOnTheRoad.meshes.push(house);
		house.scaling = new Vector3(0.1, 0.1, 0.1);
		house.position = new Vector3(4.5, 0.56, 8);
		house.rotation = new Vector3(0, (118.8 * Math.PI) / 180, 0);
	}

	/**
	 * freezeMaterial material into a mesh and it's children
	 * @param mesh
	 */
	freezeMaterialOnMesh(mesh: Mesh) {
		mesh.material.freeze();
		if (mesh.getChildMeshes && mesh.getChildMeshes.length > 0) {
			mesh.getChildMeshes().forEach(mesh => {
				this.freezeMaterialOnMesh(mesh as Mesh);
			});
		}
	}

	/**
	 * Create Car whith innerValue for expected result
	 */
	createCars() {
		// first car
		const car: TargetMesh = this.meshs.get("Car") as TargetMesh;
		let color = [
			{ color: new BABYLON.Color3(0, 0, 0) as Color3, name: $localize`noire` },
			{ color: new BABYLON.Color3(0, 0, 1) as Color3, name: $localize`bleue` },
			{ color: new BABYLON.Color3(0.839, 0.898, 0.024) as Color3, name: $localize`jaune` },
			{ color: new BABYLON.Color3(0.5, 0, 1) as Color3, name: $localize`violette` },
			{ color: new BABYLON.Color3(0, 0.541, 0.184) as Color3, name: $localize`verte` },
			{ color: new BABYLON.Color3(1, 0.349, 0) as Color3, name: $localize`orange` },
			{ color: new BABYLON.Color3(1, 0, 0.5) as Color3, name: $localize`rose` },
			{ color: new BABYLON.Color3(0.435, 0.427, 0.427) as Color3, name: $localize`grise` }
			//{ color: new BABYLON.Color3(1, 0, 0) as Color3, name: $localize`rouge` }
		];
		this.color = color.slice();
		car.renderingGroupId = 1;
		car.rotationQuaternion = null;
		car.rotation = new BABYLON.Vector3((268 * Math.PI) / 180, (360 * Math.PI) / 180, (268 * Math.PI) / 180);
		car.scaling = new BABYLON.Vector3(0.005, 0.005, 0.005);
		car.getChildMeshes().forEach(subMesh => {
			subMesh.layerMask = this.camera.layerMask;
			subMesh.isPickable = true;
		});
		car.layerMask = this.camera.layerMask;
		car.checkCollisions = true;
		car.isPickable = true;
		car.draggable = true;
		let expectedResult = this.page.variable.expectedResult;
		//beware bug in bounding box the loaded default car with modify value not work when the car is clone the new object work
		this.generateBoundingBoxFromAllChild(car);
		if (Array.isArray(expectedResult)) {
			let xPosition = -(Math.floor(expectedResult.length / 2) * 1.5 - (expectedResult.length % 2 === 0 ? 0.75 : 0));
			car.position = new BABYLON.Vector3(xPosition, 0.17, -5);

			if (expectedResult.length > 0) {
				for (let index = 0; index < expectedResult.length; index++) {
					const colorNumber = AppUtils.getRandomIntInclusive(0, color.length - 1);
					const choosenColor = color.splice(colorNumber, 1)[0];
					const nextCar = car.clone("car_" + choosenColor.name) as TargetMesh;
					nextCar.color = choosenColor.name;
					nextCar.value = expectedResult[index];
					nextCar.renderingGroupId = 2;
					const original = this.scene.getMaterialByName("car_body");
					const colorMaterial = this.createColorMaterialFromClone(
						original,
						choosenColor.color,
						choosenColor.name,
						this.assetsContainerOnTheRoad
					);
					this.replaceMaterialOnMesh(nextCar, original, colorMaterial, this.assetsContainerOnTheRoad, this.materials);
					nextCar.position.x = xPosition;
					this.generateBoundingBoxFromAllChild(nextCar);
					nextCar.layerMask = this.camera.layerMask;
					nextCar.initialPosition = nextCar.position.clone();
					this.meshs.set(nextCar.name, nextCar);
					this.assetsContainerOnTheRoad.meshes.push(nextCar);
					this.targetMesh.push(nextCar as TargetMesh);
					xPosition += 1.5;
				}
			}
		} else {
			const colorNumber = AppUtils.getRandomIntInclusive(0, color.length - 1);
			const choosenColor = color.splice(colorNumber, 1)[0];
			const nextCar = car.clone("car_" + choosenColor.name) as TargetMesh;
			nextCar.color = choosenColor.name;
			const original = this.scene.getMaterialByName("car_body");
			const colorMaterial = this.createColorMaterialFromClone(
				original,
				choosenColor.color,
				choosenColor.name,
				this.assetsContainerOnTheRoad
			);
			this.replaceMaterialOnMesh(nextCar, original, colorMaterial, this.assetsContainerOnTheRoad, this.materials);
			nextCar.layerMask = this.camera.layerMask;
			this.meshs.set(nextCar.name, nextCar);
			this.assetsContainerOnTheRoad.meshes.push(nextCar);
			this.targetMesh.push(nextCar as TargetMesh);
			nextCar.position = new BABYLON.Vector3(0, 0.17, -5);
			// clone also need to redeclare a bounding box
			this.generateBoundingBoxFromAllChild(nextCar);
			nextCar.value = expectedResult.valueOf();
		}
		car.dispose();
	}

	/**
	 * Replace car on starting position
	 */
	resetAllCarPosition(result?: ResultOnTheRoad) {
		if (result) {
			result.carsError.forEach(car => {
				this.resetCarPosition(car.color);
			});
		} else {
			this.targetMesh.forEach(car => {
				this.resetCarPosition(car);
			});
		}
	}

	/**
	 * a mesh or the color
	 * @param car
	 */
	resetCarPosition(car: TargetMesh | string) {
		if (car instanceof TargetMesh) {
			car.position = car.initialPosition.clone();
		} else {
			const carMesh = this.targetMesh.find(m => m.color === car);
			carMesh.position = carMesh.initialPosition.clone();
		}
	}

	/**
	 * Move all car or a list in the good position
	 */
	setAllCarOnCorrectPosition(result?: ResultOnTheRoad) {
		if (result) {
			result.carsError.forEach(car => {
				this.setCarOnCorrectPosition(car.color);
			});
		} else {
			this.targetMesh.forEach(car => {
				this.setCarOnCorrectPosition(car);
			});
		}
	}

	/**
	 * Position a car mesh in good answer position can be a mesh or a string color
	 * @param car
	 */
	setCarOnCorrectPosition(car: TargetMesh | string) {
		if (car instanceof TargetMesh) {
			const groundTile = this.targetSubMesh.find(s => s.value === car.value);
			if (groundTile) {
				car.position.x = groundTile.getBoundingInfo().boundingBox.centerWorld.x;
				car.position.z = groundTile.getBoundingInfo().boundingBox.centerWorld.z;
			}
		} else {
			const lCar = this.targetMesh.find(m => m.color === car);
			const groundTile = this.targetSubMesh.find(s => s.value === lCar.value);
			if (groundTile) {
				lCar.position.x = groundTile.getBoundingInfo().boundingBox.centerWorld.x;
				lCar.position.z = groundTile.getBoundingInfo().boundingBox.centerWorld.z;
			}
		}
	}

	/**
	 * Create the ground floor with the rad subdivide into segment holding her value
	 */
	createGroundTile() {
		this.targetSubMesh = [];
		this.targetMesh = [];

		const grid = {
			h: 1,
			w: this.numberItems
		};
		// create Tiled Mesh for a graduated Road
		const tiledGround: Mesh = new BABYLON.MeshBuilder.CreateTiledGround("road", {
			xmin: -8,
			zmin: -3,
			xmax: 8,
			zmax: 3,
			subdivisions: grid,
			Updatable: true
		});
		tiledGround.isPickable = false;

		tiledGround.layerMask = this.camera.layerMask;
		this.meshs.set(tiledGround.name, tiledGround);
		this.assetsContainerOnTheRoad.meshes.push(tiledGround);
		this.createMaterialOnTheRoad();

		// Create Multi Material
		const multimat: MultiMaterial = new BABYLON.MultiMaterial("multi", this.scene);
		if (this.page.variable.noRoadHelp) {
			multimat.subMaterials.push(this.materials.get("emptyRoad"));
		} else {
			multimat.subMaterials.push(this.materials.get("linedRoad"));
		}
		this.assetsContainerOnTheRoad.multiMaterials.push(multimat);

		// Apply the multi material
		// Define multimat as material of the tiled ground
		tiledGround.material = multimat;

		// Needed variables to set subMeshes
		//const verticesCount = tiledGround.getTotalVertices();
		// if we send total vertice into subMesh the subMesh become global of parent and not generate the bounding box so we send 1
		const verticesCount = 1;
		const tileIndicesLength = tiledGround.getIndices().length / (grid.w * grid.h);

		// Set subMeshes of the tiled ground
		tiledGround.subMeshes = [];
		this.KPosttargetMesh = [];
		let base = 0;
		const visibleItem = this.randomKPostVisibility(grid.w);

		for (let col = 0; col < grid.w; col++) {
			const subMesh = new BABYLON.SubMesh(
				col % multimat.subMaterials.length,
				0,
				verticesCount,
				base,
				tileIndicesLength,
				tiledGround,
				null,
				true
			) as TargetSubMesh;
			if (this.page.variable.unitStep instanceof Fraction) {
				// TODO calculate value in fraction for each graduation
				subMesh.value = this.page.variable.nbMin.valueOf() + col * this.page.variable.unitStep.valueOf();
			} else {
				subMesh.value = this.page.variable.nbMin.valueOf() + col * this.page.variable.unitStep;
			}
			tiledGround.subMeshes.push(subMesh);

			this.generateKilometerPost(subMesh, visibleItem ? visibleItem.includes(col) : true);
			this.targetSubMesh.push(subMesh);
			base += tileIndicesLength;
		}

		//duplicate road mesh to fill left and right around the road
		const leftRoad = tiledGround.clone("leftRoad");
		leftRoad.position = new BABYLON.Vector3(-16, 0, 0);
		this.assetsContainerOnTheRoad.meshes.push(leftRoad);
		this.meshs.set(leftRoad.name, leftRoad);
		const rightRoad = tiledGround.clone("rightRoad");
		this.assetsContainerOnTheRoad.meshes.push(rightRoad);
		this.meshs.set(rightRoad.name, rightRoad);
		rightRoad.position = new BABYLON.Vector3(16, 0, 0);

		//generate top and bottom ground around the road.
		const upperTileGround = new BABYLON.MeshBuilder.CreateGround("upperGround", { width: 40, height: 10 }, this.scene);
		upperTileGround.isPickable = false;
		this.assetsContainerOnTheRoad.meshes.push(upperTileGround);
		this.meshs.set(upperTileGround.name, upperTileGround);
		upperTileGround.layerMask = this.camera.layerMask;
		upperTileGround.material = this.materials.get("grass");
		upperTileGround.position = new BABYLON.Vector3(0, 0, 8);
		const lowerTileGround = new BABYLON.MeshBuilder.CreateGround("lowerGround", { width: 40, height: 20 }, this.scene);
		lowerTileGround.isPickable = false;
		this.assetsContainerOnTheRoad.meshes.push(lowerTileGround);
		this.meshs.set(lowerTileGround.name, lowerTileGround);
		lowerTileGround.layerMask = this.camera.layerMask;
		lowerTileGround.material = this.materials.get("grass");
		lowerTileGround.position = new BABYLON.Vector3(0, 0, -13);
		const carNumber = Array.isArray(this.page.variable.expectedResult) ? this.page.variable.expectedResult.length : 1;
		const parkingGround = new BABYLON.MeshBuilder.CreateGround("parking", { width: 2 * carNumber, height: 3 }, this.scene);
		parkingGround.isPickable = false;
		this.assetsContainerOnTheRoad.meshes.push(parkingGround);
		this.meshs.set(parkingGround.name, parkingGround);
		parkingGround.layerMask = this.camera.layerMask;
		parkingGround.material = this.materials.get("parking");
		parkingGround.position = new BABYLON.Vector3(0, 0.001, -4.5);
		const parkingGroundR: Mesh = new BABYLON.MeshBuilder.CreateGround("parkingR", { width: 0.3, height: 3 }, this.scene);
		parkingGroundR.isPickable = false;
		this.assetsContainerOnTheRoad.meshes.push(parkingGroundR);
		this.meshs.set(parkingGroundR.name, parkingGroundR);
		parkingGroundR.layerMask = this.camera.layerMask;
		const mat = this.materials.get("parkingR");
		parkingGroundR.material = this.materials.get("parkingR");

		mat.diffuseTexture.hasAlpha = true;
		parkingGroundR.material = mat;
		const parkingGroundR2: Mesh = new BABYLON.MeshBuilder.CreateGround("parkingR2", { width: 0.3, height: 3 }, this.scene);
		parkingGroundR.position = new BABYLON.Vector3(-(carNumber + 0.15), 0.001, -4.5);
		parkingGroundR2.isPickable = false;
		this.assetsContainerOnTheRoad.meshes.push(parkingGroundR2);
		this.meshs.set(parkingGroundR2.name, parkingGroundR2);
		parkingGroundR2.layerMask = this.camera.layerMask;
		const material = this.materials.get("parkingR").clone("parkingR2");
		parkingGroundR2.material = material;
		this.assetsContainerOnTheRoad.materials.push(parkingGroundR2.material);
		const texture: Texture = material.diffuseTexture.clone() as Texture;
		this.assetsContainerOnTheRoad.textures.push(texture);
		texture.uAng = (180 * Math.PI) / 180;
		texture.wAng = (180 * Math.PI) / 180;
		material.diffuseTexture = texture;
		material.ambientTexture = texture;
		material.specularTexture = texture;
		material.emissiveTexture = texture;
		parkingGroundR2.position = new BABYLON.Vector3(carNumber + 0.15, 0.001, -4.5);
	}

	displayedNumbberAllowedRoad() {
		let numberItems: number;
		if (this.page.variable.difficulty === 1) {
			numberItems = 4;
		} else if (this.page.variable.difficulty === 2) {
			numberItems = 2;
		} else if (this.page.variable.difficulty === 3) {
			numberItems = 0;
		}

		return numberItems;
	}

	randomKPostVisibility(totalItem: number): number[] {
		const quantityVisible = this.displayedNumbberAllowedRoad();
		if (quantityVisible !== undefined) {
			const visibleItem = [];
			const bagNumber = Array.from({ length: totalItem - 2 }, (v, k) => k + 1);
			// remove from the bag of possible kpost the index of the expected result
			if (Array.isArray(this.page.variable.expectedResult)) {
				this.page.variable.expectedResult.forEach(result => {
					const valueToRemove = Math.round(
						(result.valueOf() - this.page.variable.nbMin.valueOf()) / this.page.variable.unitStep.valueOf()
					);
					if (valueToRemove > 0) {
						const indexInsideBag = bagNumber.findIndex(f => valueToRemove === f);
						if (indexInsideBag > -1) {
							bagNumber.splice(indexInsideBag, 1);
						}
					}
				});
			} else {
				const valueToRemove = Math.round(
					(this.page.variable.expectedResult.valueOf() - this.page.variable.nbMin.valueOf()) /
						this.page.variable.unitStep.valueOf()
				);
				if (valueToRemove > 0) {
					const indexInsideBag = bagNumber.findIndex(f => valueToRemove === f);
					if (indexInsideBag > -1) {
						bagNumber.splice(indexInsideBag, 1);
					}
				}
			}
			// push first and last visible
			visibleItem.push(0, totalItem - 1);
			for (let index = 0; index < quantityVisible; index++) {
				if (bagNumber.length > 0) {
					const selectedIndex: number = AppUtils.getRandomIntInclusive(0, bagNumber.length - 1);
					visibleItem.push(bagNumber.splice(selectedIndex, 1)[0]);
				}
			}
			return visibleItem;
		} else {
			return null;
		}
	}

	/**
	 * Generate the kilometer post with the value for each road subdivision
	 * @param subMesh
	 * @param visible
	 */
	generateKilometerPost(subMesh: TargetSubMesh, visible = true) {
		const kpost: TargetMesh = BABYLON.MeshBuilder.CreatePlane("kpost_" + subMesh.value, { width: 1, height: 1 }, this.scene);
		this.faceCameraMesh(kpost, this.camera);
		this.KPosttargetMesh.push(kpost);
		this.assetsContainerOnTheRoad.meshes.push(kpost);
		this.meshs.set(kpost.name, kpost);
		const KpostPositionVector = subMesh.getBoundingInfo().boundingBox.center.clone();
		KpostPositionVector.z = 3;
		KpostPositionVector.y = 0.5;
		kpost.position = KpostPositionVector;
		kpost.layerMask = this.camera.layerMask;

		const textureSize = this.globalService.lowPerformanceMode ? 128 : 512;
		const textureKpost: DynamicTexture = new BABYLON.DynamicTexture("textureKpost", textureSize, this.scene);
		this.textures.set(textureKpost.name, textureKpost);
		this.assetsContainerOnTheRoad.textures.push(textureKpost);
		const textureContext = textureKpost.getContext();
		const materialKPost = new BABYLON.StandardMaterial("MatkPost", this.scene);
		this.materials.set(materialKPost.name, materialKPost);
		this.assetsContainerOnTheRoad.materials.push(materialKPost);

		materialKPost.diffuseTexture = textureKpost;
		materialKPost.specularColor = BABYLON.Color3.Black();
		kpost.material = materialKPost;
		textureContext.clearRect(0, 0, textureSize, textureSize);
		textureContext.fillStyle = "rgba(0, 0, 0, 0.2)";
		textureContext.fillRect(0, 0, textureSize, textureSize);
		textureContext.drawImage(this.images.get("kpost"), 0, 0);
		textureContext.fill();
		textureKpost.update();
		this.drawText(subMesh.value.toString(), textureKpost, DistanceUnit.Km);
		kpost.visible = visible;
		kpost.setEnabled(kpost.visible);
	}

	public showAllKpost() {
		this.enableAndHideAllKpost();
		this.scene.onAfterRenderObservable.addOnce(() => {
			this.KPosttargetMesh.forEach(kpost => {
				if (!this.checkKpostsCollisions(kpost)) {
					kpost.setEnabled(true);
				} else {
					kpost.setEnabled(false);
				}
			});
		});
		this.resetVisibilityAllKpost();
	}
	public enableAndHideAllKpost() {
		this.KPosttargetMesh.forEach(kpost => {
			kpost.visibility = 0;
			kpost.setEnabled(true);
		});
	}
	public resetVisibilityAllKpost() {
		this.KPosttargetMesh.forEach(kpost => {
			kpost.visibility = 1;
		});
	}
	checkKpostsCollisions(kpost: Mesh) {
		return this.KPosttargetMesh.some(kpost2 => {
			const km = Number(kpost.id.split("_")[1]);
			const km2 = Number(kpost2.id.split("_")[1]);
			if (km2 < km || (kpost2.id === this.KPosttargetMesh[this.KPosttargetMesh.length - 1].id && kpost.id !== kpost2.id)) {
				const collision = kpost2.intersectsMesh(kpost);
				if (collision && kpost2.isEnabled()) {
					return true;
				}
			}
			return false;
		});
	}

	public showOnlyVisiblepost() {
		this.KPosttargetMesh.forEach(kpost => {
			kpost.setEnabled(kpost.visible);
		});
	}

	private drawText(text: string, textureKpost: DynamicTexture, unit: DistanceUnit) {
		const ratio = this.globalService.lowPerformanceMode ? 128 / 512 : 1;
		let font;

		const maxChar = Math.max(String(this.page.variable.nbMin.valueOf()).length, String(this.page.variable.nbMax.valueOf()).length);

		// Add text to dynamic texture
		switch (maxChar) {
			case 1:
				font = "bold " + 300 * ratio + "px monospace";
				textureKpost.drawText(text, 175 * ratio, 450 * ratio, font, "black", null, true, true);
				break;
			case 2:
				font = "bold " + 250 * ratio + "px monospace";
				textureKpost.drawText(text, (110 + (maxChar - text.length) * 65) * ratio, 450 * ratio, font, "black", null, true, true);
				break;
			case 3:
				font = "bold " + 140 * ratio + "px monospace";
				textureKpost.drawText(text, (130 + (maxChar - text.length) * 25) * ratio, 400 * ratio, font, "black", null, true, true);
				break;
			case 4:
				font = "bold " + 100 * ratio + "px monospace";
				textureKpost.drawText(text, (138 + (maxChar - text.length) * 10) * ratio, 390 * ratio, font, "black", null, true, true);
				break;
			default:
				font = "bold " + 90 * ratio + "px monospace";
				textureKpost.drawText(text, 130 * ratio, 390 * ratio, font, "black", null, true, true);
				break;
		}
		font = "bold " + 150 * ratio + "px monospace";
		textureKpost.drawText(unit, 160 * ratio, 150 * ratio, font, "black", null, true, true);

		textureKpost.hasAlpha = true;
	}

	private createMaterialOnTheRoad() {
		this.materials = new Map<string, StandardMaterial>();
		this.textures.forEach((texture, key) => {
			const mat = new BABYLON.StandardMaterial(key, this.scene);
			this.materials.set(mat.name, mat);
			this.assetsContainerOnTheRoad.materials.push(mat);
			mat.diffuseTexture = texture;
			mat.specularTexture = texture;
			mat.emissiveTexture = texture;
			mat.ambientTexture = texture;
		});

		if(!this.globalService.lowPerformanceMode){	
			this.materials.get("grass").bumpTexture = this.textures.get("normalGrass");
			this.materials.get("grass").bumpTexture.level = 20;
			
			(this.materials.get("grass").bumpTexture as unknown as DynamicTexture).uAng = (40 * Math.PI) / 180;
			(this.materials.get("grass").bumpTexture as unknown as DynamicTexture).vScale = 5;
			(this.materials.get("grass").bumpTexture as unknown as DynamicTexture).uScale = 20;
		}
		(this.materials.get("grass").diffuseTexture as unknown as DynamicTexture).uAng = (40 * Math.PI) / 180;
		(this.materials.get("grass").diffuseTexture as unknown as DynamicTexture).level = 2;
		/*(this.materials.get("grass").bumpTexture as unknown as DynamicTexture).vScale = 2.5;
		(this.materials.get("grass").bumpTexture as unknown as DynamicTexture).uScale = 10;
		(this.materials.get("grass").diffuseTexture as unknown as DynamicTexture).vScale = 2.5;
		(this.materials.get("grass").diffuseTexture as unknown as DynamicTexture).uScale = 10;
		(this.materials.get("grass").specularTexture as unknown as DynamicTexture).vScale = 2.5;
		(this.materials.get("grass").specularTexture as unknown as DynamicTexture).uScale = 10;
		(this.materials.get("grass").emissiveTexture as unknown as DynamicTexture).vScale = 2.5;
		(this.materials.get("grass").emissiveTexture as unknown as DynamicTexture).uScale = 10;
		(this.materials.get("grass").ambientTexture as unknown as DynamicTexture).vScale = 2.5;
		(this.materials.get("grass").ambientTexture as unknown as DynamicTexture).uScale = 10;*/

		(this.materials.get("grass").diffuseTexture as unknown as DynamicTexture).uAng = (40 * Math.PI) / 180;
		(this.materials.get("grass").diffuseTexture as unknown as DynamicTexture).vScale = 5;
		(this.materials.get("grass").diffuseTexture as unknown as DynamicTexture).uScale = 20;
		(this.materials.get("grass").specularTexture as unknown as DynamicTexture).uAng = (40 * Math.PI) / 180;
		(this.materials.get("grass").specularTexture as unknown as DynamicTexture).vScale = 5;
		(this.materials.get("grass").specularTexture as unknown as DynamicTexture).uScale = 20;
		(this.materials.get("grass").emissiveTexture as unknown as DynamicTexture).uAng = (40 * Math.PI) / 180;
		(this.materials.get("grass").emissiveTexture as unknown as DynamicTexture).vScale = 5;
		(this.materials.get("grass").emissiveTexture as unknown as DynamicTexture).uScale = 20;
		(this.materials.get("grass").ambientTexture as unknown as DynamicTexture).uAng = (40 * Math.PI) / 180;
		(this.materials.get("grass").ambientTexture as unknown as DynamicTexture).vScale = 5;
		(this.materials.get("grass").ambientTexture as unknown as DynamicTexture).uScale = 20;
		(this.materials.get("parkingR").specularTexture as unknown as DynamicTexture).wrapU = BABYLON.Constants.TEXTURE_CLAMP_ADDRESSMODE;
		(this.materials.get("parkingR").emissiveTexture as unknown as DynamicTexture).wrapU = BABYLON.Constants.TEXTURE_CLAMP_ADDRESSMODE;
		(this.materials.get("parkingR").ambientTexture as unknown as DynamicTexture).wrapU = BABYLON.Constants.TEXTURE_CLAMP_ADDRESSMODE;
		(this.materials.get("parkingR").diffuseTexture as unknown as DynamicTexture).wrapU = BABYLON.Constants.TEXTURE_CLAMP_ADDRESSMODE;
		(this.materials.get("parkingR").specularTexture as unknown as DynamicTexture).wrapV = BABYLON.Constants.TEXTURE_CLAMP_ADDRESSMODE;
		(this.materials.get("parkingR").emissiveTexture as unknown as DynamicTexture).wrapV = BABYLON.Constants.TEXTURE_CLAMP_ADDRESSMODE;
		(this.materials.get("parkingR").ambientTexture as unknown as DynamicTexture).wrapV = BABYLON.Constants.TEXTURE_CLAMP_ADDRESSMODE;
		(this.materials.get("parkingR").diffuseTexture as unknown as DynamicTexture).wrapV = BABYLON.Constants.TEXTURE_CLAMP_ADDRESSMODE;
	}

	public createSceneGui(text: string[]) {
		this.advancedTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("myUI", true, this.scene);
		this.assetsContainerOnTheRoad.textures.push(this.advancedTexture);

		this.advancedTexture.layer.layerMask = this.cameraHud.layerMask;
		this.bubble = new BABYLON.GUI.Rectangle();

		const maxCharLine = Math.max(...text.map(line => line.length));

		this.bubble.width = 0.01 * maxCharLine + 0.005;
		this.bubble.height = 0.05 * text.length;
		this.bubble.background = "#224295";
		this.bubble.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT;
		this.bubble.verticalAlignment = Control.VERTICAL_ALIGNMENT_TOP;
		this.bubble.left = "5%";
		this.bubble.adaptWidthToChildren = false;
		this.bubble.adaptHeightToChildren = false;
		this.bubble.cornerRadius = 10;
		this.bubble.thickness = 10;
		this.bubble.color = "#ffffff";
		this.bubble.alpha = 1;
		this.setBubbleTopPosition();
		this.textBlock = new BABYLON.GUI.TextBlock();
		this.textBlock.alpha = 1;
		this.textBlock.text = text.reduce((previousValue, currentValue) => previousValue + "\n" + currentValue);
		this.textBlock.textWrapping = true;
		this.textBlock.resizeToFit = false;
		this.textBlock.color = "#ffffff";
		this.textBlock.fontSize = "20%";
		this.textBlock.fontFamily = "Quicksand-Bold";
		this.textBlock.fontWeight = "bold";

		this.buttonValider = BABYLON.GUI.Button.CreateImageWithCenterTextButton(
			"Valider",
			$localize`Valider`,
			"assets/babylon/justePoint/textures/start.png"
		);
		this.buttonValider.width = "250px";
		this.buttonValider.height = "250px";
		this.buttonValider.fontSize = "45px";
		this.buttonValider.fontFamily = "Quicksand-Bold";
		this.buttonValider.fontWeight = "bold";
		this.buttonValider.thickness = 0;
		this.buttonValider.color = "#ffffff";
		this.buttonValider.top = "17%";
		this.buttonValider.left = "35%";
		this.buttonValider.isVisible = false;
		this.lastCalled = new Date().getTime();
		this.buttonValider.onPointerClickObservable.add((x, state) => {
			const now = new Date().getTime();
			if(now - this.lastCalled < 500 /* half a second */) {
				// skip next calls
				// AppUtils.debug("skip next calls");
				state.skipNextObservers = true;
			} else {
				// AppUtils.debug("sendButtonValidate()");
				this.lastCalled = now;
				this.sendButtonValidate();
			}
		});
		// if double event bug persist use this:
		// this.buttonValider.onPointerDownObservable.add(this.sendButtonValidate.bind(this));
		this.bubble.addControl(this.textBlock);
		this.advancedTexture.addControl(this.buttonValider);
		this.advancedTexture.addControl(this.bubble);
	}

	setBubbleTopPosition(){
		if (this.bubble){
			if (this.marginTop && this.marginTop < 0 ){
				this.bubble.top= ((Math.round((Math.abs(this.marginTop) + 10) * window.devicePixelRatio)));
			}
			else {
				this.bubble.top = "5%";
			}
		}
	}

	toggleGui(visible: boolean) {
		this.bubble.isVisible = visible;
	}

	sendButtonValidate(eventData?: Vector2WithInfo, eventState?: EventState) {
		this.buttonValider.isVisible = false;
		let carTrue = 0;
		const error = [];
		let success = false;
		let reversed = false;
		this.targetMesh.forEach(car => {
			const groundTile = this.targetSubMesh.find(s => s.value === car.value);
			if (groundTile) {
				if (car.intersectsMesh(groundTile as unknown as AbstractMesh, false, false)) {
					carTrue += 1;
				} else {
					if (Array.isArray(this.page.variable.expectedResult)) {
						this.page.variable.expectedResult.forEach(result => {
							const altGroundTile = this.targetSubMesh.find(s => s.value === result.valueOf());
							if (car.intersectsMesh(altGroundTile as unknown as AbstractMesh, false, false)) {
								reversed = true;
							}
						});
					}
					error.push({ color: car.color, correctValue: car.value });
				}
			} else {
				throw new Error("Probleme de 'variable exercice' le nombre rechercher n'existe pas dans les bornes");
			}
		});

		success = carTrue === this.targetMesh.length;
		this.waitUserAnswer.next({ success, response: new ResultOnTheRoad(error), reversed: reversed });
		console.log("success", success);
		console.log("error", error);
	}
	getStepPrecision(currentUnitStep?: number) {
		let unitStep;
		if (!currentUnitStep) {
			unitStep = this.page.variable.unitStep.valueOf();
		} else {
			unitStep = currentUnitStep;
		}
		if (unitStep.toString().includes(".")) {
			const untilCommaInd = unitStep.toString().indexOf(".");
			return unitStep.toString().length - 1 - untilCommaInd;
		} else {
			return 0;
		}
	}

	/**
	 * @returns Return an array of string with the consigne for the exercice
	 * @param secondShot to know if it is the second try and adapt scenario accordingly
	 */
	getConsigneExerciceBasketTTS(secondShot?: boolean): string[] {
		const consigneArrayTTS = [];
		const theFraction = $localize`:consigne jeu juste point type Fraction:la fraction `;
		const toFraction = $localize`:consigne jeu juste point type Fraction:à la fraction `;
		const ofFraction = $localize`:consigne jeu juste point type Fraction:de la fraction `;
		const theNumber = $localize`:consigne jeu juste point type Nombre:le nombre `;
		const toNombre = $localize`:consigne jeu juste point type Nombre:au nombre `;
		const ofNumber = $localize`:consigne jeu juste point type Nombre:du nombre `;
		const toTypeResult = this.page.variable.expectedResult instanceof Fraction ? toFraction : toNombre;
		const theTypeResult = this.page.variable.expectedResult instanceof Fraction ? theFraction : theNumber;
		const ofTypeResult = this.page.variable.expectedResult instanceof Fraction ? ofFraction : ofNumber;

		let phrase = $localize`Mets le ballon dans le panier correspondant ${toTypeResult}`;
		if (!secondShot && this.cabriService.teamTotalAwards > 1 && this.cabriService.teamTotalAwards < 5) {
			// after the first answer:
			if (!this.consigneScenariosArray || this.consigneScenariosArray.length === 0) {
				if (this.page.accountService.team.length === 1) {
					this.consigneScenariosArray = [
						$localize`Peux-tu atteindre ${theTypeResult}`,
						$localize`Essaye de viser ${theTypeResult}`,
						$localize`Maintenant vise ${theTypeResult}`,
						$localize`Et maintenant ${theTypeResult}`,
						$localize`Lance le ballon dans le panier ${ofTypeResult}`
					]
				} else {
					this.consigneScenariosArray = [
						$localize`Peux-tu atteindre ${theTypeResult}`,
						$localize`Essaye de viser ${theTypeResult}`,
						$localize`Vise ${theTypeResult}`,
						$localize`Peux-tu atteindre ${theTypeResult}`,
						$localize`Lance le ballon dans le panier ${ofTypeResult}`
					]
				}
				AppUtils.shuffleArray(this.consigneScenariosArray);
			}
			const [first, ...rest] = this.consigneScenariosArray;
			this.consigneScenariosArray = rest;
			phrase = first;
		} else if (!secondShot && this.cabriService.teamTotalAwards >= 6) {
			// after 6 answers:
			const consigneScenariosFastArray = [
				$localize`Vise ${theTypeResult}`,
				$localize`Dans ${theTypeResult}`,
				this.page.variable.expectedResult instanceof Fraction ? theTypeResult : $localize`Le`,
				this.page.variable.expectedResult instanceof Fraction ? $localize`Maintenant la` : $localize`Maintenant le`,
			]
			const index = Math.floor(Math.random() * consigneScenariosFastArray.length);
			phrase = consigneScenariosFastArray[index];
		} else if (secondShot) {
			// after remediation:
			const consigneScenariosFastArray = [
				$localize`Alors, dans quel panier est ${theTypeResult}`,
				$localize`Mais alors, où se cache donc ${theTypeResult}`,
				$localize`Où est donc ${theTypeResult}`,
				$localize`Mais alors, où se cache ${theTypeResult}`,
				$localize`Donc… Où est ${theTypeResult}`,
				$localize`Du coup, où se trouve ${theTypeResult}`
			]
			const index = Math.floor(Math.random() * consigneScenariosFastArray.length);
			phrase = consigneScenariosFastArray[index];
		}

		this.page.basketCommonPhrase = phrase;
		let expectedResult = "";
		if (this.page.variable.expectedResult instanceof Fraction) {
			expectedResult = this.page.scenario.ttsFraction(this.page.variable.expectedResult);
		} else {
			expectedResult = this.page.variable.expectedResult.valueOf().toString();
		}
		consigneArrayTTS.push(this.page.basketCommonPhrase + expectedResult);
		return consigneArrayTTS;
	}

	/**
	 * @returns Return an array of string with the consigne for the Road exercice
	 */
	getConsigneExerciceRoad(): [string[], string] {
		let opSign: string;
		const stepPrecision = this.getStepPrecision();
		const nb1 = Number(this.page.variable.expectedResult[0].toFixed(stepPrecision));
		const nb2 = Number(this.page.variable.expectedResult[1].toFixed(stepPrecision));
		let pas: number;
		this.page.scenario.resultPhrase = undefined;
		if (this.page.variable.operation !== operation.PlacementDePoint) {
			switch (this.page.variable.operation) {
				case operation.addition:
					opSign = "+";
				break;
				case operation.soustraction:
					opSign = "-";
				break;
			}
			pas = nb2 - nb1;
			const result = eval(nb1 + opSign + pas).toFixed(stepPrecision);
			this.page.scenario.resultPhrase = $localize`Oui`+`, ${nb1} ${opSign} ${pas} =${result} !`;
			this.page.scenario.resultPhraseRemediation = `${nb1} ${opSign} ${pas} =${result} !`;
			// console.error("XXX nb1 = ", nb1);
			// console.error("XXX nb2 = ", nb2);
			// console.error("XXX result phrase = ", this.page.scenario.resultPhrase);
		}

		const consigneArrayDisplayed = [];
		let consigneArrayTTS: string[] = [];
		let consigneTTSRoad;
		const nbrItem = this.targetMesh.length;
		const lesVoitures = $localize`les voitures`;
		const laVoiture = $localize`la voiture`;
		const laLesVoiture = nbrItem > 1 ? lesVoitures : laVoiture;
		consigneArrayDisplayed.push($localize`Place ${laLesVoiture} selon les consignes suivantes : `);
		this.targetMesh.forEach((voiture, index, array) => {

			if (index === 0 || this.page.variable.operation === operation.PlacementDePoint) {
				const kmValue = voiture.value.toString();
				// TEXT:
				consigneArrayDisplayed.push($localize`La voiture ${voiture.color} a roulé ${kmValue} ${DistanceUnit.Km}`);
				// TTS:
				consigneArrayTTS.push($localize`La voiture ${voiture.color} a roulé ${kmValue} ${DistanceUnit.Km}`)
			} else {
				const currentColor = array[index - 1].color;
				if (this.page.variable.operation === operation.addition) {
					const currentKmValue = (voiture.value.valueOf() - array[index - 1].value.valueOf()).toFixed(stepPrecision);
					// TEXT:
					consigneArrayDisplayed.push(
						$localize`La voiture ${voiture.color} est à ${currentKmValue} ${DistanceUnit.Km} devant la voiture ${currentColor}`
					);
					// TTS:
					consigneArrayTTS.push($localize`La ${voiture.color} est à ${currentKmValue} ${DistanceUnit.Km} devant`);
				} else {
					const currentKmValue = (array[index - 1].value.valueOf() - voiture.value.valueOf()).toFixed(stepPrecision);
					// TEXT:
					consigneArrayDisplayed.push(
						$localize`La ${voiture.color} est à ${currentKmValue} ${DistanceUnit.Km} derrière la voiture ${currentColor}`
					);
					// TTS:
					consigneArrayTTS.push($localize`La ${voiture.color} est à ${currentKmValue} ${DistanceUnit.Km} derrière ${currentColor}`);
				}
			}
		});
		if (this.page.currentUser.awardsCurrent.total < 1) {
			// read full consigne the first time
			consigneArrayTTS = consigneArrayDisplayed;
		}
		const virgule = $localize`virgule`;
		const kilometre = $localize`kilomètre`;
		consigneTTSRoad = consigneArrayTTS.reduce((p, n) => p + "! " + n)
		.replace(/\./g, " "+ virgule +" ")
		.replace(/km/g, kilometre);
		console.error("consigneTTSRoad ", consigneTTSRoad);
		return [consigneArrayDisplayed, consigneTTSRoad];
	}

	highlightMesh(active: boolean, mesh: Mesh, wSubMesh = false, color?: Color3) {
		if (wSubMesh && mesh.getChildMeshes()?.length > 0) {
			mesh.getChildMeshes().forEach(subMesh => {
				if (active) {
					this.hl.addMesh(subMesh as Mesh, color ? color : (new BABYLON.Color3(0.01, 0.81, 0.01) as Color3), false);
				} else {
					this.hl.removeMesh(subMesh as Mesh);
				}
			});
		}
		if (active) {
			this.hl.addMesh(mesh, color ? color : (new BABYLON.Color3(0.01, 0.81, 0.01) as Color3), false);
		} else {
			this.hl.removeMesh(mesh);
		}
	}

	highlightCar(active: boolean, color: string | string[] | ResultOnTheRoad) {
		if (Array.isArray(color)) {
			color.forEach(c => {
				const color = this.color.find(co => co.name === c);
				const carMesh = this.targetMesh.find(m => m.color === c);
				this.highlightMesh(
					active,
					carMesh.getChildMeshes(false, cMesh => {
						return cMesh.id.includes("body.body");
					})[0] as Mesh,
					false,
					color.color
				);
			});
		} else if (color instanceof ResultOnTheRoad) {
			color.carsError.forEach(c => {
				const color = this.color.find(co => co.name === c.color);
				const carMesh = this.targetMesh.find(m => m.color === c.color);
				this.highlightMesh(
					active,
					carMesh.getChildMeshes(false, cMesh => {
						return cMesh.id.includes("body.body");
					})[0] as Mesh,
					false,
					color.color
				);
			});
		} else {
			const color3 = this.color.find(co => co.name === color);
			const carMesh = this.targetMesh.find(m => m.color === color);
			this.highlightMesh(
				active,
				carMesh.getChildMeshes(false, cMesh => {
					return cMesh.id.includes("body.body");
				})[0] as Mesh,
				false,
				color3.color
			);
		}
	}

	remediationRoadPart1(): Promise<{ turn: number; range: number; value: number }> {
		return new Promise<{ turn: number; range: number; value: number }>((resolve, reject) => {
			let firstMesh: TargetSubMesh;
			let firstIndex: number;

			const expectedResult = this.page.variable.expectedResult;
			//find interval with two consecutive road suMesh not in the expected result
			for (let index = 0; index < this.targetSubMesh.length; index++) {
				if (Array.isArray(expectedResult)) {
					if (
						index + 1 < this.targetSubMesh.length &&
						!expectedResult.includes(this.targetSubMesh[index].value.valueOf()) &&
						!expectedResult.includes(this.targetSubMesh[index + 1].value.valueOf())
					) {
						firstMesh = this.targetSubMesh[index];
						firstIndex = index;
						break;
					}
				} else {
					if (
						index + 1 < this.targetSubMesh.length &&
						expectedResult.valueOf() !== this.targetSubMesh[index].value.valueOf() &&
						expectedResult.valueOf() !== this.targetSubMesh[index + 1].value.valueOf()
					) {
						firstMesh = this.targetSubMesh[index];
						firstIndex = index;
						break;
					}
				}
			}
			if (!firstMesh) {
				firstMesh = this.targetSubMesh[0];
				firstIndex = 0;
			}
			let turnToLast = this.targetSubMesh.length - firstIndex - 2;
			if (turnToLast > 9) {
				turnToLast = 9;
			}

			const option = {
				width: firstMesh.getBoundingInfo().boundingBox.extendSize.x * 2,
				height: firstMesh.getBoundingInfo().boundingBox.extendSize.z * 2
			};

			this.remediationPlanes = [];
			const plane: Mesh = BABYLON.MeshBuilder.CreateGround("remediation", option, this.scene);
			this.assetsContainerOnTheRoad.meshes.push(plane);
			this.remediationPlanes.push(plane);
			plane.material = this.createColorMaterial(
				new BABYLON.Color3(1, 0.5, 0) as Color3,
				"remediation",
				this.assetsContainerOnTheRoad
			);
			this.assetsContainerOnTheRoad.materials.push(plane.material);
			plane.layerMask = this.camera.layerMask;
			plane.setPivotPoint(new BABYLON.Vector3(-firstMesh.getBoundingInfo().boundingBox.extendSize.x, 0, 0));
			plane.position.x = firstMesh.getBoundingInfo().boundingBox.centerWorld.x + firstMesh.getBoundingInfo().boundingBox.extendSize.x;
			plane.position.y = 0.01;
			plane.visibility = 0.7;
			const animationBox: Animation = new BABYLON.Animation(
				"tutoAnimation",
				"scaling.x",
				30,
				BABYLON.Animation.ANIMATIONTYPE_FLOAT,
				BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE
			);

			const keys = [];
			keys.push({
				frame: 0,
				value: 0.1
			});
			keys.push({
				frame: 30,
				value: 1
			});
			animationBox.setKeys(keys);
			plane.animations.push(animationBox);
			this.scene.beginAnimation(plane, 0, 30, false).onAnimationEndObservable.addOnce(() => {});
			const stepPrecision = this.getStepPrecision();
			resolve({
				turn: turnToLast,
				range: Number(((turnToLast + 1) * this.page.variable.unitStep.valueOf()).toFixed(stepPrecision)),
				value: this.targetSubMesh[turnToLast + firstIndex + 1].value.valueOf()
			});
		});
	}
	remediationRoadPart2(turn: number): Promise<void> {
		return new Promise<void>((resolve, reject) => {
			const plane = this.remediationPlanes[0];
			const remediationPlanes = this.remediationPlanes;
			const scene = this.scene;
			const primaryMaterial = plane.material;
			const secondaryMaterial = this.createColorMaterial(
				new BABYLON.Color3(0.01, 0.81, 0.01) as Color3,
				"remediation",
				this.assetsContainerOnTheRoad
			);
			let secondaryMat = true;
			repeatPlane(turn, plane, this.page);

			function repeatPlane(turn: number, iplane: Mesh, page: JeuJustePointPage) {
				if (!page.scenario.skipSequence) {
					iplane.animations = [];
					const newPlane = iplane.clone("remediation_" + turn);
					if (secondaryMat) {
						newPlane.material = secondaryMaterial;
					} else {
						newPlane.material = primaryMaterial;
					}
					secondaryMat = !secondaryMat;
					remediationPlanes.push(newPlane);
					newPlane.position = iplane.position.clone();
					newPlane.position.x += iplane.getBoundingInfo().boundingBox.extendSize.x * 2;
					const animationBox: Animation = new BABYLON.Animation(
						"tutoAnimation" + turn,
						"rotation.z",
						5,
						BABYLON.Animation.ANIMATIONTYPE_FLOAT,
						BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE
					);

					const keys = [];
					keys.push({
						frame: 0,
						value: (90 * Math.PI) / 180
					});
					keys.push({
						frame: 5,
						value: 0
					});
					animationBox.setKeys(keys);
					newPlane.animations.push(animationBox);
					scene.beginAnimation(newPlane, 0, 5, false, 1.5).onAnimationEndObservable.addOnce(() => {
						if (turn > 1) {
							repeatPlane(turn - 1, newPlane, page);
						} else {
							resolve();
						}
					});
				} else {
					resolve();
				}
			}
		});
	}

	customLoadingScreen() {
		BABYLON.DefaultLoadingScreen.prototype.displayLoadingUI = () => {
			this.page.customLoader = true;
		};

		BABYLON.DefaultLoadingScreen.prototype.hideLoadingUI = () => {
			this.page.customLoader = false;
		};
	}

	disposeRemediationRoad() {
		this.remediationPlanes.forEach(plane => {
			plane.material?.dispose();
			plane.dispose();
		});
	}
}

results matching ""

    No results matching ""