<commit_before>/* global webkitRequestAnimationFrame, webkitCancelAnimationFrame */
import _get from 'lodash/get';
import _slice from 'lodash/slice';
import React from 'react';
import pleaseWait from 'please-wait';
import Lifespan from 'lifespan';

import Toile, {
	mState,
	toileType,
	appState,
	transformCoords,
	inverseProjectionMatrix,
	canvasMode,
	specialKey,
} from '../toile/toile';

import {
	changeTransformOrigin,
	glyphBoundingBox,
} from '../prototypo.js/utils/generic';
import {
	matrixMul,
	dot2D,
	mulScalar2D,
	subtract2D,
	normalize2D,
	add2D,
	distance2D,
	round2D,
} from '../prototypo.js/utils/linear';

import {rawToEscapedContent} from '../helpers/input-transform.helpers';

import LocalClient from '../stores/local-client.stores';

import FontUpdater from './font-updater.components';

const raf = requestAnimationFrame || webkitRequestAnimationFrame;
const rafCancel = cancelAnimationFrame || webkitCancelAnimationFrame;

const MINIMUM_DRAG_THRESHOLD = 6;
const MINIMUM_DRAG_DIRECTIONAL_THRESHOLD = 10;

const agnosticCtrl
	= navigator.platform.indexOf('Mac') === -1 ? specialKey.CTRL : specialKey.META;

const directionalMod = {
	X: 0b1,
	Y: 0b10,
};

const onCurveModMode = {
	WIDTH_MOD: 0b1,
	ANGLE_MOD: 0b10,
};

class ManualChangeBatcher {
	constructor(client) {
		this.changes = {};
		this.client = client;
	}

	addManualChange(change, glyphName) {
		this.changes = {
			...this.changes,
			[glyphName]: {
				...this.changes[glyphName],
				...change,
			},
		};
	}

	applyManualChange() {
		const keys = Object.keys(this.changes);

		for (let i = 0; i < keys.length; i++) {
			const glyphName = keys[i];
			const changes = this.changes[glyphName];

			this.client.dispatchAction('/change-glyph-node-manually', {
				changes,
				glyphName,
				label: 'manual edition',
			});
		}
		this.changes = {};
	}
}

export function changeGlyphManually(
	changes,
	glyph,
	manualChangeBatcher,
	globalMode,
	componentName,
) {
	const glyphName = globalMode ? componentName : glyph.base || glyph.name;

	manualChangeBatcher.addManualChange(changes, glyphName);
}

function calculateHandleCoordinateModification(parentRef, newPos, opHandle) {
	const d = subtract2D(newPos, parentRef);
	const zero = {x: 0, y: 0};
	const dr = distance2D(d, zero);
	const radius = distance2D(opHandle, parentRef);
	let sign = 1;

	if (d.y !== 0) {
		sign = Math.sign(d.y);
	}
	const pointa = mulScalar2D(1 / dr ** 2, {
		x: sign * d.x * Math.sqrt(radius ** 2 * dr ** 2),
		y: Math.abs(d.y) * Math.sqrt(radius ** 2 * dr ** 2),
	});
	const pointb = mulScalar2D(1 / dr ** 2, {
		x: -sign * d.x * Math.sqrt(radius ** 2 * dr ** 2),
		y: -Math.abs(d.y) * Math.sqrt(radius ** 2 * dr ** 2),
	});

	const point = dot2D(d, pointa) <= 0 ? pointa : pointb;

	return add2D(point, parentRef);
}

export function handleModification(
	manualChangeBatcher,
	glyph,
	draggedItem,
	newPos,
	unsmoothMod,
	unparallelMod,
	globalMode,
	toile,
) {
	const {parentId, transforms, componentName, opId} = draggedItem.data;
	const handle = _get(glyph, draggedItem.id);
	const opHandle = _get(glyph, opId);
	const handlePos = {
		x: handle.xBase,
		y: handle.yBase,
	};
	const parent = _get(glyph, parentId);
	let xTransform = 1;
	let yTransform = 1;
	let angleTransform = 0;

	for (let i = 0; i < transforms.length; i++) {
		const transform = transforms[i];

		if (transform) {
			xTransform
				/= transform.name.indexOf('scaleX') === -1 ? 1 : transform.param;
			yTransform
				/= transform.name.indexOf('scaleY') === -1 ? 1 : transform.param;
			angleTransform
				+= transform.name.indexOf('rotate') === -1 ? 0 : transform.param;
		}
	}
	const newVectorPreTransform = subtract2D(newPos, handlePos);
	const newVectorScale = {
		x: newVectorPreTransform.x * xTransform,
		y: newVectorPreTransform.y * yTransform,
	};
	const newVector = {
		x:
			newVectorScale.x * Math.cos(angleTransform)
			+ newVectorScale.y * Math.sin(angleTransform),
		y:
			newVectorScale.y * Math.cos(angleTransform)
			- newVectorScale.x * Math.sin(angleTransform),
	};
	const refLength = distance2D(newPos, parent);
	const tension = refLength / distance2D(handlePos, parent);
	const isIn
		= toileType.NODE_IN === draggedItem.type
		|| toileType.CONTOUR_NODE_IN === draggedItem.type;

	const direction = isIn ? 'handleIn' : 'handleOut';
	const oppositeDirection = isIn ? 'handleOut' : 'handleIn';

	const changes = {
		[`${parentId}.${direction}.x`]: newVector.x,
		[`${parentId}.${direction}.y`]: newVector.y,
	};

	if (
		!unsmoothMod
		&& (parent.baseTypeIn === 'smooth' || parent.baseTypeOut === 'smooth')
	) {
		const opVector = calculateHandleCoordinateModification(
			parent,
			newPos,
			opHandle,
		);
		const modVector = subtract2D(opVector, {
			x: opHandle.xBase * xTransform,
			y: opHandle.yBase * yTransform,
		});
		const transformedModVector = {
			x:
				modVector.x * Math.cos(angleTransform)
				+ modVector.y * Math.sin(angleTransform),
			y:
				modVector.y * Math.cos(angleTransform)
				- modVector.x * Math.sin(angleTransform),
		};

		changes[`${parentId}.${oppositeDirection}.x`] = transformedModVector.x;
		changes[`${parentId}.${oppositeDirection}.y`] = transformedModVector.y;
	}

	changeGlyphManually(
		changes,
		glyph,
		manualChangeBatcher,
		globalMode,
		componentName,
	);
}

export function onCurveModification(
	manualChangeBatcher,
	glyph,
	draggedItem,
	onCurveBasePoints,
	newPos,
	appStateValue,
	modToApply,
	globalMode,
) {
	const {
		baseWidth,
		oppositeId,
		baseAngle,
		skeleton,
		angleOffset,
		transforms,
		componentName,
	} = draggedItem.data;

	const current = _get(glyph, draggedItem.id);
	const opposite = onCurveBasePoints.opposite;
	const currentBase = onCurveBasePoints.current;
	let posToUse = newPos;

	if (modToApply & onCurveModMode.WIDTH_MOD && opposite && currentBase) {
		const newBaseVector = normalize2D(subtract2D(currentBase, opposite));
		const newBaseAngle = Math.atan2(newBaseVector.y, newBaseVector.x);
		const translateVector = subtract2D({x: 0, y: 0}, opposite);
		const translatedPos = add2D(translateVector, newPos);
		const newBaseProjectedPoint = {
			x:
				translatedPos.x * Math.cos(newBaseAngle)
				+ translatedPos.y * Math.sin(newBaseAngle),
			y:
				-translatedPos.x * Math.sin(newBaseAngle)
				+ translatedPos.y * Math.cos(newBaseAngle),
		};

		posToUse = add2D(opposite, {
			x: newBaseProjectedPoint.x * newBaseVector.x,
			y: newBaseProjectedPoint.x * newBaseVector.y,
		});
	}

	if (modToApply & onCurveModMode.ANGLE_MOD && opposite && currentBase) {
		const previousLength = distance2D(currentBase, opposite);
		const direction = normalize2D(subtract2D(posToUse, opposite));

		posToUse = add2D(opposite, mulScalar2D(previousLength, direction));
	}

	const deltaVector = round2D(
		subtract2D(posToUse, round2D({x: current.xBase, y: current.yBase})),
	);

	const inOffest = round2D(subtract2D(current.handleIn, current));
	const outOffset = round2D(subtract2D(current.handleOut, current));

	const inNewPos = round2D(add2D(posToUse, inOffest));
	const outNewPos = round2D(add2D(posToUse, outOffset));

	const inVector = round2D(
		subtract2D(inNewPos, {
			x: current.handleIn.xBase,
			y: current.handleIn.yBase,
		}),
	);
	const outVector = round2D(
		subtract2D(outNewPos, {
			x: current.handleOut.xBase,
			y: current.handleOut.yBase,
		}),
	);

	const changes = {};

	let xTransform = 1;
	let yTransform = 1;
	let angleTransform = 0;

	for (let i = 0; i < transforms.length; i++) {
		const transform = transforms[i];

		if (transform) {
			xTransform
				/= transform.name.indexOf('scaleX') === -1 ? 1 : transform.param;
			yTransform
				/= transform.name.indexOf('scaleY') === -1 ? 1 : transform.param;
			angleTransform
				+= transform.name.indexOf('rotate') === -1 ? 0 : transform.param;
		}
	}

	const scaledDeltaVector = {
		x: deltaVector.x * xTransform,
		y: deltaVector.y * yTransform,
	};
	const scaledInVector = {
		x: inVector.x * xTransform,
		y: inVector.y * yTransform,
	};
	const scaledOutVector = {
		x: outVector.x * xTransform,
		y: outVector.y * yTransform,
	};

	const transformedDeltaVector = {
		x:
			scaledDeltaVector.x * Math.cos(angleTransform)
			+ scaledDeltaVector.y * Math.sin(angleTransform),
		y:
			scaledDeltaVector.y * Math.cos(angleTransform)
			- scaledDeltaVector.x * Math.sin(angleTransform),
	};
	const transformedInVector = {
		x:
			scaledInVector.x * Math.cos(angleTransform)
			+ scaledInVector.y * Math.sin(angleTransform),
		y:
			scaledInVector.y * Math.cos(angleTransform)
			- scaledInVector.x * Math.sin(angleTransform),
	};
	const transformedOutVector = {
		x:
			scaledOutVector.x * Math.cos(angleTransform)
			+ scaledOutVector.y * Math.sin(angleTransform),
		y:
			scaledOutVector.y * Math.cos(angleTransform)
			- scaledOutVector.x * Math.sin(angleTransform),
	};

	changes[`${draggedItem.id}.x`] = transformedDeltaVector.x;
	changes[`${draggedItem.id}.y`] = transformedDeltaVector.y;
	changes[`${draggedItem.id}.handleIn.x`] = transformedInVector.x;
	changes[`${draggedItem.id}.handleIn.y`] = transformedInVector.y;
	changes[`${draggedItem.id}.handleOut.x`] = transformedOutVector.x;
	changes[`${draggedItem.id}.handleOut.y`] = transformedOutVector.y;

	changeGlyphManually(
		changes,
		glyph,
		manualChangeBatcher,
		globalMode,
		componentName,
	);
}

function changeSpacing(client, glyph, draggedItem, newPos) {
	if (draggedItem.id === 'spacingLeft') {
		client.dispatchAction('/change-letter-spacing', {
			value: glyph.spacingLeft - glyph.baseSpacingLeft - newPos.x,
			side: 'left',
			letter: String.fromCharCode(glyph.unicode),
		});
	}
	else if (draggedItem.id === 'spacingRight') {
		client.dispatchAction('/change-letter-spacing', {
			value:
				newPos.x
				- glyph.advanceWidth
				+ glyph.spacingRight
				- glyph.baseSpacingRight,
			side: 'right',
			letter: String.fromCharCode(glyph.unicode),
		});
	}
}

export default class GlyphCanvas extends React.PureComponent {
	constructor(props) {
		super(props);
		this.state = {
			rafId: 0,
			values: {},
			uiRuler: true,
			guides: [],
			error: null,
		};

		this.changeParam = this.changeParam.bind(this);
		this.download = this.download.bind(this);
	}

	componentWillMount() {
		pleaseWait.instance.finish();
		this.client = LocalClient.instance();
		this.manualChangeBatcher = new ManualChangeBatcher(this.client);
		this.lifespan = new Lifespan();

		this.client
			.getStore('/fontInstanceStore', this.lifespan)
			.onUpdate((head) => {
				if (
					this.state.glyph
					&& window.glyph
					&& this.state.glyph.name !== window.glyph.name
				) {
					this.resetAppMode = true;
				}
				this.setState({
					glyph: window.glyph,
					inputGlyphInteraction: head.toJS().d.inputGlyphInteraction,
					template: head.toJS().d.templateToLoad,
				});
			})
			.onDelete(() => {
				this.setState(undefined);
			});

		this.client
			.getStore('/prototypoStore', this.lifespan)
			.onUpdate((head) => {
				this.setState({
					canvasMode: head.toJS().d.canvasMode,
					uiOutline: head.toJS().d.uiOutline,
					uiRuler: head.toJS().d.uiRuler,
					family: head.toJS().d.family,
					variant: head.toJS().d.variant,
					uiText: head.toJS().d.uiText,
					uiWord: head.toJS().d.uiWord,
					glyphUnicode: head.toJS().d.glyphSelected,
					name: head.toJS().d.fontName,
					glyphs: head.toJS().d.glyphs,
				});
			})
			.onDelete(() => {
				this.setState(undefined);
			});

		this.client
			.getStore('/undoableStore', this.lifespan)
			.onUpdate((head) => {
				this.setState({
					values: head.toJS().d.controlsValues,
					guides: head.toJS().d.guides,
				});
			})
			.onDelete(() => {
				this.setState(undefined);
			});
	}

	componentDidMount() {
		this.toile = new Toile(this.canvas);
		this.setCamera(
			{x: 0, y: 0},
			1,
			this.canvas.clientHeight,
			this.canvas.clientWidth,
		);

		if (module.hot) {
			module.hot.accept('../toile/toile', () => {
				const ToileConstructor = require('../toile/toile').default; // eslint-disable-line global-require
				const [z, , , , tx, ty] = this.toile.viewMatrix;
				const newTs = {
					x: tx,
					y: ty,
				};

				this.toile = new ToileConstructor(this.canvas);

				this.setCameraCenter(
					newTs,
					z,
					-this.canvas.clientHeight,
					this.canvas.clientWidth,
				);
			});
		}

		this.startRaf();
	}

	startRaf() {
		const frameCounters = {
			componentMenu: 0,
		};
		let componentMenuPos = {};
		let componentHovered = {};
		let selectedItems = [];
		let contourSelected;
		const contourSelectedIndex = 0;
		// let dragging = true;
		let mouse = this.toile.getMouseState();
		let appStateValue = appState.DEFAULT;
		let preSelection;
		let appMode;
		let oldAppMode;
		let oldAppState;
		let oldViewMatrix;
		let oldCanvasMode;
		let mouseDoubleClick;
		let pause = false;
		let firstDraw = true;
		let mouseStart;
		let draggingNotStarted = false;
		let directionalNotStarted = false;
		let directionalValue;
		let globalMode = false;

		// Box select variables
		let mouseBoxStart;

		// on curve modification width and angle base points
		let onCurveBasePoints = {};

		const rafFunc = () => {
			try {
				// Handling all keyboard event
				let enteringPreview = false;
				let resetManualPoint = false;
				let displacementArrow;
				let supprPressed;
				let exitingPreview = false;
				let previewMode = false;
				let modRange = 1;
				let unparallelMod = false;
				let unsmoothMod = false;
				let curveMode = 0;
				let distrModification = false;
				let directionalModifier = false;
				let deleteMod = false;

				if (this.toile.keyboardUpRisingEdge.keyCode) {
					const {keyCode, special} = this.toile.keyboardUpRisingEdge;

					if (keyCode === 90 && !special) {
						exitingPreview = true;
					}
					else if (keyCode === 46 || keyCode === 8) {
						deleteMod = true;
					}
				}

				if (this.toile.keyboardUp.keyCode) {
					const {keyCode} = this.toile.keyboardUp;

					// Detect space keyboard up event to reset mode to the previous mode
					if (keyCode === 32) {
						appMode = oldAppMode;
						this.client.dispatchAction('/store-value', {
							canvasMode: oldAppMode,
						});
						if (appMode === 'select-points') {
							appStateValue = oldAppState;
						}

						oldAppMode = undefined;
						oldAppState = undefined;
					}
					this.toile.clearKeyboardInput();
				}

				if (this.toile.keyboardDownRisingEdge.keyCode) {
					const {keyCode, special} = this.toile.keyboardDownRisingEdge;

					console.log(keyCode);
					// On space edge down keyboard event switch to move mode
					// whatever the previous mode is
					if (keyCode === 32) {
						oldAppMode = this.state.canvasMode;
						oldAppState = appStateValue;
						appMode = canvasMode.MOVE;
						this.client.dispatchAction('/store-value', {canvasMode: 'move'});
					}
					else if (keyCode === 80) {
						pause = !pause;
					}
					else if (keyCode === 90 && !special) {
						enteringPreview = true;
					}
					else if (keyCode === 27) {
						resetManualPoint = true;
					}
					else if (keyCode <= 40 && keyCode >= 37) {
						displacementArrow = {
							left: keyCode === 37,
							up: keyCode === 38,
							right: keyCode === 39,
							down: keyCode === 40,
						};
					}
					else if (keyCode === 46 || keyCode === 8) {
						supprPressed = true;
						unsmoothMod = true;
					}
				}

				if (this.toile.keyboardDown.keyCode) {
					const {keyCode, special} = this.toile.keyboardDown;

					if (keyCode === 90 && !special) {
						previewMode = true;
					}
					else if (keyCode === 87) {
						// eslint-disable-next-line no-bitwise
						curveMode |= onCurveModMode.WIDTH_MOD;
					}
					else if (keyCode === 65) {
						// eslint-disable-next-line no-bitwise
						curveMode |= onCurveModMode.ANGLE_MOD;
					}
					else if (keyCode === 68) {
						distrModification = true;
					}

					// eslint-disable-next-line no-bitwise
					if (special & specialKey.SHIFT) {
						modRange = 10;
						directionalModifier = true;
					}
					// eslint-disable-next-line no-bitwise
					if (special & agnosticCtrl) {
						unparallelMod = true;
					}
					// eslint-disable-next-line no-bitwise
					if (special & specialKey.ALT) {
						unsmoothMod = true;
					}
				}

				/* eslint-disable no-bitwise, max-depth */
				const hotItems = this.toile.getHotInteractiveItem();

				let mouseMovement;

				if (mouseBoxStart) {
					hotItems.push(...this.toile.getBoxHotInteractiveItem(mouseBoxStart));
				}
				const width = this.canvas.clientWidth;
				const height = this.canvas.clientHeight;

				this.canvas.height = height;
				this.canvas.width = width;
				const oldMouse = mouse;
				let mouseClickRelease = false;

				mouse = this.toile.getMouseState();

				switch (this.state.canvasMode) {
				case 'components':
					appMode = canvasMode.COMPONENTS;
					// unselect everything but guides
					selectedItems = selectedItems.filter(
						item => item.type === toileType.GUIDE_HANDLE,
					);
					break;
				case 'select-points':
					appMode = canvasMode.SELECT_POINTS;
					break;
				case 'component-magic':
					appMode = canvasMode.SELECT_POINTS_COMPONENT;
					break;
				case 'move':
				default:
					appMode = canvasMode.MOVE;
					break;
				}

				if (mouse.state === mState.UP && oldMouse.state === mState.DOWN) {
					mouseClickRelease = true;
				}

				const {glyph} = this.state;

				if (glyph) {
					const nodes = hotItems.filter(
						item => item.type <= toileType.CONTOUR_NODE_OUT,
					);
					const spacingHandle = hotItems.filter(
						item => item.type === toileType.SPACING_HANDLE,
					);
					const guideHandle = hotItems.filter(
						item => item.type === toileType.GUIDE_HANDLE,
					);
					const contours = hotItems.filter(
						item =>
							item.type === toileType.GLYPH_CONTOUR
							|| item.type === toileType.GLYPH_COMPONENT_CONTOUR,
					);
					const globalContours = hotItems.filter(
						item => item.type === toileType.GLYPH_GLOBAL_COMPONENT_CONTOUR,
					);
					let componentMenu = hotItems.filter(
						item => item.type === toileType.COMPONENT_MENU_ITEM_CENTER,
					);
					let componentChoice = hotItems.filter(
						item => item.type === toileType.COMPONENT_MENU_ITEM,
					);
					const componentChoiceClass = hotItems.filter(
						item => item.type === toileType.COMPONENT_MENU_ITEM_CLASS,
					);
					let components = hotItems.filter(
						item =>
							item.type === toileType.COMPONENT_CHOICE
							|| item.type === toileType.COMPONENT_NONE_CHOICE,
					);
					const rulers = hotItems.filter(
						item => item.type === toileType.RULER,
					);

					const [mousePosInWorld] = transformCoords(
						[mouse.pos],
						inverseProjectionMatrix(this.toile.viewMatrix),
						this.toile.height / this.toile.viewMatrix[0],
					);

					// This is used to detect a glyph change event that
					// makes it necessary to stop all active mode
					if (this.resetAppMode) {
						appStateValue = appState.DEFAULT;
						selectedItems = [];
						this.resetAppMode = false;
					}

					const outside = this.toile.glyphOutsideView(glyph);

					if (outside && !this.props.glyphOutsideView) {
						this.client.dispatchAction('/store-value', {
							glyphOutsideView: true,
						});
					}
					else if (!outside && this.props.glyphOutsideView) {
						this.client.dispatchAction('/store-value', {
							glyphOutsideView: false,
						});
					}

					// Detection of double click in any mode
					if (mouse.edge === mState.DOWN) {
						// resetting the view is only possible when nothing is selected
						if (
							mouseDoubleClick
							&& (appStateValue === appState.DEFAULT
								|| appStateValue
									& (appState.CONTOUR_SELECTED
										| appState.CONTOUR_GLOBAL_SELECTED))
						) {
							this.resetView(glyph, height, width);
						}
						else {
							mouseDoubleClick = true;
							setTimeout(() => {
								mouseDoubleClick = false;
							}, 300);
						}
					}

					// Managing guides and rulers
					if (
						mouse.edge === mState.DOWN
						&& !(
							appStateValue
							& (appState.DRAGGING_CONTOUR
								| appState.DRAGGING_POINTS
								| appState.DRAGGING_SPACING)
						)
					) {
						if (guideHandle.length > 0 && nodes.length === 0) {
							appStateValue = appState.DRAGGING_GUIDE;
							selectedItems = [guideHandle[0]];
							this.storeSelectedItems(selectedItems);
						}
						else if (rulers.length > 0) {
							const axe = rulers[0].id === 'verticalRuler' ? 'x' : 'y';
							const newGuide = {
								id: `guide${Date.now()}`,
								[axe]: mouse.pos[axe],
								isNew: true,
							};

							this.client.dispatchAction('/change-guides', {
								guides: this.state.guides.concat(newGuide),
							});

							appStateValue = appState.DRAGGING_GUIDE;
							selectedItems = [
								{
									id: newGuide.id,
									type: toileType.GUIDE_HANDLE,
									data: {
										x: newGuide.x,
										y: newGuide.y,
										isNew: true,
									},
								},
							];
							this.storeSelectedItems(selectedItems);
						}
					}
					else if (appStateValue & appState.GUIDE_SELECTED) {
						if (mouseClickRelease) {
							appStateValue = appState.DEFAULT;
							selectedItems = [];
							this.storeSelectedItems(selectedItems);
						}
						else if (deleteMod) {
							this.client.dispatchAction('/change-guides', {
								guides: this.state.guides.filter(
									guide => guide.id !== selectedItems[0].id,
								),
								label: 'delete guide',
								force: true,
							});

							appStateValue = appState.DEFAULT;
							selectedItems = [];
							this.storeSelectedItems(selectedItems);
						}
					}
					else if (
						appStateValue & appState.DRAGGING_GUIDE
						&& mouseClickRelease
					) {
						// deleting the guide when it has been released on a ruler
						if (selectedItems[0].type === toileType.GUIDE_HANDLE) {
							if (rulers.length > 0) {
								this.client.dispatchAction('/change-guides', {
									guides: this.state.guides.filter(
										guide => guide.id !== selectedItems[0].id,
									),
									label: 'delete guide',
									force: true,
								});
								appStateValue = appState.DEFAULT;
							}
							else {
								let label = 'move guide';

								if (selectedItems[0].data.isNew) {
									delete selectedItems[0].data.isNew;
									label = 'add guide';
								}

								this.client.dispatchAction('/change-guides', {
									guides: this.state.guides,
									label,
									force: true,
								});
								appStateValue = appState.GUIDE_SELECTED;
								this.storeSelectedItems(selectedItems);
							}
						}
					}

					// =========================================================
					// =========================================================
					// This is the state machine state changing part
					// There is 3 first level state
					if (
						appMode === canvasMode.MOVE
						&& !(
							appStateValue
							& (appState.GUIDE_SELECTED | appState.DRAGGING_GUIDE)
						)
					) {
						if (mouse.state === mState.DOWN) {
							appStateValue = appState.MOVING;
						}
						else {
							appStateValue = appState.DEFAULT;
						}
					}
					if (appMode === canvasMode.COMPONENTS) {
						// On mouse release with look for any hot menu item
						// and change component accordingly
						if (mouseClickRelease) {
							if (componentChoice.length > 0) {
								const [choice] = componentChoice;

								this.client.dispatchAction('/change-component', {
									glyph,
									id: choice.data.componentId,
									name: choice.data.baseId,
								});

								componentChoice = [];
								componentMenu = [];
								components = [];
							}
							if (componentChoiceClass.length > 0) {
								const [choice] = componentChoiceClass;

								this.client.dispatchAction('/change-component-class', {
									componentClass: choice.data.componentClass,
									name: choice.data.baseId,
								});

								componentChoice = [];
								componentMenu = [];
								components = [];
							}
						}

						// If a component geometry is hovered
						// We set the correct mode to draw it
						if (
							(appStateValue === appState.DEFAULT
								|| appStateValue === appState.COMPONENT_HOVERED)
							&& components.length > 0
						) {
							const [candidateComp] = components;

							if (candidateComp.data.bases.length > 1) {
								appStateValue = appState.COMPONENT_HOVERED;
								componentHovered = candidateComp;
							}
						}
						else if (componentMenu.length > 0) {
							appStateValue = appState.COMPONENT_MENU_HOVERED;
						}
						else if (
							!(
								appStateValue
								& (appState.GUIDE_SELECTED | appState.DRAGGING_GUIDE)
							)
						) {
							componentHovered = {};
							appStateValue = appState.DEFAULT;
						}
					}
					if (
						appMode === canvasMode.SELECT_POINTS
						|| appMode === canvasMode.SELECT_POINTS_COMPONENT
					) {
						// Manual edition mode
						if (
							appStateValue === appState.DEFAULT
							&& mouse.edge === mState.DOWN
						) {
							if (nodes.length > 0) {
								const [currentNode] = nodes;

								selectedItems = [currentNode];
								appStateValue = appState.DRAGGING_POINTS;
								mouseStart = currentNode.data.center;
								console.log(mouseStart);
								if (currentNode.type === toileType.NODE) {
									onCurveBasePoints.current = currentNode.data.center;
									onCurveBasePoints.opposite = _get(
										glyph,
										currentNode.data.oppositeId,
									);
								}
								draggingNotStarted = true;
								directionalNotStarted = true;
							}
							else if (spacingHandle.length > 0) {
								appStateValue = appState.DRAGGING_SPACING;
								selectedItems = [spacingHandle[0]];
							}
							else {
								appStateValue = appState.BOX_SELECTING;
								mouseBoxStart = mouse.pos;
							}
							this.storeSelectedItems(selectedItems);
						}
						else if (
							appStateValue & appState.BOX_SELECTING
							&& mouseClickRelease
						) {
							if (hotItems.length > 0) {
								if (
									this.toile.keyboardDown.keyCode
									&& this.toile.keyboardDown.special & specialKey.SHIFT
								) {
									nodes.forEach((node) => {
										let toAdd = true;
										let toRemove;

										selectedItems.forEach((item, idx) => {
											if (node.id == item.id) {
												toRemove = idx;
												toAdd = false;
											}
										});

										if (toAdd) {
											selectedItems.push(node);
										}
									});
									appStateValue = appState.POINTS_SELECTED;
									mouseBoxStart = undefined;
								}
								else {
									selectedItems = hotItems;
									appStateValue = appState.POINTS_SELECTED;
									mouseBoxStart = undefined;
									globalMode = false;
								}
							}
							else {
								selectedItems = [];
								appStateValue = appState.DEFAULT;
								mouseBoxStart = undefined;
								globalMode = false;
							}
							this.client.dispatchAction('/store-value', {
								globalMode,
							});
							this.storeSelectedItems(selectedItems);
						}
						else if (
							appStateValue & appState.DRAGGING_SPACING
							&& mouseClickRelease
						) {
							appStateValue = appState.SPACING_SELECTED;
							draggingNotStarted = false;
							directionalNotStarted = false;
							this.client.dispatchAction('/change-glyph-node-manually', {
								label: 'manual edition',
								force: true,
								changes: {},
							});
						}
						else if (
							appStateValue & appState.DRAGGING_GUIDE
							&& mouseClickRelease
						) {
							appStateValue = appState.GUIDE_SELECTED;
							draggingNotStarted = false;
							directionalNotStarted = false;
							this.client.dispatchAction('/change-glyph-node-manually', {
								label: 'manual edition',
								force: true,
								changes: {},
							});
						}
						else if (
							appStateValue & appState.DRAGGING_POINTS
							&& mouseClickRelease
						) {
							appStateValue = appState.POINTS_SELECTED;
							draggingNotStarted = false;
							directionalNotStarted = false;
							this.client.dispatchAction('/change-glyph-node-manually', {
								label: 'manual edition',
								force: true,
								changes: {},
							});
						}
						else if (
							appStateValue & appState.POINTS_SELECTED
							&& mouse.edge === mState.DOWN
						) {
							if (nodes.length > 0) {
								const selectedHot = nodes.filter(node =>
									selectedItems.reduce(
										(acc, item) => acc || item.id === node.id,
										false,
									),
								);
								const notSelectedHot = nodes.filter(
									node =>
										!selectedItems.reduce(
											(acc, item) => acc || item.id === node.id,<commit_msg>fixing selecting a point under the panel by selecting points on mouse click release<commit_after>/* global webkitRequestAnimationFrame, webkitCancelAnimationFrame */
import _get from 'lodash/get';
import _slice from 'lodash/slice';
import React from 'react';
import pleaseWait from 'please-wait';
import Lifespan from 'lifespan';

import Toile, {
	mState,
	toileType,
	appState,
	transformCoords,
	inverseProjectionMatrix,
	canvasMode,
	specialKey,
} from '../toile/toile';

import {
	changeTransformOrigin,
	glyphBoundingBox,
} from '../prototypo.js/utils/generic';
import {
	matrixMul,
	dot2D,
	mulScalar2D,
	subtract2D,
	normalize2D,
	add2D,
	distance2D,
	round2D,
} from '../prototypo.js/utils/linear';

import {rawToEscapedContent} from '../helpers/input-transform.helpers';

import LocalClient from '../stores/local-client.stores';

import FontUpdater from './font-updater.components';

const raf = requestAnimationFrame || webkitRequestAnimationFrame;
const rafCancel = cancelAnimationFrame || webkitCancelAnimationFrame;

const MINIMUM_DRAG_THRESHOLD = 6;
const MINIMUM_DRAG_DIRECTIONAL_THRESHOLD = 10;

const agnosticCtrl
	= navigator.platform.indexOf('Mac') === -1 ? specialKey.CTRL : specialKey.META;

const directionalMod = {
	X: 0b1,
	Y: 0b10,
};

const onCurveModMode = {
	WIDTH_MOD: 0b1,
	ANGLE_MOD: 0b10,
};

class ManualChangeBatcher {
	constructor(client) {
		this.changes = {};
		this.client = client;
	}

	addManualChange(change, glyphName) {
		this.changes = {
			...this.changes,
			[glyphName]: {
				...this.changes[glyphName],
				...change,
			},
		};
	}

	applyManualChange() {
		const keys = Object.keys(this.changes);

		for (let i = 0; i < keys.length; i++) {
			const glyphName = keys[i];
			const changes = this.changes[glyphName];

			this.client.dispatchAction('/change-glyph-node-manually', {
				changes,
				glyphName,
				label: 'manual edition',
			});
		}
		this.changes = {};
	}
}

export function changeGlyphManually(
	changes,
	glyph,
	manualChangeBatcher,
	globalMode,
	componentName,
) {
	const glyphName = globalMode ? componentName : glyph.base || glyph.name;

	manualChangeBatcher.addManualChange(changes, glyphName);
}

function calculateHandleCoordinateModification(parentRef, newPos, opHandle) {
	const d = subtract2D(newPos, parentRef);
	const zero = {x: 0, y: 0};
	const dr = distance2D(d, zero);
	const radius = distance2D(opHandle, parentRef);
	let sign = 1;

	if (d.y !== 0) {
		sign = Math.sign(d.y);
	}
	const pointa = mulScalar2D(1 / dr ** 2, {
		x: sign * d.x * Math.sqrt(radius ** 2 * dr ** 2),
		y: Math.abs(d.y) * Math.sqrt(radius ** 2 * dr ** 2),
	});
	const pointb = mulScalar2D(1 / dr ** 2, {
		x: -sign * d.x * Math.sqrt(radius ** 2 * dr ** 2),
		y: -Math.abs(d.y) * Math.sqrt(radius ** 2 * dr ** 2),
	});

	const point = dot2D(d, pointa) <= 0 ? pointa : pointb;

	return add2D(point, parentRef);
}

export function handleModification(
	manualChangeBatcher,
	glyph,
	draggedItem,
	newPos,
	unsmoothMod,
	unparallelMod,
	globalMode,
	toile,
) {
	const {parentId, transforms, componentName, opId} = draggedItem.data;
	const handle = _get(glyph, draggedItem.id);
	const opHandle = _get(glyph, opId);
	const handlePos = {
		x: handle.xBase,
		y: handle.yBase,
	};
	const parent = _get(glyph, parentId);
	let xTransform = 1;
	let yTransform = 1;
	let angleTransform = 0;

	for (let i = 0; i < transforms.length; i++) {
		const transform = transforms[i];

		if (transform) {
			xTransform
				/= transform.name.indexOf('scaleX') === -1 ? 1 : transform.param;
			yTransform
				/= transform.name.indexOf('scaleY') === -1 ? 1 : transform.param;
			angleTransform
				+= transform.name.indexOf('rotate') === -1 ? 0 : transform.param;
		}
	}
	const newVectorPreTransform = subtract2D(newPos, handlePos);
	const newVectorScale = {
		x: newVectorPreTransform.x * xTransform,
		y: newVectorPreTransform.y * yTransform,
	};
	const newVector = {
		x:
			newVectorScale.x * Math.cos(angleTransform)
			+ newVectorScale.y * Math.sin(angleTransform),
		y:
			newVectorScale.y * Math.cos(angleTransform)
			- newVectorScale.x * Math.sin(angleTransform),
	};
	const refLength = distance2D(newPos, parent);
	const tension = refLength / distance2D(handlePos, parent);
	const isIn
		= toileType.NODE_IN === draggedItem.type
		|| toileType.CONTOUR_NODE_IN === draggedItem.type;

	const direction = isIn ? 'handleIn' : 'handleOut';
	const oppositeDirection = isIn ? 'handleOut' : 'handleIn';

	const changes = {
		[`${parentId}.${direction}.x`]: newVector.x,
		[`${parentId}.${direction}.y`]: newVector.y,
	};

	if (
		!unsmoothMod
		&& (parent.baseTypeIn === 'smooth' || parent.baseTypeOut === 'smooth')
	) {
		const opVector = calculateHandleCoordinateModification(
			parent,
			newPos,
			opHandle,
		);
		const modVector = subtract2D(opVector, {
			x: opHandle.xBase * xTransform,
			y: opHandle.yBase * yTransform,
		});
		const transformedModVector = {
			x:
				modVector.x * Math.cos(angleTransform)
				+ modVector.y * Math.sin(angleTransform),
			y:
				modVector.y * Math.cos(angleTransform)
				- modVector.x * Math.sin(angleTransform),
		};

		changes[`${parentId}.${oppositeDirection}.x`] = transformedModVector.x;
		changes[`${parentId}.${oppositeDirection}.y`] = transformedModVector.y;
	}

	changeGlyphManually(
		changes,
		glyph,
		manualChangeBatcher,
		globalMode,
		componentName,
	);
}

export function onCurveModification(
	manualChangeBatcher,
	glyph,
	draggedItem,
	onCurveBasePoints,
	newPos,
	appStateValue,
	modToApply,
	globalMode,
) {
	const {
		baseWidth,
		oppositeId,
		baseAngle,
		skeleton,
		angleOffset,
		transforms,
		componentName,
	} = draggedItem.data;

	const current = _get(glyph, draggedItem.id);
	const opposite = onCurveBasePoints.opposite;
	const currentBase = onCurveBasePoints.current;
	let posToUse = newPos;

	if (modToApply & onCurveModMode.WIDTH_MOD && opposite && currentBase) {
		const newBaseVector = normalize2D(subtract2D(currentBase, opposite));
		const newBaseAngle = Math.atan2(newBaseVector.y, newBaseVector.x);
		const translateVector = subtract2D({x: 0, y: 0}, opposite);
		const translatedPos = add2D(translateVector, newPos);
		const newBaseProjectedPoint = {
			x:
				translatedPos.x * Math.cos(newBaseAngle)
				+ translatedPos.y * Math.sin(newBaseAngle),
			y:
				-translatedPos.x * Math.sin(newBaseAngle)
				+ translatedPos.y * Math.cos(newBaseAngle),
		};

		posToUse = add2D(opposite, {
			x: newBaseProjectedPoint.x * newBaseVector.x,
			y: newBaseProjectedPoint.x * newBaseVector.y,
		});
	}

	if (modToApply & onCurveModMode.ANGLE_MOD && opposite && currentBase) {
		const previousLength = distance2D(currentBase, opposite);
		const direction = normalize2D(subtract2D(posToUse, opposite));

		posToUse = add2D(opposite, mulScalar2D(previousLength, direction));
	}

	const deltaVector = round2D(
		subtract2D(posToUse, round2D({x: current.xBase, y: current.yBase})),
	);

	const inOffest = round2D(subtract2D(current.handleIn, current));
	const outOffset = round2D(subtract2D(current.handleOut, current));

	const inNewPos = round2D(add2D(posToUse, inOffest));
	const outNewPos = round2D(add2D(posToUse, outOffset));

	const inVector = round2D(
		subtract2D(inNewPos, {
			x: current.handleIn.xBase,
			y: current.handleIn.yBase,
		}),
	);
	const outVector = round2D(
		subtract2D(outNewPos, {
			x: current.handleOut.xBase,
			y: current.handleOut.yBase,
		}),
	);

	const changes = {};

	let xTransform = 1;
	let yTransform = 1;
	let angleTransform = 0;

	for (let i = 0; i < transforms.length; i++) {
		const transform = transforms[i];

		if (transform) {
			xTransform
				/= transform.name.indexOf('scaleX') === -1 ? 1 : transform.param;
			yTransform
				/= transform.name.indexOf('scaleY') === -1 ? 1 : transform.param;
			angleTransform
				+= transform.name.indexOf('rotate') === -1 ? 0 : transform.param;
		}
	}

	const scaledDeltaVector = {
		x: deltaVector.x * xTransform,
		y: deltaVector.y * yTransform,
	};
	const scaledInVector = {
		x: inVector.x * xTransform,
		y: inVector.y * yTransform,
	};
	const scaledOutVector = {
		x: outVector.x * xTransform,
		y: outVector.y * yTransform,
	};

	const transformedDeltaVector = {
		x:
			scaledDeltaVector.x * Math.cos(angleTransform)
			+ scaledDeltaVector.y * Math.sin(angleTransform),
		y:
			scaledDeltaVector.y * Math.cos(angleTransform)
			- scaledDeltaVector.x * Math.sin(angleTransform),
	};
	const transformedInVector = {
		x:
			scaledInVector.x * Math.cos(angleTransform)
			+ scaledInVector.y * Math.sin(angleTransform),
		y:
			scaledInVector.y * Math.cos(angleTransform)
			- scaledInVector.x * Math.sin(angleTransform),
	};
	const transformedOutVector = {
		x:
			scaledOutVector.x * Math.cos(angleTransform)
			+ scaledOutVector.y * Math.sin(angleTransform),
		y:
			scaledOutVector.y * Math.cos(angleTransform)
			- scaledOutVector.x * Math.sin(angleTransform),
	};

	changes[`${draggedItem.id}.x`] = transformedDeltaVector.x;
	changes[`${draggedItem.id}.y`] = transformedDeltaVector.y;
	changes[`${draggedItem.id}.handleIn.x`] = transformedInVector.x;
	changes[`${draggedItem.id}.handleIn.y`] = transformedInVector.y;
	changes[`${draggedItem.id}.handleOut.x`] = transformedOutVector.x;
	changes[`${draggedItem.id}.handleOut.y`] = transformedOutVector.y;

	changeGlyphManually(
		changes,
		glyph,
		manualChangeBatcher,
		globalMode,
		componentName,
	);
}

function changeSpacing(client, glyph, draggedItem, newPos) {
	if (draggedItem.id === 'spacingLeft') {
		client.dispatchAction('/change-letter-spacing', {
			value: glyph.spacingLeft - glyph.baseSpacingLeft - newPos.x,
			side: 'left',
			letter: String.fromCharCode(glyph.unicode),
		});
	}
	else if (draggedItem.id === 'spacingRight') {
		client.dispatchAction('/change-letter-spacing', {
			value:
				newPos.x
				- glyph.advanceWidth
				+ glyph.spacingRight
				- glyph.baseSpacingRight,
			side: 'right',
			letter: String.fromCharCode(glyph.unicode),
		});
	}
}

export default class GlyphCanvas extends React.PureComponent {
	constructor(props) {
		super(props);
		this.state = {
			rafId: 0,
			values: {},
			uiRuler: true,
			guides: [],
			error: null,
		};

		this.changeParam = this.changeParam.bind(this);
		this.download = this.download.bind(this);
	}

	componentWillMount() {
		pleaseWait.instance.finish();
		this.client = LocalClient.instance();
		this.manualChangeBatcher = new ManualChangeBatcher(this.client);
		this.lifespan = new Lifespan();

		this.client
			.getStore('/fontInstanceStore', this.lifespan)
			.onUpdate((head) => {
				if (
					this.state.glyph
					&& window.glyph
					&& this.state.glyph.name !== window.glyph.name
				) {
					this.resetAppMode = true;
				}
				this.setState({
					glyph: window.glyph,
					inputGlyphInteraction: head.toJS().d.inputGlyphInteraction,
					template: head.toJS().d.templateToLoad,
				});
			})
			.onDelete(() => {
				this.setState(undefined);
			});

		this.client
			.getStore('/prototypoStore', this.lifespan)
			.onUpdate((head) => {
				this.setState({
					canvasMode: head.toJS().d.canvasMode,
					uiOutline: head.toJS().d.uiOutline,
					uiRuler: head.toJS().d.uiRuler,
					family: head.toJS().d.family,
					variant: head.toJS().d.variant,
					uiText: head.toJS().d.uiText,
					uiWord: head.toJS().d.uiWord,
					glyphUnicode: head.toJS().d.glyphSelected,
					name: head.toJS().d.fontName,
					glyphs: head.toJS().d.glyphs,
				});
			})
			.onDelete(() => {
				this.setState(undefined);
			});

		this.client
			.getStore('/undoableStore', this.lifespan)
			.onUpdate((head) => {
				this.setState({
					values: head.toJS().d.controlsValues,
					guides: head.toJS().d.guides,
				});
			})
			.onDelete(() => {
				this.setState(undefined);
			});
	}

	componentDidMount() {
		this.toile = new Toile(this.canvas);
		this.setCamera(
			{x: 0, y: 0},
			1,
			this.canvas.clientHeight,
			this.canvas.clientWidth,
		);

		if (module.hot) {
			module.hot.accept('../toile/toile', () => {
				const ToileConstructor = require('../toile/toile').default; // eslint-disable-line global-require
				const [z, , , , tx, ty] = this.toile.viewMatrix;
				const newTs = {
					x: tx,
					y: ty,
				};

				this.toile = new ToileConstructor(this.canvas);

				this.setCameraCenter(
					newTs,
					z,
					-this.canvas.clientHeight,
					this.canvas.clientWidth,
				);
			});
		}

		this.startRaf();
	}

	startRaf() {
		const frameCounters = {
			componentMenu: 0,
		};
		let componentMenuPos = {};
		let componentHovered = {};
		let selectedItems = [];
		let contourSelected;
		const contourSelectedIndex = 0;
		// let dragging = true;
		let mouse = this.toile.getMouseState();
		let appStateValue = appState.DEFAULT;
		let preSelection;
		let appMode;
		let oldAppMode;
		let oldAppState;
		let oldViewMatrix;
		let oldCanvasMode;
		let mouseDoubleClick;
		let pause = false;
		let firstDraw = true;
		let mouseStart;
		let draggingNotStarted = false;
		let directionalNotStarted = false;
		let directionalValue;
		let globalMode = false;

		// Box select variables
		let mouseBoxStart;

		// on curve modification width and angle base points
		let onCurveBasePoints = {};

		const rafFunc = () => {
			try {
				// Handling all keyboard event
				let enteringPreview = false;
				let resetManualPoint = false;
				let displacementArrow;
				let supprPressed;
				let exitingPreview = false;
				let previewMode = false;
				let modRange = 1;
				let unparallelMod = false;
				let unsmoothMod = false;
				let curveMode = 0;
				let distrModification = false;
				let directionalModifier = false;
				let deleteMod = false;

				if (this.toile.keyboardUpRisingEdge.keyCode) {
					const {keyCode, special} = this.toile.keyboardUpRisingEdge;

					if (keyCode === 90 && !special) {
						exitingPreview = true;
					}
					else if (keyCode === 46 || keyCode === 8) {
						deleteMod = true;
					}
				}

				if (this.toile.keyboardUp.keyCode) {
					const {keyCode} = this.toile.keyboardUp;

					// Detect space keyboard up event to reset mode to the previous mode
					if (keyCode === 32) {
						appMode = oldAppMode;
						this.client.dispatchAction('/store-value', {
							canvasMode: oldAppMode,
						});
						if (appMode === 'select-points') {
							appStateValue = oldAppState;
						}

						oldAppMode = undefined;
						oldAppState = undefined;
					}
					this.toile.clearKeyboardInput();
				}

				if (this.toile.keyboardDownRisingEdge.keyCode) {
					const {keyCode, special} = this.toile.keyboardDownRisingEdge;

					console.log(keyCode);
					// On space edge down keyboard event switch to move mode
					// whatever the previous mode is
					if (keyCode === 32) {
						oldAppMode = this.state.canvasMode;
						oldAppState = appStateValue;
						appMode = canvasMode.MOVE;
						this.client.dispatchAction('/store-value', {canvasMode: 'move'});
					}
					else if (keyCode === 80) {
						pause = !pause;
					}
					else if (keyCode === 90 && !special) {
						enteringPreview = true;
					}
					else if (keyCode === 27) {
						resetManualPoint = true;
					}
					else if (keyCode <= 40 && keyCode >= 37) {
						displacementArrow = {
							left: keyCode === 37,
							up: keyCode === 38,
							right: keyCode === 39,
							down: keyCode === 40,
						};
					}
					else if (keyCode === 46 || keyCode === 8) {
						supprPressed = true;
						unsmoothMod = true;
					}
				}

				if (this.toile.keyboardDown.keyCode) {
					const {keyCode, special} = this.toile.keyboardDown;

					if (keyCode === 90 && !special) {
						previewMode = true;
					}
					else if (keyCode === 87) {
						// eslint-disable-next-line no-bitwise
						curveMode |= onCurveModMode.WIDTH_MOD;
					}
					else if (keyCode === 65) {
						// eslint-disable-next-line no-bitwise
						curveMode |= onCurveModMode.ANGLE_MOD;
					}
					else if (keyCode === 68) {
						distrModification = true;
					}

					// eslint-disable-next-line no-bitwise
					if (special & specialKey.SHIFT) {
						modRange = 10;
						directionalModifier = true;
					}
					// eslint-disable-next-line no-bitwise
					if (special & agnosticCtrl) {
						unparallelMod = true;
					}
					// eslint-disable-next-line no-bitwise
					if (special & specialKey.ALT) {
						unsmoothMod = true;
					}
				}

				/* eslint-disable no-bitwise, max-depth */
				const hotItems = this.toile.getHotInteractiveItem();

				let mouseMovement;

				if (mouseBoxStart) {
					hotItems.push(...this.toile.getBoxHotInteractiveItem(mouseBoxStart));
				}
				const width = this.canvas.clientWidth;
				const height = this.canvas.clientHeight;

				this.canvas.height = height;
				this.canvas.width = width;
				const oldMouse = mouse;
				let mouseClickRelease = false;

				mouse = this.toile.getMouseState();

				switch (this.state.canvasMode) {
				case 'components':
					appMode = canvasMode.COMPONENTS;
					// unselect everything but guides
					selectedItems = selectedItems.filter(
						item => item.type === toileType.GUIDE_HANDLE,
					);
					break;
				case 'select-points':
					appMode = canvasMode.SELECT_POINTS;
					break;
				case 'component-magic':
					appMode = canvasMode.SELECT_POINTS_COMPONENT;
					break;
				case 'move':
				default:
					appMode = canvasMode.MOVE;
					break;
				}

				if (mouse.state === mState.UP && oldMouse.state === mState.DOWN) {
					mouseClickRelease = true;
				}

				const {glyph} = this.state;

				if (glyph) {
					const nodes = hotItems.filter(
						item => item.type <= toileType.CONTOUR_NODE_OUT,
					);
					const spacingHandle = hotItems.filter(
						item => item.type === toileType.SPACING_HANDLE,
					);
					const guideHandle = hotItems.filter(
						item => item.type === toileType.GUIDE_HANDLE,
					);
					const contours = hotItems.filter(
						item =>
							item.type === toileType.GLYPH_CONTOUR
							|| item.type === toileType.GLYPH_COMPONENT_CONTOUR,
					);
					const globalContours = hotItems.filter(
						item => item.type === toileType.GLYPH_GLOBAL_COMPONENT_CONTOUR,
					);
					let componentMenu = hotItems.filter(
						item => item.type === toileType.COMPONENT_MENU_ITEM_CENTER,
					);
					let componentChoice = hotItems.filter(
						item => item.type === toileType.COMPONENT_MENU_ITEM,
					);
					const componentChoiceClass = hotItems.filter(
						item => item.type === toileType.COMPONENT_MENU_ITEM_CLASS,
					);
					let components = hotItems.filter(
						item =>
							item.type === toileType.COMPONENT_CHOICE
							|| item.type === toileType.COMPONENT_NONE_CHOICE,
					);
					const rulers = hotItems.filter(
						item => item.type === toileType.RULER,
					);

					const [mousePosInWorld] = transformCoords(
						[mouse.pos],
						inverseProjectionMatrix(this.toile.viewMatrix),
						this.toile.height / this.toile.viewMatrix[0],
					);

					// This is used to detect a glyph change event that
					// makes it necessary to stop all active mode
					if (this.resetAppMode) {
						appStateValue = appState.DEFAULT;
						selectedItems = [];
						this.resetAppMode = false;
					}

					const outside = this.toile.glyphOutsideView(glyph);

					if (outside && !this.props.glyphOutsideView) {
						this.client.dispatchAction('/store-value', {
							glyphOutsideView: true,
						});
					}
					else if (!outside && this.props.glyphOutsideView) {
						this.client.dispatchAction('/store-value', {
							glyphOutsideView: false,
						});
					}

					// Detection of double click in any mode
					if (mouse.edge === mState.DOWN) {
						// resetting the view is only possible when nothing is selected
						if (
							mouseDoubleClick
							&& (appStateValue === appState.DEFAULT
								|| appStateValue
									& (appState.CONTOUR_SELECTED
										| appState.CONTOUR_GLOBAL_SELECTED))
						) {
							this.resetView(glyph, height, width);
						}
						else {
							mouseDoubleClick = true;
							setTimeout(() => {
								mouseDoubleClick = false;
							}, 300);
						}
					}

					// Managing guides and rulers
					if (
						mouse.edge === mState.DOWN
						&& !(
							appStateValue
							& (appState.DRAGGING_CONTOUR
								| appState.DRAGGING_POINTS
								| appState.DRAGGING_SPACING)
						)
					) {
						if (guideHandle.length > 0 && nodes.length === 0) {
							appStateValue = appState.DRAGGING_GUIDE;
							selectedItems = [guideHandle[0]];
							this.storeSelectedItems(selectedItems);
						}
						else if (rulers.length > 0) {
							const axe = rulers[0].id === 'verticalRuler' ? 'x' : 'y';
							const newGuide = {
								id: `guide${Date.now()}`,
								[axe]: mouse.pos[axe],
								isNew: true,
							};

							this.client.dispatchAction('/change-guides', {
								guides: this.state.guides.concat(newGuide),
							});

							appStateValue = appState.DRAGGING_GUIDE;
							selectedItems = [
								{
									id: newGuide.id,
									type: toileType.GUIDE_HANDLE,
									data: {
										x: newGuide.x,
										y: newGuide.y,
										isNew: true,
									},
								},
							];
							this.storeSelectedItems(selectedItems);
						}
					}
					else if (appStateValue & appState.GUIDE_SELECTED) {
						if (mouseClickRelease) {
							appStateValue = appState.DEFAULT;
							selectedItems = [];
							this.storeSelectedItems(selectedItems);
						}
						else if (deleteMod) {
							this.client.dispatchAction('/change-guides', {
								guides: this.state.guides.filter(
									guide => guide.id !== selectedItems[0].id,
								),
								label: 'delete guide',
								force: true,
							});

							appStateValue = appState.DEFAULT;
							selectedItems = [];
							this.storeSelectedItems(selectedItems);
						}
					}
					else if (
						appStateValue & appState.DRAGGING_GUIDE
						&& mouseClickRelease
					) {
						// deleting the guide when it has been released on a ruler
						if (selectedItems[0].type === toileType.GUIDE_HANDLE) {
							if (rulers.length > 0) {
								this.client.dispatchAction('/change-guides', {
									guides: this.state.guides.filter(
										guide => guide.id !== selectedItems[0].id,
									),
									label: 'delete guide',
									force: true,
								});
								appStateValue = appState.DEFAULT;
							}
							else {
								let label = 'move guide';

								if (selectedItems[0].data.isNew) {
									delete selectedItems[0].data.isNew;
									label = 'add guide';
								}

								this.client.dispatchAction('/change-guides', {
									guides: this.state.guides,
									label,
									force: true,
								});
								appStateValue = appState.GUIDE_SELECTED;
								this.storeSelectedItems(selectedItems);
							}
						}
					}

					// =========================================================
					// =========================================================
					// This is the state machine state changing part
					// There is 3 first level state
					if (
						appMode === canvasMode.MOVE
						&& !(
							appStateValue
							& (appState.GUIDE_SELECTED | appState.DRAGGING_GUIDE)
						)
					) {
						if (mouse.state === mState.DOWN) {
							appStateValue = appState.MOVING;
						}
						else {
							appStateValue = appState.DEFAULT;
						}
					}
					if (appMode === canvasMode.COMPONENTS) {
						// On mouse release with look for any hot menu item
						// and change component accordingly
						if (mouseClickRelease) {
							if (componentChoice.length > 0) {
								const [choice] = componentChoice;

								this.client.dispatchAction('/change-component', {
									glyph,
									id: choice.data.componentId,
									name: choice.data.baseId,
								});

								componentChoice = [];
								componentMenu = [];
								components = [];
							}
							if (componentChoiceClass.length > 0) {
								const [choice] = componentChoiceClass;

								this.client.dispatchAction('/change-component-class', {
									componentClass: choice.data.componentClass,
									name: choice.data.baseId,
								});

								componentChoice = [];
								componentMenu = [];
								components = [];
							}
						}

						// If a component geometry is hovered
						// We set the correct mode to draw it
						if (
							(appStateValue === appState.DEFAULT
								|| appStateValue === appState.COMPONENT_HOVERED)
							&& components.length > 0
						) {
							const [candidateComp] = components;

							if (candidateComp.data.bases.length > 1) {
								appStateValue = appState.COMPONENT_HOVERED;
								componentHovered = candidateComp;
							}
						}
						else if (componentMenu.length > 0) {
							appStateValue = appState.COMPONENT_MENU_HOVERED;
						}
						else if (
							!(
								appStateValue
								& (appState.GUIDE_SELECTED | appState.DRAGGING_GUIDE)
							)
						) {
							componentHovered = {};
							appStateValue = appState.DEFAULT;
						}
					}
					if (
						appMode === canvasMode.SELECT_POINTS
						|| appMode === canvasMode.SELECT_POINTS_COMPONENT
					) {
						// Manual edition mode
						if (
							appStateValue === appState.DEFAULT
							&& mouse.edge === mState.DOWN
						) {
							if (nodes.length > 0) {
								const [currentNode] = nodes;

								selectedItems = [currentNode];
								appStateValue = appState.DRAGGING_POINTS;
								mouseStart = currentNode.data.center;
								console.log(mouseStart);
								if (currentNode.type === toileType.NODE) {
									onCurveBasePoints.current = currentNode.data.center;
									onCurveBasePoints.opposite = _get(
										glyph,
										currentNode.data.oppositeId,
									);
								}
								draggingNotStarted = true;
								directionalNotStarted = true;
							}
							else if (spacingHandle.length > 0) {
								appStateValue = appState.DRAGGING_SPACING;
								selectedItems = [spacingHandle[0]];
							}
							else {
								appStateValue = appState.BOX_SELECTING;
								mouseBoxStart = mouse.pos;
							}
						}
						else if (
							appStateValue & appState.BOX_SELECTING
							&& mouseClickRelease
						) {
							if (hotItems.length > 0) {
								if (
									this.toile.keyboardDown.keyCode
									&& this.toile.keyboardDown.special & specialKey.SHIFT
								) {
									nodes.forEach((node) => {
										let toAdd = true;
										let toRemove;

										selectedItems.forEach((item, idx) => {
											if (node.id == item.id) {
												toRemove = idx;
												toAdd = false;
											}
										});

										if (toAdd) {
											selectedItems.push(node);
										}
									});
									appStateValue = appState.POINTS_SELECTED;
									mouseBoxStart = undefined;
								}
								else {
									selectedItems = hotItems;
									appStateValue = appState.POINTS_SELECTED;
									mouseBoxStart = undefined;
									globalMode = false;
								}
							}
							else {
								selectedItems = [];
								appStateValue = appState.DEFAULT;
								mouseBoxStart = undefined;
								globalMode = false;
							}
							this.client.dispatchAction('/store-value', {
								globalMode,
							});
							this.storeSelectedItems(selectedItems);
						}
						else if (
							appStateValue & appState.DRAGGING_SPACING
							&& mouseClickRelease
						) {
							appStateValue = appState.SPACING_SELECTED;
							draggingNotStarted = false;
							directionalNotStarted = false;
							this.client.dispatchAction('/change-glyph-node-manually', {
								label: 'manual edition',
								force: true,
								changes: {},
							});
						}
						else if (
							appStateValue & appState.DRAGGING_GUIDE
							&& mouseClickRelease
						) {
							appStateValue = appState.GUIDE_SELECTED;
							draggingNotStarted = false;
							directionalNotStarted = false;
							this.client.dispatchAction('/change-glyph-node-manually', {
								label: 'manual edition',
								force: true,
								changes: {},
							});
						}
						else if (
							appStateValue & appState.DRAGGING_POINTS
							&& mouseClickRelease
						) {
							appStateValue = appState.POINTS_SELECTED;
							draggingNotStarted = false;
							directionalNotStarted = false;
							this.client.dispatchAction('/change-glyph-node-manually', {
								label: 'manual edition',
								force: true,
								changes: {},
							});
							this.storeSelectedItems(selectedItems);
						}
						else if (
							appStateValue & appState.POINTS_SELECTED
							&& mouse.edge === mState.DOWN
						) {
							if (nodes.length > 0) {
								const selectedHot = nodes.filter(node =>
									selectedItems.reduce(
										(acc, item) => acc || item.id === node.id,
										false,
									),
								);
								const notSelectedHot = nodes.filter(
									node =>
										!selectedItems.reduce(
											(acc, item) => acc || item.id === node.id,
{'fraction_non_alphanumeric': 0.12971615872794476, 'fraction_numerical': 0.0059976288444103494, 'mean_word_length': 2.61144692104269, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 85, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2974081', 'n_tokens_mistral': 25665, 'n_tokens_neox': 18410, 'n_words': 3588}