Source: hyperSphere3D.js

/**
 * @module HyperSphere3D
 * @description 3 dimensional [hypersphere]{@link https://en.wikipedia.org/wiki/N-sphere}.
 * All the vertices form a [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 Sphere from './sphere.js';
import three from '../three.js'
import FibonacciSphereGeometry from '../FibonacciSphere/FibonacciSphereGeometry.js'
import anglesRange from './anglesRange.js'
import RandomVertice from './RandomVertice/randomVerticeHSphere.js';
//import RandomCloud from './RandomVertice/randomCloudHSphere.js';
import * as utils from './utilsHSphere.js'
//import Vertice from './VerticeHypersphere.js'
import Position from './position.js'

const sHyperSphere3D = 'HyperSphere3D',
	π = Math.PI;

/**
 * 3 dimensional [hypersphere]{@link https://en.wikipedia.org/wiki/N-sphere}.
 * All the vertices form a [hypersphere]{@link https://en.wikipedia.org/wiki/N-sphere}.
 * @class
 * @extends Sphere
 */
class HyperSphere3D extends Sphere {

	/**
	 * @param {Options} options See <a href="../../../master/jsdoc/Options/Options.html" target="_blank">Options</a>.
	 * @param {object} [classSettings] <b>Circle</b> class settings. See <a href="./module-HyperSphere-HyperSphere.html" target="_blank">HyperSphere classSettings</a>.
	 **/
	constructor(options, classSettings) { super(options, classSettings); }

	//base methods

	planesGeometry(changedAngleId, aAngleControls, planeGeometry, longitudeId){

		const latitudeId = longitudeId - 1, altitudeId = latitudeId - 1;
		switch(changedAngleId){

			case altitudeId:
				planeGeometry(longitudeId);
				planeGeometry(latitudeId )
				break;
			case latitudeId:
			case longitudeId:
				planeGeometry(altitudeId);
				super.planesGeometry(changedAngleId, aAngleControls, planeGeometry, longitudeId); break;
			default: console.error(sHyperSphere3D + ': Update planes. Invalid changedAngleId = ' + changedAngleId);
				
		}
		
	}
	get axes() { return {

			//порядок размещения осей в декартовой системе координат
			//нужно что бы широта двигалась по оси y а долгота вращалась вокруг y
			indices: [3, 1, 2, 0],

		}
		
	}
	newHyperSphere(options, classSettings) { return new HyperSphere3D(options, classSettings); }
	get cookieName() { return '3DUniverse' + (this.classSettings.cookieName ? '_' + this.classSettings.cookieName : ''); }
	get altitudeRange() {

		return anglesRange.altitude;
/*		
		return {
			angleName: 'Altitude',
			min: 0, max: π,//Высота меняется в диапазоне 0 180 градусов. В центре гиперсферы вершины белого и  синего цвета по краям зеленого
		}
*/		
	}
	setW() {

		const classSettings = this.classSettings, options = classSettings.settings.options;
		if (!options.scales) options.scales = {};
		if (!options.scales.w) options.scales.w = {};
		const w = options.scales.w;
		w.max = classSettings.rRange.max;
		w.min = classSettings.rRange.min;
		
		//Если не установить это значение, то будет неверно устанавливаться значение w в geometry.attributes.position
		//потому что в гиперсфере w в geometry.attributes.position это не цвет вершины, а координата
		//Для проверки открыть http://localhost/anhr/commonNodeJS/master/HyperSphere/Examples/hyperSphere.html
		//Выбрать вершину
		//сделать шаг проигрывтеля →
		//Исчезнет ошибка HyperSphere: Invalid vertice[2] sum = 0.6560590267181396. r = 1
		w.isColor = false;
		
	};
	get probabilityDensity() {

		const _this = this;
		return {

			sectorValueName: 'sectorVolume',
			sectorValue: (probabilityDensity, i) => {

				const sector = probabilityDensity[i], r = this.classSettings.r, hb = sector.hb, ht = sector.ht;
				
				//объем сегмента
				//https://en.wikipedia.org/wiki/Sphere
				//https://www.sjsu.edu/faculty/watkins/ndim.htm сводная таблица площади и объема для сфер разной размерности
				sector[this.probabilityDensity.sectorValueName] = Math.PI * Math.PI * r * r * (ht - hb);
				return sector[this.probabilityDensity.sectorValueName];

			},
			get unverseValue() {
				
				//https://www.sjsu.edu/faculty/watkins/ndim.htm
				//Dimension = 4. Bounding Area = 2ππRRR
				const r = _this.classSettings.r;
				return 2 * Math.PI * Math.PI * r * r * r//Bounding Area

			}

		}

	}
	defaultAngles() { return { count: 5, } }//random pentachoron https://en.wikipedia.org/wiki/5-cell

	pushRandomAngle(verticeAngles) {

		//https://en.wikipedia.org/wiki/3-sphere#Hyperspherical_coordinates
		
		//Altitude
		//добиваемся равномерного распределения вершин в объеме шара
		//исчезло уплотнение в ядре шара
		verticeAngles.push(Math.acos(Math.random() * (Math.random() > 0.5 ? 1: -1)));
		
		//добиваемся равномерного распределения вершин в объеме шара
		//исчезло уплотнение на оси через полюса по оси i
		this.pushRandomLatitude(verticeAngles);
		
		this.pushRandomLongitude(verticeAngles);

	}
	color() {}
	name( getLanguageCode ) {

		//Localization
		
		const lang = {

			name: "Hypersphere",

		};

		const _languageCode = getLanguageCode();

		switch (_languageCode) {

			case 'ru'://Russian language

				lang.name = 'Гиперсфера';

				break;

		}
		return lang.name;
		
	}

	intersection(color, scene) {

		const THREE = three.THREE,
			classSettings = this.classSettings,
			mesh = new THREE.Mesh(new FibonacciSphereGeometry(((classSettings.intersection.position + 1) / 2) * classSettings.r, 320),
				//new THREE.MeshBasicMaterial( { color: color, wireframe: true } )//сетка
				new THREE.MeshLambertMaterial( {//полупрозрачные грани

					color: color,//"lightgray",
					opacity: 0.2,
					transparent: true,
					side: THREE.DoubleSide//от этого ключа зависят точки пересечения объектов

				} )
			);
		
		const lights = [], lightsCount = 6;
		for (let i = 0; i < lightsCount; i++) lights.push(new THREE.DirectionalLight(color, i > 2 ? 1 : 0.5));

		lights[0].position.set(200, 0, 0);
		lights[1].position.set(0, 200, 0);
		lights[2].position.set(0, 0, 200);
		lights[3].position.set(-200, 0, 0);
		lights[4].position.set(0, -200, 0);
		lights[5].position.set(0, 0, -200);

		for (let i = 0; i < lightsCount; i++) scene.add(lights[i]);

		return mesh;
		
	}

	//Overridden methods from base class

	middlePosition(points, boCloud = false, boCreateHypersphere = true) {

		const _this = this;
		
		//https://chat.deepseek.com/a/chat/s/8024be13-9782-4432-b29b-7c318db972d0
		/*
		Заданы несколько точек на поверхности гиперсферы в декартовой системе координат. Начало координат в центре гиперсферы.
		Найти точку на поверхности гиперсферы, равноудаленную от заданных точек.
		Написать код на javascript.
		*/
		class HypersphereEquidistantPoint {
			/**
			 * Находит точку на гиперсфере, равноудаленную от заданных точек
			 * @param {Array<Array<number>>} points - Массив точек на гиперсфере
			 * @param {boolean} [boCloud=false] true - generates a random vertice cloud.
			 * @param {boolean} [boCreateHypersphere=true] true - creates a random vertices hypersphere.
			 * @returns {Array<number>} Точка на гиперсфере, равноудаленная от заданных
			 */
			static findEquidistantPoint(points, boCloud = false, boCreateHypersphere = true) {

				const n = points[0].length; // Размерность пространства
				const radius = _this.r;
				
				if (_this.classSettings.debug) {
					
					if (!points || points.length === 0) {
						console.error(sHyperSphere3D + ': findEquidistantPoint. Должна быть задана хотя бы одна точка');
						return;
					}
	
					// Проверка, что все точки имеют одинаковую размерность
					for (let point of points) {
						if (point.length !== n) {
							console.error(sHyperSphere3D + ': findEquidistantPoint. Все точки должны иметь одинаковую размерность');
							return;
						}
					}

				}

				// 1. Находим среднее арифметическое всех точек (центроид)
				const centroid = new Array(n).fill(0);

				for (let i = 0; i < n; i++) {
					for (let j = 0; j < points.length; j++) {
						centroid[i] += points[j][i];
					}
					centroid[i] /= points.length;
				}

				// 2. Нормализуем, чтобы получить точку на гиперсфере
				const norm = Math.sqrt(centroid.reduce((sum, val) => sum + val * val, 0));
				
				_this.setArc(radius, 1 - norm);

				let middleVertice;
				
				if (norm < 7e-17) {
					
					// центроид в начале координат

/*					
					const settings = _this.classSettings.settings;
					const radius = _this.classSettings.overriddenProperties.r(settings.guiPoints ? settings.guiPoints.timeId : settings.options.player.getTimeId());
*/					
					const oppositeVertices = points;

					//https://chat.deepseek.com/a/chat/s/85a1d029-0033-437b-a750-c58f9590bd4c
					/*
					Дана сфера. На поверхности сферы заданы три точки в декартовой системе координат. Начало координат находится в центре сферы.
			Построить плоскость, проходящую через заданные три точки.
			Построить нормаль к этой плоскости такую, что бы она проходила через центр сферы.
			Вычислить координаты двух точек, в которых норамль пересекается с данной сферой.
					*/
					/*
					Сделать подобные вычисления для гиперсферы в 4-мерном пространстве (n=4). Теперь уже заданы не три, а черыте точки на гиперсфере. Написать код на javascript.
					*/
					// Функция вычисления определителя 3x3
					// Функция вычисления определителя 3x3
					function det3x3(m) {
						return m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
							- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
							+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
					}

					// Скалярное произведение в 4D
					function dot4d(a, b) {
						return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
					}

					// Вычитание векторов в 4D
					function sub4d(a, b) {
						return [a[0] - b[0], a[1] - b[1], a[2] - b[2], a[3] - b[3]];
					}

					// Норма вектора в 4D
					function norm4d(v) {
						return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2] + v[3] * v[3]);
					}

					// Нормализация вектора в 4D
					function normalize4d(v) {
						const n = norm4d(v);
						if (n < 1e-12) return v;
						return [v[0] / n, v[1] / n, v[2] / n, v[3] / n];
					}

					// Проекция вектора u на вектор v
					function project4d(u, v) {
						const scale = dot4d(u, v) / dot4d(v, v);
						return [v[0] * scale, v[1] * scale, v[2] * scale, v[3] * scale];
					}

					// Ортогонализация Грама-Шмидта для набора векторов
					function gramSchmidt4d(vectors) {
						const basis = [];

						for (let i = 0; i < vectors.length; i++) {
							let v = vectors[i].slice();

							// Вычитаем проекции на все предыдущие базисные векторы
							//				for (let j = 0; j < i; j++)
							for (let j = 0; j < basis.length; j++) {
								const proj = project4d(v, basis[j]);
								v = sub4d(v, proj);
							}

							// Если вектор не нулевой, добавляем в базис
							if (norm4d(v) > 1e-10) {
								basis.push(normalize4d(v));
							}
						}

						return basis;
					}

					// Генерация случайного вектора в 4D
					function randomVector4d() {
						// Генерируем случайные числа с нормальным распределением
						// для равномерного распределения на сфере
						let v = [
							Math.random() - 0.5,
							Math.random() - 0.5,
							Math.random() - 0.5,
							Math.random() - 0.5
						];

						// Немного вариативности
						const n = norm4d(v);
						if (n > 1e-12) {
							v = [v[0] / n, v[1] / n, v[2] / n, v[3] / n];
						}

						return v;
					}

					// Находит случайную нормаль, ортогональную заданному подпространству
					function findRandomNormal(subspaceBasis) {
						// Начинаем со случайного вектора
						let normal = randomVector4d();

						// Делаем его ортогональным ко всем векторам базиса подпространства
						for (const basisVec of subspaceBasis) {
							const proj = project4d(normal, basisVec);
							normal = sub4d(normal, proj);
						}

						// Нормализуем
						const n = norm4d(normal);
						if (n < 1e-12) {
							// Случай, когда случайный вектор лежит в подпространстве
							// Попробуем другой подход: найдем любой вектор, не входящий в span
							for (let attempt = 0; attempt < 10; attempt++) {
								normal = randomVector4d();
								let isOrthogonal = true;
								for (const basisVec of subspaceBasis) {
									if (Math.abs(dot4d(normal, basisVec)) > 0.1) {
										isOrthogonal = false;
										break;
									}
								}
								if (isOrthogonal && norm4d(normal) > 1e-10) {
									return normalize4d(normal);
								}
							}

							// Если не получилось, возьмем стандартный базисный вектор
							// и сделаем его ортогональным
							normal = [1, 0, 0, 0];
							for (const basisVec of subspaceBasis) {
								const proj = project4d(normal, basisVec);
								normal = sub4d(normal, proj);
							}
						}

						return normalize4d(normal);
					}

					// Основная функция
					function findIntersectionPoints4D(p1, p2, p3, p4) {
						// Проверка размерности
						if (p1.length !== 4 || p2.length !== 4 || p3.length !== 4 || p4.length !== 4) {
							console.error(sHyperSphere3D + ": findIntersectionPoints4D. Все точки должны быть 4-мерными [x,y,z,w]");
							return;
						}

						// Вычисляем векторы из p1 к другим точкам
						const v1 = sub4d(p2, p1);
						const v2 = sub4d(p3, p1);
						const v3 = sub4d(p4, p1);

						// Вычисляем компоненты нормали как миноры 3x3
						const a = det3x3([
							[v1[1], v1[2], v1[3]],
							[v2[1], v2[2], v2[3]],
							[v3[1], v3[2], v3[3]]
						]);

						const b = -det3x3([
							[v1[0], v1[2], v1[3]],
							[v2[0], v2[2], v2[3]],
							[v3[0], v3[2], v3[3]]
						]);

						const c = det3x3([
							[v1[0], v1[1], v1[3]],
							[v2[0], v2[1], v2[3]],
							[v3[0], v3[1], v3[3]]
						]);

						const d = -det3x3([
							[v1[0], v1[1], v1[2]],
							[v2[0], v2[1], v2[2]],
							[v3[0], v3[1], v3[2]]
						]);

						// Проверка на вырожденность
						const normSq = a * a + b * b + c * c + d * d;
						let normal;
						let isDegenerate = false;
						
						if (normSq < 1e-12) {

							//Вырожденный случай: точки лежат в подпространстве меньшей размерности
							isDegenerate = true;
							//console.log("Вырожденный случай: точки лежат в подпространстве меньшей размерности");
							//console.log("Будет выбрана случайная нормаль");

							// Находим базис подпространства, содержащего точки
							const vectors = [v1, v2, v3];
							const basis = gramSchmidt4d(vectors);

							//console.log(`Размерность подпространства: ${basis.length}`);

							// Выбираем случайную нормаль, ортогональную этому подпространству
							normal = findRandomNormal(basis);

							/*
							// Проверяем, что нормаль действительно ортогональна
							console.log("Проверка ортогональности:");
							for (let i = 0; i < basis.length; i++) {
								const dot = dot4d(normal, basis[i]);
								console.log(`  Скалярное произведение с базисом ${i}: ${dot.toFixed(10)}`);
							}
							*/
						} else {
							normal = [a, b, c, d];
							const N = Math.sqrt(normSq);
							normal = [a / N, b / N, c / N, d / N];
						}

						// Радиус гиперсферы (расстояние от центра до любой точки)
						const R = radius;
//						const R = Math.sqrt(p1[0] * p1[0] + p1[1] * p1[1] + p1[2] * p1[2] + p1[3] * p1[3]);

						//случайным образом из двух точек пересечения нормали с гиперсферой выбираем одну
						const scale = Math.random() > 0.5 ? R : -R;
						return [
							normal[0] * scale,
							normal[1] * scale,
							normal[2] * scale,
							normal[3] * scale
						];
						/*
						// Вычисляем две точки пересечения
						const scale = R; // normal уже нормализован
						const point1 = [
							normal[0] * scale,
							normal[1] * scale,
							normal[2] * scale,
							normal[3] * scale
						];
			
						const point2 = [
							normal[0] * -scale,
							normal[1] * -scale,
							normal[2] * -scale,
							normal[3] * -scale
						];
			
						return {
							normal: normal,
							radius: R,
							isDegenerate: isDegenerate,
							intersectionPoints: [point1, point2]
						};
						*/
					}

					/*
					// Тестовые функции
					function testNonDegenerate() {
						console.log("=== Тест 1: Невырожденный случай ===");
						const p1 = [1, 0, 0, 0];
						const p2 = [0, 1, 0, 0];
						const p3 = [0, 0, 1, 0];
						const p4 = [0, 0, 0, 1];
			
						const result = findIntersectionPoints4D(p1, p2, p3, p4);
						console.log("Нормаль:", result.normal.map(x => x.toFixed(4)));
						console.log("Точки пересечения:");
						console.log("  P+:", result.intersectionPoints[0].map(x => x.toFixed(4)));
						console.log("  P-:", result.intersectionPoints[1].map(x => x.toFixed(4)));
						console.log("Радиус:", result.radius.toFixed(4));
						console.log("Вырожденный?:", result.isDegenerate);
						console.log();
					}
			
					function testDegenerate1() {
						console.log("=== Тест 2: Вырожденный случай (3D подпространство) ===");
						// Все точки лежат в гиперплоскости w=0 (3D пространство)
						const p1 = [1, 0, 0, 0];
						const p2 = [0, 1, 0, 0];
						const p3 = [0, 0, 1, 0];
						const p4 = [0.5, 0.5, 0, 0]; // Тоже в w=0
			
						const result = findIntersectionPoints4D(p1, p2, p3, p4);
						console.log("Нормаль:", result.normal.map(x => x.toFixed(4)));
						console.log("Точки пересечения:");
						console.log("  P+:", result.intersectionPoints[0].map(x => x.toFixed(4)));
						console.log("  P-:", result.intersectionPoints[1].map(x => x.toFixed(4)));
						console.log("Радиус:", result.radius.toFixed(4));
						console.log("Вырожденный?:", result.isDegenerate);
						console.log();
					}
			
					function testDegenerate2() {
						console.log("=== Тест 3: Вырожденный случай (2D плоскость) ===");
						// Все точки лежат в плоскости z=0, w=0 (2D пространство)
						const p1 = [1, 0, 0, 0];
						const p2 = [0, 1, 0, 0];
						const p3 = [0.5, 0.5, 0, 0];
						const p4 = [-0.5, 0.5, 0, 0];
			
						const result = findIntersectionPoints4D(p1, p2, p3, p4);
						console.log("Нормаль:", result.normal.map(x => x.toFixed(4)));
						console.log("Точки пересечения:");
						console.log("  P+:", result.intersectionPoints[0].map(x => x.toFixed(4)));
						console.log("  P-:", result.intersectionPoints[1].map(x => x.toFixed(4)));
						console.log("Радиус:", result.radius.toFixed(4));
						console.log("Вырожденный?:", result.isDegenerate);
						console.log();
					}
			
					function testDegenerate3() {
						console.log("=== Тест 4: Вырожденный случай (1D линия) ===");
						// Все точки лежат на одной линии
						const p1 = [1, 0, 0, 0];
						const p2 = [2, 0, 0, 0];
						const p3 = [3, 0, 0, 0];
						const p4 = [4, 0, 0, 0];
			
						try {
							const result = findIntersectionPoints4D(p1, p2, p3, p4);
							console.log("Нормаль:", result.normal.map(x => x.toFixed(4)));
							console.log("Точки пересечения:");
							console.log("  P+:", result.intersectionPoints[0].map(x => x.toFixed(4)));
							console.log("  P-:", result.intersectionPoints[1].map(x => x.toFixed(4)));
							console.log("Радиус:", result.radius.toFixed(4));
							console.log("Вырожденный?:", result.isDegenerate);
						} catch (error) {
							console.log("Ошибка:", error.message);
						}
						console.log();
					}
			
					// Проверка, что точки действительно лежат на гиперсфере
					function verifyPoints(result) {
						console.log("Проверка расстояний от центра:");
						for (let i = 0; i < 2; i++) {
							const p = result.intersectionPoints[i];
							const dist = Math.sqrt(p[0] * p[0] + p[1] * p[1] + p[2] * p[2] + p[3] * p[3]);
							console.log(`  Точка ${i + 1}: расстояние = ${dist.toFixed(6)}, радиус = ${result.radius.toFixed(6)}`);
						}
					}
			
					// Запуск тестов
					console.log("=== Тестирование алгоритма для 4D гиперсферы ===\n");
					testNonDegenerate();
					testDegenerate1();
					testDegenerate2();
					testDegenerate3();		// Вычисление точек пересечения нормали со сферой
					*/

					const point = findIntersectionPoints4D(
						[
							oppositeVertices[0][0],
							oppositeVertices[0][1],
							oppositeVertices[0][2],
							oppositeVertices[0][3],
						],
						[
							oppositeVertices[1][0],
							oppositeVertices[1][1],
							oppositeVertices[1][2],
							oppositeVertices[1][3],
						],
						[
							oppositeVertices[2][0],
							oppositeVertices[2][1],
							oppositeVertices[2][2],
							oppositeVertices[2][3],
						],
						[
							oppositeVertices[3][0],
							oppositeVertices[3][1],
							oppositeVertices[3][2],
							oppositeVertices[3][3],
						],
					);

					middleVertice = Position(point);

//					return Position(point);

				} else {

					// Нормализуем векторы
					middleVertice = centroid.map(coord => coord / norm);
//					const result = centroid.map(coord => coord / norm);
	
					/*
					// 3. Проверяем расстояния до всех точек
					const distances = points.map(point =>
						this.calculateDistance(result, point)
					);
	
					console.log('Расстояния до заданных точек:', distances);
					*/
	
//					return result;

				}
				_this.randomVertices(_this.vertice2angles(middleVertice), _this.object3D.parent, boCloud, boCreateHypersphere);
				return middleVertice;
				
			}

			/**
			 * Вычисляет расстояние между двумя точками на гиперсфере (хордальное расстояние)
			 * @param {Array<number>} point1 
			 * @param {Array<number>} point2 
			 * @returns {number} Расстояние
			 */
			static calculateDistance(point1, point2) {
				let sum = 0;
				for (let i = 0; i < point1.length; i++) {
					const diff = point1[i] - point2[i];
					sum += diff * diff;
				}
				return Math.sqrt(sum);
			}

			/**
			 * Вычисляет сферическое (угловое) расстояние между точками
			 * @param {Array<number>} point1 
			 * @param {Array<number>} point2 
			 * @returns {number} Угол в радианах
			 */
			static calculateAngularDistance(point1, point2) {
				let dot = 0;
				for (let i = 0; i < point1.length; i++) {
					dot += point1[i] * point2[i];
				}
				// Ограничиваем dot для избежания ошибок округления
				dot = Math.max(-1, Math.min(1, dot));
				return Math.acos(dot);
			}

			/**
			 * Альтернативный метод: через минимизацию суммы квадратов расстояний
			 * @param {Array<Array<number>>} points 
			 * @param {number} iterations - Количество итераций для градиентного спуска
			 * @returns {Array<number>}
			 */
			static findEquidistantPointGradient(points, iterations = 1000) {
				const n = points[0].length;

				// Начинаем со случайной точки на гиперсфере
				let point = this.randomPointOnHypersphere(n);

				const learningRate = 0.01;

				for (let iter = 0; iter < iterations; iter++) {
					// Вычисляем градиент функции потерь
					const gradient = new Array(n).fill(0);

					for (let p of points) {
						const dist = this.calculateDistance(point, p);
						for (let i = 0; i < n; i++) {
							gradient[i] += (point[i] - p[i]) / (dist + 1e-10);
						}
					}

					// Обновляем точку
					for (let i = 0; i < n; i++) {
						point[i] -= learningRate * gradient[i];
					}

					// Проецируем обратно на гиперсферу
					const norm = Math.sqrt(point.reduce((sum, val) => sum + val * val, 0));
					for (let i = 0; i < n; i++) {
						point[i] /= norm;
					}

					// Уменьшаем learning rate
					if (iter % 100 === 0 && iter > 0) {
						learningRate *= 0.9;
					}
				}

				return point;
			}

			/**
			 * Генерирует случайную точку на гиперсфере
			 * @param {number} dimension - Размерность
			 * @returns {Array<number>}
			 */
			static randomPointOnHypersphere(dimension) {
				const point = new Array(dimension);
				let sum = 0;

				// Генерируем случайные нормальные числа
				for (let i = 0; i < dimension; i++) {
					point[i] = Math.random() * 2 - 1;
					sum += point[i] * point[i];
				}

				// Нормализуем
				const norm = Math.sqrt(sum);
				return point.map(coord => coord / norm);
			}
		}

		/*
		// Пример использования
		function example() {
			try {
				// Пример для 3D сферы (обычной сферы)
				console.log('Пример 1: 3D сфера');
				const points3D = [
					[1, 0, 0],
					[0, 1, 0],
					[0, 0, 1]
				];

				const result3D = HypersphereEquidistantPoint.findEquidistantPoint(points3D);
				console.log('Точка на сфере:', result3D);
				console.log('Норма:', Math.sqrt(result3D.reduce((sum, val) => sum + val * val, 0)));

				// Пример для 4D гиперсферы
				console.log('\nПример 2: 4D гиперсфера');
				const points4D = [
					[1, 0, 0, 0],
					[0, 1, 0, 0],
					[0, 0, 1, 0],
					[0, 0, 0, 1]
				];

				const result4D = HypersphereEquidistantPoint.findEquidistantPoint(points4D);
				console.log('Точка на гиперсфере:', result4D);
				console.log('Норма:', Math.sqrt(result4D.reduce((sum, val) => sum + val * val, 0)));

				// Пример с симметричными точками
				console.log('\nПример 3: Симметричные точки');
				const symmetricPoints = [
					//[1, 0, 0],
					//[-1, 0, 0],
					//[0, 1, 0],
					//[0, -1, 0]
					
					[1, 0, 0, 0],
					[-1, 0, 0, 0],
					[0, 1, 0, 0],
					[0, -1, 0, 0]
				];

				const resultSymmetric = HypersphereEquidistantPoint.findEquidistantPoint(symmetricPoints);
				console.log('Точка на сфере:', resultSymmetric);
				console.log('Норма:', Math.sqrt(resultSymmetric.reduce((sum, val) => sum + val * val, 0)));

				// Проверка расстояний
				const distances = symmetricPoints.map(point =>
					HypersphereEquidistantPoint.calculateDistance(resultSymmetric, point)
				);
				console.log('Расстояния до каждой точки:', distances);

				return {
					'3D_example': result3D,
					'4D_example': result4D,
					'symmetric_example': resultSymmetric
				};

			} catch (error) {
				console.error('Ошибка:', error.message);
			}
		}

		// Запуск примера
		example();
		*/

/*		
		// Экспорт класса для использования в других модулях
		if (typeof module !== 'undefined' && module.exports) {
			module.exports = HypersphereEquidistantPoint;
		}
*/		
		return HypersphereEquidistantPoint.findEquidistantPoint(points, boCloud, boCreateHypersphere);

	}
	ZeroArray() { return [0, 0, 0]; }
	Vertice(angles) { return utils.angles(angles); }
	/**
	 * Converts a vertice position to vertice angles.
	 * @param {array} vertice array of the vertice axes
	 * @returns Vertice angles.
	 */
	vertice2angles(vertice) {

		/*https://chat.deepseek.com/a/chat/s/5c55507d-4660-4e04-8920-0e1c791a9c3c
		Задана точка vertice на 3D гиперсфере в декартовой системе координат. Начало координат находится в центре сферы.
		Положение точки обозначить как
		x = vertice[0]
		y = vertice[1]
		z = vertice[2]
		w = vertice[3]
		Вычислить координаты точки в полярной системе координат.
		Углы в полярной системе координат обозначить как:
		longitude - долгота (азимутальный угол)  в диапазоне от -π до π,
		latitude - широта (зенитный угол) в диапазоне -π/2 на южном полюсе до π/2 на северном полюсе.,
		altitude - полярный угол от оси W  в диапазоне от 0 до π.
		Радиус сферы обозначить как r.
		Написать код на javascript.
		*/
		function cartesianToPolar4D(vertice) {

//			const [x, y, z, w] = vertice;
			const x = vertice.x, y = vertice.y, z = vertice.z, w = vertice.w;

			// Вычисляем радиус сферы
			const r = Math.sqrt(x * x + y * y + z * z + w * w);

			// Если радиус равен 0, возвращаем нулевые координаты
			if (r === 0) {

/*
				return {
					longitude: 0,
					latitude: 0,
					altitude: 0,
					radius: 0
				};
*/
				return utils.angles([0, 0, 0]);

			}

			// Вычисляем altitude (угол от оси W) в диапазоне [0, π]
			const altitude = Math.acos(w / r);

			// Проекция на 3D пространство (x,y,z)
			const r_xyz = Math.sqrt(x * x + y * y + z * z);

			// Вычисляем latitude (широта) в диапазоне [-π/2, π/2]
			let latitude;
			if (r_xyz === 0) {
				// Если проекция на (x,y,z) равна 0, точка на полюсе W-оси
				latitude = (w >= 0) ? Math.PI / 2 : -Math.PI / 2;
			} else {
				latitude = Math.asin(z / r_xyz);
			}

			// Проекция на плоскость (x,y)
			const r_xy = Math.sqrt(x * x + y * y);

			// Вычисляем longitude (долгота) в диапазоне [-π, π]
			let longitude;
			if (r_xy === 0) {
				// Если проекция на (x,y) равна 0, долгота не определена, устанавливаем 0
				longitude = 0;
			} else {
				longitude = Math.atan2(y, x);
				// atan2 уже возвращает значение в диапазоне [-π, π]
			}

/*
			return {
				longitude: longitude,    // долгота [-π, π]
				latitude: latitude,      // широта [-π/2, π/2]  
				altitude: altitude,      // полярный угол от оси W [0, π]
				radius: r               // радиус сферы
			};
*/
			return utils.angles([altitude, latitude, longitude]);

		}
/*
		// Пример использования:
		const vertice2 = [1, 0, 0, 0];  // Точка на гиперсфере
		const polarCoords = cartesianToPolar4D(vertice2);

		console.log("Декартовы координаты:", vertice2);
		console.log("Полярные координаты:");
		console.log("Долгота (longitude):", polarCoords.longitude);
		console.log("Широта (latitude):", polarCoords.latitude);
		console.log("Полярный угол (altitude):", polarCoords.altitude);
		console.log("Радиус (radius):", polarCoords.radius);
*/		
		return cartesianToPolar4D(Position(vertice));

	}
	a2v(angles, r) {

		/*https://chat.deepseek.com/a/chat/s/831d9a90-2396-4b09-b548-288415124814
		Задана точка на 3D гиперсфере в полярной системе координат. Начало координат находится в центре 3D гиперсферы.
		Положение точки обозначить как
		angles.longitude - долгота (азимутальный угол)  в диапазоне от -π до π,
		angles.latitude - широта (зенитный угол) в диапазоне -π/2 на южном полюсе до π/2 на северном полюсе.,
		angles.altitude - полярный угол от оси W  в диапазоне от 0 до π.
		Радиус сферы обозначить как r.
		Долгота, широта и полярный угол могут выходить за пределы заданного диапазона.
		Вычислить координаты точки в декартовой системе координат.
		Написать код на javascript
		*/
		/**
		 * Вычисляет декартовы координаты (x, y, z) точки на сфере,
		 * заданной в полярной системе координат (r, широта, долгота).
		 *
		 * Предполагается, что:
		 * - Широта (latitude) находится в диапазоне от -π/2 (южный полюс) до π/2 (северный полюс).
		 * - Долгота (longitude) находится в диапазоне от -π до π.
		 * - Углы заданы в радианах.
		 *
		 * @param {number} r - Радиус сферы.
		 * @param {object} angles - Объект с углами.
		 * @param {number} angles.latitude - Широта (от -π/2 до π/2).
		 * @param {number} angles.longitude - Долгота (от -π до π).
		 * @returns {array} Массив с декартовыми координатами [x, y, z].
		 */
/*		
		function polarToCartesian(angles, r) {
			// Нормализуем углы к стандартным диапазонам
			const normalizedLon = normalizeAngle(angles.longitude, anglesRange.longitude.min, anglesRange.longitude.max);
			const normalizedLat = normalizeAngle(angles.latitude,  anglesRange.latitude.min, anglesRange.latitude.max);
			const normalizedAlt = normalizeAngle(angles.altitude,  anglesRange.altitude.min,  anglesRange.altitude.max);

			// Вычисляем декартовы координаты
			const x = r * Math.sin(normalizedAlt) * Math.cos(normalizedLat) * Math.cos(normalizedLon);
			const y = r * Math.sin(normalizedAlt) * Math.cos(normalizedLat) * Math.sin(normalizedLon);
			const z = r * Math.sin(normalizedAlt) * Math.sin(normalizedLat);
			const w = r * Math.cos(normalizedAlt);

//			return { x, y, z, w };
			return [ x, y, z, w ];
		}

		function normalizeAngle(angle, min, max) {
			const range = max - min;

			// Приводим угол к диапазону [min, max)
			let normalized = angle;
			while (normalized < min) {
				normalized += range;
			}
			while (normalized >= max) {
				normalized -= range;
			}

			return normalized;
		}
*/
		// Альтернативная версия с более простой нормализацией
		function polarToCartesian(angles, r) {

/*			
			// Используем остаток от деления для нормализации
			const lon = ((angles.longitude + Math.PI) % (2 * Math.PI)) - Math.PI;
			const lat = ((angles.latitude + Math.PI / 2) % Math.PI) - Math.PI / 2;
			const alt = angles.altitude % Math.PI;
*/			
			const lon = angles.longitude;
			const lat = angles.latitude;
			const alt = angles.altitude;

			// Вычисляем декартовы координаты
			const x = r * Math.sin(alt) * Math.cos(lat) * Math.cos(lon);
			const y = r * Math.sin(alt) * Math.cos(lat) * Math.sin(lon);
			const z = r * Math.sin(alt) * Math.sin(lat);
			const w = r * Math.cos(alt);

//			return { x, y, z, w };
			return [ x, y, z, w ];
		}
/*		
		// Пример использования
		const angles2 = {
			longitude: Math.PI / 4,    // 45°
			//latitude: Math.PI / 6,     // 30°
			latitude: Math.PI / 2 + 0.1,//out of range
			altitude: Math.PI / 3      // 60°
		};
		const radius = 1;

		const coords2 = Position(polarToCartesian(angles2, radius));
		console.log('Декартовы координаты:', coords2);
		const angles3 = this.vertice2angles(coords2);
		const coords3 = Position(polarToCartesian(angles2, radius));

		// Проверка: расстояние от начала координат должно быть равно радиусу
		const distance = Math.sqrt(coords.x ** 2 + coords.y ** 2 + coords.z ** 2 + coords.w ** 2);
		console.log('Расстояние от центра:', distance); // Должно быть равно radius
*/		
		return polarToCartesian(angles, r);

	}
	get verticeEdgesLengthMax() { return 4/*6*/; }//нельзя добавлть новое ребро если у вершины уже 6 ребра
	get dimension() { return 4; }//space dimension
	get verticesCountMin() { return 4; }
/*
	getRandomMiddleAngles(oppositeVertices) {
		
		//https://chat.deepseek.com/a/chat/s/85a1d029-0033-437b-a750-c58f9590bd4c

//		Дана сфера. На поверхности сферы заданы три точки в декартовой системе координат. Начало координат находится в центре сферы.
//Построить плоскость, проходящую через заданные три точки.
//Построить нормаль к этой плоскости такую, что бы она проходила через центр сферы.
//Вычислить координаты двух точек, в которых норамль пересекается с данной сферой.

//		Сделать подобные вычисления для гиперсферы в 4 - мерном пространстве(n = 4).Теперь уже заданы не три, а черыте точки на гиперсфере.Написать код на javascript.

		// Функция вычисления определителя 3x3
		// Функция вычисления определителя 3x3
		function det3x3(m) {
			return m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
				- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
				+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
		}

		// Скалярное произведение в 4D
		function dot4d(a, b) {
			return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
		}

		// Вычитание векторов в 4D
		function sub4d(a, b) {
			return [a[0] - b[0], a[1] - b[1], a[2] - b[2], a[3] - b[3]];
		}

		// Норма вектора в 4D
		function norm4d(v) {
			return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2] + v[3] * v[3]);
		}

		// Нормализация вектора в 4D
		function normalize4d(v) {
			const n = norm4d(v);
			if (n < 1e-12) return v;
			return [v[0] / n, v[1] / n, v[2] / n, v[3] / n];
		}

		// Проекция вектора u на вектор v
		function project4d(u, v) {
			const scale = dot4d(u, v) / dot4d(v, v);
			return [v[0] * scale, v[1] * scale, v[2] * scale, v[3] * scale];
		}

		// Ортогонализация Грама-Шмидта для набора векторов
		function gramSchmidt4d(vectors) {
			const basis = [];

			for (let i = 0; i < vectors.length; i++) {
				let v = vectors[i].slice();

				// Вычитаем проекции на все предыдущие базисные векторы
//				for (let j = 0; j < i; j++)
				for (let j = 0; j < basis.length; j++)
				{
					const proj = project4d(v, basis[j]);
					v = sub4d(v, proj);
				}

				// Если вектор не нулевой, добавляем в базис
				if (norm4d(v) > 1e-10) {
					basis.push(normalize4d(v));
				}
			}

			return basis;
		}

		// Генерация случайного вектора в 4D
		function randomVector4d() {
			// Генерируем случайные числа с нормальным распределением
			// для равномерного распределения на сфере
			let v = [
				Math.random() - 0.5,
				Math.random() - 0.5,
				Math.random() - 0.5,
				Math.random() - 0.5
			];

			// Немного вариативности
			const n = norm4d(v);
			if (n > 1e-12) {
				v = [v[0] / n, v[1] / n, v[2] / n, v[3] / n];
			}

			return v;
		}

		// Находит случайную нормаль, ортогональную заданному подпространству
		function findRandomNormal(subspaceBasis) {
			// Начинаем со случайного вектора
			let normal = randomVector4d();

			// Делаем его ортогональным ко всем векторам базиса подпространства
			for (const basisVec of subspaceBasis) {
				const proj = project4d(normal, basisVec);
				normal = sub4d(normal, proj);
			}

			// Нормализуем
			const n = norm4d(normal);
			if (n < 1e-12) {
				// Случай, когда случайный вектор лежит в подпространстве
				// Попробуем другой подход: найдем любой вектор, не входящий в span
				for (let attempt = 0; attempt < 10; attempt++) {
					normal = randomVector4d();
					let isOrthogonal = true;
					for (const basisVec of subspaceBasis) {
						if (Math.abs(dot4d(normal, basisVec)) > 0.1) {
							isOrthogonal = false;
							break;
						}
					}
					if (isOrthogonal && norm4d(normal) > 1e-10) {
						return normalize4d(normal);
					}
				}

				// Если не получилось, возьмем стандартный базисный вектор
				// и сделаем его ортогональным
				normal = [1, 0, 0, 0];
				for (const basisVec of subspaceBasis) {
					const proj = project4d(normal, basisVec);
					normal = sub4d(normal, proj);
				}
			}

			return normalize4d(normal);
		}

		// Основная функция
		function findIntersectionPoints4D(p1, p2, p3, p4) {
			// Проверка размерности
			if (p1.length !== 4 || p2.length !== 4 || p3.length !== 4 || p4.length !== 4) {
				console.error(sHyperSphere3D + ": findIntersectionPoints4D. Все точки должны быть 4-мерными [x,y,z,w]");
				return;
			}

			// Вычисляем векторы из p1 к другим точкам
			const v1 = sub4d(p2, p1);
			const v2 = sub4d(p3, p1);
			const v3 = sub4d(p4, p1);

			// Вычисляем компоненты нормали как миноры 3x3
			const a = det3x3([
				[v1[1], v1[2], v1[3]],
				[v2[1], v2[2], v2[3]],
				[v3[1], v3[2], v3[3]]
			]);

			const b = -det3x3([
				[v1[0], v1[2], v1[3]],
				[v2[0], v2[2], v2[3]],
				[v3[0], v3[2], v3[3]]
			]);

			const c = det3x3([
				[v1[0], v1[1], v1[3]],
				[v2[0], v2[1], v2[3]],
				[v3[0], v3[1], v3[3]]
			]);

			const d = -det3x3([
				[v1[0], v1[1], v1[2]],
				[v2[0], v2[1], v2[2]],
				[v3[0], v3[1], v3[2]]
			]);

			// Проверка на вырожденность
			const normSq = a * a + b * b + c * c + d * d;
			let normal;
			let isDegenerate = false;

			if (normSq < 1e-12) {

				//Вырожденный случай: точки лежат в подпространстве меньшей размерности
				isDegenerate = true;
				//console.log("Вырожденный случай: точки лежат в подпространстве меньшей размерности");
				//console.log("Будет выбрана случайная нормаль");

				// Находим базис подпространства, содержащего точки
				const vectors = [v1, v2, v3];
				const basis = gramSchmidt4d(vectors);

				//console.log(`Размерность подпространства: ${basis.length}`);

				// Выбираем случайную нормаль, ортогональную этому подпространству
				normal = findRandomNormal(basis);

				// Проверяем, что нормаль действительно ортогональна
				//console.log("Проверка ортогональности:");
				//for (let i = 0; i < basis.length; i++) {
				//	const dot = dot4d(normal, basis[i]);
				//	console.log(`  Скалярное произведение с базисом ${i}: ${dot.toFixed(10)}`);
				//}

			} else {
				normal = [a, b, c, d];
				const N = Math.sqrt(normSq);
				normal = [a / N, b / N, c / N, d / N];
			}

			// Радиус гиперсферы (расстояние от центра до любой точки)
			const R = Math.sqrt(p1[0] * p1[0] + p1[1] * p1[1] + p1[2] * p1[2] + p1[3] * p1[3]);

			//случайным образом из двух точек пересечения нормали с гиперсферой выбираем одну
			const scale = Math.random() > 0.5 ? R : -R;
			return [
				normal[0] * scale,
				normal[1] * scale,
				normal[2] * scale,
				normal[3] * scale
			];

			//// Вычисляем две точки пересечения
			//const scale = R; // normal уже нормализован
			//const point1 = [
			//	normal[0] * scale,
			//	normal[1] * scale,
			//	normal[2] * scale,
			//	normal[3] * scale
			//];

			//const point2 = [
			//	normal[0] * -scale,
			//	normal[1] * -scale,
			//	normal[2] * -scale,
			//	normal[3] * -scale
			//];

			//return {
			//	normal: normal,
			//	radius: R,
			//	isDegenerate: isDegenerate,
			//	intersectionPoints: [point1, point2]
			//};

		}

		// Тестовые функции
		//function testNonDegenerate() {
		//	console.log("=== Тест 1: Невырожденный случай ===");
		//	const p1 = [1, 0, 0, 0];
		//	const p2 = [0, 1, 0, 0];
		//	const p3 = [0, 0, 1, 0];
		//	const p4 = [0, 0, 0, 1];

		//	const result = findIntersectionPoints4D(p1, p2, p3, p4);
		//	console.log("Нормаль:", result.normal.map(x => x.toFixed(4)));
		//	console.log("Точки пересечения:");
		//	console.log("  P+:", result.intersectionPoints[0].map(x => x.toFixed(4)));
		//	console.log("  P-:", result.intersectionPoints[1].map(x => x.toFixed(4)));
		//	console.log("Радиус:", result.radius.toFixed(4));
		//	console.log("Вырожденный?:", result.isDegenerate);
		//	console.log();
		//}

		//function testDegenerate1() {
		//	console.log("=== Тест 2: Вырожденный случай (3D подпространство) ===");
		//	// Все точки лежат в гиперплоскости w=0 (3D пространство)
		//	const p1 = [1, 0, 0, 0];
		//	const p2 = [0, 1, 0, 0];
		//	const p3 = [0, 0, 1, 0];
		//	const p4 = [0.5, 0.5, 0, 0]; // Тоже в w=0

		//	const result = findIntersectionPoints4D(p1, p2, p3, p4);
		//	console.log("Нормаль:", result.normal.map(x => x.toFixed(4)));
		//	console.log("Точки пересечения:");
		//	console.log("  P+:", result.intersectionPoints[0].map(x => x.toFixed(4)));
		//	console.log("  P-:", result.intersectionPoints[1].map(x => x.toFixed(4)));
		//	console.log("Радиус:", result.radius.toFixed(4));
		//	console.log("Вырожденный?:", result.isDegenerate);
		//	console.log();
		//}

		//function testDegenerate2() {
		//	console.log("=== Тест 3: Вырожденный случай (2D плоскость) ===");
		//	// Все точки лежат в плоскости z=0, w=0 (2D пространство)
		//	const p1 = [1, 0, 0, 0];
		//	const p2 = [0, 1, 0, 0];
		//	const p3 = [0.5, 0.5, 0, 0];
		//	const p4 = [-0.5, 0.5, 0, 0];

		//	const result = findIntersectionPoints4D(p1, p2, p3, p4);
		//	console.log("Нормаль:", result.normal.map(x => x.toFixed(4)));
		//	console.log("Точки пересечения:");
		//	console.log("  P+:", result.intersectionPoints[0].map(x => x.toFixed(4)));
		//	console.log("  P-:", result.intersectionPoints[1].map(x => x.toFixed(4)));
		//	console.log("Радиус:", result.radius.toFixed(4));
		//	console.log("Вырожденный?:", result.isDegenerate);
		//	console.log();
		//}

		//function testDegenerate3() {
		//	console.log("=== Тест 4: Вырожденный случай (1D линия) ===");
		//	// Все точки лежат на одной линии
		//	const p1 = [1, 0, 0, 0];
		//	const p2 = [2, 0, 0, 0];
		//	const p3 = [3, 0, 0, 0];
		//	const p4 = [4, 0, 0, 0];

		//	try {
		//		const result = findIntersectionPoints4D(p1, p2, p3, p4);
		//		console.log("Нормаль:", result.normal.map(x => x.toFixed(4)));
		//		console.log("Точки пересечения:");
		//		console.log("  P+:", result.intersectionPoints[0].map(x => x.toFixed(4)));
		//		console.log("  P-:", result.intersectionPoints[1].map(x => x.toFixed(4)));
		//		console.log("Радиус:", result.radius.toFixed(4));
		//		console.log("Вырожденный?:", result.isDegenerate);
		//	} catch (error) {
		//		console.log("Ошибка:", error.message);
		//	}
		//	console.log();
		//}

		//// Проверка, что точки действительно лежат на гиперсфере
		//function verifyPoints(result) {
		//	console.log("Проверка расстояний от центра:");
		//	for (let i = 0; i < 2; i++) {
		//		const p = result.intersectionPoints[i];
		//		const dist = Math.sqrt(p[0] * p[0] + p[1] * p[1] + p[2] * p[2] + p[3] * p[3]);
		//		console.log(`  Точка ${i + 1}: расстояние = ${dist.toFixed(6)}, радиус = ${result.radius.toFixed(6)}`);
		//	}
		//}

		//// Запуск тестов
		//console.log("=== Тестирование алгоритма для 4D гиперсферы ===\n");
		//testNonDegenerate();
		//testDegenerate1();
		//testDegenerate2();
		//testDegenerate3();		// Вычисление точек пересечения нормали со сферой

		const point = findIntersectionPoints4D(
			[
				oppositeVertices[0][0],
				oppositeVertices[0][1],
				oppositeVertices[0][2],
				oppositeVertices[0][3],
			],
			[
				oppositeVertices[1][0],
				oppositeVertices[1][1],
				oppositeVertices[1][2],
				oppositeVertices[1][3],
			],
			[
				oppositeVertices[2][0],
				oppositeVertices[2][1],
				oppositeVertices[2][2],
				oppositeVertices[2][3],
			],
			[
				oppositeVertices[3][0],
				oppositeVertices[3][1],
				oppositeVertices[3][2],
				oppositeVertices[3][3],
			],
		);
		return this.vertice2angles(Position([point[0], point[1], point[2], point[3]]));
		
	}
*/
	/**
	 * @param {THREE.Scene} scene [THREE.Scene]{@link https://threejs.org/docs/index.html?q=sce#api/en/scenes/Scene}
	 * @param {Options} options See <a href="../../jsdoc/Options/Options.html" target="_blank">Options</a>.
	 * @param {object} randomVerticesSettings See <b>randomVerticesSettings</b> of the <a href="./module-HyperSphere-RandomVertices.html" target="_blank">RandomVertices</a> class.
	 * @returns new RandomVertices child class.
	 */
	newRandomVertices(scene, options, randomVerticesSettings) { return new RandomVertices(scene, options, randomVerticesSettings); }
	get RandomVertice() { return RandomVertice; }
//	get RandomCloud() { return RandomCloud; }

}

const sRandomVertices = 'RandomVertices'
class RandomVertices extends Sphere.RandomVertices {

	constructor(scene, options, randomVerticesSettings) {

		if (randomVerticesSettings.np === undefined) randomVerticesSettings.np = 6;//Количество окружностей в сфере, которые создаются из случайных точек для двумерной гиперсферы
//		randomVerticesSettings.spheresCount = 1;//7;//облако случайных точек делаю из 1 + spheresCount * 2 сфер, которые создаются из случайных точек для двумерной гиперсферы
		if (randomVerticesSettings.debug && (typeof randomVerticesSettings.debug === 'object') && randomVerticesSettings.debug.oneCircles && (randomVerticesSettings.debug.oneCircles.altitudeShift === undefined)) randomVerticesSettings.debug.oneCircles.altitudeShift = 0;
		super(scene, options, randomVerticesSettings);
		this.class = HyperSphere3D;
		
	}

	//overridden methods

	updateCirclesRadiusRadians(changeCirclesPoints, params){
		
		this.aCirclesRadiusRadians.length = 0;
		this.aCirclesRadiusRadians.boUpdate = true;
		changeCirclesPoints(params);
		this.aCirclesRadiusRadians.boUpdate = undefined;
		
	}
	getHyperSphere(options, classSettings) {

		let circlesSphere;
		circlesSphere = new HyperSphere3D(options, classSettings);
		return circlesSphere;

	}
	getArcAngle(vertice, oppositeVertice) {

		//DeepSeek. вычислить угол между двумя точками на поверхности 3D гиперсферы. Координаты точек указаны в радианах
		//векторы
		//A=(ψ1,θ1,ϕ1) - vertice
		const ψ1 = vertice[0], θ1 = vertice[1], ϕ1 = vertice[2];
		//B=(ψ1,θ1,ϕ1) - oppositeVertice
		const ψ2 = oppositeVertice[0], θ2 = oppositeVertice[1], ϕ2 = oppositeVertice[2];
		//где
		//ψ - высота
		//θ — широта (от −90° до 90°),
		//ϕ — долгота (от −180° до 180°),
		const arccos = Math.acos, sin = Math.sin, cos = Math.cos;
		const alpha = cos(ψ1) * cos(ψ2) + sin(ψ1) * sin(ψ2) * (cos(θ1) * cos(θ2) + sin(θ1) * sin(θ2) * cos(ϕ1 - ϕ2));
		let θ = arccos(alpha);
		if (isNaN(θ)) {

			if (alpha - 1 < 3e-16) θ = 0;//alpha не может быть больше 1, но иногда alpha вычисляется с небольшой погрешностью 1.0000000000000002, которая нарушает это правило
			else console.error(sRandomVertices + ': getArcAngle. Invalid θ = ' + θ);

		}
		return θ;

	}
	oppositeVertice0(params, inaccurateLatitude) {

		const oppositeVertice = params.oppositeVertice;
		if (oppositeVertice.hasOwnProperty('0')) return;
		let altitude = oppositeVertice.altitude;
		Object.defineProperty(oppositeVertice, '0', {

			get: () => { return altitude; },
			set: (altitudeNew) => {

				altitude = altitudeNew;
				return true;

			},

		});

	}
	//	antipodeCenter(params, antipodeLatitude) { return [antipodeLatitude(params.oppositeVertice.latitude), params.oppositeVertice.longitude - π]; }
	zeroArray() { return utils.angles([0, 0, 0]); }
	onePointArea(d, np) {

		//объем шара в котором в среднем будет находиться одна случайная точка.
		//Площадь сферы вычисляем из площади боковой поверхности цилиндра, поделенной на количество точек на окружности np
		//Цилиндр расположен на экваторе сферы так, чтобы его середина касалась экватора
		//Высота цилиндра в радианах равна d.
		const h = 2 * Math.tan(d / 2),//Высота цилиндра радиусом 1. См. https://en.wikipedia.org/wiki/Trigonometric_functions
			S = 2 * π * h;//Площадь боковой поверхности цилиндра радиусом 1
		return S / np;//Площадь сферы на которой в среднем будет находиться одна случайная точка.

	}
	numPoints(d, s, circleId, x) {

		//Для вычисления количества случайных точек numPoints около окружности, расположенной на расстоянии circleDistance радиан
		//я вычисляю площадь шарового пояса между параллелями S и делю ее на s площадь сферы на которой в среднем будет находиться одна случайная точка.
		const cos = Math.cos,
			h1 = cos(x),//расстояние от текущей окружности до центра шара
			hprev = cos((circleId - 1) * d),//расстояние от предыдущей окружности до центра шара
			h = h1 - hprev,//высота шарового пояса
			S = Math.abs(2 * π * h);//DeepSeek. площадь шарового пояса между параллелями
		return Math.round(S / s);//количество случайных точек около окружности, расположенной на расстоянии circleDistance радиан

	}
/*
	center(params) {

		//center is antipode of the opposite vertice
		//Центр окружностей случайных точек center находится с противоположной от params.oppositeVertice стороны гиперсферы
		const antipodeLatitude = (latitude) => { return -latitude; },
			//			center = params.randomVertices.antipodeCenter(params, antipodeLatitude);
			center = [params.oppositeVertice.altitude, antipodeLatitude(params.oppositeVertice.latitude), params.oppositeVertice.longitude - π];

		Object.defineProperty(center, 'altitude', { get: () => { return center[0]; }, });
		Object.defineProperty(center, 'lat', {

			get: () => { return center[1]; },
			set: (lat) => {

				params.oppositeVertice.latitude = antipodeLatitude(lat);
				return true;

			},

		});
		Object.defineProperty(center, 'lng', { get: () => { return center[2]; }, });
		return center;

	}
*/	
	getCirclePoint(circleDistance, params, options) {

		const point2D = this.getCirclePoint2D(circleDistance, params, options);
		return utils.angles([options.altitude, point2D[0], point2D[1]]);

	}
	circlesCount(np) { return this.boCreateCirclesPoints ?
		
		np - //если количество окружностей равно количеству точек на окружности, то точки будут равномерно располагаться на гиперсфере
		(this.circlesCountDelta != undefined ?
			this.circlesCountDelta ://количество окружностей  нужно уменьшить если окружности находятся внутри или снаружи противоположной вершины.
			0
		):
		
		this.aSpheres[this.circlesId].length + 1; }
	getNumPoints(circleDistance, R, dCircleDistance) {

		return parseInt(
			2 * π * Math.sin(circleDistance / R)//длинна окружности для гиперсферы радиусом 1
			/ dCircleDistance
		);

	}
//	pointIdErase(pointId) { return pointId === undefined ? 0 : pointId; }
	
	//Облако случайных точек это массив массивов случайноых точек сфер.
	//Создаем массив окружностей на сфере, на которой находится противоположная точка.
	//Во время ее создания запоминаем в aCirclesRadiusRadians радиусы всех окружностей в радианах.
	//Потом создаем массивы окружностей сфер, расположенных внутри и снаружи от сферы, на которой находится противоположная точка.
	//Каждый элемент массива окружностей сфер находится внутри или снаружи на расстоянии, которое было занесено в aCirclesRadiusRadians.
	aCirclesRadiusRadians = [];
	
	setCirclesCloud(randomVerticesSettings, params) {

		const aCirclesRadiusRadians = this.aCirclesRadiusRadians;
		randomVerticesSettings.spheresCount = 0
		
		//рисуем окружности вокруг противопрложной точки
		this.aCirclesRadiusRadians.boUpdate ||= this.boCreateCirclesPoints;
		this.setCircles(0, randomVerticesSettings.spheresCount, params.center.altitude);
		this.aCirclesRadiusRadians.boUpdate = undefined;
		if (this.boCreateCirclesPoints) {

			//Выделяем место для точек в this.circlesPoints
			for(let k = 1; k < this.aSpheres[0].length; k++) this.setCircles(0, k, params.center.altitude);
			
		} else {

			//рисуем окружности внутри и снаружи от противоположной точки
			const altitude = params.center.altitude;

			if (!randomVerticesSettings.debug || !randomVerticesSettings.debug.oneCircles)//Во время отладки не рисуем окружности внутри и снаружи от противоположной точки чтобы лучше можно было разглядеть окружности возле противоположной  вершины
				for (let sphereId = 1; sphereId < this.aSpheres.length; sphereId++) {
	
					randomVerticesSettings.spheresCount = sphereId;//Возможно эта строка не нужна
	
					
					const x = this.aSpheres.length - sphereId - 1;
					
					const circlesRadius = aCirclesRadiusRadians[x];// + y;
					if (circlesRadius === 0) {
	
						console.error(sRandomVertices + '.setCirclesCloud: circlesRadius === 0');
						continue;
	
					}
/*					
					const getAltitudeShiftMax = (inCircles) => {

						return this.aCirclesRadiusRadians[sphereId - 1] * inCircles;
						
					}
					circlesPointsOptions.altitudeShiftMaxIn = getAltitudeShiftMax(1);
					circlesPointsOptions.altitudeShiftMaxOut = getAltitudeShiftMax(-1);
*/					
//					circlesPointsOptions.altitudeShiftMax = this.aCirclesRadiusRadians[sphereId - 1];
//					this.aSpheres[sphereId - 1].altitudeShiftMax = this.aCirclesRadiusRadians[sphereId - 1];
					this.setCircles(undefined, sphereId, altitude - circlesRadius, sphereId);//рисуем окружности внутри от противоположной точки
					this.setCircles(undefined, sphereId, altitude + circlesRadius, sphereId);//рисуем окружности снаружи от противоположной точки
					
				}

		}
		this.createCirclesSphere();
	
	}
	setCirclesCloudOnePoint(randomVerticesSettings) { for (let sphereId = 0; sphereId < randomVerticesSettings.spheresCount; sphereId++) this.setCirclesOnePoint(sphereId); }
	pushCirclesRadiusRadians(options, i, abc, R, getCirclePoint, numPoints, params) {

		const aCirclesRadiusRadians = this.aCirclesRadiusRadians;
		if (aCirclesRadiusRadians && (
				(
					aCirclesRadiusRadians.boUpdate &&//изменяется дуга между вершинами. Например когда пользователь изменил коодинату вершины 
					(options.circlesPointsCount === 0)//рисуем окружности вокруг противопрложной точки
				)||
//				this.boCreateCirclesPoints
				(this.boCreateCirclesPoints && aCirclesRadiusRadians.boUpdate)
			) && (i === 0)) {

			const boCreateCirclesPoints = this.boCreateCirclesPoints;
			this.boCreateCirclesPoints = false;
			const b = abc.b, circleDistance = (b === 0 ? 0 ://дуга между вершинами гиперсферы равна нулю. Значит радиус окружности вокруг вершины тоже равен нулю
				abc.a / (aCirclesRadiusRadians.x + b) + abc.c) * R;
			this.boCreateCirclesPoints = boCreateCirclesPoints;
			aCirclesRadiusRadians.push(this.getArcAngle(getCirclePoint({ i: i, numPoints: numPoints, circleDistance: circleDistance, altitude: options.altitude }), params.oppositeVertice)); //Запомнить расстояние между нулевой точкой каждой окружности и противоположной вершиной, равное радиусу окружности в радианах.

		}
	}
	circlesRadiusRadiansSetX(x) { this.aCirclesRadiusRadians.x = x; }
	getAltitudeShift(debug, circleIdMax, options, params) {

		const circleId = options.circleId;
//		if (!debug || !debug.oneCircles || (debug.oneCircles.altitudeShift === undefined)) return 0;
		if (debug && debug.oneCircles && !debug.oneCircles.altitudeShiftIsDefined){

			let altitudeShift = debug.oneCircles.altitudeShift;
			Object.defineProperty(debug.oneCircles, 'altitudeShift', {
				
				get: () => { return altitudeShift; },
				set: (altitudeShiftNew) => {
		
					if (altitudeShift === altitudeShiftNew) return true;
					altitudeShift = altitudeShiftNew;
					if (params.randomVertices) params.randomVertices.changeCirclesPoints();
					return true;
		
				},
			
			});
			debug.oneCircles.altitudeShiftIsDefined = true;
			
		}
		if (circleIdMax === 1) return 0;//В текушей сфере всего одна окружность. Ее не нужно сдвигать.
/*		
		const altitudeShiftMax = debug.oneCircles && debug.oneCircles.altitudeShift ? debug.oneCircles.altitudeShift : options.sphereId != undefined ?
				this.aCirclesRadiusRadians[options.sphereId - 1] * 1 * (
					options.altitude - params.center.altitude < 0 ?
						1 ://рисуем окружности внутри от противоположной точки
						-1//рисуем окружности снаружи от противоположной точки
				) :
				0,
*/				
//		const altitudeShiftMax = ((options.sphereId === undefined) ? 0 : this.aSpheres[options.sphereId - 1].altitudeShiftMax * 
		const altitudeShiftMax = ((options.sphereId === undefined) ? 0 : this.aSpheres[options.sphereId].altitudeShiftMax * 
									(options.altitude - params.center.altitude < 0 ?
										-1 ://рисуем окружности внутри от противоположной точки
										1//рисуем окружности снаружи от противоположной точки
								 )) || 0,
			a = altitudeShiftMax / (1 - 1 / (circleIdMax * circleIdMax * circleIdMax)), b = altitudeShiftMax - a;
		let altitudeShift = a / (circleId * circleId * circleId) + b;
//altitudeShift = 0;
		if (options.sphereId != undefined) {
			
			const aSphere = this.aSpheres[options.sphereId];
			if (aSphere.altitudeShiftMax === undefined) {

/*				
				aSphere.altitudeShift = altitudeShift;
				Object.defineProperty(aSphere, 'altitudeShift', {
			
					get: () => { return altitudeShift; },
					set: (altitudeShiftNew) => {
			
						if (altitudeShift === altitudeShiftNew) return true;
						altitudeShift = altitudeShiftNew;
						params.randomVertices.changeCirclesPoints();
						return true;
			
					},
			
				});
*/				
				let altitudeShiftMax;
//				aSphere.altitudeShiftMax = altitudeShiftMax;
				Object.defineProperty(aSphere, 'altitudeShiftMax', {
			
					get: () => {

						if (altitudeShiftMax === undefined) altitudeShiftMax = 0;//this.aCirclesRadiusRadians[options.sphereId - 1]
						return altitudeShiftMax;
					
					},
					set: (altitudeShiftMaxNew) => {

//						const boAltitudeShiftMaxIsUndefined = altitudeShiftMax === undefined;
						if (altitudeShiftMax === altitudeShiftMaxNew) return true;
						altitudeShiftMax = altitudeShiftMaxNew;
//						if (!boAltitudeShiftMaxIsUndefined)
						params.randomVertices.changeCirclesPoints();
						this.circlesSphere.setVerticesRange(this.verticesRange.start, this.verticesRange.count);
						return true;
			
					},
			
				});
//				aSphere.altitudeShiftIsDefined = true;}

			}

		}
		return altitudeShift;
	
	}
	verticesRange = {
		
		start: 0,
		count: 0,

	}
	gui(fParent, getLanguageCode = () => { return 'en' }, dat = { controllerNameAndTitle: () => {}}) {

		//Localization

		const lang = {

			notSelected: 'not selected',

			randomVertices: 'Random vertices',
			
			spheres: 'Spheres',
			spheresTitle: 'Spheres of the random vertices',

			altitudeShift: 'Altitude Shift Max',
			altitudeShiftTitle: 'Altitude Shift Max',
		
		};

		const _languageCode = getLanguageCode();

		switch (_languageCode) {

			case 'ru'://Russian language

				lang.notSelected = 'Не выбрана';
				
				lang.randomVertices = 'Случайные вершины';
				
				lang.spheres = 'Сферы';
				lang.spheresTitle = 'Сферы случайных вершин';

				lang.altitudeShiftMax = 'Поправка высоты';
				lang.altitudeShiftTitleMax = 'Поправка высоты';

				break;
			default://Custom language

		}

//		const sR = 'r', sL = 'l';
		let aSphere;
		const fRandomVertices = fParent.addFolder(lang.randomVertices),
			cSpheres = fRandomVertices.add({ Spheres: lang.notSelected }, 'Spheres', { [lang.notSelected]: -1 }).onChange((value) => {

				const sphereId = parseInt(value);
				let boDisplay = false;
				this.verticesRange.start = 0;
				this.verticesRange.count = 0;
				if (sphereId != -1) {

					boDisplay = true;
					this.aSpheres[sphereId].forEach((circle) => this.verticesRange.count += circle.numPoints);
					if (sphereId != 0) {
						
						this.verticesRange.count *= 2;
						this.aSpheres[0].forEach((circle) => this.verticesRange.start += circle.numPoints);
						for(let i = 1; i < sphereId; i++) {
	
							let sphereNumPoints = 0;
							this.aSpheres[i].forEach((circle) => sphereNumPoints += circle.numPoints);
							this.verticesRange.start += 2 * sphereNumPoints;
	
						}

					}
					aSphere = this.aSpheres[sphereId];
					if (aSphere.altitudeShiftMax != undefined) cAltitudeShiftMax.setValue(aSphere.altitudeShiftMax);
					else boDisplay = false;

				} else {

					for(let i = 0; i < this.aSpheres.length; i++) {
					
						this.aSpheres[i].forEach((circle) => this.verticesRange.count += circle.numPoints);
						if (i != 0) this.aSpheres[i].forEach((circle) => this.verticesRange.count += circle.numPoints);//Все ненулевые сферы (сферы, которые не проходят через противоположную вершину) повторяются два раза - внутри и снаружи противоположной вершины

					}

				}
				_display(cAltitudeShiftMax.domElement.parentNode.parentNode, boDisplay);
				this.circlesSphere.setVerticesRange(this.verticesRange.start, this.verticesRange.count);
				
			});
		cSpheres.__select[0].selected = true;
		dat.controllerNameAndTitle(cSpheres, lang.spheres, lang.spheresTitle);
		const cAltitudeShiftMax = fRandomVertices.add({altitudeShiftMax: 0}, 'altitudeShiftMax', 0, 0.1, 0.0001).onChange((altitudeShiftMax) => { aSphere.altitudeShiftMax = altitudeShiftMax; });
		dat.controllerNameAndTitle(cAltitudeShiftMax, lang.altitudeShiftMax, lang.altitudeShiftTitleMax);
		_display(cAltitudeShiftMax.domElement.parentNode.parentNode, false);
		
		this.cSpheresAppendChild = () => {

			if (this.aSpheres.length === 0) return;
			const appendItem = (innerHTML, value = innerHTML) => {
				
				const opt = document.createElement('option');
				opt.innerHTML = innerHTML;
				opt.setAttribute('value', value);
				cSpheres.__select.appendChild(opt);
				
			};
			let itemId = 0;
			appendItem(itemId++);
			for (; itemId < this.aSpheres.length; itemId++) { appendItem(itemId); }
					
		}
		
	}

	/////////////////////////////overridden methods

}
RandomVertices.ZeroArray = () => { return [0, 0, 0]; }
RandomVertices.getCenter = (params) => {

	//center is antipode of the opposite vertice
	//Центр окружностей случайных точек center находится с противоположной от params.oppositeVertice стороны гиперсферы
	const antipodeLatitude = (latitude) => { return -latitude; },
		//			center = params.randomVertices.antipodeCenter(params, antipodeLatitude);
		center = [params.oppositeVertice.altitude, antipodeLatitude(params.oppositeVertice.latitude), params.oppositeVertice.longitude - π];

	Object.defineProperty(center, 'altitude', { get: () => { return center[0]; }, });
	Object.defineProperty(center, 'lat', {

		get: () => { return center[1]; },
		set: (lat) => {

			params.oppositeVertice.latitude = antipodeLatitude(lat);
			return true;

		},

	});
	Object.defineProperty(center, 'lng', { get: () => { return center[2]; }, });
	return center;

}
RandomVertices.Center = (params, inaccurateLatitude) => {

	const Vertice = (vertice) => {
	
		if (vertice.longitude != undefined) return;
		Object.defineProperty(vertice, 'altitude', {
			
			get: () => { return vertice[0]; },
			set: (altitude) => {
	
				if (vertice[0] === altitude) return true;
				vertice[0] = altitude;
				if (params.randomVertices) {

					params.randomVertices.aCirclesRadiusRadians.length = 0;
					params.randomVertices.circlesPoints.length = 0;
					params.randomVertices.aSpheres.length = 0;
					params.randomVertices.setCirclesPoints();
//					params.randomVertices.changeCirclesPoints();

				}
				return true;
	
			},
		
		});
		Object.defineProperty(vertice, 'latitude', {
			
			get: () => { return vertice[1]; },
			set: (latitude) => {
	
				if (vertice[1] === latitude) return true;
				vertice[1] = latitude;
				if (params.randomVertices) params.randomVertices.changeCirclesPoints();
				return true;
	
			},
		
		});
		Object.defineProperty(vertice, 'longitude', {
			
			get: () => { return vertice[2]; },
			set: (longitude) => {
	
				if (vertice[2] === longitude) return true;
				vertice[2] = longitude;
				if (params.randomVertices) params.randomVertices.changeCirclesPoints();
				return true;
	
			},
		
		});
	
	}
	utils.angles(params.vertice);
	utils.angles(params.oppositeVertice);
	
	const center = params.center;
	if (center.length < 1) center.push(0);
	if (center.length < 2) center.push(0);
	if (center.length < 3) center.push(0);
	//	params.randomVertices.defineCenterCoordinates(params);
	if (center.altitude === undefined)
		Object.defineProperty(center, 'altitude', {

			get: () => { return center[0]; },
			set: (altitude) => {

				if (center[0] === altitude) return true;
				center[0] = altitude;
				if (params.randomVertices) params.randomVertices.changeCirclesPoints();
				return true;

			},

		});
	if (center.lat === undefined)
		Object.defineProperty(center, 'lat', {

			get: () => { return center[1]; },
			set: (lat) => {

				if (center[1] === lat) return true;
				center[1] = lat;
				if (params.randomVertices) params.randomVertices.changeCirclesPoints();
				return true;

			},

		});
	if (center.lng === undefined)
		Object.defineProperty(center, 'lng', {

			get: () => { return center[2]; },
			set: (lng) => {

				if (center[2] === lng) return true;
				center[2] = lng;

				if (params.randomVertices) params.randomVertices.changeCirclesPoints();
				return true;

			},

		});
//console.warn('inaccurateLatitude(center.lat) was removed')
	//center.lat = inaccurateLatitude(center.lat);

}
HyperSphere3D.RandomVertices = RandomVertices;

export default HyperSphere3D;

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