Source: hyperSphere.js

/**
 * @module HyperSphere
 * @description Base class for n dimensional [hypersphere]{@link https://en.wikipedia.org/wiki/N-sphere}.
 *
 * @author [Andrej Hristoliubov]{@link https://github.com/anhr}
 *
 * @copyright 2011 Data Arts Team, Google Creative Lab
 *
 * @license under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
*/


import ND from '../nD/nD.js';
//Эти строки не позволяют выполнить команду npm run build
//import ND from '../nD/build/nD.module.js';
//import ND from '../nD/build/nD.module.min.js';
//import ND from 'https://raw.githack.com/anhr/commonNodeJS/master/nD/nD.js';
//import ND from 'https://raw.githack.com/anhr/commonNodeJS/master/nD/build/nD.module.js';
//import ND from 'https://raw.githack.com/anhr/commonNodeJS/master/nD/build/nD.module.min.js';

//если использовать эту строку, появится ошибка:
//Error: Illegal reassignment to import 'ND'
//При выполнении npm run build
//if (ND.default) ND = ND.default;

//Когда хочу вывести на холст точки вместо ребер то использую MyPoints вместо ND
//При этом ребра не создаются что дает экономию времени
import MyPoints from '../myPoints/myPoints.js';
import ColorPicker from '../colorpicker/colorpicker.js';

//Получаю ошибку
//myThree: duplicate myThree. Please use one instance of the myThree class.
//если на веб странце импортировать import MyThree from '../../../commonNodeJS/master/myThree/build/myThree.module.js';
//import MyThree from '../myThree/myThree.js';
import three from '../three.js'

import ProgressBar from '../ProgressBar/ProgressBar.js'
//import WebGPU from '../../../WebGPU/master/WebGPU.js';
import PositionController from '../PositionController.js';
import MyObject from '../myObject.js'

const sHyperSphere = 'HyperSphere', sOverride = sHyperSphere + ': Please override the %s method in your child class.',
	π = Math.PI;

/**
 * Base class for n dimensional [hypersphere]{@link https://en.wikipedia.org/wiki/N-sphere}. Extends <a href="../../jsdoc/MyObject/module-myObject-MyObject.html" target="_blank">MyObject</a>.
 * @class
 * @extends MyObject
 */
class HyperSphere extends MyObject {

	/**
	 * Base class for n dimensional [hypersphere]{@link https://en.wikipedia.org/wiki/N-sphere}.
	 * @param {Options} options See <a href="../../../master/jsdoc/Options/Options.html" target="_blank">Options</a>.
	 * @param {object} [classSettings] <b>HyperSphere</b> class settings.
	 * @param {object} [classSettings.intersection] HyperSphere intersection.
	 * <pre>
	 *	For Circle intersector is line.
	 *	For Sphere intersector is plane.
	 *	For HyperSphere intersector is sphere.
	 * </pre>
	 * @param {float} [classSettings.intersection.position=0.0] Position of the intersector.
	 * <pre>
	 *	For Circle <b>position</b> is Y coordinate of the intersection line.
	 *	For Sphere <b>position</b> is Z coordinate of the intersection plane.
	 *	For HyperSphere <b>position</b> is radius of the intersection sphere.
	 * </pre>
	 * @param {number|string} [classSettings.intersection.color=0x0000FF] Color of the intersector. Example: 'red'.
	 * @param {object} [classSettings.projectParams] Parameters of project the hypersphere onto the canvas.
	 * @param {THREE.Scene} classSettings.projectParams.scene [THREE.Scene]{@link https://threejs.org/docs/index.html?q=sce#api/en/scenes/Scene}
	 * @param {object} [classSettings.projectParams.params={}] The following parameters are available
	 * @param {object} [classSettings.projectParams.params.center={x: 0.0, y: 0.0, z: 0.0}] center of the hypersphere
	 * @param {float} [classSettings.projectParams.params.center.x=0.0] X axis of the center
	 * @param {float} [classSettings.projectParams.params.center.y=0.0] Y axis of the center
	 * @param {float} [classSettings.projectParams.params.center.z=0.0] Z axis of the center
	 * @param {float} [classSettings.r=1.0] HyperSphere radius.
	 * @param {boolean|object} [classSettings.edges={}] HyperSphere edges
	 * <pre>
	 *	false - Doesn't create edges to reduce the creation time of the hypersphere
	 * </pre>
	 * @param {boolean} [classSettings.edges.project=true] false - Doesn't project edges onto canvas
	 * @param {enum} [classSettings.edges.creationMethod=edgesCreationMethod.Random] method for creating edges. See <a href="./module-HyperSphere-HyperSphere.html#.edgesCreationMethod" target="_blank">edgesCreationMethod</a>
	 * @param {Function} [classSettings.onSelectScene] Callback function that called after <a href="../../player/jsdoc/module-Player-Player.html" target="_blank">Player</a> time was changed.
	 * <pre>
	 * parameter <b>hyperSphere</b> <a href="../../HyperSphere/jsdoc/" target="_blank">HyperSphere</a> object.
	 * parameter <b>timeId</b> <a href="../../player/jsdoc/module-Player-Player.html" target="_blank">Player</a> index.
	 * parameter <b>t</b> current time.
	 * Also see <a href="../../player/jsdoc/module-Player.html#~onSelectScene" target="_blank">onSelectScene</a> type definition.
	 * </pre>
	 * @param {object} [classSettings.settings] The following settings are available
	 * @param {object} [classSettings.settings.object] HyperSphere object.
	 * @param {String} [classSettings.settings.object.name] name of hypersphere.
	 * @param {String|number} [classSettings.settings.object.color='lime'] color of edges or vertices.
	 * <pre>
	 * String - color name. See list of available color names in the <b>_colorKeywords</b> object in the [Color.js]{@link https://github.com/mrdoob/three.js/blob/dev/src/math/Color.js} file.
	 * number - color [Hex triplet]{@link https://en.wikipedia.org/wiki/Web_colors#Hex_triplet}. Example: 0x0000ff - blue color.
	 * Default color is lime for <a href="./module-Circle-Circle.html" target="_blank">Circle</a> and <a href="./module-Sphere-Sphere.html" target="_blank">Sphere</a>.
	 * Default color of vertice of the <a href="./module-HyperSphere3D-HyperSphere3D.html" target="_blank">Hypersphere3D</a> is depends from the altitude angle of the vertice according the <a href="../../myThree/jsdoc/module-MyThree-MyThree.html" target="_blank">options.palette</a> parameter.
	 * See <b>classSettings.settings.object.geometry.angles</b> for details. For example if <b>options.palette</b> is <b>MyThree.ColorPicker.paletteIndexes.BGYW</b>, then:
	 * <table>
		 <tr><td>Vertice Altitude</td><td>Vertice Color</td></tr>
		 <tr><td>0</td><td>white</td></tr>
		 <tr><td>π / 2</td><td>lime</td></tr>
		 <tr><td>π</td><td>blue</td></tr>
		</table>
	 * <b>MyThree.ColorPicker.paletteIndexes.BGYW</b> palette parameter is default.
	 * Note: You can define color of the each vertice separately in the <b>classSettings.settings.object.geometry.colors</b> parameter.
	 * <pre>
	 * @param {object} [classSettings.settings.object.geometry] HyperSphere geometry.
	 * @param {array|object} [classSettings.settings.object.geometry.angles] n-dimensional hypersphere vertice angles.
	 * <pre>
	 * array - array of vertex angles.
	 *	Every item of array is n-dimensional array of vertex angles.
	 *	
	 *	All the vertices of the <b><a href="module-Circle.html" target="_blank">Circle</a></b> form a circle.
	 *	For <b><a href="module-Circle.html" target="_blank">Circle</a></b> every vertice is array of one angle.
	 *		Vertex angle is the longitude of the circle of the hypersphere in the range from <b>- π</b> to <b>π</b>.
	 *		Vertex angle is angle of rotation around of <b>Z</b> axis in 3D space.
	 *		Angle is begin from <b>X = 0, Y = 1</b>.
	 *		Every vertex is <b>[
				Math.cos(θ),//x
				Math.sin(θ)//y
			]</b> array. <b>θ</b> is vertex angle.
	 *		Example of Circle with three vertices is triangle:
	 *		<b>classSettings.settings.object.geometry.angles: angles: [
	 *			[],                 //vertice[0] = [0                   ,1]
	 *			[Math.PI * 2 / 3],  //vertice[1] = [0.8660254037844387	,-0.4999999999999998]
	 *			[- Math.PI * 2 / 3] //vertice[2] = [-0.8660254037844387	,-0.4999999999999998]
	 *		]</b>,
	 *		
	 *	All the vertices of the <b><a href="module-Sphere.html" target="_blank">Sphere</a></b> form a sphere.
	 *	For <b><a href="module-Sphere.html" target="_blank">Sphere</a></b> every vertice is array of two angles.
	 *		The first vertex angle is the latitude of the sphere of the hypersphere in the range from <b>- π / 2</b> to <b>π / 2</b>.
	 *		Zero latitude is located at the equator.
	 *		
	 *		The second vertex angle is the longitude of the sphere of the hypersphere in the range from <b>- π</b> to <b>π</b>.
	 *		The second vertex angle is angle of rotation of the cross section around of <b>Y</b> axis.
	 *		
	 *		Example of Sphere with 4 vertices is pyramid:
	 *		<b>classSettings.settings.object.geometry.angles: [
	 *		
	 *			[ Math.PI / 2,  0                  ],//vertice[0] = [ 0                 , 1  , 0   ]
	 *			[-Math.PI / 6,  Math.PI * 2 * 0 / 3],//vertice[1] = [-0.8660254037844387,-0.5, 0   ]
	 *			[-Math.PI / 6,  Math.PI * 2 * 1 / 3],//vertice[2] = [ 0.4330127018922192,-0.5,-0.75]
	 *			[-Math.PI / 6, -Math.PI * 2 * 1 / 3,//vertice[3] = [ 0.4330127018922195,-0.5, 0.75]
	 *			
	 *		]</b>,
	 *		
	 *	All the vertices of the <a href="module-HyperSphere3D.html" target="_blank">HyperSphere3D</a></b> form a [hypersphere]{@link https://en.wikipedia.org/wiki/N-sphere}.
	 *	For <b><a href="module-HyperSphere3D.html" target="_blank">HyperSphere3D</a></b> every vertice is array of three angles.
	 *		The first vertex angle is the altitude of the hypersphere of the hypersphere in the range from <b>0</b> to <b>π / 2</b>.
	 *		Zero altitude is located at the center of the hypersphere.
	 *		
	 *		The second vertex angle is the latitude of the hypersphere of the hypersphere in the range from <b>- π / 2</b> to <b>π / 2</b>.
	 *		Zero latitude is located at the equator.
	 *		
	 *		The third vertex angle is the longitude of the hypersphere of the hypersphere in the range from <b>- π</b> to <b>π</b>.
	 *		The third vertex angle is angle of rotation of the cross section around of <b>Y</b> axis.
	 *		
	 *		Example of HyperSphere with 5 vertices is [pentahedroid]{@link https://en.wikipedia.org/wiki/5-cell}:
	 *		<b>classSettings.settings.object.geometry.angles: [
	 *			[],
	 *			[Math.PI / 2, Math.PI / 2],
	 *			[
	 *				  Math.PI / 2,//Altitude
	 *				- Math.PI / 6,//Latitude
	 *				  Math.PI * 0,//Longitude
	 *			],
	 *			[Math.PI / 2, - Math.PI / 6,   Math.PI * 2 * 1 / 3],
	 *			[Math.PI / 2, - Math.PI / 6, - Math.PI * 2 * 1 / 3],
			]</b>,
	 * object - see below:
	 * </pre>
	 * @param {number} [classSettings.settings.object.geometry.angles.count=3|4|5] Count of vertices with random position.
	 * <pre>
	 * Default values:
	 *	3 for <b><a href="module-Circle.html" target="_blank">Circle</a></b> - triangle.
	 *	4 for <b><a href="module-Sphere.html" target="_blank">Sphere</a></b> - pyramid.
	 *	5 for <b><a href="module-HyperSphere3D.html" target="_blank">HyperSphere3D</a></b> - [pentahedroid]{@link https://en.wikipedia.org/wiki/5-cell}.
	 * </pre>
	 * @param {array} [classSettings.settings.object.geometry.times] array of vertices angles for different player <a href="../../player/jsdoc/module-Player-Player.html" target="_blank">Player</a> times. See <b>angles</b> above.
	 * <pre>
	 * See <a href="../../player/jsdoc/module-Player.html#~onSelectScene" target="_blank">Player.onSelectScene</a> for details.
	 * Every item of the array is array of vertices angles for current <b>Player</b> time.
	 * <b>times</b> have priority before <b>angles</b>.
	 * </pre>
	 * @param {array} [classSettings.settings.object.geometry.colors] array of colors of vertices.
	 * <pre>
 	 * Color of the each vertice is group of three (RGB) items of the colors array in range from 0 to 1. See [THREE.Color]{@link https://threejs.org/docs/?q=Color#api/en/math/Color} for details.
	 * Example:
	 * [
	 * 	1, 0, 0,//First vertice is red.
	 * 	0, 1, 0,//Second vertice green.
	 * 	0, 0, 1,//Third vertice blue.
	 * ]
	 * </pre>
	 * Note: Color of vertice is defined in the <b>classSettings.settings.object.color</b> parameter if it not exists in the colors array.
	 * @param {array} [classSettings.settings.object.geometry.opacity] array of opacities of each vertice. Each item of array is float value in the range of 0.0 - 1.0 indicating how transparent the material is. A value of 0.0 indicates fully transparent, 1.0 is fully opaque.
	 * @param {object} [classSettings.settings.object.geometry.indices] Array of <b>indices</b> of edges of hypersphere.
	 * @param {array|object} [classSettings.settings.object.geometry.indices.edges] HyperSphere edges.
	 * <pre>
	 * array - array of edges.
	 *	Every edge is array of indices of vertices from
	 *	<b>classSettings.settings.object.geometry.position</b>
	 *	Example: <b>[[0,1], [1,2], [2,0]],//triangle</b>
	 * object - see below:
	 * </pre>
	 * @param {number} [classSettings.settings.object.geometry.indices.edges.count=3] edges count.
	 * @param {boolean|object} [classSettings.debug=false] Debug mode.
	 * <pre>
	 *	true - Diagnoses your code and display detected errors to console.
	 *	object - Diagnoses your code and display detected errors to console.
	 * </pre>
	 * @param {boolean|Array} [classSettings.debug.probabilityDensity=[]] Probability density of distribution of vertices over the surface of the hypersphere.
	 * <pre>
	 *	false - do not calculate probability density.
	 *	[] - calculate probability density.
	 * </pre>
	 * @param {boolean} [classSettings.debug.testVertice=true]
	 * <pre>
	 * Test of converting of the vertice coordinates from Cartesian Coordinates to Polar Coordinates
	 * and Polar Coordinates to Cartesian Coordinates
	 * and display detected errors to console.
	 * </pre>
	 * @param {boolean} [classSettings.debug.middleVertice=true] Middle vertice log.
	 * @param {boolean} [classSettings.debug.log=true] Vertices and edges log.
	 * @param {boolean} [classSettings.debug.edges=true] Edges log. Have effect if <b>log = true</b> only
	 * @param {Function} [classSettings.continue] Callback function that called after hypersphere edges was created.
	 * @param {boolean} [classSettings.boRemove] false - do not delete the previous hypersphere while projecting a new hypersphere on scene.
	 * @param {boolean} [classSettings.boGui] false - do not include hypersphere GUI.
	 * @param {object} [classSettings.overriddenProperties] Overridden properties. The following properties can be override:
	 * @param {Function} [classSettings.overriddenProperties.oppositeVertice] Returns the opposite vertice.
	 * <pre>
	 * parameter <b>oppositeAngleId</b>. Opposite vertice identifier.
	 * parameter <b>timeId</b>. <a href="../../player/jsdoc/module-Player-Player.html" target="_blank">Player</a> index is current time identifier.
	 * </pre>
	 * @param {Array} [classSettings.overriddenProperties.position] Returns an array of vertice positions.
	 * @param {Array} [classSettings.overriddenProperties.position0] Returns an array of vertice positions for the <a href="../../player/jsdoc/module-Player-Player.html" target="_blank">Player</a>'s start time.
	 * See <b>settings.options.playerOptions.min</b> of the <a href="../../player/jsdoc/module-Player-Player.html" target="_blank">Player</a>.
	 * @param {Function} [classSettings.overriddenProperties.updateVertices] Update vertices.
	 * <pre>
	 * parameter <b>vertices</b>. Array of new vertices angles.
	 * </pre>
	 * @param {Function} [classSettings.overriddenProperties.vertices] Returns an empty array of vertices.
	 * @param {Function} [classSettings.overriddenProperties.r] Returns a hypersphere radius.
	 * <pre>
	 * parameter <b>timeId</b>. <a href="../../player/jsdoc/module-Player-Player.html" target="_blank">Player</a> index is time identifier.
	 * </pre>
	 * @param {Function} [classSettings.overriddenProperties.pushMiddleVertice] pushes a middle vertice into time angles array.
	 * <pre>
	 * parameter <b>timeId</b>. <a href="../../player/jsdoc/module-Player-Player.html" target="_blank">Player</a> index is time identifier.
	 * parameter <b>middleVertice</b>. Array of the middle vertice angles to push.
	 * </pre>
	 * @param {Function} [classSettings.overriddenProperties.angles] Returns a vertice angles array.
	 * <pre>
	 * parameter <b>anglesId</b>. Vertice identifier.
	 * parameter <b>timeId</b>. <a href="../../player/jsdoc/module-Player-Player.html" target="_blank">Player</a> index is time identifier.
	 * </pre>
	 * @param {Function} [classSettings.overriddenProperties.verticeAngles] Returns a vertice angles.
	 * <pre>
	 * parameter <b>anglesCur</b>. Array of the vertice angles.
	 * parameter <b>verticeId</b>. Vertice index.
	 * </pre>
	 * @param {Function} [classSettings.overriddenProperties.verticeText] Returns a vertice text if user is move mouse over vertice.
	 * <pre>
	 * parameter <b>intersection</b>.
	 * parameter <b>text</b>. Callback function, what returns a vertice text.
	 * </pre>
	 * @param {Function} [classSettings.overriddenProperties.text] Returns a part of the vertice text.
	 **/
	constructor(options, classSettings = {}) {

		if (!classSettings.onSelectScene) classSettings.onSelectScene = (hyperSphere, timeId, t) => { if (this.middleVertices) return this.middleVertices(timeId, t); }
		//for playing in http://localhost/anhr/commonNodeJS/master/HyperSphere/Examples/hyperSphere.html
		options.onSelectScene = (index, t) => {

			if (classSettings.onSelectScene) return classSettings.onSelectScene(this, index, t);
			else if (this.middleVertices) return hyperSphere.middleVertices(index, t);
			return true;//Сдедующий шаг проигрывателя выполняется только после посторения всех вершин без временной задержки
		
		}
		classSettings.settings = classSettings.settings || {};
		if (classSettings.debug) classSettings.settings.debug = classSettings.debug;
		classSettings.settings.options = options;
		if (classSettings.settings.guiPoints) classSettings.settings.guiPoints.setIntersectionProperties = (intersection) => {
			
			intersection.nearestEdgeVerticeId = intersection.index;//если не задать это значение, то index будет интерпретироваться как индекс ребра и программа в ребре будет искать индекс вершины, ближайшей к point
			//Для проверки открыть http://localhost/anhr/universe/main/hyperSphere/Examples/
			//Сделать один шаг проигрывателя →
			//С помошю gui выбрать вершину
			//Выбрать "Средняя" для вычисления среднего зачения этой вершины исходя из положения противоположных вершин: opposite vertices
							
		};
		super( classSettings.settings );
		Object.defineProperty(classSettings.settings.bufferGeometry.userData, 'positionBlockLength', {

			get: () => { return classSettings.settings.object.geometry.angles.length; },

		});
		this.rotateLatitude = - π / 2;//Поворачиваем широту на 90 градусов что бы начало координат широты находилось на экваторе;
		const _this = this, THREE = three.THREE;
		if (classSettings.debug === undefined) classSettings.debug = true;
		if (classSettings.debug === true) classSettings.debug = {};
		if (classSettings.debug) {

			classSettings.debug.timestamp = window.performance.now();
			classSettings.debug.logTimestamp = (text = '', timestamp) =>
				console.log('time: ' + text + ((window.performance.now() - (timestamp ? timestamp : classSettings.debug.timestamp)) / 1000) + ' sec.');
			if (classSettings.debug.testVertice != false) classSettings.debug.testVertice = true;
			if (classSettings.debug.middleVertice != false) classSettings.debug.middleVertice = true;

		}
		if (classSettings.projectParams.scene)
			classSettings.projectParams.scene.userData = new Proxy(classSettings.projectParams.scene.userData, {
	
				set: (userData, name, value) => {
	
					switch (name) {
	
						case 't':
							classSettings.settings.bufferGeometry.userData.timeId = userData.index;
							break;
							
					}
					userData[name] = value;
					return true;
	
				}
				
			});
		
		this.classSettings = classSettings;

		const cookieOptions = {};
		if (options.dat) options.dat.cookie.getObject(this.cookieName, cookieOptions);

		let edgesOld = cookieOptions.edgesOld || { project: true, };
		if (classSettings.overriddenProperties && !classSettings.overriddenProperties.edges) classSettings.overriddenProperties.edges = () => { return false; };
		classSettings.edges = cookieOptions.edges === false ? classSettings.overriddenProperties.edges() : cookieOptions.edges || classSettings.edges;
		classSettings.edges = classSettings.edges === false ? classSettings.overriddenProperties.edges() : classSettings.edges;
		if (classSettings.edges != false) classSettings.edges = classSettings.edges || {};
		if ((classSettings.edges != false) && (classSettings.edges.project === undefined)) classSettings.edges.project = true;

		if (classSettings.r === undefined) classSettings.r = 1;
		//Нельзя менять радиус гиперсферы
		Object.defineProperty( classSettings, "r", { writable: false, });//classSettings.r freeze. https://stackoverflow.com/a/10843598/5175935

		classSettings.rRange = classSettings.rRange || {};
		if (classSettings.rRange.min === undefined) classSettings.rRange.min = -1;
		if (classSettings.rRange.max === undefined) classSettings.rRange.max = 1;
		classSettings.settings = classSettings.settings || {};
		const settings = classSettings.settings;
		settings.object = settings.object || {};
		settings.object.name = settings.object.name || this.name(options.getLanguageCode);

		//не получается сменить имя оси
		//if (options.scales.w.name === 'w') options.scales.w.name = 't';

		settings.object.geometry = settings.object.geometry || {};

		//for debug
		//для 2D гиперсферы это плотность вероятности распределения вершин по поверхности сферы в зависимости от третьей координаты вершины z = vertice.[2]
		//Плотности разбил на несколько диапазонов в зависимости от третьей координаты вершины z = vertice.[2]
		//Разбил сферу на sc = 5 сегментов от 0 до 4.
		//Границы сегментов вычисляю по фомулам:
		//Высота сегмента hs = d / sc = 2 / 5 = 0.4
		//Нижняя граница сегмента hb = hs * i - r
		//Верхняя граница сегмента ht = hs * (i + 1) - r
		//где r = 1 - радиус сферыб d = 2 * r = 2 - диаметр сферы, i - индекс сегмента
		if (classSettings.debug && (classSettings.debug.probabilityDensity != false)) classSettings.debug.probabilityDensity = [];
		const probabilityDensity = classSettings.debug.probabilityDensity;
		if (probabilityDensity) {

			for (let i = 0; i < 5; i++) probabilityDensity.push({ count: 0, });
			probabilityDensity.options = { d: classSettings.r * 2, };
			probabilityDensity.options.sc = probabilityDensity.length;//Количество сегментов
			probabilityDensity.options.hs = probabilityDensity.options.d / probabilityDensity.options.sc;//Высота сегмента
			let sectorsValue = 0;
			probabilityDensity.forEach((sector, i) => {

				sector.hb = probabilityDensity.options.hs * i - classSettings.r;//Нижняя граница сегмента
				sector.ht = probabilityDensity.options.hs * (i + 1) - classSettings.r;//Верхняя граница сегмента
				sectorsValue += _this.probabilityDensity.sectorValue(probabilityDensity, i);

			});
			let unverseValue = this.probabilityDensity.unverseValue;
			if (unverseValue === undefined) {

				unverseValue = π;
				const r = classSettings.r;
				for (let i = 0; i < (_this.dimension - 1); i++) unverseValue *= 2 * r;

			}
			if (unverseValue != sectorsValue) console.error(sHyperSphere + ': Unverse value = ' + unverseValue + '. Sectors value = ' + sectorsValue);

		}
		settings.object.geometry.angles = settings.object.geometry.angles || this.defaultAngles();
		const anglesObject2Array = () => {
			
			const geometryAngles = settings.object.geometry.angles;
			if (geometryAngles instanceof Array) return;
			const angles = [];
			Object.keys(geometryAngles).forEach((key) => angles[key] = geometryAngles[key]);
			settings.object.geometry.angles = angles;
			
		}
		(classSettings.anglesObject2Array || anglesObject2Array)();
		
		settings.object.geometry.angles = new Proxy(settings.object.geometry.angles || this.defaultAngles(), {

			get: (angles, name) => {

				const verticeId = parseInt(name);
				if (!isNaN(verticeId)) {

					if (verticeId >= angles.length) {

						console.error(sHyperSphere + ': get vertice angles failed! verticeId = ' + verticeId + ' is great angles.length = ' + angles.length);
						return;
						
					}
					const length = _this.dimension - 1;
					return new Proxy(angles[verticeId], {

						get: (verticeAngles, name) => {

							const angleId = parseInt(name);
							if (!isNaN(angleId)) {

								if (angleId >= verticeAngles.length) return 0.0;
								let angle = verticeAngles[angleId];
								return angle;

							}
							switch (name) {

								case 'length': return length;//_this.dimension - 1;
								case 'forEach': return (item) => {
								
									for (let axisId = 0; axisId < length; axisId++) item(verticeAngles[axisId] != undefined ? verticeAngles[axisId] : 0, axisId);

								}

							}
							return verticeAngles[name];

						},
						set: (verticeAngles, name, value) => {

							const angleId = parseInt(name);
							if (!isNaN(angleId)) {

								const angle = value;
//								if (angleId >= verticeAngles.length)
								if (angleId >= length)
								{

									console.error(sHyperSphere + ': set vertice angles failed! angleId = ' + angleId + ' is great of verticeAngles.length = ' + verticeAngles.length);
									return false;
								}
								if (verticeAngles[angleId] != angle) {

									const range = angles.ranges[angleId];
									if ((angle < range.min) || (angle > range.max)) console.error(sHyperSphere + ': Set angle[' + angleId + '] = ' + angle + ' of the vertice ' + verticeId + ' is out of range from ' + range.min + ' to ' + range.max);

									verticeAngles[angleId] = angle;

									//если тут обновлять вершину то каждая вершина будет обноляться несколько раз в зависимости от количества углов. Сейчас вершина обновляется после обновления всех углов вершины
									if(_this.isSetPositionAttributeFromPoint != false) {
										
										_this.setPositionAttributeFromPoint(verticeId);//обновляем только одну ось в декартовой системе координат
										_this.bufferGeometry.attributes.position.needsUpdate = true;

									}
									
									//если тут обновлять гиперсферу, то будет тратиться лишнее время, когда одновременно изменяется несколько вершин
									//Сейчас я сначала изменяю все вершины, а потом обновляю гиперсферу
									//_this.update(verticeId);

								}

							} else verticeAngles[name] = value;
							return true;

						}

					});

				}
				switch (name) {

					case 'pushRandomAngle': return () => {

						const verticeAngles = [];
						_this.pushRandomAngle(verticeAngles);
						angles.push(verticeAngles);

					}
					case 'push': return (verticeAngles) => {

						for (let angleId = 0; angleId < verticeAngles.length; angleId++) {

							const angle = verticeAngles[angleId], range = angles.ranges[angleId];
							if ((angle < range.min) || (angle > range.max)) console.error(sHyperSphere + ': Vertice angle[' + angleId + '] = ' + angle + ' is out of range from ' + range.min + ' to ' + range.max);

						}
						angles.push(verticeAngles);

					}
					case 'guiLength': return angles.length;
					case 'player': return this.anglesPlayer();

				}
				return angles[name];

			},
			//set settings.object.geometry.angles
			set: (aAngles, name, value) => {

				switch (name) {

					case 'guiLength'://изменилось количество вершин
						if (value < 2) return true;
						const angles = settings.object.geometry.angles;
						for (let i = aAngles.length; i < value; i++) angles.pushRandomAngle();//add vertices
						aAngles.length = value;//remove vertice

						//update buffer
						this.setPositionAttributeFromPoints(angles, true);
						aAngles.length = value;//remove vertices
						if (classSettings.edges) {//Для экономии времени не добавляю ребра если на холст вывожу только вершины

							_this.removeMesh();
							_this.pushEdges();

						}
						else _this.project();
						return true;
					case 'length':
						console.warn(sHyperSphere + ': set geometry.angles.length. Use guiLength instead')
						return true;

				}
				const i = parseInt(name);
				if (!isNaN(i)) {

					aAngles[i] = value;
//					const object = _this.object();
					const object = _this.object3D;
					if (object) object.userData.myObject.setPositionAttributeFromPoint(i, _this.angles2Vertice(value));

				}
				else aAngles[name] = value;
				return true;

			}

		});
		this.anglesPlayer = (timeId) => {

			const playerProxy = new Proxy({}, {
						
				get: (player, name) => {

					switch (name) {

						case 'id': return timeId != undefined ? timeId : classSettings.settings.options.player.getTimeId();
						case 't': return classSettings.settings.options.player.getTime(timeId);
						case 'r': return playerProxy.t * classSettings.r;
							
					}
					return player[name];
					
				},
				
			});
			return playerProxy;
			
		}
		{//hide angles
			
			const angles = settings.object.geometry.angles;
	
			//Angles range
			angles.ranges = [];
			for (let angleId = 0; angleId < this.dimension - 1; angleId++) {
	
				const range = {}
				switch (this.dimension - 2 - angleId) {
	
					case 0:
						range.angleName = 'Longitude';
						range.min = - π;
						range.max = π;
						break;
					case 1:
						range.angleName = 'Latitude';
						range.min = 0 + this.rotateLatitude;//- π / 2;
						range.max = π + this.rotateLatitude;//π / 2;
						break;
					case 2:
						range.angleName = this.altitudeRange.angleName;//'Altitude';
						range.min = this.altitudeRange.min;//0;
						range.max = this.altitudeRange.max;//π / 2;
						break;
					default: console.error(sHyperSphere + ': vertice angles ranges. Invalid angleId = ' + angleId);
	
				}
				angles.ranges.push(range);
	
			}
	
			//angles[0][0] = 10;//error hyperSphere.js:548 HyperSphere: Set angle[0] = 10 of the vertice 0 is out of range from -1.5707963267948966 to 1.5707963267948966
			if (angles.count != undefined)
				for (let i = angles.length; i < angles.count; i++) angles.pushRandomAngle();

		}
		settings.object.geometry.position = new Proxy(settings.object.geometry.position || [], {

			get: (target, name) => {

				let _position = settings.object.geometry.angles;
				let i = parseInt(name);
				if (!isNaN(i)) {

					if (settings.object.geometry.times){} else {

						if (i > _position.length) console.error(sHyperSphere + ': position get. Invalid index = ' + i + ' position.length = ' + _position.length);
						else if (i === _position.length)
							settings.object.geometry.angles.pushRandomAngle();

					}
					const _vertice = _position[i],
						//anglesPlayer = settings.object.geometry.angles.player,
						//timeId = anglesPlayer.id,
						timeId = _position.player.id,
						// r = anglesPlayer.r;
						angle2Vertice = () => {

							const vertice = _this.angles2Vertice(i, timeId);
							if (classSettings.debug) {
	
								const sum = vertice.radius, r = classSettings.overriddenProperties.r(timeId);
								if (Math.abs(sum - r) > 9.5e-8)
									console.error(sHyperSphere + ': Invalid vertice[' + i + '] sum = ' + sum + '. r = ' + r);
	
							}
							return vertice;
	
						}
					return new Proxy(angle2Vertice(), {

						get: (vertice, name) => {

							switch (name) {

/*									
								//дуга между вершинами
								case 'arcTo': return (verticeTo) => {

									//Calculate the arc length between two points over a hyper-sphere
									//Reference: https://www.physicsforums.com/threads/calculate-the-arc-length-between-two-points-over-a-hyper-sphere.658661/post-4196208
									const a = vertice, b = verticeTo, R = 1, acos = Math.acos;
									let ab = 0;//dot product
									for (let i = 0; i < a.length; i++) ab += a[i] * b[i];
									return R * acos(ab / (R * R))

								}
*/								
								//расстояние между вершинами по прямой в декартовой системе координат
								//Если надо получить расстояние между вершинами по дуге в полярной системе координат, то надо вызвать 
								//classSettings.settings.object.geometry.position.angles[verticeId].distanceTo
								case 'distanceTo': return (verticeTo) => {

									if (verticeTo.length != vertice.length) {

										console.error(sHyperSphere + ': vertice.distanceTo. Invalid vertice.length.');
										return;

									}
									let distance = 0;
									vertice.forEach((axis, i) => distance += Math.pow(axis - verticeTo[i], 2));
									return Math.sqrt(distance);

								}
								case 'edges':

									_vertice.edges = _vertice.edges || new Proxy([], {

										get: (edges, name) => {

											switch (name) {

												case 'push': return (edgeId, verticeId) => {

													const sPush = sHyperSphere + ': Vertice' + (verticeId === undefined ? '' : '[' + verticeId + ']') + '.edges.push(' + edgeId + '):';

													if (edges.length >= _this.verticeEdgesLength) {

														console.error(sPush + ' invalid edges.length = ' + edges.length);
														return;

													}
													//find for duplicate edgeId
													for (let j = 0; j < edges.length; j++) {

														if (edges[j] === edgeId) {

															console.error(sPush + ' duplicate edgeId: ' + edgeId);
															return;

														}

													}

													edges.push(edgeId);

												}

											}
											return edges[name];

										},
									});
									return _vertice.edges;

								case 'angles': return _vertice;
								case 'vector':
									//для совместимости с Player.getPoints. Туда попадает когда хочу вывести на холст точки вместо ребер и использую дя этого MyPoints вместо ND
									const vertice2 = vertice[2], vertice3 = vertice[3];
									//Если вернуть THREE.Vector4 то будет неправильно отображаться цвет точки
									if (vertice3 === undefined)
										return new three.THREE.Vector3(vertice[0], vertice[1], vertice2 === undefined ? 0 : vertice2);
									return new three.THREE.Vector4(vertice[0], vertice[1], vertice2 === undefined ? 0 : vertice2, vertice3 === undefined ? 1 : vertice3);
								case 'x': return vertice[0];
								case 'y': return vertice[1];
								case 'z': return vertice[2];
								case 'w': return vertice[3];//для совместимости с Player.getColors. Туда попадает когда хочу вывести на холст точки вместо ребер и использую для этого MyPoints вместо ND
								case 'toJSON': return (item) => {

									let res = '[';
									vertice.forEach(axis => { res += axis + ', ' })
									return res.substring(0, res.length-2) + ']';
										
								}
								case 'timeId': return timeId;

							}
							if (!isNaN(parseInt(name))) return vertice[name] === undefined ? 0 : vertice[name];
							return vertice[name];

						},
						set: (vertice, name, value) => {

							switch (name) {

								case 'edges':
									_vertice[name] = value;
									if (value === undefined) delete _vertice[name];
									return true;

							}
							vertice[name] = value;
							return true;

						}

					});

				}
				switch (name) {

					case 'angles': return new Proxy(_position, {

						get: (angles, name) => {

							switch (name) {

								case 'length': return angles.length;
								case 'player': return angles.player;

							}
							const verticeId = parseInt(name);
							if (isNaN(verticeId)) {

								console.error(sHyperSphere + ': Get vertice angles failed. Invalid verticeId = ' + verticeId);
								return;

							}
							const vertice = new Proxy(angles[name], {

								get: (angles, name) => {

									switch (name) {

										case 'middleVertice': return (oppositeVerticesId = vertice.oppositeVerticesId, timeId, boPushMiddleVertice = true) => {

											//find middle vertice between opposite vertices

											//Среднее значение углов
											//ссылка не работает https://wiki5.ru/wiki/Mean_of_circular_quantities#Mean_of_angles
											//https://en.wikipedia.org/wiki/Circular_mean

											//массив для хранения сумм декартовых координат противоположных вершин
											//для 1D гиперсферы это: aSum[0] = x, aSum[1] = y.
											//для 2D гиперсферы это: aSum[0] = x, aSum[1] = y, aSum[2] = z.
											//для 3D гиперсферы это: aSum[0] = x, aSum[1] = y, aSum[2] = z, aSum[3] = w.
											const aSum = [];

											const oppositeVertices = [];
											oppositeVerticesId.forEach(oppositeAngleId => {

												const oppositeVertice = classSettings.overriddenProperties.oppositeVertice(oppositeAngleId, timeId);
												oppositeVertice.forEach((axis, i) => {

													if (aSum[i] === undefined) aSum[i] = 0;
													aSum[i] += axis
												
												});
												oppositeVertices.push(oppositeVertice);

											});
/*
											let sum = 0;
											for (let i = 0; i < _this.dimension; i ++) sum += aSum[i];
*/
											let isZero = true;
											for (let i = 0; i < _this.dimension; i ++) {
												
												if(aSum[i] != 0) {

													isZero = false;
													break;
													
												};

											}
											
											//if (isZero === 0) {

												//В этом случае средняя вершина не определена
												//для 1D гиперсферы. Противополжные вершины находятся ровно на противоположных сторонах окружности.
													//Средняя вершина, находится посередине одной из двух дуг, соеденяющих проитвоположные вершины.
												//для 2D гиперсферы это .
												//для 3D гиперсферы это .
												//Думаю тут надо применить вероятностный метод определения средней вершины
											//}
											
											const middleVertice = isZero ? _this.getRandomMiddleAngles(oppositeVertices) : _this.vertice2angles(aSum);
											if (classSettings.debug && classSettings.debug.middleVertice) {

												console.log('opposite to vertice[' + verticeId + '] vertices:');
												oppositeVerticesId.forEach(oppositeVerticeId => {

													const verticeAngles = position[oppositeVerticeId].angles;
													console.log('vertice[' + oppositeVerticeId + '] anlges: ' + JSON.stringify(verticeAngles));

												});
												console.log('Middle vertice ' + JSON.stringify(_this.angles2Vertice(middleVertice, timeId)) + ' angles: ' + JSON.stringify(middleVertice));

											}
											const geometry = settings.object.geometry;
											if (boPushMiddleVertice) classSettings.overriddenProperties.pushMiddleVertice(timeId, middleVertice);
											return middleVertice;

										}
										//идентификаторы всех вершин, которые связаны с текущей вершиной через ребра
										case 'oppositeVerticesId': return new Proxy(angles.edges, {

											get: (verticeEdges, name) => {

												const i = parseInt(name);
												if (!isNaN(i)) {

													const edge = settings.object.geometry.indices.edges[verticeEdges[i]];
													if (verticeId === edge[0]) return edge[1];
													if (verticeId === edge[1]) return edge[0];
													console.error(sHyperSphere + ': Get oppositeVerticesId failed.');
													return;

												}
												return verticeEdges[name];

											}

										});

									}
									return angles[name];

								},

							});
							return vertice;

						},
						set: (angles, name, value) => {

							const verticeId = parseInt(name);
							if (!isNaN(verticeId)) {

								const verticeAngles = angles[verticeId];
								if (classSettings.debug && ((verticeAngles.length != (_this.dimension - 1)) || (value.length != (_this.dimension - 1)))) console.error(sHyperSphere + ': Set vertice[' + verticeId + '] angles failed. Invalid angles count.')
								this.isSetPositionAttributeFromPoint = false;
								for (let j = 0; j < value.length; j++) verticeAngles[j] = value[j];
								delete this.isSetPositionAttributeFromPoint;
								this.setPositionAttributeFromPoint(verticeId);//обновляем geometry.attributes

							} else angles[name] = value;
							return true;

						}
						
					});
					case 'count': return _position.count === undefined ? _position.length : _position.count;
					case 'forEach': return (item) => {

						const pos = classSettings.overriddenProperties.position;
						for (let verticeId = 0; verticeId < pos.length; verticeId++) item(pos[verticeId], verticeId);
					
					}
					case 'length': return _position.length;
					case 'push': return (position) => { console.error(sHyperSphere + ': deprecated push vertice. Use "settings.object.geometry.angles.pushRandomAngle()" instead.'); };

					//for debug
					case 'test': return () => {

						if (!classSettings.debug) return;

						_position.forEach((verticeAngles, verticeId) => {

							/*
							//for testing please uncommet the "//test for angles range" in the http://localhost/anhr/commonNodeJS/master/HyperSphere/Examples/hyperSphere.html page
							let sLog = 'vertice[' + verticeId + '] angles test:\n   verticeAngles[';
							verticeAngles.forEach(angle => sLog = sLog + angle + ', ');
							sLog = sLog + ']\nnormalizedAngles['
							_this.normalizeVerticeAngles(verticeAngles).forEach(angle => sLog = sLog + angle + ', ');
							console.log(sLog + ']');
							*/
							
							for (let angleId = 0; angleId < verticeAngles.length; angleId++) {

								const angle = verticeAngles[angleId];
								const range = settings.object.geometry.angles.ranges[angleId];
								if ((angle < range.min) || (angle > range.max)) {
									
									console.error(sHyperSphere + ': ' + range.angleName + ' angle[' + angleId + '] = ' + angle + ' of the vertice ' + verticeId + ' is out of range from ' + range.min + ' to ' + range.max);
									_position[verticeId] = _this.normalizeVerticeAngles(verticeAngles);

								}

							}
							
							const vertice = settings.object.geometry.position[verticeId], strVerticeId = 'geometry.position[' + verticeId + ']'
							_this.TestVertice(vertice, strVerticeId);
							vertice.edges.forEach(edgeId => {

								if (typeof edgeId !== "number") console.error(sHyperSphere + ': position.test()', strVerticeId = 'position(' + verticeId + ')' + '. ' + strVerticeId + '. Invalid edgeId = ' + edgeId);

							});

						})
					}

				}
				return target[name];//Обращение к settings.object.geometry.position Proxy низшего уровня

			},
			//set settings.object.geometry.position
			set: (target, name, value) => {

				const _position = settings.object.geometry.angles;
				const i = parseInt(name);
				if (!isNaN(i)) {

					if (value instanceof Array === true) {//для совместимости с Player.getPoints. Туда попадает когда хочу вывести на холст точки вместо ребер и использую дя этого MyPoints вместо ND

						console.warn(sHyperSphere + ': Set vertice was deprecated. Use set angle instead.')
						const angles = this.vertice2angles(value);
						if (classSettings.debug) {

							const angles2vertice = this.angles2Vertice(angles);
							if (angles2vertice.length != value.length) console.error(sHyperSphere + ': Set vertice failed. angles2vertice.length = ' + angles2vertice.length + ' is not equal value.length = ' + value.length);
							const d = 0;
							angles2vertice.forEach((axis, i) => { if (Math.abs(axis - value[i]) > d) console.error(sHyperSphere + ': Set vertice failed. axis = ' + axis + ' is not equal to value[' + i + '] = ' + value[i]) });

						}
						settings.object.geometry.position[i].angles(angles);

					}

				} else _position[name] = value;
				return true;

			}

		});
		const position = settings.object.geometry.position;
		
		//иммитация наследования классов
		classSettings.settings.overriddenProperties.positionOffsetId = (positionId) => {
			
			const settings = classSettings.settings;
			return settings.bufferGeometry.userData.timeId * settings.object.geometry.angles.length + positionId;
			
		}
		if (!classSettings.overriddenProperties) classSettings.overriddenProperties = {};
		const overriddenProperties = classSettings.overriddenProperties;
		if (!overriddenProperties.oppositeVertice) overriddenProperties.oppositeVertice = (oppositeAngleId) => { return position[oppositeAngleId]; }
		if (!overriddenProperties.position) Object.defineProperty(overriddenProperties, 'position', { get: () => { return position; }, });
		if (!overriddenProperties.position0) Object.defineProperty(overriddenProperties, 'position0', { get: () => { return position; }, });
		if (!overriddenProperties.updateVertices) overriddenProperties.updateVertices = (vertices) => {

			if (vertices.length != position.length) console.error(sHyperSphere + ': classSettings.overriddenProperties.updateVertices(). Invalid vertices.length = ' + vertices.length);
			for (let verticeId = 0; verticeId < position.length; verticeId++)
				position.angles[verticeId] = vertices[verticeId];
			this.bufferGeometry.attributes.position.needsUpdate = true;
			
		}
		if (!overriddenProperties.vertices) overriddenProperties.vertices = () => { return []; }
		if (!overriddenProperties.r) overriddenProperties.r = (timeId) => { return classSettings.r; }
		if (!overriddenProperties.pushMiddleVertice) overriddenProperties.pushMiddleVertice = () => {}
		if (!overriddenProperties.angles) overriddenProperties.angles = (anglesId) => { return classSettings.settings.object.geometry.angles[anglesId]; }
		if (!overriddenProperties.verticeAngles) overriddenProperties.verticeAngles = (anglesCur, verticeId) => { return anglesCur[verticeId]; }
		if (!overriddenProperties.verticeText) overriddenProperties.verticeText = (intersection, text) => { return text(classSettings.settings.object.geometry.angles,  this.searchNearestEdgeVerticeId(intersection.index, intersection)); }
		if (!overriddenProperties.text) overriddenProperties.text = () => { return ''; }
		if (!overriddenProperties.onSelectSceneEndSetDrawRange) overriddenProperties.onSelectSceneEndSetDrawRange = (timeId) => {}

		//нужно для classSettings.settings.object.geometry.times в проекте universe
		//В этом случае нужно знать количество углов вершины еще до того как будет получена Universe.hyperSphere.dimension
		classSettings.dimension = this.dimension;

		this.pointLength = () => { return this.dimension > 2 ? this.dimension : 3; }//itemSize of the buiffer.attributes.position должен быть больше 2. Иначе при копировании из буфера в THREE.Vector3 координата z = undefined
		this.getPoint = (anglesId, timeId) => {

			const r = classSettings.overriddenProperties.r(timeId),
				angles = typeof anglesId === "number" ? classSettings.overriddenProperties.angles(anglesId, timeId) : anglesId,
				a2v = (angles) => {
	
				//https://en.wikipedia.org/wiki/N-sphere#Spherical_coordinates
				const n = this.dimension, φ = [],//angles,
					x = [], cos = Math.cos, sin = Math.sin;
				//нужно для того, чтобы начало координат широты находилось на экваторе
				for (let i = 0; i < angles.length; i++) {
	
					const rotateLatitude = this.getRotateLatitude(i);
					φ.push((rotateLatitude === 0 ? 1 : - 1) * angles[i] - rotateLatitude);//Для широты меняем знак угола что бы положительная широта была в северном полушарии
	
				}
	
				//добавляем оси
				
				for (let index = 0; index < n; index++) {
	
					let axis = r;
					const i = this.axes.indices[index],
						mulCount = //количество множителей для данной оси
						i < (n - 1) ?
							i + 1: //на один больше порядкового номера оси
							i;//или равно порядковому номеру оси если это последняя ось
					for (let j = 0; j < mulCount; j++) {
	
						if(j === (mulCount - 1)){
	
							//Это последний множитель для текущей оси
							if (i != (n - 1)) {
								
								//Это не последняя ось
								axis *= cos(φ[j]);
								continue;
	
							}
							
						}
						axis *= sin(φ[j]);
	
					}
					x.push(axis);
	
				}
				return x;
	
			}
			//angles.forEach((angle, i) => { console.log('angle[' + i + '] = ' + angle) })
			const vertice = a2v(angles);
			if (this.classSettings.debug && this.classSettings.debug.testVertice){
	
				const vertice2angles = this.vertice2angles(vertice),
					angles2vertice = a2v(vertice2angles);
				const value = vertice;
				if (angles2vertice.length != value.length) console.error(sHyperSphere + ': Set vertice failed. angles2vertice.length = ' + angles2vertice.length + ' is not equal value.length = ' + value.length);
				const d = 7e-16;
				angles2vertice.forEach((axis, i) => { if(Math.abs(axis - value[i]) > d) console.error(sHyperSphere + ': Set vertices[' + anglesId + '] failed. axis = ' + axis + ' is not equal to value[' + i + '] = ' + value[i]) } );
				
			}
			const proxyVertice = new Proxy(vertice, {

				get: (vertice, name) => {

					switch (name) {

						case 'x': return vertice[0];
						case 'y': return vertice[1];
						case '2':
						case 'z':
							const axisZ = vertice[2];
							return axisZ != undefined ? axisZ : 0;//В двумерной гиперсфере (окружности) координата Z = 0
						case 'w': return vertice[3];
						case 'length':
							const length = vertice.length;
							return length > 2 ? length : 3;
						case 'forEach': return (item) => { for (let verticeId = 0; verticeId < proxyVertice.length; verticeId++) item(proxyVertice[verticeId], verticeId); }

					}
					return vertice[name];

				},

			});
			return proxyVertice;
		
		}

		this.searchNearestEdgeVerticeId = (verticeId, intersection) => {

			if (!classSettings.edges.project) return verticeId;
			if (intersection.nearestEdgeVerticeId != undefined) return intersection.nearestEdgeVerticeId;
			const array = intersection ? intersection.object.geometry.index.array : undefined, edge = array ? [array[intersection.index], array[intersection.index + 1]] : [];
			let minDistance = Infinity;//, pointId;
			const position = intersection.object.geometry.attributes.position,
				point = intersection.point ? intersection.point : new THREE.Vector3().fromBufferAttribute(position, intersection.index),
				distance = (i) => {
				
				const pointIndex = edge[i],
					distance = point.distanceTo(new THREE.Vector3().fromBufferAttribute(position, pointIndex));
				if (minDistance > distance) {

					minDistance = distance;
					intersection.nearestEdgeVerticeId = pointIndex;

				}

			}
			distance(0);
			distance(1);
			return intersection.nearestEdgeVerticeId;

		}
		this.getRotateLatitude = (i) => i === (this.dimension - 3) ? this.rotateLatitude : 0;
		this.setPositionAttributeFromPoints(settings.object.geometry.angles);//itemSize of the buiffer.attributes.position должен быть больше 2. Иначе при копировании из буфера в THREE.Vector3 координата z = undefined

		settings.object.geometry.indices = settings.object.geometry.indices || [];
		if (!(settings.object.geometry.indices instanceof Array)) {

			const indices = [];
			Object.keys(settings.object.geometry.indices).forEach((key) => indices[key] = settings.object.geometry.indices[key]);
			settings.object.geometry.indices = indices;

		}
		const indices = settings.object.geometry.indices;
		indices[0] = indices[0] || [];
		if (indices.edges)
			if (indices.edges instanceof Array) {

				indices[0] = indices.edges;
				indices[0].count = indices.edges.length;

			} else {

				const edges = indices[0];
				Object.keys(indices.edges).forEach((key) => edges[key] = indices.edges[key]);
				indices.edges = edges;

			}
		indices.edges = new Proxy(indices[0], {

			get: (_edges, name) => {

				const edgeId = parseInt(name);
				if (!isNaN(edgeId)) {

					let edge = _edges[edgeId];
					return edge;

				}
				const setVertice = (edge, edgeVerticeId, verticeId, edgeId) => {

					if (verticeId >= position.length) verticeId = 0;
					const vertice = position[verticeId];//push random vertice if not exists
					if (edgeVerticeId != undefined) edge[edgeVerticeId] = verticeId;

					vertice.edges.push(edgeId === undefined ? _edges.length : edgeId, verticeId);

				}
				switch (name) {

					case 'setVertices': return () => {

						if (_edges.length > 0) _this.boTestVertice = false;

					}
					case 'push': return (edge = []) => {

						let vertice0Id = edge[0] === undefined ? _edges.length : edge[0],
							vertice1Id = edge[1] === undefined ? _edges.length + 1 : edge[1];
						const sPushEdge = ': Push edge. '
						if ((vertice0Id >= position.length) || (vertice1Id >= position.length)) {

							console.error(sHyperSphere + sPushEdge + 'edge[' + vertice0Id + ', ' + vertice1Id + ']. Invalid vertice range from 0 to ' + (position.length - 1));
							return;

						}
						if ((position[vertice0Id].edges.length >= _this.verticeEdgesLength) || (position[vertice1Id].edges.length >= _this.verticeEdgesLength))
							return;//Не добавлять новое ребро если у его вершин количество ребер больше или равно _this.verticeEdgesLength
						setVertice(edge, 0, vertice0Id);
						setVertice(edge, 1, vertice1Id);
						if (classSettings.debug) {

							if (edge.length != 2) console.error(sHyperSphere + sPushEdge + 'Invalid edge.length = ' + edge.length);
							else if (edge[0] === edge[1]) console.error(sHyperSphere + sPushEdge + 'edge = [' + edge + '] Duplicate vertices.');
							_edges.forEach((edgeCur, i) => { if (((edgeCur[0] === edge[0]) && (edgeCur[1] === edge[1])) || ((edgeCur[0] === edge[1]) && (edgeCur[1] === edge[0]))) console.error(sHyperSphere + sPushEdge + 'edges[' + i + ']. Duplicate edge[' + edge + ']') });

						}

						return _edges.push(edge);

					}
					case 'pushEdges': return (edge = []) => {

						_edges.count = _edges.count || 3;
						for (let i = 0; i < _edges.count; i++) {

							const edge = _edges[i];
							if (edge) {

								setVertice(edge, 0, edge[0], i);
								setVertice(edge, 1, edge[1], i);

							} else {

								if (i === (_edges.count - 1)) indices.edges.push([settings.object.geometry.position.length - 1, 0])//loop edges
								else indices.edges.push();

							}

						}

					}

				}
				return _edges[name];

			},
			//set indices.edges
			set: (_edges, name, value) => {

				switch (name) {

					case 'length':
						const position = settings.object.geometry.position;
						for (let i = value; i < settings.object.geometry.position.length; i++) position[i].edges = undefined;//delete position[i].edges;
						break;

				}
				_edges[name] = value;
				return true;

			}

		});
		indices.edges.setVertices();

		this.getPositionItem = (vertice, name) => {

			switch (name) {

				case 'radius': 
					let r = 0;
					vertice.forEach(axis => r += axis * axis);
					return Math.sqrt(r);

			}

		}

		this.axisName = (angleId) => {

			//Localization

			const lang = [

				'Altitude',
				'Latitude',
				'Longitude',

			]

			switch (options.getLanguageCode()) {

				case 'ru'://Russian language

					lang[0] = 'Высота';
					lang[1] = 'Широта';
					lang[2] = 'Долгота';
					break;

			}
			return lang[angleId + 4 - _this.dimension];

		}

		//Localization

		const getLanguageCode = options.getLanguageCode;

		const lang = {

			advansed: 'Advansed',

			angles: 'Angles',
			anglesTitle: 'Polar coordinates.',

			angle: 'Angle',

			edges: 'Edges',
			edgesTitle: 'Edges indexes of the vertice',
			oppositeVertice: 'Opposite vertice',

			highlightEdges: 'Highlight',
			highlightEdgesTitle: 'Highlight edges of the vertice.',

			middleVertice: 'Middle vertice',
			middleVerticeTitle: 'Find middle vertice between opposite vertices.',

			plane: 'Plane',
			planes: 'Planes',
			planesTitle: 'Planes of rotation of angles.',

			radius: 'Radius',
			radiusTitle: 'Hypersphere radius.',

			defaultButton: 'Default',
			defaultAnglesTitle: 'Restore default angles.',

			notSelected: 'not selected',
			arc: 'Arc',

			pointId: "Point Id",
			edgeId: "Edge Id",

		};

		const _languageCode = getLanguageCode();

		switch (_languageCode) {

			case 'ru'://Russian language

				lang.advansed = 'Дополнительно';

				lang.angles = 'Углы';
				lang.anglesTitle = 'Полярные координаты.';

				lang.angle = 'Угол';

				lang.edges = 'Ребра';
				lang.edgesTitle = 'Индексы ребер, имеющих эту вершину';
				lang.oppositeVertice = 'Противоположная вершина';

				lang.highlightEdges = 'Выделить';
				lang.highlightEdgesTitle = 'Выделить ребра этой вершины.';

				lang.middleVertice = 'Средняя';
				lang.middleVerticeTitle = 'Найти среднюю вершину между противоположными вершинами.';

				lang.plane = 'Плоскость';
				lang.planes = 'Плоскости';
				lang.planesTitle = 'Плоскости вращения углов.';

				lang.radius = 'Радиус';
				lang.radiusTitle = 'Радиус гиперсферы.';

				lang.defaultButton = 'Восстановить';
				lang.defaultAnglesTitle = 'Восстановить углы по умолчанию';

				lang.notSelected = 'Не выбрано';
				lang.arc = 'Дуга';

				lang.pointId = 'Индекс вершины';
				lang.edgeId = 'Индекс ребра';

				break;
			default://Custom language

		}

		//Эту функцию надо содать до вызова this.pushEdges(); потому что когда используется MyPoints для вывода на холст вершин вместо ребер,
		//вызывается this.project вместо this.pushEdges()
		/**
		 * Projects the hypersphere onto the canvas 
		 * @param {THREE.Scene} scene [THREE.Scene]{@link https://threejs.org/docs/index.html?q=sce#api/en/scenes/Scene}
		 * @param {object} [params={}] The following parameters are available
		 * @param {object} [params.center={x: 0.0, y: 0.0, z: 0.0}] center of the hypersphere
		 * @param {float} [params.center.x=0.0] X axis of the center
		 * @param {float} [params.center.y=0.0] Y axis of the center
		 * @param {float} [params.center.z=0.0] Z axis of the center
		 */
		this.project = (scene, params = {}) => {

			if (scene) {

				_this.classSettings.projectParams = _this.classSettings.projectParams || {};
				_this.classSettings.projectParams.scene = scene;

			} else scene = _this.classSettings.projectParams.scene;

			let nd, myPoints;
			this.object = () => {

				console.warn(sHyperSphere + ': this.object() a was deprecated. Use this.object3D instead')
				return nd && nd.object3D ? nd.object3D : myPoints ? myPoints : undefined;
			
			}
			/**
			 * returns a THREE graphical object
			 */
			Object.defineProperty(this, 'object3D', {

				get: () => { return nd && nd.object3D ? nd.object3D : myPoints ? myPoints : undefined; },
//				set: (object3DNew) => { },
//				configurable: true,//https://stackoverflow.com/a/25518045/5175935

			});
			const aAngleControls = [];

			this.objectOpacity = 0.3;
			this.opacity = (transparent = true, opacity = this.objectOpacity) => {

				if (!nd) myPoints.userData.opacity(transparent ? opacity : 1);
				else nd.opacity(nd.object3D, transparent, opacity);

			}

			const removeObject = (object) => {

				if (!object) return;
				scene.remove(object);
//				if (options.guiSelectPoint) options.guiSelectPoint.removeMesh(object);

			}

			//remove previous hypersphere
			this.remove = (scene) => {

				if (classSettings.boRemove === false) return;
				for (var i = scene.children.length - 1; i >= 0; i--) {

					const child = scene.children[i];
					this.remove(child);
					removeObject(child);

				}

			}
			this.remove(scene);
			this.removeHyperSphere = () => {

//				const object = _this.object();
				const object = _this.object3D;
				if (nd) nd = undefined;
				if (myPoints) myPoints = undefined;
				removeObject(object);

			}
			this.removeMesh = () => {

				settings.object.geometry.indices.edges.length = 0;
				_this.remove(classSettings.projectParams.scene);
				if (nd) nd = undefined;
				if (myPoints) myPoints = undefined;

			}

			this.Test();

			this.color();

			if (this.setW) this.setW();

			this.update = (verticeId, changedAngleId, timeId) => {

				const points = nd && (nd.object3D.visible === true) ? nd.object3D : myPoints;
				/*
				const vertice = settings.object.geometry.position[verticeId];
				this.setPositionAttributeFromPoint(verticeId, vertice, timeId);
				*/
				this.bufferGeometry.attributes.position.needsUpdate = true;
				this.bufferGeometry.attributes.color.needsUpdate = true;
				if (settings.options.axesHelper)
					settings.options.axesHelper.updateAxes();
				const guiSelectPoint = settings.options.guiSelectPoint;
				if (guiSelectPoint) {

					guiSelectPoint.update(true);
					guiSelectPoint.exposePosition();
					const setControl = (control) => {

						if (!control || !control.getValue()) return;
						control.setValue(false);
						control.setValue(true);

					}
					setControl(aAngleControls.cHighlightEdges);
					setControl(aAngleControls.cMiddleVertice);

					if (aAngleControls.cross) {

						aAngleControls.cross.position.copy(settings.object.geometry.position[aAngleControls.oppositeVerticeId]);
						if (aAngleControls.cross.position.z === undefined) aAngleControls.cross.position.z = 0;

					}

					if (aAngleControls.arc) aAngleControls.createArc();

					if (aAngleControls.planes) aAngleControls.planes.update(changedAngleId);

					const position = settings.bufferGeometry.userData.position[verticeId];
					aAngleControls.cRadius.setValue(position.radius);

				}

			}
			this.projectGeometry = () => {
				const raycaster = {
					text: (intersection) => {

						return classSettings.overriddenProperties.verticeText(intersection, (angles, index) => {

							let text = //'\n' + lang.angles + ':'
								classSettings.overriddenProperties.text(/*tab*/'', angles, lang)
								+ '\n' + lang.pointId + ': ' + index
								+ '\n' + lang.angles + ':'
							angles[index].forEach((axisAngle, angleId) => { text += '\n\t' + this.axisName(angleId) + ': ' + axisAngle })
							return text;

						});

					},
				}
				this.line = (settings, r = 1) => {

					const options = classSettings.settings.options;
					return this.newHyperSphere(
						//Если не делать копию classSettings.settings.options, то изменится classSettings.settings.options.scales.w.min и max,
						//что приведет к неправильному цвету вершины в universe при ее ручном изменении
						{

							player: options.player,
							getLanguageCode: options.getLanguageCode,
							guiSelectPoint: options.guiSelectPoint,
							renderer: options.renderer,
							palette: options.palette,
							scales: options.scales,
							
						},
						{

							cookieName: settings.cookieName,
							boRemove: false,
							boGui: false,
							edges: settings.edges === undefined ? {

								project: true,//Если линия создается в виде ребер, то отображать ребра на холсте
								creationMethod: HyperSphere.edgesCreationMethod.Random,

							} : settings.edges,
							projectParams: { scene: classSettings.projectParams.scene, },
//							r: r * classSettings.r,
							r: classSettings.overriddenProperties.r(classSettings.settings.guiPoints ? classSettings.settings.guiPoints.timeId : 0),
							debug: classSettings.debug,
							settings: {

								object: {

									name: settings.object.name,
									color: settings.object.color,
									geometry: {

										MAX_POINTS: settings.object.geometry.MAX_POINTS,
										angles: settings.object.geometry.angles,
										opacity: settings.object.geometry.opacity,
										indices: {

											edges: settings.object.geometry.indices.edges,

										}

									}

								}

							},

						});

				}
				const intersection = (parent) => {

					{//hide userData
						
//						const userData = this.object().userData;
						const userData = this.object3D.userData;
						userData.player = userData.player || {};//for ND
						userData.player.boArrayFuncs = false;//не создавать массив userData.player.arrayFuncs потому что точки объекта буду получать из this.object3D.geometry.attributes.position
						userData.player.arrayFuncs = new Proxy([], {

							get: (arrayFuncs, name) => {

								const verticeId = parseInt(name);
								if (!isNaN(verticeId)) {

//									const position = this.object().geometry.attributes.position;
									const position = this.object3D.geometry.attributes.position;
									return (
										position.itemSize === 4 ?
											new THREE.Vector4() :
											position.itemSize === 3 ?
												new THREE.Vector3() :
												undefined//console.error(sHyperSphere + ': get arrayFuncs item failed! Invalid position.itemSize = ' + position.itemSize)
									).fromBufferAttribute(position, verticeId);
									
								}
								return arrayFuncs[name];
								
							},
							
						});
						
					}
					if (!classSettings.intersection) return;

					if (classSettings.intersection.position === undefined) classSettings.intersection.position = 0;
					const mesh = this.intersection(classSettings.intersection.color === undefined ? "lightgray" ://0x0000FF : //blue
						classSettings.intersection.color, scene);

					//Localization

					const lang = {

						intersector: 'Intersector',

					};

					switch (options.getLanguageCode()) {

						case 'ru'://Russian language

							lang.intersector = 'Сечение';

							break;

					}
					mesh.name = lang.intersector;
					parent.add(mesh);
					if (options.guiSelectPoint) options.guiSelectPoint.addMesh(mesh);

				},
					guiSelectPoint = settings.options.guiSelectPoint,
					gui = (object) => {

						const anglesDefault = [];
						object.userData.gui = {

							get isLocalPositionReadOnly() { return true; },
							get hyperSphere() { return _this; },
							setValues: (verticeId, anglesCur) => {

								//пользователь выбрал вершину

/*бесконечный цикл
								const guiPoints = settings.guiPoints;
								guiPoints.verticeId = 0;
								guiPoints.getVerticeId(parseInt(verticeId));
								verticeId = guiPoints.verticeId;
*/								
								verticeId = parseInt(verticeId);

								anglesDefault.length = 0;
								if (!anglesCur) anglesCur = settings.object.geometry.angles;
								const verticeAngles = classSettings.overriddenProperties.verticeAngles(anglesCur, verticeId);

								for (let i = (aAngleControls.cEdges.__select.length - 1); i > 0; i--)
									aAngleControls.cEdges.__select.remove(i);
								_display(aAngleControls.fOppositeVertice.domElement, false);

								if (verticeAngles.edges) {

									const edges = settings.object.geometry.indices.edges;
									const timeVerticeId = settings.guiPoints ? settings.guiPoints.getVerticeId(parseInt(verticeId)) : verticeId;
									verticeAngles.edges.forEach(edgeId => {

										const opt = document.createElement('option'),
											edge = edges[edgeId];
										opt.innerHTML = '(' + edgeId + ') ' + timeVerticeId + ', ' + (
											edge[0] === timeVerticeId ?
												edge[1] : edge[1] === timeVerticeId ?
													edge[0] :
													console.error(sHyperSphere + ': Vertice edges GUI. Invalid edge vertices: ' + edge)
										);
										opt.setAttribute('value', edgeId);
										aAngleControls.cEdges.__select.appendChild(opt);

									});

									//Не нужно показывать список ребер, выреление ребер и среднюю вершину для этой вершины если у вершины нет ребер
									//У вершины нет ребер если пользователь убрал галочку в 'Ребра' в настройках гиперсферы
									const boDisplay = verticeAngles.edges.length === 0 ? false : true;
									_display(aAngleControls.cEdges.domElement.parentElement.parentElement, boDisplay);
									_display(aAngleControls.cHighlightEdges.domElement.parentElement.parentElement, boDisplay);
									_display(aAngleControls.cMiddleVertice.domElement.parentElement.parentElement, boDisplay);

								}

								aAngleControls.verticeId = classSettings.settings.guiPoints && (classSettings.settings.guiPoints.verticeId != undefined) ? classSettings.settings.guiPoints.verticeId : verticeId;

								//если оставить эту линию то если угол выходит за пределы допустимого,
								//то этот угол автоматически возвращается в пределы допустимого с помощью органа управления gui
								//Но это не отображается на изображении гиперсферы
								//_this.isUpdate = false;

								for (let i = 0; i < verticeAngles.length; i++) {

									const angle = verticeAngles[i], cAngle = aAngleControls[i], min = cAngle.__min, max = cAngle.__max;

									//Не изменять позицию вершины когда устанавливаются углы вершины потому
									//что вычисление позиции вершины в зависимости от ее углов приводит к небольшим погрешностям,
									//которые приводят к повлению ошибки
									//Для проверки закоментитвать строку ниже.
									//Открыть http://localhost/anhr/commonNodeJS/master/HyperSphere/Examples/hyperSphere.html
									//Сделать один шаг проигрывателя →
									//Выбрать вершину 3
									//Сделать один шаг проигрывателя →
									//Появится ошибка:
									//HyperSphere: Invalid vertice[2] sum = 0.999078566893569. r = 1
									cAngle.boSetPosition = false;
									
									cAngle.setValue(angle);
									delete cAngle.boSetPosition;
									
									if ((angle < min) || (angle > max)) {

										//Localization

										const lang = {

											error: '%n = %s  of the %v vertice is out of range from %min to %max',

										};

										switch (options.getLanguageCode()) {

											case 'ru'://Russian language

												lang.error = '%n = %s вершины %v выходит из допустимого диапазона от %min до %max';

												break;

										}

										const sError = lang.error.replace('%s', angle).
											replace('%n', cAngle.__li.querySelector(".property-name").innerHTML).
											replace('%v', verticeId).
											replace('%min', min).
											replace('%max', max);
										console.error(sError);
										alert(sError);

									}
									anglesDefault.push(angle);

								}

							},
							reset: (verticeId) => {

								const resetControl = (control) => {

									if (!control) return;
									const boValue = control.getValue();
									control.setValue(false);
									if ((verticeId != -1) && boValue) control.setValue(boValue);

								};
								resetControl(aAngleControls.cHighlightEdges);
								resetControl(aAngleControls.cMiddleVertice);
								resetControl(aAngleControls.cPlanes);

								if (aAngleControls.removeCross) aAngleControls.removeCross();
								if (aAngleControls.removeArc) aAngleControls.removeArc();

							},
							addControllers: (fParent, readOnly) => {

								settings.options.guiSelectPoint.setReadOnlyPosition(true);
								const geometry = settings.object.geometry,
									position = geometry.position,
									edges = geometry.indices.edges,
									dat = three.dat,
									fAdvansed = fParent.addFolder(lang.advansed);

								//Angles

								const createAnglesControls = (fParent, aAngleControls, anglesDefault) => {

									const fAngles = fParent.addFolder(lang.angles);
									dat.folderNameAndTitle(fAngles, lang.angles, lang.anglesTitle);
									for (let angleId = 0; angleId < (_this.dimension - 1); angleId++) {

										const angles = settings.object.geometry.angles,
											range = angles.ranges[angleId],
											cAngle = fAngles.add({ angle: 0, }, 'angle', range.min, range.max, 2 * π / 360).onChange((angle) => {

												if (cAngle.boSetPosition === false) return;
//												const guiPoints = _this.object().userData.myObject.guiPoints;
												const guiPoints = _this.object3D.userData.myObject.guiPoints;
												guiPoints.verticeId = undefined;
												const verticeAngles = classSettings.overriddenProperties.verticeAngles(guiPoints.timeAngles || angles, aAngleControls.verticeId);
												if (verticeAngles[angleId] === angle) return;
												verticeAngles[angleId] = angle;
												_this.setPositionAttributeFromPoint(aAngleControls.verticeId, undefined, guiPoints.timeId);
												_this.update(aAngleControls.verticeId, angleId, guiPoints.timeId);
	
											});
										dat.controllerNameAndTitle(cAngle, this.axisName(angleId));

										aAngleControls.push(cAngle); 

									}

									//Restore default angles.
									const cRestoreDefaultAngles = fAngles.add({

										defaultF: () => {
											
											aAngleControls.forEach((cAngle, i) => cAngle.setValue(anglesDefault[i]));
											settings.options.guiSelectPoint.update(true);
										
										},

									}, 'defaultF');
									dat.controllerNameAndTitle(cRestoreDefaultAngles, lang.defaultButton, lang.defaultAnglesTitle);

									return fAngles;

								}
								createAnglesControls(fAdvansed, aAngleControls, anglesDefault);

								aAngleControls.cRadius = fAdvansed.add({ verticeRadius: options.player.getTime(), }, 'verticeRadius', options.scales.w.min, options.scales.w.max, (options.scales.w.max - options.scales.w.min)/100).onChange((verticeRadius) => {

									if (readOnly.isReadOnlyController(aAngleControls.cRadius)) return;
									console.log('verticeRadius = ' + verticeRadius);

								});
								dat.controllerNameAndTitle(aAngleControls.cRadius, lang.radius, lang.radiusTitle);
								readOnly.readOnlyEl(aAngleControls.cRadius, true);
									
								//Edges

								aAngleControls.cEdges = fAdvansed.add({ Edges: lang.notSelected }, 'Edges', { [lang.notSelected]: -1 }).onChange((edgeId) => {

									edgeId = parseInt(edgeId);
									_display(aAngleControls.fOppositeVertice.domElement, edgeId === -1 ? false : true);
									aAngleControls.removeArc = () => {

										if (aAngleControls.arc && aAngleControls.arc.removeHyperSphere) aAngleControls.arc.removeHyperSphere();
										aAngleControls.arc = undefined;

									}
									aAngleControls.removeCross = () => {

										if (aAngleControls.cross) classSettings.projectParams.scene.remove(aAngleControls.cross);
										aAngleControls.cross = undefined;

									}
									let boTransparent;
									if (edgeId === -1) {

										aAngleControls.removeCross();
										aAngleControls.removeArc();
										aEdgeAngleControls.verticeId = undefined;
										boTransparent = false;

									} else {

										const sChangeVerticeEdge = ': Change vertice edge. ',
											edge = edges[edgeId],
											oppositeVerticeId = edge[0] === aAngleControls.verticeId ? edge[1] : edge[1] === aAngleControls.verticeId ? edge[0] : console.error(sHyperSphere + sChangeVerticeEdge + 'Invalid edge vertices: ' + edge),
											oppositeVerticeAngles = position[oppositeVerticeId].angles;
										if (oppositeVerticeAngles.length != aEdgeAngleControls.length) console.error(sHyperSphere + sChangeVerticeEdge + 'Invalid opposite vertice angles length = ' + oppositeVerticeAngles.length);
										aEdgeAngleControls.verticeId = oppositeVerticeId;
										edgeAnglesDefault.length = 0;
										for (let i = 0; i < oppositeVerticeAngles.length; i++) {

											const angle = oppositeVerticeAngles[i],
												cAngle = aEdgeAngleControls[i];
											cAngle.boSetPosition = false;
											cAngle.setValue(angle);
											delete cAngle.boSetPosition;
											edgeAnglesDefault.push(angle);

										}

										//рисуем крестик на противоположной вершине выбранного ребра
										aAngleControls.removeCross();
										vertices.length = 0;
										if (settings.guiPoints) settings.bufferGeometry.userData.selectedTimeId = settings.guiPoints.timeId;
										const oppositeVertice = position[oppositeVerticeId], crossSize = 0.05;
										pushVertice([0, 0, crossSize]);
										pushVertice([0, 0, -crossSize]);
										pushVertice([-crossSize, -crossSize, 0]);
										pushVertice([crossSize, crossSize, 0]);
										pushVertice([crossSize, -crossSize, 0]);
										pushVertice([-crossSize, crossSize, 0]);
										aAngleControls.cross = addObject2Scene(vertices, 'white');
										aAngleControls.cross.position.copy(oppositeVertice);
										if (aAngleControls.cross.position.z === undefined) aAngleControls.cross.position.z = 0;
										aAngleControls.oppositeVerticeId = oppositeVerticeId;
										aAngleControls.MAX_POINTS = 1 + 2 * 2 * 2 * 2;// * 2 * 2 * 2;//17;//количество вершин дуги когда угол между крайними вершинами дуги 180 градусов. https://stackoverflow.com/questions/31399856/drawing-a-line-with-three-js-dynamically/31411794#31411794

										//Create arc between edge's vertices i.e between vertice and opposite vertice.
										aAngleControls.createArc = () => {

											let verticeId = 0;
											const arcAngles = [],//массив вершин в полярной системе координат, которые образуют дугу
												//если не копировать каждый угол в отделности, то в новой вершине останутся старые ребра
												copyVertice = (vertice) => {

													const verticeAngles = _this.vertice2angles(vertice)
													if (aAngleControls.arc) {

														aAngleControls.arc.classSettings.settings.object.geometry.angles[verticeId] = verticeAngles;
														aAngleControls.arc.object().geometry.drawRange.type = this.bufferGeometry.drawRange.types.edges;//строка выше портит drawRange.type
														verticeId++;

													} else arcAngles.push(verticeAngles);

												},
												arcVerticesCount = 2,
												d = π / arcVerticesCount,
												cd = 1 / Math.sin(d),//Поправка для координат вершин что бы они равномерно располагались по дуге
												vertice = position[aAngleControls.verticeId],// oppositeVertice = position[aAngleControls.oppositeVerticeId],
												//дуга между вершинами
												arcTo = (verticeTo, vertice) => {
				
													//Calculate the arc length between two points over a hyper-sphere
													//Reference: https://www.physicsforums.com/threads/calculate-the-arc-length-between-two-points-over-a-hyper-sphere.658661/post-4196208
													const a = vertice, b = verticeTo, R = 1, acos = Math.acos;
													let ab = 0;//dot product
													for (let i = 0; i < a.length; i++) ab += a[i] * b[i];
													return R * acos(ab / (R * R))
				
												},
												distance = arcTo(oppositeVertice, vertice),
//												distance = vertice.arcTo(oppositeVertice),
												arcCount = distance * (aAngleControls.MAX_POINTS - 1) / π;
											//Не получилось равномерно разделить дугу на части.
											//Если начало и конец дуги расположены напротив друг друга на окружности или на сфере или на 4D hypersphere
											//то все вершины стягиваются к началу и концу дуги за исключением вершины, расположенной посередине дуги
											//Поэтому вершины на дуге получаю путем деления дуги на пополам. Полученные половинки снова делю пополам и т.д.
											let maxLevel = 1;//на сколько частей делить дугу.
											//если maxLevel = 1 то дуга делится на 2 части с одной вершиной посередине
											//если maxLevel = 2 то дуга делится на 4 части с тремя вершинами посередине
											//если maxLevel = 3 то дуга делится на 8 частей с 7 вершинами посередине
											//Таким образом дугу можно разделит только на 2 в степени maxLevel частей
											let count = 2;
											while (count <= arcCount) {

												count *= 2;
												maxLevel++;

											}
											let level = 1;//текущий уровень деления дуги
											copyVertice(vertice);
											let i = 0;
											let halfArcParams = { vertice: vertice, oppositeVertice: oppositeVertice, level: level };
											if (aAngleControls.progressBar) aAngleControls.progressBar.boStop = true;
											aAngleControls.progressBar = new ProgressBar(
												undefined,//settings.options.renderer.domElement.parentElement,
												(progressBar, index, callback) => {

													if (progressBar.boStop) {

														//Этот процес вычисления дуги нужно остановить потому что начался другой процесс вычисления дуги
														progressBar.remove();
														return;

													}

													progressBar.value = i;
													i++;

													//делить дугу на две части

													if (callback) halfArcParams = callback;
													const vertice = halfArcParams.vertice,
														oppositeVertice = halfArcParams.oppositeVertice;
													let level = halfArcParams.level;

													const arcVerticeStep = [];//Шаги, с которым изменяются углы при построении дуги в полярной системе координат
													for (let k = 0; k < vertice.length; k++)
														arcVerticeStep.push((oppositeVertice[k] - vertice[k]) / arcVerticesCount);
													const arcVerice = [];//Координаты вершины в полярной системе координат
													for (let j = 0; j < vertice.length; j++) arcVerice.push(vertice[j] + arcVerticeStep[j] * cd);
													level++;
													if (level <= maxLevel) {

														//если не делать это преобразование,
														//то когда начало и конец дуги на ходятся на противоположных концах гиперсферы,
														//средняя точка попадает в центр окружности или сферы или гиперсферы,
														//а это находится вне гиперсферы.
														//В этом случае все вершины дуги, кроме средней вершины, стягиваются к началу или концу дуги.
														const halfVertice = _this.angles2Vertice(_this.vertice2angles(arcVerice));

														progressBar.step({

															vertice: vertice, oppositeVertice: halfVertice, level: level,
															next: { vertice: halfVertice, oppositeVertice: oppositeVertice, level: level, next: halfArcParams.next }

														});

													} else {

														if (halfArcParams.next)
															progressBar.step(halfArcParams.next);
														copyVertice(arcVerice);
														copyVertice(oppositeVertice);
														if (!halfArcParams.next) {

															if (aAngleControls.arc) {

																const geometry = aAngleControls.arc.object().geometry;
																if (geometry.attributes.position.count < verticeId) console.error(sHyperSphere + '.aAngleControls.createArc: Invalid geometry.attributes.position.count = ' + geometry.attributes.position.count);
																geometry.setDrawRange(0, verticeId * 2 - 1);//geometry.attributes.position.itemSize);//Непонятно почему draw count так вычисляется. Еще смотри class ND.constructor.create3DObject
																console.log(' maxLevel = ' + maxLevel + ' position.count = ' + aAngleControls.arc.object().geometry.attributes.position.count + ' drawRange.count = ' + aAngleControls.arc.object().geometry.drawRange.count + ' Vertices count = ' + verticeId);
																geometry.attributes.position.needsUpdate = true;
																geometry.attributes.color.needsUpdate = true;

															} else {

																if (this.child) this.child.arc(aAngleControls, lang, arcAngles);
																else {
																	console.error(sHyperSphere + ': Непонятно когда сюда попадает')
/*
																	const arcEdges = [];
																	for (let i = 0; i < (aAngleControls.MAX_POINTS - 1); i++) arcEdges.push([i, i + 1]);
																	aAngleControls.arc = this.line({

																		cookieName: 'arc',//если не задать cookieName, то настройки дуги будут браться из настроек гиперсферы
																		//edges: false,
																		object: {

																			name: lang.arc,
																			geometry: {

																				MAX_POINTS: aAngleControls.MAX_POINTS,
																				angles: arcAngles,
																				//opacity: 0.3,
																				indices: {

																					edges: arcEdges,

																				}

																			}

																		},

																	});
*/																	

																}

															}
															console.log(' maxLevel = ' + maxLevel + ' position.count = ' + aAngleControls.arc.object().geometry.attributes.position.count + ' drawRange.count = ' + aAngleControls.arc.object().geometry.drawRange.count + ' Vertices count = ' + verticeId);
															const distance = arcTo(position[aAngleControls.oppositeVerticeId], position[aAngleControls.verticeId]);
//															const distance = position[aAngleControls.verticeId].arcTo(position[aAngleControls.oppositeVerticeId]);
															if (classSettings.debug) {

																let vertice, distanceDebug = 0;
																const position = aAngleControls.arc.classSettings.settings.object.geometry.position;
																for (let i = 0; i < (verticeId === 0 ? position.length : verticeId); i++) {

																	const verticeCur = position[i];
																	if (vertice)
																		distanceDebug += vertice.distanceTo(verticeCur);
																	vertice = verticeCur;

																}
																console.log('distance = ' + distance + ' distanceDebug = ' + distanceDebug + ' error = ' + (distanceDebug - distance) + ' arcAngles.length = ' + arcAngles.length);

															}
															progressBar.remove();
															aAngleControls.progressBar = undefined;

														}

													}

												}, {

												sTitle: 'Long time iteration process',
												max: maxLevel * maxLevel - 2,

											});

										}
										aAngleControls.createArc();
										boTransparent = true;

									}
									_this.opacity(boTransparent);


								});
								aAngleControls.cEdges.__select[0].selected = true;
								dat.controllerNameAndTitle(aAngleControls.cEdges, lang.edges, lang.edgesTitle);
								const aEdgeAngleControls = [], edgeAnglesDefault = [];
								aAngleControls.fOppositeVertice = fAdvansed.addFolder(lang.oppositeVertice);
								_display(aAngleControls.fOppositeVertice.domElement, false);
								createAnglesControls(aAngleControls.fOppositeVertice, aEdgeAngleControls, edgeAnglesDefault);

								const itemSize = this.bufferGeometry.attributes.position.itemSize,
									vertices = [],
									pushVertice = (vertice) => {

										vertice.forEach(axis => vertices.push(axis))
										for (let i = vertice.length; i < itemSize; i++) vertices.push(0);

										//Каждая вершина должна иметь не меньше 3 координат что бы не поучить ошибку:
										//THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values. 
										for (let i = 0; i < (3 - itemSize); i++) vertices.push(0);

									},
									addObject2Scene = (vertices, color) => {

										const buffer = new THREE.BufferGeometry().setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), itemSize > 3 ? itemSize : 3)),
											lineSegments = new THREE.LineSegments(buffer, new THREE.LineBasicMaterial({ color: color, }));
										classSettings.projectParams.scene.add(lineSegments);
										return lineSegments;

									}

								//highlight edges Подсветить ребра для этой вершины

								let oppositeVerticeEdges;
								aAngleControls.cHighlightEdges = fAdvansed.add({ boHighlightEdges: false }, 'boHighlightEdges').onChange((boHighlightEdges) => {

									_this.opacity(boHighlightEdges);
									if (boHighlightEdges) {

										const verticeId = aAngleControls.verticeId,
											angles = position.angles[verticeId];
										vertices.length = 0;
										angles.edges.forEach(edgeId => {

											const edge = geometry.indices.edges[edgeId];
											edge.forEach(edgeVerticeId => { pushVertice(position[edgeVerticeId]); });

										});
										oppositeVerticeEdges = addObject2Scene(vertices, 'white');

									} else {

										if (oppositeVerticeEdges) classSettings.projectParams.scene.remove(oppositeVerticeEdges);
										oppositeVerticeEdges = undefined;

									}

								});
								dat.controllerNameAndTitle(aAngleControls.cHighlightEdges, lang.highlightEdges, lang.highlightEdgesTitle);

								//Middle vertice

								let middleVerticeEdges;
								aAngleControls.cMiddleVertice = fAdvansed.add({ boMiddleVertice: false }, 'boMiddleVertice').onChange((boMiddleVertice) => {

									_this.opacity(boMiddleVertice);
									if (boMiddleVertice) {

										const verticeId = aAngleControls.verticeId,
											angles = classSettings.overriddenProperties.position0.angles[verticeId],
											oppositeVerticesId = angles.oppositeVerticesId,
											settings = classSettings.settings,
											timeId = settings.guiPoints ? settings.guiPoints.timeId : options.player.getTimeId(),
											middleVertice = _this.angles2Vertice(angles.middleVertice(oppositeVerticesId, timeId + 1, false), timeId),
											userData = settings.bufferGeometry.userData;
										
										userData.selectedTimeId = timeId;//Для корректной работы position[oppositeVerticeId] когда во вселенной пользователь выбрал вершину не на последнем времени проигрывателя.
										//Для проверки открыть http://localhost/anhr/universe/main/hyperSphere/Examples/
										//Сделать два шага проигрывателя, нажав →
										//Выбрать вершину не на последнем шаге проигрывателя
										//Вычислить средее значение вершины, пометив пункт "Средняя"
										
										vertices.length = 0;
										oppositeVerticesId.forEach(oppositeVerticeId => {

											pushVertice(middleVertice);
											pushVertice(position[oppositeVerticeId]);

										});
										delete userData.selectedTimeId;
										middleVerticeEdges = addObject2Scene(vertices, 'blue');

									} else {

										if (middleVerticeEdges) classSettings.projectParams.scene.remove(middleVerticeEdges);
										middleVerticeEdges = undefined;

									}

								});
								dat.controllerNameAndTitle(aAngleControls.cMiddleVertice, lang.middleVertice, lang.middleVerticeTitle);

								//Planes of rotation of angles.

								aAngleControls.cPlanes = fAdvansed.add({ boPlanes: false }, 'boPlanes').onChange((boPlanes) => {

									if (!boPlanes) {

										if (aAngleControls.planes) aAngleControls.planes.forEach((plane) => plane.removeHyperSphere())
										aAngleControls.planes = undefined;
										return;

									}

									const vertice = position.angles[aAngleControls.verticeId],
										longitudeId = this.dimension - 2, latitudeId = longitudeId - 1, altitudeId = latitudeId - 1;
									const planeGeometry = (verticeAngleId, planeAngles) => {

										const plane = aAngleControls.planes ? aAngleControls.planes[verticeAngleId] : undefined;
										planeAngles = planeAngles || plane.classSettings.settings.object.geometry.angles;
										let planeVerticeId = 0;

										let start, stop;
										switch (verticeAngleId) {

											case latitudeId:
											case longitudeId:
												start = -π; stop = π;
												break;
											case altitudeId:
												const altitudeRange = settings.object.geometry.angles.ranges[verticeAngleId];
												start = altitudeRange.min; stop = altitudeRange.max;
												break;
											default: console.error(sHyperSphere + ': Planes of rotation of angles. Invalid verticeAngleId = ' + verticeAngleId);

										}
										for (let i = start; i <= stop; i = i + (π / 20)) {

											const planeAngle = [];
											const vertice = position.angles[aAngleControls.verticeId];
											for (let verticeAngleId = 0; verticeAngleId < vertice.length; verticeAngleId++) planeAngle.push(vertice[verticeAngleId]);
											planeAngle[verticeAngleId] = i;

											//if (this.classSettings.debug) console.log(sHyperSphere + ': ' + settings.object.geometry.angles.ranges[verticeAngleId].angleName + '. VerticeId = ' + planeAngles.length);
											planeAngles[planeVerticeId++] = this.normalizeVerticeAngles(planeAngle);

										}
//										if (plane) plane.object().geometry.attributes.position.needsUpdate = true;
										if (plane) plane.object3D.geometry.attributes.position.needsUpdate = true;

									}
									for (let verticeAngleId = 0; verticeAngleId < vertice.length; verticeAngleId++) {

										const planeAngles = [], angleName = settings.object.geometry.angles.ranges[verticeAngleId].angleName;
										planeGeometry(verticeAngleId, planeAngles);
										const planeEdges = [];
										for (let i = 0; i < (planeAngles.length - 1); i++) planeEdges.push([i, i + 1]);
										if (!aAngleControls.planes) {

											aAngleControls.planes = [];
											aAngleControls.planes.update = (changedAngleId) => { this.planesGeometry(changedAngleId, aAngleControls, planeGeometry, longitudeId); }

										}
										aAngleControls.planes[verticeAngleId] = this.line({

											cookieName: 'plane_' + verticeAngleId,//если не задать cookieName, то настройки дуги будут браться из настроек гиперсферы
											object: {

												name: angleName,
												color: 'white',
												geometry: {

													angles: planeAngles,
													opacity: 0.3,
													indices: { edges: planeEdges, }

												}

											},

										}, position.angles.player.t);
										const plane = aAngleControls.planes[verticeAngleId];
										plane.opacity();

									}

								});
								dat.controllerNameAndTitle(aAngleControls.cPlanes, lang.planes, lang.planesTitle);

								return fAdvansed;

							},

						}

					};
				if ((classSettings.edges != false) && classSettings.edges.project) {

					if (myPoints) {

						myPoints.visible = false;
						if (options.eventListeners) options.eventListeners.removeParticle(myPoints);
						if (guiSelectPoint) {

							guiSelectPoint.removeMesh(myPoints, false);
							myPoints.children.forEach(child => guiSelectPoint.removeMesh(child, false));

						}

					}
					if (nd) {

						nd.object3D.visible = true;
						if (options.eventListeners) options.eventListeners.addParticle(nd.object3D);
						if (guiSelectPoint) {

							guiSelectPoint.addMesh(nd.object3D);
							nd.object3D.children.forEach(child => guiSelectPoint.addMesh(child));

						}

					} else {

						settings.scene = scene;
						if ((settings.object.geometry.indices.edges.length === 0)) this.pushEdges();
						else {

							if ((settings.object.geometry.position[0].length > 3) && (!settings.object.color)) settings.object.color = {};//Color of vertice from palette
							nd = new ND(this.dimension, settings);

							nd.guiPoints.searchNearestEdgeVerticeId = this.searchNearestEdgeVerticeId;

							nd.object3D.userData.raycaster = raycaster;
							
							params.center = params.center || {}
							nd.object3D.position.x = params.center.x || 0;
							nd.object3D.position.y = params.center.y || 0;
							nd.object3D.position.z = params.center.z || 0;

							gui(nd.object3D);

							intersection(nd.object3D);

							if (this.onSelectScene) this.onSelectScene();

						}

					}

				} else {

					if (nd) {

						nd.object3D.visible = false;
						if (options.eventListeners) options.eventListeners.removeParticle(nd.object3D);
						if (guiSelectPoint) {

							guiSelectPoint.removeMesh(nd.object3D);
							nd.object3D.children.forEach(child => guiSelectPoint.removeMesh(child, false));

						}

					}
					if (myPoints) {

						if (myPoints.visible != true) {

							myPoints.visible = true;
							if (options.eventListeners) options.eventListeners.addParticle(myPoints);
							if (guiSelectPoint) {

								guiSelectPoint.addMesh(myPoints);
								myPoints.children.forEach(child => guiSelectPoint.addMesh(child));

							}

						}

					} else {

					const points = undefined;

						//for debug
						//Выводим углы вместо вершин. Нужно для отладки равномерного распределения вершин в гиперсфре
						//См. randomPosition()
						/*
						points = [];
						settings.object.geometry.position.forEach(vertive => points.push(vertive.angles));
						*/

						if (
							(classSettings.settings.object.color != undefined) &&
							(typeof classSettings.settings.object.color != "object") &&
							(typeof classSettings.settings.object.color != "function")
						) {

							const color = new three.THREE.Color(classSettings.settings.object.color);
							classSettings.settings.options.setPalette(new ColorPicker.palette({ palette: [{ percent: 0, r: color.r * 255, g: color.g * 255, b: color.b * 255, },] }));

						}
						new MyPoints(points, scene, {

							pointsOptions: {

								name: settings.object.name,
								color: this.color(),//settings.object.color,
								colors: settings.object.geometry.colors,
								opacity: settings.object.geometry.opacity,
								onReady: (points) => {

									myPoints = points;
									myPoints.userData.raycaster = raycaster;
									gui(myPoints);
									intersection(points);

								},
								guiPoints: settings.guiPoints,
								//shaderMaterial: false,

							},
							options: settings.options,
							object: settings.object,
							bufferGeometry: settings.bufferGeometry,
							isPositionControllerReadOnly: true,
							isSetPosition: settings.isSetPosition,
							guiPoints: settings.guiPoints,

						});

					}

				}

			}
			this.projectGeometry();

			//шаг проигрывателя player
			//Вычислем middle vertices
			this.middleVertices = (timeId, t) => {

				if (timeId === 0) return;//не вычисляется средняя точка когда проигрыватель в начале
				const geometry = settings.object.geometry, position = geometry.position, edges = geometry.indices.edges;
				if (edges.length === 0) {

					//Create edges
					this.onSelectScene = () => {

						//Непонятно как сюда попадает
						options.onSelectScene(/*index,*/ t);
						delete this.onSelectScene;

					}
					if (cEdges) cEdges.setValue(true);
					else {

						//нет ручной настройки
						classSettings.edges = cookieOptions.edgesOld || edgesOld;
						_this.projectGeometry();

					}
					return;

				}
				let progressBar, verticeId = 0;
				if ((typeof WebGPU != 'undefined') && WebGPU.isSupportWebGPU()) {

					const firstMatrix = [
						[1, 2, 3, 4],
						[5, 6, 7, 8]
					],
						secondMatrix = [
							[1, 2],
							[3, 4],
							[5, 6],
							[7, 8],
						];
					new WebGPU({

						input: { matrices: [firstMatrix, secondMatrix] },

						//shaderCode: shaderCode,
						shaderCodeFile: '../Shader.c',

						results: [

							{

								count: firstMatrix.length * secondMatrix[0].length +

									//result matrix has reserved three elements in the head of the matrix for size of the matrix.
									//First element is dimension of result matrix.
									//Second element is rows count of the matrix.
									//Third element is columns count of the matrix.
									//See settings.size of out2Matrix method in https://raw.githack.com/anhr/WebGPU/master/jsdoc/module-WebGPU-WebGPU.html
									3,
								out: out => {

									console.log('out:');
									console.log(new Float32Array(out));
									const matrix = WebGPU.out2Matrix(out);
									console.log('matrix:');
									console.log(matrix);

								}

							},
						],

					});

				}
				const overriddenProperties = classSettings.overriddenProperties,
					vertices = overriddenProperties.vertices(),
					timestamp = classSettings.debug ? window.performance.now() : undefined,
					step = () => {

						progressBar.value = verticeId;
						const stepItem = () => {

							const vertice = overriddenProperties.position0.angles[verticeId].middleVertice(undefined, timeId);
							if (vertices) vertices.push(vertice);
							verticeId += 1;
							if (verticeId >= position.length) {

								progressBar.remove();

								if (classSettings.debug) classSettings.debug.logTimestamp('Play step. ', timestamp);

								//Обновление текущей вершины без обновления холста для экономии времени
								overriddenProperties.updateVertices(vertices);

								if (classSettings.debug) {

									classSettings.debug.logTimestamp('Copy vertices. ', timestamp);
									this.logHyperSphere();

								}
								else this.oldR = undefined;
								this.onSelectSceneEnd(timeId);
								return true;

							}
							//options.player.continue();

						}
						if (!stepItem()) progressBar.step();

					},
					bufferGeometry = classSettings.settings.bufferGeometry,
					drawRange = bufferGeometry.drawRange,
					sTakeMiddleVertices = 'Take middle vertices';
				if (classSettings.debug.log != false) console.log('\ntimeId = ' + timeId + '. ' + sTakeMiddleVertices + '.')

				//Установить drawRange что бы не появлялась ошибка
				//HyperSphere.angles2Vertice: anglesId = 2. positionId = 28 is out of range from 0 to 24
				this.setVerticesRange(drawRange.start,
					((drawRange.start + drawRange.count) / (bufferGeometry.index != null ? bufferGeometry.attributes.position.itemSize : 1)) + position.length);
				
				progressBar = new ProgressBar(options.renderer.domElement.parentElement, step, {

					sTitle: 't = ' + t + '<br> ' + sTakeMiddleVertices,
					max: position.length - 1,

				});
				return true;//player pause

			}

			if (classSettings.debug)
				classSettings.debug.logTimestamp('Project. ');

		}
		this.onSelectSceneEnd = (timeId) => {

			classSettings.overriddenProperties.onSelectSceneEndSetDrawRange(timeId);
			
			//Если не установить это значение, то будет неверно устанавливаться значение w в 
			options.player.endSelect();
			
			options.player.continue();
			
		}
		if (classSettings.mode === undefined) classSettings.mode = 0;//решил оставить режим, в котором сначала добавляются ребра а потом уже создаются вершины для них
		switch (classSettings.mode) {

			//connect vertices by edges
			case 0:

				//default vertices
				if (this.verticesCountMin === undefined) {

					console.error(sHyperSphere + ': Please define verticesCountMin in your child class.');
					break;

				}
				const count = position.count === undefined ? this.verticesCountMin : position.count;
				if (count < 2) {

					console.error(sHyperSphere + ': Invalid classSettings.settings.object.geometry.position.count < 2');
					//return;

				}

				if (probabilityDensity) {

					//для 2D гиперсферы это плотность вероятности распределения вершин по поверхости сферы в зависимости от третьей координаты вершины z = vertice.[2]
					//Плотности разбил на несколько диапазонов в зависимости от третьей координаты вершины z = vertice.[2]
					//Разбил сферу на sc = 5 сегментов от 0 до 4.
					//Границы сегментов вычисляю по фомулам:
					//Высота сегмента hs = d / sc = 2 / 5 = 0.4
					//Нижняя граница hb = hs * i - r
					//Верхняя граница ht = hs * (i + 1) - r
					//где r = 1 - радиус сферыб d = 2 * r = 2 - диаметр сферы, i - индекс сегмента
					//0. From -1 to -0.6
					//1. From -0.6 to -0.2
					//2. From -0.2 to 0.2
					//3. From 0.2 to 0.6
					//4. From 0.6 to 1
					console.log('');
					console.log('Probability density.');
					const table = [];
					probabilityDensity.forEach((segment, segmentId) => {

						segment.density = segment.count / segment[_this.probabilityDensity.sectorValueName];//segment.square;
						segment.height = segment.ht - segment.hb;
						table.push(segment);

					})
					const sectorValueName = _this.probabilityDensity.sectorValueName;
					if (!sectorValueName) console.error(sHyperSphere + ': Invalid sectorValueName = ' + sectorValueName);
					console.table(table, ['count', 'hb', 'ht', 'height',
						sectorValueName,
						'density']);
					console.log('');
					classSettings.debug.logTimestamp('Push positions. ');

				}
				this.pushEdges = () => {

					const geometry = this.classSettings.settings.object.geometry, edges = geometry.indices.edges, position = geometry.position;

					//Localization

					const lang = { progressTitle: "Creating edges.<br>Vertice's edges %s from " + this.verticeEdgesLength, };

					switch (settings.options.getLanguageCode()) {

						case 'ru'://Russian language

							lang.progressTitle = 'Создание ребер.<br>Ребер у вершины %s из ' + this.verticeEdgesLength;

							break;

					}

					if (classSettings.edges.creationMethod === undefined) classSettings.edges.creationMethod = edgesCreationMethod.Random;//.NearestVertice;
					switch (classSettings.edges.creationMethod) {

						case edgesCreationMethod.Random:
							let verticeEdgesCur = 1, verticeId = 0,
								boCompleted = false;//Кажется это не нужно
							const progressBar = new ProgressBar(settings.options.renderer.domElement.parentElement, () => {

								const nextVertice = () => {

									progressBar.value = verticeId;

									//цикл поиска вершины, в которую можно добавить еще одно ребро
									const verticeIdStart = verticeId;
									do {

										verticeId++;
										if (verticeId >= position.length) {

											verticeEdgesCur++;
											progressBar.title(lang.progressTitle.replace('%s', verticeEdgesCur + 1));
											if (verticeEdgesCur >= this.verticeEdgesLength) {

												if (this.projectGeometry) this.projectGeometry();
												if (classSettings.debug) classSettings.debug.logTimestamp('Push edges. ');
												progressBar.remove();
												const edges = settings.object.geometry.indices[0], times = settings.object.geometry.times;
												edges.timeEdgesCount = edges.length;
												if (times) {

													const timeVerticesCount = settings.object.geometry.times[0].length;
													for (let timeId = 1; timeId < settings.object.geometry.rCount; timeId++){
	
														const  shift = timeVerticesCount * timeId;
														for (let edgeId = 0; edgeId < edges.timeEdgesCount; edgeId++) {
	
															const edge = edges[edgeId];
															edges.push([edge[0] + shift, edge[1] + shift]);
	
														}
															
													}

												}
												if (this.classSettings.projectParams) this.project(this.classSettings.projectParams.scene, this.classSettings.projectParams.params);
												if (times && classSettings.edges.project)
													this.setEdgesRange();
												if (classSettings.overriddenProperties.project) classSettings.overriddenProperties.project();
												boCompleted = true;
												return;// true;

											}
											verticeId = 0;

										}

									} while ((position[verticeId].edges.length >= this.verticeEdgesLength) && (verticeIdStart != verticeId));
									progressBar.step();

								}
								if (boCompleted) return;
								let oppositeVerticeId = verticeId + 1;
								if (oppositeVerticeId >= position.length) oppositeVerticeId = 0;
								//Поиск вершины у которой ребер меньше максимального количества ребер и у которой нет нового ребра
								const oppositeVerticeIdFirst = oppositeVerticeId;
								while (true) {

									const oppositeVerticeEdges = position[oppositeVerticeId].edges;
									if (oppositeVerticeEdges.length < this.verticeEdgesLength) {

										//поиск нового ребра в списке ребер этой вершины
										let boContinue = false;
										for (let oppositeVerticeEdgeId = 0; oppositeVerticeEdgeId < oppositeVerticeEdges.length; oppositeVerticeEdgeId++) {

											const oppositeVerticeEdge = edges[oppositeVerticeEdges[oppositeVerticeEdgeId]];
											if (
												(oppositeVerticeEdge[0] === verticeId) && (oppositeVerticeEdge[1] === oppositeVerticeId) ||
												(oppositeVerticeEdge[1] === verticeId) && (oppositeVerticeEdge[0] === oppositeVerticeId)
											) {

												boContinue = true;//это ребро уже существует
												break;

											}

										}
										if (boContinue) {

											oppositeVerticeId++;
											if (oppositeVerticeId >= position.length) oppositeVerticeId = 0;
											continue;//Новое ребро уже есть в текущей вершине. Перейти на следующую вершину

										}
										break;//нашел противоположное ребро

									} else {

										oppositeVerticeId++;
										if (oppositeVerticeId >= position.length) oppositeVerticeId = 0;

									}
									if (oppositeVerticeIdFirst === oppositeVerticeId) break;

								}

								//Возможно был пройден полный круг поиска противолположной вершины и ничего найдено не было
								if (verticeId != oppositeVerticeId) edges.push([verticeId, oppositeVerticeId]);

								nextVertice();

							}, {

								sTitle: lang.progressTitle.replace('%s', verticeEdgesCur + 1),
								max: position.length,
								timeoutPeriod: 3,

							});
							break;
						default: console.error(sHyperSphere + ': pushEdges. Invalid classSettings.edges.creationMethod = ' + classSettings.edges.creationMethod);

					}

				}
				this._verticeEdgesLength = this.verticeEdgesLengthMax;

				if (
					classSettings.edges &&
					(classSettings.settings.object.geometry.indices.edges.length === 0)//ребер нет
				) this.pushEdges();//Для экономии времени не добавляю ребра если на холст вывожу только вершины
				else if (this.classSettings.projectParams) this.project(this.classSettings.projectParams.scene, this.classSettings.projectParams.params);

				break;

			case 1: indices.edges.pushEdges(); break;//push edges. сначала добавляются ребра а потом уже создаются вершины для них
			default: console.error(sHyperSphere + ': Unknown mode: ' + classSettings.mode); return;

		}

		let cEdges;
		if (options.dat && options.dat.gui && (classSettings.boGui != false)) {

			const getLanguageCode = options.getLanguageCode;

			//Localization

			const lang = {

				vertices: 'Vertices',
				verticesCount: 'Count',
				verticesCountTitle: 'Vertices count',

				edges: 'Edges',
				edgesTitle: 'Create edges',

				edge: 'Edge',

				verticeEdgesCountTitle: 'Количество ребер у вершины',

				project: 'Project',
				projectTitle: 'Project edges onto canvas',

			};

			const _languageCode = getLanguageCode();

			switch (_languageCode) {

				case 'ru'://Russian language

					lang.vertices = 'Вершины';
					lang.verticesCount = 'Количество';
					lang.verticesCountTitle = 'Количество вершин';

					lang.edges = 'Ребра';
					lang.edgesTitle = 'Создать ребра';

					lang.edge = 'Ребро';

					lang.verticeEdgesCountTitle = 'Количество ребер у вершины';

					lang.project = 'Отображать';
					lang.projectTitle = 'Отображать ребра на холсте';

					break;

			}

			const folders = options.dat.gui.__folders;
			let fParent;
			Object.keys(folders).forEach((key) => {

				const folder = folders[key], id = folder.id;
				if (id && (id === 'fOptions')) fParent = folder;

			});
			const fHyperSphere = fParent.addFolder(this.name(getLanguageCode)), dat = three.dat;
			const addSettingsFolder = classSettings.overriddenProperties.addSettingsFolder;
			if (addSettingsFolder) addSettingsFolder(fParent, getLanguageCode);

			//vertices

			const fVertices = fHyperSphere.addFolder(lang.vertices);
			fVertices.add(new PositionController((shift) => { cVerticesCount.setValue(settings.object.geometry.angles.length + shift); },
				{ settings: { offset: 1, }, min: 1, max: 1000, step: 1, getLanguageCode: options.getLanguageCode }));

			//Vertices count
			const cVerticesCount = dat.controllerZeroStep(fVertices, settings.object.geometry.angles, 'guiLength');
			dat.controllerNameAndTitle(cVerticesCount, lang.verticesCount, lang.verticesCountTitle);

			//vertice edges

			const fVerticeEdges = fVertices.addFolder(lang.edges);
			fVerticeEdges.add(new PositionController((shift) => { cVerticeEdgesCount.setValue(this.verticeEdgesLength + shift); },
				{ settings: { offset: 1, }, min: 1, max: 10, step: 1, getLanguageCode: options.getLanguageCode }));

			//Vertice edges count
			const cVerticeEdgesCount = dat.controllerZeroStep(fVerticeEdges, this, 'verticeEdgesLength');
			dat.controllerNameAndTitle(cVerticeEdgesCount, lang.verticesCount, lang.verticeEdgesCountTitle);

			//edges

			const objectEdges = { boEdges: ((typeof classSettings.edges) === 'object') || (classSettings.edges === true) ? true : false },
				setCockie = () => { options.dat.cookie.setObject(_this.cookieName, { edges: classSettings.edges, edgesOld: edgesOld, }); };
			if (!classSettings.overriddenProperties.isEdgesOnly) classSettings.overriddenProperties.isEdgesOnly = () => { return false; }
			cEdges = fHyperSphere.add(objectEdges, 'boEdges').onChange((boEdges) => {

				if (boEdges) {

					classSettings.edges = edgesOld;
					cProject.setValue(classSettings.edges.project);

				} else {

					if (classSettings.overriddenProperties.isEdgesOnly()) {

						cEdges.setValue(true);
						return;

					}
					edgesOld = classSettings.edges;
					classSettings.edges = false;

				}

				displayEdge();
				_this.projectGeometry();
				setCockie();

			});
			const fEdge = fHyperSphere.addFolder(lang.edge),
				objectEdge = { boProject: ((typeof classSettings.edges) === 'object') ? classSettings.edges.project : false },
				cProject = fEdge.add(objectEdge, 'boProject').onChange((boProject) => {

					if (classSettings.edges.project === boProject) return;
					classSettings.edges.project = boProject;
					_this.projectGeometry();
					setCockie();

				}),
				displayEdge = () => { _display(fEdge.domElement, classSettings.edges); };
			displayEdge();
			dat.controllerNameAndTitle(cEdges, lang.edges, lang.edgesTitle);
			dat.controllerNameAndTitle(cProject, lang.project, lang.projectTitle);

		}
		this._verticeEdgesLength;
		this.boTestVertice = true;

	}

	/**
	 * get default color is 'lime'
	 */
	get defaultColor() { return 'lime'; }

	/**
	 * get hyper sphere angles. See <b>classSettings.settings.object.geometry.angles</b> parameter of the <b>hyperSphere</b> constructor.
	 */
	get angles() {

		//без разницы какую строку выбрать
		return this.classSettings.settings.object.geometry.angles;
		//return this.classSettings.settings.object.geometry.position.angles
	
	}

	get verticeEdgesLength() { return this._verticeEdgesLength; }
/*
	set verticeEdgesLength(length) {

		this._verticeEdgesLength = length;
		if (this.removeMesh) this.removeMesh();
		this.pushEdges();

	}
*/

	//base methods

	/**
	 * Push random longitude into vertice angles
	 * @param {array} verticeAngles vertice angles
	 */
	pushRandomLongitude(verticeAngles) {

		const ranges = this.classSettings.settings.object.geometry.angles.ranges, longitudeRange = ranges[ranges.length - 1];
		verticeAngles.push(Math.random() * (longitudeRange.max - longitudeRange.min) + longitudeRange.min);
		
	}
	/**
	 * Base method that returns a name of the hyper sphere in the child classes.
	 * @returns a console error if your call this method directly.
	 */
	name() { console.error(sOverride.replace('%s', 'name')); }
	/**
	 * Writes to console an important information about hyper sphere, that can help you for debugging.
	 */
	logHyperSphere() {

		if (!this.classSettings.debug || (this.classSettings.debug.log === false)) return;
		console.log(this.cookieName);
		let i = 0, progressBarValue = 0,
			log = 0;//position log
		const settings = this.classSettings.settings, geometry = settings.object.geometry, position = geometry.position, edges = geometry.indices.edges,
			sLogHyperSphere = sHyperSphere + ': logHyperSphere()',
			progressBar = new ProgressBar(settings.options.renderer.domElement.parentElement, () => {

				switch (log){
					case 0://vertices log
						const vertice = position[i];
						console.log('vertice[' + i + '] = ' + JSON.stringify(vertice) + ', timeId = ' + vertice.timeId + ', angles = ' + JSON.stringify(vertice.angles) + ' edges = ' + JSON.stringify(vertice.edges) + ' r = ' + vertice.radius);
						break;
					case 1://edges log
						const edge = edges[i];
						console.log('edges[' + i + '] = ' + JSON.stringify(edge))
						break;
					default: console.error(sLogHyperSphere + '. Invalid log = ' + log);
				}
				progressBar.value = progressBarValue;
				progressBarValue++;
				i++;
				switch (log){
					case 0://position log
						if (i === position.length) {

							if (this.classSettings.debug.edges === false) {
								
								progressBar.remove();
								break;

							}
							log++;//edges log
							i = 0;

						}
						progressBar.step();
						break;
					case 1://edges log
						if (i >= edges.length) {
							
							progressBar.remove();
							this.oldR = undefined;
							if (this.classSettings.debug)
								this.classSettings.debug.logTimestamp('Geometry log. ');
							
						} else progressBar.step();
						break;
					default: console.error(sLogHyperSphere + '. Invalid log = ' + log);
				}
				
			}, {

			sTitle: 'Geometry log',
			max: position.length - 1 + edges.length - 1,

		});
		
	}
	/**
	 * <pre>
	 * Writes a console error, if any vertices angle is out of the angles range. Normalizes a vertices angle to available range, if out of the angles range is occures.
	 * Writes a console error, if identifier of any edge of the vertices is incorrect.
	 * </pre>
	 */
	Test(){

		if (!this.classSettings.debug) return;
		
		const geometry = this.classSettings.settings.object.geometry;
		geometry.position.test();

		//for future using
		if (geometry.indices.faces) geometry.indices.faces.test();
		
	}
	/**
	 * Writes a console error, if vertice edges count is incorrect.
	 * @param {object} vertice vertice for testing
	 * @param {string} strVerticeId name of the vertice Id
	 */
	TestVertice(vertice, strVerticeId){

		if (!this.boTestVertice) return;
		if (this.classSettings.edges === false) return;
		if (vertice.edges.length < (this.verticeEdgesLength - 1))//Допускается количество ребер на одно меньше максимального значения потому что при опреденном количестве вершин для некоротых вершин не хватает противоположных вершин
			console.error(sHyperSphere + ': Test(). Invalid ' + strVerticeId + '.edges.length = ' + vertice.edges.length);
		
	}
	/**
	 * Converts a vertice angles to vertice position.
	 * @param {number|array} anglesId 
	 * <pre>
	 * number: vertice id
	 * array: array of the vertice angles
	 * </pre>
	 * @param {number} timeId player time id
	 * @returns Vertice position.
	 */
	angles2Vertice(anglesId, timeId) {

		if (typeof anglesId != "number")
			return this.getPoint(anglesId, timeId);

		if (this.classSettings.debug) {
				
			if (anglesId >= this.bufferGeometry.userData.position.length) console.error(sHyperSphere + '.angles2Vertice: Invalid anglesId = ' + anglesId);
			//Пока что не вижу случая, когда надо получить position за пределами this.bufferGeometry.drawRange
			else {

				const bufferGeometry = this.bufferGeometry, drawRange = bufferGeometry.drawRange;
				if (drawRange.type === drawRange.types.vertices) {
						
					const count = bufferGeometry.drawRange.count, start = bufferGeometry.drawRange.start,
						offset = this.positionOffset(this.bufferGeometry.attributes.position, anglesId);
					let sError;
					if (bufferGeometry.index === null) {

						const positionId = offset / bufferGeometry.attributes.position.itemSize;
						if ((positionId >= (count + start)) || (positionId < start))
							sError = '. positionId = ' + positionId;
							
					} else {
							
						if ((offset >= (count + start)) || (offset < start))
							sError = '. offset = ' + offset;

					}
					if ( sError != undefined ) console.error(sHyperSphere + '.angles2Vertice: anglesId = ' + anglesId + sError + ' is out of range from ' + start + ' to ' + (count + start));

				}

			}

		}
		const userData = this.classSettings.settings.bufferGeometry.userData, playerIndexCur = userData.timeId;
		if (timeId === undefined) timeId = playerIndexCur;
		userData.timeId = timeId;
		const vertice = this.bufferGeometry.userData.position[anglesId];
		userData.timeId = playerIndexCur;
		return vertice;

	}
	/**
	 * Converts a vertice position to vertice angles.
	 * @param {array} vertice array of the vertice axes
	 * @returns Vertice angles.
	 */
	vertice2angles(vertice) {
		
		//https://en.wikipedia.org/wiki/N-sphere#Spherical_coordinates
		//тангенс — отношение стороны противолежащего катета vertice[1] к стороне прилежащегоvertice[0], (tg или tan);
		const x = [],//для разных размерностей гиперсферы координаты вершины расположены в разном порядке в соответствии с this.axes.indices
			n = this.dimension - 1, φ = [], atan2 = Math.atan2, sqrt = Math.sqrt;

		if (vertice.length <= n) {

			console.error(sHyperSphere + ': vertice2angles. Invalid vertice.length = ' + vertice.length);
			return;
			
		}

		for (let index = 0; index < vertice.length; index++) x.push(vertice[this.axes.indices[index]]);

		for (let i = 0; i < n; i++) {

			const axes = {};
			if (i === (n - 1)) {
				
				axes.y = x[n]; axes.x = x[n - 1];
				
			} else {
				
				let sum = 0;
				for(let j = (i + 1); j <= n; j++) sum += x[j] * x[j];
				axes.y = sqrt(sum); axes.x = x[i];

			}
			const rotateLatitude = this.getRotateLatitude(i);
			φ.push((rotateLatitude === 0 ? 1 : -1) * atan2(axes.y, axes.x) - rotateLatitude);//Для широты меняем знак угола что бы положительная широта была в северном полушарии
			
			
		}

		//установить углы так, что бы они влезли допустимый диапазон органов управления углов, когда пользователь захочет посмотреть или изменить эти углы
		const longitudeId = φ.length - 1, latitudeId = longitudeId - 1, altitudeId = latitudeId - 1,
			ranges = this.classSettings.settings.object.geometry.angles.ranges;
		let latitude = φ[latitudeId], longitude = φ[longitudeId], altitude = φ[altitudeId];
		if (altitude != undefined) {//у одномерной и двумернй гиперсферы нет высоты

			const altitudeRange = ranges[altitudeId];
			if (altitude > altitudeRange.max) {//π / 2
				
			} else if (altitude < altitudeRange.min) {//0
				
				console.error('Under constraction')
				altitude -= π;
	
			}
			φ[altitudeId] = altitude;
			
		}
		if (latitude != undefined) {//у одномерной гиперсферы нет широты
			
			const latitudeRange = ranges[latitudeId];//, longitudeRange = ranges[longitudeId];
			if (latitude > latitudeRange.max) {//π / 2
				
				latitude = π - latitude;
	
				//долготу развернуть на 180 градусов
				if (longitude > 0) longitude -= π;
				else if (longitude < 0) longitude += π;
				
			} else if (latitude < latitudeRange.min) {//- π / 2
				
				console.error('Under constraction')
				latitude -= π;
	
			}
			φ[latitudeId] = latitude;
			φ[longitudeId] = longitude;

		}
		
		return φ;

	}
	/**
	 * Normalizes a vertices angles to available range, if out of the angles range is occures.
	 * @param {array} verticeAngles vertice angles
	 * @returns Normalized a vertices angles.
	 */
	normalizeVerticeAngles(verticeAngles){ return this.vertice2angles(this.angles2Vertice(verticeAngles)); }

}

const edgesCreationMethod = {
	
	Random: 0,//'Random',
	NearestVertice: 1,//'NearestVertice',
	
}
Object.freeze(edgesCreationMethod);
/**
 * Enums a methods for creating edges:
 * <pre>
 * Random: every vertice of the edge have random position.
 * NearestVertice: Vertices of the edge have nearest position.
 * </pre>
 * */
HyperSphere.edgesCreationMethod = edgesCreationMethod;

/**
 * <a href="../../../master/nD/jsdoc/" target="_blank">ND</a>
 * */
HyperSphere.ND = ND;

export default HyperSphere;

const _display = (element, boDisplay) => { element.style.display = boDisplay === false ? 'none' : 'block'; }