/**
* @module ND
* @description N-dimensional graphics
* @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
*
* @see [4D Geometry Viewer]{@link https://github.com/anhr/humke-4d-geometry}
* @see [Tesseract]{@link https://ciechanow.ski/tesseract/}
* @see [4D-Shapes]{@link https://artemonigiri.github.io/4D-Shapes/}
* @see [The Regular Polychora]{@link https://www.qfbox.info/4d/regular}
*/
import three from '../three.js'
import Options from '../Options.js'
import PositionController from '../PositionController.js';
import MyMath from '../myMath/myMath.js'
import MyObject from '../myObject.js'
/*
dimention geometry points edges faces bodyes 4D objects
1 line 2 0
2 triangle 3 3 1
3 tetrahedron 4 6 4 1
4 pentatope 5 10 10 5 1
*/
/**
* N-dimensional graphics. Extends <a href="../../jsdoc/MyObject/module-myObject-MyObject.html" target="_blank">MyObject</a>.
* @class
* @extends MyObject
*/
class ND extends MyObject {
/**
* N-dimensional graphics.
* Creates an N-dimensional graphic object,
* checks for a collision between an n-dimensional plane and an n-dimensional graphics object and returns the (n-1)-dimensional intersection geometry if a collision was occurs.
* @param {number} n space dimension of the graphical object.
* @param {object} [settings={}] The following settings are available
* @param {object} [settings.object] geometry, position and rotation of the n-dimensional graphical object.
* @param {String} [settings.object.name] name of n-dimensional graphical object.
* @param {number|String|object} [settings.object.color='lime'] color of N-dimensional graphic object.
* <pre>
* number - [Hex triplet]{@link https://en.wikipedia.org/wiki/Web_colors#Hex_triplet} color. Example: 0xffffff - white color
* 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.
* object - Sets the color separately for each vertice.
* You can choose one way for setting of the vertice color from two available:
*
* 1. Set the fourth <b>w</b> coordinate of each vertex in a range from
* <b>settings.options.scales.w.min</b> to
* <b>settings.options.scales.w.max</b>
*
* <b>w</b> coordinate is index of palette color. See method from <b>ColorPicker</b> class.
* Example:
* settings.object.geometry.position: [
* //pyramid
* [0,-0.9428090415820634,0.33333333333333326, 1],
* [0.8164965662730563,0.4714045207910317,0.33333333333333326, 0.5],
* [-0.8164965662730563,0.4714045207910317,0.33333333333333326, 0],
* [7.32733549761259e-9,4.230438555019589e-9,-1.0, -1.0],
* ],
*
* 2. Set a <b>settings.object.geometry.colors</b> array.
* Have effect only if <b>settings.object.geometry.colors</b> are not defined.
* </pre>
* @param {boolean|object} [settings.object.faces] true or object - display the n-dimensional graphical object faces instead of edges.
* @param {float} [settings.object.faces.opacity=0.5] color Float 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.
* If the <b>transparent</b> property is not set to true, the material will remain fully opaque and this value will only affect its color.
* See [Material.opacity]{@link https://threejs.org/docs/#api/en/materials/Material.opacity}.
* @param {boolean} [settings.object.faces.transparent= true] Defines whether this material is transparent.
* This has an effect on rendering as transparent objects need special treatment and are rendered after non-transparent objects.
* When set to true, the extent to which the material is transparent is controlled by setting its <b>opacity</b> property.
* See [Material.transparent]{@link https://threejs.org/docs/#api/en/materials/Material.transparent}.
* @param {Array|Object} [settings.object.geometry] Array of vertices and indices of the n-dimensional graphical object.
* <pre>
* Every item of array is n-dimensional vector of vertice of object.
* Or Object. See object's keys below.
* </pre>
* @param {Array} [settings.object.geometry.position] Array of vertices of the n-dimensional graphical object.
* <pre>
* Every item of array is n-dimensional vector of vertice of object.
* For example, if you want to create a tetrahedron, then you need to create an array of 4 vertices.
* <b>settings.object.geometry.position: [
* [-0.6, 0.1, 0.8],//0
* [0.7, 0.5, 0.9],//1
* [0, -0.4, 0.8],//2
* [0, 0, -0.6]//3
* ]</b>,
* </pre>
* @param {Array} [settings.object.geometry.colors] Array of colors for the each vertex.
* <pre>
* Every vertex is associated with 3 values of the <b>colors</b> array.
* Each value of the <b>colors</b> array is red or green or blue color of the particular vertex in range from 0 to 1.
*
* 0 is no color.
* 1 is full color.
*
* For example:
* settings.object.geometry.colors: [
* 1, 0, 0,//red color of the <b>position[0]</b> vertex.
* 0, 1, 0,//green color of the <b>position[1]</b> vertex.
* 0, 0, 1,//blue color of the <b>position[2]</b> vertex.
* 1, 1, 1,//white color of the <b>position[3]</b> vertex.
* ],
* Have effect only if <b>settings.object.geometry.position</b> points are not <b>THREE.Vector4</b> type.
* See <b>arrayFuncs</b> parametr of the <a href="../../player/jsdoc/module-Player-Player.getPoints.html" target="_blank">Player.getPoints(...)</a> for details.
* </pre>
* @param {array} [settings.object.geometry.opacity] array of opacities for the each vertex. 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 {Array} [settings.object.geometry.boRememberPosition=true] true - Remember vertex positions for higher performance. As result, new vertex positions have no effect.
* @param {Array} [settings.object.geometry.indices] Array of <b>indices</b> of vertices of the n-dimensional graphical object.
* Allows for vertices to be re-used across multiple segments.
* <pre>
* <b>Indices</b> is divided to segments:
*
* <b>indices[0]</b> is edges. Every edge is two indexes of the edge's vertices. Used in 1D objects and higher.
* <b>indices[1]</b> is faces. Every face is three indexes of the edges from <b>indices[0]</b>. Used in 3D objects and higher.
* <b>indices[2]</b> is bodies. Every bodie is four face indexes from <b>indices[1]</b>. Used in 4D objects and higher.
* For example:
*
* <b>n</b> = 1 line.
* <b>settings.object.geometry.position</b> = [
* [-0.5, 1],//0
* [0.5]//1
*]
* <b>settings.object.geometry.indices</b> = [
* [
* 0,//index of the settings.object.geometry.position[0] = [-0.5, 1]
* 1,//index of the settings.object.geometry.position[1] = [0.5]
* ]//0
*]//0
*
* <b>n</b> = 2 triangle
* <b>settings.object.geometry.position</b> = [
* [-0.7, 0.2],//0
* [0.8, 0.6],//1
* [0.1, -0.5]//2
*],
* //edges
* <b>settings.object.geometry.indices[0]</b> = [
* [0, 1],//0 index of the settings.object.geometry.positions [-0.7, 0.2] and [0.8, 0.6]
* [0, 2],//1 index of the settings.object.geometry.positions [-0.7, 0.2] and [0.1, -0.5]
* [1, 2] //2 index of the settings.object.geometry.positions [0.8, 0.6] and [0.1, -0.5]
*]
*
* <b>n</b> = 3 tetrahedron.
* <b>settings.object.geometry.position</b> = [
* [0.8, -0.6, 0.1],//0
* [0.9, 0.7, 0.5],//1
* [0.8, 0, -0.4],//2
* [-0.6, 0.1, 0.1]//3
* ],
* //edges
* <b>settings.object.geometry.indices[0]</b> = [
* [0, 1]//0 index of the settings.object.geometry.positions [0.8, -0.6, 0.1] and [0.9, 0.7, 0.5]
* [0, 2]//1 index of the settings.object.geometry.positions [0.8, -0.6, 0.1] and [0.8, 0, -0.4]
* [0, 3]//2 index of the settings.object.geometry.positions [0.8, -0.6, 0.1] and [-0.6, 0.1, 0.1]
* [1, 2]//3 index of the settings.object.geometry.positions [0.9, 0.7, 0.5] and [0.8, 0, -0.4]
* [1, 3]//4 index of the settings.object.geometry.positions [0.9, 0.7, 0.5] and [-0.6, 0.1, 0.1]
* [2, 3]//5 index of the settings.object.geometry.positions [0.8, 0, -0.4] and [-0.6, 0.1, 0.1]
*]
* //faces. Indices of the edges <b>settings.object.geometry.indices[0]</b>
* <b>settings.object.geometry.indices[1]</b> = [
* [0, 1, 3]//tetrahedron's face 0
* [0, 2, 4]//tetrahedron's face 1
* [3, 4, 5]//tetrahedron's face 2
* [1, 2, 5]//tetrahedron's face 3
*]
*
* <b>n</b> = 4 pentachoron [5-cell]{@link https://en.wikipedia.org/wiki/5-cell}.
* <b>settings.object.geometry.position</b> = [
* [0.8, -0.6, 0.1, -0.85],//0
* [0.9, 0.7, 0.5, -0.55],//1
* [0.8, 0, -0.4, 0],//2
* [-0.6, 0.1, -0.3, 0.55],//3
* [-0.5, 0.2, 0.3, 0.85],//4
* ],
* //edges
* <b>settings.object.geometry.indices[0]</b> = [
* [0, 1]//0 index of the settings.object.geometry.positions [0.8, -0.6, 0.1, -0.85] and [0.9, 0.7, 0.5, -0.55]
* [0, 2]//1 index of the settings.object.geometry.positions [0.8, -0.6, 0.1, -0.85] and [0.8, 0, -0.4, 0]
* [0, 3]//2 index of the settings.object.geometry.positions [0.8, -0.6, 0.1, -0.85] and [-0.6, 0.1, -0.3, 0.55]
* [0, 4]//3 index of the settings.object.geometry.positions [0.8, -0.6, 0.1, -0.85] and [-0.5, 0.2, 0.3, 0.85]
* [1, 2]//4 index of the settings.object.geometry.positions [0.9, 0.7, 0.5, -0.55] and [0.8, 0, -0.4, 0]
* [1, 3]//5 index of the settings.object.geometry.positions [0.9, 0.7, 0.5, -0.55] and [-0.6, 0.1, -0.3, 0.55]
* [1, 4]//6 index of the settings.object.geometry.positions [0.9, 0.7, 0.5, -0.55] and [-0.5, 0.2, 0.3, 0.85]
* [2, 3]//7 index of the settings.object.geometry.positions [0.8, 0, -0.4, 0] and [-0.6, 0.1, -0.3, 0.55]
* [2, 4]//8 index of the settings.object.geometry.positions [0.8, 0, -0.4, 0] and [-0.5, 0.2, 0.3, 0.85]
* [3, 4]//9 index of the settings.object.geometry.positions [-0.6, 0.1, 0.1, 0.55] and [-0.5, 0.2, 0.3, 0.85]
*]
* //faces. Indices of the edges <b>settings.object.geometry.indices[0]</b>
* <b>settings.object.geometry.indices[1]</b> = [
* [7, 8, 9],//0 no 0, 1 vertices
* [5, 6, 9],//1 no 0, 2 vertices
* [4, 6, 8],//2 no 0, 3 vertices
* [4, 5, 7],//3 no 0, 4 vertices
* [2, 3, 9],//4 no 1, 2 vertices
* [1, 3, 8],//5 no 1, 3 vertices
* [1, 2, 7],//6 no 1, 4 vertices
* [0, 3, 6],//7 no 2, 3 vertices
* [0, 2, 5],//8 no 2, 4 vertices
* [0, 1, 4],//9 no 3, 4 vertices
*]
* //bodies. Indices of the faces <b>settings.object.geometry.indices[1]</b>
* <b>settings.object.geometry.indices[2]</b> = [
* [2, 1, 3, 0],//0 no 0 vertice
* [5, 6, 4, 0],//1 no 1 vertice
* [8, 7, 1, 4],//2 no 2 vertice
* [9, 7, 2, 5],//3 no 3 vertice
* [9, 8, 3, 6],//4 no 4 vertice
*]
* </pre>
* @param {Array|number} [settings.object.position] Array - position of the n-dimensional graphical object in n-dimensional coordinates.
* <pre>
* number - position of the 0 coordinate of the n-dimensional graphical object.
* <pre>
* @param {Array|number} [settings.object.rotation] Array - rotation in radians of the n-dimensional graphical object in n-dimensional coordinates.
* <table>
<tr><td><b>n</b> space dimension</td><td>Array index</td><td>Axis of rotation</td><td>Axis type</td><td>Note</td></tr>
<tr><td>0</td><td></td><td></td><td></td><td>no rotation</td></tr>
<tr><td>1</td><td></td><td></td><td></td><td>no rotation</td></tr>
<tr><td>2</td><td>0</td><td></td><td></td><td>No effect for 2-dimensional space</td></tr>
<tr><td></td><td>1</td><td></td><td></td><td>No effect for 2-dimensional space</td></tr>
<tr><td></td><td>2</td><td>2(z)</td><td>point</td><td></td></tr>
<tr><td>3</td><td>0</td><td>0(x)</td><td>line</td><td></td></tr>
<tr><td></td><td>1</td><td>1(y)</td><td></td><td></td></tr>
<tr><td></td><td>2</td><td>2(z)</td><td></td><td></td></tr>
<tr><td>4</td><td>0</td><td>0, 1(xy)</td><td>plane</td><td></td></tr>
<tr><td></td><td>1</td><td>0, 2(xz)</td><td></td><td></td></tr>
<tr><td></td><td>2</td><td>0, 3(xw)</td><td></td><td></td></tr>
<tr><td></td><td>3</td><td>1, 2(yz)</td><td></td><td></td></tr>
<tr><td></td><td>4</td><td>1, 3(yw)</td><td></td><td></td></tr>
<tr><td></td><td>5</td><td>2, 3(zw)</td><td></td><td></td></tr>
<tr><td>5</td><td>0</td><td>0, 1, 2(xyz)</td><td>3D space</td><td></td></tr>
<tr><td></td><td>1</td><td>0, 1, 3(xyw)</td><td></td><td></td></tr>
<tr><td></td><td>2</td><td>0, 1, 4(xy4)</td><td></td><td></td></tr>
<tr><td></td><td>3</td><td>0, 2, 3(xzw)</td><td></td><td></td></tr>
<tr><td></td><td>4</td><td>0, 2, 4(xz4)</td><td></td><td></td></tr>
<tr><td></td><td>5</td><td>0, 3, 4(xw4)</td><td></td><td></td></tr>
<tr><td></td><td>6</td><td>1, 2, 3(yzw)</td><td></td><td></td></tr>
<tr><td></td><td>7</td><td>1, 2, 4(yz4)</td><td></td><td></td></tr>
<tr><td></td><td>8</td><td>1, 3, 4(yw4)</td><td></td><td></td></tr>
<tr><td></td><td>9</td><td>2, 3, 4(zw4)</td><td></td><td></td></tr>
</table>
* <pre>
* number - rotation in radians around axis 0 or rotation around axis 2 for 2D objects i.e. space dimension n = 2.
* See [Can rotations in 4D be given an explicit matrix form?]{@link https://math.stackexchange.com/questions/1402362/can-rotations-in-4d-be-given-an-explicit-matrix-form}, [Rotation matrix]{@link https://en.wikipedia.org/wiki/Rotation_matrix}.
* Examples:
* <b>n = 4</b>, <b>rotation = [Math.PI / 5, 1, 2, 3, 4, 5]</b>
* rotation around 0, 1(xy) plane is Math.PI / 5 radians.
* rotation around 0, 2(xz) plane is 1 radian.
* etc.
*
* <b>n = 4</b>, <b>rotation = Math.PI / 5</b>
* rotation around 0, 1(xy) plane is Math.PI / 5 radians.
*
* <b>n = 2</b>, <b>rotation = [0, 0, Math.PI / 4]</b>
* rotation around 2(z) point is Math.PI / 4 radians.
*
* <b>n = 2</b>, <b>rotation = Math.PI / 5</b>
* rotation around 2(z) point is Math.PI / 5 radians.
* <pre>
* @param {Array} [settings.object.geometry.iAxes] array of indices of the axes.
* For example if <b>iAxes</b> is [1,2], then axis 1 interpret as axis 0 and axis 2 interpret as axis 1.
* As result, you can rotate axes around another axis to 90 degrees.
* In example above you have rotated axis 1 and 2 around axis 0 to 90 degrees.
* @param {Boolean} [settings.plane=false] true - create <b>vectorPlane</b>. See <b>settings.vectorPlane</b> below.
* @param {Array} [settings.vectorPlane] n-dimensional position of the panel
* intersecting with the <b>settings.object.geometry</b> n-dimensional graphical object. Available only if <b>settings.plane</b> is true.
* @param {THREE.Scene} [settings.scene] [THREE.Scene]{@link https://threejs.org/docs/index.html?q=sce#api/en/scenes/Scene}.
* Define <b>scene</b> if you want visualise n-dimensional plane and n-dimensional object to 3-D space of the <b>scene</b>.
* @param {Options} [settings.options] See <a href="../../jsdoc/Options/Options.html" target="_blank">Options</a>.
* Uses only if <b>scene</b> is defined.
* @param {Event} [settings.onIntersection] Plane and object intersection event.
* The <b>onIntersection</b> function parameter is the (n-1)-dimensional geometry of the intersection if a collision occurred, or undefined if a collision did not occur.
* @param {Boolean} [settings.isRaycaster=true] false - Ignoring of the mouse over of the object event or accept of the event if <b>isRaycaster</b> is true or undefined.
* @see [4D Geometry Viewer]{@link https://github.com/anhr/humke-4d-geometry}
* @see [Tesseract]{@link https://ciechanow.ski/tesseract/}
* @see [4D-Shapes]{@link https://artemonigiri.github.io/4D-Shapes/}
* @see [The Regular Polychora]{@link https://www.qfbox.info/4d/regular}
*/
constructor( n, settings ) {
super( settings );
const options = settings.options, _ND = this;
settings.object.raycaster = settings.object.raycaster || {};
settings.object.raycaster.text = settings.object.raycaster.text || function( intersection ) {
//Localization
const getLanguageCode = settings.options.getLanguageCode;
const lang = {
pointId: "point Id",
edgeId: "edge Id",
faceId: "face Id",
bodyId: "body Id",
segmentId: "segment Id",
};
const _languageCode = getLanguageCode();
switch (_languageCode) {
case 'ru'://Russian language
lang.pointId = 'Индекс точки';
lang.edgeId = 'Индекс ребра';
lang.faceId = 'Индекс грани';
lang.bodyId = 'Индекс тела';
lang.segmentId = 'Индекс сегмента';
break;
}
const index = intersection.object.geometry.index, edge = [
index.getX(intersection.indexNew),
index.getY(intersection.indexNew)
],
indices = intersection.object.userData.geometry.indices;
edges = indices[0];
//find point id
var minDistance = Infinity, pointId;
function distance ( i ) {
const pointIndex = edge[i],
distance = intersection.point.distanceTo( new THREE.Vector3().fromBufferAttribute( intersection.object.geometry.attributes.position, pointIndex ) );
if ( minDistance > distance ) {
minDistance = distance;
pointId = pointIndex;
}
}
distance ( 0 );
distance ( 1 );
var text = '\n' + lang.pointId + ': ' + pointId;
//Find edge index
const drawRange = settings.bufferGeometry.drawRange;
// for ( var segmentIndex = 0; segmentIndex < edges.length; segmentIndex++ )
// for ( var segmentIndex = drawRange.start; segmentIndex < (drawRange === Infinity) ? edges.length : (drawRange.start + drawRange.count) / 2; segmentIndex++ ) {
for ( var segmentIndex = drawRange.start; segmentIndex < ( ( drawRange === Infinity) ? edges.length : drawRange.start + drawRange.count ); segmentIndex++ ) {
const edgeCur = edges[segmentIndex];
if (
( ( edgeCur[0] === edge[0] ) && ( edgeCur[1] === edge[1] ) ) ||
( ( edgeCur[0] === edge[1] ) && ( edgeCur[1] === edge[0] ) )
) {
text += '\n' + lang.edgeId + ': ' + segmentIndex;
edges.selected = segmentIndex;
//find segment id.
//indices[1] is faces
//indices[2] is bodies
var detectedIndex;//индекс элемента текушего сегмента segment = indices[indicesSegment]
//в котором встечается индекс segmentIndex выбранного сегмента перудыдущего уровня
for ( var indicesSegment = 1; indicesSegment < indices.length; indicesSegment++ ) {
const segment = indices[indicesSegment];//текуший сегмент
//Встречаются ли в текушем сегменте segment индекс segmentIndex выбранного сегмента перудыдущего уровня
segment.forEach( ( segmentItem, segmentIndexCur ) => {
//найти в текущем элементе сегмента segmentItem индекс segmentIndex выбранного сегмента перудыдущего уровня
//и присвоить detectedIndex = segmentIndexCur равному индексу текущего элемента сегмента
for ( var i = 0; i < segmentItem.length; i++ ) {
//segmentIndex индекс выбранного сегмента перудыдущего уровня
//для segment = indices[1] is faces это индекс выбранного edge
//для segment = indices[2] is bodies это индекс выбранного face
if ( segmentItem[i] === segmentIndex ) {
//if ( detectedIndex != undefined ) console.log( 'Duplicate segment: ' + i );
detectedIndex = segmentIndexCur;
break;
}
}
} );
if ( detectedIndex === undefined ) {
console.error( 'ND: mouse move. Index of segment was not detected' );
break;
}
else {
segmentIndex = detectedIndex;
var segmentName;
switch( indicesSegment ) {//индекс ткушего сегмета
case 1: segmentName = lang.faceId; break;
case 2: segmentName = lang.bodyId; break;
default: segmentName = lang.segmentId;
}
text += '\n' + segmentName + ': ' + segmentIndex;
segment.selected = segmentIndex;
}
}
const segment = indices[indices.length - 1][segmentIndex];
break;
}
}
return text;
};
settings.object.name = settings.object.name || 'Object';
if ( settings.object.aObjects ) settings.object.aObjects.nD = this;
settings.object.geometry = settings.object.geometry || {};
if ( settings.object.geometry instanceof Array ) {
const position = settings.object.geometry;
settings.object.geometry = { position: position, }
}
//Эту строку нельзя использовать потому что во вселенной будет ошибка
//TypeError: classSettings.overriddenProperties.position0.angles[verticeId].middleVertice is not a function
//если:
//Открыть http://localhost/anhr/universe/main/hyperSphere/Examples/ что бы не было видно ребер classSettings.edges.project = false
//Сделать один шаг проигрывателя: нажать →
//Сделать ребра видимыми: Поставить галочку Гиперсфера\Ребро\Отображать.
//Сделать один шаг проигрывателя: нажать →
//Это происходить потому что когда проигрыватель находится не в начальном положении timeId > 0, то в settings.object.geometry.position попадают вершины не из начального времени
//settings.object.geometry.position = settings.object.geometry.position || [];
if (!settings.object.geometry.position) settings.object.geometry.position = [];
class Vector extends ND.VectorN {
/* *
* @description
* <pre>
* An n-dimensional vector is point in an n-dimensional space.
* The length of an array is the dimension of the space.
* @param {Array} [array=0] array of the values for appropriate axes.
* </pre>
* @example //Creates a point in 2-dimensional space. -5 is value for 0 axis and 7.8 is value for 1 axis.
* const vector = new ND.Vector( [-5, 7.8] );
* const point = vector.point;//THREE.Vector3( -5, 7.8, 0 )
* const vector0 = vector[0]//-5
* const vector1 = vector[1]//7.8
*/
constructor( array = 0, vectorSettings = {} ) {
array = super(n, array).array;
const _this = this;
//https://stackoverflow.com/questions/2449182/getter-setter-on-javascript-array
return new Proxy( array, {
get: function ( target, name ) {
var i = parseInt( name );
if ( isNaN( i ) ) {
switch ( name ) {
/* *
* @description
* <pre>
* <b><a href="./NDVector.ND.Vector.html" target="_blank">ND.Vector</a>.point</b>.
* Projection of the <b>ND.Vector</b> object into 3D space.
* Returns <b>THREE.Vector3</b> object.
* Projection of 1-dimensional vector into 3D space: <b>THREE.Vector3( vector[0], 0, 0 ) </b>.
* Projection of 2-dimensional vector into 3D space: <b>THREE.Vector3( vector[0], vector[1], 0 ) </b>.
* Projection of 3-dimensional vector into 3D space: <b>THREE.Vector3( vector[0], vector[1], vector[2] ) </b>.
* </pre>
* @See <a href="./NDVector.ND.Vector.html" target="_blank">ND.Vector</a>
*/
case "point":
const THREE = three.THREE;
if ((typeof settings.object.color === "object") && (array.length >= 4))
return new THREE.Vector4( this.get( undefined, 0 ), this.get( undefined, 1 ), this.get( undefined, 2 ), this.get( undefined, 3 ) );//цвет каждой вершины зависить от оси w этой вершины
return new THREE.Vector3( this.get( undefined, 0 ), this.get( undefined, 1 ), this.get( undefined, 2 ) );//Цвет всех вершин определяется из settings.object.color или из settings.object.geometry.colors или одинаковый по умолчанию
default: {
return _this[name];
}
}
return;
}
if ( i >= n )
return 0;
if ( ( array.length > n ) && settings.object.geometry.iAxes && ( i < settings.object.geometry.iAxes.length ) )
i = settings.object.geometry.iAxes[i];
return array[i];
},
set: function ( target, name, value ) {
const i = parseInt( name );
if ( !isNaN( i ) ) {
if ( i >= array.length ) {
array.push( value );
return array.length;
}
array[i] = value;
_ND.intersection();
if ( vectorSettings.onChange ) vectorSettings.onChange();
return true;
}
switch ( name ) {
case 'onChange':
vectorSettings.onChange = value;
return vectorSettings.onChange;
default: console.error( 'ND: Vector set. Invalid name: ' + name );
}
}
} );
}
push( value ) { console.error( 'ND.Vector.push() unavailable' ); }
pop() { console.error( 'ND.Vector.pop() unavailable' ); }
}
function update() {
_ND.intersection();
// object3D.geometry.attributes.position.array = new THREE.BufferGeometry().setFromPoints( geometry.D3.points ).attributes.position.array;
object3D.geometry.attributes.position.needsUpdate = true;
if (options.guiSelectPoint) options.guiSelectPoint.update();
object3D.children.forEach( child => {
if ( child.type === 'Sprite' ) child.position.copy( geometry.D3.points[child.userData.pointID] );
} );
}
function proxyPosition() {
if ( settings.object.position && settings.object.position.isProxy ) return settings.object.position;
return new Proxy( settings.object.position ? settings.object.position instanceof Array ? settings.object.position : [settings.object.position] : [], {
get: function ( target, name, args ) {
const i = parseInt( name );
if ( !isNaN( i ) ) {
if ( target instanceof Array ) {
if ( i < target.length && ( target[i] !== undefined ) )
return target[i];
return 0;
}
return target;
}
switch ( name ) {
case 'isProxy': return true;
case 'folders':
target.folders = target.folders || [];
return target.folders;
case 'arguments': return;//for dat.GUI
case 'clear': return function () {
target.forEach( ( pos, i ) => target[i] = 0 );
}
case 'forEach': return target.forEach;
case 'length': return target.length;
default: console.error( 'ND: settings.object.position Proxy. Invalid name: ' + name );
}
},
set: function ( target, name, value ) {
target[name] = value;
settings.object.geometry.position.reset();
const input = target.folders[name].cPosition.domElement.querySelector( 'input' );
if ( parseFloat( input.value ) !== value ) {
input.value = value;
update();
}
return true;
},
} );
}
settings.object.position = proxyPosition();
function proxyRotation() {
if ( settings.object.rotation && settings.object.rotation.isProxy ) return settings.object.rotation;
return new Proxy( settings.object.rotation ? settings.object.rotation instanceof Array ? settings.object.rotation : [settings.object.rotation] : [], {
get: function ( target, name, args ) {
const i = parseInt( name );
if ( !isNaN( i ) ) {
if ( target instanceof Array ) {
if ( i < target.length && ( target[i] !== undefined ) ) return target[i];
return 0;
}
return target;
}
switch ( name ) {
case 'isProxy': return true;
case 'boUseRotation': return target.boUseRotation;
case 'folders':
target.folders = target.folders || [];
return target.folders;
case 'trigonometry':
if ( !target.trigonometry ) {
target.trigonometry = new Proxy( [], {
get: function ( target, name, args ) {
const i = parseInt( name );
if ( !isNaN( i ) ) {
if ( !target[i] ) {
settings.object.rotation.boUseRotation = true;
const angle = settings.object.rotation[i];
settings.object.rotation.boUseRotation = false;
target[i] = { sin: Math.sin( angle ), cos: Math.cos( angle ) };
}
return target[i];
}
switch ( name ) {
default: console.error( 'ND: settings.object.rotation Proxy. Invalid name: ' + name );
}
},
set: function ( target, name, value ) {
target[name] = value;
if ( isNaN( parseInt( name ) ) ) return true;
return true;
},
} );
}
return target.trigonometry;
case 'isRotation': return function () {
target.boUseRotation = true;
var boRotation = false
for ( var j = 0; j < n; j++ ) {
if ( settings.object.rotation[j] !== 0 ) {
boRotation = true;
break;
}
}
target.boUseRotation = false;
return boRotation;
}
case 'clear': return function () {
target.forEach( ( angle, i ) => target[i] = 0 );
target.trigonometry = undefined;
}
case 'arguments': return;//for dat.GUI
case 'forEach': return target.forEach;
case 'length': return target.length;
default: console.error( 'ND: settings.object.rotation Proxy. Invalid name: ' + name );
}
},
set: function ( target, name, value ) {
target[name] = value;
if ( isNaN( parseInt( name ) ) ) return true;
settings.object.rotation.trigonometry[name].cos = Math.cos( value );
settings.object.rotation.trigonometry[name].sin = Math.sin( value );
settings.object.geometry.position.reset();
if ( target.folders ) {
const input = target.folders[name].cRotation.domElement.querySelector( 'input' );
if ( parseFloat( input.value ) !== value ) {
input.value = value;
}
}
update();
return true;
},
} );
}
if ( !settings.object.rotation || !settings.object.rotation.isProxy ) {
settings.object.rotation = proxyRotation();
settings.object.rotation.boUseRotation = false;
}
if ( settings.object.geometry.position.target ) settings.object.geometry.position = settings.object.geometry.position.target;
settings.object.geometry.position.boPositionError = true;
const rotationAxes = [[]];//массив осей вращения
function setRotationAxes() {
if ( n < 2 ) return;
if ( rotationAxes[0].length != 0 ) return;
//create rotation axies
//первый ряд
//индексы рядов и колонок матрицы, в которые заносятся значения тригонометрических функций
//Тригонометрические функци надо внестив следующие 4 ячейки матрицы:
//m[tI[0], tI[0]] = cos, m[tI[0], tI[1]] = -sin, m[tI[1], tI[0]] = sin, m[tI[1], tI[1]] = cos.
//const tI = [0,1];
//Для первой матрицы получается
//m[0, 0] = cos, m[0, 1] = -sin, m[1, 0] = sin, m[1, 1] = cos.
if ( n === 2 ) rotationAxes[0].push( 2 );//в двумерном пространстве вращение вокруг оси 2
else for ( var j = 0; j < ( n - 2 ); j++ ) rotationAxes[0].push( j );
rotationAxes[0].tI = [0, 1];
const iLastColumn = rotationAxes[0].length - 1;
var boLastRow = false;
while ( !boLastRow ) {
const iLastRow = rotationAxes.length - 1, lastRow = rotationAxes[iLastRow],
row = [];
for ( var j = iLastColumn; j >= 0; j-- ) {
const prevColumn = lastRow[j];
var iAxis;//индекс оси
if ( j === iLastColumn ) iAxis = prevColumn + 1;//увеличить на 1 последнюю колонку
else iAxis = prevColumn;
if ( iAxis >= n ) {
function createRow( j ) {
if ( j <= 0 ) return false;//последний ряд
//если это последняя колонка и если индекс оси больше количества осей в n пространстве
//увеличить индекс оси в предыдущей колонке
const prevRowColumn = lastRow[j - 1] + 1;
//предыдущая колонка не может больше или рано текушей колонке в предыдущем ряде
if ( prevRowColumn >= lastRow[j] ) return createRow( j - 1 );
row[j - 1] = prevRowColumn;
//индекс оси в текущей колонке равен индексу в предыдущей колонке плюс 1
row[j] = row[j - 1] + 1;
//все последующие колонки увеличиваются на единицу
for ( var k = j + 1; k <= iLastColumn; k++ ) row[k] = row[k - 1] + 1;
//копируем из последнего ряда оставшиеся колонки слева
j = j - 2;
while ( j >= 0 ) {
row[j] = lastRow[j];
j--;
}
return true;
}
boLastRow = !createRow( j );
break;
}
else row[j] = iAxis;
}
if ( !boLastRow ) {
row.tI = [lastRow.tI[0]];
var tI1 = lastRow.tI[1] + 1;
if ( tI1 >= n ) {
row.tI[0]++;
tI1 = row.tI[0] + 1;
}
row.tI[1] = tI1;
if ( row.length === 0 ) {
console.error( 'ND positionWorld get: row is empty' );
break;
}
rotationAxes.push( row );
//debug
if ( iLastRow === rotationAxes.length - 1 ) {
console.error( 'ND positionWorld get: row is not added' );
break;
}
}
}
}
const positionWorld = new Proxy( settings.object.geometry.position ? settings.object.geometry.position : [], {
get: function ( target, name ) {
const i = parseInt( name );
if ( !isNaN( i ) ) {
settings.object.geometry.position.boPositionError = false;
const positionPoint = settings.object.geometry.position[i];
if ( positionPoint.positionWorld ) {
//не надо снова вычислять мировые координатя точки если они уже вычислены
settings.object.geometry.position.boPositionError = true;
return positionPoint.positionWorld;
}
const array = [];
if ( positionPoint !== undefined ) {
if ( !( settings.object.position instanceof Array ) ) {
console.error( 'ND positionWorld get: settings.object.position is not array' );
settings.object.position = [settings.object.position];
}
if ( settings.object.rotation.isRotation() ) {
//https://math.stackexchange.com/questions/1402362/can-rotations-in-4d-be-given-an-explicit-matrix-form
//https://en.wikipedia.org/wiki/Rotation_matrix
function getMatrix( index ) {
const cos = settings.object.rotation.trigonometry[index].cos, sin = settings.object.rotation.trigonometry[index].sin,
array = [];
const tI = rotationAxes[index].tI;
for ( var i = 0; i < n; i++ ) {
const row = [];
for ( var j = 0; j < n; j++ ) {
if ( n === 3 ) {
const iR = n - i - 1, jR = n - j - 1;
if ( ( iR === tI[0] ) && ( jR === tI[0] ) ) row.push( cos );
else if ( ( iR === tI[0] ) && ( jR === tI[1] ) ) row.push( sin );
else if ( ( iR === tI[1] ) && ( jR === tI[0] ) ) row.push( -sin );
else if ( ( iR === tI[1] ) && ( jR === tI[1] ) ) row.push( cos );
else if ( iR === jR ) row.push( 1 );
else row.push( 0 );
} else {
if ( ( i === tI[0] ) && ( j === tI[0] ) ) row.push( cos );
else if ( ( i === tI[0] ) && ( j === tI[1] ) ) row.push( -sin );
else if ( ( i === tI[1] ) && ( j === tI[0] ) ) row.push( sin );
else if ( ( i === tI[1] ) && ( j === tI[1] ) ) row.push( cos );
else if ( i === j ) row.push( 1 );
else row.push( 0 );
}
}
array.push( row );
}
return new MyMath.Matrix( array );
}
var m3;
setRotationAxes();
/*
//test
{
const a = [
[1, 2],
[3, 4],
[5, 6],
],
b = [
[7, 8, 9 ,10],
[11, 12, 13,14],
],
v = [15, 16, 17, 18];
const c = new MyMath.Matrix( a ).multiply( b );
const cv = c * v;
//const cv = c.multiply( v );
console.log(cv);
const m1 = math.matrix( a ),
m2 = math.matrix( b ),
c2 = math.multiply( m1, m2 );
const cv2 = math.multiply( c2, v );
console.log(cv2);
}
*/
if ( n === 2 ) m3 = getMatrix( 0 );//вращение только вокруг оси 2
for ( var j = 0; j < rotationAxes.length; j++ ) {
const m = getMatrix( j );
if ( m3 ) m3 = m3.multiply( m );//m3 = math.multiply( m3, m );
else m3 = m;
}
var position = [];
for ( var j = 0; j < n; j++ ) position.push( positionPoint[j] );
const p = m3.multiply( position );
p.forEach( ( value, i ) => {
if ( value !== undefined ) {
array.push( value + settings.object.position[i] );
} else console.error( 'ND: positionWorld get: invalig array item = ' + value );
} )
} else {
positionPoint.forEach( ( value, j ) => array.push( value + settings.object.position[j] ) );
setRotationAxes();
}
} else console.error('ND positionWorld get index')
if (settings.object.geometry.boRememberPosition === undefined) settings.object.geometry.boRememberPosition = true;
if (settings.object.geometry.boRememberPosition) positionPoint.positionWorld = array;
settings.object.geometry.position.boPositionError = true;
return array;
}
switch ( name ) {
case 'isProxy': return true;
case 'target': return;// target; Если вершинрнуть target, то неверно сработает if ( settings.object.geometry.position.target ) и позиция intersection будет неверна
case 'copy':
return function () {
const v = [];
settings.object.geometry.position.boPositionError = false;
settings.object.geometry.position.forEach( ( value, i ) => {
v[i] = positionWorld[i];
settings.object.geometry.position.boPositionError = false;
} );
settings.object.geometry.position.boPositionError = true;
return v;
}
}
return settings.object.geometry.position[name];
},
} );
//делаю как объект для совместимости с GuiIndices потому что в javascript нельзя передовать ссылки на переменные
//https://stackoverflow.com/questions/7744611/pass-variables-by-reference-in-javascript
const _prevLine = {};
function proxyGeometryPosition() {
const geometry = settings.object.geometry;
if ( geometry.position && geometry.position.isProxy ) return geometry.position;
const playerPosition = geometry.playerPosition, position = playerPosition ? geometry.playerPosition[0] : geometry.position ? geometry.position : [];
return new Proxy(
//Эту строку нельзя использовать потому что во вселенной будет ошибка
//TypeError: classSettings.overriddenProperties.position0.angles[verticeId].middleVertice is not a function
//если:
//Открыть http://localhost/anhr/universe/main/hyperSphere/Examples/ что бы не было видно ребер classSettings.edges.project = false
//Сделать один шаг проигрывателя: нажать →
//Сделать ребра видимыми: Поставить галочку Гиперсфера\Ребро\Отображать.
//Сделать один шаг проигрывателя: нажать →
//Это происходить потому что когда проигрыватель находится не в начальном положении timeId > 0, то в settings.object.geometry.position попадают вершины не из начального времени
//settings.object.geometry.position ? settings.object.geometry.position : [],
position,
{
get: function ( target, name, args ) {
const i = parseInt( name );
if ( !isNaN( i ) ) {
const positionId = i;
if ( settings.object.geometry.position.boPositionError ) {
//срабатывает когда меняется позиция вершины.
//Не хочу менять boPositionError в этом случае, потому что это может происходить на веб странице пользователя
//console.error( 'ND: Use positionWorld instread settings.object.geometry.position' );
}
if ( i >= target.length ) {
console.error( 'ND get settings.object.geometry.position: invalid index = ' + i );
return;
}
if ( target[i] instanceof Array ) {
return new Proxy( target[i], {
get: function ( target, name, args ) {
const i = parseInt( name );
if ( !isNaN( i ) ) {
if ( i >= target.length ) return 0;
const axis = target[i];
if ( isNaN( axis ) ) console.error( 'ND get settings.object.geometry.position[i][' + i + '] = ' + target[i] );
return axis;
}
switch ( name ) {
case 'reset': return function() { delete target.positionWorld; }
case 'distanceTo': return (verticeTo) => {
const vertice = target;
if (vertice.length != verticeTo.length) {
console.error(sUniverse + ': settings.object.geometry.position[i].distanceTo(...). vertice.length != verticeTo.length');
return;
}
//const distance = new three.THREE.Vector3(vertice[0], vertice[1], vertice[2], ).distanceTo(new three.THREE.Vector3(verticeTo[0], verticeTo[1], verticeTo[2], ));
let sum = 0;
vertice.forEach((axis, i) => {
const d = axis - verticeTo[i];
sum += d * d;
})
return Math.sqrt(sum);
}
}
return target[name];
},
set: function ( target, name, value ) {
const i = parseInt( name );
target[name] = value;
if ( !isNaN( i ) ) {
// _ND.bufferGeometry.userData.position[positionId][i] = value;
target.positionWorld = undefined;
if ( _prevLine.prevLine ) {
_prevLine.prevLine.geometry.attributes.position.array = new THREE.BufferGeometry().setFromPoints( geometry.D3.points ).attributes.position.array;
_prevLine.prevLine.geometry.attributes.position.needsUpdate = true;
}
update();//изменилась позиция вершины
}
return true;
},
} );
}
console.error( 'ND: get settings.object.geometry.position is not array.' )
return [target[i]];
}
switch ( name ) {
case 'isProxy': return true;
case 'boPositionError': return target.boPositionError;
case 'target': return target;
/*
* Returns a new vector with the same values as this one.
*/
case "clone":
return function ( i ) {
const v = [];
target[i].forEach( ( value, j ) => v[j] = target[i][j] );
return v;
}
case "reset": return function () { target.forEach( item => delete item.positionWorld ); }
default: return target[name];
}
},
set: function ( target, name, value ) {
const i = parseInt( name );
if ( !isNaN( i ) ) {
//изменилась позиция вершины
target[name].positionWorld = undefined;
}
target[name] = value;
return true;
},
} );
}
settings.object.geometry.position = proxyGeometryPosition();
//indices
function setIndices() {
if ( settings.object.geometry.indices ) return;
settings.object.geometry.indices = [];
settings.object.geometry.indices.boAddIndices = true;
}
setIndices();
//edges
function proxyEdges( newEdges ) {
edges = newEdges || edges;
if ( edges && edges.isProxy ) return edges;
return new Proxy( edges ? edges : [],
{
get: function ( edges, name, value ) {
const i = parseInt( name );
if (!isNaN(i)) {
const edge = edges[i];
edge.intersection = ( geometryIntersection/*, color*/ ) => {
const i = parseInt(name);
if (!isNaN(i)) {
if (i.toString() !== name) {
console.error('ND: settings.object.geometry.indices[]intersection. invalid name = ' + name);
return;
}
if (edges.length === 0) return;//no edges
if (i >= edges.length) {
console.error('ND: settings.object.geometry.indices[]intersection. invalid length: ' + edges.length);
this.indices = { intersection: {} };
return;
}
var indices = edges[i];
//Когда размерность графического оъекта меньше 3
//и когда он создается из объета большей размерности
//то indices это прокси
//if (indices.indices) indices = indices.indices;
if (indices.length !== 2) {
console.error('ND: settings.object.geometry.indices[]intersection. indices.length = ' + indices.length);
return;
}
if (indices[0] === indices[1]) {
console.error('ND: settings.object.geometry.indices[]intersection. indices[0] === indices[1] = ' + indices[0]);
return;
}
const position0 = new Vector(positionWorld[indices[0]]),
position1 = new Vector(positionWorld[indices[1]]);
function indicesIntersection(position) {
switch (n) {
case 2:
break;
default:
if (!position)
break;
position.push(vectorPlane[n - 1]);
break;
}
indices.intersection = { position: position, }
if (indices.intersection.position) indices.intersection.position.iEdge = i;
}
switch (n) {
case 1:
if (vectorPlane[0].between(position0[0], position1[0], true))
geometryIntersection.position.push([vectorPlane[0]]);
break;
case 2:
var vector;
if (vectorPlane[1].between(position0[1], position1[1], true)) {
const a = (position1[1] - position0[1]) / (position1[0] - position0[0]),
b = position0[1] - a * position0[0],
x = (a === 0) || isNaN(a) || (Math.abs(a) === Infinity) ?
position1[0] :
(vectorPlane[1] - b) / a;
if (isNaN(x) || (x === undefined)) { console.error('ND.intersection: x = ' + x + ' position1[0] = ' + position1[0] + ' position0[0] = ' + position0[0]); }
//Если x почти равно position0[0] и position0[0] то будем считать что x между ними
const d = 1e-15;//-1.1102230246251565e-16
if (!x.between(position0[0], position1[0], true)) {
if (!((Math.abs(x - position0[0]) < d) && (Math.abs(x - position0[0]) < d))) {
indices.intersection = {};
break;
}
//else console.log('x почти равно position0[0] и position0[0]');
}
vector = [x, vectorPlane[1]];
}
indicesIntersection(vector);
break;
case 3:
var pos;
//Если позиции вершины находится на этом расстоянии от панели, то будем считать, что она находится на панели
//Для проверки запустить canvas 3D с geometry Variant 1 и проигрыватель в точке t = 0.6.
//
//В примере canvas 3D с geometry.position Variant 2 вершина точно находится на панели
const d = 5.56e-17;
if (Math.abs(vectorPlane[n - 1] - position1[n - 1]) < d) pos = position1;
else if (Math.abs(vectorPlane[n - 1] - position0[n - 1]) < d) pos = position0;
if (pos) {
//Вершина находится на панели.
//Для проверки запустить canvas 3D и установить время проигрывателя t = 0.3 так чтобы вершина 2 пирамиды попала на панель
//В этом случает треугольник пересечения сведется к трем точкам с одинаковыми координатами.
indicesIntersection([pos[0], pos[1]]);
indices.intersection.boVerticeOnPanel = true;
} else {
const nD02 = new ND(n - 1, {
plane: true,
object: {
geometry: {
position: positionWorld.copy(),//settings.object.geometry.position,
indices: [[indices]],
iAxes: [1, 2],
},
},
vectorPlane: vectorPlane.array,
}),
arrayIntersects02 = nD02.intersection();
const nD12 = new ND(n - 1, {
plane: true,
object: {
geometry: {
position: positionWorld.copy(),//settings.object.geometry.position,
indices: [[indices]],
iAxes: [0, 2],
},
},
vectorPlane: vectorPlane.array,
}),
arrayIntersects12 = nD12.intersection();
indicesIntersection(arrayIntersects02.length && arrayIntersects12.length ?
[arrayIntersects12[0][0], arrayIntersects02[0][0]] : undefined);
}
break;
default:
function intersectionAxis(axis) {
const nD0 = new ND(2, {
plane: true,
object: {
geometry: {
position: positionWorld,//settings.object.geometry.position,
indices: [[indices]],
iAxes: [axis, n - 1],
},
},
vectorPlane: vectorPlane.array,
});
return nD0.intersection();
}
const arrayIntersections = [];
var boIntersect = true;
for (var iIntersection = 0; iIntersection < n - 1; iIntersection++) {
const item = intersectionAxis(iIntersection);
if (boIntersect && (item.length === 0)) {
boIntersect = false;
break;
}
if (item.length) arrayIntersections.push(item[0][0]);
}
indicesIntersection(boIntersect ? arrayIntersections : undefined);
}
} else console.error('ND: settings.object.geometry.indices[]intersection. invalid name: ' + name);
};
return edge;
}
switch ( name ) {
case 'intersection': return undefined;
case 'edges': return edges;
case 'isProxy': return true;
}
return edges[name];
},
set: function ( edges, prop, value ) {
const index = parseInt( prop );
if ( isNaN( index ) ) {
switch ( prop ) {
case 'length': break;
case 'edges': settings.object.geometry.indices[0] = proxyEdges( value ); break;
case 'selected': edges.selected = value; break;
default: edges[prop] = value;
// default: console.error( 'ND settings.object.geometry.indices[0].set: invalid prop: ' + prop );
}
return true;
}
if ( value instanceof Array ) {
//Do not add a duplicate edge
for ( var i = 0; i < edges.length; i++ ) {
const edge = edges[i];
if (
( ( edge[0] === value[0] ) && ( edge[1] === value[1] ) ) ||
( ( edge[1] === value[0] ) && ( edge[0] === value[1] ) )
) {
//console.error('ND.proxyEdges: Duplicate edge: ' + edge);//for http://localhost/anhr/egocentricUniverse/master/Examples/2D.html
value.index = i;
return true;
}
}
edges[index] = value;
value.index = index;
return true;
}
const indices = value;
if ( indices.length !== 2 ) {
console.error( 'ND: settings.object.geometry.indices.push invalid indices.length = ' + indices.length );
return true;
}
//find duplicate edge
for ( var i = 0; i < edges.length; i++ ) {
const edgeIndices = edges[i];
if ( ( edgeIndices[0] === indices[0] ) && ( edgeIndices[1] === indices[1] ) ) {
console.error( 'ND: settings.object.geometry.indices.push under constraction' );
return;
}
if ( ( edgeIndices[0] === indices[1] ) && ( edgeIndices[1] === indices[0] ) ) {
console.error( 'ND: settings.object.geometry.indices.push under constraction' );
//у этого ребра есть ребро близнец, у которого индексы вершин поменены местами
//Поэтому можно не искать точку пересечения а брать ее из близнеца
indices[0] = settings.object.geometry.indices[i].indices[0];
indices[1] = settings.object.geometry.indices[i].indices[1];
return;
}
}
edges[index] = value;
return true;
}
} );
}
var edges;
function setEdges() {
edges = settings.object.geometry.indices[0];
var boArray = edges instanceof Array;
if ( !settings.object.geometry.indices[0] || boArray ) {
const indices = settings.object.geometry.indices;
if ( boArray ) { if ( !indices[0].isProxy ) indices[0] = proxyEdges(); }
else indices.push( proxyEdges() );
}
}
setEdges();
//Сгруппировать индексы ребер объета из settings.object.geometry.edges по сегментам обекта
//Например если объект это линия:
//n = 1
//settings.object.geometry.indices = [0, 1]//одно ребро
//settings.object.geometry.edges = [0, 1]
//settings.object.geometry.iEdges = [0]
//где
// 0, //индекс ребра [0, 1] из settings.object.geometry.edges
//
//Объект это треугольник:
//n = 2
//settings.object.geometry.indices = [[0, 1], [1, 2], [0, 2]]//3 ребра
//settings.object.geometry.edges = [[0, 1], [1, 2], [0, 2]]
//settings.object.geometry.iEdges = [0, 1, 2]
//где
// 0, //индекс первого ребра [0, 1] из settings.object.geometry.edges
// 1, //индекс второго ребра [0, 2] из settings.object.geometry.edges
// 2, //индекс третьего ребра [1, 2] из settings.object.geometry.edges
//
//Объект это пирамида:
//n = 3
//settings.object.geometry.indices = [
// [[0, 1], [1, 2], [2, 0]],
// [[0, 1], [1, 3], [3, 0]],
// [[0, 2], [2, 3], [3, 0]],
// [[1, 2], [2, 3], [3, 1]]
//]
//4 грани 6 ребер.
//settings.object.geometry.edges = 0: (2) [
// [0, 1],//0
// [1, 2],//1
// [2, 0],//2
// [1, 3],//3
// [3, 0],//4
// [2, 3]//5
//]
//ребра [0, 2] и [3, 1] заменяю на [2, 0] и [1, 3]
//settings.object.geometry.iEdges = [[0, 1, 2], [0, 3, 4], [2, 5, 4], [1, 5, 3]]
function addEdge( indices ) {
if ( settings.object.geometry.position.length < 2 ) return;//одна точка не имеет ребер
switch ( n ) {
case 1://Example [[0,1]]
const edges = settings.object.geometry.indices[0];
if ( settings.object.geometry.position ) {
indices = [];
positionWorld.forEach( function ( indice, i ) { indices.push( i ) } );
edges.push( indices );
}
break;
default: console.error( 'ND: default edges failed! n = ' + n );
}
}
function proxySegments() {
return new Proxy( [], {
get: function ( target, name ) {
const i = parseInt( name );
if ( !isNaN( i ) )
return target[i];
switch ( name ) {
case 'push': return target.push;
case 'length': return target.length;
case 'isProxy': return true;
case 'forEach': return target.forEach;
case 'selected': return target.selected;
default: console.error( 'ND: settings.object.geometry.indices[' + name + ']. Invalid name: ' + name );
}
},
set: function ( target, name, value ) {
const index = parseInt( name );
if ( isNaN( index ) ) {
switch ( name ) {
case 'length': break;
case 'selected': target.selected = value; break;
default: console.error( 'ND settings.object.geometry.indices[' + name + ']: invalid name: ' + name );
return false;
}
return true;
}
if ( value instanceof Array === false ) {
console.error( 'ND settings.object.geometry.indices[' + l + ']: invalid name: ' + name );
return false;
}
//remove duplicate indices from value
for ( var i = value.length - 1; i >= 0; i-- ) {
for ( var j = i - 1; j >= 0; j-- ) {
if ( value[i] === value[j] ) {
console.error( 'nD proxySegments() set: duplicate index = ' + value[i] );
value.splice( i, 1 );
continue;
}
}
}
//Do not add a duplicate segment
for ( var i = 0; i < target.length; i++ ) {
const segment = target[i],
aDetected = [];//список индексов, которые уже встечались в текущем сегменте
if ( segment.length !== value.length )
continue;//segment не может быть дупликатным если его длинна не равно длинне value
for ( var j = 0; j < segment.length; j++ ) {
aDetected[j] = false;
for ( var k = 0; k < value.length; k++ ) {
if ( segment[j] === value[k] ) {
aDetected[j] = true;
break;
}
}
}
var boDetected = true;
for ( var j = 0; j < aDetected.length; j++ ) {
if ( !aDetected[j] ) {
boDetected = false;
break;
}
}
if ( boDetected ) {
value.index = i;
return true;
}
}
target[index] = value;
value.index = index;
return true;
}
} );
}
function addEdges( level, geometry, positionIndices = [], levelIndices ) {
if ( positionIndices.length === 0 ) positionWorld.forEach( function ( position, i ) { positionIndices.push( i ) } );
geometry = geometry || settings.object.geometry;
if ( !geometry.indices[0] ) geometry.indices[0] = [];
const edges = geometry.indices[0];
if ( ( n === 2 ) && ( geometry.position.length === 2 ) ) {
edges.push( [0, 1] );
return;
}
if ( level === undefined ) return;
if ( level > 2 ) {
for ( var i = 0; i < positionIndices.length; i++ ) {
const posIndices = [];
positionIndices.forEach( function ( indice, j ) { if ( positionIndices[i] !== positionIndices[j] ) posIndices.push( positionIndices[j] ); } );
const lIndices = [];//тут перечислены индексы всех индексов, котоые были добавлены в settings.object.geometry.indices
addEdges( level - 1, undefined, posIndices, lIndices );
if ( lIndices.length ) {
const l = level - 2;
if ( l === 0 ) console.error( 'ND addEdges: invalid l = ' + 1 );
settings.object.geometry.indices[l] = settings.object.geometry.indices[l] === undefined ? proxySegments() : settings.object.geometry.indices[l];
settings.object.geometry.indices[l].push( lIndices );
if ( levelIndices ) levelIndices.push( lIndices.index );
}
}
}
switch ( level ) {
case 2:
//перечислены индексы вершин, которые образуют грань и которые составляют замкнутую линию ребер грани.
//По умолчанию для грани в виде треугольника индексы вершин совпадают с положением вершины в
//settings.object.geometry.position т.е. positionIndices = [0, 1, 2]
if ( !positionIndices ) {
positionIndices = [];
settings.object.geometry.position.forEach( function ( item, i ) { positionIndices.push( i ) } );
}
const length = positionIndices.length;
function addItem( start = 0 ) {
for (var i = start; i < length; i++) {
if (start === 0)
addItem(i + 1);
else {
const edge = [positionIndices[start - 1], positionIndices[i]];
edges.push(edge);
if (levelIndices) levelIndices.push(edge.index);
}
}
/*Это было добавлено в commit 'Update ND.js' 'устранил бесконечный цикл добавления ребра'
* Но при этом стало некорректно работать intersection
* Смотри пример в http://localhost/anhr/commonNodeJS/master/nD/Examples/
* Что то не получается воспроизвести бесконечный цикл добавления ребра
for ( var i = start; i < length; i++ ) {
if ( start === 0 ) {
if (!addItem( i + 1 )) return false;
} else {
const edge = [ positionIndices[start - 1], positionIndices[i] ], length = edges.length;
edges.push( edge );
if (length === edges.length) return false;//новое ребро не добавилось. Эта проверка появилась во время отладки http://localhost/anhr/egocentricUniverse/master/Examples/2D.html
//Иначе будет бесконечный цикл
if ( levelIndices ) levelIndices.push( edge.index );
}
}
return true;
*/
}
addItem();
break;
}
}
function appendEdges() {
switch ( n ) {
case 1://[0, 1]
addEdge();
break;
default: if ( settings.object.geometry.indices[0].length === 0 ) addEdges( n );
}
}
appendEdges();
if ( settings.object.geometry.indices.boAddIndices ) {
//В каждом сегменте geometry.indices[i] должно встечаться определенное количество индексов из передыдущего сегмента
//geometry.indices[i - 1]. В противном случае в geometry.indices[i] добавляем новый элемент, в который добавляем
//индексы из передыдущего сегмента geometry.indices[i - 1].
//Это нужно для того, что бы во время пересечения объекта с plane появлялся замкнутый intersection.
//Например после пересечения 3 мерного объекта с plane получалась замкнутая линия, тоесть начало и конец линии соединяем дополнительным ребром.
//Для проверки в примере запускаем _4D и _3D
const indices = settings.object.geometry.indices;
for( var i = 1; i < indices.length; i++ ) {
const indice = indices[i];
const arrayIndices = [];//Каждый элемент этого массива показывает сколько раз встречается индекс в сегменте geometry.indices[i].
var max = 0;//максимальное количество сколько раз встречается индекс в сегменте geometry.indices[i].
for ( var j = 0; j < indice.length; j++ ) {
const segment = indice[j];
for ( var k = 0; k < segment.length; k++ ) {
const index = segment[k];
if ( arrayIndices[index] === undefined ) arrayIndices[index] = 0;
arrayIndices[index]++;
if ( arrayIndices[index] > max ) max = arrayIndices[index];
}
}
const arraySegment = [];//сюда добавляю все индексы, котоые встречаются меньше нужного
for ( var j = 0; j < arrayIndices.length; j++ ) {
if ( arrayIndices[j] < max ) arraySegment.push( j );
}
if ( arraySegment.length > 0 ) indice.push( arraySegment );
}
}
var vectorPlane;
const geometry = {
get position() {
//https://stackoverflow.com/questions/2449182/getter-setter-on-javascript-array
return new Proxy( this, {
get: function ( target, name ) {
switch ( name ) {
case 'length':
return settings.object.geometry.position.length;
}
const i = parseInt( name );
if ( isNaN( i ) ) {
console.error( 'ND.geometry.position: invalid name: ' + name );
return;
}
return new Vector( positionWorld[i] );
},
} );
},
get copy() {
const copySettings = {
geometry: { position: [], indices: [] },
position: [],
rotation: [],
aObjects: settings.object.aObjects,
name: settings.object.name,
};
settings.object.geometry.position.forEach( vertice => {
const copy = [];
vertice.forEach( axis => copy.push(axis) );
copySettings.geometry.position.push( copy );
} );
settings.object.geometry.indices.forEach( indexes => {
const copy = [];
indexes.forEach( arrayIndex => {
const copyArrayIndex = [];
if ( arrayIndex.indices ) arrayIndex = arrayIndex.indices;
arrayIndex.forEach( index => copyArrayIndex.push(index) );
copy.push(copyArrayIndex);
} );
copySettings.geometry.indices.push( copy );
} );
function copyItem( array ){
const copy = [];
array.forEach( item => { copy.push( item ); } );
return copy;
}
copySettings.position = copyItem( settings.object.position );
settings.object.rotation.boUseRotation = true;
copySettings.rotation = copyItem( settings.object.rotation );
settings.object.rotation.boUseRotation = false;
return copySettings;
},
get geometry() { return settings.object.geometry },
set geometry( geometry ) {
settings.object.geometry.position = geometry.position;
if ( geometry.indices ) {
settings.object.geometry.indices.length = 1;
for ( var i = 0; i < geometry.indices.length; i++ ) {
if ( i === 0 ) {
//убрать edge.intersection, который был создан в ND более высокого уровня, а поэтому в нем указан неверный settings.object.geometry.iAxes, что приводит к перепутыванию осей
settings.object.geometry.indices[0].edges.length = 0;
geometry.indices[0].forEach( edge => settings.object.geometry.indices[0].edges.push( edge.indices === undefined ? edge : edge.indices ) );
} else settings.object.geometry.indices[i] = geometry.indices[i];
}
} else delete settings.object.geometry.indices;
},
//Projection of nD object to 3D space for visualization
D3: {
//Returns a points of projection
get points() { return _ND.bufferGeometry.userData.position; },
//returns indices of the faces vertices
get faceIndices() {
const aFaceIndices = [];
const indices = settings.object.geometry.indices;
if ( indices.length < 2 ) return aFaceIndices;
indices[1].forEach( face => {
const faceVertices = [];
face.forEach( ( edge, iEdge ) => {
if ( iEdge > 2 ) {
console.error( 'ND: geometry.D3.faceIndices get. invalid face edges count.');
return;
}
const edgeVertices = indices[0][edge].indices;
if ( faceVertices.length === 0 ) {
faceVertices.push( edgeVertices[0] );
faceVertices.push( edgeVertices[1] );
} else {
var boVertice0 = false, boVertice1 = false;
for ( var i = 0; i < faceVertices.length; i++ ) {
const faceVertice = faceVertices[i];
if ( faceVertice === edgeVertices[0] ) boVertice1 = true;
else if ( faceVertice === edgeVertices[1] ) boVertice0 = true;
}
if ( !boVertice0 && !boVertice1 ) console.error( 'ND: geometry.D3.faceIndices get. Missing push');
else if ( boVertice0 != boVertice1 ) {
faceVertices.push( edgeVertices[boVertice0 ? 0 : 1] );
}//else вершины третьего ребра совпадают с одной оз вершин первого и второго ребра
}
});
if ( faceVertices.length === 3 ) aFaceIndices.push( faceVertices[0], faceVertices[1], faceVertices[2] );
else console.error( 'ND: PolyhedronGeometry: subdivide. Invalid face vertices count');
});
return aFaceIndices;
},
//Returns an indices of projection
get indices() {
const indices = [], colors = [];
//если объект не состоит из одной вершины и имеет ребера
if ( settings.object.geometry.indices[0].length !== 0 ) {
const edges = settings.object.geometry.indices[0];
for ( var i = 0; i < edges.length; i++ ) {
let edge = edges[i];//.indices;
if ( edge !== undefined ) {
if ( edge.length === undefined ) edge = [0, 1];//for compatibility with EgocentricUniverse
if ( edge.length !== 2 ) {
console.error( 'ND.geometry.D3.get indices: invalid edge.length = ' + edge.length );
return;
}
if ( edge[0] === edge[1] ) {
console.error( 'ND.geometry.D3.get indices: duplicate edge index = ' + edge[0] );
return;
}
//indices.push( ...edge );//incompatible with https://raw.githack.com/anhr/egocentricUniverse/dev/1D.html
edge.forEach( vertice => indices.push( vertice ) );
if ( this.color && ( typeof this.color != "object") ) {
//одинаковый цвет для всех ребер
const color = new THREE.Color(this.color);
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
const rgb = hexToRgb( color.getHexString() );
function push() {
colors.push( rgb.r );
colors.push( rgb.g );
colors.push( rgb.b );
}
push();
if ( edges.length === 1 ) push();//Это линия. Надо установить цвет конца
}
} else console.error( 'ND.geometry.D3.get indices: invalid edge. Возможно вычислены не все точки пересечения' );
}
if (this.color && (typeof this.color === "object")) {
//цвет вершин
if ( colors.length != 0 ) console.error('ND.geometry.D3.get vrtices colors: Invalid colors.length = ' + colors.length);
settings.object.geometry.position.forEach( vertice => {
const rgb = settings.options.palette.toColor(vertice.w, settings.options.scales.w.min, settings.options.scales.w.max );
colors.push(rgb.r);
colors.push(rgb.g);
colors.push(rgb.b);
} );
}
}
// const indicesAndColors = _ND.indicesAndColors(this.color), indices = indicesAndColors.indices, colors = indicesAndColors.colors;
_ND.bufferGeometry.setIndex(indices);
if ( _ND.setDrawRange ) _ND.setDrawRange( 0, indices.length, _ND.bufferGeometry.drawRange.types.edges );//Если тут не установить drawRange, то будут отбражаться не все ребра в http://localhost/anhr/universe/main/hyperSphere/Examples/
return { indices: indices, colors: colors, };
// return indicesAndColors;
},
},
}
const points = [];
for ( var i = 0; i < geometry.position.length; i++ )
points.push( geometry.position[i].point );
this.getPoint = (i) => { return points[i]; }
this.setPositionAttributeFromPoints( points );
vectorPlane = vectorPlane || new Vector( settings.vectorPlane );
if ( !vectorPlane || !vectorPlane.point ) vectorPlane = new Vector( vectorPlane );
var objectIntersect;//порекция объека пересечения панеди с графическим объектом на 3D пространство.
this.opacity = ( object3D, transparent, opacity ) => { _ND.verticesOpacity( transparent, opacity ); }
//The user has selected a segment of nD object
const selectSegment = {
line: undefined,//segment of nD object, selected by user
removeLine: function (line) {
if (line) {
line.parent.remove(line);
line = undefined;
}
return line;
},
opacityDefault: 0.3,
opacityItem: function (item, parentObject, transparent, opacity = selectSegment.opacityDefault) {
if (!item.material) return;
if (transparent && (opacity === 0) && (Object.is(item.parent, parentObject))) parentObject.remove(item);
else {
if (!item.parent) parentObject.add(item);
_ND.opacity( item, transparent, opacity );
}
}
}
function create3DObject( geometry, settings3D = {} ) {
let nD;
if ( !geometry.D3 ) {
nD = new ND( n, {
plane: true,
object: { geometry: geometry, color: settings3D.color, },
} );
geometry = nD.geometry;
} else nD = _ND;
if (geometry.position.length === 0) return;
const color = settings3D.color || 'white';//0xffffff
geometry.D3.color = color;
const indices3D = geometry.D3.indices, indices = indices3D.indices;
const buffer = nD.bufferGeometry;
if ( settings3D.faces ) {
if ( settings3D.faces === true ) settings3D.faces = {};
if ( settings3D.faces.color === undefined ) settings3D.faces.color = color;
if ( settings3D.faces.opacity === undefined ) settings3D.faces.opacity = 0.5;
if ( settings3D.faces.transparent === undefined ) settings3D.faces.transparent = true;
buffer.setIndex( geometry.D3.faceIndices );
buffer.computeVertexNormals ();
} else if ( buffer.index === null ) buffer.setIndex( indices )
let lineBasicMaterialParameters;
lineBasicMaterialParameters = {
vertexColors: true,
toneMapped: false,
}
if ( settings.object.geometry.opacity ) lineBasicMaterialParameters.transparent = true;//установлена прозрачность вершин
const object = indices.length > 1 ?
settings3D.faces ?
new THREE.Mesh(buffer, new THREE.MeshLambertMaterial({
color: color,
opacity: settings3D.faces.opacity,
transparent: settings3D.faces.transparent,
side: THREE.DoubleSide
} ) ) :
new THREE.LineSegments( buffer, new THREE.LineBasicMaterial( lineBasicMaterialParameters ) ) :
new THREE.Points( buffer, new THREE.PointsMaterial( {
color: color,
sizeAttenuation: false,
size: options.point.size / ( options.point.sizePointsMaterial * 2 ),
} ) );
if ( settings3D.name )
object.name = settings3D.name;
scene.add( object );
object.userData.myObject = nD;
object.userData.geometry = geometry.geometry;
object.userData.onMouseDown = function ( intersection ) {
const indices = geometry.geometry.indices, segmentIndex = 0,//edges
segment = indices[segmentIndex], selectedIndex = segment.selected;
selectSegment.line = selectSegment.removeLine(selectSegment.line );
const opacityDefault = intersection.event && intersection.event.button === 0 ? selectSegment.opacityDefault : 1, parentObject = object;
function opacity( transparent = true, opacity = opacityDefault ) {
scene.children.forEach(item => selectSegment.opacityItem(item, parentObject, transparent, opacity));
}
opacity();
const buffer = new THREE.BufferGeometry().setFromPoints( geometry.D3.points );
const lineIndices = [];
function createIndices( item, level ) {
if ( level > 0 ) {
level--;
for ( var i = 0; i < item.length; i++ ) createIndices( indices[level][item[i]], level );
return;
}
const itemIndices = item.indices || item;
if ( itemIndices.length !== 2 ) console.error( 'ND: createIndices. Invalid itemIndices.length = ' + itemIndices.length );
var boDetected = false;
for ( var lineIndicesIndex = 0; lineIndicesIndex < lineIndices.length; lineIndicesIndex += 2 ) {
if ( ( lineIndices[lineIndicesIndex] === itemIndices[0] ) && ( lineIndices[lineIndicesIndex + 1] === itemIndices[1] ) ) {
boDetected = true;
break;
}
}
if ( !boDetected ) itemIndices.forEach( i => { lineIndices.push( i ); } );
}
createIndices( segment[selectedIndex], segmentIndex );
selectSegment.line = new THREE.LineSegments( buffer.setIndex( lineIndices ), new THREE.LineBasicMaterial( { color: object.material.color, } ) );
parentObject.add( selectSegment.line );
}
object.userData.nd = function ( fParent, dat, gui = false, boUpdate = false ) {
if ( !object.userData.nd.update )object.userData.nd.update = function(){ object.userData.nd( fParent, dat, gui, true ) }
if ( !boUpdate ) {
if ( fParent.parent.fCustom ) {
fParent.parent.fCustom.parent.removeFolder( fParent.parent.fCustom );
fParent.parent.fCustom = undefined;
}
if ( !gui && geometry.geometry.gui )
fParent.parent.fCustom = geometry.geometry.gui( fParent, dat, settings.options, _ND );
}
//Localization
const getLanguageCode = options.getLanguageCode;
const lang = {
vertices: 'Vertices',
verticesTitle: 'Vertices.',
edges: 'Edges',
edgesTitle: 'The selected edge lists the vertex indices of the edge.',
distance: 'Distance',
distanceTitle: 'Distance between edge vertices.',
faces: 'Faces',
facesTitle: 'The selected face lists the indexes of the edges of that face.',
bodies: 'Bodies',
bodiesTitle: 'The selected body lists the indexes of the faces of this body.',
objects: 'Objects',
objectsTitle: 'The selected object lists the indexes of the objects that this object consists of. It can be indexes of bodies.',
position: 'Position',
rotation: 'Rotation',
rotationPointTitle: 'Rotation point',
rotationAxisTitle: 'Rotation axis',
rotationPlaneTitle: 'Axes of plane of rotation.',
rotationSpaceTitle: 'Axes of space of rotation.',
rotationnDSpaceTitle: 'Axes of multi dimensional space of rotation.',
defaultButton: 'Default',
defaultPositionTitle: 'Restore default position',
defaultRotationTitle: 'Restore default rotation',
notSelected: 'Not selected',
};
const _languageCode = getLanguageCode();
switch ( _languageCode ) {
case 'ru'://Russian language
lang.vertices = 'Вершины';
lang.verticesTitle = 'Вершины.';
lang.edges = 'Ребра';
lang.edgesTitle = 'В выбранном ребре перечислены индексы вершин ребра.';
lang.distance = 'Расстояние';
lang.distanceTitle = 'Расстояние между вершинами ребра.';
lang.faces = 'Грани';
lang.facesTitle = 'В выбранной грани перечислены индексы ребер этой грани.';
lang.bodies = 'Тела';
lang.bodiesTitle = 'В выбранном теле перечислены индексы граней этого тела.';
lang.objects = 'Объекты';
lang.objectsTitle = 'В выбранном объекте перечислены индексы объектов, из которого состоит этот объект. Это могут быть индексы тел.';
lang.position = 'Позиция';
lang.rotation = 'Вращение';
lang.rotationPointTitle = 'Точка вращения.';
lang.rotationAxisTitle = 'Ось вращения.';
lang.rotationPlaneTitle = 'Оси плоскости вращения.';
lang.rotationSpaceTitle = 'Оси пространства вращения.';
lang.rotationnDSpaceTitle = 'Оси многомерного пространства вращения..';
lang.defaultButton = 'Восстановить';
lang.defaultPositionTitle = 'Восстановить позицию объекта по умолчанию';
lang.defaultRotationTitle = 'Восстановить вращение объекта по умолчанию';
lang.notSelected = 'Не выбран';
break;
}
for ( var i = fParent.__controllers.length - 1; i >= 0; i-- ) { fParent.remove( fParent.__controllers[i] ); }
const indices = geometry.geometry.indices, segmentIndex = indices.length - 1;
function addController(
segmentIndex,//settings.object.geometry.indices index
fParent,
segmentItems,//массив индексов элементов текущего segment, которые выбрал пользователь
prevLine//выбранный пользователем сегмент объекта на более высоком уровне. Например если пользователь выбрал ребро то prevLine указывает на выбранную пользователем грань
) {
_prevLine.prevLine = prevLine;
var segment;
if ( segmentItems ) {
function addItem( item, i ) {
item.i = i;
segment.push( item );
}
segment = [];
if ( segmentIndex === -1 ) {
//vertices
//непонятно почему, но для 2D вершины перечислены в segmentItems.indices
if ( segmentItems.forEach )
segmentItems.forEach( i => addItem( geometry.geometry.position[i], i ) );
else segmentItems.indices.forEach( i => addItem( geometry.geometry.position[i], i ) );
} else {
//indices
const index = indices[segmentIndex];
segmentItems.forEach( i => addItem( index[i].indices ? index[i].indices : index[i], i ) );
}
}else segment = indices[segmentIndex];
const items = { Items: [lang.notSelected] };
var fChildSegment, line;
var name, title;
switch ( segmentIndex ) {
case -1: name = lang.vertices; title = lang.verticesTitle; break;
case 0: name = lang.edges; title = lang.edgesTitle; break;
case 1: name = lang.faces; title = lang.facesTitle; break;
case 2: name = lang.bodies; title = lang.bodiesTitle; break;
default: name = lang.objects; title = lang.objectsTitle;
}
const fSegment = fParent.addFolder( name );
let cDistance;
fSegment.userData = { objectItems: true, }
dat.folderNameAndTitle( fSegment, name, title );
switch ( segmentIndex ) {
case 0://edges
cDistance = dat.controllerZeroStep( fSegment, { value: 0, }, 'value' );
cDistance.domElement.querySelector( 'input' ).readOnly = true;
dat.controllerNameAndTitle( cDistance, lang.distance, lang.distanceTitle );
break;
}
const cSegment = fSegment.add( items, 'Items', { [lang.notSelected]: -1 } ).onChange( function ( value ) {
if ( fChildSegment ) {
fChildSegment.__controllers.forEach( ( item, i ) => {
const controller = fChildSegment.__controllers[i];
if ( controller.__select && ( controller.__select.selectedIndex != 0 ) ) {
controller.__select.selectedIndex = 0;
controller.__onChange();
}
} );
fSegment.removeFolder( fChildSegment );
fChildSegment = undefined;
}
const selectedIndex = cSegment.__select.selectedIndex - 1;
line = selectSegment.removeLine(line);
const parentObject = object;
function opacity(transparent = true, opacity = selectSegment.opacityDefault ) {
scene.children.forEach(item => selectSegment.opacityItem(item, parentObject, transparent, opacity));
}
//Непонятно почему, но прозрачность линии зависит от индекса текущего сегмента segmentIndex.
//Для проверки открыть холст 5D.
//Если в 5D прозрачность делать постоянной 0.3, то при выборе тела (body) из объета, объект буден практически непрозрачным
function getOpacity() {
return -0.1 * segmentIndex + selectSegment.opacityDefault;
}
function removeVerticeControls(){
if ( segmentIndex !== -1 ) return;
for ( var i = fSegment.__controllers.length - 1; i >=0 ; i-- ) {
const controller = fSegment.__controllers[i];
if ( Object.is(cSegment, controller) ) continue;
fSegment.remove(controller);
}
}
if ( selectedIndex === -1 ) {
switch ( segmentIndex ) {
case 0://edges
cDistance.setValue( '' );
break;
}
removeVerticeControls();
if ( prevLine ) {
selectSegment.opacityItem(prevLine, parentObject, false);
if (prevLine.userData.prevLine) selectSegment.opacityItem(prevLine.userData.prevLine, parentObject, true, getOpacity() );
else opacity( true );
return;
}
opacity( false );
return;
}
if ( prevLine ) {
opacity(true, 0);
selectSegment.opacityItem(prevLine, parentObject, true, getOpacity() );
if (prevLine.userData.prevLine) selectSegment.opacityItem(prevLine.userData.prevLine, parentObject, true, 0 );
} else opacity();
if ( segmentIndex === -1 ) {
//Vertices
removeVerticeControls();
//если так сделать, то при выборе объекта пересечения почемуто исчезают _prevLine.prevLine и object3D
//и как результат появляется ошибка когда пользователь изменяет положение вершины
// const vertice = geometry.geometry.position[segment[selectedIndex].i];
const vertice = settings.object.geometry.position[segment[selectedIndex].i];
for ( var i = 0; i < vertice.length; i++ ) {
switch(i){
case 0:
case 1:
case 2:
var axis;
switch(i){
case 0: axis = options.scales.x; break;
case 1: axis = options.scales.y; break;
case 2: axis = options.scales.z; break;
}
fSegment.add( vertice, i, axis.min, axis.max, ( axis.max - axis.min ) / 100 );
break;
default: dat.controllerZeroStep( fSegment, vertice, i );
}
}
} else {
const buffer = new THREE.BufferGeometry().setFromPoints( geometry.D3.points );
const lineIndices = [];
function createIndices( item, level ) {
if ( level > 0 ) {
level--;
for ( var i = 0; i < item.length; i++ ) createIndices( indices[level][item[i]], level );
return;
}
const itemIndices = item.indices || item;
if ( itemIndices.length !== 2 ) console.error( 'ND: createIndices. Invalid itemIndices.length = ' + itemIndices.length );
var boDetected = false;
for ( var lineIndicesIndex = 0; lineIndicesIndex < lineIndices.length; lineIndicesIndex += 2 ) {
if ( ( lineIndices[lineIndicesIndex] === itemIndices[0] ) && ( lineIndices[lineIndicesIndex + 1] === itemIndices[1] ) ) {
boDetected = true;
break;
}
}
if ( !boDetected ) itemIndices.forEach( i => { lineIndices.push( i ); } );
}
createIndices(segment[selectedIndex], segmentIndex);
line = new THREE.LineSegments(buffer.setIndex(lineIndices), new THREE.LineBasicMaterial({ color: object.material.color }));
switch ( segmentIndex ) {
case 0://edges
//debug
if ( lineIndices.length != 2 ) {
console.error( 'ND: Select edge. Invalid lineIndices.length = ' + lineIndices.length );
break;
}
const position0 = geometry.geometry.position[lineIndices[0]],
position1 = settings.object.geometry.position[lineIndices[1]];
if (position0.length && position1.length) cDistance.setValue( position0.distanceTo(position1) );
else console.error("ND: Select edge. Invalid edge's vertices distance");
break;
}
//debug
line.userData.name = fSegment.name + ' ' + value;
parentObject.add( line );
}
if (prevLine && line) line.userData.prevLine = prevLine;
if (segmentIndex >= 0) fChildSegment = addController(segmentIndex - 1, fSegment, segment[selectedIndex], line);
} );
dat.controllerNameAndTitle(cSegment, '');
var selectedItem = 0;
const selected = segmentIndex >= 0 ? indices[segmentIndex].selected : undefined;
var selectedOpt;//debug
for ( var i = 0; i < segment.length; i++ ) {
const item = segment[i], opt = document.createElement( 'option' ),
itemIndex = item.index != undefined ? item.index : item.i != undefined ? item.i : i;
opt.innerHTML = '(' + (item.i === undefined ? i : item.i) + ') ' + (segmentIndex === -1 ? '' : ( item.indices ? item.indices : item ).toString() );
opt.item = item;
if ( ( selected != undefined) && ( selected === itemIndex ) ) {
selectedOpt = opt;//debug
selectedItem = i + 1;
}
cSegment.__select.appendChild( opt );
}
if ( selected && !selectedOpt ) console.error( 'ND: addController. Selected item was not detected' );
cSegment.__select[selectedItem].selected = true;
if ( selectedItem != 0 ) {
cSegment.__select.selectedIndex = selectedItem;
cSegment.setValue( cSegment.__select[selectedItem].innerHTML );
}
return fSegment;
}
const childFolders = Object.keys(fParent.__folders);
childFolders.forEach( folderName => {
const childFolder = fParent.__folders[folderName];
childFolder.__controllers.forEach( ( item, i ) => {
var controller;
if ( childFolder.userData && childFolder.userData.objectItems ) controller = childFolder.__controllers[i];
else if ( folderName === 'indices' ) {
//ищем controller в GuiIndices
Object.keys( childFolder.__folders ).forEach( folderName => {
if ( !controller ) {
const folder = childFolder.__folders[folderName];
if ( folder.userData && folder.userData.objectItems ) controller = folder.__controllers[i];
}
});
}
if ( controller && controller.__select && controller.__select.selectedIndex != 0 ) {
controller.__select.selectedIndex = 0;
controller.__onChange();
}
} );
if ( !childFolder.customFolder ) fParent.removeFolder( childFolder );
} );
const fPosition = fParent.addFolder( lang.position ),
fRotation = n < 2 ? undefined : fParent.addFolder( lang.rotation );
function rotation( i ) {
if ( rotationAxes[i].length === 0 ) return;
settings.object.rotation.boUseRotation = true;
settings.object.rotation.folders[i] = {
cRotation: fRotation.add( settings.object.rotation, i, 0, Math.PI * 2, 0.01 ).onChange( function ( value ) { update(); } ),
default: settings.object.rotation[i],
}
var name = '', title = '';
rotationAxes[i].forEach( axis => name = name + ( name.length === 0 ? '' : ',' ) + axis );
switch( n ){
case 2: title = lang.rotationPointTitle; break;
case 3: title = lang.rotationAxisTitle; break;
case 4: title = lang.rotationPlaneTitle; break;
case 5: title = lang.rotationSpaceTitle; break;
default: title = lang.rotationnDSpaceTitle; break;
}
dat.controllerNameAndTitle( settings.object.rotation.folders[i].cRotation, n === 2 ? '2' : name, title );
settings.object.rotation.boUseRotation = false;
}
for ( var i = 0; i < n; i++ ) {
const axisName = i;
//position
{
const f = fPosition.addFolder( axisName );
settings.object.position.folders[i] = {
positionController: new PositionController( function ( shift ) { settings.object.position[axisName] += shift; },
{ getLanguageCode: options.getLanguageCode, } ),
default: settings.object.position[i],
};
f.add( settings.object.position.folders[i].positionController );
settings.object.position.folders[i].cPosition = dat.controllerZeroStep( f, settings.object.position, i, function ( value ) { update(); } );
dat.controllerNameAndTitle( settings.object.position.folders[i].cPosition, axisName );
}
}
//rotation
if ( n === 2 ) rotation( 0 );//поворот только вокруг оси 2
else rotationAxes.forEach( (item, i ) => rotation( i ) );
//Restore default position.
const buttonPositionDefault = fPosition.add( {
defaultF: function ( value ) { settings.object.position.folders.forEach( item => item.cPosition.setValue( item.default ) ); },
}, 'defaultF' );
dat.controllerNameAndTitle( buttonPositionDefault, lang.defaultButton, lang.defaultPositionTitle );
//Restore default rotation.
if ( fRotation ) {
const buttonRotationDefault = fRotation.add( {
defaultF: function ( value ) { settings.object.rotation.folders.forEach( item => item.cRotation.setValue( item.default ) ); },
}, 'defaultF' );
dat.controllerNameAndTitle( buttonRotationDefault, lang.defaultButton, lang.defaultRotationTitle );
}
addController( segmentIndex, fParent );
}
if ( options.guiSelectPoint ) options.guiSelectPoint.addMesh( object );
//raycaster
if (settings.isRaycaster != false) {
object.userData.raycaster = {
onIntersection: function ( intersection, mouse ) {
intersection.indexNew = intersection.index;
//если убрать строку ниже, то будет ошибка
//Cannot read properties of undefined (reading 'cameraTarget')
//в строке
//if (!func.cameraTarget)
//в функции this.changeTarget класса Player.cameraTarget в файле player.js
//
//Но зато будут видны иденксы точек, ребер, граней и т.д. если навести мышку на многомерную фигуру
//
//Лень разбираться почему так сделал.
//Для решения проблемы создаю новую переменную intersection.indexNew. Смотри строку выше.
//
//Для тестирования запусить http://localhost/anhr/commonNodeJS/master/nD/Examples/
//На холсте 5D щелкнуть мышкой на Object или Intersection.
delete intersection.index;
Options.raycaster.onIntersection( intersection, options, scene, options.camera, options.renderer );
},
onIntersectionOut: function () {
//когда мышка покидает объект, нужно удалить индексы ребер, граней и т.д., над которыми была мышка.
//В противном случае, когда выбираешь объект в dat.GUI,
//автоматически веберется ребро, грань и т.д. над которыми последний раз была мышка
//Визуально это вызывает недоумение у пользователя
geometry.geometry.indices.forEach( indice => indice.selected = undefined );
Options.raycaster.onIntersectionOut( scene, options.renderer );
},
onMouseDown: function ( intersection, event ) {
intersection.event = event;//Теперь можно выполнять разные действия в зависимости от нажатой кнопки мыши
Options.raycaster.onMouseDown( intersection, options );
},
text: settings.object.raycaster ? settings.object.raycaster.text : undefined,
}
if ( options.eventListeners ) options.eventListeners.addParticle( object );
}
return object;
}
/**
* @returns an array of intersection points of <b>vectorPlane</b> and <b>geometry</b>. See constructor for details.
* @param {geometryIntersection} [geometryIntersection = { position: [], indices: [[]] }] Arrays of vertices and indexes of the result of the intersection of the panel and the nD object. See <b>settings.object.geometry</b> of <b>ND</b> constructor for details.
* @param {array} [geometryIntersection.position] Array of vertices of the result of the intersection. See <b>settings.object.geometry.position</b> of <b>ND</b> constructor for details.
* @param {array} [geometryIntersection.indices] Array of <b>indices</b> of vertices of the result of the intersection. See <b>settings.object.geometry.indices</b> of <b>ND</b> constructor for details.
* @param {array} [iIntersections] Array of indices that have been added to <b>geometryIntersection.indices</b>
*/
this.intersection = function ( geometryIntersection = { position: [], indices: [[]] }, iIntersections ) {
if ( settings.plane === false ) return;
function intersection( iEdge, aEdge ) {
for ( var i = 0; i < geometryIntersection.position.length; i++ ) {
if ( geometryIntersection.position[i].iEdge === iEdge ) {
//duplicate position
if (
aEdge &&
( aEdge.length < 2 )//Длинна ребра линии пересечения получается больше 2 если ребро объекта лежит на пенели.
//Для проверки на canvas 3D сделать две вершины по оси z равными -0.4
//И проиграть проигрыватель на t = 0.3.
) aEdge.push( i );
return;
}
}
const edge = settings.object.geometry.indices[0][iEdge];
edge.intersection( geometryIntersection );//, settings.object.color );
const position = edge.intersection.position;
if ( position ) {
var boAdd = true;
if ( edge.intersection.boVerticeOnPanel ) {
//Вершина на панели. В этом случае все ребра, сходящиеся к этой вершине буду выдвать одну и ту же точку пересечения
//Не нужно добавлять повторяющиеся точки.
for ( var i = 0; i < geometryIntersection.position.length; i++ ) {
if ( position.equals( geometryIntersection.position[i] ) ) {
boAdd = false;
aEdge.boVerticeOnPanel = true;
break;
}
}
}
if ( boAdd ) {
geometryIntersection.position.push( position );
if ( aEdge ) aEdge.push( geometryIntersection.position.length - 1 );
}
}
}
if ( !geometryIntersection.position.isProxy )
geometryIntersection.position = new Proxy( geometryIntersection.position, {
get: function ( target, name, args ) {
switch ( name ) {
case 'isProxy': return true;
case 'target': return target;
case "reset":
return function () {
target.forEach( item => item.positionWorld = undefined );
}
}
return target[name];
},
set: function ( target, name, value ) {
const i = parseInt( name );
if ( isNaN( i ) ) {
if ( name === "boPositionError" ) {
target[name] = value;
return true;
}
return target[name];
}
if ( i >= target.length ) {
//find duplicate item
var boDuplicate = false;
for ( var j = 0; j < i; j++ ) {
boDuplicate = true;
for ( var k = 0; k < target[j].length; k++ ) {
if ( target[j][k] !== value[k] ) {
boDuplicate = false;
break;
}
}
if ( boDuplicate ) break;
}
if ( !boDuplicate ) target.push( value );
return target.length;
}
target[i] = value;
return true;
}
} );
switch ( n ) {
case 1:
if ( settings.object.geometry.indices.length !== 1 ) console.error( 'ND.intersection: under constraction. Это не линия.' );
const edge = settings.object.geometry.indices[0][0];
edge.intersection( geometryIntersection );
break;
case 2:
const iFaces = settings.object.geometry.indices[1];
if ( iFaces ) settings.object.geometry.indices[1].forEach( function ( iFace ) { iFace.forEach( function ( iEdge ) { intersection( iEdge ) } ); } );
else {
for ( var i = 0; i < settings.object.geometry.indices[0].length; i++ ) { intersection( i ); }
addEdges( undefined, geometryIntersection );
}
break;
default: {
var iSegments = settings.iSegments || ( n - 2 ),//Индекс массива индексов сегментов settings.object.geometry.indices[iSegments]
//для которого получаем вершины и индексы
segments;//массив индексов сегментов для которого получаем вершины и индексы
while( iSegments >= 0 ) {
segments = settings.object.geometry.indices[iSegments];
if ( segments ) break;
iSegments--;
}
//settings.indice индекс сегмента в текущем массиве индексов сегментов settings.object.geometry.indices[iSegments][settings.indice]
if ( settings.indice === undefined ) {
for ( var i = 0; i < segments.length; i++ ) {
const nd = new ND( n, {
plane: true,
object: { geometry: {
indices: settings.object.geometry.indices,
position: positionWorld.copy(),
},
// color: 'white',
}, indice: i, iSegments: iSegments, } ),
s = iSegments - 1;
var iIntersections;
if ( s !== 0 ) {//Не создавать iIntersections для ребер
iIntersections = [];
}
nd.intersection( geometryIntersection, iIntersections );
if ( iIntersections && iIntersections.length ) {
geometryIntersection.indices[s] = geometryIntersection.indices[s] || proxySegments();
geometryIntersection.indices[s].push( iIntersections );
}
}
//Ищем ребра с одной вершиной. Такие ребра появляются если вершина находится на панели
//Это нужно когда объект пересекается панелью и одна из вершин находтся на панели
//Тода появляется два ребра с одной вершиной. Я их удаляю и объединяю в одно ребро.
//Для проверки запустить canvas 3D с geometry Variant 1 и проигрыватель установить на t = 0.6
const edges = geometryIntersection.indices[0];
var vertices = [];//список вершин ребер с одной вершиной.
for ( var i = edges.length - 1; i >= 0; i-- ) {
const edge = edges[i];
if ( edge.boVerticeOnPanel && ( edge.length === 1 ) ) {
vertices.push( edge[0] );
edges.splice( i, 1 );
}
}
switch( vertices.length ) {
case 0:
case 1://одна вершина находится на панели
break;
case 2:
edges.push( vertices );
break;
default: console.error( 'ND.intersection: invalid edge.' );
}
if ( edges.length > 1 ) {
//ищем вершины с одним ребром и объединяем такие вершины в новое ребро.
//Это нужно чтобы линия грани была замкнутая.
//Для проверки создаем в примере _4D и _3D
for ( var i = 0; i < geometryIntersection.position.length; i++ ) {
var verticesCount = 0;
for ( var j = 0; j < edges.length; j++ ) {
const edge = edges[j];
for ( var k = 0; k < edge.length; k++ ) {
if ( edge[k] === i ) verticesCount++;
}
}
if ( verticesCount < 2 ) {
if ( verticesCount === 0 ) console.error( 'ND.intersection: Invalid verticesCount = ' + verticesCount );
else vertices.push( i );
}
}
if ( vertices.length > 0 ) {
if ( vertices.length != 2 ) console.error( 'ND.intersection: invalid edge.' );
else edges.push( vertices );
}
}
} else {
var segment = segments[settings.indice];
if ( iSegments > 1 ) {
for ( var i = 0; i < segment.length; i++ ) {
const nd = new ND( n, {
plane: true,
object: settings.object, indice: segment[i], iSegments: iSegments - 1,
} );
if ( n > 4 ) {
if ( n === 5 ) {
var iIntersect = [];
nd.intersection( geometryIntersection, iIntersect );
if ( iIntersect.length ) {
if ( iIntersect.length === 1 ) {
iIntersect = iIntersect[0];
iIntersections.push( iIntersect );
} else {
const ind = n - 4;
geometryIntersection.indices[ind] = geometryIntersection.indices[ind] || proxySegments();
geometryIntersection.indices[ind].push( iIntersect );
if ( iIntersections ) iIntersections.push( iIntersect.index );
}
}
} else console.error( 'ND.intersection: n = ' + n + ' under constraction' );
} else nd.intersection( geometryIntersection, iIntersections );
}
} else {
const edge = [];
if ( segment.indices ) segment = segment.indices;
for ( var i = 0; i < segment.length; i++ ) { intersection( segment[i], edge ); }
if ( edge.length > 0 ) {
if ( ( edge.length !== 2 ) || ( edge[0] === edge[1] ) ) {
//длинна массива edge может быть меньше 2 если всего одна вершина находится на панели
//В этом случае линия пересечения geometryIntersection состоит из одной точки и невозможно создать ребро
if ( !edge.boVerticeOnPanel ) {
const error = 'ND.intersection: invalid edge';
console.error( error );
return;
}
}
const intersectionEdges = geometryIntersection.indices[0];
var duplicateEdge = false;
for ( var i = 0; i < intersectionEdges.length; i++ ) {
const intersectionEdge = intersectionEdges[i];
if (
( intersectionEdge[0] === edge[0] ) && ( intersectionEdge[1] === edge[1] ) ||
( intersectionEdge[0] === edge[1] ) && ( intersectionEdge[1] === edge[0] )
) {
duplicateEdge = true;
if ( iIntersections ) iIntersections.push( i );
break;
}
}
if ( !duplicateEdge ) {
if ( iIntersections ) iIntersections.push( intersectionEdges.length );
intersectionEdges.push( edge );
}
}
}
}
}
}
if ( settings.onIntersection )
settings.onIntersection( geometryIntersection );
if ( scene ) {
if ( objectIntersect ) {
if ( options.guiSelectPoint ) options.guiSelectPoint.removeMesh( objectIntersect );
scene.remove( objectIntersect );
if (options.eventListeners) options.eventListeners.removeParticle( objectIntersect );
}
if ( geometryIntersection.position.length )
objectIntersect = create3DObject( geometryIntersection, { name: 'Intersection', color: 'white' } );
}
return geometryIntersection.position;
}
const THREE = three.THREE, scene = settings.scene;
if ( scene ) {
//Если есть scene, обязательно должен быть options. Здесь создать options неполучится
const scales = options.scales;
if ( n <= 1 ) scales.y = undefined;
if ( n <= 2 ) scales.z = undefined;
scales.text ||= {};
scales.text.rect ||= {};
scales.text.rect.displayRect = false;
scales.text.precision = 2;
}
var object3D;
function projectTo3D() {
if ( !scene ) return;
//Graphic object. Currenyly is line
if ( object3D ) {
for ( var i = object3D.children.length - 1; i >= 0; i-- ) {
const child = object3D.children[i];
if ( child instanceof THREE.Sprite ) {
object3D.remove( child );
if ( options.guiSelectPoint ) options.guiSelectPoint.removeMesh( child );
}
}
scene.remove( object3D );
if ( options.guiSelectPoint ) options.guiSelectPoint.removeMesh( object3D );
options.eventListeners.removeParticle( object3D );
object3D = undefined;
}
object3D = create3DObject( geometry, {
name: settings.object.name,
faces: settings.object.faces,
color: settings.object.color || _ND.defaultColor,//'lime',//0x00ff00,//'green'
} );
}
projectTo3D();
//Plane
/* * @class
* @description
* <pre>
* N-dimensional intersection plane.
* Used to create a section with an n-dimensional graphic object.
* For 1 dimensional space <b>ND.Plane</b> is point.
* For 2 dimensional space <b>ND.Plane</b> is line.
* For 3 dimensional space <b>ND.Plane</b> is plane.
* For n-dimensional space <b>ND.Plane</b> is n-dimensional plane.
* </pre>
*/
class Plane {
constructor() {
var mesh;
this.createMesh = function () {
if ( !scene ) return;
const color = 0x0000FF;//blue
switch ( n ) {
case 1://point
options.point.size = ( options.scales.x.max - options.scales.x.min ) * 500;//10
mesh = new THREE.Points( new THREE.BufferGeometry().setFromPoints( [
new THREE.Vector3( 0, 0, 0 )
], 3 ),
new THREE.PointsMaterial( {
color: color,
sizeAttenuation: false,
} ) );
break;
case 2://line
mesh = new THREE.Line( new THREE.BufferGeometry().setFromPoints( [
new THREE.Vector3( options.scales.x.min, 0, 0 ), new THREE.Vector3( options.scales.x.max, 0, 0 )
] ), new THREE.LineBasicMaterial( { color: color } ) );
break;
case 3://plane
mesh = new THREE.GridHelper( 2, 10, color, color );
mesh.rotation.x = Math.PI / 2;
break;
default: {
return;//I can not render 4D and higher panel
}
}
mesh.name = 'Plane';
scene.add( mesh );
if ( options.guiSelectPoint ) options.guiSelectPoint.addMesh( mesh );
mesh.position.copy( vectorPlane.point );
//raycaster
mesh.userData.raycaster = {
onIntersection: function ( intersection, mouse ) {
delete intersection.index;
Options.raycaster.onIntersection( intersection, options, scene, options.camera, options.renderer );
},
onIntersectionOut: function () { Options.raycaster.onIntersectionOut( scene, options.renderer ); },
onMouseDown: function ( intersection ) { Options.raycaster.onMouseDown( intersection, options ); },
}
options.eventListeners.addParticle( mesh );
//
vectorPlane.onChange = function () {
mesh.position.copy( vectorPlane.point );
mesh.updateMatrix();
}
}
}
}
if ( settings.plane === undefined ) settings.plane = false;
if ( settings.plane ) {
const plane = new Plane();
plane.createMesh();
}
/**
* @description
* Returns N-dimensional vector of the plane that intersects nD object.
*/
this.vectorPlane;
/**
* @description
* <pre>
* returns geometry of N-dimensional object. See <b>settings.object.geometry</b> parameter of <a href="./module-ND-ND.html" target="_blank">ND</a>.
* key <b>geometry</b> - get or set a geometry of nD object. See See <b>settings.object.geometry</b> parameter of <a href="./module-ND-ND.html" target="_blank">ND</a>.
* key <b>D3</b> - Projection of nD object to 3D space for visualization.
* <b>D3.points</b> - Points of projection. See <a href="https://threejs.org/docs/index.html?q=BufferGeometry#api/en/core/BufferGeometry.setFromPoints" target="_blank">.setFromPoints</a> of <a href="https://threejs.org/docs/index.html?q=BufferGeometry#api/en/core/BufferGeometry" target="_blank">THREE.BufferGeometry</a>.
* <b>D3.indices</b> - Indices of points of projection. See <a href="https://threejs.org/docs/index.html?q=BufferGeometry#api/en/core/BufferGeometry.setIndex" target="_blank">.setIndex</a> of <a href="https://threejs.org/docs/index.html?q=BufferGeometry#api/en/core/BufferGeometry" target="_blank">THREE.BufferGeometry</a>.
* <b>D3.color</b> - color of projection.
* </pre>
*/
this.geometry;
Object.defineProperties( this, {
vectorPlane: {
get: function () { return vectorPlane; }
},
geometry: {
get: function () { return geometry; },
set: function ( geometryNew ) {
geometry.geometry = geometryNew;
settings.object.name = 'Object';
settings.object.rotation.clear();
settings.object.position.clear();
projectTo3D();
this.intersection()
},
},
object3D: {
get: () => { return object3D; },
},
object: {
get: function () { return settings.object; },
set: function ( object ) {
if ( !object ) {
console.error( 'ND.object set: invalid object' );
return;//отсутствует холст с размерностью пространства верхнего уровня
}
if ( !object.update )
settings.object = object;
const p = object.position;
if ( p ) settings.object.position = [...p];
const r = object.rotation;
if ( r ) {
if ( r instanceof Array ) settings.object.rotation = [...r];
else settings.object.rotation = r;
}
settings.object.name = settings.object.name || 'Object';
//copy indices
if ( object.geometry.indices ) {
const indices = [];
object.geometry.indices.forEach( array => {
const item = [];
indices.push( item );
array.forEach( index => {
if ( index.indices ) item.push( index.indices );
else item.push( index );
} );
} );
settings.object.geometry.indices = indices;
}
setIndices();
setEdges();
if ( !settings.object.geometry.indices[0].isProxy ) settings.object.geometry.indices[0] = proxyEdges( object.geometry.indices[0] );
settings.object.position = proxyPosition();
settings.object.rotation = proxyRotation();
settings.object.geometry.position = proxyGeometryPosition();
settings.object.geometry.position.reset();
appendEdges();
if ( object.update ) {
object3D.geometry.setFromPoints( geometry.D3.points ).setIndex( geometry.D3.indices.indices );
if ( settings.options && settings.options.guiSelectPoint ) settings.options.guiSelectPoint.updatePoints();
return;
}
projectTo3D();
this.intersection()
},
},
} );
}
//Overridden methods from base class
get defaultColor() { return 'lime'; }
positionOffset(position, positionId) { return positionId * position.itemSize; }
}
ND.gui = class {
/**
* Custom controllers for N-dimensional graphic object
* @param {Options} options See <b>options</b> parameter of <a href="../../myThree/jsdoc/module-MyThree-MyThree.html" target="_blank">MyThree</a> class.
* @param {GUI} dat [dat.GUI()]{@link https://github.com/dataarts/dat.gui}.
* @param {GUI} fParent parent folder.
* @example new ND.gui( options, dat, fMesh );
*/
constructor( options, dat, fParent ) {
//Localization
const getLanguageCode = options.getLanguageCode;
const lang = {
nD: 'nD',
nDTitle: 'n-dimensional object',
};
const _languageCode = getLanguageCode();
switch ( _languageCode ) {
case 'ru'://Russian language
lang.nDTitle = 'n-мерный объект';
break;
}
const fND = fParent.addFolder( lang.nD );
dat.folderNameAndTitle( fND, lang.nD, lang.nDTitle );
this.object = function( object, dat ) {
var display = 'none';
if ( object && object.userData.nd ) {
display = 'block';
object.userData.nd( fND, dat );
} else Object.keys( fND.__folders ).forEach( key => {
const folder = fND.__folders[key];
if ( !folder.userData || ( folder.userData.objectItems === undefined ) ) return;
folder.__controllers.forEach( cSegment => {
if (cSegment.__select) {
const selectedItem = 0;
cSegment.__select.selectedIndex = selectedItem;
cSegment.setValue( cSegment.__select[selectedItem].innerHTML );
}
} );
} );
if (object) fND.domElement.style.display = display;
}
}
}
ND.VectorN = class {
/**
* base class for n-dimensional vector.
* @param {number} n dimension of the vector.
* @param {Array} array array of the values of the vector.
*/
constructor(n, array) {
if (array.isVector) return array;
if (array instanceof Array === false) {
if (typeof array === 'number') array = [array];
else if (array.array) array = array.array;
else console.error('FermatSpiral: Vector: invalid array type');
}
if (n !== undefined) while (array.length < n) array.push(0);
//https://stackoverflow.com/questions/2449182/getter-setter-on-javascript-array
return new Proxy(array, {
get: function (target, name) {
var i = parseInt(name);
if (isNaN(i)) {
switch (name) {
case "array": return array;
/*
* Adds v to this vector.
*/
case "add":
return function (v) {
target.forEach((value, i) => target[i] += v[i]);
return this;
}
case "index": return vectorSettings.index;
case "isVector": return true;
default: console.error('ND: Vector get. Invalid name: ' + name);
}
return;
}
if (i >= n)
return 0;
if ((array.length > n) && settings.object.geometry.iAxes && (i < settings.object.geometry.iAxes.length))
i = settings.object.geometry.iAxes[i];
return array[i];
},
});
}
}
export default ND;
//https://stackoverflow.com/a/18881828/5175935
if ( !Number.prototype.between )
Number.prototype.between = function ( a, b, inclusive ) {
var min = Math.min.apply( Math, [a, b] ),
max = Math.max.apply( Math, [a, b] );
return inclusive ? this >= min && this <= max : this > min && this < max;
};
//Comparing arrays https://stackoverflow.com/a/14853974/5175935
// Warn if overriding existing method
if ( Array.prototype.equals )
console.warn( "Overriding existing Array.prototype.equals. Possible causes: New API defines the method, there's a framework conflict or you've got double inclusions in your code." );
// attach the .equals method to Array's prototype to call it on any array
Array.prototype.equals = function (array) {
// if the other array is a falsy value, return
if ( !array )
return false;
// compare lengths - can save a lot of time
if ( this.length != array.length )
return false;
for (var i = 0, l = this.length; i < l; i++) {
// Check if we have nested arrays
if (this[i] instanceof Array && array[i] instanceof Array) {
// recurse into the nested arrays
if ( !this[i].equals( array[i] ) )
return false;
}
else if (this[i] != array[i]) {
// Warning - two different object instances will never be equal: {x:20} != {x:20}
return false;
}
}
return true;
}
// Hide method from for-in loops
Object.defineProperty( Array.prototype, "equals", { enumerable: false } );