Compare commits

..

141 Commits

Author SHA1 Message Date
Mike Lynch 0ada3fce6f A few more ux refinements, and added the ability to hide captions 2024-04-26 18:31:27 +10:00
Mike Lynch 3f83bde533 Very cursed but entertaining bug as I try to get links scaling well 2024-04-26 16:57:21 +10:00
Mike Lynch 6f4d4cc633 Link foreshortening works, but updating the geometry of every edge
is making large objects like the 120-cell noticeably stuttery
2024-04-26 11:42:37 +10:00
Mike Lynch fb9c78d82f Fixed some scale problems 2024-04-26 08:59:26 +10:00
Mike Lynch e478abe7c6 Scaled the ends of the links so that they have w-perspective 2024-04-26 07:34:27 +10:00
Mike Lynch f79a90e0d9 Added the rest of the regular 3-d polyhedra 2024-04-25 12:38:40 +10:00
Mike Lynch 78ebb381ee Played around with the hyperplane and zoom so that it all looks
better with unit radius normalisation
2024-04-25 11:25:01 +10:00
Mike Lynch f99901f1b0 Normalised dodecahedron to unit radius 2024-04-25 11:07:21 +10:00
Mike Lynch 39fe6e5e40 Normalised 120-cell to unit radius 2024-04-25 11:05:23 +10:00
Mike Lynch 836e0d5ab6 Normalised 600-cell and snub 24-cell to unit radius 2024-04-25 11:03:11 +10:00
Mike Lynch 0be8c47608 Normalised 24-cell to unit radius 2024-04-25 11:00:14 +10:00
Mike Lynch 5e31403420 Normalised tesseract to unit radius 2024-04-25 10:58:41 +10:00
Mike Lynch aba20124db Normalised 5-cell and 16-cell to unit radius 2024-04-25 10:57:03 +10:00
Mike Lynch 1ec7955861 Merge branch 'feature-node-foreshortening' 2024-04-14 16:10:23 +10:00
Mike Lynch 1e5db22c25 scale factor for node foreshortening 2024-04-14 16:08:46 +10:00
Mike Lynch 680f9997f9 Added descriptions for each of the shapes 2024-04-14 16:05:17 +10:00
Mike Lynch cab5878ac8 Added node size scaling with w-foreshortening - looks kind of goofy 2024-04-09 15:05:39 +10:00
Mike Lynch b22ac6546d Node saturation now matches the basis colour saturation too 2024-04-07 12:38:06 +10:00
Mike Lynch eafc906210 Using HSL to derive the colour scheme 2024-04-07 11:53:33 +10:00
Mike Lynch 3d64c73a5e Fixed bug introduced when reverting the simplified rotation UU 2024-04-07 11:37:21 +10:00
Mike Lynch bc9e86d918 Revert "Simplified rotation ui"
This reverts commit 6b0c5cf97e.
2024-04-07 11:30:52 +10:00
bombinans 01a12bfe2a Merge pull request 'Added model of snub 24-cell' (#13) from feature-snub-24-cell into main
Reviewed-on: #13
2024-04-06 22:11:46 +00:00
Mike Lynch 9d303afaa0 Added model of snub 24-cell 2024-04-07 08:10:26 +10:00
bombinans 973d1d944b Merge pull request '600-cell-layers' (#12) from 600-cell-layers into main
Reviewed-on: #12
2023-11-04 05:35:59 +00:00
Mike Lynch 777b36d048 Finished layered view of 600-cell, refactored cellindex and moved all
of the reference data there.
2023-11-04 16:30:46 +11:00
Mike Lynch 6c360fcafd Start of code to build tetrahedra from 600-cell 2023-11-03 12:57:40 +11:00
Mike Lynch 7f35056ab8 Made default thickness and nodesize a bit bigger 2023-11-03 10:38:35 +11:00
bombinans 1b9f4478a8 Merge pull request 'feature-more-ui-tweaks' (#11) from feature-more-ui-tweaks into main
Reviewed-on: #11
2023-11-02 22:59:07 +00:00
Mike Lynch bef56b211b Updated lil-gui to 0.19.0 so options control doesn't jump to the end
when you select a different shape
2023-11-03 09:56:19 +11:00
Mike Lynch e805c64de6 Added zoom control 2023-11-03 09:55:44 +11:00
bombinans 029b14f53a Merge pull request 'Automatically shifts the camera distance to match the hyperplane.' (#10) from feature-improve-hyperplane into main
Reviewed-on: #10
2023-11-02 07:41:43 +00:00
Mike Lynch d9e7ae716b Automatically shifts the camera distance to match the hyperplane.
Put back 120-cell as the default.
2023-11-02 18:40:37 +11:00
Mike Lynch 25a2c5ebe6 Added a manual chunks setting to stop Vite complaining on build 2023-11-02 16:52:40 +11:00
bombinans 5494660d76 Merge pull request 'bugfix-layered-links' (#9) from bugfix-layered-links into main
Reviewed-on: #9
2023-11-02 05:37:33 +00:00
Mike Lynch 46dfb808f4 Added better layer names 2023-11-02 16:36:18 +11:00
Mike Lynch e7cb1856e0 Added a flag to turn off link2opacity for layered 120-cell 2023-11-02 16:26:48 +11:00
bombinans c281c4569a Merge pull request 'feature-120-cell-layers' (#7) from feature-120-cell-layers into main
Reviewed-on: #7
2023-11-02 03:15:51 +00:00
Mike Lynch c883bd7406 Cleaned up some redundant old stuff 2023-11-02 14:13:45 +11:00
Mike Lynch 6f1c5a58e6 Finished layer-by-layer visualisation of 120-cell 2023-11-02 14:10:13 +11:00
Mike Lynch 7323935a1f Added layered 120-cell 2023-11-01 12:52:59 +11:00
Mike Lynch 6d9610a1c6 options menu gets sensible default when shape changes 2023-11-01 11:57:46 +11:00
Mike Lynch c481d24f3c Refactored dodecahedron 2023-11-01 11:52:48 +11:00
Mike Lynch 029e6f3161 Added 600-cell and 120-cell 2023-11-01 11:49:42 +11:00
Mike Lynch 4bf38858ea Added 24-cell 2023-11-01 11:43:54 +11:00
Mike Lynch 50214adbe3 Reinstated 5-cell and 16-cell 2023-11-01 11:40:23 +11:00
Mike Lynch 71c6aa62ac Removed old inscribed tesseract 2023-11-01 11:15:18 +11:00
Mike Lynch 76f463ae03 Added refactored tesseract 2023-11-01 11:14:57 +11:00
Mike Lynch 42d1871a9e Improved option defaults and URL params 2023-11-01 10:47:46 +11:00
Mike Lynch 203850ba39 First draft of refactored optional / visible nodes and links 2023-11-01 10:22:26 +11:00
Mike Lynch 06c1b074b3 Started generalising / unifying inscribed links and layers as different
ways of picking out parts of a structure
2023-10-27 17:32:44 +11:00
Mike Lynch 2f59c0b3a5 Added a callback to set link and node visibility based on label at
the render stage
2023-10-27 12:45:46 +11:00
Mike Lynch 944416f92b Visualisations of combinations of layers 2023-10-27 10:42:35 +11:00
Mike Lynch 8c256629b1 Added a new partition of the 120-cell into layers 2023-10-27 10:18:54 +11:00
Mike Lynch fb1d6ddd45 Removed logging 2023-10-18 11:22:24 +11:00
Mike Lynch 1f6c35c7b8 Refactored label120cell.js so that it works a bit better on the repl 2023-10-18 11:22:13 +11:00
Mike Lynch 88049ca891 renamed testbed.js to label120cell.js 2023-10-18 10:21:28 +11:00
bombinans bf8356b0a2 Merge pull request 'feature-update-link' (#3) from feature-update-link into main
Reviewed-on: #3
2023-09-30 02:53:16 +00:00
Mike Lynch e539f4f3af Changed source link to go to Gitea 2023-09-30 12:48:34 +10:00
Mike Lynch 6e8cfa8763 Added tetrahedra inscriptions for dodecahedron, rearranged UI 2023-09-15 18:33:33 +10:00
Mike Lynch fb8067a13e added a dodecahedron 2023-09-14 16:42:20 +10:00
Mike Lynch 63b2f8f22e Improved colours so that the smaller polytopes still get high-contrast
nodes and inscriptions
2023-09-10 18:26:15 +10:00
Mike Lynch 229218ec40 Inscriptions of 16-cells in 24-cell 2023-09-09 17:50:43 +10:00
Mike Lynch 7c49116bba Added tesseract inscriptions 2023-09-09 17:47:49 +10:00
Mike Lynch fc5acaeda8 Added inscriptions for 600-cell, improved the gui, you can now see
all five 600-cells in the 120-cell for full mindbending
2023-09-06 18:03:44 +10:00
Mike Lynch 73c1cb0193 Added credits and link to github 2023-09-02 17:52:06 +10:00
Mike Lynch 6b0c5cf97e Simplified rotation ui 2023-09-02 17:41:00 +10:00
Mike Lynch 62ae3486d8 Refactored the gui defaults and added a 120-cell with one inscribed
600-cell
2023-09-02 17:11:08 +10:00
Mike Lynch 41d8f7ed92 Updated indexing for the smaller polytopes 2023-09-01 19:00:55 +10:00
Mike Lynch f9d59a777d Merge branch 'feature-120-cell-index' 2023-09-01 18:07:59 +10:00
Mike Lynch 1581f3d31f Added separate file for 120-cell index 2023-09-01 18:07:41 +10:00
Mike Lynch 9f08a82c0c Rearranging things a bit before publishing 2023-09-01 18:04:19 +10:00
Mike Lynch 26f927daae Building it up layer by layer 2023-09-01 17:34:27 +10:00
Mike Lynch c093c89ce6 Indexing algorithm tested on the 50-cell skeleton of great circles
without any deadlocks or label mismatches
2023-08-28 08:31:45 +10:00
Mike Lynch 43ed181d75 might have a working version of coherent indexing, tests ok with
a great circle
2023-08-27 18:14:51 +10:00
Mike Lynch f3bd62d2c2 Merge branch 'feature-120-cell-index' 2023-08-26 16:03:08 +10:00
Mike Lynch f4176b9ced successfully traversed a great circle 2023-08-26 16:02:48 +10:00
Mike Lynch 3b806c3796 Basic function to colour a single dodecahedron defined by an face and
a neighbouring vertex
2023-08-26 13:54:44 +10:00
Mike Lynch 8a926f0552 Successfully auto-coloured a single dodecahedron 2023-08-25 18:08:22 +10:00
Mike Lynch 2808b256a2 Manual compound-of-tetrahedra colouring 2023-08-21 17:25:51 +10:00
Mike Lynch 6c6402bad9 Merge branch 'feature-120-cell-index' 2023-08-20 11:29:19 +10:00
Mike Lynch 02cfdc5f80 Stored the test 120-cell colours as a static array for performance 2023-08-20 11:28:13 +10:00
Mike Lynch 6436efece2 Moved the 120-cell dodecahedron detection code to its own file, and
tested it colouring every dodecahedron when it builds the 120-cell:
looks good
2023-08-20 11:18:29 +10:00
Mike Lynch 585627f140 Brute-force dodecahedron detector 2023-08-20 10:50:28 +10:00
Mike Lynch 32644b5d1e Can get the two dodecahedra which a face belongs to 2023-08-20 10:29:50 +10:00
Mike Lynch 228d1a91c4 Dodecahedron detector is working on a single test case 2023-08-20 09:46:30 +10:00
Mike Lynch 5a09caef93 Getting towards an auto-dodecahedon detector for the 120-cell 2023-08-19 18:00:19 +10:00
Mike Lynch aa5501e14a Moved the unsuccessful indexing code out of testbed and renamed it 2023-08-19 16:23:29 +10:00
Mike Lynch 34c9755bd1 Merge remote-tracking branch 'origin/feature-120-cell-index' into feature-120-cell-index 2023-08-17 08:33:08 +10:00
Mike Lynch e1047a46e8 Manual labelling 2023-08-17 08:32:41 +10:00
Mike Lynch 147b9af466 Manual indexing ideas 2023-08-16 09:40:11 +10:00
Mike Lynch 2f609594e5 Faces and stuff 2023-08-13 17:51:25 +10:00
Mike Lynch 7cc7331db1 Merge branch 'feature-faces' into temp-merge-faces 2023-08-12 07:33:38 +10:00
Mike Lynch 7ac70fd261 Added return from interate callback 2023-08-12 07:30:36 +10:00
Mike Lynch 85a171da22 Fixed syntax error but the naive version of the tetrahedral
algorithm doesn't work
2023-08-11 21:08:39 +10:00
Mike Lynch a412b5d8aa Broken, don't understand why yet 2023-08-11 18:07:12 +10:00
Mike Lynch 9c33831d83 Tetrahedra 2023-08-11 07:33:45 +10:00
Mike Lynch 971a791339 Added indexing for tesseract 2023-08-06 18:54:32 +10:00
Mike Lynch c3766b749b Hour of nutty hacking and it's labelling 105 nodes 2023-08-06 13:41:01 +10:00
Mike Lynch 62bc994788 More documentation of what I'm doing with the 120 cell 2023-08-06 11:26:38 +10:00
Mike Lynch d82e057e6b Added label_nodes and used it to test manual icosahedral partitions 2023-08-06 11:26:23 +10:00
Mike Lynch c5d2427db2 testbed version of function to partition the chords from a vertex
into two disjoint icosahedral sets
2023-08-06 11:25:55 +10:00
Mike Lynch a38c852178 Made 120-cell the default and changed order in the gui so it's last 2023-08-06 11:25:09 +10:00
Mike Lynch 74cea5750d Changed import to require in permute_testbed 2023-08-06 08:42:23 +10:00
Mike Lynch 4ba8b0617c Added notes/planning for 120-cell partition 2023-08-06 08:41:44 +10:00
Mike Lynch 372a9c1a37 fixed bug with nodesize from links 2023-08-04 17:11:41 +10:00
Mike Lynch a076427af3 Added manual coherent indexing for 16-cell 2023-08-04 16:55:19 +10:00
Mike Lynch ec56fdd75b Indexing for 5-cell 2023-08-04 15:56:21 +10:00
Mike Lynch b96fa62a08 Fixed node colours from params 2023-08-04 15:28:08 +10:00
Mike Lynch db5630cf5a default 24-cell 2023-08-04 15:21:54 +10:00
Mike Lynch d8b5d4eacb bringing back 24-cell indexing 2023-08-04 15:21:02 +10:00
Mike Lynch 1a12c82a5d starting 2023-08-04 15:14:25 +10:00
Mike Lynch 0433267e62 further colour tweaks 2023-08-04 15:04:52 +10:00
Mike Lynch fdd75b103b Adjustable node sizes relative to links 2023-08-04 14:10:39 +10:00
Mike Lynch 82195b717f 600-cell hard-coded indexing and coloured nodes 2023-08-04 13:49:39 +10:00
Mike Lynch 0fdf8a40e4 Stuck an abstraction between permutations and real coordinates for
the 600-cell so that I can label them
2023-08-04 11:10:57 +10:00
Mike Lynch bee04c7e65 Made 600-cell the default 2023-08-04 11:09:49 +10:00
Mike Lynch 2c4a525b3e Several different broken attempts at indexing 2023-08-03 17:06:03 +10:00
Mike Lynch 1416f6121f Wrote a partition algorithm which Does Not Work! 2023-08-02 17:53:17 +10:00
Mike Lynch bf8824a43e Added basic automatic colour schemes and colourised the first
24-cell of the 600-cell
2023-08-02 16:58:51 +10:00
Mike Lynch 3f95a17f45 Reinstated 120-cell faces 2023-07-31 17:37:19 +10:00
Mike Lynch bf0b0559ef Merge branch 'main' into feature-faces 2023-07-31 17:33:40 +10:00
Mike Lynch c500957609 Turned off debug for 600-cell 2023-07-31 17:24:26 +10:00
Mike Lynch 2992f4ab66 Added debug flag to edge detector 2023-07-31 17:23:57 +10:00
Mike Lynch f4ec6d403e Made all shapes have the same thickness 2023-07-30 17:56:01 +10:00
Mike Lynch c2ac5d7928 removed debug stuff 2023-07-30 11:46:34 +10:00
Mike Lynch 13510c1ac1 Fixed bug which gave 600-cell too many edges 2023-07-30 11:46:13 +10:00
Mike Lynch 1bd838dd74 Improved default colours 2023-07-29 16:55:31 +10:00
Mike Lynch efb4cf7608 Refactored all the GUI and rotation stuff out, colour links are working 2023-07-29 16:52:00 +10:00
Mike Lynch 0b78ea2939 Everything's working except for default colours 2023-07-29 16:44:18 +10:00
Mike Lynch d253a851e9 almost through big refactor 2023-07-29 16:23:08 +10:00
Mike Lynch de0f3dc848 Refactoring 2023-07-29 12:06:34 +10:00
Mike Lynch f3e5a5b430 Adjustable colours and thickness, almost done 2023-07-29 11:52:43 +10:00
Mike Lynch 1a918c6d3b Merge branch 'feature-url-params' 2023-07-29 10:40:29 +10:00
Mike Lynch fe5761aede Link copying and param parsing works 2023-07-29 10:40:15 +10:00
Mike Lynch 7f7e79fc7c Made 120-cell the default 2023-07-28 17:40:39 +10:00
Mike Lynch 13b3d3514a broken 2023-07-28 17:39:57 +10:00
Mike Lynch 19c73d0f80 More shape experiments 2023-07-28 17:06:42 +10:00
Mike Lynch 8a581a9d64 Changed to pointer events 2023-07-28 11:04:08 +10:00
Mike Lynch 183e6becb2 deglitched rotations 2023-07-28 10:47:33 +10:00
Mike Lynch 766b3b8e6e Bug in 16-cell definition - too many edges 2023-07-28 09:55:31 +10:00
Mike Lynch b3a4e43fe0 Added a light, updated screenshots 2023-07-28 08:40:27 +10:00
22 changed files with 3899 additions and 351 deletions

View File

@ -4,7 +4,12 @@ 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
[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" />
<img src="https://raw.githubusercontent.com/spikelynch/fourdjs/main/docs/screenshot-24cell.png" width="445" />
[Basic interactive demo](https://etc.mikelynch.org/fourjs/)

137
cell120.js 100644
View File

@ -0,0 +1,137 @@
// 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 100644
View File

@ -0,0 +1,188 @@
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 100644
View File

@ -0,0 +1,37 @@
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,
]
}

View File

@ -0,0 +1,12 @@
# 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

View File

@ -0,0 +1,244 @@
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.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 670 KiB

After

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -1,23 +1,24 @@
import * as THREE from 'three'; import * as THREE from 'three';
const HYPERPLANE = 2; const HYPERPLANE = 2.0;
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, structure) { constructor(node_ms, link_ms, face_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();
} }
@ -33,8 +34,8 @@ class FourDShape extends THREE.Group {
} }
} }
makeNode(material, v3) { makeNode(material, v3, scale) {
const geometry = new THREE.SphereGeometry(NODE_SIZE); const geometry = new THREE.SphereGeometry(this.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);
@ -42,75 +43,145 @@ class FourDShape extends THREE.Group {
} }
makeLink(material, link) { makeLink(material, link) {
const n1 = this.nodes3[link.source].v3; const n1 = this.nodes3[link.source];
const n2 = this.nodes3[link.target].v3; const n2 = this.nodes3[link.target];
const length = n1.distanceTo(n2); const s1 = n1.scale;
const s2 = n2.scale;
const length = n1.v3.distanceTo(n2.v3);
const centre = new THREE.Vector3(); const centre = new THREE.Vector3();
centre.lerpVectors(n1, n2, 0.5); centre.lerpVectors(n1.v3, n2.v3, 0.5);
const geometry = new THREE.CylinderGeometry(LINK_SIZE, LINK_SIZE, 1); const geometry = new THREE.CylinderGeometry(
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); edge.lookAt(n2.v3);
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) { updateLink(link, links_show) {
const n1 = this.nodes3[link.source].v3; const n1 = this.nodes3[link.source];
const n2 = this.nodes3[link.target].v3; const n2 = this.nodes3[link.target];
const length = n1.distanceTo(n2); const s1 = n1.scale;
const s2 = n2.scale;
const length = n1.v3.distanceTo(n2.v3);
const centre = new THREE.Vector3(); const centre = new THREE.Vector3();
centre.lerpVectors(n1, n2, 0.5); centre.lerpVectors(n1.v3, n2.v3, 0.5);
link.object.scale.copy(new THREE.Vector3(1, 1, length)); // take the average of the ends as the thickness - as a workaround,
// 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); link.object.lookAt(n2.v3);
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.hyperplane / (this.hyperplane + v4.w); const k = this.fourDscale(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 v3 = this.fourDtoV3(n.x, n.y, n.z, n.w, []); const k = this.fourDscale(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,
object: this.makeNode(material, v3) scale: k,
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) { render3(rotations, nodes_show, links_show) {
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 v3 = this.fourDtoV3(n.x, n.y, n.z, n.w, rotations); const v4 = this.fourDrotate(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);
// could do scaling here this.nodes3[n.id].object.scale.copy(s3);
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 l of this.links ) { for( const f of this.faces ) {
this.updateLink(l); this.setFaceGeometry(f, f.object.geometry);
} }
} }
} }
export { FourDShape }; export { FourDShape };

220
gui.js 100644
View File

@ -0,0 +1,220 @@
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 };

View File

@ -5,9 +5,28 @@
<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>

View File

@ -0,0 +1,294 @@
// 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;
}

View File

@ -0,0 +1,78 @@
// 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 100644

File diff suppressed because it is too large Load Diff

173
layer600cell.js 100644
View File

@ -0,0 +1,173 @@
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
View File

@ -1,238 +1,252 @@
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';
import { GUI } from 'lil-gui'; const FACE_OPACITY = 0.3;
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);
scene.background = new THREE.Color(0xdddddd); camera.position.set(0, 0, CAMERA_K / 2);
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
const NODE_OPACITY = 1.0; scene.background = new THREE.Color(DEFAULTS.background);
const LINK_OPACITY = 1.0; const material = new THREE.MeshStandardMaterial({ color: DEFAULTS.color });
const node_colours = get_colours(DEFAULTS.color);
const node_ms = [
new THREE.MeshStandardMaterial( { color: 0x90ebff } )
];
for( const node_m of node_ms ) { material.transparent = true;
node_m.roughness = 0.9; material.opacity = 0.5;
if( NODE_OPACITY < 1.0 ) {
node_m.transparent = true; const node_ms = node_colours.map((c) => new THREE.MeshStandardMaterial({color: c}));
node_m.opacity = NODE_OPACITY; const link_ms = node_colours.map((c) => new THREE.MeshStandardMaterial({color: c}));
node_ms.map((m) => {
m.transparent = true;
m.opacity = 1.0;
} }
} );
const link_ms = [ link_ms.map((m) => {
new THREE.MeshStandardMaterial( { color: 0x90ebff } ) m.transparent = true;
m.opacity = 0.5;
}
);
const face_ms = [
new THREE.MeshLambertMaterial( { color: 0x44ff44 } )
]; ];
for( const link_m of link_ms ) { for( const face_m of face_ms ) {
link_m.metalness = 0.8; face_m.transparent = true;
link_m.roughness = 0.1; face_m.opacity = FACE_OPACITY;
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()
};
const STRUCTURES = POLYTOPES.build_all();
const STRUCTURES_BY_NAME = {};
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);
} }
shape = new FourDShape(node_ms, link_ms, STRUCTURES[name]); structure = STRUCTURES_BY_NAME[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;
createShape(DEFAULT_SHAPE); function changeShape() {
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`);
}
}
camera.position.z = 4; gui = new FourDGUI(
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 startX = 0; let theta0 = 0;
let startY = 0; let psi0 = 0;
let startX0 = 0; let dragx0 = 0;
let startY0 = 0; let dragy0 = 0;
let dragging = false;
renderer.domElement.addEventListener("mousedown", (event) => {
renderer.domElement.addEventListener("pointerdown", (event) => {
if( event.buttons === 1 ) { if( event.buttons === 1 ) {
startX = event.clientX; theta0 = theta;
startY = event.clientY; psi0 = psi;
startX0 = theta / dragK; dragx0 = event.clientX;
startY0 = theta / dragK; dragy0 = event.clientY;
dragging = true;
} }
}) })
renderer.domElement.addEventListener("pointermove", (event) => {
renderer.domElement.addEventListener("mousemove", (event) => {
if( event.buttons === 1 ) { if( event.buttons === 1 ) {
theta = (event.clientX - startX + startX0) * dragK; const theta1 = theta0 + (event.clientX - dragx0) * dragK;
psi = (event.clientY - startY + startY0) * dragK; const psi1 = psi0 + (event.clientY - dragy0) * dragK;
gui.params.dtheta = theta1 - theta;
gui.params.dpsi = psi1 - psi;
theta = theta1;
psi = psi1;
} }
}) })
// set up GUI renderer.domElement.addEventListener("pointerup", (event) => {
dragging = false;
})
const gui = new GUI(); createShape(gui.params.shape, gui.params.option);
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 = gui_params.hyperplane; shape.hyperplane = 1 / gui.params.hyperplane;
shape.render3(rotations); camera.position.set(0, 0, gui.params.zoom * CAMERA_K * gui.params.hyperplane);
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 );
} }
animate(); animate();

276
package-lock.json generated
View File

@ -4,8 +4,11 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "fourdjs",
"dependencies": { "dependencies": {
"lil-gui": "^0.18.2", "color": "^4.2.3",
"color-scheme": "^1.0.1",
"lil-gui": "^0.19.0",
"three": "^0.154.0" "three": "^0.154.0"
}, },
"devDependencies": { "devDependencies": {
@ -13,9 +16,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-wlkQBWb79/jeEEoRmrxt/yhn5T1lU236OCNpnfRzaCJHZ/5gf82uYx1qmADTBWE0AR/v7FiozE1auk2riyQd3w==", "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -29,9 +32,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-NI/gnWcMl2kXt1HJKOn2H69SYn4YNheKo6NZt1hyfKWdMbaGadxjZIkcj4Gjk/WPxnbFXs9/3HjGHaknCqjrww==", "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -45,9 +48,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-FM9NQamSaEm/IZIhegF76aiLnng1kEsZl2eve/emxDeReVfRuRNmvT28l6hoFD9TsCxpK+i4v8LPpEj74T7yjA==", "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -61,9 +64,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-XmrFwEOYauKte9QjS6hz60FpOCnw4zaPAb7XV7O4lx1r39XjJhTN7ZpXqJh4sN6q60zbP6QwAVVA8N/wUyBH/w==", "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -77,9 +80,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-bMqBmpw1e//7Fh5GLetSZaeo9zSC4/CMtrVFdj+bqKPGJuKyfNJ5Nf2m3LknKZTS+Q4oyPiON+v3eaJ59sLB5A==", "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -93,9 +96,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-LoTK5N3bOmNI9zVLCeTgnk5Rk0WdUTrr9dyDAQGVMrNTh9EAPuNwSTCgaKOKiDpverOa0htPcO9NwslSE5xuLA==", "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -109,9 +112,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-62jX5n30VzgrjAjOk5orYeHFq6sqjvsIj1QesXvn5OZtdt5Gdj0vUNJy9NIpjfdNdqr76jjtzBJKf+h2uzYuTQ==", "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -125,9 +128,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-dT4URUv6ir45ZkBqhwZwyFV6cH61k8MttIwhThp2BGiVtagYvCToF+Bggyx2VI57RG4Fbt21f9TmXaYx0DeUJg==", "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -141,9 +144,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-BWncQeuWDgYv0jTNzJjaNgleduV4tMbQjmk/zpPh/lUdMcNEAxy+jvneDJ6RJkrqloG7tB9S9rCrtfk/kuplsQ==", "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -157,9 +160,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-JPXORvgHRHITqfms1dWT/GbEY89u848dC08o0yK3fNskhp0t2TuNUnsrrSgOdH28ceb1hJuwyr8R/1RnyPwocw==", "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -173,9 +176,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-kArPI0DopjJCEplsVj/H+2Qgzz7vdFSacHNsgoAKpPS6W/Ndh8Oe24HRDQ5QCu4jHgN6XOtfFfLpRx3TXv/mEg==", "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -189,9 +192,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-b/tmngUfO02E00c1XnNTw/0DmloKjb6XQeqxaYuzGwHe0fHVgx5/D6CWi+XH1DvkszjBUkK9BX7n1ARTOst59w==", "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@ -205,9 +208,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-KXPY69MWw79QJkyvUYb2ex/OgnN/8N/Aw5UDPlgoRtoEfcBqfeLodPr42UojV3NdkoO4u10NXQdamWm1YEzSKw==", "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -221,9 +224,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-komK3NEAeeGRnvFEjX1SfVg6EmkfIi5aKzevdvJqMydYr9N+pRQK0PGJXk+bhoPZwOUgLO4l99FZmLGk/L1jWg==", "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -237,9 +240,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-632T5Ts6gQ2WiMLWRRyeflPAm44u2E/s/TJvn+BP6M5mnHSk93cieaypj3VSMYO2ePTCRqAFXtuYi1yv8uZJNA==", "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -253,9 +256,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-MsHtX0NgvRHsoOtYkuxyk4Vkmvk3PLRWfA4okK7c+6dT0Fu4SUqXAr9y4Q3d8vUf1VWWb6YutpL4XNe400iQ1g==", "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -269,9 +272,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-djST6s+jQiwxMIVQ5rlt24JFIAr4uwUnzceuFL7BQT4CbrRtqBPueS4GjXSiIpmwVri1Icj/9pFRJ7/aScvT+A==", "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -285,9 +288,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-naeRhUIvhsgeounjkF5mvrNAVMGAm6EJWiabskeE5yOeBbLp7T89tAEw0j5Jm/CZAwyLe3c67zyCWH6fsBLCpw==", "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -301,9 +304,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-qkT2+WxyKbNIKV1AEhI8QiSIgTHMcRctzSaa/I3kVgMS5dl3fOeoqkb7pW76KwxHoriImhx7Mg3TwN/auMDsyQ==", "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -317,9 +320,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-HC4/feP+pB2Vb+cMPUjAnFyERs+HJN7E6KaeBlFdBv799MhD+aPJlfi/yk36SED58J9TPwI8MAcVpJgej4ud0A==", "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -333,9 +336,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-ovjwoRXI+gf52EVF60u9sSDj7myPixPxqzD5CmkEUmvs+W9Xd0iqISVBQn8xcx4ciIaIVlWCuTbYDOXOnOL44Q==", "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -349,9 +352,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.18.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-imUxH9a3WJARyAvrG7srLyiK73XdX83NXQkjKvQ+7vPh3ZxoLrzvPkQKKw2DwZ+RV2ZB6vBfNHP8XScAmQC3aA==", "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -364,10 +367,52 @@
"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.15", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.15.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-3WOOLhrvuTGPRzQPU6waSDWrDTnQriia72McWcn6UCi43GhCHrXH4S59hKMeez+IITmdUuUyvbU9JIp+t3xlPQ==", "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"bin": { "bin": {
@ -377,34 +422,34 @@
"node": ">=12" "node": ">=12"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/android-arm": "0.18.15", "@esbuild/android-arm": "0.18.20",
"@esbuild/android-arm64": "0.18.15", "@esbuild/android-arm64": "0.18.20",
"@esbuild/android-x64": "0.18.15", "@esbuild/android-x64": "0.18.20",
"@esbuild/darwin-arm64": "0.18.15", "@esbuild/darwin-arm64": "0.18.20",
"@esbuild/darwin-x64": "0.18.15", "@esbuild/darwin-x64": "0.18.20",
"@esbuild/freebsd-arm64": "0.18.15", "@esbuild/freebsd-arm64": "0.18.20",
"@esbuild/freebsd-x64": "0.18.15", "@esbuild/freebsd-x64": "0.18.20",
"@esbuild/linux-arm": "0.18.15", "@esbuild/linux-arm": "0.18.20",
"@esbuild/linux-arm64": "0.18.15", "@esbuild/linux-arm64": "0.18.20",
"@esbuild/linux-ia32": "0.18.15", "@esbuild/linux-ia32": "0.18.20",
"@esbuild/linux-loong64": "0.18.15", "@esbuild/linux-loong64": "0.18.20",
"@esbuild/linux-mips64el": "0.18.15", "@esbuild/linux-mips64el": "0.18.20",
"@esbuild/linux-ppc64": "0.18.15", "@esbuild/linux-ppc64": "0.18.20",
"@esbuild/linux-riscv64": "0.18.15", "@esbuild/linux-riscv64": "0.18.20",
"@esbuild/linux-s390x": "0.18.15", "@esbuild/linux-s390x": "0.18.20",
"@esbuild/linux-x64": "0.18.15", "@esbuild/linux-x64": "0.18.20",
"@esbuild/netbsd-x64": "0.18.15", "@esbuild/netbsd-x64": "0.18.20",
"@esbuild/openbsd-x64": "0.18.15", "@esbuild/openbsd-x64": "0.18.20",
"@esbuild/sunos-x64": "0.18.15", "@esbuild/sunos-x64": "0.18.20",
"@esbuild/win32-arm64": "0.18.15", "@esbuild/win32-arm64": "0.18.20",
"@esbuild/win32-ia32": "0.18.15", "@esbuild/win32-ia32": "0.18.20",
"@esbuild/win32-x64": "0.18.15" "@esbuild/win32-x64": "0.18.20"
} }
}, },
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.2", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
@ -415,10 +460,15 @@
"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.18.2", "version": "0.19.0",
"resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.18.2.tgz", "resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.19.0.tgz",
"integrity": "sha512-DgdrLy3/KGC0PiQLKgOcJMPItP4xY4iWgJ9+91Zaxfr8GCTmMps05QS9w9jW7yspILlbscbquwjOwxmWnSx5Uw==" "integrity": "sha512-02/Z7rPng3GXWFwkQVj1hQaJYo2fIEYctqe0ima5uI/N2HEagB9ZGCQKkVWr3UuKfTr0arto3Q9prTB8sxtJJw=="
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.6", "version": "3.3.6",
@ -445,9 +495,9 @@
"dev": true "dev": true
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.27", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -473,9 +523,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "3.26.3", "version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.26.3.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-7Tin0C8l86TkpcMtXvQu6saWH93nhG3dGQ1/+l5V2TDMceTxO7kDiK6GzbfLWNNxqJXm591PcEZUozZm51ogwQ==", "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"dev": true, "dev": true,
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
@ -488,6 +538,14 @@
"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",
@ -503,14 +561,14 @@
"integrity": "sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug==" "integrity": "sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug=="
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.4.6", "version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.6.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-EY6Mm8vJ++S3D4tNAckaZfw3JwG3wa794Vt70M6cNJ6NxT87yhq7EC8Rcap3ahyHdo8AhCmV9PTk+vG1HiYn1A==", "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",
"postcss": "^8.4.26", "postcss": "^8.4.27",
"rollup": "^3.25.2" "rollup": "^3.27.1"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"

View File

@ -1,9 +1,12 @@
{ {
"dependencies": { "dependencies": {
"lil-gui": "^0.18.2", "color": "^4.2.3",
"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"
} }

View File

@ -1,23 +1,28 @@
import * as PERMUTE from './permute.js'; import * as PERMUTE from './permute.js';
function scale_and_index(nodes, scale) { import * as CELLINDEX from './cellindex.js';
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;
} }
function auto_detect_edges(nodes, neighbours) { export function auto_detect_edges(nodes, neighbours, debug=false) {
const seen = {}; const seen = {};
const nnodes = nodes.length; const nnodes = nodes.length;
const links = []; const links = [];
@ -29,6 +34,10 @@ function auto_detect_edges(nodes, neighbours) {
} }
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();
@ -40,21 +49,24 @@ function auto_detect_edges(nodes, neighbours) {
} }
} }
} }
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 r5 = Math.sqrt(5); const c1 = Math.sqrt(5) / 4;
const r2 = Math.sqrt(2) / 2;
return { return {
name: '5-cell',
nodes: [ nodes: [
{id:1, x: r2, y: r2, z: r2, w: -r2 / r5 }, {id:1, label: 1, x: c1, y: c1, z: c1, w: -0.25 },
{id:2, x: r2, y: -r2, z: -r2, w: -r2 / r5 }, {id:2, label: 2, x: c1, y: -c1, z: -c1, w: -0.25 },
{id:3, x: -r2, y: r2, z: -r2, w: -r2 / r5 }, {id:3, label: 3, x: -c1, y: c1, z: -c1, w: -0.25 },
{id:4, x: -r2, y: -r2, z: r2, w: -r2 / r5 }, {id:4, label: 4, x: -c1, y: -c1, z: c1, w: -0.25 },
{id:5, x: 0, y: 0, z: 0, w: 4 * r2 / r5 }, {id:5, label: 5, x: 0, y: 0, z: 0, w: 1 },
], ],
links: [ links: [
{ id:1, source:1, target: 2}, { id:1, source:1, target: 2},
@ -67,7 +79,13 @@ 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.`,
}; };
}; };
@ -75,46 +93,218 @@ 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);
console.log('cell16 auto_detect_edges'); nodes[0].label = 1;
console.log(nodes); nodes[3].label = 2;
const links = auto_detect_edges(nodes, 8); nodes[5].label = 3;
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 = scale_and_index(PERMUTE.coordinates([1, 1, 1, 1], 0), Math.sqrt(2) / 2); const nodes = PERMUTE.coordinates([1, 1, 1, 1], 0);
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 = scale_and_index(PERMUTE.coordinates([0, 0, 1, 1], 0), 1); const nodes = PERMUTE.coordinates([0, 0, 1, 1], 0);
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;
@ -131,42 +321,546 @@ 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();
return scale_and_index(nodes, 0.5); index_nodes(nodes);
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();
PERMUTE.coordinates([phi, 1, 1 / phi, 0], 0, true) for( const n of nodes ) {
].flat(); n.label = label_vertex(n, coords, CELLINDEX.PARTITION600);
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;
}
}
export const cell600 = () => { function audit_link_labels(nodes, links) {
const nodes = make_600cell_vertices(); for( const l of links ) {
const links = auto_detect_edges(nodes, 20); const n1 = get_node(nodes, l.source);
return { const n2 = get_node(nodes, l.target);
nodes: nodes, if( n1.label === n2.label ) {
links: links console.log(`link ${l.id} joins ${n1.id} ${n2.id} with label ${n2.label}`);
}
} }
} }
export const cell600 = () => {
const nodes = make_600cell_vertices();
const links = auto_detect_edges(nodes, 12);
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 {
name: '600-cell',
nodes: nodes,
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 100644
View File

@ -0,0 +1,85 @@
// 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,
};

View File

@ -3,5 +3,14 @@
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' ]
}
}
}
}
}) })