Compare commits
No commits in common. "main" and "feature-big-polytopes" have entirely different histories.
main
...
feature-bi
11
README.md
11
README.md
|
@ -4,12 +4,7 @@ FourDjs
|
||||||
Visualisations of four-dimensional polytopes projected into 3-space, rendered
|
Visualisations of four-dimensional polytopes projected into 3-space, rendered
|
||||||
with three.js
|
with three.js
|
||||||
|
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/spikelynch/fourdjs/main/docs/screenshot-24cell.png" width="445" />
|
||||||
|
|
||||||
[Basic interactive demo](https://etc.mikelynch.org/fourjs/)
|
[Basic interactive demo](https://etc.mikelynch.org/fourjs/)
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/spikelynch/fourdjs/main/docs/screenshot-24cell.png" width="612" /><br />
|
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/spikelynch/fourdjs/main/docs/screenshot-120cell.png" width="612" /><br />
|
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/spikelynch/fourdjs/main/docs/screenshot-600cell.png" width="612" />
|
|
||||||
|
|
||||||
|
|
||||||
|
|
137
cell120.js
137
cell120.js
|
@ -1,137 +0,0 @@
|
||||||
// trying to go from faces to dodecahedra
|
|
||||||
|
|
||||||
|
|
||||||
function shared_vertices(f1, f2) {
|
|
||||||
return f1.nodes.filter((f) => f2.nodes.includes(f));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function adjacent_faces(f1, f2) {
|
|
||||||
// adjacent faces which share an edge, not just a vertex
|
|
||||||
const intersect = shared_vertices(f1, f2);
|
|
||||||
if( intersect.length < 2 ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if( intersect.length > 2 ) {
|
|
||||||
console.log(`warning: faces ${f1.id} and ${f2.id} have too many common vertices`);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function find_adjacent_faces(faces, face) {
|
|
||||||
const neighbours = faces.filter((f) => f.id !== face.id && adjacent_faces(f, face));
|
|
||||||
return neighbours;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function find_dodeca_mutuals(faces, f1, f2) {
|
|
||||||
// for any two adjacent faces, find their common neighbours where
|
|
||||||
// all three share exactly one vertex (this, I think, guarantees that
|
|
||||||
// all are on the same dodecahedron)
|
|
||||||
|
|
||||||
const n1 = find_adjacent_faces(faces, f1);
|
|
||||||
const n2 = find_adjacent_faces(faces, f2);
|
|
||||||
const common = n1.filter((f1) => n2.filter((f2) => f1.id === f2.id).length > 0 );
|
|
||||||
// there's one extra here - the third which has two nodes in common with
|
|
||||||
// both f1 and f2 - filter it out
|
|
||||||
const mutuals = common.filter((cf) => {
|
|
||||||
const shared = cf.nodes.filter((n) => f1.nodes.includes(n) && f2.nodes.includes(n));
|
|
||||||
return shared.length === 1
|
|
||||||
});
|
|
||||||
return mutuals;
|
|
||||||
}
|
|
||||||
|
|
||||||
function find_dodeca_next(faces, dodeca, f1, f2) {
|
|
||||||
// of a pair of mutuals, return the one we haven't already got
|
|
||||||
const m = find_dodeca_mutuals(faces, f1, f2);
|
|
||||||
if( dodeca.filter((f) => f.id === m[0].id ).length > 0 ) {
|
|
||||||
m.shift();
|
|
||||||
}
|
|
||||||
return m[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// from any two mutual faces, return all the faces in their dodecahedron
|
|
||||||
|
|
||||||
function make_dodecahedron(faces, f1, f2) {
|
|
||||||
const dodecahedron = [ f1, f2 ];
|
|
||||||
|
|
||||||
// take f1 as the 'center', get the other four around it from f2
|
|
||||||
const fs = find_dodeca_mutuals(faces, f1, f2);
|
|
||||||
const f3 = fs[0];
|
|
||||||
const f6 = fs[1];
|
|
||||||
dodecahedron.push(f3);
|
|
||||||
const f4 = find_dodeca_next(faces, dodecahedron, f1, f3);
|
|
||||||
dodecahedron.push(f4);
|
|
||||||
const f5 = find_dodeca_next(faces, dodecahedron, f1, f4);
|
|
||||||
dodecahedron.push(f5);
|
|
||||||
dodecahedron.push(f6);
|
|
||||||
|
|
||||||
// get the next ring
|
|
||||||
|
|
||||||
const f7 = find_dodeca_next(faces, dodecahedron, f6, f2);
|
|
||||||
dodecahedron.push(f7);
|
|
||||||
const f8 = find_dodeca_next(faces, dodecahedron, f2, f3);
|
|
||||||
dodecahedron.push(f8);
|
|
||||||
const f9 = find_dodeca_next(faces, dodecahedron, f3, f4);
|
|
||||||
dodecahedron.push(f9);
|
|
||||||
const f10 = find_dodeca_next(faces, dodecahedron, f4, f5);
|
|
||||||
dodecahedron.push(f10);
|
|
||||||
const f11 = find_dodeca_next(faces, dodecahedron, f5, f6);
|
|
||||||
dodecahedron.push(f11);
|
|
||||||
|
|
||||||
// get the last
|
|
||||||
|
|
||||||
const f12 = find_dodeca_next(faces, dodecahedron, f7, f8);
|
|
||||||
dodecahedron.push(f12);
|
|
||||||
|
|
||||||
return dodecahedron;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// for a face, pick an edge, and then find the other two faces which
|
|
||||||
// share this edge. These can be used as the starting points for the
|
|
||||||
// first face's two dodecahedra
|
|
||||||
|
|
||||||
function find_edge_neighbours(faces, face) {
|
|
||||||
const n1 = face.nodes[0];
|
|
||||||
const n2 = face.nodes[1];
|
|
||||||
return faces.filter((f) => f.id !== face.id && f.nodes.includes(n1) && f.nodes.includes(n2));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// each face is in two dodecahedra: this returns them both
|
|
||||||
|
|
||||||
function face_to_dodecahedra(faces, f) {
|
|
||||||
const edge_friends = find_edge_neighbours(faces, f);
|
|
||||||
const d1 = make_dodecahedron(faces, f, edge_friends[0]);
|
|
||||||
const d2 = make_dodecahedron(faces, f, edge_friends[1]);
|
|
||||||
return [ d1, d2 ];
|
|
||||||
}
|
|
||||||
|
|
||||||
// brute-force calculation of all dodecahedra
|
|
||||||
|
|
||||||
function dd_fingerprint(dodecahedron) {
|
|
||||||
const ids = dodecahedron.map((face) => face.id);
|
|
||||||
ids.sort()
|
|
||||||
return ids.join(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function make_120cell_dodecahedra(faces) {
|
|
||||||
const dodecas = [];
|
|
||||||
const seen = {};
|
|
||||||
for( const face of faces ) {
|
|
||||||
const dds = face_to_dodecahedra(faces, face);
|
|
||||||
for( const dd of dds ) {
|
|
||||||
const fp = dd_fingerprint(dd);
|
|
||||||
if( ! (fp in seen) ) {
|
|
||||||
console.log(`added dodeca ${fp}`);
|
|
||||||
dodecas.push(dd);
|
|
||||||
seen[fp] = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dodecas;
|
|
||||||
}
|
|
188
cellindex.js
188
cellindex.js
|
@ -1,188 +0,0 @@
|
||||||
|
|
||||||
export const LAYER_NAMES = {
|
|
||||||
0: "North pole",
|
|
||||||
1: "Arctic circle",
|
|
||||||
2: "North temperate",
|
|
||||||
3: "Tropic of Cancer",
|
|
||||||
4: "Equator",
|
|
||||||
5: "Tropic of Capricorn",
|
|
||||||
6: "South temperate",
|
|
||||||
7: "Antarctic circle (all)"
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const INDEX120 = {
|
|
||||||
"1": [
|
|
||||||
27,38,48,49,61,68,74,87,95,98,105,120, 126,131,140,149,156,165,174,
|
|
||||||
179,185,200,207,210,218,223,226,231,234,239,241,248,252,253,258,263,
|
|
||||||
265,272,274,279,284,285,289,296,300,301,306,311,313,320,324,325,331,
|
|
||||||
334,339,342,347,350,356,357,362,367,369,376,378,383,388,389,393,400,
|
|
||||||
403,406,413,414,419,420,425,427,438,440,444,448,449,453,458,460,469,
|
|
||||||
471,473,474,487,488,490,494,499,503,511,512,513,514,525,527,530,532,
|
|
||||||
539,543,546,550,555,558,563,566,572,573,580,581,585,592,593,600
|
|
||||||
],
|
|
||||||
"2":[
|
|
||||||
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,28,
|
|
||||||
30,31,34,35,37,40,41,44,46,47,50,51,53,56,57,60,62,63,66,67,69,72,73,
|
|
||||||
76,78,79,82,83,85,88,90,91,93,96,97,100,102,103,106,107,109,112,113,
|
|
||||||
116,118,119,122,123,125,128,129,132,134,135,138,139,141,144,145,148,
|
|
||||||
150,151,154,155,157,160,161,164,166,167,170,171,173,176,177,180,182,
|
|
||||||
183,186,187,189,192,193,196,198,199,202,203,205,208,209,212,214,215
|
|
||||||
],
|
|
||||||
"3":[
|
|
||||||
26,39,45,52,64,65,75,86,94,99,108,117,127,130,137,152,153,168,175,
|
|
||||||
178,188,197,206,211,219,222,227,230,235,238,244,245,251,254,257,264,
|
|
||||||
268,269,273,280,283,286,292,293,299,302,305,312,316,317,321,328,330,
|
|
||||||
335,338,343,348,349,355,358,363,366,370,375,377,384,385,392,394,399,
|
|
||||||
404,405,415,416,417,418,426,428,437,439,441,445,452,456,457,459,470,
|
|
||||||
472,475,476,485,486,491,495,498,502,509,510,515,516,526,528,529,531,
|
|
||||||
538,542,547,551,554,559,562,567,569,576,577,584,588,589,596,597
|
|
||||||
],
|
|
||||||
"4":[
|
|
||||||
32,33,43,54,58,71,77,84,92,101,110,115,121,136,143,146,159,162,169,
|
|
||||||
184,190,195,204,213,220,221,228,229,236,237,242,247,249,256,260,261,
|
|
||||||
266,271,276,277,281,288,290,295,297,304,308,309,315,318,322,327,329,
|
|
||||||
336,340,341,346,351,354,359,361,368,371,374,379,382,387,390,396,397,
|
|
||||||
401,408,409,410,423,424,430,432,433,435,443,447,450,454,461,463,466,
|
|
||||||
468,477,478,483,484,489,493,500,504,507,508,517,518,522,524,533,535,
|
|
||||||
540,544,545,549,553,560,561,568,570,575,578,583,587,590,595,598
|
|
||||||
],
|
|
||||||
"5":[
|
|
||||||
29,36,42,55,59,70,80,81,89,104,111,114,124,133,142,147,158,163,172,
|
|
||||||
181,191,194,201,216,217,224,225,232,233,240,243,246,250,255,259,262,
|
|
||||||
267,270,275,278,282,287,291,294,298,303,307,310,314,319,323,326,332,
|
|
||||||
333,337,344,345,352,353,360,364,365,372,373,380,381,386,391,395,398,
|
|
||||||
402,407,411,412,421,422,429,431,434,436,442,446,451,455,462,464,465,
|
|
||||||
467,479,480,481,482,492,496,497,501,505,506,519,520,521,523,534,536,
|
|
||||||
537,541,548,552,556,557,564,565,571,574,579,582,586,591,594,599
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const LAYERS120 = {
|
|
||||||
"0": [154,266,158,222,218,250,254,162,268,156,160,252,256,166,270,272,
|
|
||||||
164,220,224,168],
|
|
||||||
"1": [2,318,314,30,414,510,362,26,506,410,338,458,462,110,
|
|
||||||
106,6,350,346,34,482,418,474,330,442,450,98,90,14,
|
|
||||||
364,28,508,476,394,570,572,124,122,478,348,32,480,
|
|
||||||
512,396,576,574,126,128,8,352,38,422,486,334,454,
|
|
||||||
446,94,102,514,366,518,342,470,466,114,118,16,368,
|
|
||||||
36,484,516,398,580,578,130,132,412,316,420,332,452,
|
|
||||||
444,92,100,4,320,416,340,464,460,108,112,40,488,424,
|
|
||||||
336,448,456,104,96,520,400,582,584,136,134,344,468,472,120,116],
|
|
||||||
"2":[246,242,42,170,426,50,434,178,226,322,46,430,174,54,182,438,326,
|
|
||||||
230,78,558,526,62,494,542,590,142,74,554,386,300,298,202,290,206,
|
|
||||||
522,58,538,490,138,586,236,234,186,60,540,188,282,378,66,546,194,
|
|
||||||
68,196,548,382,286,498,82,562,530,146,594,76,556,204,80,208,560,
|
|
||||||
388,292,588,44,428,492,140,524,64,544,380,238,190,240,192,284,
|
|
||||||
592,496,432,48,528,144,70,198,550,72,552,200,288,384,502,534,566,
|
|
||||||
86,598,150,390,302,210,304,214,294,84,212,564,88,568,216,296,392,
|
|
||||||
596,500,436,52,532,148,324,244,172,248,180,228,176,56,440,184,232,
|
|
||||||
328,504,536,152,600],
|
|
||||||
"3":[369,273,275,371,17,353,257,261,357,9,401,305,306,402,21,307,308,
|
|
||||||
403,22,404,259,263,355,11,359,277,279,373,19,375,309,310,405,23,
|
|
||||||
406,258,262,354,10,358,274,276,370,18,372,356,260,264,360,12,407,
|
|
||||||
311,312,408,24,374,278,280,376,20],"4":[229,325,173,45,429,493,
|
|
||||||
141,525,589,289,385,557,77,205,201,137,521,553,73,585,285,381,
|
|
||||||
193,65,545,593,145,497,529,225,321,433,49,177,139,587,491,281,377,
|
|
||||||
539,59,187,283,379,189,61,541,197,287,199,237,239,191,293,389,213,
|
|
||||||
149,533,565,85,597,295,391,211,147,531,563,83,595,227,323,171,43,
|
|
||||||
427,523,151,503,535,231,327,439,55,183,599,383,551,71,87,215,567,
|
|
||||||
169,241,245,181,489,425,41,185,233,235,195,537,57,591,543,63,495,
|
|
||||||
143,549,69,501,179,499,435,51,175,431,47,527,203,291,387,555,75,
|
|
||||||
207,299,297,559,79,437,53,209,561,81,301,303,547,67,243,247],
|
|
||||||
"5":[457,337,461,105,109,469,341,465,117,113,89,97,441,449,329,93,445,
|
|
||||||
101,453,333,573,395,125,127,575,121,569,393,123,571,451,331,443,
|
|
||||||
99,91,129,577,131,579,397,107,459,111,339,463,95,447,335,103,455,
|
|
||||||
133,135,581,583,399,115,119,467,471,343
|
|
||||||
],
|
|
||||||
"6":[1,417,313,409,413,317,421,5,475,345,473,481,349,483,33,513,507,
|
|
||||||
13,363,511,7,487,351,485,37,517,15,519,367,515,3,423,319,415,39,
|
|
||||||
25,505,509,477,29,411,27,479,347,31,365,419,35,315,361
|
|
||||||
],
|
|
||||||
"7":[217,153,221,157,265,161,165,269,249,253,251,255,267,159,155,
|
|
||||||
163,219,271,223,167]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Schoute's partition via https://arxiv.org/abs/1010.4353
|
|
||||||
|
|
||||||
export const PARTITION600 = {
|
|
||||||
|
|
||||||
"2,0,0,0": 1,
|
|
||||||
"0,2,0,0": 1,
|
|
||||||
"0,0,2,0": 1,
|
|
||||||
"0,0,0,2": 1,
|
|
||||||
"1,1,1,1": 1,
|
|
||||||
"1,1,-1,-1": 1,
|
|
||||||
"1,-1,1,-1": 1,
|
|
||||||
"1,-1,-1,1": 1,
|
|
||||||
"1,-1,-1,-1": 1,
|
|
||||||
"1,-1,1,1": 1,
|
|
||||||
"1,1,-1,1": 1,
|
|
||||||
"1,1,1,-1": 1,
|
|
||||||
|
|
||||||
"k,0,-t,-1": 2,
|
|
||||||
"0,k,1,-t": 2,
|
|
||||||
"t,-1,k,0": 2,
|
|
||||||
"1,t,0,k": 2,
|
|
||||||
"t,k,0,-1": 2,
|
|
||||||
"1,0,k,t": 2,
|
|
||||||
"k,-t,-1,0": 2,
|
|
||||||
"0,1,-t,k": 2,
|
|
||||||
"1,k,t,0": 2,
|
|
||||||
"t,0,-1,k": 2,
|
|
||||||
"0,t,-k,-1": 2,
|
|
||||||
"k,-1,0,-t": 2,
|
|
||||||
|
|
||||||
"t,0,1,k": 3,
|
|
||||||
"0,t,-k,1": 3,
|
|
||||||
"1,-k,-t,0": 3,
|
|
||||||
"k,1,0,-t": 3,
|
|
||||||
"0,k,1,t": 3,
|
|
||||||
"t,1,-k,0": 3,
|
|
||||||
"k,0,t,-1": 3,
|
|
||||||
"1,-t,0,k": 3,
|
|
||||||
"t,-k,0,-1": 3,
|
|
||||||
"0,1,-t,-k": 3,
|
|
||||||
"1,0,-k,t": 3,
|
|
||||||
"k,t,1,0": 3,
|
|
||||||
|
|
||||||
"t,0,-1,-k": 4,
|
|
||||||
"0,t,k,-1": 4,
|
|
||||||
"1,-k,t,0": 4,
|
|
||||||
"k,1,0,t": 4,
|
|
||||||
"t,1,k,0": 4,
|
|
||||||
"0,k,-1,-t": 4,
|
|
||||||
"1,-t,0,-k": 4,
|
|
||||||
"k,0,-t,1": 4,
|
|
||||||
"0,1,t,k": 4,
|
|
||||||
"t,-k,0,1": 4,
|
|
||||||
"k,t,-1,0": 4,
|
|
||||||
"1,0,k,-t": 4,
|
|
||||||
|
|
||||||
"k,0,t,1": 5,
|
|
||||||
"0,k,-1,t": 5,
|
|
||||||
"t,-1,-k,0": 5,
|
|
||||||
"1,t,0,-k": 5,
|
|
||||||
"1,0,-k,-t": 5,
|
|
||||||
"t,k,0,1": 5,
|
|
||||||
"0,1,t,-k": 5,
|
|
||||||
"k,-t,1,0": 5,
|
|
||||||
"t,0,1,-k": 5,
|
|
||||||
"1,k,-t,0": 5,
|
|
||||||
"k,-1,0,t": 5,
|
|
||||||
"0,t,k,1": 5
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const LAYERS600 = {
|
|
||||||
"0":[2,42,46,50,54,106,110,44,48,52,56,108,112],
|
|
||||||
"1":[10,12,14,16,18,20,22,24,34,36,38,40,98,102,100,104,90,92,94,96],
|
|
||||||
"2":[26,58,74,28,76,30,60,32,62,78,80,64],
|
|
||||||
"3":[3,4,5,6,7,8,65,113,81,66,114,82,67,115,83,68,116,84,69,117,85,70,118,86,71,119,87,72,120,88],
|
|
||||||
"4":[25,29,27,31,57,61,59,63,73,75,77,79],
|
|
||||||
"5":[97,101,99,103,33,35,37,39,89,91,93,95,9,11,13,15,17,19,21,23],
|
|
||||||
"6":[41,105,49,43,51,45,107,47,109,53,55,111],
|
|
||||||
"7":[1]
|
|
||||||
};
|
|
37
colours.js
37
colours.js
|
@ -1,37 +0,0 @@
|
||||||
import ColorScheme from 'color-scheme';
|
|
||||||
import Color from 'color';
|
|
||||||
|
|
||||||
export const get_colours = (basis) => {
|
|
||||||
const basis_c = Color(basis);
|
|
||||||
const hslb = basis_c.hsl();
|
|
||||||
const hue = hslb['color'][0];
|
|
||||||
const saturation = hslb['color'][1];
|
|
||||||
const luminance = hslb['color'][2];
|
|
||||||
const scheme = new ColorScheme;
|
|
||||||
scheme.from_hue(hue).scheme("tetrade").distance(0.75);
|
|
||||||
const colours = scheme.colors().slice(1, 9);
|
|
||||||
colours.reverse();
|
|
||||||
const hsl = colours.map((c) => Color("#" + c).hsl());
|
|
||||||
const resaturated = hsl.map((hslc) => hslc.saturationl(saturation).rgbNumber());
|
|
||||||
resaturated.unshift(basis);
|
|
||||||
return resaturated;
|
|
||||||
}
|
|
||||||
|
|
||||||
// basic colours where 0 = blue
|
|
||||||
// 1 - dark blue
|
|
||||||
// 2 - white
|
|
||||||
// 3 - light cyan
|
|
||||||
// 4 - light orange
|
|
||||||
// 5 - dark orange
|
|
||||||
|
|
||||||
export const get_plain_colours = (basis) => {
|
|
||||||
return [
|
|
||||||
basis,
|
|
||||||
0xffffff,
|
|
||||||
0x00ff00,
|
|
||||||
0xff0000,
|
|
||||||
0x0000ff,
|
|
||||||
0xff9900,
|
|
||||||
0x000000,
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
# Indexing manually ideas
|
|
||||||
|
|
||||||
- a list of all the faces (and maybe dodecahedra) which lets me assign
|
|
||||||
labels to them and lights up the interface
|
|
||||||
|
|
||||||
- where faces / vertices are repeated in the table, assigning a vertex in one
|
|
||||||
spot changes it everywhere else
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
separately - take the links generated by this codebase and apply them to
|
|
||||||
the d3.js forcemap code I wrote for the 24-cell
|
|
|
@ -1,244 +0,0 @@
|
||||||
|
|
||||||
Steps forward -
|
|
||||||
|
|
||||||
1. algorithm which, given a face, finds the two dodecahedra it belongs to
|
|
||||||
|
|
||||||
2. using this, generate a list of all 120 dodecahedra:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
1.
|
|
||||||
|
|
||||||
For a face: there are five edges, and ten other faces sharing an edge.
|
|
||||||
|
|
||||||
These edges are in two sets: one for each dodecahedron. The sets are defined
|
|
||||||
by them sharing vertices which aren't in the first face.
|
|
||||||
|
|
||||||
Go around a set of five, by pairs: for each pair, find the other neighbour -
|
|
||||||
|
|
||||||
this gives the next five faces.
|
|
||||||
|
|
||||||
There's only one face left, which is defined by the shared other vertices of
|
|
||||||
the last five.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
2. have tried manual labelling and it will drive me crazy before I finish
|
|
||||||
|
|
||||||
3. Automated approach based on what I've got so far:
|
|
||||||
|
|
||||||
- should be possible to colour a single dodecahedron from a single face and
|
|
||||||
one other vertex (to pick a chirality)
|
|
||||||
|
|
||||||
- write a function to do this - the compound-of-four-tetrahedra map can be
|
|
||||||
more or less hard coded: follow the pattern 1-2-3-4-5, 3-4-5-1-2, etc out
|
|
||||||
from the inner ring, and map the original face's permutation
|
|
||||||
|
|
||||||
From this:
|
|
||||||
|
|
||||||
- colour the first dodecahedron, picking a chirality
|
|
||||||
|
|
||||||
- the next vertices from each of this dodecahedron's vertices have colours
|
|
||||||
which come from the first dodeca
|
|
||||||
|
|
||||||
- these can be used to colour the next layer of dodecahedra
|
|
||||||
|
|
||||||
- and so on
|
|
||||||
|
|
||||||
Alternatively:
|
|
||||||
|
|
||||||
- do it by the discrete Hopf fibration, one fibre at a time
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// old shit below that didn't work VVVV
|
|
||||||
|
|
||||||
|
|
||||||
Chords: 1.74806 - the 120-cell has 7200 chords of this length
|
|
||||||
|
|
||||||
Looking for a way to partition the 600 vertices of the 120 cell into five
|
|
||||||
disjoint 600-cells, each of which has 120 vertices.
|
|
||||||
|
|
||||||
(there are 10 such 600-cells so two ways to do the partition I guess)
|
|
||||||
|
|
||||||
a 600-cell has 720 edges! optimistically this means that each chord in the
|
|
||||||
collection of 7200 belongs to one and only one of the 600-cells.
|
|
||||||
|
|
||||||
|
|
||||||
the way forward:
|
|
||||||
|
|
||||||
I need to take the 7200 chords (pairs of nodes) and divide them into sets
|
|
||||||
which are connected to one another - with any luck, each of these will be
|
|
||||||
one of the 10 600-cells
|
|
||||||
|
|
||||||
Then need to sort these 10 sets of 120 vertices into the two sets of 5
|
|
||||||
|
|
||||||
|
|
||||||
collate chords by node
|
|
||||||
|
|
||||||
Each 120-cell vertex has 24 of the chord3s from it - as a 600-cell has 12
|
|
||||||
edges to each vertex, this suggests that each 120-vertex belongs to two
|
|
||||||
600-cells with a disjoint set of vertices
|
|
||||||
|
|
||||||
Next algorithm - gather each 600-cell
|
|
||||||
|
|
||||||
use the chords as the basis for this.
|
|
||||||
|
|
||||||
n1 -> 24 chords -> add these 24 neighbours
|
|
||||||
|
|
||||||
bad luck - traversing chord3s from the first vertex reaches all 600 vertices-
|
|
||||||
which isn't suprising as the two 5 disjoint sets overlap. Sigh.
|
|
||||||
|
|
||||||
Use the angles between the chords? seems a bit complex
|
|
||||||
|
|
||||||
Get the angles from the 600-cell model. Use these to separate out the sets of
|
|
||||||
24 chords from a point on the 120-cell.
|
|
||||||
|
|
||||||
Notes from dinner:
|
|
||||||
|
|
||||||
- all of the 60-degree angles are chords joining the vertices of the tetrahedra
|
|
||||||
- there should be two sets of these
|
|
||||||
|
|
||||||
for eg - this works for the chords from 1!
|
|
||||||
|
|
||||||
[ 25, 41 ],
|
|
||||||
[ 25, 97 ],
|
|
||||||
[ 25, 109 ],
|
|
||||||
[ 25, 157 ],
|
|
||||||
[ 25, 161 ],
|
|
||||||
[ 41, 97 ],
|
|
||||||
[ 41, 109 ],
|
|
||||||
[ 41, 173 ],
|
|
||||||
[ 41, 177 ],
|
|
||||||
[ 97, 113 ],
|
|
||||||
[ 97, 161 ],
|
|
||||||
[ 97, 177 ],
|
|
||||||
[ 37, 53 ],
|
|
||||||
[ 37, 93 ],
|
|
||||||
[ 37, 113 ],
|
|
||||||
[ 37, 157 ],
|
|
||||||
[ 37, 161 ],
|
|
||||||
[ 53, 93 ],
|
|
||||||
[ 53, 113 ],
|
|
||||||
[ 53, 173 ],
|
|
||||||
[ 53, 177 ],
|
|
||||||
[ 173, 177 ]
|
|
||||||
[ 93, 109 ],
|
|
||||||
[ 93, 157 ],
|
|
||||||
[ 93, 173 ],
|
|
||||||
[ 109, 157 ],
|
|
||||||
[ 109, 173 ],
|
|
||||||
[ 113, 161 ],
|
|
||||||
[ 113, 177 ],
|
|
||||||
[ 157, 161 ],
|
|
||||||
|
|
||||||
|
|
||||||
[ 29, 45 ], 5
|
|
||||||
[ 29, 101 ], 101
|
|
||||||
[ 29, 105 ], 105
|
|
||||||
[ 29, 153 ], 153
|
|
||||||
[ 29, 165 ], 165
|
|
||||||
[ 45, 101 ],
|
|
||||||
[ 45, 105 ],
|
|
||||||
[ 45, 169 ], 169
|
|
||||||
[ 45, 181 ], 181
|
|
||||||
[ 101, 117 ], 117
|
|
||||||
[ 101, 165 ],
|
|
||||||
[ 101, 181 ],
|
|
||||||
[ 105, 153 ],
|
|
||||||
[ 105, 169 ],
|
|
||||||
[ 33, 49 ], 33 49
|
|
||||||
[ 33, 89 ], 89
|
|
||||||
[ 33, 117 ],
|
|
||||||
[ 33, 153 ],
|
|
||||||
[ 33, 165 ],
|
|
||||||
[ 49, 89 ],
|
|
||||||
[ 49, 117 ],
|
|
||||||
[ 49, 169 ],
|
|
||||||
[ 49, 181 ],
|
|
||||||
[ 169, 181 ],
|
|
||||||
[ 89, 105 ],
|
|
||||||
[ 89, 153 ],
|
|
||||||
[ 89, 169 ],
|
|
||||||
[ 117, 165 ],
|
|
||||||
[ 117, 181 ],
|
|
||||||
[ 153, 165 ],
|
|
||||||
|
|
||||||
|
|
||||||
So each of these is one of the two icosahedral pyramids from node 1.
|
|
||||||
|
|
||||||
Doing this manually for the rest of the partition is possible, but could it
|
|
||||||
be automated based on angles?
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Plan for Sunday:
|
|
||||||
|
|
||||||
* use the existing label_subgraph to make a function which partitions the
|
|
||||||
60-angle chords into two groups (like I did manually above)
|
|
||||||
|
|
||||||
// this is done and seems to work
|
|
||||||
|
|
||||||
* test this labelling manually (ie colour one set of 60-angle vertices)
|
|
||||||
|
|
||||||
// done this with the manual labels and it looks good
|
|
||||||
|
|
||||||
* make another labeling routine which can fill out the rest of the 600-cell
|
|
||||||
from the starting dodecahedron, by only following chords which are at 60
|
|
||||||
to the entering chord
|
|
||||||
|
|
||||||
Then the big algorithm does the following:
|
|
||||||
|
|
||||||
- start from node 1, find 60-angles, pick one partition at random, label that 600-cell
|
|
||||||
|
|
||||||
- find the next unlabelled node
|
|
||||||
|
|
||||||
- find 60-angles, partition them, pick a partition with no unlabelled cells and label that 600-cell
|
|
||||||
|
|
||||||
- repeat the previous step for the remaining three 600-cells
|
|
||||||
|
|
||||||
Alternative, more manual option: just write the second labelling routine and
|
|
||||||
do the rest by hand
|
|
||||||
|
|
||||||
|
|
||||||
[ 25, 41 ],
|
|
||||||
[ 25, 97 ],
|
|
||||||
[ 25, 109 ],
|
|
||||||
[ 25, 157 ],
|
|
||||||
[ 25, 161 ],
|
|
||||||
[ 41, 97 ],
|
|
||||||
[ 41, 109 ],
|
|
||||||
[ 41, 173 ],
|
|
||||||
[ 41, 177 ],
|
|
||||||
[ 97, 113 ],
|
|
||||||
[ 97, 161 ],
|
|
||||||
[ 97, 177 ],
|
|
||||||
[ 37, 53 ],
|
|
||||||
[ 37, 93 ],
|
|
||||||
[ 37, 113 ],
|
|
||||||
[ 37, 157 ],
|
|
||||||
[ 37, 161 ],
|
|
||||||
[ 53, 93 ],
|
|
||||||
[ 53, 113 ],
|
|
||||||
[ 53, 173 ],
|
|
||||||
[ 53, 177 ],
|
|
||||||
[ 173, 177 ]
|
|
||||||
[ 93, 109 ],
|
|
||||||
[ 93, 157 ],
|
|
||||||
[ 93, 173 ],
|
|
||||||
[ 109, 157 ],
|
|
||||||
[ 109, 173 ],
|
|
||||||
[ 113, 161 ],
|
|
||||||
[ 113, 177 ],
|
|
||||||
[ 157, 161 ],
|
|
||||||
25 41 97 109
|
|
||||||
157 161 173 177
|
|
||||||
113 37 53 93
|
|
||||||
|
|
||||||
|
|
||||||
Another idea - look at colouring the vertices of a dodecahedron according to
|
|
||||||
the four compound tetrahedra and see if this can be repeated automatically to
|
|
||||||
neighbouring cells
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.6 MiB |
Binary file not shown.
Before Width: | Height: | Size: 565 KiB After Width: | Height: | Size: 670 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.9 MiB |
135
fourDShape.js
135
fourDShape.js
|
@ -1,24 +1,23 @@
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
|
||||||
const HYPERPLANE = 2.0;
|
const HYPERPLANE = 2;
|
||||||
const W_FORESHORTENING = 0.04;
|
|
||||||
|
const NODE_SIZE = 0.01;
|
||||||
|
const LINK_SIZE = 0.01;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class FourDShape extends THREE.Group {
|
class FourDShape extends THREE.Group {
|
||||||
|
|
||||||
constructor(node_ms, link_ms, face_ms, structure) {
|
constructor(node_ms, link_ms, structure) {
|
||||||
super();
|
super();
|
||||||
this.node_ms = node_ms;
|
this.node_ms = node_ms;
|
||||||
this.link_ms = link_ms;
|
this.link_ms = link_ms;
|
||||||
this.face_ms = face_ms;
|
|
||||||
this.nodes4 = structure.nodes;
|
this.nodes4 = structure.nodes;
|
||||||
this.nodes3 = {};
|
this.nodes3 = {};
|
||||||
this.links = structure.links;
|
this.links = structure.links;
|
||||||
this.faces = ( "faces" in structure ) ? structure.faces : [];
|
|
||||||
this.node_scale = 1;
|
|
||||||
this.link_scale = 1;
|
|
||||||
this.hyperplane = HYPERPLANE;
|
this.hyperplane = HYPERPLANE;
|
||||||
this.foreshortening = W_FORESHORTENING;
|
|
||||||
this.initShapes();
|
this.initShapes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,8 +33,8 @@ class FourDShape extends THREE.Group {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
makeNode(material, v3, scale) {
|
makeNode(material, v3) {
|
||||||
const geometry = new THREE.SphereGeometry(this.node_size);
|
const geometry = new THREE.SphereGeometry(NODE_SIZE);
|
||||||
const sphere = new THREE.Mesh(geometry, material);
|
const sphere = new THREE.Mesh(geometry, material);
|
||||||
sphere.position.copy(v3);
|
sphere.position.copy(v3);
|
||||||
this.add(sphere);
|
this.add(sphere);
|
||||||
|
@ -43,145 +42,75 @@ class FourDShape extends THREE.Group {
|
||||||
}
|
}
|
||||||
|
|
||||||
makeLink(material, link) {
|
makeLink(material, link) {
|
||||||
const n1 = this.nodes3[link.source];
|
const n1 = this.nodes3[link.source].v3;
|
||||||
const n2 = this.nodes3[link.target];
|
const n2 = this.nodes3[link.target].v3;
|
||||||
const s1 = n1.scale;
|
const length = n1.distanceTo(n2);
|
||||||
const s2 = n2.scale;
|
|
||||||
const length = n1.v3.distanceTo(n2.v3);
|
|
||||||
const centre = new THREE.Vector3();
|
const centre = new THREE.Vector3();
|
||||||
centre.lerpVectors(n1.v3, n2.v3, 0.5);
|
centre.lerpVectors(n1, n2, 0.5);
|
||||||
const geometry = new THREE.CylinderGeometry(
|
const geometry = new THREE.CylinderGeometry(LINK_SIZE, LINK_SIZE, 1);
|
||||||
this.link_scale * s2, this.link_scale * s1, 1,
|
|
||||||
16, 1, true
|
|
||||||
);
|
|
||||||
const cyl = new THREE.Mesh(geometry, material);
|
const cyl = new THREE.Mesh(geometry, material);
|
||||||
const edge = new THREE.Group();
|
const edge = new THREE.Group();
|
||||||
edge.add(cyl);
|
edge.add(cyl);
|
||||||
edge.position.copy(centre);
|
edge.position.copy(centre);
|
||||||
edge.scale.copy(new THREE.Vector3(1, 1, length));
|
edge.scale.copy(new THREE.Vector3(1, 1, length));
|
||||||
edge.lookAt(n2.v3);
|
edge.lookAt(n2);
|
||||||
cyl.rotation.x = Math.PI / 2.0;
|
cyl.rotation.x = Math.PI / 2.0;
|
||||||
this.add(edge);
|
this.add(edge);
|
||||||
return edge;
|
return edge;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLink(link, links_show) {
|
updateLink(link) {
|
||||||
const n1 = this.nodes3[link.source];
|
const n1 = this.nodes3[link.source].v3;
|
||||||
const n2 = this.nodes3[link.target];
|
const n2 = this.nodes3[link.target].v3;
|
||||||
const s1 = n1.scale;
|
const length = n1.distanceTo(n2);
|
||||||
const s2 = n2.scale;
|
|
||||||
const length = n1.v3.distanceTo(n2.v3);
|
|
||||||
const centre = new THREE.Vector3();
|
const centre = new THREE.Vector3();
|
||||||
centre.lerpVectors(n1.v3, n2.v3, 0.5);
|
centre.lerpVectors(n1, n2, 0.5);
|
||||||
// take the average of the ends as the thickness - as a workaround,
|
link.object.scale.copy(new THREE.Vector3(1, 1, length));
|
||||||
// because I haven't worked out how to reshape tapered links without
|
|
||||||
// having to reassign a new geometry to every link
|
|
||||||
const link_mean = this.link_scale * (s1 + s2) * 0.5;
|
|
||||||
link.object.scale.copy(new THREE.Vector3(link_mean, link_mean, length));
|
|
||||||
link.object.position.copy(centre);
|
link.object.position.copy(centre);
|
||||||
link.object.lookAt(n2.v3);
|
link.object.lookAt(n2);
|
||||||
link.object.children[0].rotation.x = Math.PI / 2.0;
|
link.object.children[0].rotation.x = Math.PI / 2.0;
|
||||||
link.object.visible = (!links_show || link.label in links_show);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fourDtoV3(x, y, z, w, rotations) {
|
||||||
setFaceGeometry(face, geometry) {
|
|
||||||
const values = [];
|
|
||||||
for( const f of face.nodes ) {
|
|
||||||
const v3 = this.nodes3[f].v3;
|
|
||||||
values.push(v3.x);
|
|
||||||
values.push(v3.y);
|
|
||||||
values.push(v3.z);
|
|
||||||
}
|
|
||||||
const v3 = this.nodes3[face.nodes[0]].v3;
|
|
||||||
values.push(v3.x);
|
|
||||||
values.push(v3.y);
|
|
||||||
values.push(v3.z);
|
|
||||||
const vertices = new Float32Array(values);
|
|
||||||
geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
makeFace(material, face) {
|
|
||||||
const geometry = new THREE.BufferGeometry();
|
|
||||||
this.setFaceGeometry(face, geometry)
|
|
||||||
const mesh = new THREE.Mesh( geometry, material );
|
|
||||||
this.add(mesh);
|
|
||||||
return mesh;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fourDtoV3_old(x, y, z, w, rotations) {
|
|
||||||
const v4 = new THREE.Vector4(x, y, z, w);
|
const v4 = new THREE.Vector4(x, y, z, w);
|
||||||
for ( const m4 of rotations ) {
|
for ( const m4 of rotations ) {
|
||||||
v4.applyMatrix4(m4);
|
v4.applyMatrix4(m4);
|
||||||
}
|
}
|
||||||
const k = this.fourDscale(v4.w);
|
const k = this.hyperplane / (this.hyperplane + v4.w);
|
||||||
return new THREE.Vector3(v4.x * k, v4.y * k, v4.z * k);
|
return new THREE.Vector3(v4.x * k, v4.y * k, v4.z * k);
|
||||||
}
|
}
|
||||||
|
|
||||||
fourDscale(w) {
|
|
||||||
return this.hyperplane / ( this.hyperplane + w );
|
|
||||||
}
|
|
||||||
|
|
||||||
fourDrotate(x, y, z, w, rotations) {
|
|
||||||
const v4 = new THREE.Vector4(x, y, z, w);
|
|
||||||
for ( const m4 of rotations ) {
|
|
||||||
v4.applyMatrix4(m4);
|
|
||||||
}
|
|
||||||
return v4;
|
|
||||||
}
|
|
||||||
|
|
||||||
fourDtoV3(v4) {
|
|
||||||
const k = this.fourDscale(v4.w);
|
|
||||||
return new THREE.Vector3(v4.x * k, v4.y * k, v4.z * k);
|
|
||||||
}
|
|
||||||
|
|
||||||
initShapes() {
|
initShapes() {
|
||||||
for( const n of this.nodes4 ) {
|
for( const n of this.nodes4 ) {
|
||||||
const k = this.fourDscale(n.w);
|
const v3 = this.fourDtoV3(n.x, n.y, n.z, n.w, []);
|
||||||
const v3 = new THREE.Vector3(n.x * k, n.y * k, n.z * k);
|
|
||||||
const material = this.getMaterial(n, this.node_ms);
|
const material = this.getMaterial(n, this.node_ms);
|
||||||
this.nodes3[n.id] = {
|
this.nodes3[n.id] = {
|
||||||
v3: v3,
|
v3: v3,
|
||||||
scale: k,
|
object: this.makeNode(material, v3)
|
||||||
label: n.label,
|
|
||||||
object: this.makeNode(material, v3, k)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
for( const l of this.links ) {
|
for( const l of this.links ) {
|
||||||
const material = this.getMaterial(l, this.link_ms);
|
const material = this.getMaterial(l, this.link_ms);
|
||||||
l.object = this.makeLink(material, l);
|
l.object = this.makeLink(material, l);
|
||||||
}
|
}
|
||||||
for( const f of this.faces ) {
|
|
||||||
const material = this.getMaterial(f, this.face_ms);
|
|
||||||
f.object = this.makeFace(material, f);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
render3(rotations, nodes_show, links_show) {
|
render3(rotations) {
|
||||||
this.scalev3 = new THREE.Vector3(this.node_scale, this.node_scale, this.node_scale);
|
|
||||||
for( const n of this.nodes4 ) {
|
for( const n of this.nodes4 ) {
|
||||||
const v4 = this.fourDrotate(n.x, n.y, n.z, n.w, rotations);
|
const v3 = this.fourDtoV3(n.x, n.y, n.z, n.w, rotations);
|
||||||
const k = this.fourDscale(v4.w);
|
|
||||||
const v3 = new THREE.Vector3(v4.x * k, v4.y * k, v4.z * k);
|
|
||||||
const s4 = k * this.node_scale * this.foreshortening;
|
|
||||||
const s3 = new THREE.Vector3(s4, s4, s4);
|
|
||||||
this.nodes3[n.id].v3 = v3;
|
this.nodes3[n.id].v3 = v3;
|
||||||
this.nodes3[n.id].scale = k * this.foreshortening;
|
|
||||||
this.nodes3[n.id].object.position.copy(v3);
|
this.nodes3[n.id].object.position.copy(v3);
|
||||||
this.nodes3[n.id].object.scale.copy(s3);
|
// could do scaling here
|
||||||
this.nodes3[n.id].object.visible = ( !nodes_show || n.label in nodes_show );
|
|
||||||
}
|
|
||||||
for( const l of this.links ) {
|
|
||||||
this.updateLink(l, links_show);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for( const f of this.faces ) {
|
for( const l of this.links ) {
|
||||||
this.setFaceGeometry(f, f.object.geometry);
|
this.updateLink(l);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { FourDShape };
|
export { FourDShape };
|
220
gui.js
220
gui.js
|
@ -1,220 +0,0 @@
|
||||||
import { GUI } from 'lil-gui';
|
|
||||||
|
|
||||||
|
|
||||||
const DEFAULTS = {
|
|
||||||
nodesize: 0.25,
|
|
||||||
nodeopacity: 1,
|
|
||||||
linksize: 0.2,
|
|
||||||
linkopacity: 0.75,
|
|
||||||
link2opacity: 0.75,
|
|
||||||
shape: '120-cell',
|
|
||||||
option: 'none',
|
|
||||||
visibility: 5,
|
|
||||||
inscribed: false,
|
|
||||||
inscribe_all: false,
|
|
||||||
color: 0x3293a9,
|
|
||||||
background: 0xd4d4d4,
|
|
||||||
hyperplane: 0.93,
|
|
||||||
zoom: 1,
|
|
||||||
xRotate: 'YW',
|
|
||||||
yRotate: 'XW',
|
|
||||||
dtheta: 0,
|
|
||||||
damping: false,
|
|
||||||
captions: true,
|
|
||||||
dpsi: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class FourDGUI {
|
|
||||||
|
|
||||||
constructor(shapes, changeShape, setColor, setBackground, setNodeOpacity,setLinkOpacity, setVisibility, showDocs) {
|
|
||||||
this.gui = new GUI();
|
|
||||||
const SHAPE_NAMES = shapes.map((s) => s.name);
|
|
||||||
|
|
||||||
this.parseLinkParams();
|
|
||||||
const guiObj = this;
|
|
||||||
this.params = {
|
|
||||||
shape: this.link['shape'],
|
|
||||||
option: this.link['option'],
|
|
||||||
inscribed: this.link['inscribed'],
|
|
||||||
inscribe_all: this.link['inscribe_all'],
|
|
||||||
linksize: this.link['linksize'],
|
|
||||||
linkopacity: this.link['linkopacity'],
|
|
||||||
link2opacity: this.link['linkopacity'],
|
|
||||||
nodesize: this.link['nodesize'],
|
|
||||||
nodeopacity: this.link['nodeopacity'],
|
|
||||||
depth: this.link['depth'],
|
|
||||||
color: this.link['color'],
|
|
||||||
background: this.link['background'],
|
|
||||||
hyperplane: this.link['hyperplane'],
|
|
||||||
zoom: this.link['zoom'],
|
|
||||||
xRotate: this.link['xRotate'],
|
|
||||||
yRotate: this.link['yRotate'],
|
|
||||||
damping: false,
|
|
||||||
captions: true,
|
|
||||||
dtheta: this.link['dtheta'],
|
|
||||||
dpsi: this.link['dpsi'],
|
|
||||||
"copy link": function () { guiObj.copyUrl() }
|
|
||||||
};
|
|
||||||
let options_ctrl;
|
|
||||||
this.gui.add(this.params, 'shape', SHAPE_NAMES).onChange((shape) => {
|
|
||||||
const options = this.getShapeOptions(shapes, shape);
|
|
||||||
options_ctrl = options_ctrl.options(options).onChange((option) => {
|
|
||||||
setVisibility(option)
|
|
||||||
});
|
|
||||||
options_ctrl.setValue(options[0])
|
|
||||||
changeShape(shape)
|
|
||||||
});
|
|
||||||
const options = this.getShapeOptions(shapes, this.params['shape']);
|
|
||||||
options_ctrl = this.gui.add(this.params, 'option').options(options).onChange((option) => {
|
|
||||||
setVisibility(option)
|
|
||||||
});
|
|
||||||
this.gui.add(this.params, 'hyperplane', 0.5, 1 / 0.8);
|
|
||||||
this.gui.add(this.params, 'zoom', 0.1, 2.0);
|
|
||||||
this.gui.add(this.params, 'nodesize', 0, 1);
|
|
||||||
this.gui.add(this.params, 'nodeopacity', 0, 1).onChange(setNodeOpacity);
|
|
||||||
this.gui.add(this.params, 'linksize', 0, 1);
|
|
||||||
this.gui.add(this.params, 'linkopacity', 0, 1).onChange(
|
|
||||||
(v) => setLinkOpacity(v, true)
|
|
||||||
);
|
|
||||||
this.gui.add(this.params, 'link2opacity', 0, 1).onChange(
|
|
||||||
(v) => setLinkOpacity(v, false)
|
|
||||||
);
|
|
||||||
this.gui.addColor(this.params, 'color').onChange(setColor);
|
|
||||||
this.gui.addColor(this.params, 'background').onChange(setBackground);
|
|
||||||
this.gui.add(this.params, 'xRotate', [ 'YW', 'YZ', 'ZW' ]);
|
|
||||||
this.gui.add(this.params, 'yRotate', [ 'XZ', 'XY', 'XW' ]);
|
|
||||||
this.gui.add(this.params, 'captions').onChange(showDocs);
|
|
||||||
this.gui.add(this.params, 'damping');
|
|
||||||
this.gui.add(this.params, 'copy link');
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getShapeOptions(shapes, shape) {
|
|
||||||
const spec = shapes.filter((s) => s.name === shape);
|
|
||||||
if( spec && spec[0].options ) {
|
|
||||||
return spec[0].options.map((o) => o.name);
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
numParam(param, parser) {
|
|
||||||
const value = this.urlParams.get(param);
|
|
||||||
if( value ) {
|
|
||||||
const n = parser(value);
|
|
||||||
if( n !== NaN ) {
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return DEFAULTS[param];
|
|
||||||
}
|
|
||||||
|
|
||||||
stringToHex(cstr) {
|
|
||||||
return parseInt('0x' + cstr.substr(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
hexToString(hex) {
|
|
||||||
return '#' + hex.toString(16);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
parseLinkParams() {
|
|
||||||
this.linkUrl = new URL(window.location.toLocaleString());
|
|
||||||
this.link = {};
|
|
||||||
const guiObj = this;
|
|
||||||
|
|
||||||
this.urlParams = this.linkUrl.searchParams;
|
|
||||||
for( const param of [ "shape", "xRotate", "yRotate", "option" ]) {
|
|
||||||
const value = this.urlParams.get(param);
|
|
||||||
if( value ) {
|
|
||||||
this.link[param] = value;
|
|
||||||
} else {
|
|
||||||
this.link[param] = DEFAULTS[param];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for( const param of [ "inscribed", "inscribe_all"] ) {
|
|
||||||
this.link[param] = ( this.urlParams.get(param) === 'y' );
|
|
||||||
}
|
|
||||||
this.link['hyperplane'] = this.numParam('hyperplane', parseFloat);
|
|
||||||
this.link['zoom'] = this.numParam('zoom', parseFloat);
|
|
||||||
this.link['linksize'] = this.numParam('linksize', parseFloat);
|
|
||||||
this.link['linkopacity'] = this.numParam('linkopacity', parseFloat);
|
|
||||||
this.link['link2opacity'] = this.numParam('link2opacity', parseFloat);
|
|
||||||
this.link['nodesize'] = this.numParam('nodesize', parseFloat);
|
|
||||||
this.link['nodeopacity'] = this.numParam('nodeopacity', parseFloat);
|
|
||||||
this.link['color'] = this.numParam('color', (s) => guiObj.stringToHex(s));
|
|
||||||
this.link['background'] = this.numParam('background', (s) => guiObj.stringToHex(s));
|
|
||||||
this.link['dpsi'] = this.numParam('dpsi', parseFloat);
|
|
||||||
this.link['dtheta'] = this.numParam('dtheta', parseFloat);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
copyUrl() {
|
|
||||||
const url = new URL(this.linkUrl.origin + this.linkUrl.pathname);
|
|
||||||
url.searchParams.append("shape", this.params.shape);
|
|
||||||
url.searchParams.append("option", this.params.option);
|
|
||||||
url.searchParams.append("inscribed", this.params.inscribed ? 'y': 'n');
|
|
||||||
url.searchParams.append("inscribe_all", this.params.inscribe_all ? 'y': 'n');
|
|
||||||
url.searchParams.append("linksize", this.params.linksize.toString());
|
|
||||||
url.searchParams.append("nodesize", this.params.nodesize.toString());
|
|
||||||
url.searchParams.append("nodeopacity", this.params.nodesize.toString());
|
|
||||||
url.searchParams.append("linkopacity", this.params.nodeopacity.toString());
|
|
||||||
url.searchParams.append("link2opacity", this.params.link2opacity.toString());
|
|
||||||
url.searchParams.append("color", this.hexToString(this.params.color));
|
|
||||||
url.searchParams.append("background", this.hexToString(this.params.background));
|
|
||||||
url.searchParams.append("hyperplane", this.params.hyperplane.toString());
|
|
||||||
url.searchParams.append("zoom", this.params.zoom.toString());
|
|
||||||
url.searchParams.append("xRotate", this.params.xRotate);
|
|
||||||
url.searchParams.append("yRotate", this.params.yRotate);
|
|
||||||
url.searchParams.append("dtheta", this.params.dtheta.toString());
|
|
||||||
url.searchParams.append("dpsi", this.params.dpsi.toString());
|
|
||||||
this.copyTextToClipboard(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
copyTextToClipboard(text) {
|
|
||||||
if (!navigator.clipboard) {
|
|
||||||
this.fallbackCopyTextToClipboard(text);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
navigator.clipboard.writeText(text).then(function() {
|
|
||||||
console.log('Async: Copying to clipboard was successful!');
|
|
||||||
}, function(err) {
|
|
||||||
console.error('Async: Could not copy text: ', err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fallbackCopyTextToClipboard(text) {
|
|
||||||
var textArea = document.createElement("textarea");
|
|
||||||
textArea.value = text;
|
|
||||||
|
|
||||||
// Avoid scrolling to bottom
|
|
||||||
textArea.style.top = "0";
|
|
||||||
textArea.style.left = "0";
|
|
||||||
textArea.style.position = "fixed";
|
|
||||||
|
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.focus();
|
|
||||||
textArea.select();
|
|
||||||
|
|
||||||
try {
|
|
||||||
var successful = document.execCommand('copy');
|
|
||||||
var msg = successful ? 'successful' : 'unsuccessful';
|
|
||||||
console.log('Fallback: Copying text command was ' + msg);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Fallback: Oops, unable to copy', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.removeChild(textArea);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export { FourDGUI, DEFAULTS };
|
|
19
index.html
19
index.html
|
@ -5,28 +5,9 @@
|
||||||
<title>FourD</title>
|
<title>FourD</title>
|
||||||
<style>
|
<style>
|
||||||
body { margin: 0; }
|
body { margin: 0; }
|
||||||
div#description {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 20%;
|
|
||||||
z-index: 2;
|
|
||||||
font-family: sans-serif;
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
div#info {
|
|
||||||
position: fixed;
|
|
||||||
bottom:0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 2;
|
|
||||||
border:0.5em;
|
|
||||||
font-family: sans-serif }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="module" src="/main.js"></script>
|
<script type="module" src="/main.js"></script>
|
||||||
<div id="description"></div>
|
|
||||||
<div id="info">by <a target="_blank" href="https://mikelynch.org/">Mike Lynch</a> -
|
|
||||||
<a target="_blank" href="https://git.tilde.town/bombinans/fourdjs">source</a></div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -1,294 +0,0 @@
|
||||||
// bad stuff
|
|
||||||
|
|
||||||
function find_chords(chords, n) {
|
|
||||||
return chords.filter((c) => c[0].id === n.id || c[1].id === n.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function find_neighbours(chords, n) {
|
|
||||||
const c = find_chords(chords, n);
|
|
||||||
return c.map((c) => c[0].id === n.id ? c[1] : c[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// for a list of pairs [n1, n2] (these are nodes which share a common angle
|
|
||||||
// from a center), find all the groups of nodes which don't appear in a pair
|
|
||||||
// together
|
|
||||||
|
|
||||||
function partition_nodes(pairs) {
|
|
||||||
let groups = [];
|
|
||||||
const seen = new Set();
|
|
||||||
for( const pair of pairs ) {
|
|
||||||
// both nodes are in a group already
|
|
||||||
if( seen.has(pair[0]) && seen.has(pair[1]) ) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let already = false;
|
|
||||||
// check if either node is already in a group
|
|
||||||
for( const group of groups ) {
|
|
||||||
if( group.has(pair[0]) ) {
|
|
||||||
group.add(pair[1]);
|
|
||||||
seen.add(pair[1]);
|
|
||||||
already = true;
|
|
||||||
continue;
|
|
||||||
} else if( group.has(pair[1]) ) {
|
|
||||||
group.has(pair[0]);
|
|
||||||
seen.has(pair[0]);
|
|
||||||
already = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if neither of the pair was in a former group, start a new group
|
|
||||||
if( !already ) {
|
|
||||||
groups.push(new Set(pair));
|
|
||||||
}
|
|
||||||
// collapse any groups which now have common elements
|
|
||||||
groups = collapse_groups(groups);
|
|
||||||
}
|
|
||||||
return groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
// given a list of groups, if any have common elements, collapse them
|
|
||||||
|
|
||||||
function collapse_groups(groups) {
|
|
||||||
const new_groups = [ ];
|
|
||||||
for( group of groups ) {
|
|
||||||
let collapsed = false;
|
|
||||||
for( new_group of new_groups ) {
|
|
||||||
const i = intersection(group, new_group);
|
|
||||||
if( i.size > 0 ) {
|
|
||||||
for( const e of group ) {
|
|
||||||
new_group.add(e);
|
|
||||||
}
|
|
||||||
collapsed = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if( !collapsed ) {
|
|
||||||
new_groups.push(new Set(group));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new_groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function intersection(s1, s2) {
|
|
||||||
const i = new Set();
|
|
||||||
for( const e of s1 ) {
|
|
||||||
if( s2.has(e) ) {
|
|
||||||
i.add(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
function union(s1, s2) {
|
|
||||||
const u = new Set(s1);
|
|
||||||
for( const e of s2 ) {
|
|
||||||
u.add(e);
|
|
||||||
}
|
|
||||||
return u;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function vector_angle(n1, n2, n3) {
|
|
||||||
const v1 = new THREE.Vector4(n1.x, n1.y, n1.z, n1.w);
|
|
||||||
const v2 = new THREE.Vector4(n2.x, n2.y, n2.z, n2.w);
|
|
||||||
const v3 = new THREE.Vector4(n3.x, n3.y, n3.z, n3.w);
|
|
||||||
v2.sub(v1);
|
|
||||||
v3.sub(v1);
|
|
||||||
const dp = v2.dot(v3);
|
|
||||||
return Math.acos(dp / ( v2.length() * v3.length()));
|
|
||||||
}
|
|
||||||
|
|
||||||
function neighbour_angles_orig(chords, n) {
|
|
||||||
const ns = find_neighbours(chords, n);
|
|
||||||
const angles = {};
|
|
||||||
for( let i = 0; i < ns.length - 1; i++ ) {
|
|
||||||
for( let j = i + 1; j < ns.length; j++ ) {
|
|
||||||
const n2 = ns[i];
|
|
||||||
const n3 = ns[j];
|
|
||||||
const a = THREE.MathUtils.radToDeg(vector_angle(n, n2, n3));
|
|
||||||
const af = (a).toFixed(3);
|
|
||||||
if( ! (af in angles) ) {
|
|
||||||
angles[af] = [];
|
|
||||||
}
|
|
||||||
angles[af].push([n2.id, n3.id]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return angles;
|
|
||||||
}
|
|
||||||
|
|
||||||
function neighbour_angles(chords, n, angle) {
|
|
||||||
const ns = find_neighbours(chords, n);
|
|
||||||
const pairs = [];
|
|
||||||
for( let i = 0; i < ns.length - 1; i++ ) {
|
|
||||||
for( let j = i + 1; j < ns.length; j++ ) {
|
|
||||||
const n2 = ns[i];
|
|
||||||
const n3 = ns[j];
|
|
||||||
const a = THREE.MathUtils.radToDeg(vector_angle(n, n2, n3));
|
|
||||||
const af = (a).toFixed(3);
|
|
||||||
if( af === angle ) {
|
|
||||||
pairs.push([n2.id, n3.id]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pairs;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function make_120_partition(nodes, n) {
|
|
||||||
const chords = find_all_chords(nodes);
|
|
||||||
const chord3 = chords["1.74806"]; // these are edges of the 600-cells;
|
|
||||||
const pairs60 = neighbour_angles(chord3, n, "60.000");
|
|
||||||
const icosas = partition_nodes(pairs60);
|
|
||||||
|
|
||||||
n.label = 1;
|
|
||||||
const angles = icosa_nodes(nodes, icosas[0]);
|
|
||||||
label_120_partition_r(nodes, chord3, 1, n, angles);
|
|
||||||
}
|
|
||||||
|
|
||||||
// recursive function to label a single 600-cell vertex partition of the
|
|
||||||
// 120-cell by following icosahedral nets
|
|
||||||
// this doesn't work! completely - labels only 108-112
|
|
||||||
|
|
||||||
function label_120_partition_r(nodes, chords, label, origin, neighbours) {
|
|
||||||
console.log(`label_120_partition_r ${origin.id}`);
|
|
||||||
console.log(neighbours.map((n) => n.id).join(', '));
|
|
||||||
|
|
||||||
// first try to label everything
|
|
||||||
const unlabelled = [];
|
|
||||||
for( const n of neighbours ) {
|
|
||||||
if( n.label === 0 ) {
|
|
||||||
console.log(`Labelled ${n.id} ${label}`);
|
|
||||||
n.label = label;
|
|
||||||
unlabelled.push(n);
|
|
||||||
} else if( n.label !== label ) {
|
|
||||||
console.log(`node ${n.id} is already in group ${n.label}`);
|
|
||||||
//return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for( const n of unlabelled ) {
|
|
||||||
// the angles represent two icosahedral pyramids - partition them and
|
|
||||||
// pick the one which is at 60 to the edge we arrived on
|
|
||||||
//console.log(`looking for more neighbors for ${n}`);
|
|
||||||
const pairs60 = neighbour_angles(chords, n, "60.000");
|
|
||||||
const icosas = partition_nodes(pairs60);
|
|
||||||
const icosa = choose_icosa(nodes, origin, n, icosas);
|
|
||||||
const icosa_n = icosa_nodes(nodes, icosa);
|
|
||||||
console.log(`recursing to ${nice_icosa(nodes,icosa)}`);
|
|
||||||
return label_120_partition_r(nodes, chords, label, n, icosa_n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// given a pair of icosa-sets, pick the one which is at the right angle to
|
|
||||||
// the incoming vector
|
|
||||||
|
|
||||||
function choose_icosa(nodes, origin, n1, icosas) {
|
|
||||||
for( const icosa of icosas ) {
|
|
||||||
const inodes = icosa_nodes(nodes, icosa);
|
|
||||||
const a60 = inodes.map((ni) => {
|
|
||||||
const a = THREE.MathUtils.radToDeg(vector_angle(n1, origin, ni));
|
|
||||||
return a.toFixed(3);
|
|
||||||
});
|
|
||||||
if( a60.filter((a) => a === "60.000").length > 0 ) {
|
|
||||||
return icosa;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log("No icosa found!");
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function icosa_nodes(nodes, icosa) {
|
|
||||||
return Array.from(icosa).map((nid) => node_by_id(nodes, nid)).sort((a, b) => a.id - b.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function node_by_id(nodes, nid) {
|
|
||||||
const ns = nodes.filter((n) => n.id === nid);
|
|
||||||
return ns[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function enumerate_icosas(nodes) {
|
|
||||||
const chords = find_all_chords(nodes);
|
|
||||||
const chord3 = chords["1.74806"]; // these are edges of the 600-cells;
|
|
||||||
|
|
||||||
for( const n of nodes ) {
|
|
||||||
const pairs60 = neighbour_angles(chord3, n, "60.000");
|
|
||||||
const icosas = partition_nodes(pairs60);
|
|
||||||
for( const icosa of icosas ) {
|
|
||||||
const inodes = icosa_nodes(nodes, icosa);
|
|
||||||
console.log(icosa_to_csv(n.id, inodes).join(','));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function icosa_to_csv(nid, icosa) {
|
|
||||||
const cols = [ nid ];
|
|
||||||
const ia = icosa.map((n) => n.id);
|
|
||||||
for( let i = 1; i < 601; i++ ) {
|
|
||||||
if( ia.includes(i) ) {
|
|
||||||
cols.push(i);
|
|
||||||
} else {
|
|
||||||
cols.push('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cols;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function start_icosas(nodes, chords, origin) {
|
|
||||||
const pairs60 = neighbour_angles(chords, origin, "60.000");
|
|
||||||
return partition_nodes(pairs60).map((i) => nice_icosa(nodes, i));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function next_icosa(nodes, chords, origin, nid) {
|
|
||||||
const n = node_by_id(nodes, nid);
|
|
||||||
const pairs60 = neighbour_angles(chords, n, "60.000");
|
|
||||||
const icosas = partition_nodes(pairs60);
|
|
||||||
const icosa = choose_icosa(nodes, origin, n, icosas);
|
|
||||||
|
|
||||||
return nice_icosa(nodes, icosa);
|
|
||||||
}
|
|
||||||
|
|
||||||
function nice_icosa(nodes, icosa) {
|
|
||||||
return icosa_nodes(nodes, icosa).map((n) => n.id).join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function find_by_chord(nodesid, n, d) {
|
|
||||||
const EPSILON = 0.02;
|
|
||||||
return Object.keys(nodesid).filter((n1) => {
|
|
||||||
const d2 = dist2(nodesid[n1], nodesid[n]);
|
|
||||||
return Math.abs(d2 - d ** 2) < EPSILON;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function has_chord(n1, n2, d) {
|
|
||||||
const d2 = dist2(n1, n2);
|
|
||||||
const EPSILON = 0.01;
|
|
||||||
return Math.abs(d2 - d ** 2) < EPSILON;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function find_all_chords(nodes) {
|
|
||||||
const chords = {};
|
|
||||||
for( let i = 0; i < nodes.length - 1; i++ ) {
|
|
||||||
for( let j = i + 1; j < nodes.length; j++ ) {
|
|
||||||
const n1 = nodes[i];
|
|
||||||
const n2 = nodes[j];
|
|
||||||
const chord = Math.sqrt(dist2(n1, n2)).toFixed(5);
|
|
||||||
if( !(chord in chords) ) {
|
|
||||||
chords[chord] = [];
|
|
||||||
}
|
|
||||||
chords[chord].push([n1, n2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return chords;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
// New approach with tetrahedral coloring
|
|
||||||
|
|
||||||
function find_edges(links, nid) {
|
|
||||||
return links.filter((l) => l.source === nid || l.target === nid );
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function find_adjacent(links, nid) {
|
|
||||||
return find_edges(links, nid).map((l) => {
|
|
||||||
if( l.source === nid ) {
|
|
||||||
return l.target;
|
|
||||||
} else {
|
|
||||||
return l.source;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function iterate_graph(nodes, links, n, fn) {
|
|
||||||
const queue = [];
|
|
||||||
const seen = {};
|
|
||||||
const nodes_id = {};
|
|
||||||
nodes.map((n) => nodes_id[n.id] = n);
|
|
||||||
|
|
||||||
queue.push(n.id);
|
|
||||||
seen[n.id] = true;
|
|
||||||
fn(n);
|
|
||||||
|
|
||||||
while( queue.length > 0 ) {
|
|
||||||
const v = queue.shift();
|
|
||||||
find_adjacent(links, v).map((aid) => {
|
|
||||||
if( !(aid in seen) ) {
|
|
||||||
seen[aid] = true;
|
|
||||||
queue.push(aid);
|
|
||||||
fn(nodes_id[aid]);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// stupid tetrahedral labelling
|
|
||||||
// keeps getting stuck
|
|
||||||
|
|
||||||
|
|
||||||
function naive_label_120cell(nodes, links, n) {
|
|
||||||
const nodes_id = {};
|
|
||||||
nodes.map((n) => nodes_id[n.id] = n);
|
|
||||||
iterate_graph(nodes, links, nodes[0], (n) => {
|
|
||||||
const cols = new Set();
|
|
||||||
const nbors = find_adjacent(links, n.id);
|
|
||||||
for( const nb of nbors ) {
|
|
||||||
if( nodes_id[nb].label > 0 ) {
|
|
||||||
cols.add(nodes_id[nb].label);
|
|
||||||
}
|
|
||||||
for( const nb2 of find_adjacent(links, nb) ) {
|
|
||||||
if( nb2 !== n.id && nodes_id[nb].label > 0 ) {
|
|
||||||
cols.add(nodes_id[nb2].label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const pcols = [ 1, 2, 3, 4, 5 ].filter((c) => !cols.has(c));
|
|
||||||
if( pcols.length < 1 ) {
|
|
||||||
console.log(`Got stuck, no options at ${n.id}`);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
n.label = pcols[0];
|
|
||||||
console.log(`found ${pcols.length} colors for node ${n.id}`);
|
|
||||||
console.log(`applied ${pcols[0]} to node ${n.id}`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
1207
label120cell.js
1207
label120cell.js
File diff suppressed because it is too large
Load Diff
173
layer600cell.js
173
layer600cell.js
|
@ -1,173 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
import * as POLYTOPES from './polytopes.js';
|
|
||||||
|
|
||||||
// face detection for the 600-cell
|
|
||||||
|
|
||||||
|
|
||||||
export function nodes_links(links, nodeid) {
|
|
||||||
return links.filter((l) => l.source === nodeid || l.target === nodeid);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function linked(links, n1, n2) {
|
|
||||||
const ls = nodes_links(nodes_links(links, n1), n2);
|
|
||||||
if( ls.length ) {
|
|
||||||
return ls[0]
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function fingerprint(ids) {
|
|
||||||
const sids = [...ids];
|
|
||||||
sids.sort();
|
|
||||||
return sids.join(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function make_600cell() {
|
|
||||||
const nodes = POLYTOPES.make_600cell_vertices();
|
|
||||||
const links = POLYTOPES.auto_detect_edges(nodes, 12);
|
|
||||||
return {
|
|
||||||
nodes: nodes,
|
|
||||||
links: links
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function link_to_tetras(nodes, links, link) {
|
|
||||||
const n1 = link.source;
|
|
||||||
const n2 = link.target;
|
|
||||||
const nl1 = nodes_links(links, n1).filter((l) => l.id !== link.id);
|
|
||||||
const nl2 = nodes_links(links, n2).filter((l) => l.id !== link.id);
|
|
||||||
const p1 = new Set();
|
|
||||||
const p = new Set();
|
|
||||||
for( const nl of nl1 ) {
|
|
||||||
if( nl.source !== n1 ) {
|
|
||||||
p1.add(nl.source);
|
|
||||||
}
|
|
||||||
if( nl.target !== n1 ) {
|
|
||||||
p1.add(nl.target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for( const nl of nl2 ) {
|
|
||||||
if( nl.source !== n2 && p1.has(nl.source) ) {
|
|
||||||
p.add(nl.source);
|
|
||||||
}
|
|
||||||
if( nl.target !== n2 && p1.has(nl.target) ) {
|
|
||||||
p.add(nl.target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const lp = Array.from(p);
|
|
||||||
const seen = {};
|
|
||||||
const tetras = [];
|
|
||||||
for( const p1 of lp ) {
|
|
||||||
for( const p2 of lp ) {
|
|
||||||
if( p1 != p2 ) {
|
|
||||||
if( linked(links, p1, p2) ) {
|
|
||||||
const fp = fingerprint([n1, n2, p1, p2]);
|
|
||||||
if( !seen[fp] ) {
|
|
||||||
seen[fp] = true;
|
|
||||||
tetras.push({fingerprint: fp, nodes: [n1, n2, p1, p2]})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tetras;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function auto_600cell_cells(nodes, links) {
|
|
||||||
const seen = {};
|
|
||||||
const tetras = [];
|
|
||||||
links.map((link) => {
|
|
||||||
link_to_tetras(nodes, links, link).map((lt) => {
|
|
||||||
if( !seen[lt.fingerprint] ) {
|
|
||||||
seen[lt.fingerprint] = true;
|
|
||||||
tetras.push(lt.nodes);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
return tetras;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function node_by_id(nodes, nid) {
|
|
||||||
const ns = nodes.filter((n) => n.id === nid);
|
|
||||||
return ns[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function tetra_w(nodes, tetra) {
|
|
||||||
let w = 0;
|
|
||||||
for( const nid of tetra ) {
|
|
||||||
const node = node_by_id(nodes, nid);
|
|
||||||
w += node.w;
|
|
||||||
}
|
|
||||||
return w / 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sorted_600cells() {
|
|
||||||
const cell600 = make_600cell();
|
|
||||||
const tetras = auto_600cell_cells(cell600.nodes, cell600.links);
|
|
||||||
const layers = tetras.map((t) => { return { "nodes": t, w: tetra_w(cell600.nodes, t) } });
|
|
||||||
layers.sort((a, b) => b.w - a.w);
|
|
||||||
return layers;
|
|
||||||
}
|
|
||||||
|
|
||||||
// const cell600 = make_600cell();
|
|
||||||
|
|
||||||
// const layers = sorted_600cells(cell600.nodes, cell600.links);
|
|
||||||
// for( const cell of layers ) {
|
|
||||||
// // const fp = fingerprint(cell.nodes);
|
|
||||||
// console.log(`${cell.w} ${cell.nodes}`);
|
|
||||||
// }
|
|
||||||
|
|
||||||
export function make_layered_600cell() {
|
|
||||||
const tetras = sorted_600cells()
|
|
||||||
|
|
||||||
const LAYERS = [
|
|
||||||
[ "00", 20 ],
|
|
||||||
[ "01", 20 ],
|
|
||||||
[ "02", 30 ],
|
|
||||||
[ "03", 60 ],
|
|
||||||
[ "04", 60 ],
|
|
||||||
[ "05", 60 ],
|
|
||||||
[ "06", 20 ],
|
|
||||||
[ "07", 60 ],
|
|
||||||
[ "08", 20 ],
|
|
||||||
[ "09", 60 ],
|
|
||||||
[ "10", 60 ],
|
|
||||||
[ "11", 60 ],
|
|
||||||
[ "12", 30 ],
|
|
||||||
[ "13", 20 ],
|
|
||||||
[ "14", 20 ]
|
|
||||||
];
|
|
||||||
|
|
||||||
const vertices = {};
|
|
||||||
const seen = {};
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
for( const layer of LAYERS ) {
|
|
||||||
const label = layer[0];
|
|
||||||
const n = layer[1];
|
|
||||||
vertices[label] = [];
|
|
||||||
console.log(`Layer ${label} starting at ${i}`);
|
|
||||||
for( const t of tetras.slice(i, i + n) ) {
|
|
||||||
console.log(t);
|
|
||||||
for( const n of t.nodes ) {
|
|
||||||
if( !seen[n] ) {
|
|
||||||
vertices[label].push(n);
|
|
||||||
seen[n] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i += n;
|
|
||||||
}
|
|
||||||
return JSON.stringify(vertices);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
348
main.js
348
main.js
|
@ -1,251 +1,237 @@
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import * as POLYTOPES from './polytopes.js';
|
import * as POLYTOPES from './polytopes.js';
|
||||||
import { rotfn } from './rotation.js';
|
|
||||||
import { FourDGUI, DEFAULTS } from './gui.js';
|
|
||||||
import { FourDShape } from './fourDShape.js';
|
import { FourDShape } from './fourDShape.js';
|
||||||
import { get_colours } from './colours.js';
|
|
||||||
|
|
||||||
const FACE_OPACITY = 0.3;
|
import { GUI } from 'lil-gui';
|
||||||
const CAMERA_K = 5;
|
|
||||||
|
|
||||||
|
const DEFAULT_SHAPE = '5-cell';
|
||||||
|
|
||||||
|
// hacky stuff for 4d rotations
|
||||||
|
|
||||||
|
// see https://math.stackexchange.com/questions/1402362/can-rotations-in-4d-be-given-an-explicit-matrix-form#1402376
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function rotZW(theta) {
|
||||||
|
const ctheta = Math.cos(theta);
|
||||||
|
const stheta = Math.sin(theta);
|
||||||
|
return new THREE.Matrix4(
|
||||||
|
ctheta, -stheta, 0, 0,
|
||||||
|
stheta, ctheta, 0, 0,
|
||||||
|
0, 0, 1, 0,
|
||||||
|
0, 0, 0, 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotYW(theta) {
|
||||||
|
const ctheta = Math.cos(theta);
|
||||||
|
const stheta = Math.sin(theta);
|
||||||
|
return new THREE.Matrix4(
|
||||||
|
ctheta, 0, -stheta, 0,
|
||||||
|
0, 1, 0, 0,
|
||||||
|
stheta, 0, ctheta, 0,
|
||||||
|
0, 0, 0, 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotYZ(theta) {
|
||||||
|
const ctheta = Math.cos(theta);
|
||||||
|
const stheta = Math.sin(theta);
|
||||||
|
return new THREE.Matrix4(
|
||||||
|
ctheta, 0, 0, -stheta,
|
||||||
|
0, 1, 0, 0,
|
||||||
|
0, 0, 1, 0,
|
||||||
|
stheta, 0, 0, ctheta,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotXW(theta) {
|
||||||
|
const ctheta = Math.cos(theta);
|
||||||
|
const stheta = Math.sin(theta);
|
||||||
|
return new THREE.Matrix4(
|
||||||
|
1, 0, 0, 0,
|
||||||
|
0, ctheta, -stheta, 0,
|
||||||
|
0, stheta, ctheta, 0,
|
||||||
|
0, 0, 0, 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotXZ(theta) {
|
||||||
|
const ctheta = Math.cos(theta);
|
||||||
|
const stheta = Math.sin(theta);
|
||||||
|
return new THREE.Matrix4(
|
||||||
|
1, 0, 0, 0,
|
||||||
|
0, ctheta, 0, -stheta,
|
||||||
|
0, 0, 1, 0,
|
||||||
|
0, stheta, 0, ctheta,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotXY(theta) {
|
||||||
|
const ctheta = Math.cos(theta);
|
||||||
|
const stheta = Math.sin(theta);
|
||||||
|
return new THREE.Matrix4(
|
||||||
|
1, 0, 0, 0,
|
||||||
|
0, 1, 0, 0,
|
||||||
|
0, 0, ctheta, -stheta,
|
||||||
|
0, 0, stheta, ctheta,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// scene, lights and camera
|
|
||||||
|
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
|
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
|
||||||
|
|
||||||
const light = new THREE.PointLight(0xffffff, 2);
|
const light = new THREE.PointLight(0xffffff, 2);
|
||||||
light.position.set(10, 10, 10);
|
light.position.set(10, 10, 10);
|
||||||
scene.add(light);
|
scene.add(light);
|
||||||
const light2 = new THREE.PointLight(0xffffff, 2);
|
|
||||||
light2.position.set(-10, 5, 10);
|
|
||||||
scene.add(light);
|
|
||||||
const amblight = new THREE.AmbientLight(0xffffff, 0.5);
|
const amblight = new THREE.AmbientLight(0xffffff, 0.5);
|
||||||
scene.add(amblight);
|
scene.add(amblight);
|
||||||
|
|
||||||
camera.position.set(0, 0, CAMERA_K / 2);
|
scene.background = new THREE.Color(0xdddddd);
|
||||||
|
|
||||||
camera.lookAt(0, 0, 0);
|
|
||||||
//camera.position.z = 4;
|
|
||||||
|
|
||||||
const renderer = new THREE.WebGLRenderer({antialias: true});
|
const renderer = new THREE.WebGLRenderer({antialias: true});
|
||||||
renderer.setSize( window.innerWidth, window.innerHeight );
|
renderer.setSize( window.innerWidth, window.innerHeight );
|
||||||
document.body.appendChild( renderer.domElement );
|
document.body.appendChild( renderer.domElement );
|
||||||
|
|
||||||
// set up colours and materials for gui callbacks
|
|
||||||
|
|
||||||
scene.background = new THREE.Color(DEFAULTS.background);
|
const NODE_OPACITY = 1.0;
|
||||||
const material = new THREE.MeshStandardMaterial({ color: DEFAULTS.color });
|
const LINK_OPACITY = 1.0;
|
||||||
const node_colours = get_colours(DEFAULTS.color);
|
|
||||||
|
|
||||||
|
const node_ms = [
|
||||||
|
new THREE.MeshStandardMaterial( { color: 0x90ebff } )
|
||||||
|
];
|
||||||
|
|
||||||
material.transparent = true;
|
for( const node_m of node_ms ) {
|
||||||
material.opacity = 0.5;
|
node_m.roughness = 0.9;
|
||||||
|
|
||||||
|
if( NODE_OPACITY < 1.0 ) {
|
||||||
const node_ms = node_colours.map((c) => new THREE.MeshStandardMaterial({color: c}));
|
node_m.transparent = true;
|
||||||
const link_ms = node_colours.map((c) => new THREE.MeshStandardMaterial({color: c}));
|
node_m.opacity = NODE_OPACITY;
|
||||||
|
|
||||||
node_ms.map((m) => {
|
|
||||||
m.transparent = true;
|
|
||||||
m.opacity = 1.0;
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
link_ms.map((m) => {
|
|
||||||
m.transparent = true;
|
|
||||||
m.opacity = 0.5;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const face_ms = [
|
|
||||||
new THREE.MeshLambertMaterial( { color: 0x44ff44 } )
|
|
||||||
];
|
|
||||||
|
|
||||||
for( const face_m of face_ms ) {
|
|
||||||
face_m.transparent = true;
|
|
||||||
face_m.opacity = FACE_OPACITY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const link_ms = [
|
||||||
|
new THREE.MeshStandardMaterial( { color: 0x90ebff } )
|
||||||
|
];
|
||||||
|
|
||||||
const STRUCTURES = POLYTOPES.build_all();
|
for( const link_m of link_ms ) {
|
||||||
|
link_m.metalness = 0.8;
|
||||||
|
link_m.roughness = 0.1;
|
||||||
|
|
||||||
const STRUCTURES_BY_NAME = {};
|
if( LINK_OPACITY < 1.0 ) {
|
||||||
|
link_m.transparent = true;
|
||||||
|
link_m.opacity = LINK_OPACITY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const STRUCTURES = {
|
||||||
|
'5-cell': POLYTOPES.cell5(),
|
||||||
|
'16-cell': POLYTOPES.cell16(),
|
||||||
|
'tesseract': POLYTOPES.tesseract(),
|
||||||
|
'24-cell': POLYTOPES.cell24(),
|
||||||
|
'120-cell': POLYTOPES.cell120(),
|
||||||
|
'600-cell': POLYTOPES.cell600()
|
||||||
|
};
|
||||||
|
|
||||||
STRUCTURES.map((s) => STRUCTURES_BY_NAME[s.name] = s);
|
|
||||||
|
|
||||||
let shape = false;
|
let shape = false;
|
||||||
let structure = false;
|
|
||||||
let node_show = [];
|
|
||||||
let link_show = [];
|
|
||||||
|
|
||||||
|
function createShape(name) {
|
||||||
function createShape(name, option) {
|
|
||||||
if( shape ) {
|
if( shape ) {
|
||||||
scene.remove(shape);
|
scene.remove(shape);
|
||||||
}
|
}
|
||||||
structure = STRUCTURES_BY_NAME[name];
|
shape = new FourDShape(node_ms, link_ms, STRUCTURES[name]);
|
||||||
shape = new FourDShape(node_ms, link_ms, face_ms, structure);
|
|
||||||
scene.add(shape);
|
scene.add(shape);
|
||||||
setVisibility(option ? option : structure.options[0].name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayDocs(name) {
|
|
||||||
const docdiv = document.getElementById("description");
|
|
||||||
const description = STRUCTURES_BY_NAME[name].description;
|
|
||||||
if( description ) {
|
|
||||||
docdiv.innerHTML =`<p>${name}</p><p>${description}</p>`;
|
|
||||||
} else {
|
|
||||||
docdiv.innerHTML =`<p>${name}</p>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showDocs(visible) {
|
|
||||||
console.log(`showDocs ${visible}`);
|
|
||||||
const docdiv = document.getElementById("description");
|
|
||||||
if( visible ) {
|
|
||||||
docdiv.style.display = '';
|
|
||||||
} else {
|
|
||||||
docdiv.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialise gui and read params from URL
|
|
||||||
|
|
||||||
// callbacks to do things which are triggered by controls: reset the shape,
|
|
||||||
// change the colors. Otherwise we just read stuff from gui.params.
|
|
||||||
|
|
||||||
function setColors(c) {
|
|
||||||
const nc = get_colours(c);
|
|
||||||
for( let i = 0; i < node_ms.length; i++ ) {
|
|
||||||
node_ms[i].color = new THREE.Color(nc[i]);
|
|
||||||
link_ms[i].color = new THREE.Color(nc[i]);
|
|
||||||
}
|
|
||||||
material.color = new THREE.Color(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setBackground(c) {
|
|
||||||
scene.background = new THREE.Color(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLinkOpacity(o, primary) {
|
|
||||||
if( structure.nolink2opacity ) {
|
|
||||||
link_ms.map((lm) => lm.opacity = o);
|
|
||||||
} else {
|
|
||||||
if( primary ) {
|
|
||||||
link_ms[0].opacity = o;
|
|
||||||
} else {
|
|
||||||
link_ms.slice(1).map((lm) => lm.opacity = o);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNodeOpacity(o) {
|
|
||||||
node_ms.map((nm) => nm.opacity = o);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let gui;
|
|
||||||
|
|
||||||
|
|
||||||
function changeShape() {
|
createShape(DEFAULT_SHAPE);
|
||||||
createShape(gui.params.shape);
|
|
||||||
displayDocs(gui.params.shape);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setVisibility(option_name) {
|
|
||||||
const option = structure.options.filter((o) => o.name === option_name);
|
|
||||||
if( option.length ) {
|
|
||||||
node_show = option[0].nodes;
|
|
||||||
link_show = option[0].links;
|
|
||||||
} else {
|
|
||||||
console.log(`Error: option '${option_name}' not found`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
gui = new FourDGUI(
|
camera.position.z = 4;
|
||||||
STRUCTURES,
|
|
||||||
changeShape,
|
|
||||||
setColors,
|
|
||||||
setBackground,
|
|
||||||
setNodeOpacity,
|
|
||||||
setLinkOpacity,
|
|
||||||
setVisibility,
|
|
||||||
showDocs
|
|
||||||
);
|
|
||||||
|
|
||||||
// these are here to pick up colour settings from the URL params
|
|
||||||
setColors(gui.params.color);
|
|
||||||
setBackground(gui.params.background);
|
|
||||||
|
|
||||||
const dragK = 0.005;
|
const dragK = 0.005;
|
||||||
const damping = 0.99;
|
|
||||||
|
|
||||||
let theta = 0;
|
let theta = 0;
|
||||||
let psi = 0;
|
let psi = 0;
|
||||||
let theta0 = 0;
|
let startX = 0;
|
||||||
let psi0 = 0;
|
let startY = 0;
|
||||||
let dragx0 = 0;
|
let startX0 = 0;
|
||||||
let dragy0 = 0;
|
let startY0 = 0;
|
||||||
let dragging = false;
|
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener("mousedown", (event) => {
|
||||||
renderer.domElement.addEventListener("pointerdown", (event) => {
|
|
||||||
if( event.buttons === 1 ) {
|
if( event.buttons === 1 ) {
|
||||||
theta0 = theta;
|
startX = event.clientX;
|
||||||
psi0 = psi;
|
startY = event.clientY;
|
||||||
dragx0 = event.clientX;
|
startX0 = theta / dragK;
|
||||||
dragy0 = event.clientY;
|
startY0 = theta / dragK;
|
||||||
dragging = true;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
renderer.domElement.addEventListener("pointermove", (event) => {
|
|
||||||
|
renderer.domElement.addEventListener("mousemove", (event) => {
|
||||||
if( event.buttons === 1 ) {
|
if( event.buttons === 1 ) {
|
||||||
const theta1 = theta0 + (event.clientX - dragx0) * dragK;
|
theta = (event.clientX - startX + startX0) * dragK;
|
||||||
const psi1 = psi0 + (event.clientY - dragy0) * dragK;
|
psi = (event.clientY - startY + startY0) * dragK;
|
||||||
gui.params.dtheta = theta1 - theta;
|
|
||||||
gui.params.dpsi = psi1 - psi;
|
|
||||||
theta = theta1;
|
|
||||||
psi = psi1;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
renderer.domElement.addEventListener("pointerup", (event) => {
|
// set up GUI
|
||||||
dragging = false;
|
|
||||||
})
|
|
||||||
|
|
||||||
createShape(gui.params.shape, gui.params.option);
|
const gui = new GUI();
|
||||||
displayDocs(gui.params.shape);
|
|
||||||
|
const gui_params = {
|
||||||
|
shape: DEFAULT_SHAPE,
|
||||||
|
hyperplane: 2,
|
||||||
|
xRotate: 'YW',
|
||||||
|
yRotate: 'XZ',
|
||||||
|
};
|
||||||
|
|
||||||
|
gui.add(gui_params, 'shape',
|
||||||
|
[ '5-cell', '16-cell', 'tesseract', '24-cell', '120-cell', '600-cell' ]
|
||||||
|
).onChange(createShape)
|
||||||
|
|
||||||
|
gui.add(gui_params, 'hyperplane', 1.5, 4);
|
||||||
|
gui.add(gui_params, 'xRotate', [ 'YW', 'YZ', 'ZW' ]);
|
||||||
|
gui.add(gui_params, 'yRotate', [ 'XZ', 'XY', 'XW' ]);
|
||||||
|
|
||||||
|
const ROTFN = {
|
||||||
|
XY: rotXY,
|
||||||
|
XZ: rotXZ,
|
||||||
|
XW: rotXW,
|
||||||
|
YZ: rotYZ,
|
||||||
|
YW: rotYW,
|
||||||
|
ZW: rotZW,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const rotation = new THREE.Matrix4();
|
||||||
|
|
||||||
function animate() {
|
function animate() {
|
||||||
requestAnimationFrame( animate );
|
requestAnimationFrame( animate );
|
||||||
|
|
||||||
if( ! dragging ) {
|
|
||||||
theta += gui.params.dtheta;
|
|
||||||
psi += gui.params.dpsi;
|
|
||||||
if( gui.params.damping ) {
|
|
||||||
gui.params.dtheta = gui.params.dtheta * damping;
|
|
||||||
gui.params.dpsi = gui.params.dpsi * damping;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rotations = [
|
const rotations = [
|
||||||
rotfn[gui.params.xRotate](theta),
|
ROTFN[gui_params.xRotate](theta),
|
||||||
rotfn[gui.params.yRotate](psi)
|
ROTFN[gui_params.yRotate](psi)
|
||||||
];
|
];
|
||||||
shape.hyperplane = 1 / gui.params.hyperplane;
|
shape.hyperplane = gui_params.hyperplane;
|
||||||
camera.position.set(0, 0, gui.params.zoom * CAMERA_K * gui.params.hyperplane);
|
shape.render3(rotations);
|
||||||
|
|
||||||
shape.node_scale = gui.params.nodesize;
|
|
||||||
shape.link_scale = gui.params.linksize * gui.params.nodesize * 0.5;
|
|
||||||
shape.render3(rotations, node_show, link_show);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
renderer.render( scene, camera );
|
renderer.render( scene, camera );
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,8 @@
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "fourdjs",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color": "^4.2.3",
|
"lil-gui": "^0.18.2",
|
||||||
"color-scheme": "^1.0.1",
|
|
||||||
"lil-gui": "^0.19.0",
|
|
||||||
"three": "^0.154.0"
|
"three": "^0.154.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -16,9 +13,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.15.tgz",
|
||||||
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
|
"integrity": "sha512-wlkQBWb79/jeEEoRmrxt/yhn5T1lU236OCNpnfRzaCJHZ/5gf82uYx1qmADTBWE0AR/v7FiozE1auk2riyQd3w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
@ -32,9 +29,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm64": {
|
"node_modules/@esbuild/android-arm64": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.15.tgz",
|
||||||
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
|
"integrity": "sha512-NI/gnWcMl2kXt1HJKOn2H69SYn4YNheKo6NZt1hyfKWdMbaGadxjZIkcj4Gjk/WPxnbFXs9/3HjGHaknCqjrww==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -48,9 +45,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-x64": {
|
"node_modules/@esbuild/android-x64": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.15.tgz",
|
||||||
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
|
"integrity": "sha512-FM9NQamSaEm/IZIhegF76aiLnng1kEsZl2eve/emxDeReVfRuRNmvT28l6hoFD9TsCxpK+i4v8LPpEj74T7yjA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -64,9 +61,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.15.tgz",
|
||||||
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
|
"integrity": "sha512-XmrFwEOYauKte9QjS6hz60FpOCnw4zaPAb7XV7O4lx1r39XjJhTN7ZpXqJh4sN6q60zbP6QwAVVA8N/wUyBH/w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -80,9 +77,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.15.tgz",
|
||||||
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
|
"integrity": "sha512-bMqBmpw1e//7Fh5GLetSZaeo9zSC4/CMtrVFdj+bqKPGJuKyfNJ5Nf2m3LknKZTS+Q4oyPiON+v3eaJ59sLB5A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -96,9 +93,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.15.tgz",
|
||||||
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
|
"integrity": "sha512-LoTK5N3bOmNI9zVLCeTgnk5Rk0WdUTrr9dyDAQGVMrNTh9EAPuNwSTCgaKOKiDpverOa0htPcO9NwslSE5xuLA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -112,9 +109,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.15.tgz",
|
||||||
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
|
"integrity": "sha512-62jX5n30VzgrjAjOk5orYeHFq6sqjvsIj1QesXvn5OZtdt5Gdj0vUNJy9NIpjfdNdqr76jjtzBJKf+h2uzYuTQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -128,9 +125,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm": {
|
"node_modules/@esbuild/linux-arm": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.15.tgz",
|
||||||
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
|
"integrity": "sha512-dT4URUv6ir45ZkBqhwZwyFV6cH61k8MttIwhThp2BGiVtagYvCToF+Bggyx2VI57RG4Fbt21f9TmXaYx0DeUJg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
@ -144,9 +141,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.15.tgz",
|
||||||
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
|
"integrity": "sha512-BWncQeuWDgYv0jTNzJjaNgleduV4tMbQjmk/zpPh/lUdMcNEAxy+jvneDJ6RJkrqloG7tB9S9rCrtfk/kuplsQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -160,9 +157,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.15.tgz",
|
||||||
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
|
"integrity": "sha512-JPXORvgHRHITqfms1dWT/GbEY89u848dC08o0yK3fNskhp0t2TuNUnsrrSgOdH28ceb1hJuwyr8R/1RnyPwocw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
@ -176,9 +173,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.15.tgz",
|
||||||
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
|
"integrity": "sha512-kArPI0DopjJCEplsVj/H+2Qgzz7vdFSacHNsgoAKpPS6W/Ndh8Oe24HRDQ5QCu4jHgN6XOtfFfLpRx3TXv/mEg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
|
@ -192,9 +189,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.15.tgz",
|
||||||
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
|
"integrity": "sha512-b/tmngUfO02E00c1XnNTw/0DmloKjb6XQeqxaYuzGwHe0fHVgx5/D6CWi+XH1DvkszjBUkK9BX7n1ARTOst59w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
|
@ -208,9 +205,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.15.tgz",
|
||||||
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
|
"integrity": "sha512-KXPY69MWw79QJkyvUYb2ex/OgnN/8N/Aw5UDPlgoRtoEfcBqfeLodPr42UojV3NdkoO4u10NXQdamWm1YEzSKw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
@ -224,9 +221,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.15.tgz",
|
||||||
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
|
"integrity": "sha512-komK3NEAeeGRnvFEjX1SfVg6EmkfIi5aKzevdvJqMydYr9N+pRQK0PGJXk+bhoPZwOUgLO4l99FZmLGk/L1jWg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
|
@ -240,9 +237,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.15.tgz",
|
||||||
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
|
"integrity": "sha512-632T5Ts6gQ2WiMLWRRyeflPAm44u2E/s/TJvn+BP6M5mnHSk93cieaypj3VSMYO2ePTCRqAFXtuYi1yv8uZJNA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
@ -256,9 +253,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.15.tgz",
|
||||||
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
|
"integrity": "sha512-MsHtX0NgvRHsoOtYkuxyk4Vkmvk3PLRWfA4okK7c+6dT0Fu4SUqXAr9y4Q3d8vUf1VWWb6YutpL4XNe400iQ1g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -272,9 +269,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.15.tgz",
|
||||||
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
|
"integrity": "sha512-djST6s+jQiwxMIVQ5rlt24JFIAr4uwUnzceuFL7BQT4CbrRtqBPueS4GjXSiIpmwVri1Icj/9pFRJ7/aScvT+A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -288,9 +285,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.15.tgz",
|
||||||
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
|
"integrity": "sha512-naeRhUIvhsgeounjkF5mvrNAVMGAm6EJWiabskeE5yOeBbLp7T89tAEw0j5Jm/CZAwyLe3c67zyCWH6fsBLCpw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -304,9 +301,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.15.tgz",
|
||||||
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
|
"integrity": "sha512-qkT2+WxyKbNIKV1AEhI8QiSIgTHMcRctzSaa/I3kVgMS5dl3fOeoqkb7pW76KwxHoriImhx7Mg3TwN/auMDsyQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -320,9 +317,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.15.tgz",
|
||||||
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
|
"integrity": "sha512-HC4/feP+pB2Vb+cMPUjAnFyERs+HJN7E6KaeBlFdBv799MhD+aPJlfi/yk36SED58J9TPwI8MAcVpJgej4ud0A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -336,9 +333,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.15.tgz",
|
||||||
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
|
"integrity": "sha512-ovjwoRXI+gf52EVF60u9sSDj7myPixPxqzD5CmkEUmvs+W9Xd0iqISVBQn8xcx4ciIaIVlWCuTbYDOXOnOL44Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
@ -352,9 +349,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.15.tgz",
|
||||||
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
|
"integrity": "sha512-imUxH9a3WJARyAvrG7srLyiK73XdX83NXQkjKvQ+7vPh3ZxoLrzvPkQKKw2DwZ+RV2ZB6vBfNHP8XScAmQC3aA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -367,52 +364,10 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/color": {
|
|
||||||
"version": "4.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
|
||||||
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
|
||||||
"dependencies": {
|
|
||||||
"color-convert": "^2.0.1",
|
|
||||||
"color-string": "^1.9.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-convert": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"color-name": "~1.1.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-name": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
|
||||||
},
|
|
||||||
"node_modules/color-scheme": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-scheme/-/color-scheme-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-4x+ya6+z6g9DaTFSfVzTZc8TSjxHuDT40NB43N3XPUkQlF6uujhwH8aeMeq8HBgoQQog/vrYgJ16mt/eVTRXwQ=="
|
|
||||||
},
|
|
||||||
"node_modules/color-string": {
|
|
||||||
"version": "1.9.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
|
||||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
|
||||||
"dependencies": {
|
|
||||||
"color-name": "^1.0.0",
|
|
||||||
"simple-swizzle": "^0.2.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.15",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.15.tgz",
|
||||||
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
|
"integrity": "sha512-3WOOLhrvuTGPRzQPU6waSDWrDTnQriia72McWcn6UCi43GhCHrXH4S59hKMeez+IITmdUuUyvbU9JIp+t3xlPQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -422,34 +377,34 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@esbuild/android-arm": "0.18.20",
|
"@esbuild/android-arm": "0.18.15",
|
||||||
"@esbuild/android-arm64": "0.18.20",
|
"@esbuild/android-arm64": "0.18.15",
|
||||||
"@esbuild/android-x64": "0.18.20",
|
"@esbuild/android-x64": "0.18.15",
|
||||||
"@esbuild/darwin-arm64": "0.18.20",
|
"@esbuild/darwin-arm64": "0.18.15",
|
||||||
"@esbuild/darwin-x64": "0.18.20",
|
"@esbuild/darwin-x64": "0.18.15",
|
||||||
"@esbuild/freebsd-arm64": "0.18.20",
|
"@esbuild/freebsd-arm64": "0.18.15",
|
||||||
"@esbuild/freebsd-x64": "0.18.20",
|
"@esbuild/freebsd-x64": "0.18.15",
|
||||||
"@esbuild/linux-arm": "0.18.20",
|
"@esbuild/linux-arm": "0.18.15",
|
||||||
"@esbuild/linux-arm64": "0.18.20",
|
"@esbuild/linux-arm64": "0.18.15",
|
||||||
"@esbuild/linux-ia32": "0.18.20",
|
"@esbuild/linux-ia32": "0.18.15",
|
||||||
"@esbuild/linux-loong64": "0.18.20",
|
"@esbuild/linux-loong64": "0.18.15",
|
||||||
"@esbuild/linux-mips64el": "0.18.20",
|
"@esbuild/linux-mips64el": "0.18.15",
|
||||||
"@esbuild/linux-ppc64": "0.18.20",
|
"@esbuild/linux-ppc64": "0.18.15",
|
||||||
"@esbuild/linux-riscv64": "0.18.20",
|
"@esbuild/linux-riscv64": "0.18.15",
|
||||||
"@esbuild/linux-s390x": "0.18.20",
|
"@esbuild/linux-s390x": "0.18.15",
|
||||||
"@esbuild/linux-x64": "0.18.20",
|
"@esbuild/linux-x64": "0.18.15",
|
||||||
"@esbuild/netbsd-x64": "0.18.20",
|
"@esbuild/netbsd-x64": "0.18.15",
|
||||||
"@esbuild/openbsd-x64": "0.18.20",
|
"@esbuild/openbsd-x64": "0.18.15",
|
||||||
"@esbuild/sunos-x64": "0.18.20",
|
"@esbuild/sunos-x64": "0.18.15",
|
||||||
"@esbuild/win32-arm64": "0.18.20",
|
"@esbuild/win32-arm64": "0.18.15",
|
||||||
"@esbuild/win32-ia32": "0.18.20",
|
"@esbuild/win32-ia32": "0.18.15",
|
||||||
"@esbuild/win32-x64": "0.18.20"
|
"@esbuild/win32-x64": "0.18.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
@ -460,15 +415,10 @@
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-arrayish": {
|
|
||||||
"version": "0.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
|
||||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
|
|
||||||
},
|
|
||||||
"node_modules/lil-gui": {
|
"node_modules/lil-gui": {
|
||||||
"version": "0.19.0",
|
"version": "0.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.18.2.tgz",
|
||||||
"integrity": "sha512-02/Z7rPng3GXWFwkQVj1hQaJYo2fIEYctqe0ima5uI/N2HEagB9ZGCQKkVWr3UuKfTr0arto3Q9prTB8sxtJJw=="
|
"integrity": "sha512-DgdrLy3/KGC0PiQLKgOcJMPItP4xY4iWgJ9+91Zaxfr8GCTmMps05QS9w9jW7yspILlbscbquwjOwxmWnSx5Uw=="
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.6",
|
"version": "3.3.6",
|
||||||
|
@ -495,9 +445,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.31",
|
"version": "8.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz",
|
||||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
"integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -523,9 +473,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "3.29.4",
|
"version": "3.26.3",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.26.3.tgz",
|
||||||
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
|
"integrity": "sha512-7Tin0C8l86TkpcMtXvQu6saWH93nhG3dGQ1/+l5V2TDMceTxO7kDiK6GzbfLWNNxqJXm591PcEZUozZm51ogwQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
|
@ -538,14 +488,6 @@
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/simple-swizzle": {
|
|
||||||
"version": "0.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
|
||||||
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
|
||||||
"dependencies": {
|
|
||||||
"is-arrayish": "^0.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||||
|
@ -561,14 +503,14 @@
|
||||||
"integrity": "sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug=="
|
"integrity": "sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug=="
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "4.5.3",
|
"version": "4.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.6.tgz",
|
||||||
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
|
"integrity": "sha512-EY6Mm8vJ++S3D4tNAckaZfw3JwG3wa794Vt70M6cNJ6NxT87yhq7EC8Rcap3ahyHdo8AhCmV9PTk+vG1HiYn1A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.18.10",
|
"esbuild": "^0.18.10",
|
||||||
"postcss": "^8.4.27",
|
"postcss": "^8.4.26",
|
||||||
"rollup": "^3.27.1"
|
"rollup": "^3.25.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color": "^4.2.3",
|
"lil-gui": "^0.18.2",
|
||||||
"color-scheme": "^1.0.1",
|
|
||||||
"lil-gui": "^0.19.0",
|
|
||||||
"three": "^0.154.0"
|
"three": "^0.154.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.4.6"
|
"vite": "^4.4.6"
|
||||||
},
|
}
|
||||||
"type": "module"
|
|
||||||
}
|
}
|
||||||
|
|
760
polytopes.js
760
polytopes.js
|
@ -1,28 +1,23 @@
|
||||||
|
|
||||||
import * as PERMUTE from './permute.js';
|
import * as PERMUTE from './permute.js';
|
||||||
|
|
||||||
import * as CELLINDEX from './cellindex.js';
|
function scale_and_index(nodes, scale) {
|
||||||
|
|
||||||
function index_nodes(nodes, scale) {
|
|
||||||
let i = 1;
|
let i = 1;
|
||||||
for( const n of nodes ) {
|
for( const n of nodes ) {
|
||||||
n["id"] = i;
|
n["id"] = i;
|
||||||
i++;
|
i++;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scale_nodes(nodes, scale) {
|
|
||||||
for( const n of nodes ) {
|
|
||||||
for( const a of [ 'x', 'y', 'z', 'w' ] ) {
|
for( const a of [ 'x', 'y', 'z', 'w' ] ) {
|
||||||
n[a] = scale * n[a];
|
n[a] = scale * n[a];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dist2(n1, n2) {
|
function dist2(n1, n2) {
|
||||||
return (n1.x - n2.x) ** 2 + (n1.y - n2.y) ** 2 + (n1.z - n2.z) ** 2 + (n1.w - n2.w) ** 2;
|
return (n1.x - n2.x) ** 2 + (n1.y - n2.y) ** 2 + (n1.z - n2.z) ** 2 + (n1.w - n2.w) ** 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function auto_detect_edges(nodes, neighbours, debug=false) {
|
function auto_detect_edges(nodes, neighbours) {
|
||||||
const seen = {};
|
const seen = {};
|
||||||
const nnodes = nodes.length;
|
const nnodes = nodes.length;
|
||||||
const links = [];
|
const links = [];
|
||||||
|
@ -34,10 +29,6 @@ export function auto_detect_edges(nodes, neighbours, debug=false) {
|
||||||
}
|
}
|
||||||
d2.sort((a, b) => a.d2 - b.d2);
|
d2.sort((a, b) => a.d2 - b.d2);
|
||||||
const closest = d2.slice(1, neighbours + 1);
|
const closest = d2.slice(1, neighbours + 1);
|
||||||
if( debug ) {
|
|
||||||
console.log(`closest = ${closest.length}`);
|
|
||||||
console.log(closest);
|
|
||||||
}
|
|
||||||
for( const e of closest ) {
|
for( const e of closest ) {
|
||||||
const ids = [ n1.id, e.id ];
|
const ids = [ n1.id, e.id ];
|
||||||
ids.sort();
|
ids.sort();
|
||||||
|
@ -49,24 +40,21 @@ export function auto_detect_edges(nodes, neighbours, debug=false) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if( debug ) {
|
|
||||||
console.log(`Found ${links.length} edges`)
|
|
||||||
}
|
|
||||||
return links;
|
return links;
|
||||||
}
|
}
|
||||||
|
|
||||||
// too small and simple to calculate
|
// too small and simple to calculate
|
||||||
|
|
||||||
export const cell5 = () => {
|
export const cell5 = () => {
|
||||||
const c1 = Math.sqrt(5) / 4;
|
const r5 = Math.sqrt(5);
|
||||||
|
const r2 = Math.sqrt(2) / 2;
|
||||||
return {
|
return {
|
||||||
name: '5-cell',
|
|
||||||
nodes: [
|
nodes: [
|
||||||
{id:1, label: 1, x: c1, y: c1, z: c1, w: -0.25 },
|
{id:1, x: r2, y: r2, z: r2, w: -r2 / r5 },
|
||||||
{id:2, label: 2, x: c1, y: -c1, z: -c1, w: -0.25 },
|
{id:2, x: r2, y: -r2, z: -r2, w: -r2 / r5 },
|
||||||
{id:3, label: 3, x: -c1, y: c1, z: -c1, w: -0.25 },
|
{id:3, x: -r2, y: r2, z: -r2, w: -r2 / r5 },
|
||||||
{id:4, label: 4, x: -c1, y: -c1, z: c1, w: -0.25 },
|
{id:4, x: -r2, y: -r2, z: r2, w: -r2 / r5 },
|
||||||
{id:5, label: 5, x: 0, y: 0, z: 0, w: 1 },
|
{id:5, x: 0, y: 0, z: 0, w: 4 * r2 / r5 },
|
||||||
],
|
],
|
||||||
links: [
|
links: [
|
||||||
{ id:1, source:1, target: 2},
|
{ id:1, source:1, target: 2},
|
||||||
|
@ -79,13 +67,7 @@ export const cell5 = () => {
|
||||||
{ id:8, source:3, target: 4},
|
{ id:8, source:3, target: 4},
|
||||||
{ id:9, source:3, target: 5},
|
{ id:9, source:3, target: 5},
|
||||||
{ id:10, source:4, target: 5},
|
{ id:10, source:4, target: 5},
|
||||||
],
|
]
|
||||||
options: [ { name: '--' }],
|
|
||||||
description: `Five tetrahedra joined at ten faces with three
|
|
||||||
tetrahedra around each edge. The 5-cell is the simplest regular
|
|
||||||
four-D polytope and the four-dimensional analogue of the tetrahedron.
|
|
||||||
A corresponding polytope, or simplex, exists for every n-dimensional
|
|
||||||
space.`,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -93,218 +75,46 @@ export const cell5 = () => {
|
||||||
export const cell16 = () => {
|
export const cell16 = () => {
|
||||||
let nodes = PERMUTE.coordinates([1, 1, 1, 1], 0);
|
let nodes = PERMUTE.coordinates([1, 1, 1, 1], 0);
|
||||||
nodes = nodes.filter((n) => n.x * n.y * n.z * n.w > 0);
|
nodes = nodes.filter((n) => n.x * n.y * n.z * n.w > 0);
|
||||||
|
scale_and_index(nodes, 0.75);
|
||||||
nodes[0].label = 1;
|
console.log('cell16 auto_detect_edges');
|
||||||
nodes[3].label = 2;
|
console.log(nodes);
|
||||||
nodes[5].label = 3;
|
const links = auto_detect_edges(nodes, 8);
|
||||||
nodes[6].label = 4;
|
|
||||||
nodes[7].label = 1;
|
|
||||||
nodes[4].label = 2;
|
|
||||||
nodes[2].label = 3;
|
|
||||||
nodes[1].label = 4;
|
|
||||||
|
|
||||||
index_nodes(nodes);
|
|
||||||
scale_nodes(nodes, 0.5);
|
|
||||||
const links = auto_detect_edges(nodes, 6);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: '16-cell',
|
|
||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
links: links,
|
links: links
|
||||||
options: [ { name: '--' }],
|
|
||||||
description: `Sixteen tetrahedra joined at 32 faces with four
|
|
||||||
tetrahedra around each edge. The 16-cell is the four-dimensional
|
|
||||||
analogue of the octahedron and is dual to the tesseract. Every
|
|
||||||
n-dimensional space has a corresponding polytope in this family.`,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const tesseract = () => {
|
export const tesseract = () => {
|
||||||
const nodes = PERMUTE.coordinates([1, 1, 1, 1], 0);
|
const nodes = scale_and_index(PERMUTE.coordinates([1, 1, 1, 1], 0), Math.sqrt(2) / 2);
|
||||||
index_nodes(nodes);
|
|
||||||
|
|
||||||
for( const n of nodes ) {
|
|
||||||
if( n.x * n.y * n.z * n.w > 0 ) {
|
|
||||||
n.label = 2;
|
|
||||||
} else {
|
|
||||||
n.label = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scale_nodes(nodes, 0.5);
|
|
||||||
const links = auto_detect_edges(nodes, 4);
|
const links = auto_detect_edges(nodes, 4);
|
||||||
links.map((l) => { l.label = 0 });
|
|
||||||
|
|
||||||
for( const p of [ 1, 2 ] ) {
|
|
||||||
const nodes16 = nodes.filter((n) => n.label === p);
|
|
||||||
const links16 = auto_detect_edges(nodes16, 6);
|
|
||||||
links16.map((l) => l.label = p);
|
|
||||||
links.push(...links16);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'Tesseract',
|
|
||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
links: links,
|
links: links
|
||||||
options: [
|
|
||||||
{ name: 'none', links: [ 0 ] },
|
|
||||||
{ name: 'one 16-cell', links: [ 0, 1 ] },
|
|
||||||
{ name: 'both 16-cells', links: [ 0, 1, 2 ] },
|
|
||||||
],
|
|
||||||
description: `The most well-known four-dimensional shape, the
|
|
||||||
tesseract is analogous to the cube, and is constructed by placing two
|
|
||||||
cubes in parallel hyperplanes and joining their corresponding
|
|
||||||
vertices. It consists of eight cubes joined at 32 face with three
|
|
||||||
cubes around each edge, and is dual to the 16-cell. Every
|
|
||||||
n-dimensional space has a cube analogue or measure polytope.`,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const CELL24_INDEXING = {
|
|
||||||
x: { y: 1, z: 3, w: 2 },
|
|
||||||
y: { z: 2, w: 3 },
|
|
||||||
z: { w: 1 }
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
function node_by_id(nodes, nid) {
|
|
||||||
const ns = nodes.filter((n) => n.id === nid);
|
|
||||||
return ns[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const cell24 = () => {
|
export const cell24 = () => {
|
||||||
const nodes = PERMUTE.coordinates([0, 0, 1, 1], 0);
|
const nodes = scale_and_index(PERMUTE.coordinates([0, 0, 1, 1], 0), 1);
|
||||||
|
const links = auto_detect_edges(nodes, 6);
|
||||||
for( const n of nodes ) {
|
|
||||||
const axes = ['x', 'y', 'z', 'w'].filter((a) => n[a] !== 0);
|
|
||||||
n.label = CELL24_INDEXING[axes[0]][axes[1]];
|
|
||||||
}
|
|
||||||
|
|
||||||
scale_nodes(nodes, Math.sqrt(2) / 2);
|
|
||||||
index_nodes(nodes);
|
|
||||||
const links = auto_detect_edges(nodes, 8);
|
|
||||||
links.map((l) => l.label = 0);
|
|
||||||
|
|
||||||
for( const p of [ 1, 2, 3 ] ) {
|
|
||||||
const nodes16 = nodes.filter((n) => n.label === p);
|
|
||||||
const links16 = auto_detect_edges(nodes16, 6);
|
|
||||||
links16.map((l) => l.label = p);
|
|
||||||
links.push(...links16);
|
|
||||||
}
|
|
||||||
// links.map((l) => {
|
|
||||||
// const ls = [ l.source, l.target ].map((nid) => node_by_id(nodes, nid).label);
|
|
||||||
// for ( const c of [1, 2, 3] ) {
|
|
||||||
// if( ! ls.includes(c) ) {
|
|
||||||
// l.label = c
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: '24-cell',
|
|
||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
links: links,
|
links: links
|
||||||
base: {},
|
|
||||||
options: [
|
|
||||||
{ name: 'none', links: [ 0 ] },
|
|
||||||
{ name: 'one 16-cell', links: [ 0, 1 ] },
|
|
||||||
{ name: 'three 16-cells', links: [ 0, 1, 2, 3 ] }
|
|
||||||
],
|
|
||||||
description: `A unique object without an exact analogue in higher
|
|
||||||
or lower dimensions, the 24-cell is made of twenty-four octahedra
|
|
||||||
joined at 96 faces, with three around each edge. The 24-cell is
|
|
||||||
self-dual.`,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function make_120cell_vertices() {
|
||||||
// face detection for the 120-cell
|
|
||||||
|
|
||||||
// NOTE: all of these return node ids, not nodes
|
|
||||||
|
|
||||||
// return all the links which connect to a node
|
|
||||||
|
|
||||||
function nodes_links(links, nodeid) {
|
|
||||||
return links.filter((l) => l.source === nodeid || l.target === nodeid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter to remove a link to a given id from a set of links
|
|
||||||
|
|
||||||
function not_to_this(link, nodeid) {
|
|
||||||
return !(link.source === nodeid || link.target === nodeid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// given nodes n1, n2, return all neighbours of n2 which are not n1
|
|
||||||
|
|
||||||
function unmutuals(links, n1id, n2id) {
|
|
||||||
const nlinks = nodes_links(links, n2id).filter((l) => not_to_this(l, n1id));
|
|
||||||
return nlinks.map((l) => {
|
|
||||||
if( l.source === n2id ) {
|
|
||||||
return l.target;
|
|
||||||
} else {
|
|
||||||
return l.source;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function fingerprint(ids) {
|
|
||||||
const sids = [...ids];
|
|
||||||
sids.sort();
|
|
||||||
return sids.join(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function auto_120cell_faces(links) {
|
|
||||||
const faces = [];
|
|
||||||
const seen = {};
|
|
||||||
let id = 1;
|
|
||||||
for( const edge of links ) {
|
|
||||||
const v1 = edge.source;
|
|
||||||
const v2 = edge.target;
|
|
||||||
const n1 = unmutuals(links, v2, v1);
|
|
||||||
const n2 = unmutuals(links, v1, v2);
|
|
||||||
const shared = [];
|
|
||||||
for( const a of n1 ) {
|
|
||||||
const an = unmutuals(links, v1, a);
|
|
||||||
for( const d of n2 ) {
|
|
||||||
const dn = unmutuals(links, v2, d);
|
|
||||||
for( const x of an ) {
|
|
||||||
for( const y of dn ) {
|
|
||||||
if( x == y ) {
|
|
||||||
shared.push([v1, a, x, d, v2])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if( shared.length !== 3 ) {
|
|
||||||
console.log(`Bad shared faces for ${edge.id} ${v1} ${v2}`);
|
|
||||||
}
|
|
||||||
for( const face of shared ) {
|
|
||||||
const fp = fingerprint(face);
|
|
||||||
if( !seen[fp] ) {
|
|
||||||
faces.push({ id: id, nodes: face });
|
|
||||||
id++;
|
|
||||||
seen[fp] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return faces;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function make_120cell_vertices() {
|
|
||||||
const phi = 0.5 * (1 + Math.sqrt(5));
|
const phi = 0.5 * (1 + Math.sqrt(5));
|
||||||
const r5 = Math.sqrt(5);
|
const r5 = Math.sqrt(5);
|
||||||
const phi2 = phi * phi;
|
const phi2 = phi * phi;
|
||||||
|
@ -321,546 +131,42 @@ export function make_120cell_vertices() {
|
||||||
PERMUTE.coordinates([r5, phiinv, phi, 0], 0, true),
|
PERMUTE.coordinates([r5, phiinv, phi, 0], 0, true),
|
||||||
PERMUTE.coordinates([2, 1, phi, phiinv], 0, true),
|
PERMUTE.coordinates([2, 1, phi, phiinv], 0, true),
|
||||||
].flat();
|
].flat();
|
||||||
index_nodes(nodes);
|
return scale_and_index(nodes, 0.5);
|
||||||
scale_nodes(nodes, 0.25 * Math.sqrt(2));
|
|
||||||
return nodes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const cell120 = () => {
|
||||||
|
|
||||||
|
|
||||||
function label_nodes(nodes, ids, label) {
|
|
||||||
nodes.filter((n) => ids.includes(n.id)).map((n) => n.label = label);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function label_faces_120cell(nodes, faces, cfaces, label) {
|
|
||||||
const ns = new Set();
|
|
||||||
for( const fid of cfaces ) {
|
|
||||||
const face = faces.filter((f)=> f.id === fid );
|
|
||||||
if( face.length > 0 ) {
|
|
||||||
for ( const nid of face[0].nodes ) {
|
|
||||||
ns.add(nid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
label_nodes(nodes, Array.from(ns), label);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function link_labels(nodes, link) {
|
|
||||||
const n1 = nodes.filter((n) => n.id === link.source);
|
|
||||||
const n2 = nodes.filter((n) => n.id === link.target);
|
|
||||||
return [ n1[0].label, n2[0].label ];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// version of the 120-cell where nodes are partitioned by
|
|
||||||
// layer and the links follow that
|
|
||||||
|
|
||||||
export const cell120_layered = (max) => {
|
|
||||||
const nodes = make_120cell_vertices();
|
const nodes = make_120cell_vertices();
|
||||||
const links = auto_detect_edges(nodes, 4);
|
const links = auto_detect_edges(nodes, 4);
|
||||||
|
|
||||||
nodes.map((n) => n.label = 9); // make all invisible by default
|
|
||||||
|
|
||||||
for (const cstr in CELLINDEX.LAYERS120 ) {
|
|
||||||
label_nodes(nodes, CELLINDEX.LAYERS120[cstr], Number(cstr));
|
|
||||||
}
|
|
||||||
|
|
||||||
links.map((l) => {
|
|
||||||
const labels = link_labels(nodes, l);
|
|
||||||
if( labels[0] >= labels[1] ) {
|
|
||||||
l.label = labels[0];
|
|
||||||
} else {
|
|
||||||
l.label = labels[1];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const options = [];
|
|
||||||
const layers = [];
|
|
||||||
|
|
||||||
for( const i of [ 0, 1, 2, 3, 4, 5, 6, 7 ] ) {
|
|
||||||
layers.push(i);
|
|
||||||
options.push({
|
|
||||||
name: CELLINDEX.LAYER_NAMES[i],
|
|
||||||
links: [...layers],
|
|
||||||
nodes: [...layers]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: '120-cell layered',
|
|
||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
links: links,
|
links: links
|
||||||
nolink2opacity: true,
|
|
||||||
options: options,
|
|
||||||
description: `This version of the 120-cell lets you explore its
|
|
||||||
structure by building each layer from the 'north pole' onwards.`,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function make_600cell_vertices() {
|
||||||
|
const phi = 0.5 * (1 + Math.sqrt(5));
|
||||||
export const cell120_inscribed = () => {
|
|
||||||
const nodes = make_120cell_vertices();
|
|
||||||
const links = auto_detect_edges(nodes, 4);
|
|
||||||
|
|
||||||
for( const cstr in CELLINDEX.INDEX120 ) {
|
|
||||||
label_nodes(nodes, CELLINDEX.INDEX120[cstr], Number(cstr));
|
|
||||||
}
|
|
||||||
|
|
||||||
links.map((l) => l.label = 0);
|
|
||||||
|
|
||||||
for( const p of [ 1, 2, 3, 4, 5 ]) {
|
|
||||||
const nodes600 = nodes.filter((n) => n.label === p);
|
|
||||||
const links600 = auto_detect_edges(nodes600, 12);
|
|
||||||
links600.map((l) => l.label = p);
|
|
||||||
links.push(...links600);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: '120-cell',
|
|
||||||
nodes: nodes,
|
|
||||||
links: links,
|
|
||||||
options: [
|
|
||||||
{ name: "none", links: [ 0 ]},
|
|
||||||
{ name: "one inscribed 600-cell", links: [ 0, 1 ] },
|
|
||||||
{ name: "five inscribed 600-cells", links: [ 0, 1, 2, 3, 4, 5 ] }
|
|
||||||
],
|
|
||||||
description: `The 120-cell is the four-dimensional analogue of the
|
|
||||||
dodecahedron, and consists of 120 dodecahedra joined at 720 faces,
|
|
||||||
with three dodecahedra around each edge. It is dual to the 600-cell,
|
|
||||||
and five 600-cells can be inscribed in its vertices.`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function partition_coord(i, coords, invert) {
|
|
||||||
const j = invert ? -i : i;
|
|
||||||
if( j >= 0 ) {
|
|
||||||
return coords[j];
|
|
||||||
}
|
|
||||||
return "-" + coords[-j];
|
|
||||||
}
|
|
||||||
|
|
||||||
function partition_fingerprint(n, coords, invert) {
|
|
||||||
const p = ['x','y','z','w'].map((a) => partition_coord(n[a], coords, invert));
|
|
||||||
const fp = p.join(',');
|
|
||||||
return fp;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function label_vertex(n, coords, partition) {
|
|
||||||
const fp = partition_fingerprint(n, coords, false);
|
|
||||||
if( fp in partition ) {
|
|
||||||
return partition[fp];
|
|
||||||
} else {
|
|
||||||
const ifp = partition_fingerprint(n, coords, true);
|
|
||||||
if( ifp in partition ) {
|
|
||||||
return partition[ifp];
|
|
||||||
}
|
|
||||||
console.log(`Map for ${fp} ${ifp} not found`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function map_coord(i, coords, values) {
|
|
||||||
if( i >= 0 ) {
|
|
||||||
return values[coords[i]];
|
|
||||||
}
|
|
||||||
return -values[coords[-i]];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function make_600cell_vertices() {
|
|
||||||
const coords = {
|
|
||||||
0: '0',
|
|
||||||
1: '1',
|
|
||||||
2: '2',
|
|
||||||
3: 't',
|
|
||||||
4: 'k'
|
|
||||||
};
|
|
||||||
const t = 0.5 * (1 + Math.sqrt(5));
|
|
||||||
const values = {
|
|
||||||
'0': 0,
|
|
||||||
'1': 1,
|
|
||||||
'2': 2,
|
|
||||||
't': t,
|
|
||||||
'k': 1 / t
|
|
||||||
};
|
|
||||||
|
|
||||||
const nodes = [
|
const nodes = [
|
||||||
PERMUTE.coordinates([0, 0, 0, 2], 0),
|
PERMUTE.coordinates([0, 0, 0, 2], 0),
|
||||||
PERMUTE.coordinates([1, 1, 1, 1], 0),
|
PERMUTE.coordinates([1, 1, 1, 1], 0),
|
||||||
PERMUTE.coordinates([3, 1, 4, 0], 0, true)
|
|
||||||
].flat();
|
|
||||||
|
|
||||||
for( const n of nodes ) {
|
PERMUTE.coordinates([phi, 1, 1 / phi, 0], 0, true)
|
||||||
n.label = label_vertex(n, coords, CELLINDEX.PARTITION600);
|
].flat();
|
||||||
}
|
return scale_and_index(nodes, 0.75);
|
||||||
|
|
||||||
for( const n of nodes ) {
|
|
||||||
for( const a of [ 'x', 'y', 'z', 'w'] ) {
|
|
||||||
n[a] = map_coord(n[a], coords, values);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
index_nodes(nodes);
|
|
||||||
scale_nodes(nodes, 0.5);
|
|
||||||
return nodes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_node(nodes, id) {
|
|
||||||
const ns = nodes.filter((n) => n.id === id);
|
|
||||||
if( ns ) {
|
|
||||||
return ns[0]
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function audit_link_labels(nodes, links) {
|
|
||||||
for( const l of links ) {
|
|
||||||
const n1 = get_node(nodes, l.source);
|
|
||||||
const n2 = get_node(nodes, l.target);
|
|
||||||
if( n1.label === n2.label ) {
|
|
||||||
console.log(`link ${l.id} joins ${n1.id} ${n2.id} with label ${n2.label}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const cell600 = () => {
|
export const cell600 = () => {
|
||||||
const nodes = make_600cell_vertices();
|
const nodes = make_600cell_vertices();
|
||||||
const links = auto_detect_edges(nodes, 12);
|
const links = auto_detect_edges(nodes, 20);
|
||||||
|
|
||||||
links.map((l) => l.label = 0);
|
|
||||||
|
|
||||||
for( const p of [1, 2, 3, 4, 5]) {
|
|
||||||
const nodes24 = nodes.filter((n) => n.label === p);
|
|
||||||
const links24 = auto_detect_edges(nodes24, 8);
|
|
||||||
links24.map((l) => l.label = p);
|
|
||||||
links.push(...links24);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: '600-cell',
|
|
||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
links: links,
|
links: links
|
||||||
options: [
|
|
||||||
{ name: "none", links: [ 0 ]},
|
|
||||||
{ name: "one 24-cell", links: [ 0, 1 ] },
|
|
||||||
{ name: "five 24-cells", links: [ 0, 1, 2, 3, 4, 5 ] }
|
|
||||||
],
|
|
||||||
description: `The 600-cell is the four-dimensional analogue of the
|
|
||||||
icosahedron, and consists of 600 tetrahedra joined at 1200 faces
|
|
||||||
with five tetrahedra around each edge. It is dual to the 120-cell.
|
|
||||||
Its 120 vertices can be partitioned into five sets which form the
|
|
||||||
vertices of five inscribed 24-cells.`,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const cell600_layered = () => {
|
|
||||||
const nodes = make_600cell_vertices();
|
|
||||||
const links = auto_detect_edges(nodes, 12);
|
|
||||||
|
|
||||||
nodes.map((n) => n.label = 9); // make all invisible by default
|
|
||||||
|
|
||||||
for (const cstr in CELLINDEX.LAYERS600 ) {
|
|
||||||
label_nodes(nodes, CELLINDEX.LAYERS600[cstr], Number(cstr));
|
|
||||||
}
|
|
||||||
|
|
||||||
links.map((l) => {
|
|
||||||
const labels = link_labels(nodes, l);
|
|
||||||
if( labels[0] >= labels[1] ) {
|
|
||||||
l.label = labels[0];
|
|
||||||
} else {
|
|
||||||
l.label = labels[1];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const options = [];
|
|
||||||
const layers = [];
|
|
||||||
|
|
||||||
for( const i of [ 0, 1, 2, 3, 4, 5, 6, 7 ] ) {
|
|
||||||
layers.push(i);
|
|
||||||
options.push({
|
|
||||||
name: CELLINDEX.LAYER_NAMES[i],
|
|
||||||
links: [...layers],
|
|
||||||
nodes: [...layers]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: '600-cell layered',
|
|
||||||
nodes: nodes,
|
|
||||||
links: links,
|
|
||||||
nolink2opacity: true,
|
|
||||||
options: options,
|
|
||||||
description: `This version of the 600-cell lets you explore its
|
|
||||||
structure by building each layer from the 'north pole' onwards.`,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const snub24cell = () => {
|
|
||||||
const nodes600 = make_600cell_vertices();
|
|
||||||
const links600 = auto_detect_edges(nodes600, 12);
|
|
||||||
|
|
||||||
const nodes = nodes600.filter((n) => n.label != 1);
|
|
||||||
const links = links600.filter((l) => {
|
|
||||||
const sn = node_by_id(nodes, l.source);
|
|
||||||
const tn = node_by_id(nodes, l.target);
|
|
||||||
return sn && tn;
|
|
||||||
});
|
|
||||||
|
|
||||||
links.map((l) => l.label = 0);
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: 'Snub 24-cell',
|
|
||||||
nodes: nodes,
|
|
||||||
links: links,
|
|
||||||
options: [ { name: "--" } ],
|
|
||||||
description: `The snub 24-cell is a semiregular polytope which
|
|
||||||
connects the 24-cell with the 600-cell. It consists of 24 icosahedra
|
|
||||||
and 120 tetrahedra, and is constructed by removing one of the
|
|
||||||
five inscribed 24-cells from a 600-cell.`
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function make_dodecahedron_vertices() {
|
|
||||||
const phi = 0.5 * (1 + Math.sqrt(5));
|
|
||||||
const phiinv = 1 / phi;
|
|
||||||
|
|
||||||
const nodes = [
|
|
||||||
{ x: 1, y: 1, z: 1, w: 0, label: 4 },
|
|
||||||
{ x: 1, y: 1, z: -1, w: 0, label: 3 },
|
|
||||||
{ x: 1, y: -1, z: 1, w: 0, label: 3 },
|
|
||||||
{ x: 1, y: -1, z: -1, w: 0, label: 2 },
|
|
||||||
|
|
||||||
{ x: -1, y: 1, z: 1, w: 0, label: 3 },
|
|
||||||
{ x: -1, y: 1, z: -1, w: 0, label: 1 },
|
|
||||||
{ x: -1, y: -1, z: 1, w: 0, label: 5 },
|
|
||||||
{ x: -1, y: -1, z: -1, w: 0, label: 3 },
|
|
||||||
|
|
||||||
{ x: 0, y: phi, z: phiinv, w: 0, label: 5 },
|
|
||||||
{ x: 0, y: phi, z: -phiinv, w: 0 , label: 2 },
|
|
||||||
{ x: 0, y: -phi, z: phiinv, w: 0, label: 4 },
|
|
||||||
{ x: 0, y: -phi, z: -phiinv, w: 0 , label: 1 },
|
|
||||||
|
|
||||||
{ x: phiinv, y: 0, z: phi, w: 0 , label: 2},
|
|
||||||
{ x: phiinv, y: 0, z: -phi, w: 0 , label: 4},
|
|
||||||
{ x: -phiinv, y: 0, z: phi, w: 0 , label: 1},
|
|
||||||
{ x: -phiinv, y: 0, z: -phi, w: 0 , label: 5},
|
|
||||||
|
|
||||||
{ x: phi, y: phiinv, z:0, w: 0 , label: 1},
|
|
||||||
{ x: phi, y: -phiinv, z:0, w: 0 , label: 5},
|
|
||||||
{ x: -phi, y: phiinv, z:0, w: 0 , label: 4},
|
|
||||||
{ x: -phi, y: -phiinv, z:0, w: 0 , label: 2},
|
|
||||||
];
|
|
||||||
scale_nodes(nodes, 1 / Math.sqrt(3));
|
|
||||||
index_nodes(nodes);
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dodecahedron = () => {
|
|
||||||
const nodes = make_dodecahedron_vertices();
|
|
||||||
const links = auto_detect_edges(nodes, 3);
|
|
||||||
links.map((l) => l.label = 0);
|
|
||||||
|
|
||||||
for( const p of [ 1, 2, 3, 4, 5 ]) {
|
|
||||||
const tetran = nodes.filter((n) => n.label === p);
|
|
||||||
const tetral = auto_detect_edges(tetran, 3);
|
|
||||||
tetral.map((l) => l.label = p);
|
|
||||||
links.push(...tetral);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: 'Dodecahedron',
|
|
||||||
nodes: nodes,
|
|
||||||
links: links,
|
|
||||||
options: [
|
|
||||||
{ name: "none", links: [ 0 ]},
|
|
||||||
{ name: "one tetrahedron", links: [ 0, 1 ] },
|
|
||||||
{ name: "five tetrahedra", links: [ 0, 1, 2, 3, 4, 5 ] }
|
|
||||||
],
|
|
||||||
description: `The dodecahedron is a three-dimensional polyhedron
|
|
||||||
which is included here so that you can see the partition of its
|
|
||||||
vertices into five interlocked tetrahedra. This structure is the
|
|
||||||
basis for the partition of the 120-cell's vertices into five
|
|
||||||
600-cells.`
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const tetrahedron = () => {
|
|
||||||
const r2 = Math.sqrt(2);
|
|
||||||
const r3 = Math.sqrt(3);
|
|
||||||
return {
|
|
||||||
name: 'Tetrahedron',
|
|
||||||
nodes: [
|
|
||||||
{id:1, label: 1, x: 2 * r2 / 3, y: 0, z: -1/3, w: 0 },
|
|
||||||
{id:2, label: 2, x: -r2 / 3, y: r2 / r3, z: -1/3, w: 0 },
|
|
||||||
{id:3, label: 3, x: -r2 / 3, y: -r2 / r3, z: -1/3, w: 0 },
|
|
||||||
{id:4, label: 4, x: 0, y: 0, z: 1, w: 0 },
|
|
||||||
],
|
|
||||||
links: [
|
|
||||||
{ id:1, source:1, target: 2},
|
|
||||||
{ id:2, source:1, target: 3},
|
|
||||||
{ id:3, source:1, target: 4},
|
|
||||||
{ id:4, source:2, target: 3},
|
|
||||||
{ id:5, source:2, target: 4},
|
|
||||||
{ id:6, source:3, target: 4},
|
|
||||||
],
|
|
||||||
options: [ { name: '--' }],
|
|
||||||
description: `The simplest three-dimensional polytope, consisting of four triangles joined at six edges. The 5-cell is its four-dimensional analogue.`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const octahedron = () => {
|
|
||||||
const nodes = [
|
|
||||||
{id: 1, label: 1, x: 1, y: 0, z: 0, w: 0},
|
|
||||||
{id: 2, label: 1, x: -1, y: 0, z: 0, w: 0},
|
|
||||||
{id: 3, label: 2, x: 0, y: 1, z: 0, w: 0},
|
|
||||||
{id: 4, label: 2, x: 0, y: -1, z: 0, w: 0},
|
|
||||||
{id: 5, label: 3, x: 0, y: 0, z: 1, w: 0},
|
|
||||||
{id: 6, label: 3, x: 0, y: 0, z: -1, w: 0},
|
|
||||||
];
|
|
||||||
const links = [
|
|
||||||
{id:1, source: 1, target: 3},
|
|
||||||
{id:2, source: 1, target: 4},
|
|
||||||
{id:3, source: 1, target: 5},
|
|
||||||
{id:4, source: 1, target: 6},
|
|
||||||
{id:5, source: 2, target: 3},
|
|
||||||
{id:6, source: 2, target: 4},
|
|
||||||
{id:7, source: 2, target: 5},
|
|
||||||
{id:8, source: 2, target: 6},
|
|
||||||
{id:9, source: 3, target: 5},
|
|
||||||
{id:10, source: 3, target: 6},
|
|
||||||
{id:11, source: 4, target: 5},
|
|
||||||
{id:12, source: 4, target: 6},
|
|
||||||
]
|
|
||||||
links.map((l) => { l.label = 0 });
|
|
||||||
return {
|
|
||||||
name: 'Octahedron',
|
|
||||||
nodes: nodes,
|
|
||||||
links: links,
|
|
||||||
options: [ { name: '--' }],
|
|
||||||
description: `The three-dimensional cross-polytope, the 16-cell is its four-dimensional analogue.`,
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export const cube = () => {
|
|
||||||
const nodes = [
|
|
||||||
{id: 1, label: 1, x: 1, y: 1, z: 1, w: 0},
|
|
||||||
{id: 2, label: 2, x: -1, y: 1, z: 1, w: 0},
|
|
||||||
{id: 3, label: 2, x: 1, y: -1, z: 1, w: 0},
|
|
||||||
{id: 4, label: 1, x: -1, y: -1, z: 1, w: 0},
|
|
||||||
{id: 5, label: 2, x: 1, y: 1, z: -1, w: 0},
|
|
||||||
{id: 6, label: 1, x: -1, y: 1, z: -1, w: 0},
|
|
||||||
{id: 7, label: 1, x: 1, y: -1, z: -1, w: 0},
|
|
||||||
{id: 8, label: 2, x: -1, y: -1, z: -1, w: 0},
|
|
||||||
];
|
|
||||||
scale_nodes(nodes, 1/Math.sqrt(3));
|
|
||||||
const links = auto_detect_edges(nodes, 3);
|
|
||||||
links.map((l) => { l.label = 0 });
|
|
||||||
return {
|
|
||||||
name: 'Cube',
|
|
||||||
nodes: nodes,
|
|
||||||
links: links,
|
|
||||||
options: [ { name: '--' }],
|
|
||||||
description: `The three-dimensional measure polytope, the tesseract is its four-dimensional analogue.`,
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function make_icosahedron_vertices() {
|
|
||||||
const phi = 0.5 * (1 + Math.sqrt(5));
|
|
||||||
|
|
||||||
const nodes = [
|
|
||||||
{ x: 0, y: 1, z: phi, w: 0, label: 1 },
|
|
||||||
{ x: 0, y: -1, z: phi, w: 0, label: 1 },
|
|
||||||
{ x: 0, y: 1, z: -phi, w: 0, label: 1 },
|
|
||||||
{ x: 0, y: -1, z: -phi, w: 0, label: 1 },
|
|
||||||
{ x: 1, y: phi, z: 0, w: 0, label: 2 },
|
|
||||||
{ x: -1, y: phi, z: 0, w: 0, label: 2 },
|
|
||||||
{ x: 1, y: -phi, z: 0, w: 0, label: 2 },
|
|
||||||
{ x: -1, y: -phi, z: 0, w: 0, label: 2 },
|
|
||||||
{ x: phi, y: 0, z: 1, w: 0, label: 3},
|
|
||||||
{ x: phi, y: 0, z: -1, w: 0, label: 3},
|
|
||||||
{ x: -phi, y: 0, z: 1, w: 0, label: 3},
|
|
||||||
{ x: -phi, y: 0, z: -1, w: 0, label: 3},
|
|
||||||
];
|
|
||||||
|
|
||||||
scale_nodes(nodes, 1/Math.sqrt((5 + Math.sqrt(5)) / 2));
|
|
||||||
index_nodes(nodes);
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const icosahedron = () => {
|
|
||||||
const nodes = make_icosahedron_vertices();
|
|
||||||
const links = auto_detect_edges(nodes, 5);
|
|
||||||
links.map((l) => l.label = 0);
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: 'Icosahedron',
|
|
||||||
nodes: nodes,
|
|
||||||
links: links,
|
|
||||||
options: [
|
|
||||||
{ name: "--"},
|
|
||||||
],
|
|
||||||
description: `The icosahedron is a twenty-sided polyhedron and is dual to the dodecahedron. Its four-dimensional analogue is the 600-cell.`
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const build_all = () => {
|
|
||||||
return [
|
|
||||||
tetrahedron(),
|
|
||||||
octahedron(),
|
|
||||||
cube(),
|
|
||||||
icosahedron(),
|
|
||||||
dodecahedron(),
|
|
||||||
cell5(),
|
|
||||||
cell16(),
|
|
||||||
tesseract(),
|
|
||||||
cell24(),
|
|
||||||
snub24cell(),
|
|
||||||
cell600(),
|
|
||||||
cell600_layered(),
|
|
||||||
cell120_inscribed(),
|
|
||||||
cell120_layered()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const radii = (shape) => {
|
|
||||||
return shape.nodes.map(n => Math.sqrt(n.x * n.x + n.y * n.y + n.z * n.z + n.w * n.w))
|
|
||||||
}
|
|
85
rotation.js
85
rotation.js
|
@ -1,85 +0,0 @@
|
||||||
// hacky stuff for 4d rotations
|
|
||||||
|
|
||||||
// see https://math.stackexchange.com/questions/1402362/can-rotations-in-4d-be-given-an-explicit-matrix-form#1402376
|
|
||||||
|
|
||||||
import * as THREE from 'three';
|
|
||||||
|
|
||||||
|
|
||||||
function rotZW(theta) {
|
|
||||||
const ctheta = Math.cos(theta);
|
|
||||||
const stheta = Math.sin(theta);
|
|
||||||
return new THREE.Matrix4(
|
|
||||||
ctheta, -stheta, 0, 0,
|
|
||||||
stheta, ctheta, 0, 0,
|
|
||||||
0, 0, 1, 0,
|
|
||||||
0, 0, 0, 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotYW(theta) {
|
|
||||||
const ctheta = Math.cos(theta);
|
|
||||||
const stheta = Math.sin(theta);
|
|
||||||
return new THREE.Matrix4(
|
|
||||||
ctheta, 0, -stheta, 0,
|
|
||||||
0, 1, 0, 0,
|
|
||||||
stheta, 0, ctheta, 0,
|
|
||||||
0, 0, 0, 1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotYZ(theta) {
|
|
||||||
const ctheta = Math.cos(theta);
|
|
||||||
const stheta = Math.sin(theta);
|
|
||||||
return new THREE.Matrix4(
|
|
||||||
ctheta, 0, 0, -stheta,
|
|
||||||
0, 1, 0, 0,
|
|
||||||
0, 0, 1, 0,
|
|
||||||
stheta, 0, 0, ctheta,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotXW(theta) {
|
|
||||||
const ctheta = Math.cos(theta);
|
|
||||||
const stheta = Math.sin(theta);
|
|
||||||
return new THREE.Matrix4(
|
|
||||||
1, 0, 0, 0,
|
|
||||||
0, ctheta, -stheta, 0,
|
|
||||||
0, stheta, ctheta, 0,
|
|
||||||
0, 0, 0, 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotXZ(theta) {
|
|
||||||
const ctheta = Math.cos(theta);
|
|
||||||
const stheta = Math.sin(theta);
|
|
||||||
return new THREE.Matrix4(
|
|
||||||
1, 0, 0, 0,
|
|
||||||
0, ctheta, 0, -stheta,
|
|
||||||
0, 0, 1, 0,
|
|
||||||
0, stheta, 0, ctheta,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotXY(theta) {
|
|
||||||
const ctheta = Math.cos(theta);
|
|
||||||
const stheta = Math.sin(theta);
|
|
||||||
return new THREE.Matrix4(
|
|
||||||
1, 0, 0, 0,
|
|
||||||
0, 1, 0, 0,
|
|
||||||
0, 0, ctheta, -stheta,
|
|
||||||
0, 0, stheta, ctheta,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const rotfn = {
|
|
||||||
XY: rotXY,
|
|
||||||
XZ: rotXZ,
|
|
||||||
XW: rotXW,
|
|
||||||
YZ: rotYZ,
|
|
||||||
YW: rotYW,
|
|
||||||
ZW: rotZW,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,5 @@
|
||||||
import { defineConfig, loadEnv } from 'vite';
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: '/fourjs/',
|
base: '/fourjs/'
|
||||||
build: {
|
|
||||||
rollupOptions: {
|
|
||||||
output: {
|
|
||||||
manualChunks: {
|
|
||||||
threejs: [ 'three' ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue