/**
* @module Sphere
* @description Sphere graphical object.
*
* @author [Andrej Hristoliubov]{@link https://github.com/anhr}
*
* @copyright 2011 Data Arts Team, Google Creative Lab
*
* @license under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
//import Edges from './edges.js';
import Circle from './circle.js';
import Triangle from './triangle.js';
//debug
import FibonacciSphereGeometry from '../FibonacciSphere/FibonacciSphereGeometry.js'
import three from '../three.js'
const sSphere = 'Sphere';
let isFacesIndicesProxy = false;
class Sphere extends Circle
{
//Overridden methods from base class
displayDebug( THREE, center, r, scene ) {
return super.displayDebug( THREE, center, r );
}
name( getLanguageCode ) {
//Localization
const lang = {
name: "Sphere",
};
const _languageCode = getLanguageCode();
switch (_languageCode) {
case 'ru'://Russian language
lang.name = 'Сфера';
break;
default://Custom language
if ((guiParams.lang === undefined) || (guiParams.lang.languageCode != _languageCode))
break;
Object.keys(guiParams.lang).forEach(function (key) {
if (lang[key] === undefined)
return;
lang[key] = guiParams.lang[key];
});
}
return lang.name;
}
logSphere() {
if (!this.debug) return;
this.logCircle();
this.classSettings.settings.object.geometry.indices.bodies.forEach((body, i) => console.log('indices.bodies[' + i + '] = ' + JSON.stringify( body )));
}
get verticeEdgesLengthMax() { return 6 }//нельзя добавлять новое ребро если у вершины уже 6 ребер
TestVertice( vertice, strVerticeId ){
if ((vertice.edges.length !== 3)//пирамида
&& (vertice.edges.length !== 6))
console.error(sSphere + '. Invalid ' + strVerticeId + '.edges.length = ' + vertice.edges.length);
}
TestFace( faceId, sFaceId ){
const length = this.classSettings.settings.object.geometry.indices[1][faceId].length;
if (length !== 3)//пирамида
console.error(sSphere + '. Invalid ' + sFaceId + '.length = ' + length);
super.TestFace( faceId, sFaceId );
}
Indices(){
const settings = this.classSettings.settings,
sIndices = sSphere + '.Indices';
if (settings.object.geometry.indices.count != undefined) {
settings.object.geometry.indices[1].count = settings.object.geometry.indices.count;
delete settings.object.geometry.indices.count;
}
settings.object.geometry.indices[2] = settings.object.geometry.indices[2] || settings.object.geometry.indices.bodies || [];
super.Indices();
const debug = this.debug;
if (!isFacesIndicesProxy) {
settings.object.geometry.indices = new Proxy(settings.object.geometry.indices, {
get: (_indices, name) => {
switch (name) {
case 'bodies':
if (!_indices[2] || !_indices[2].isBodiesProxy) {
_indices[2] = new Proxy(_indices[2] || [], {
get: function (_bodies, name) {
const i = parseInt(name);
if (!isNaN(i)) {
_bodies[i] = _bodies[i] || [];
if (!_bodies[i].isBodyProxy){
_bodies[i] = new Proxy( _bodies[i], {
get: (_body, name) => {
switch (name) {
case 'isBodyProxy': return true;
case 'push': return ( faceId ) => {
if (debug) {
if (faceId === undefined) console.error(sIndices + ': Invalid faceId = ' + faceId );
for ( let i = 0; i < _body.length; i++ ) {
if (_body[i] === faceId ) {
console.error(sIndices + ': Duplicate body faceId = ' + faceId );
return;
}
}
}
_body.push( faceId );
const faces = _indices[1];
if(faces[faceId] === undefined) faces[faceId] = {};
}
}
return _body[name];
},
} );
}
return _bodies[i];
}
switch (name) {
case 'isBodiesProxy': return true;
}
return _bodies[name];
},
});
const faces = _indices[1];
_indices[2].forEach( body => body.forEach( faceId => faces[faceId] = faces[faceId] || {} ) );
for ( let i = 0; i < faces.length; i++ ) faces[i] = faces[i] || {};
}
return _indices[2];
case 'faces':
return new Proxy(settings.object.geometry.indices.bodies[this.classSettings.bodyId], {
get: (body, name) => {
const i = parseInt(name);
if (!isNaN(i)) {
return indices[1][body[i]];
}
switch (name) {
case 'test': return () => {
if (!this.debug) return;
body.forEach(faceId => {
settings.object.geometry.indices.faces[faceId].face.TestFace();
});
}
case 'push': return (face=[]) => {
const faces = _indices[1],
facesLength = faces.push(face),
faceId = facesLength - 1;
face = faces[faceId];//converts face to Proxy
face.face = new Triangle( this.options, {
faceId: faceId,
settings: settings,
} );
body.push(faceId);
return facesLength;
}
}
return body[name];
},
});
}
return _indices[name];
},
});
isFacesIndicesProxy = true;
}
const facesCount = settings.object.geometry.indices[1].count || 4,//default is pyramid
indices = settings.object.geometry.indices,
body = indices.bodies[this.classSettings.bodyId];
for ( let i = body.length; i < facesCount; i++ ) body.push( i );
//сразу заменяем все грани на прокси, потому что в противном случае, когда мы создаем прокси грани в get, каждый раз,
//когда вызывается get, в результате может получться бесконечная вложенная конструкция и появится сообщение об ошибке:
//EgocentricUniverse: Face get. Duplicate proxy
const defaultIndices = {
edges:
[
//будет создано в Utils.edges
{},
{},
{},
{ vertices: [0,3] },//3
{ vertices: [1,3] },//4
{ vertices: [2,3] },//5
],
faces:
[
[],//уже создано в Circle.Indices
[0, 3, 4],
[1, 4, 5],
[2, 3, 5]
]
}
const edges = settings.object.geometry.indices.edges, defaultEdges = defaultIndices.edges, edgesLength = 6;
for (let edgeId = 3;//вершины первых тех ребер потом будут вычисляться в Utils.edges
edgeId < edgesLength; edgeId++) {
let edge = edges[edgeId];
if (!edge) {
edges.pushDefault(defaultEdges[edgeId]);
continue;
}
//default edge vertices
if (!edge.vertices) edge.vertices = defaultEdges[edgeId].vertices;
const verticesCount = 2, vertices = edge.vertices;
for ( let i = vertices.length; i < verticesCount; i++ ) vertices.push( defaultEdges[edgeId].vertices[i] );
}
settings.object.geometry.indices.faces.forEach( ( face, faceId ) => {
//default face edges
const edgesCount = 3;
if ((faceId === 0) && (edgesCount != face.length)) console.error(sIndices + ': Duplicate default faceId = ' + faceId);//эта грань уже была создана в Circle.Indices
if (faceId > 3) console.error(sIndices + ': Invalid faceId = ' + faceId );
for ( let i = face.length; i < edgesCount; i++ ) face.push( defaultIndices.faces[faceId][i] );
face.face = new Triangle( this.options, {
faceId: faceId,
settings: settings,
} );
});
}
/**
* Sphere graphical object.
* @param {Options} options See <a href="../../../jsdoc/Options/Options.html" target="_blank">Options</a>.
* @param {object} [classSettings] Sphere class settings.
* @param {number} [classSettings.bodyId=0] Identifier of the array of the faces ids in the <b>classSettings.settings.object.geometry.indices.bodies</b> array.
* @param {number} [classSettings.faceGroups=0] Face groups level. You can define sphere's faces count (fc) as <b>fc = 2 ^ ((faceGroups + 1) * 2</b>.
* <pre>
* Examples:
* <table>
<tr><td><b>faceGroups</b></td><td>faces count</td></tr>
<tr><td>0</td><td>4</td></tr>
<tr><td>1</td><td>16</td></tr>
<tr><td>2</td><td>64</td></tr>
<tr><td>3</td><td>256</td></tr>
<tr><td>4</td><td>1024</td></tr>
<tr><td>5</td><td>4096</td></tr>
</table>
* </pre>
*
* @param {object} [classSettings.settings] The following settings are available
* @param {object} [classSettings.settings.object] Sphere object.
* @param {String} [classSettings.settings.object.name='Sphere'] name of sphere.
* @param {String} [classSettings.settings.object.color='lime'] color of edges.
* @param {object} [classSettings.settings.object.geometry] Sphere geometry.
* @param {object} [classSettings.settings.object.geometry.indices] Array of <b>indices</b> of vertices, edges, faces and bodies of sphere.
* @param {number} [classSettings.settings.object.geometry.indices.count=4] facess count. Default sphere is pyramid with 4 faces.
* @param {Array} [classSettings.settings.object.geometry.indices.edges=[{}, {}, {}, {vertices: [0,3]}, {vertices: [1,3]}, {vertices: [2,3]}]] Edges array. Default edges count is <b>classSettings.settings.object.geometry.indices.count</b>.
* @param {object} [classSettings.settings.object.geometry.indices.edges.edge] Edges array item is edge.
* @param {Array} [classSettings.settings.object.geometry.indices.edges.edge.vertices] Array of edge vertices indices. Every edge have two vertices.
* @param {float} [classSettings.settings.object.geometry.indices.edges.edge.distance=1.632993154528117] Edge length. Distance between edge vertices.
* @param {Array} [classSettings.settings.object.geometry.indices.faces=[[0, 1, 2], [0, 3, 4], [1, 4, 5], [2, 3, 5]]] Faces array. Every item of the <b>faces</b> array is array of edges indices for current face.
* @param {Array} [classSettings.settings.object.geometry.indices.bodies=[[0, 1, 2, 3]]] Bodies array. Every item of the <b>bodies</b> array is array of facess indices for current body.
* @param {boolean} [classSettings.debug=false] Debug mode. Diagnoses your code and display detected errors in console
**/
constructor( options, classSettings={} ) {
if (classSettings.bodyId === undefined) classSettings.bodyId = 0;
super( options, classSettings );
/**
* Projects a sphere to the canvas
* @param {THREE.Scene} scene [THREE.Scene]{@link https://threejs.org/docs/index.html?q=sce#api/en/scenes/Scene}
* @param {object} [params] The following parameters are available
* @param {object} [params.center] center of the sphere
* @param {float} [params.center.x=0.0] X axis of the center
* @param {float} [params.center.y=0.0] Y axis of the center
* @param {float} [params.center.z=0.0] z axis of the center
* @param {float} [params.radius=1.0] radius of the sphere
*/
this.project = (scene, params={}) => {
const sProject = sSphere + '.project', settings = this.classSettings.settings;
settings.options = options;//for debug. See Triangle.project
params.center = params.center || {x: 0.0, y: 0.0, z: 0.0}
//remove previous sphere
this.remove(scene);
const THREE = three.THREE,
r = params.radius !== undefined ? params.radius : 1.0,
center = new THREE.Vector3(params.center.x, params.center.y, params.center.z),
faceEdgeFaceAngle = Math.acos( 1 / 3 ),//Face-edge-face angle. approx. 70.5288° or 1.2309594173407747 radians https://en.wikipedia.org/wiki/Tetrahedron
edgeEdgeAngle = 2 * Math.PI / 3,//120° or 2.0943951023931953 radians
//углы поворота первого треугольника
rotation1 = new THREE.Euler(
//x
//наклоняю на угол, равный углу между гранями пирамиды
Math.PI + faceEdgeFaceAngle,//approx. 70.5288° * 2 or 4.372552070930568 radians
//y
0,
//z
//И поворачиваю так, что бы новая вершина треугольника оказалась на вершине приамиды
Math.PI * ( 2 / 3 + 1 )//300° or 5.235987755982988 radians;
),
groupPosition = new THREE.Group();//сюда помещаем все грани и отладочные объекты что бы у них была одинаковая позиция
groupPosition.position.copy(center);
scene.add(groupPosition);
settings.object.geometry.indices.faces.forEach( face => {
//каждую грань (треугольник) помещаю в пару вложенных друг в друга группы что бы было удобно их поворачивать
const group = new THREE.Group(), groupFace = new THREE.Group();
groupFace.add(group);
const faceId = face.face.classSettings.faceId;
switch( faceId ) {
case 0: break;//нулевой треугольник никуда не поворачиваю. Это будет основание пирамиды
case 1://Первый треуголник
//boProject = false;
group.rotation.copy(rotation1);
break;
case 2://Второй треуголник
//Сначала поворачиваю как первый треугольник
group.rotation.copy(rotation1);
//А потом уже родительску группу поворачиваю на 120° по оси высоты пирамиды которая совпадает с ось z так,
//что бы треугольник совпал со второй гранью пирамиды
groupFace.rotation.z = edgeEdgeAngle;//120° or 4.1887902047863905 radians
break;
case 3://Третий треуголник
//Сначала повораяиваю как первый треугольник
group.rotation.copy(rotation1);
//А потом уже родительску группу поворачиваю на 240° по оси высоты пирамиды которая совпадает с ось z так,
//что бы треугольник совпал с третьей гранью пирамиды
groupFace.rotation.z = edgeEdgeAngle * 2;//240° or 4.1887902047863905 radians
break;
default: console.error(sProject + ': Invalid faceId = ' + faceId);
}
group.updateMatrixWorld( true );//обновить group.matrix и group.matrixWorld после ее поворота
groupPosition.add( groupFace );
if (this.debug && options.guiSelectPoint && Triangle.debug) {
groupFace.name = 'groupFace ' + faceId;
options.guiSelectPoint.addMesh( groupFace );
}
// if (boProject)
face.face.project(group, r);
} );
settings.scene = scene;
//add faces
//вычисляем новые грани путем разделения граней нулевого body на 4 новых грани
//каждое ребро грани делим пополам и полученные 3 вершины соединяем ребрами
const indices = settings.object.geometry.indices,
position = settings.object.geometry.position,
bodies = indices.bodies,
body = bodies[this.classSettings.bodyId],
faces = indices.faces,
edges = indices.edges;
this.classSettings.faceGroups = this.classSettings.faceGroups || 0;
for (let faceGroupsId = 0; faceGroupsId < this.classSettings.faceGroups; faceGroupsId++) {
if (this.debug) console.log('faceGroupsId' + faceGroupsId)
edges.forEach(edge => delete edge.halfEdgeId);
//divide all edges to two half edges
edges.forEach(edge => edge.halfEdge);
//divide all faces to 4 faces
const newEdges = [];
faces.forEach((face, faceId) => {
//console.log('face = ' + face + ' faceId = ' + faceId);
//if (faceId != 0) return;
//return;
const commonVertice = (edge1, edge2) => {
if ((edge1[0] === edge2[0]) || (edge1[0] === edge2[1])) return edge1[0];
if ((edge1[1] === edge2[0]) || (edge1[1] === edge2[1])) return edge1[1];
console.error(sProject + ': no common vertices for [' + edge1 + '] and [' + edge2 + '] edges.');
},
getEdgeId = (v1, v2) => {
for (let i = 0; i < newEdges.length; i++) {
const edgeId = newEdges[i], edge = edges[edgeId];
if (((edge[0] === v1) && (edge[1] === v2)) || ((edge[0] === v2) && (edge[1] === v1))) return edgeId;
}
const edgeIndex = edges.push([v1, v2]) - 1;
newEdges.push(edgeIndex);
return edgeIndex;
}
const edge0Id = face[0], edge0 = edges[edge0Id], halfEdge0Id = edge0.halfEdgeId, edge0Old = edge0.old,// halfEdge0 = edges[halfEdge0Id],
edge1Id = face[1], edge1 = edges[edge1Id], edge1Old = edge1.old,// halfEdge1Id = edge1.halfEdgeId, halfEdge1 = edges[halfEdge1Id],
edge2Id = face[2], edge2 = edges[edge2Id], halfEdge2Id = edge2.halfEdgeId, edge2Old = edge2.old,// halfEdge2 = edges[halfEdge2Id],
v0 = commonVertice(edge0Old, edge2Old), v1 = commonVertice(edge0Old, edge1Old), v2 = commonVertice(edge1Old, edge2Old),
v4 = edge0[1], v5 = edge1[1], v6 = edge2[1],
edge12Id = getEdgeId(v4, v6),// edge12 = edges[edge12Id],
edge13Id = getEdgeId(v4, v5),// edge13 = edges[edge13Id],
edge14Id = getEdgeId(v5, v6);// edge14 = edges[edge14Id];
//replace face edges
if (v0 !== edge0[0]) face[0] = halfEdge0Id;
face[1] = edge12Id;
if (v0 !== edge2[0]) face[2] = halfEdge2Id;
//create new faces
const createFace = (edgeIds, vertice) => {
const faceEdges = [edges[edgeIds[0]], edges[edgeIds[1]], edges[edgeIds[2]]];
const edge0 = faceEdges[0][0] === vertice ? edgeIds[0] : faceEdges[0].halfEdgeId,
edge1 = faceEdges[1][0] === vertice ? edgeIds[1] : faceEdges[1].halfEdgeId;
faces.push([edge0, edge1, edgeIds[2]]);
}
createFace([edge2Id, edge1Id, edge14Id], v2);
createFace([edge1Id, edge0Id, edge13Id], v1);
faces.push([edge12Id, edge13Id, edge14Id]);//face Id = 4
});
}
this.Test();//for debug
this.display(3, {
position: params.center,
});
if (this.debug && Triangle.debug) {
const color = "lightgray", opacity = 0.2;
const sphere = new THREE.Mesh(new FibonacciSphereGeometry(r),
new THREE.MeshLambertMaterial({
color: color,
opacity: opacity,
transparent: true,
side: THREE.DoubleSide//от этого ключа зависят точки пересечения объектов
})
);
groupPosition.add(sphere);
if (typeof Intersections != 'undefined') new Intersections(sphere, plane);
}
this.logSphere();
}
}
}
export default Sphere;