Compare commits

...

18 Commits

Author SHA1 Message Date
Mike Lynch
92c0fa6c27 release notes and version bump 2026-02-07 18:00:59 +11:00
Mike Lynch
301609fd41 Applied the new colour scheme to the 120-cell / 5-cell partition 2026-02-07 17:56:29 +11:00
Mike Lynch
8cc2622ae4 I seem to have done it 2026-02-07 17:16:13 +11:00
Mike Lynch
7bf5adcb3a Antipode-detector to complete the mapping 2026-02-07 11:32:06 +11:00
Mike Lynch
3c874326a6 Equator is done! 2026-02-07 11:04:42 +11:00
Mike Lynch
29a9d60fdf Typo in last version of metamap 2026-02-07 10:44:08 +11:00
Mike Lynch
93d8a31f71 Next layer is done 2026-02-05 18:02:22 +11:00
Mike Lynch
e809e72083 routine to find the shared neighbours of n vertices for the next layer 2026-02-05 12:15:10 +11:00
Mike Lynch
128788c490 Finised the second layer 2026-02-05 08:32:28 +11:00
Mike Lynch
0ef3263b32 Half of the next layer 2026-02-03 08:22:27 +11:00
Mike Lynch
64cb9540b2 The mapping seems to work, for the artic circle at least 2026-02-01 17:45:06 +11:00
Mike Lynch
14d5186a45 Added the document where I'm mapping the inscribed 600-cell to the primary one 2026-02-01 16:41:44 +11:00
Mike Lynch
da085a7ed0 renamed the script that makes the 5-cell inscription 2026-01-31 13:29:41 +11:00
Mike Lynch
29925962d0 More mucking around with colouring nodes 2026-01-31 13:26:53 +11:00
Mike Lynch
36b48b24e6 Merge branch 'bugfix-colour-picker' into feature-120-cell-more-inscriptions 2026-01-21 14:41:38 +11:00
Mike Lynch
36d3eaff93 More explorations of the 120-cell's 5-cells 2026-01-21 14:39:30 +11:00
Mike Lynch
70247110f3 Fixed bug in colour picker 2026-01-20 09:45:31 +11:00
Mike Lynch
637b327db5 Fiddling around trying to fix the colour picker bug 2026-01-20 09:31:43 +11:00
12 changed files with 2379 additions and 106 deletions

87
600_CELL_MAPPING.md Normal file
View File

@ -0,0 +1,87 @@
# 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,6 +1,12 @@
CHANGELOG 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 ## v1.2 - 18/1/2026
Added a second visualisation of the 120-cell's 5-cells without the 120-cell links Added a second visualisation of the 120-cell's 5-cells without the 120-cell links

View File

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

@ -452,7 +452,7 @@ function coherent_5cells(cell120, all5) {
function coherent_all() {
const cell120 = POLYTOPES.cell120_inscribed(); const cell120 = POLYTOPES.cell120_inscribed();
@ -490,3 +490,65 @@ for( let i = 1; i < 121; i++ ) {
} }
console.log(JSON.stringify(idict, null, 2)); 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

@ -9,8 +9,8 @@ export const get_colours = (basis) => {
const luminance = hslb['color'][2]; const luminance = hslb['color'][2];
const scheme = new ColorScheme; const scheme = new ColorScheme;
scheme.from_hue(hue).scheme("tetrade").distance(0.75); scheme.from_hue(hue).scheme("tetrade").distance(0.75);
const scolours = scheme.colors(); const colours = scheme.colors();
const colours = [ ...scolours, ...scolours, ...scolours, ...scolours, ...scolours, ...scolours, ...scolours, ...scolours ]; colours.reverse();
const hsl = colours.map((c) => Color("#" + c).hsl()); const hsl = colours.map((c) => Color("#" + c).hsl());
const resaturated = hsl.map((hslc) => hslc.saturationl(saturation).rgbNumber()); const resaturated = hsl.map((hslc) => hslc.saturationl(saturation).rgbNumber());
resaturated.unshift(basis); resaturated.unshift(basis);

16
gui.js
View File

@ -6,13 +6,13 @@ const DEFAULTS = {
nodeopacity: 1, nodeopacity: 1,
linksize: 1.0, linksize: 1.0,
linkopacity: 0.75, linkopacity: 0.75,
shape: '120-cell', shape: '600-cell',
link2opacity: 0.75, link2opacity: 0.75,
option: 'none', option: 'none',
visibility: 5, visibility: 5,
inscribed: false, inscribed: false,
inscribe_all: false, inscribe_all: false,
color: 0x3293a9, colour: 0x3293a9,
background: 0xd4d4d4, background: 0xd4d4d4,
hyperplane: 0.93, hyperplane: 0.93,
zoom: 1, zoom: 1,
@ -46,7 +46,7 @@ class FourDGUI {
nodesize: this.link['nodesize'], nodesize: this.link['nodesize'],
nodeopacity: this.link['nodeopacity'], nodeopacity: this.link['nodeopacity'],
depth: this.link['depth'], depth: this.link['depth'],
color: this.link['color'], colour: this.link['colour'],
background: this.link['background'], background: this.link['background'],
hyperplane: this.link['hyperplane'], hyperplane: this.link['hyperplane'],
zoom: this.link['zoom'], zoom: this.link['zoom'],
@ -60,8 +60,6 @@ class FourDGUI {
}; };
if( funcs.extras ) { if( funcs.extras ) {
for( const label in funcs.extras ) { for( const label in funcs.extras ) {
console.log(label);
console.log(funcs.extras[label]);
this.params[label] = funcs.extras[label]; this.params[label] = funcs.extras[label];
} }
} }
@ -83,10 +81,9 @@ class FourDGUI {
this.gui.add(this.params, 'nodesize', 0, 1.5); 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, 'nodeopacity', 0, 1).onChange(funcs.setNodeOpacity);
this.gui.add(this.params, 'linksize', 0, 2); this.gui.add(this.params, 'linksize', 0, 2);
console.log(funcs.setLinkOpacity);
this.gui.add(this.params, 'linkopacity', 0, 1).onChange((v) => funcs.setLinkOpacity(v, true)); this.gui.add(this.params, '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.add(this.params, 'link2opacity', 0, 1).onChange((v) => funcs.setLinkOpacity(v, false));
this.gui.addColor(this.params, 'color').onChange(funcs.setColor); this.gui.addColor(this.params, 'colour').onChange(funcs.setColours);
this.gui.addColor(this.params, 'background').onChange(funcs.setBackground); this.gui.addColor(this.params, 'background').onChange(funcs.setBackground);
this.gui.add(this.params, 'xRotate', [ 'YW', 'YZ', 'ZW' ]); this.gui.add(this.params, 'xRotate', [ 'YW', 'YZ', 'ZW' ]);
this.gui.add(this.params, 'yRotate', [ 'XZ', 'XY', 'XW' ]); this.gui.add(this.params, 'yRotate', [ 'XZ', 'XY', 'XW' ]);
@ -155,7 +152,7 @@ class FourDGUI {
this.link['link2opacity'] = this.numParam('link2opacity', parseFloat); this.link['link2opacity'] = this.numParam('link2opacity', parseFloat);
this.link['nodesize'] = this.numParam('nodesize', parseFloat); this.link['nodesize'] = this.numParam('nodesize', parseFloat);
this.link['nodeopacity'] = this.numParam('nodeopacity', parseFloat); this.link['nodeopacity'] = this.numParam('nodeopacity', parseFloat);
this.link['color'] = this.numParam('color', (s) => guiObj.stringToHex(s)); this.link['colour'] = this.numParam('colour', (s) => guiObj.stringToHex(s));
this.link['background'] = this.numParam('background', (s) => guiObj.stringToHex(s)); this.link['background'] = this.numParam('background', (s) => guiObj.stringToHex(s));
this.link['dpsi'] = this.numParam('dpsi', parseFloat); this.link['dpsi'] = this.numParam('dpsi', parseFloat);
this.link['dtheta'] = this.numParam('dtheta', parseFloat); this.link['dtheta'] = this.numParam('dtheta', parseFloat);
@ -172,7 +169,7 @@ class FourDGUI {
url.searchParams.append("nodesize", this.params.nodesize.toString()); url.searchParams.append("nodesize", this.params.nodesize.toString());
url.searchParams.append("nodeopacity", this.params.nodesize.toString()); url.searchParams.append("nodeopacity", this.params.nodesize.toString());
url.searchParams.append("linkopacity", this.params.nodeopacity.toString()); url.searchParams.append("linkopacity", this.params.nodeopacity.toString());
url.searchParams.append("color", this.hexToString(this.params.color)); url.searchParams.append("colour", this.hexToString(this.params.colour));
url.searchParams.append("background", this.hexToString(this.params.background)); url.searchParams.append("background", this.hexToString(this.params.background));
url.searchParams.append("hyperplane", this.params.hyperplane.toString()); url.searchParams.append("hyperplane", this.params.hyperplane.toString());
url.searchParams.append("zoom", this.params.zoom.toString()); url.searchParams.append("zoom", this.params.zoom.toString());
@ -190,7 +187,6 @@ class FourDGUI {
return; return;
} }
navigator.clipboard.writeText(text).then(function() { navigator.clipboard.writeText(text).then(function() {
console.log('Async: Copying to clipboard was successful!');
}, function(err) { }, function(err) {
console.error('Async: Could not copy text: ', err); console.error('Async: Could not copy text: ', err);
}); });

View File

@ -36,7 +36,7 @@
<script type="module" src="/main.js"></script> <script type="module" src="/main.js"></script>
<div id="description"></div> <div id="description"></div>
<div id="release_notes"></div> <div id="release_notes"></div>
<div id="info"><a href="#" id="show_notes">release 1.2</a> | <div id="info"><a href="#" id="show_notes">release 1.3</a> |
by <a target="_blank" href="https://mikelynch.org/">Mike Lynch</a> | by <a target="_blank" href="https://mikelynch.org/">Mike Lynch</a> |
<a target="_blank" href="https://git.tilde.town/bombinans/fourdjs">source</a></div> <a target="_blank" href="https://git.tilde.town/bombinans/fourdjs">source</a></div>

409
label_inscribed_600cell.js Normal file
View File

@ -0,0 +1,409 @@
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));

20
main.js
View File

@ -1,6 +1,12 @@
import * as THREE from 'three'; import * as THREE from 'three';
const RELEASE_NOTES = ` 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><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>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>
@ -60,7 +66,8 @@ document.body.appendChild( renderer.domElement );
// set up colours and materials for gui callbacks // set up colours and materials for gui callbacks
scene.background = new THREE.Color(DEFAULTS.background); scene.background = new THREE.Color(DEFAULTS.background);
const node_colours = get_colours(DEFAULTS.color); const node_colours = get_colours(DEFAULTS.colour);
const node_ms = node_colours.map((c) => new THREE.MeshStandardMaterial({color: c})); const node_ms = node_colours.map((c) => new THREE.MeshStandardMaterial({color: c}));
const link_ms = node_colours.map((c) => new THREE.MeshStandardMaterial({color: c})); const link_ms = node_colours.map((c) => new THREE.MeshStandardMaterial({color: c}));
@ -77,7 +84,6 @@ link_ms.map((m) => {
} }
); );
console.log("link_ms", link_ms);
const face_ms = [ const face_ms = [
@ -155,7 +161,7 @@ relnotes.addEventListener('click', releaseNotes);
// callbacks to do things which are triggered by controls: reset the shape, // callbacks to do things which are triggered by controls: reset the shape,
// change the colors. Otherwise we just read stuff from gui.params. // change the colors. Otherwise we just read stuff from gui.params.
function setColors(c) { function setColours(c) {
const nc = get_colours(c); const nc = get_colours(c);
for( let i = 0; i < node_ms.length; i++ ) { for( let i = 0; i < node_ms.length; i++ ) {
node_ms[i].color = new THREE.Color(nc[i]); node_ms[i].color = new THREE.Color(nc[i]);
@ -163,7 +169,7 @@ function setColors(c) {
} }
if( shape ) { if( shape ) {
// taperedLink.set_color updates according to the link index // taperedLink.set_color updates according to the link index
shape.links.map((l) => l.object.set_color(nc)); shape.links.map((l) => l.object.set_colour(nc));
} }
} }
@ -200,8 +206,6 @@ function changeShape() {
} }
function setVisibility(option_name) { function setVisibility(option_name) {
console.log("setVisibility", option_name);
console.log(structure.options);
const option = structure.options.filter((o) => o.name === option_name); const option = structure.options.filter((o) => o.name === option_name);
if( option.length ) { if( option.length ) {
node_show = option[0].nodes; node_show = option[0].nodes;
@ -216,7 +220,7 @@ gui = new FourDGUI(
{ {
shapes: STRUCTURES, shapes: STRUCTURES,
changeShape: changeShape, changeShape: changeShape,
setColors: setColors, setColours: setColours,
setBackground: setBackground, setBackground: setBackground,
setNodeOpacity: setNodeOpacity, setNodeOpacity: setNodeOpacity,
setLinkOpacity: setLinkOpacity, setLinkOpacity: setLinkOpacity,
@ -226,7 +230,7 @@ gui = new FourDGUI(
); );
// these are here to pick up colour settings from the URL params // these are here to pick up colour settings from the URL params
setColors(gui.params.color); setColours(gui.params.colour);
setBackground(gui.params.background); setBackground(gui.params.background);
const dragK = 0.005; const dragK = 0.005;

View File

@ -216,6 +216,7 @@ export const cell24 = () => {
links16.map((l) => l.label = p); links16.map((l) => l.label = p);
links.push(...links16); links.push(...links16);
} }
// colour links `
// links.map((l) => { // links.map((l) => {
// const ls = [ l.source, l.target ].map((nid) => node_by_id(nodes, nid).label); // const ls = [ l.source, l.target ].map((nid) => node_by_id(nodes, nid).label);
// for ( const c of [1, 2, 3] ) { // for ( const c of [1, 2, 3] ) {
@ -348,9 +349,39 @@ export function make_120cell_vertices() {
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;
}
function label_nodes(nodes, ids, label) {
export function label_nodes(nodes, ids, label) {
nodes.filter((n) => ids.includes(n.id)).map((n) => n.label = label); nodes.filter((n) => ids.includes(n.id)).map((n) => n.label = label);
} }
@ -443,15 +474,6 @@ export const cell120_inscribed = () => {
links.push(...links600); links.push(...links600);
} }
const CELL5S = CELLINDEX.CELL120_CELL5.cell5s;
for( const c5 in CELL5S ) {
const nodes5 = nodes.filter((n) => CELL5S[c5].includes(n.id));
const links5 = auto_detect_edges(nodes5, 5);
links5.map((l) => l.label = 8);
links.push(...links5);
}
return { return {
name: '120-cell', name: '120-cell',
nodes: nodes, nodes: nodes,
@ -460,22 +482,73 @@ export const cell120_inscribed = () => {
{ name: "none", links: [ 0 ]}, { name: "none", links: [ 0 ]},
{ name: "one inscribed 600-cell", links: [ 0, 1 ] }, { name: "one inscribed 600-cell", links: [ 0, 1 ] },
{ name: "five inscribed 600-cells", links: [ 0, 1, 2, 3, 4, 5 ] }, { name: "five inscribed 600-cells", links: [ 0, 1, 2, 3, 4, 5 ] },
{ name: "120 inscribed 5-cells", links: [ 0, 8 ] },
], ],
description: `The 120-cell is the four-dimensional analogue of the description: `The 120-cell is the four-dimensional analogue of the
dodecahedron, and consists of 120 dodecahedra joined at 720 faces, dodecahedron, and consists of 120 dodecahedra joined at 720 faces,
with three dodecahedra around each edge. It is dual to the 600-cell, with three dodecahedra around each edge. It is dual to the 600-cell,
and five 600-cells can be inscribed in its vertices. The converse and five 600-cells can be inscribed in its vertices.` ,
of this allows 120 5-cells (each of which has one vertex in each
of the 5 600-cells) to be inscribed in the 120-cell.`,
} }
} }
export const cell120_alt = () => {
const nodes = make_120cell_vertices_alt();
const links = auto_detect_edges(nodes, 4);
links.map((l) => l.label = 0);
return {
name: '120-cell-alt',
nodes: nodes,
links: links,
options: [
{ name: "none", links: [ 0 ]},
],
description: `alt 120-cell`,
}
}
export const cell120_inscribed_cell5 = () => { export const cell120_inscribed_cell5 = () => {
const nodes = make_120cell_vertices(); const nodes = make_120cell_vertices();
const links = []; 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 ) { for( const cstr in CELLINDEX.INDEX120 ) {
label_nodes(nodes, CELLINDEX.INDEX120[cstr], Number(cstr)); label_nodes(nodes, CELLINDEX.INDEX120[cstr], Number(cstr));
@ -483,30 +556,101 @@ export const cell120_inscribed_cell5 = () => {
links.map((l) => l.label = 0); links.map((l) => l.label = 0);
const CELL5S = CELLINDEX.CELL120_CELL5.cell5s; const CELL5S = CELLINDEX.CELL120_CELL5.some_cell5s;
const link5s = [];
const nodes_subset = [];
for( const c5 in CELL5S ) { for( const c5 in CELL5S ) {
const nodes5 = nodes.filter((n) => CELL5S[c5].includes(n.id)); const nodes5 = nodes.filter((n) => CELL5S[c5].includes(n.id));
const links5 = auto_detect_edges(nodes5, 5); const cell5links = auto_detect_edges(nodes5, 5);
links5.map((l) => l.label = Number(c5)); cell5links.map((l) => l.label = 8);
links.push(...links5); link5s.push(...cell5links);
for( const n5 of nodes5 ) {
nodes_subset.push(n5);
}
} }
const show_links = Array.from({ length: 128 }, (_, i) => i); // 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 { return {
name: '120 5-cells', 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, nodes: nodes,
links: links, links: links,
options: [ options: [
{ name: "none", links: show_links}, { name: "none", links: [ 0 ]},
{ name: "one inscribed 600-cell", links: [ 0, 1 ] },
], ],
description: `The 120 5-cells from the 120-cell, without the latter's links. This colouring is pretty arbitrary, being based on the algorithm which partitioned the nodes: a later version will have something that's based on the symmetries of the 600-cells which each of the 5-cells has its nodes in.`, description: `foo`,
} }
} }
function partition_coord(i, coords, invert) { function partition_coord(i, coords, invert) {
const j = invert ? -i : i; const j = invert ? -i : i;
if( j >= 0 ) { if( j >= 0 ) {
@ -640,7 +784,12 @@ export const cell600_layered = () => {
const nodes = make_600cell_vertices(); const nodes = make_600cell_vertices();
const links = auto_detect_edges(nodes, 12); const links = auto_detect_edges(nodes, 12);
nodes.map((n) => n.label = 9); // make all invisible by default const plabels = {};
nodes.map((n) => {
plabels[n.id] = n.label;
n.label = 9
}); // make all invisible by default
for (const cstr in CELLINDEX.LAYERS600 ) { for (const cstr in CELLINDEX.LAYERS600 ) {
label_nodes(nodes, CELLINDEX.LAYERS600[cstr], Number(cstr)); label_nodes(nodes, CELLINDEX.LAYERS600[cstr], Number(cstr));
@ -655,6 +804,12 @@ export const cell600_layered = () => {
} }
}); });
// 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 options = [];
const layers = []; const layers = [];
@ -663,7 +818,7 @@ export const cell600_layered = () => {
options.push({ options.push({
name: CELLINDEX.LAYER_NAMES[i], name: CELLINDEX.LAYER_NAMES[i],
links: [...layers], links: [...layers],
nodes: [...layers] nodes: [...node_c, ...layers]
}) })
} }

View File

@ -4,11 +4,11 @@ const EPSILON = 0.001;
class TaperedLink extends THREE.Group { class TaperedLink extends THREE.Group {
constructor(baseMaterial, color_i, n1, n2, r1, r2) { constructor(baseMaterial, colour_i, n1, n2, r1, r2) {
super(); super();
const geometry = new THREE.ConeGeometry( 1, 1, 16, true ); const geometry = new THREE.ConeGeometry( 1, 1, 16, true );
const cplane = new THREE.Plane(new THREE.Vector3(0, -1, 0), 0.5); const cplane = new THREE.Plane(new THREE.Vector3(0, -1, 0), 0.5);
this.color_i = color_i; this.colour_i = colour_i;
this.material = baseMaterial.clone(); this.material = baseMaterial.clone();
this.material.clippingPlanes = [ cplane ]; this.material.clippingPlanes = [ cplane ];
this.object = new THREE.Mesh( geometry, this.material ); this.object = new THREE.Mesh( geometry, this.material );
@ -56,8 +56,9 @@ class TaperedLink extends THREE.Group {
} }
set_color(colors) { set_colour(colours) {
this.material.color = new THREE.Color(colors[this.color_i]); console.log(`taperedLink.set_colour {this.colour_i} {colours[this.colour_i]}`);
this.material.color = new THREE.Color(colours[this.colour_i]);
} }
} }