Compare commits

..

No commits in common. "main" and "feature-120-cell-layers" have entirely different histories.

20 changed files with 415 additions and 4937 deletions

View File

@ -1,87 +0,0 @@
# 600-cell mapping
These are the nodes from a 120-cell which are on one of its inscribed 600-cells,
sorted into layers (just the first half because the second half mirror these ones)
Start: [ 27 ]
0: [ 27 ]
0.618:
[
223, 253, 265, 331,
339, 393, 419, 427,
473, 511, 539, 555
]
1
[
95, 105, 131, 140, 165, 179,
185, 207, 258, 274, 306,
313, 347, 367, 449, 471,
499, 527, 573, 585
]
1.175
[
231, 285, 289, 324,
378, 388, 413, 425,
487, 513, 543, 563
]
1.414
[
48, 49, 61, 68, 74, 87, 234,
239, 241, 248, 300, 301, 356, 357,
369, 376, 403, 406, 444, 453, 460,
469, 490, 503, 525, 532, 572, 581,
592, 593
]
## Manual mapping progress
Starting from 27 on the inscribed 600 cell and 1 on the primary 600 cell, here are
the two arctic circles
Pole: 27: 1
Arctic circle:
419: 41
223: 49
253: 45
331: 53
427: 109
339: 105
511: 51
265: 107
473: 111
539: 55
555: 43
393: 47
Next: for each face on this icosahedron, find the other vertex - these are the next 20
367
131
499
179
471
165
449
258
274
95
347
313
185
140
527
573
105
585
306
207
Then - for

View File

@ -1,28 +0,0 @@
CHANGELOG
=========
## v1.3 - 7/2/2026
Went to inordinate lengths to apply the partition of the 600-cell (into five
24-cells) to the 5-cell inscription in the 120-cell, so that they could be coloured
in a way which reveals some of that symmetry.
## v1.2 - 18/1/2026
Added a second visualisation of the 120-cell's 5-cells without the 120-cell links
and with more colours added so you can get a sense of the individual 5-cells.
## 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,15 +0,0 @@
# NOTES
## Labelling the inscribed 600-cells in a 120-cell
I want to apply the partition of the 600-cell into five 24-cells to the inscribed
600-cells in the 120-cell, so that I can use this partition to colour the inscribed
5-cells - since each 5-cell has a vertex in each of the 600-cells, getting a
partition for just one will be enough.
The challenge is that the 600-cells in the 120-cell are rotated differently to the
coordinates for the original 600-cell. And I don't have enough maths to line them up.
What about - given one of the inscribed 600-cell, find all of the 24-cells? (there
are 25 possible ones so sorting them out will be a pain)

View File

@ -1,554 +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;
}
function coherent_all() {
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));
}
function coherent_one_set() {
const cell120 = POLYTOPES.cell120_inscribed();
const all5 = gather_5cells(cell120);
const c5ns = cell5_tetras(cell120, all5, all5[0]);
const celli = c5ns.map((c5) => [ "1", "2", "3", "4", "5" ].map((l) => c5[l]));
const idict = {};
for( let i = 0; i < celli.length; i++ ) {
idict[i + 1] = celli[i];
}
console.log(JSON.stringify(idict, null, 2));
}
function cell120_csv() {
const cell120 = POLYTOPES.cell120_inscribed();
const coords = [ 'x', 'y', 'z', 'w' ];
console.log("id,label,x,y,z,w,zeroes");
for( const n of cell120.nodes ) {
const zc = coords.filter((c) => n[c] === 0);
console.log(`${n.id},${n.label},${n.x},${n.y},${n.z},${n.w},${zc.length}`);
}
}
function cell600_links(cell600, n) {
const links = cell600.links.filter((l) => l.source === n.id || l.target === n.id);
const nbors = links.map((l) => {
if( l.source === n.id ) {
return l.target;
} else {
return l.source;
}
});
return nbors.sort((a, b) => a - b);
}
function cell600_csv() {
const cell600 = POLYTOPES.cell600();
console.log("id,label,x,y,z,w,d");
const n0 = cell600.nodes[0];
for( const n of cell600.nodes ) {
const d = dist(n0, n);
const nbors = cell600_links(cell600, n);
const nids = nbors.join(',');
console.log(`${n.id},${n.label},${n.x},${n.y},${n.z},${n.w},${d},${nids}`);
}
}
cell600_csv();

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +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();
colours.reverse();
const hsl = colours.map((c) => Color("#" + c).hsl());
const resaturated = hsl.map((hslc) => hslc.saturationl(saturation).rgbNumber());
resaturated.unshift(basis);
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
@ -34,4 +28,4 @@ export const get_plain_colours = (basis) => {
0xff9900,
0x000000,
]
}
}

View File

@ -1 +0,0 @@

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) {
if( "label" in entity ) {
return entity.label
} else {
return 0;
}
getMaterial(entity, materials) {
if( "label" in entity ) {
return materials[entity.label];
} else {
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);
this.add( edge );
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;
const k = this.hyperplane / (this.hyperplane + v4.w);
return new THREE.Vector3(v4.x * k, v4.y * k, v4.z * k);
}
fourDtoV3(v4) {
const k = this.fourDscale(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);

119
gui.js
View File

@ -2,25 +2,20 @@ import { GUI } from 'lil-gui';
const DEFAULTS = {
nodesize: 0.6,
nodeopacity: 1,
linksize: 1.0,
linkopacity: 0.75,
shape: '600-cell',
link2opacity: 0.75,
option: 'none',
thickness: 0.25,
nodesize: 1.25,
linkopacity: 0.5,
link2opacity: 0.5,
shape: '',
option: '',
visibility: 5,
inscribed: false,
inscribe_all: false,
colour: 0x3293a9,
color: 0x3293a9,
background: 0xd4d4d4,
hyperplane: 0.93,
zoom: 1,
xRotate: 'YZ',
yRotate: 'XZ',
hyperplane: 2,
rotation: 'rigid',
dtheta: 0,
damping: false,
captions: true,
dpsi: 0,
}
@ -28,10 +23,12 @@ 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);
// set default shape + option from the first shape
DEFAULTS.shape = shapes[0].name;
DEFAULTS.option = shapes[0].options[0].name;
this.parseLinkParams();
const guiObj = this;
@ -40,65 +37,52 @@ 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'],
colour: this.link['colour'],
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 ) {
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']);
options_ctrl = this.gui.add(this.params, 'option').options(options).onChange((option) => {
funcs.setVisibility(option)
const options = this.getShapeOptions(shapes, this.params['shape']);
options_ctrl = this.gui.add(this.params, 'option', 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.5);
this.gui.add(this.params, 'nodeopacity', 0, 1).onChange(funcs.setNodeOpacity);
this.gui.add(this.params, 'linksize', 0, 2);
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, 'colour').onChange(funcs.setColours);
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, 'hyperplane', 1.5, 2.25);
this.gui.add(this.params, 'thickness', 0.1, 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 {
@ -134,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;
@ -146,13 +130,11 @@ class FourDGUI {
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['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['colour'] = this.numParam('colour', (s) => guiObj.stringToHex(s));
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);
@ -165,16 +147,14 @@ 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("colour", this.hexToString(this.params.colour));
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);
@ -187,6 +167,7 @@ class FourDGUI {
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);
});
@ -221,4 +202,4 @@ class FourDGUI {
}
export { FourDGUI, DEFAULTS };
export { FourDGUI, DEFAULTS };

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.3</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>
</html>

View File

@ -1,409 +0,0 @@
import * as POLYTOPES from './polytopes.js';
import * as CELLINDEX from './cellindex.js';
// script to help me label the vertices of one of the inscribed 600-cells of a 120-cell
// with Schoute's partition (which is used to label the main 600-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;
}
}
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);
}
function round_dist(raw) {
return Math.floor(raw * 1000) / 1000;
}
export function make_one_600cell() {
const nodes = POLYTOPES.make_120cell_vertices();
const links = POLYTOPES.auto_detect_edges(nodes, 4);
for( const cstr in CELLINDEX.INDEX120 ) {
POLYTOPES.label_nodes(nodes, CELLINDEX.INDEX120[cstr], Number(cstr));
}
links.map((l) => l.label = 0);
const nodes600 = nodes.filter((n) => n.label === 1);
const links600 = POLYTOPES.auto_detect_edges(nodes600, 12);
links600.map((l) => l.label = 1);
return {
nodes: nodes600,
links: links600
}
}
export function base_600cell() {
const nodes = POLYTOPES.make_600cell_vertices();
const links = POLYTOPES.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);
}
return {
nodes: nodes,
links: links,
};
}
export function distance_groups(shape) {
// get list of other nodes by distance
// sort them and dump them out
const dists = {};
shape.nodes.map((n) => {
const draw = dist(shape.nodes[0], n);
const dtrunc = round_dist(draw);
if( !(dtrunc in dists) ) {
dists[dtrunc] = [];
}
dists[dtrunc].push(n.id);
});
return dists;
}
export function insc600_layers(cell600) {
const layers = distance_groups(cell600);
/* const sorted = Object.keys(layers).sort((a,b) => a - b);
for( const d of sorted ) {
const ids = layers[d].map((n) => n.id);
console.log(`Layer at distance ${d}`);
console.log(ids);
} */
return layers;
}
export function neighbours(shape, nid) {
const links = shape.links.filter((l) => l.source === nid || l.target == nid );
const nodes = links.map((l) => {
if( l.source === nid ) {
return l.target;
} else {
return l.source;
}
});
return nodes;
}
export function neighbours_in_subset(shape, subset, nid) {
// shape = nodes, links
// subset = a list of ids
// n = an id
// returns all of n's neighbours which are in subset
const all_nbors = neighbours(shape, nid);
return all_nbors.filter((n) => subset.includes(n));
}
export function face_vertices(shape, f1, f2, f3) {
// for f1/f2/f3 forming a triangular face, return the two vertices of the
// adjacent tetrahedra
const n1 = neighbours(shape, f1).filter((n) => n !== f2 && n !== f3);
const n2 = neighbours(shape, f2).filter((n) => n !== f1 && n !== f3);
const n3 = neighbours(shape, f3).filter((n) => n !== f1 && n !== f2);
const ns = n1.filter((n) => n2.includes(n) && n3.includes(n));
return ns;
}
export function shared_neighbours(shape, nodes) {
let ns = shape.nodes.map((n) => n.id);
for( const n of nodes ) {
ns = neighbours_in_subset(shape, ns, n);
}
return ns;
}
export function layer_neighbours(cell600, layer) {
console.log("Layer neighbours");
for( const n of layer ) {
console.log(`n = ${n}`);
const nbors = neighbours_in_subset(cell600, layer, n);
console.log(` Vertex ${n} neighbours: ` + JSON.stringify(nbors));
}
}
const ARCTIC_I_FACES = [
[ 419, 223, 253 ],
[ 419, 253, 331 ],
[ 419, 331, 427 ],
[ 419, 427, 339 ],
[ 419, 339, 223 ],
[ 253, 223, 265 ],
[ 331, 253, 473 ],
[ 427, 331, 539 ],
[ 339, 427, 555 ],
[ 511, 339, 223 ],
[ 223, 511, 265 ],
[ 253, 265, 473 ],
[ 331, 473, 539 ],
[ 427, 539, 555 ],
[ 339, 555, 511 ],
[ 393, 265, 511 ],
[ 393, 473, 265 ],
[ 393, 539, 473 ],
[ 393, 555, 539 ],
[ 393, 555, 511 ]
];
export const ARCTIC_FACES = ARCTIC_I_FACES.map((f) => {
return f.map((nid) => CELLINDEX.CELL600_METAMAP[nid]);
});
export const TEMPERATE_PENTAGONS_I = [
[ 499, 179, 471, 367, 131 ],
[ 131, 367, 165, 313, 449 ],
[ 131, 449, 185, 258, 499 ],
[ 499, 258, 140, 274, 179 ],
[ 179, 274, 527, 95, 471 ],
[ 471, 95, 347, 165, 367 ],
[ 347, 573, 105, 313, 165 ],
[ 313, 105, 585, 185, 449 ],
[ 185, 585, 306, 140, 258 ],
[ 140, 306, 207, 527, 274 ],
[ 527, 207, 573, 347, 95 ],
[ 105, 573, 207, 306, 585 ],
];
export const TEMPERATE_PENTAGONS = TEMPERATE_PENTAGONS_I.map((f) => {
return f.map((nid) => CELLINDEX.CELL600_METAMAP[nid])
});
export function layer_two(cell600, centre, faces) {
for ( const face of faces ) {
const n2 = face_vertices(cell600, face[0], face[1], face[2]);
console.log(face, n2);
}
}
export function layer_three(shape, pentagons) {
for ( const pentagon of pentagons ) {
console.log(pentagon);
const s = shared_neighbours(shape, pentagon);
console.log(s);
console.log("\n");
}
}
export const TEMPERATE_APICES = [
563,
513,
285,
324,
231,
487,
413,
425,
378,
388,
543,
289,
];
// this one generates the mapping to the base 600 cell as well, unlike
// previous versions where I did the mapping by hand
export function equator(i600, b600, apices) {
const pairs = [];
// get all 30 of the edges on the temperate dodeca
for( let i = 0; i < 11; i++ ) {
for( let j = i + 1; j < 12; j++ ) {
const s = shared_neighbours(i600, [ apices[i], apices[j] ]);
if( s.length > 0 ) {
const e = s.filter((n) => !(n in CELLINDEX.CELL600_METAMAP));
pairs.push([apices[i], apices[j], e]);
}
}
}
const MAPPED = Object.values(CELLINDEX.CELL600_METAMAP);
const eq = {};
for( const pair of pairs ) {
const b1 = CELLINDEX.CELL600_METAMAP[pair[0]];
const b2 = CELLINDEX.CELL600_METAMAP[pair[1]];
const s = shared_neighbours(b600, [ b1, b2 ]);
const e = s.filter((n) => !MAPPED.includes(n));
if( e.length !== 1 ) {
console.log(`Bad value at ${pair}`);
} else {
eq[pair[2]] = e[0];
}
}
return eq;
}
export function antipode(shape, nid) {
const n0 = shape.nodes.filter((n) => n.id === nid)[0];
if( !n0 ) {
throw new Error(`antipodes error: couldn't find node ${nid} in shape`);
}
const dists = shape.nodes.map((n) => [ dist(n, n0), n ]);
dists.sort((a, b) => b[0] - a[0]);
return dists[0][1];
}
export function check_antipodes() {
const c600 = base_600cell();
const seen = {};
c600.nodes.map((n) => {
const a = antipode(c600, n.id);
if( !seen[a.id] && !seen[n.id] ) {
seen[a.id] = true;
seen[n.id] = true;
console.log(`${n.id} - ${n.label} / ${a.id} - ${a.label}`);
if( n.label !== a.label ) {
console.lot("MISMATCH");
}
}
});
}
export function meta600_label(b600, iid) {
const bid = CELLINDEX.CELL600_METAMAP[iid];
const bn = b600.nodes.filter((n) => bid === n.id);
return bn[0].label;
}
export function map_antipodes() {
const b600 = base_600cell();
const i600 = make_one_600cell();
const already = [];
const antimap = {};
for( const inid in CELLINDEX.CELL600_METAMAP ) {
const bnid = CELLINDEX.CELL600_METAMAP[inid];
const banti = antipode(b600, Number(bnid));
const ianti = antipode(i600, Number(inid));
if( CELLINDEX.CELL600_METAMAP[ianti.id] ) {
//console.log(`Anti ${ianti.id} is already mapped`);
already.push(ianti.id);
const l1 = meta600_label(b600, inid);
const l2 = meta600_label(b600, Number(ianti.id));
//console.log(`labels: ${l1} ${l2}`);
} else {
antimap[ianti.id] = banti.id;
}
}
console.log(JSON.stringify(antimap, null, 2));
}
export function check_metamap_completeness() {
const b600 = base_600cell();
const i600 = make_one_600cell();
const labels = {};
const bids = {};
const mm = CELLINDEX.CELL600_METAMAP;
for( const i of i600.nodes ) {
if( i.id in mm ) {
const ml = meta600_label(b600, i.id);
if( !(ml in labels) ) {
labels[ml] = [];
}
labels[ml].push(i.id);
bids[mm[i.id]] = 1;
} else {
console.log(`inscribed node ${i.id} is not in metamap`);
}
}
for( const b of b600.nodes ) {
if( !(b.id in bids) ) {
console.log(`base mode ${b.id} is not mapped`);
}
}
for ( const label in labels ) {
console.log(`label ${label} has ${labels[label].length} nodes`);
}
}
// this gives a mapping from cell-120-ids of one inscribed 600-cell to the
// metamap labels, which I can then [checks notes] use to colour the 5-cells.
export function metamap_to_labels() {
const b600 = base_600cell();
const i600 = make_one_600cell();
const mapping = {};
for( const inode of i600.nodes ) {
mapping[inode.id] = meta600_label(b600, inode.id);;
}
return mapping;
}
export function cell5_labels() {
const labels = metamap_to_labels();
// now build a dict of the 120 cell5s with the colours from the above
const cell5map = {};
const CELL5S = CELLINDEX.CELL120_CELL5.cell5s;
for( const c5i in CELL5S ) {
const n1 = CELL5S[c5i][0]; // label 1 node;
const ml = labels[n1];
cell5map[c5i] = ml;
}
return cell5map;
}
export function rebuild_cell5_index() {
const labels = metamap_to_labels();
const new_cell5s = {};
const CELL5S = CELLINDEX.CELL120_CELL5;
for( const c5i in CELL5S ) {
const n1 = CELL5S[c5i][0]; // label 1 node;
const ml = labels[n1];
new_cell5s[c5i] = {
nodes: CELL5S[c5i],
label: ml
}
}
return new_cell5s;
}
const nc5 = rebuild_cell5_index();
console.log(JSON.stringify(nc5, null, 4));

View File

@ -1,173 +0,0 @@
import * as POLYTOPES from './polytopes.js';
// face detection for the 600-cell
export function nodes_links(links, nodeid) {
return links.filter((l) => l.source === nodeid || l.target === nodeid);
}
export function linked(links, n1, n2) {
const ls = nodes_links(nodes_links(links, n1), n2);
if( ls.length ) {
return ls[0]
} else {
return false;
}
}
function fingerprint(ids) {
const sids = [...ids];
sids.sort();
return sids.join(',');
}
export function make_600cell() {
const nodes = POLYTOPES.make_600cell_vertices();
const links = POLYTOPES.auto_detect_edges(nodes, 12);
return {
nodes: nodes,
links: links
}
}
export function link_to_tetras(nodes, links, link) {
const n1 = link.source;
const n2 = link.target;
const nl1 = nodes_links(links, n1).filter((l) => l.id !== link.id);
const nl2 = nodes_links(links, n2).filter((l) => l.id !== link.id);
const p1 = new Set();
const p = new Set();
for( const nl of nl1 ) {
if( nl.source !== n1 ) {
p1.add(nl.source);
}
if( nl.target !== n1 ) {
p1.add(nl.target);
}
}
for( const nl of nl2 ) {
if( nl.source !== n2 && p1.has(nl.source) ) {
p.add(nl.source);
}
if( nl.target !== n2 && p1.has(nl.target) ) {
p.add(nl.target);
}
}
const lp = Array.from(p);
const seen = {};
const tetras = [];
for( const p1 of lp ) {
for( const p2 of lp ) {
if( p1 != p2 ) {
if( linked(links, p1, p2) ) {
const fp = fingerprint([n1, n2, p1, p2]);
if( !seen[fp] ) {
seen[fp] = true;
tetras.push({fingerprint: fp, nodes: [n1, n2, p1, p2]})
}
}
}
}
}
return tetras;
}
export function auto_600cell_cells(nodes, links) {
const seen = {};
const tetras = [];
links.map((link) => {
link_to_tetras(nodes, links, link).map((lt) => {
if( !seen[lt.fingerprint] ) {
seen[lt.fingerprint] = true;
tetras.push(lt.nodes);
}
})
});
return tetras;
}
function node_by_id(nodes, nid) {
const ns = nodes.filter((n) => n.id === nid);
return ns[0];
}
export function tetra_w(nodes, tetra) {
let w = 0;
for( const nid of tetra ) {
const node = node_by_id(nodes, nid);
w += node.w;
}
return w / 4;
}
export function sorted_600cells() {
const cell600 = make_600cell();
const tetras = auto_600cell_cells(cell600.nodes, cell600.links);
const layers = tetras.map((t) => { return { "nodes": t, w: tetra_w(cell600.nodes, t) } });
layers.sort((a, b) => b.w - a.w);
return layers;
}
// const cell600 = make_600cell();
// const layers = sorted_600cells(cell600.nodes, cell600.links);
// for( const cell of layers ) {
// // const fp = fingerprint(cell.nodes);
// console.log(`${cell.w} ${cell.nodes}`);
// }
export function make_layered_600cell() {
const tetras = sorted_600cells()
const LAYERS = [
[ "00", 20 ],
[ "01", 20 ],
[ "02", 30 ],
[ "03", 60 ],
[ "04", 60 ],
[ "05", 60 ],
[ "06", 20 ],
[ "07", 60 ],
[ "08", 20 ],
[ "09", 60 ],
[ "10", 60 ],
[ "11", 60 ],
[ "12", 30 ],
[ "13", 20 ],
[ "14", 20 ]
];
const vertices = {};
const seen = {};
let i = 0;
for( const layer of LAYERS ) {
const label = layer[0];
const n = layer[1];
vertices[label] = [];
console.log(`Layer ${label} starting at ${i}`);
for( const t of tetras.slice(i, i + n) ) {
console.log(t);
for( const n of t.nodes ) {
if( !seen[n] ) {
vertices[label].push(n);
seen[n] = true;
}
}
}
i += n;
}
return JSON.stringify(vertices);
}

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();

170
main.js
View File

@ -1,41 +1,14 @@
import * as THREE from 'three';
const RELEASE_NOTES = `
<p><b>v1.3 - 7/2/2026</b></p>
<p>Went to inordinate lengths to apply the partition of the 600-cell (into five
24-cells) to the 5-cell inscription in the 120-cell, so that they could be coloured
in a way which reveals some of that symmetry.</p>
<p><b>v1.2 - 18/1/2026</b></p>
<p>Added a second visualisation of the 120-cell's 5-cells without the 120-cell links and with more colours added so you can get a sense of the individual 5-cells.</p>
<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;
// scene, lights and camera
@ -50,44 +23,37 @@ scene.add(light);
const amblight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(amblight);
camera.position.set(0, 0, CAMERA_K / 2);
camera.position.set(0, 0, 4);
camera.lookAt(0, 0, 0);
//camera.position.z = 4;
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 node_colours = get_colours(DEFAULTS.colour);
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;
}
);
)
const face_ms = [
new THREE.MeshStandardMaterial( { color: 0x44ff44 } )
new THREE.MeshLambertMaterial( { color: 0x44ff44 } )
];
for( const face_m of face_ms ) {
@ -118,91 +84,39 @@ 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,
// change the colors. Otherwise we just read stuff from gui.params.
function setColours(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]);
}
if( shape ) {
// taperedLink.set_color updates according to the link index
shape.links.map((l) => l.object.set_colour(nc));
}
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)
}
// 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) {
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
}
});
}
if( primary ) {
link_ms[0].opacity = o;
} else {
for( const lm of link_ms.slice(1) ) {
lm.opacity = o;
}
}
}
function setNodeOpacity(o) {
node_ms.map((nm) => nm.opacity = o);
}
let gui;
let gui; //
function changeShape() {
createShape(gui.params.shape);
displayDocs(gui.params.shape);
createShape(gui.params.shape);
}
function setVisibility(option_name) {
@ -216,21 +130,18 @@ function setVisibility(option_name) {
}
gui = new FourDGUI(
{
shapes: STRUCTURES,
changeShape: changeShape,
setColours: setColours,
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
setColours(gui.params.colour);
setColors(gui.params.color);
setBackground(gui.params.background);
const dragK = 0.005;
@ -271,7 +182,6 @@ renderer.domElement.addEventListener("pointerup", (event) => {
})
createShape(gui.params.shape, gui.params.option);
displayDocs(gui.params.shape);
function animate() {
requestAnimationFrame( animate );
@ -285,19 +195,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;
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);
renderer.render( scene, camera );
}
animate();

270
package-lock.json generated
View File

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

View File

@ -1,8 +1,7 @@
{
"dependencies": {
"color": "^4.2.3",
"color-scheme": "^1.0.1",
"lil-gui": "^0.19.0",
"lil-gui": "^0.18.2",
"three": "^0.154.0"
},
"devDependencies": {

View File

@ -1,6 +1,6 @@
import * as PERMUTE from './permute.js';
import * as CELLINDEX from './cellindex.js';
import * as CELL120 from './cellindex.js';
function index_nodes(nodes, scale) {
let i = 1;
@ -22,7 +22,7 @@ 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;
}
export function auto_detect_edges(nodes, neighbours, debug=false) {
function auto_detect_edges(nodes, neighbours, debug=false) {
const seen = {};
const nnodes = nodes.length;
const links = [];
@ -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);
@ -216,7 +193,6 @@ export const cell24 = () => {
links16.map((l) => l.label = p);
links.push(...links16);
}
// colour links `
// links.map((l) => {
// const ls = [ l.source, l.target ].map((nid) => node_by_id(nodes, nid).label);
// for ( const c of [1, 2, 3] ) {
@ -230,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.`,
]
};
}
@ -325,12 +301,12 @@ function auto_120cell_faces(links) {
export function make_120cell_vertices() {
const phi = 0.5 * (1 + Math.sqrt(5));
const r5 = Math.sqrt(5);
const phi2 = phi * phi;
const phiinv = 1 / phi;
const phi2inv = 1 / phi2;
function make_120cell_vertices() {
const phi = 0.5 * (1 + Math.sqrt(5));
const r5 = Math.sqrt(5);
const phi2 = phi * phi;
const phiinv = 1 / phi;
const phi2inv = 1 / phi2;
const nodes = [
PERMUTE.coordinates([0, 0, 2, 2], 0),
@ -343,45 +319,15 @@ 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;
}
export function make_120cell_vertices_alt() {
// unit radius version to make it easier to partition one of the 600-cells
const phi = 0.5 * (1 + Math.sqrt(5));
const r5 = Math.sqrt(5);
const phi2 = phi * phi;
const phiinv = 1 / phi;
const phi2inv = 1 / phi2;
const r8 = Math.sqrt(8);
const p8 = phi / r8;
const r58 = r5 / r8;
const ir8 = 1 / r8;
const ip8 = phiinv / r8;
const p28 = phi2 / r8;
const ip28 = phi2inv / r8;
const nodes = [
PERMUTE.coordinates([1, 0, 0, 0], 1),
PERMUTE.coordinates([0.5, 0.5, 0.5, 0.5], 1),
PERMUTE.coordinates([0, phiinv * 0.5, 0.5, phi * 0.5], 1, true),
PERMUTE.coordinates([p8, p8, p8, ip28], 0, true),
PERMUTE.coordinates([ir8, ir8, ir8, r58], 0, true),
PERMUTE.coordinates([ip8, ip8, ip8, p28], 0, true),
PERMUTE.coordinates([0, ip8, p8, r58], 0, true),
PERMUTE.coordinates([0, ip28, ir8, p28], 0, true),
PERMUTE.coordinates([ip8, ir8, p8, 2 * ir8], 0, true),
].flat();
index_nodes(nodes);
return nodes;
}
export function label_nodes(nodes, ids, label) {
function label_nodes(nodes, ids, label) {
nodes.filter((n) => ids.includes(n.id)).map((n) => n.label = label);
}
@ -418,8 +364,8 @@ export const cell120_layered = (max) => {
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));
for (const cstr in CELL120.LAYERS ) {
label_nodes(nodes, CELL120.LAYERS[cstr], Number(cstr));
}
links.map((l) => {
@ -436,8 +382,8 @@ export const cell120_layered = (max) => {
for( const i of [ 0, 1, 2, 3, 4, 5, 6, 7 ] ) {
layers.push(i);
options.push({
name: CELLINDEX.LAYER_NAMES[i],
options.unshift({
name: "Layer " + String(i),
links: [...layers],
nodes: [...layers]
})
@ -447,10 +393,11 @@ export const cell120_layered = (max) => {
name: '120-cell layered',
nodes: nodes,
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.`,
geometry: {
node_size: 0.02,
link_size: 0.02
},
options: options
}
}
@ -461,8 +408,8 @@ 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));
for( const cstr in CELL120.INDEX ) {
label_nodes(nodes, CELL120.INDEX[cstr], Number(cstr));
}
links.map((l) => l.label = 0);
@ -478,178 +425,91 @@ export const cell120_inscribed = () => {
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 ] },
],
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.` ,
{ name: "five inscribed 600-cells", links: [ 0, 1, 2, 3, 4, 5 ] }
]
}
}
export const cell120_alt = () => {
const nodes = make_120cell_vertices_alt();
const links = auto_detect_edges(nodes, 4);
links.map((l) => l.label = 0);
// Schoute's partition via https://arxiv.org/abs/1010.4353
return {
name: '120-cell-alt',
nodes: nodes,
links: links,
options: [
{ name: "none", links: [ 0 ]},
],
description: `alt 120-cell`,
}
}
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 cell120_inscribed_cell5 = () => {
const nodes = make_120cell_vertices();
const links = auto_detect_edges(nodes, 4);
links.map((l) => l.label = 0);
const CELL5S = CELLINDEX.CELL120_CELL5;
for( const c5 in CELL5S ) {
const nodes5 = nodes.filter((n) => CELL5S[c5].nodes.includes(n.id));
const links5 = auto_detect_edges(nodes5, 5);
links5.map((l) => l.label = CELL5S[c5].label);
links.push(...links5);
nodes5.map((n) => n.label = CELL5S[c5].label);
}
return {
name: '120-cell with 5-cells',
nodes: nodes,
links: links,
options: [
{ name: "all", links: [0, 1, 2, 3, 4, 5] },
{ name: "24", links: [0, 1 ] },
{ name: "48", links: [0, 1, 2 ] },
{ name: "72", links: [0, 1, 2, 3 ] },
{ name: "96", links: [0, 1, 2, 3, 4 ] },
{ name: "hide 1200-cell", links: [ 1, 2, 3, 4, 5 ] },
],
description: `The vertices of the 120-cell can also be partitioned
into 120 5-cells: each 5-cell has one vertex in each of the five
600-cells which can be inscribed in the 120-cell. The colours here
are taken from the partition of the inscribed 600-cells' vertices
into five 24-cells.`
}
}
export const cell120_inscribe_cell5_subset = () => {
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);
const CELL5S = CELLINDEX.CELL120_CELL5.some_cell5s;
const link5s = [];
const nodes_subset = [];
for( const c5 in CELL5S ) {
const nodes5 = nodes.filter((n) => CELL5S[c5].includes(n.id));
const cell5links = auto_detect_edges(nodes5, 5);
cell5links.map((l) => l.label = 8);
link5s.push(...cell5links);
for( const n5 of nodes5 ) {
nodes_subset.push(n5);
}
}
// now add the links of the inscribed 600-cells which have the
// node subset in them
const link_subset = (l) => {
const source = nodes_subset.filter((n) => n.id === l.source);
const target = nodes_subset.filter((n) => n.id === l.target);
return source.length === 1 && target.length === 1;
};
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);
for( const link of links600 ) {
if( link_subset(link) ) {
link5s.push(link);
}
}
}
return {
name: '120-cell 5-cell subset',
nodes: nodes_subset,
links: link5s,
options: [
{ name: "none", links: [ 0, 1, 2, 3, 4, 5, 8 ]},
],
description: `Showing only thirteen of the inscribed 5-cells and the links between them on the inscribed 600-cells`,
}
}
export const cell120_test_metamap = () => {
const o600 = cell600();
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 = 6);
const mm = CELLINDEX.CELL600_METAMAP;
const mm_ids = Object.keys(mm).map((i) => Number(i));
console.log(mm_ids);
for( const p of [ 1 ]) {
const nodes600 = nodes.filter((n) => n.label === p);
const links600 = auto_detect_edges(nodes600, 12);
console.log(links600);
const mm_links = links600.filter((l) => {
//console.log(`${l.id} ${l.source} ${l.target}`);
return mm_ids.includes(l.source) && mm_ids.includes(l.target)
});
console.log(mm_links);
mm_links.map((l) => l.label = p);
links.push(...mm_links);
}
nodes.map((n) => {
if( mm_ids.includes(n.id) ) {
const mapid = mm[n.id];
const n600 = o600.nodes.filter((n) => n.id === mapid);
n.label = n600[0].label;
} else {
n.label = 6;
}
});
return {
name: '120-cell-metamap',
nodes: nodes,
links: links,
options: [
{ name: "none", links: [ 0 ]},
{ name: "one inscribed 600-cell", links: [ 0, 1 ] },
],
description: `foo`,
}
}
function partition_coord(i, coords, invert) {
const j = invert ? -i : i;
@ -689,7 +549,7 @@ function map_coord(i, coords, values) {
}
export function make_600cell_vertices() {
function make_600cell_vertices() {
const coords = {
0: '0',
1: '1',
@ -713,7 +573,7 @@ export function make_600cell_vertices() {
].flat();
for( const n of nodes ) {
n.label = label_vertex(n, coords, CELLINDEX.PARTITION600);
n.label = label_vertex(n, coords, partition600);
}
for( const n of nodes ) {
@ -724,7 +584,7 @@ export function make_600cell_vertices() {
index_nodes(nodes);
scale_nodes(nodes, 0.5);
scale_nodes(nodes, 0.75);
return nodes;
}
@ -738,6 +598,7 @@ function get_node(nodes, id) {
}
function audit_link_labels(nodes, links) {
console.log("Link audit");
for( const l of links ) {
const n1 = get_node(nodes, l.source);
const n2 = get_node(nodes, l.target);
@ -766,108 +627,18 @@ 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.`,
]
}
}
export const cell600_layered = () => {
const nodes = make_600cell_vertices();
const links = auto_detect_edges(nodes, 12);
const plabels = {};
nodes.map((n) => {
plabels[n.id] = n.label;
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];
}
});
// recolour nodes according to 24-cell partition
nodes.map((n) => n.label = 8 + plabels[n.id]);
const node_c = [ 8, 9, 10, 11, 12, 13, 14, 15 ];
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: [...node_c, ...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;
@ -898,7 +669,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;
}
@ -916,146 +686,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.`
]
}
}
@ -1063,24 +705,13 @@ export const icosahedron = () => {
export const build_all = () => {
return [
tetrahedron(),
octahedron(),
cube(),
icosahedron(),
dodecahedron(),
cell5(),
cell16(),
tesseract(),
cell24(),
snub24cell(),
cell600(),
cell600_layered(),
cell120_inscribed(),
cell120_inscribed_cell5(),
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,67 +0,0 @@
import * as THREE from 'three';
const EPSILON = 0.001;
class TaperedLink extends THREE.Group {
constructor(baseMaterial, colour_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.colour_i = colour_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_colour(colours) {
console.log(`taperedLink.set_colour {this.colour_i} {colours[this.colour_i]}`);
this.material.color = new THREE.Color(colours[this.colour_i]);
}
}
export { TaperedLink };

View File

@ -3,14 +3,5 @@
import { defineConfig, loadEnv } from 'vite';
export default defineConfig({
base: '/fourjs/',
build: {
rollupOptions: {
output: {
manualChunks: {
threejs: [ 'three' ]
}
}
}
}
base: '/fourjs/'
})