Compare commits

..

No commits in common. "main" and "feature-snub-24-cell" have entirely different histories.

16 changed files with 220 additions and 2153 deletions

View File

@ -1,17 +0,0 @@
CHANGELOG
=========
## v1.1 - 1/1/2026
The 120-cell now includes a visualisation of its inscribed 5-cells, which honestly
looks like less of a mess than I expected it to.
## v1.0 - 16/11/2025
It's been [two years](https://mikelynch.org/2023/Sep/02/120-cell/)</a> since
I first made this, and I haven't updated it in a while, but I got tapered links to
work without too much performance overhead, so that seemed worth a version.
The results flicker a bit at low opacities but otherwise I'm pretty happy with
it.
`

View File

@ -1,26 +0,0 @@
# NOTES
New approach for the 5-cells:
Pick a tetrahedron of an inscribed 600-cell with vertices A, B, C, D
This gives pairs of vertices:
AB
AC
AD
BC
BD
CD
Each of these gives rise to seven pairs of 5-cells which are on neighboring vertices
of the 5 600-cells.
Try enumerating these and inspecting them to find one or more coherent sets of four
5-cells which lie on one tetrahedron from each of the 600-cells.
(I expect there to be more than one, like how there are two ways to partition the
120-cell vertices into 600-cells)

View File

@ -104,855 +104,6 @@ export const LAYERS120 = {
163,219,271,223,167]
};
export const CELL120_CELL5 = {
"tetras": {
},
"cell5s": {
"1": [
27,
28,
264,
309,
275
],
"2": [
223,
76,
238,
84,
225
],
"3": [
253,
44,
283,
304,
42
],
"4": [
419,
112,
197,
578,
521
],
"5": [
339,
14,
384,
382,
337
],
"6": [
331,
4,
335,
390,
386
],
"7": [
427,
160,
551,
146,
557
],
"8": [
265,
60,
64,
295,
246
],
"9": [
473,
100,
495,
213,
462
],
"10": [
393,
6,
328,
397,
326
],
"11": [
539,
164,
439,
561,
142
],
"12": [
511,
122,
456,
595,
181
],
"13": [
555,
154,
152,
545,
429
],
"14": [
95,
202,
486,
500,
465
],
"15": [
471,
208,
502,
484,
89
],
"16": [
347,
21,
348,
374,
373
],
"17": [
487,
203,
94,
468,
497
],
"18": [
165,
139,
542,
568,
434
],
"19": [
367,
18,
355,
368,
353
],
"20": [
231,
78,
86,
236,
217
],
"21": [
356,
17,
366,
354,
365
],
"22": [
503,
205,
470,
92,
481
],
"23": [
527,
106,
584,
195,
421
],
"24": [
239,
73,
222,
228,
81
],
"25": [
543,
138,
168,
435,
565
],
"26": [
48,
46,
302,
281,
255
],
"27": [
248,
62,
293,
58,
267
],
"28": [
440,
141,
562,
540,
163
],
"29": [
274,
30,
312,
261,
29
],
"30": [
179,
128,
597,
450,
505
],
"31": [
376,
22,
375,
346,
345
],
"32": [
320,
11,
405,
401,
319
],
"33": [
448,
173,
130,
587,
519
],
"34": [
460,
102,
211,
489,
479
],
"35": [
388,
2,
392,
329,
333
],
"36": [
512,
182,
596,
121,
455
],
"37": [
592,
170,
516,
443,
133
],
"38": [
120,
189,
529,
570,
411
],
"39": [
420,
198,
577,
522,
111
],
"40": [
272,
69,
65,
290,
243
],
"41": [
488,
93,
498,
204,
467
],
"42": [
156,
150,
547,
553,
431
],
"43": [
252,
53,
286,
297,
55
],
"44": [
532,
192,
117,
410,
571
],
"45": [
400,
7,
321,
396,
323
],
"46": [
580,
199,
417,
110,
523
],
"47": [
296,
63,
245,
266,
59
],
"48": [
600,
125,
178,
508,
451
],
"49": [
68,
72,
269,
242,
291
],
"50": [
324,
8,
399,
322,
395
],
"51": [
499,
96,
485,
466,
201
],
"52": [
406,
12,
317,
318,
402
],
"53": [
563,
144,
437,
162,
537
],
"54": [
234,
88,
219,
229,
80
],
"55": [
350,
24,
349,
371,
372
],
"56": [
444,
134,
515,
169,
591
],
"57": [
258,
40,
39,
277,
307
],
"58": [
285,
56,
251,
54,
298
],
"59": [
546,
151,
153,
430,
556
],
"60": [
98,
215,
475,
493,
464
],
"61": [
474,
214,
99,
461,
496
],
"62": [
357,
20,
363,
359,
364
],
"63": [
490,
212,
459,
101,
480
],
"64": [
185,
116,
415,
533,
574
],
"65": [
378,
16,
343,
341,
380
],
"66": [
218,
85,
235,
77,
232
],
"67": [
342,
15,
377,
379,
344
],
"68": [
458,
209,
491,
477,
104
],
"69": [
514,
135,
441,
590,
172
],
"70": [
226,
83,
75,
237,
224
],
"71": [
530,
119,
569,
190,
412
],
"72": [
38,
37,
257,
308,
278
],
"73": [
414,
113,
188,
575,
536
],
"74": [
362,
19,
358,
361,
360
],
"75": [
334,
1,
330,
387,
391
],
"76": [
438,
161,
538,
143,
564
],
"77": [
550,
157,
426,
560,
147
],
"78": [
566,
167,
137,
544,
436
],
"79": [
126,
177,
452,
507,
599
],
"80": [
284,
41,
254,
43,
303
],
"81": [
494,
97,
476,
463,
216
],
"82": [
200,
109,
418,
524,
579
],
"83": [
263,
25,
26,
276,
310
],
"84": [
300,
50,
52,
249,
287
],
"85": [
558,
145,
428,
159,
552
],
"86": [
403,
9,
316,
315,
407
],
"87": [
74,
82,
227,
221,
240
],
"88": [
289,
66,
244,
271,
70
],
"89": [
306,
34,
280,
33,
259
],
"90": [
572,
118,
531,
409,
191
],
"91": [
207,
90,
472,
483,
501
],
"92": [
369,
23,
370,
351,
352
],
"93": [
585,
132,
175,
517,
446
],
"94": [
105,
196,
528,
583,
422
],
"95": [
241,
67,
292,
71,
270
],
"96": [
425,
148,
559,
549,
158
],
"97": [
525,
193,
108,
423,
582
],
"98": [
174,
129,
588,
447,
520
],
"99": [
313,
10,
404,
408,
314
],
"100": [
413,
187,
576,
535,
114
],
"101": [
573,
186,
416,
115,
534
],
"102": [
49,
51,
299,
288,
250
],
"103": [
449,
180,
127,
598,
506
],
"104": [
469,
91,
206,
504,
482
],
"105": [
513,
171,
589,
136,
442
],
"106": [
279,
35,
305,
260,
36
],
"107": [
389,
3,
385,
336,
332
],
"108": [
593,
183,
509,
454,
124
],
"109": [
149,
155,
554,
432,
548
],
"110": [
325,
5,
394,
327,
398
],
"111": [
453,
123,
510,
184,
594
],
"112": [
383,
13,
338,
340,
381
],
"113": [
311,
31,
273,
32,
262
],
"114": [
581,
107,
526,
424,
194
],
"115": [
61,
57,
268,
247,
294
],
"116": [
87,
79,
230,
220,
233
],
"117": [
301,
47,
45,
256,
282
],
"118": [
131,
176,
445,
518,
586
],
"119": [
210,
103,
457,
478,
492
],
"120": [
140,
166,
567,
433,
541
]
},
};
// Schoute's partition via https://arxiv.org/abs/1010.4353
export const PARTITION600 = {

View File

@ -1,21 +1,14 @@
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);
console.log(resaturated);
return resaturated;
const hexbasis = basis.toString(16).padStart(6, "0");
scheme.from_hex(hexbasis).scheme("tetrade").variation("hard").distance(0.5);
const colours = scheme.colors().map((cs) => parseInt('0x' + cs));
const set = colours.slice(1, 9);
set.reverse();
set.unshift(colours[0]);
return set;
}
// basic colours where 0 = blue

View File

@ -1 +0,0 @@

View File

@ -1,492 +0,0 @@
import * as POLYTOPES from './polytopes.js';
// exploring more inscriptions of the 120-cell
function choice(a) {
const r = Math.floor(Math.random() * a.length);
return a[r];
}
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 dist(n1, n2) {
return Math.sqrt((n1.x - n2.x) ** 2 + (n1.y - n2.y) ** 2 + (n1.z - n2.z) ** 2 + (n1.w - n2.w) ** 2);
}
export function make_120cell() {
const nodes = POLYTOPES.make_120cell_vertices();
const links = POLYTOPES.auto_detect_edges(nodes, 4);
return {
nodes: nodes,
links: links
}
}
function round_dist(raw) {
return Math.floor(raw * 100000) / 100000;
}
export function distance_groups(cell120) {
// get list of other nodes by distance
// sort them and dump them out
const dists = {};
cell120.nodes.map((n) => {
const draw = dist(cell120.nodes[0], n);
const dtrunc = round_dist(draw);
if( !(dtrunc in dists) ) {
dists[dtrunc] = [];
}
dists[dtrunc].push(n);
});
return dists;
}
function distance_group(cell120, n0, chord) {
const nodes = []
cell120.nodes.map((n) => {
const d = round_dist(dist(n0, n));
if( d == chord ) {
nodes.push(n);
}
});
// filter and return those whose chord is also the same
const equidistant = [];
for( const n1 of nodes ) {
for( const n2 of nodes ) {
if( n2.id > n1.id ) {
if( round_dist(dist(n1, n2)) == chord ) {
equidistant.push([n1, n2]);
}
}
}
}
return equidistant;
}
export function chord_survey() {
const cell120 = POLYTOPES.cell120_inscribed();
const dgroups = distance_groups(cell120);
const dists = Object.keys(dgroups);
dists.sort();
for( const d of dists ) {
const g0 = dgroups[d][0];
dgroups[d].map((g) => {
console.log(`${g0.id}-${g.id}: ${round_dist(dist(g0, g))}`);
});
}
}
function overlap(c1, c2) {
for( const l in c1 ) {
if( c1[l] === c2[l] ) {
return true;
}
}
return false;
}
function c5match(c1, c2) {
for( const l in c1 ) {
if( c1[l] != c2[l] ) {
return false;
}
}
return true;
}
export function gather_5cells(cell120) {
const CHORD5 = round_dist(Math.sqrt(2.5));
const bins = [];
const all = [];
cell120.nodes.filter((n) => n.label === 1).map((n) => {
const cells = [ ];
const g = distance_group(cell120, n, CHORD5);
for( const pair of g ) {
let seen = false;
for( const cell of cells ) {
const c = Object.values(cell);
if( c.includes(pair[0].id) || c.includes(pair[1].id) ) {
if( !c.includes(pair[0].id) ) {
cell[pair[0].label] = pair[0].id;
}
if( !c.includes(pair[1].id) ) {
cell[pair[1].label] = pair[1].id;
}
seen = true;
break;
}
}
if( !seen ) {
const cell = {};
cell[1]= n.id;
cell[pair[0].label] = pair[0].id;
cell[pair[1].label] = pair[1].id;
cells.push(cell);
}
}
all.push(...cells);
});
return all;
}
function audit_5cells(cells) {
// this verifies that for each label (a 600-cell set), each of its
// vertices is in exactly 7 5-cells. It checks out.
['1','2','3','4','5'].map((l) => {
const sets = {};
for( const cell of cells ) {
const lv = cell[l];
if( !(lv in sets) ) {
sets[lv] = [];
}
sets[lv].push(cell);
}
for( const lv in sets ) {
const ok = ( sets[lv].length === 7 ) ? 'ok' : 'miss';
console.log(`${l},${lv},${sets[lv].length},${ok}`);
}
});
}
function try_120_5_cells_fails(cell120, cells, l) {
// iterate over every vertex in the 600-cell defined by label l,
// get all 7 5-cells including that vertex, and add them if they are
// disjoint with what we already have
// this always runs out of disjoint nodes early
const vertices = cell120.nodes.filter((n) => n.label === l);
const cellset = [];
for( const v of vertices ) {
console.log(`Vertex ${v.id}`);
const vcells = cells.filter((c) => c[l] === v.id);
const overlap_any = (cs, c) => {
for( const seen of cs ) {
if( overlap(seen, c) ) {
console.log("overlap");
console.log(c);
return true;
}
}
return false;
}
const disjoint = vcells.filter((c) => ! overlap_any(cellset, c));
console.log(`Found ${disjoint.length} disjoint cells`);
if( disjoint.length > 0 ) {
cellset.push(choice(disjoint));
}
}
console.log(`Found total of ${cellset.length} disjoint cells`);
//console.log(cellset);
}
function overlap_any(cs, c) {
for( const seen of cs ) {
if( overlap(seen, c) ) {
return true;
}
}
return false;
}
function explore_disjoint(cell120, all5, l) {
const a = all5[0];
const overlaps = all5.filter((c) => overlap(c, a));
console.log(a);
console.log(overlaps.length);
console.log(overlaps);
}
// select a five-cell from a starting vertex v
// find a neighbor of v vn on its 600 cell, find all of the 5-cells which include
// vn. Then see if we can find any from that set which are similiar neighbours to
// the other four vertices in the first 5-cell
// the idea is that the 600-cells are a guide to finding the right subset of
// 5-cells
function neighbours600(cell120, vid) {
const v = cell120.nodes.filter((node) => node.id === vid)[0];
const label = v.label;
const links = cell120.links.filter((l) => {
return l.label === v.label && (l.source === v.id || l.target == v.id );
});
const nodes = links.map((l) => {
if( l.source === v.id ) {
return l.target;
} else {
return l.source;
}
});
return nodes;
}
function cell120node(cell120, nid) {
return cell120.nodes.filter((n) => n.id === nid)[0];
}
function node_dist(cell120, aid, bid) {
const a = cell120node(cell120, aid);
const b = cell120node(cell120, bid);
return dist(a, b);
}
function print_row(v1, v2, p, v5) {
console.log(`${v1.id},${v2.id},${p},${v5[1]},${v5[2]},${v5[3]},${v5[4]},${v5[5]}`);
}
// for a pair of vertices which are on the same inscribed 600 cell,
// this returns all 7 pairs of 5-cells which contain v1 and v2 and
// which are also evenly spaced (ie every pair of vertices on the
// same 600-cell is one edge apart)
function find_adjoining_5cells(cell120, all5, v1, v2) {
const DIST600 = round_dist(node_dist(cell120, v1.id, v2.id));
const v15s = all5.filter((c5) => c5[v1.label] === v1.id);
const v25s = all5.filter((c5) => c5[v2.label] === v2.id);
let p = 0;
const c5pairs = [];
for( const v5a of v15s ) {
for( const v5b of v25s ) {
let match = true;
const d = {};
for( const label in v5a ) {
d[label] = round_dist(node_dist(cell120, v5a[label], v5b[label]));
if( d[label] != DIST600 ) {
match = false;
}
}
if( match ) {
c5pairs.push([ v5a, v5b ]);
}
}
}
return c5pairs;
}
function tetras(cell120, v) {
// given a vertex v, find all of the 600-cell tetras it's on
const n600s = neighbours600(cell120, v.id);
// need to find all sets of three neighbours which are neighbours: there
// should be 20 of these because they're faces of an icosahedron
const tetras = new Set;
for( const v2id of n600s ) {
// find mutual neighbours of the first two
const n2600s = neighbours600(cell120, v2id);
const mutuals = n2600s.filter((nid) => {
return nid != v2id && nid != v.id && n600s.includes(nid)
});
for( const nm of mutuals ) {
const nnms = neighbours600(cell120, nm);
const mutuals2 = nnms.filter((nid) => {
return nid != nm && nid != v2id && nid != v.id && mutuals.includes(nid)
});
for( const m2 of mutuals2 ) {
const t = [ v.id, v2id, nm, m2 ];
t.sort((a, b) => a - b);
const tstr = t.join(',');
tetras.add(tstr);
}
}
}
const tarray = [];
for( const t of tetras ) {
const ta = t.split(',').map((v) => Number(v));
tarray.push(ta);
}
return tarray;
}
function vertices(hedra) {
const v = new Set;
for ( const h of hedra) {
for( const p of h ) {
v.add(p);
}
}
return Array.from(v);
}
function str5cell(c5) {
return ["1","2","3","4","5"].map((l) => String(c5[l]).padStart(3, '0')).join('-');
}
function tetra_sets(cell120, all5, tetra) {
// given a tetrahedron on a 600-cell, find the sets of adjacent 5-cells on
// all of the pairs
// this is ass-backwards. Need to find tetras on the other 4 vertices of a 5-cell
const vs = tetra.map((tid) => cell120node(cell120, tid));
const pairs = [[0,1], [0,2], [0, 3], [1, 2], [1, 3], [2, 3]];
for( const p of pairs ) {
const v1 = vs[p[0]];
const v2 = vs[p[1]];
const c5pairs = find_adjoining_5cells(cell120, all5, v1, v2);
console.log(v1.id, v2.id);
console.log(c5pairs.map((p) => str5cell(p[0]) + " " + str5cell(p[1])));
}
}
function cell5_neighbourhoods(cell120, all5, c5) {
const neighbours = {}
for( const l in c5 ) {
const v = cell120node(cell120, c5[l]);
neighbours[l] = vertices(tetras(cell120, v));
}
// now take the set of all 5-cells and filter it to only those whose vertices
// are in the neighour sets. On first inspection there are 13?
const n5cells = all5.filter((c5) => {
for( const l in c5 ) {
if( ! neighbours[l].includes(c5[l]) ) {
return false;
}
}
return true;
});
return n5cells;
}
function cell5_tetras(cell120, all5, c5) {
const nb = cell5_neighbourhoods(cell120, all5, c5);
const v1 = cell120node(cell120, c5["1"]);
const ts = tetras(cell120, v1);
const c5s = [];
for( const t of ts ) {
const nt = nb.filter((n) => {
for( const l in n ) {
if( t.includes(n[l]) ) {
return true;
}
}
return false
});
for( const nc5 of nt ) {
const exact = c5s.filter((c) => c5match(c, nc5));
if( exact.length === 0 ) {
const o = c5s.filter((c) => overlap(c, nc5));
if( o.length > 0 ) {
console.log("Overlap", c5, o);
} else {
c5s.push(nc5);
}
}
}
}
return c5s;
}
function coherent_5cells_r(cell120, all5, c5s, c50) {
// Find next set of c5s, see if there are any we haven't seen,
// recurse into those ones
const c5ns = cell5_tetras(cell120, all5, c50);
const c5unseen = c5ns.filter((c5) => {
const matched = c5s.filter((c5b) => c5match(c5b, c5));
return matched.length === 0;
});
for( const c5u of c5unseen ) {
c5s.push(c5u);
}
for( const c5u of c5unseen ) {
coherent_5cells_r(cell120, all5, c5s, c5u);
}
}
function coherent_5cells(cell120, all5) {
// pick a starting point, collect coherent 5_cells, continue till
// there aren't any new ones
const c5set = [];
let c5 = all5[0];
const c5s = [];
coherent_5cells_r(cell120, all5, c5s, c5);
return c5s;
}
const cell120 = POLYTOPES.cell120_inscribed();
const all5 = gather_5cells(cell120);
const c5s = coherent_5cells(cell120, all5);
const celli = c5s.map((c5) => [ "1", "2", "3", "4", "5" ].map((l) => c5[l]));
// check it because I don't believe it yet
const vertex_check = {};
for( const c5 of celli ) {
for( const l in c5 ) {
const v = c5[l];
if( v in vertex_check ) {
console.log(`Double count vertex ${v}`);
}
vertex_check[v] = 1;
}
}
for( let i = 1; i < 601; i++ ) {
if( !vertex_check[i] ) {
console.log(`v ${i} missing`);
}
}
const idict = {};
for( let i = 1; i < 121; i++ ) {
idict[i] = celli[i - 1];
}
console.log(JSON.stringify(idict, null, 2));

View File

@ -1,10 +1,7 @@
import * as THREE from 'three';
import { TaperedLink } from './taperedLink.js';
const HYPERPLANE = 2.0;
const W_FORESHORTENING = 0.04;
class FourDShape extends THREE.Group {
@ -18,10 +15,11 @@ class FourDShape extends THREE.Group {
this.nodes3 = {};
this.links = structure.links;
this.faces = ( "faces" in structure ) ? structure.faces : [];
this.node_size = structure.geometry.node_size;
this.link_size = structure.geometry.link_size;
this.node_scale = 1;
this.link_scale = 1;
this.hyperplane = HYPERPLANE;
this.foreshortening = W_FORESHORTENING;
this.initShapes();
}
@ -29,15 +27,15 @@ class FourDShape extends THREE.Group {
// if a node/link has no label, use the 0th material
getMaterialLabel(entity) {
getMaterial(entity, materials) {
if( "label" in entity ) {
return entity.label
return materials[entity.label];
} else {
return 0;
return materials[0];
}
}
makeNode(material, v3, scale) {
makeNode(material, v3) {
const geometry = new THREE.SphereGeometry(this.node_size);
const sphere = new THREE.Mesh(geometry, material);
sphere.position.copy(v3);
@ -45,24 +43,35 @@ class FourDShape extends THREE.Group {
return sphere;
}
makeLink(materialLabel, link) {
const n1 = this.nodes3[link.source];
const n2 = this.nodes3[link.target];
const s1 = this.link_scale * n1.scale;
const s2 = this.link_scale * n2.scale;
const basematerial = this.link_ms[materialLabel];
const edge = new TaperedLink(basematerial, materialLabel, n1, n2, s1, s2);
makeLink(material, link) {
const n1 = this.nodes3[link.source].v3;
const n2 = this.nodes3[link.target].v3;
const length = n1.distanceTo(n2);
const centre = new THREE.Vector3();
centre.lerpVectors(n1, n2, 0.5);
const geometry = new THREE.CylinderGeometry(this.link_size, this.link_size, 1);
const cyl = new THREE.Mesh(geometry, material);
const edge = new THREE.Group();
edge.add(cyl);
edge.position.copy(centre);
edge.scale.copy(new THREE.Vector3(1, 1, length));
edge.lookAt(n2);
cyl.rotation.x = Math.PI / 2.0;
this.add(edge);
return edge;
}
updateLink(link, links_show) {
const n1 = this.nodes3[link.source];
const n2 = this.nodes3[link.target];
const s1 = this.link_scale * n1.scale;
const s2 = this.link_scale * n2.scale;
link.object.update(n1, n2, s1, s2);
link.object.visible = (!links_show || links_show.includes(link.label));
const n1 = this.nodes3[link.source].v3;
const n2 = this.nodes3[link.target].v3;
const length = n1.distanceTo(n2);
const centre = new THREE.Vector3();
centre.lerpVectors(n1, n2, 0.5);
link.object.scale.copy(new THREE.Vector3(this.link_scale, this.link_scale, length));
link.object.position.copy(centre);
link.object.lookAt(n2);
link.object.children[0].rotation.x = Math.PI / 2.0;
link.object.visible = (!links_show || link.label in links_show);
}
@ -91,41 +100,32 @@ class FourDShape extends THREE.Group {
}
fourDscale(w) {
return this.hyperplane / ( this.hyperplane + w );
}
fourDrotate(x, y, z, w, rotations) {
fourDtoV3(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);
const k = this.hyperplane / (this.hyperplane + v4.w);
return new THREE.Vector3(v4.x * k, v4.y * k, v4.z * k);
}
initShapes() {
for( const n of this.nodes4 ) {
const k = this.fourDscale(n.w);
const v3 = new THREE.Vector3(n.x * k, n.y * k, n.z * k);
const material = this.node_ms[this.getMaterialLabel(n)];
const v3 = this.fourDtoV3(n.x, n.y, n.z, n.w, []);
const material = this.getMaterial(n, this.node_ms);
this.nodes3[n.id] = {
v3: v3,
scale: k,
label: n.label,
object: this.makeNode(material, v3, k)
object: this.makeNode(material, v3)
};
}
for( const l of this.links ) {
const mLabel = this.getMaterialLabel(l);
l.object = this.makeLink(mLabel, l);
const material = this.getMaterial(l, this.link_ms);
l.object = this.makeLink(material, l);
}
for( const f of this.faces ) {
const material = this.face_ms(this.getMaterialLabel(f));
const material = this.getMaterial(f, this.face_ms);
f.object = this.makeFace(material, f);
}
}
@ -134,16 +134,11 @@ class FourDShape extends THREE.Group {
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 ) {
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);
const v3 = this.fourDtoV3(n.x, n.y, n.z, n.w, rotations);
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.scale.copy(s3);
this.nodes3[n.id].object.visible = ( !nodes_show || nodes_show.includes(n.label) );
this.nodes3[n.id].object.scale.copy(this.scalev3);
this.nodes3[n.id].object.visible = ( !nodes_show || n.label in nodes_show );
}
for( const l of this.links ) {
this.updateLink(l, links_show);

97
gui.js
View File

@ -2,25 +2,21 @@ import { GUI } from 'lil-gui';
const DEFAULTS = {
nodesize: 0.6,
nodeopacity: 1,
linksize: 1.0,
linkopacity: 0.75,
thickness: 1.0,
nodesize: 2.0,
linkopacity: 0.5,
link2opacity: 0.5,
shape: '120-cell',
link2opacity: 0.75,
option: 'none',
visibility: 5,
inscribed: false,
inscribe_all: false,
color: 0x3293a9,
background: 0xd4d4d4,
hyperplane: 0.93,
hyperplane: 1.5,
zoom: 1,
xRotate: 'YZ',
yRotate: 'XZ',
rotation: 'rigid',
dtheta: 0,
damping: false,
captions: true,
dpsi: 0,
}
@ -28,10 +24,9 @@ const DEFAULTS = {
class FourDGUI {
constructor(funcs) {
this.shapes = funcs.shapes;
constructor(shapes, changeShape, setColor, setBackground, setLinkOpacity, setVisibility) {
this.gui = new GUI();
const SHAPE_NAMES = this.shapes.map((s) => s.name);
const SHAPE_NAMES = shapes.map((s) => s.name);
this.parseLinkParams();
const guiObj = this;
@ -40,68 +35,54 @@ class FourDGUI {
option: this.link['option'],
inscribed: this.link['inscribed'],
inscribe_all: this.link['inscribe_all'],
linksize: this.link['linksize'],
thickness: this.link['thickness'],
linkopacity: this.link['linkopacity'],
link2opacity: this.link['link2opacity'],
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'],
rotation: this.link['rotation'],
damping: false,
captions: true,
dtheta: this.link['dtheta'],
dpsi: this.link['dpsi'],
"copy link": function () { guiObj.copyUrl() },
"copy link": function () { guiObj.copyUrl() }
};
if( funcs.extras ) {
for( const label in funcs.extras ) {
console.log(label);
console.log(funcs.extras[label]);
this.params[label] = funcs.extras[label];
}
}
let options_ctrl;
this.gui.add(this.params, 'shape', SHAPE_NAMES).onChange((shape) => {
const options = this.getShapeOptions(shape);
const options = this.getShapeOptions(shapes, shape);
options_ctrl = options_ctrl.options(options).onChange((option) => {
funcs.setVisibility(option)
setVisibility(option)
});
options_ctrl.setValue(options[0])
funcs.changeShape(shape)
changeShape(shape)
});
const options = this.getShapeOptions(this.params['shape']);
const options = this.getShapeOptions(shapes, this.params['shape']);
options_ctrl = this.gui.add(this.params, 'option').options(options).onChange((option) => {
funcs.setVisibility(option)
setVisibility(option)
});
this.gui.add(this.params, 'hyperplane', 0.5, 1 / 0.8);
this.gui.add(this.params, 'hyperplane', 1.4, 2.0);
this.gui.add(this.params, 'zoom', 0.1, 2.0);
this.gui.add(this.params, 'nodesize', 0, 1.5);
this.gui.add(this.params, 'nodeopacity', 0, 1).onChange(funcs.setNodeOpacity);
this.gui.add(this.params, 'linksize', 0, 2);
console.log(funcs.setLinkOpacity);
this.gui.add(this.params, 'linkopacity', 0, 1).onChange((v) => funcs.setLinkOpacity(v, true));
this.gui.add(this.params, 'link2opacity', 0, 1).onChange((v) => funcs.setLinkOpacity(v, false));
this.gui.addColor(this.params, 'color').onChange(funcs.setColor);
this.gui.addColor(this.params, 'background').onChange(funcs.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(this.showDocs);
this.gui.add(this.params, 'thickness', 0, 2);
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.add(this.params, 'nodesize', 0.1, 4);
this.gui.addColor(this.params, 'color').onChange(setColor);
this.gui.addColor(this.params, 'background').onChange(setBackground);
this.gui.add(this.params, 'rotation', [ 'rigid', 'tumbling', 'inside-out', 'axisymmetrical' ]);
this.gui.add(this.params, 'damping');
this.gui.add(this.params, 'copy link');
if( funcs.extras ) {
for( const label in funcs.extras ) {
this.gui.add(this.params, label);
}
}
}
getShapeOptions(shape) {
const spec = this.shapes.filter((s) => s.name === shape);
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 {
@ -137,7 +118,7 @@ class FourDGUI {
const guiObj = this;
this.urlParams = this.linkUrl.searchParams;
for( const param of [ "shape", "xRotate", "yRotate", "option" ]) {
for( const param of [ "shape", "rotation", "option" ]) {
const value = this.urlParams.get(param);
if( value ) {
this.link[param] = value;
@ -150,11 +131,10 @@ class FourDGUI {
}
this.link['hyperplane'] = this.numParam('hyperplane', parseFloat);
this.link['zoom'] = this.numParam('zoom', parseFloat);
this.link['linksize'] = this.numParam('linksize', parseFloat);
this.link['thickness'] = this.numParam('thickness', 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);
@ -168,16 +148,15 @@ class FourDGUI {
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("thickness", this.params.thickness.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("linkopacity", this.params.thickness.toString());
url.searchParams.append("link2opacity", this.params.nodesize.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("rotation", this.params.rotation);
url.searchParams.append("dtheta", this.params.dtheta.toString());
url.searchParams.append("dpsi", this.params.dpsi.toString());
this.copyTextToClipboard(url);

View File

@ -5,24 +5,6 @@
<title>FourD</title>
<style>
body { margin: 0; }
div#description {
position: fixed;
top: 0;
left: 0;
width: 20%;
z-index: 2;
font-family: sans-serif;
padding: 1em;
}
div#release_notes {
position: fixed;
top: 0;
left: 0;
width: 20%;
z-index: 2;
padding: 1em;
font-family: sans-serif;
}
div#info {
position: fixed;
bottom:0;
@ -34,11 +16,7 @@
</head>
<body>
<script type="module" src="/main.js"></script>
<div id="description"></div>
<div id="release_notes"></div>
<div id="info"><a href="#" id="show_notes">release 1.1</a> |
by <a target="_blank" href="https://mikelynch.org/">Mike Lynch</a> |
<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>
</html>

View File

@ -1,119 +0,0 @@
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GUI } from 'lil-gui';
import { TaperedLink } from './taperedLink.js';
const FACE_OPACITY = 0.3;
const CAMERA_K = 5;
// scene, lights and camera
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const light = new THREE.PointLight(0xffffff, 2);
light.position.set(10, 10, 10);
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);
scene.add(amblight);
camera.position.set(0, 0, CAMERA_K / 2);
camera.lookAt(0, 0, 0);
camera.position.z = 8;
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.localClippingEnabled = true;
const controls = new OrbitControls( camera, renderer.domElement );
controls.autoRotate = true;
document.body.appendChild( renderer.domElement );
const NODEC = 0x3293a9;
const LINKC = 0x00ff88;
const BACKGROUNDC = 0xd4d4d4;
scene.background = new THREE.Color(BACKGROUNDC);
const material = new THREE.MeshStandardMaterial({ color: LINKC });
material.transparent = true;
material.opacity = 0.7;
const node_mat = new THREE.MeshStandardMaterial({ color: NODEC });
node_mat.transparent = true;
node_mat.opacity = 0.5;
const params = {
r1: 0.5,
r2: 0.6,
sync: false,
l: 9,
rotx: 1,
roty: 0,
rotz: 0,
};
const gui = new GUI();
gui.add(params, "r1", 0.01, 1.5);
gui.add(params, "r2", 0.01, 1.5);
gui.add(params, "sync");
gui.add(params, "l", 0, 10);
gui.add(params, "rotx", 0, 4);
gui.add(params, "roty", 0, 4);
gui.add(params, "rotz", 0, 4);
function makeNode(material, pos, r) {
const geometry = new THREE.SphereGeometry(1);
const sphere = new THREE.Mesh(geometry, material);
const node = {
v3: pos,
object: sphere
};
updateNode(node, pos, r);
return node;
}
function updateNode(node, pos, r) {
node.v3 = pos;
node.object.scale.copy(new THREE.Vector3(r, r, r));
node.object.position.copy(pos);
}
const n1 = makeNode(node_mat, new THREE.Vector3(-params["l"], -1, -1), params["r1"]);
const n2 = makeNode(node_mat, new THREE.Vector3(params["l"], 1, 1), params["r2"]);
const tl = new TaperedLink(material, n1, n2, params["r1"], params["r2"]);
scene.add(n1.object);
scene.add(n2.object);
scene.add(tl);
function animate() {
requestAnimationFrame(animate);
const r1 = params["r1"];
const r2 = params["sync"] ? r1 : params["r2"]
updateNode(n1, new THREE.Vector3(- params["l"], -1, -1), r1);
updateNode(n2, new THREE.Vector3(params["l"], 1, 1), r2);
tl.update(n1, n2, r1, r2, params["rotx"], params["roty"], params["rotz"]);
controls.update();
renderer.render(scene, camera);
}
animate();

138
main.js
View File

@ -1,31 +1,15 @@
import * as THREE from 'three';
const RELEASE_NOTES = `
<p><b>v1.1 - 1/1/2026</b></p>
<p>The 120-cell now includes a visualisation of its inscribed 5-cells, which honestly
looks like less of a mess than I expected it to.</p>
<p><b>v1.0 - 16/11/2025</b></p>
<p>It's been <a target="_blank" href="https://mikelynch.org/2023/Sep/02/120-cell/">two years</a> since
I first made this, and I haven't updated it in a while, but I got tapered links to
work without too much performance overhead, so that seemed worth a version.</p>
<p>The results flicker a bit at low opacities but otherwise I'm pretty happy with
it.</p>
`;
import * as POLYTOPES from './polytopes.js';
import { rotfn } from './rotation.js';
import { get_rotation } from './rotation.js';
import { FourDGUI, DEFAULTS } from './gui.js';
import { FourDShape } from './fourDShape.js';
import { get_colours } from './colours.js';
const FACE_OPACITY = 0.3;
const CAMERA_K = 5;
const CAMERA_K = 10;
// scene, lights and camera
@ -47,37 +31,30 @@ camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.localClippingEnabled = true;
document.body.appendChild( renderer.domElement );
// set up colours and materials for gui callbacks
scene.background = new THREE.Color(DEFAULTS.background);
const material = new THREE.MeshStandardMaterial({ color: DEFAULTS.color });
const node_colours = get_colours(DEFAULTS.color);
material.transparent = true;
material.opacity = 0.5;
const node_ms = node_colours.map((c) => new THREE.MeshStandardMaterial({color: c}));
const link_ms = node_colours.map((c) => new THREE.MeshStandardMaterial({color: c}));
node_ms.map((m) => {
m.transparent = true;
m.opacity = 1.0;
}
);
link_ms.map((m) => {
m.transparent = true;
m.opacity = 0.5;
}
);
console.log("link_ms", link_ms);
)
const face_ms = [
new THREE.MeshStandardMaterial( { color: 0x44ff44 } )
new THREE.MeshLambertMaterial( { color: 0x44ff44 } )
];
for( const face_m of face_ms ) {
@ -108,44 +85,6 @@ function createShape(name, option) {
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) {
const docdiv = document.getElementById("description");
if( visible ) {
docdiv.style.display = '';
} else {
docdiv.style.display = 'none';
}
}
function releaseNotes() {
showDocs(false);
const reldiv = document.getElementById("release_notes");
reldiv.style.display = '';
reldiv.innerHTML = RELEASE_NOTES + '<p><a id="no_notes" href="#">[hide]</a>';
const goaway = document.getElementById("no_notes");
goaway.addEventListener('click', noNotes);
}
function noNotes() {
const reldiv = document.getElementById("release_notes");
reldiv.style.display = 'none';
}
const relnotes = document.getElementById('show_notes');
relnotes.addEventListener('click', releaseNotes);
// initialise gui and read params from URL
// callbacks to do things which are triggered by controls: reset the shape,
@ -157,47 +96,33 @@ function setColors(c) {
node_ms[i].color = new THREE.Color(nc[i]);
link_ms[i].color = new THREE.Color(nc[i]);
}
if( shape ) {
// taperedLink.set_color updates according to the link index
shape.links.map((l) => l.object.set_color(nc));
}
material.color = new THREE.Color(c);
}
function setBackground(c) {
scene.background = new THREE.Color(c)
}
// taperedLinks have their own materials so we have to set opacity
// on them individually. And also set the base materials as they
// will get updated from it when the shape changes
function setLinkOpacity(o, primary) {
if( structure.nolink2opacity ) {
link_ms.map((lm) => lm.opacity = o);
if( shape ) {
shape.links.map((l) => {
if( (primary && l.label == 0) || (!primary && l.label !== 0) ) {
l.object.material.opacity = o
}
});
} else {
if( primary ) {
link_ms[0].opacity = o;
} else {
link_ms.slice(1).map((lm) => lm.opacity = o);
}
}
function setNodeOpacity(o) {
node_ms.map((nm) => nm.opacity = o);
}
let gui;
function changeShape() {
createShape(gui.params.shape);
displayDocs(gui.params.shape);
}
function setVisibility(option_name) {
console.log("setVisibility", option_name);
console.log(structure.options);
const option = structure.options.filter((o) => o.name === option_name);
if( option.length ) {
node_show = option[0].nodes;
@ -209,16 +134,12 @@ function setVisibility(option_name) {
gui = new FourDGUI(
{
shapes: STRUCTURES,
changeShape: changeShape,
setColors: setColors,
setBackground: setBackground,
setNodeOpacity: setNodeOpacity,
setLinkOpacity: setLinkOpacity,
setVisibility: setVisibility,
showDocs: showDocs,
}
STRUCTURES,
changeShape,
setColors,
setBackground,
setLinkOpacity,
setVisibility
);
// these are here to pick up colour settings from the URL params
@ -263,7 +184,6 @@ renderer.domElement.addEventListener("pointerup", (event) => {
})
createShape(gui.params.shape, gui.params.option);
displayDocs(gui.params.shape);
function animate() {
requestAnimationFrame( animate );
@ -277,15 +197,13 @@ function animate() {
}
}
const rotations = [
rotfn[gui.params.xRotate](theta),
rotfn[gui.params.yRotate](psi)
];
shape.hyperplane = 1 / gui.params.hyperplane;
camera.position.set(0, 0, gui.params.zoom * CAMERA_K * gui.params.hyperplane);
const rotations = get_rotation(gui.params.rotation, theta, psi);
shape.hyperplane = gui.params.hyperplane;
camera.position.set(0, 0, gui.params.zoom * CAMERA_K / gui.params.hyperplane);
shape.link_scale = gui.params.thickness;
shape.node_scale = gui.params.nodesize;
shape.link_scale = gui.params.linksize * gui.params.nodesize * 0.5;
shape.render3(rotations, node_show, link_show);

58
package-lock.json generated
View File

@ -4,9 +4,7 @@
"requires": true,
"packages": {
"": {
"name": "fourdjs",
"dependencies": {
"color": "^4.2.3",
"color-scheme": "^1.0.1",
"lil-gui": "^0.19.0",
"three": "^0.154.0"
@ -367,48 +365,11 @@
"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": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
@ -460,11 +421,6 @@
"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": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.19.0.tgz",
@ -538,14 +494,6 @@
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
@ -561,9 +509,9 @@
"integrity": "sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug=="
},
"node_modules/vite": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
"integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
"dev": true,
"dependencies": {
"esbuild": "^0.18.10",

View File

@ -1,6 +1,5 @@
{
"dependencies": {
"color": "^4.2.3",
"color-scheme": "^1.0.1",
"lil-gui": "^0.19.0",
"three": "^0.154.0"

View File

@ -55,38 +55,19 @@ export function auto_detect_edges(nodes, neighbours, debug=false) {
return links;
}
export const linkTest = () => {
return {
name: 'linky',
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 },
],
links: [
{ id: 1, source: 1, target: 2 }
],
options: [ { name: '--' }],
description: `link`,
}
};
// too small and simple to calculate
export const cell5 = () => {
const c1 = Math.sqrt(5) / 4;
const r5 = Math.sqrt(5);
const r2 = Math.sqrt(2) / 2;
return {
name: '5-cell',
nodes: [
{id:1, label: 1, x: c1, y: c1, z: c1, w: -0.25 },
{id:2, label: 2, x: c1, y: -c1, z: -c1, w: -0.25 },
{id:3, label: 3, x: -c1, y: c1, z: -c1, w: -0.25 },
{id:4, label: 4, x: -c1, y: -c1, z: c1, w: -0.25 },
{id:5, label: 5, x: 0, y: 0, z: 0, w: 1 },
{id:1, label: 1, x: r2, y: r2, z: r2, w: -r2 / r5 },
{id:2, label: 2, x: r2, y: -r2, z: -r2, w: -r2 / r5 },
{id:3, label: 3, x: -r2, y: r2, z: -r2, w: -r2 / r5 },
{id:4, label: 4, x: -r2, y: -r2, z: r2, w: -r2 / r5 },
{id:5, label: 5, x: 0, y: 0, z: 0, w: 4 * r2 / r5 },
],
links: [
{ id:1, source:1, target: 2},
@ -100,12 +81,11 @@ export const cell5 = () => {
{ id:9, source:3, 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.`,
geometry: {
node_size: 0.02,
link_size: 0.02
},
options: [ { name: '--' }]
};
};
@ -124,18 +104,18 @@ export const cell16 = () => {
nodes[1].label = 4;
index_nodes(nodes);
scale_nodes(nodes, 0.5);
scale_nodes(nodes, 0.75);
const links = auto_detect_edges(nodes, 6);
return {
name: '16-cell',
nodes: nodes,
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.`,
geometry: {
node_size: 0.02,
link_size: 0.02
},
options: [ { name: '--' }]
};
};
@ -153,7 +133,7 @@ export const tesseract = () => {
}
}
scale_nodes(nodes, 0.5);
scale_nodes(nodes, Math.sqrt(2) / 2);
const links = auto_detect_edges(nodes, 4);
links.map((l) => { l.label = 0 });
@ -166,20 +146,18 @@ export const tesseract = () => {
return {
name: 'Tesseract',
name: 'tesseract',
nodes: nodes,
links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
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.`,
};
}
@ -205,7 +183,6 @@ export const cell24 = () => {
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);
@ -229,16 +206,16 @@ export const cell24 = () => {
name: '24-cell',
nodes: nodes,
links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
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.`,
]
};
}
@ -342,7 +319,7 @@ export function make_120cell_vertices() {
PERMUTE.coordinates([2, 1, phi, phiinv], 0, true),
].flat();
index_nodes(nodes);
scale_nodes(nodes, 0.25 * Math.sqrt(2));
scale_nodes(nodes, 0.5);
return nodes;
}
@ -416,10 +393,12 @@ export const cell120_layered = (max) => {
name: '120-cell layered',
nodes: nodes,
links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
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.`,
options: options
}
}
@ -443,60 +422,24 @@ export const cell120_inscribed = () => {
links.push(...links600);
}
const CELL5S = CELLINDEX.CELL120_CELL5.cell5s;
for( const c5 in CELL5S ) {
const nodes5 = nodes.filter((n) => CELL5S[c5].includes(n.id));
const links5 = auto_detect_edges(nodes5, 5);
links5.map((l) => l.label = 8);
links.push(...links5);
}
return {
name: '120-cell',
nodes: nodes,
links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
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 ] },
{ name: "120 inscribed 5-cells", links: [ 0, 8 ] },
],
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. The converse
of this allows 120 5-cells (each of which has one vertex in each
of the 5 600-cells) to be inscribed in the 120-cell.`,
{ name: "five inscribed 600-cells", links: [ 0, 1, 2, 3, 4, 5 ] }
]
}
}
export const cell120_inscribed_cell5 = () => {
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);
return {
name: '120-cell-5-cell',
nodes: nodes,
links: links,
options: [
{ name: "5-cells", links: [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ] },
],
description: `The 120-cell with one of its 5-cells.`,
}
}
function partition_coord(i, coords, invert) {
@ -572,7 +515,7 @@ export function make_600cell_vertices() {
index_nodes(nodes);
scale_nodes(nodes, 0.5);
scale_nodes(nodes, 0.75);
return nodes;
}
@ -614,16 +557,15 @@ export const cell600 = () => {
name: '600-cell',
nodes: nodes,
links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
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.`,
]
}
}
@ -663,10 +605,12 @@ export const cell600_layered = () => {
name: '600-cell layered',
nodes: nodes,
links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
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.`,
options: options
}
@ -684,18 +628,19 @@ export const snub24cell = () => {
return sn && tn;
});
console.log(nodes);
links.map((l) => l.label = 0);
return {
name: 'Snub 24-cell',
name: 'snub 24-cell',
nodes: nodes,
links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
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.`
}
@ -735,7 +680,6 @@ function make_dodecahedron_vertices() {
{ 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;
}
@ -753,146 +697,18 @@ export const dodecahedron = () => {
}
return {
name: 'Dodecahedron',
name: 'dodecahedron',
nodes: nodes,
links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
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.`
}
}
@ -900,10 +716,6 @@ export const icosahedron = () => {
export const build_all = () => {
return [
tetrahedron(),
octahedron(),
cube(),
icosahedron(),
dodecahedron(),
cell5(),
cell16(),
@ -916,7 +728,3 @@ export const build_all = () => {
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))
}

View File

@ -81,5 +81,24 @@ export const rotfn = {
ZW: rotZW,
};
const rotMode = {
'rigid': [ rotYW, rotXW ],
'tumbling': [ rotYW, rotXZ ],
'inside-out': [ rotYW, rotXY ],
'axisymmetrical': [ rotZW, rotXY ]
};
export const get_rotation = (mode, theta, psi) => {
const fns = rotMode[mode];
return [ fns[0](theta), fns[1](psi) ];
}
// [
// rotfn[gui.params.xRotate](theta),
// rotfn[gui.params.yRotate](psi)
// ];

View File

@ -1,66 +0,0 @@
import * as THREE from 'three';
const EPSILON = 0.001;
class TaperedLink extends THREE.Group {
constructor(baseMaterial, color_i, n1, n2, r1, r2) {
super();
const geometry = new THREE.ConeGeometry( 1, 1, 16, true );
const cplane = new THREE.Plane(new THREE.Vector3(0, -1, 0), 0.5);
this.color_i = color_i;
this.material = baseMaterial.clone();
this.material.clippingPlanes = [ cplane ];
this.object = new THREE.Mesh( geometry, this.material );
this.add( this.object );
this.update(n1, n2, r1, r2);
}
update(n1, n2, r1, r2) {
const kraw = r1 - r2;
let k = ( Math.abs(kraw) < EPSILON ) ? EPSILON : kraw;
let nbase = n1.v3;
let napex = n2.v3;
let rbase = r1;
let rapex = r2;
if( k < 0 ) {
nbase = n2.v3;
napex = n1.v3;
rbase = r2;
rapex = r1;
k = -k;
}
const l = nbase.distanceTo(napex);
const lapex = l * rapex / k;
const h = l + lapex;
this.scale.copy(new THREE.Vector3(rbase, rbase, h));
const h_offset = 0.5 * h / l;
const pos = new THREE.Vector3();
pos.lerpVectors(nbase, napex, h_offset);
this.position.copy(pos); // the group, not the cone!!
this.lookAt(nbase);
this.children[0].rotation.x = 3 * Math.PI / 2.0;
this.visible = true;
const clipnorm = new THREE.Vector3();
clipnorm.copy(napex);
clipnorm.sub(nbase);
clipnorm.negate();
clipnorm.normalize();
this.material.clippingPlanes[0].setFromNormalAndCoplanarPoint(
clipnorm, napex
);
}
set_color(colors) {
this.material.color = new THREE.Color(colors[this.color_i]);
}
}
export { TaperedLink };