Compare commits

..

No commits in common. "main" and "feature-faces" have entirely different histories.

25 changed files with 289 additions and 7499 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,137 +0,0 @@
// trying to go from faces to dodecahedra
function shared_vertices(f1, f2) {
return f1.nodes.filter((f) => f2.nodes.includes(f));
}
function adjacent_faces(f1, f2) {
// adjacent faces which share an edge, not just a vertex
const intersect = shared_vertices(f1, f2);
if( intersect.length < 2 ) {
return false;
}
if( intersect.length > 2 ) {
console.log(`warning: faces ${f1.id} and ${f2.id} have too many common vertices`);
}
return true;
}
function find_adjacent_faces(faces, face) {
const neighbours = faces.filter((f) => f.id !== face.id && adjacent_faces(f, face));
return neighbours;
}
function find_dodeca_mutuals(faces, f1, f2) {
// for any two adjacent faces, find their common neighbours where
// all three share exactly one vertex (this, I think, guarantees that
// all are on the same dodecahedron)
const n1 = find_adjacent_faces(faces, f1);
const n2 = find_adjacent_faces(faces, f2);
const common = n1.filter((f1) => n2.filter((f2) => f1.id === f2.id).length > 0 );
// there's one extra here - the third which has two nodes in common with
// both f1 and f2 - filter it out
const mutuals = common.filter((cf) => {
const shared = cf.nodes.filter((n) => f1.nodes.includes(n) && f2.nodes.includes(n));
return shared.length === 1
});
return mutuals;
}
function find_dodeca_next(faces, dodeca, f1, f2) {
// of a pair of mutuals, return the one we haven't already got
const m = find_dodeca_mutuals(faces, f1, f2);
if( dodeca.filter((f) => f.id === m[0].id ).length > 0 ) {
m.shift();
}
return m[0];
}
// from any two mutual faces, return all the faces in their dodecahedron
function make_dodecahedron(faces, f1, f2) {
const dodecahedron = [ f1, f2 ];
// take f1 as the 'center', get the other four around it from f2
const fs = find_dodeca_mutuals(faces, f1, f2);
const f3 = fs[0];
const f6 = fs[1];
dodecahedron.push(f3);
const f4 = find_dodeca_next(faces, dodecahedron, f1, f3);
dodecahedron.push(f4);
const f5 = find_dodeca_next(faces, dodecahedron, f1, f4);
dodecahedron.push(f5);
dodecahedron.push(f6);
// get the next ring
const f7 = find_dodeca_next(faces, dodecahedron, f6, f2);
dodecahedron.push(f7);
const f8 = find_dodeca_next(faces, dodecahedron, f2, f3);
dodecahedron.push(f8);
const f9 = find_dodeca_next(faces, dodecahedron, f3, f4);
dodecahedron.push(f9);
const f10 = find_dodeca_next(faces, dodecahedron, f4, f5);
dodecahedron.push(f10);
const f11 = find_dodeca_next(faces, dodecahedron, f5, f6);
dodecahedron.push(f11);
// get the last
const f12 = find_dodeca_next(faces, dodecahedron, f7, f8);
dodecahedron.push(f12);
return dodecahedron;
}
// for a face, pick an edge, and then find the other two faces which
// share this edge. These can be used as the starting points for the
// first face's two dodecahedra
function find_edge_neighbours(faces, face) {
const n1 = face.nodes[0];
const n2 = face.nodes[1];
return faces.filter((f) => f.id !== face.id && f.nodes.includes(n1) && f.nodes.includes(n2));
}
// each face is in two dodecahedra: this returns them both
function face_to_dodecahedra(faces, f) {
const edge_friends = find_edge_neighbours(faces, f);
const d1 = make_dodecahedron(faces, f, edge_friends[0]);
const d2 = make_dodecahedron(faces, f, edge_friends[1]);
return [ d1, d2 ];
}
// brute-force calculation of all dodecahedra
function dd_fingerprint(dodecahedron) {
const ids = dodecahedron.map((face) => face.id);
ids.sort()
return ids.join(',');
}
export function make_120cell_dodecahedra(faces) {
const dodecas = [];
const seen = {};
for( const face of faces ) {
const dds = face_to_dodecahedra(faces, face);
for( const dd of dds ) {
const fp = dd_fingerprint(dd);
if( ! (fp in seen) ) {
console.log(`added dodeca ${fp}`);
dodecas.push(dd);
seen[fp] = 1;
}
}
}
return dodecas;
}

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,37 +0,0 @@
import ColorScheme from 'color-scheme';
import Color from 'color';
export const get_colours = (basis) => {
const basis_c = Color(basis);
const hslb = basis_c.hsl();
const hue = hslb['color'][0];
const saturation = hslb['color'][1];
const luminance = hslb['color'][2];
const scheme = new ColorScheme;
scheme.from_hue(hue).scheme("tetrade").distance(0.75);
const colours = scheme.colors();
colours.reverse();
const hsl = colours.map((c) => Color("#" + c).hsl());
const resaturated = hsl.map((hslc) => hslc.saturationl(saturation).rgbNumber());
resaturated.unshift(basis);
return resaturated;
}
// basic colours where 0 = blue
// 1 - dark blue
// 2 - white
// 3 - light cyan
// 4 - light orange
// 5 - dark orange
export const get_plain_colours = (basis) => {
return [
basis,
0xffffff,
0x00ff00,
0xff0000,
0x0000ff,
0xff9900,
0x000000,
]
}

View File

@ -1,12 +0,0 @@
# Indexing manually ideas
- a list of all the faces (and maybe dodecahedra) which lets me assign
labels to them and lights up the interface
- where faces / vertices are repeated in the table, assigning a vertex in one
spot changes it everywhere else
separately - take the links generated by this codebase and apply them to
the d3.js forcemap code I wrote for the 24-cell

View File

@ -1,244 +0,0 @@
Steps forward -
1. algorithm which, given a face, finds the two dodecahedra it belongs to
2. using this, generate a list of all 120 dodecahedra:
1.
For a face: there are five edges, and ten other faces sharing an edge.
These edges are in two sets: one for each dodecahedron. The sets are defined
by them sharing vertices which aren't in the first face.
Go around a set of five, by pairs: for each pair, find the other neighbour -
this gives the next five faces.
There's only one face left, which is defined by the shared other vertices of
the last five.
2. have tried manual labelling and it will drive me crazy before I finish
3. Automated approach based on what I've got so far:
- should be possible to colour a single dodecahedron from a single face and
one other vertex (to pick a chirality)
- write a function to do this - the compound-of-four-tetrahedra map can be
more or less hard coded: follow the pattern 1-2-3-4-5, 3-4-5-1-2, etc out
from the inner ring, and map the original face's permutation
From this:
- colour the first dodecahedron, picking a chirality
- the next vertices from each of this dodecahedron's vertices have colours
which come from the first dodeca
- these can be used to colour the next layer of dodecahedra
- and so on
Alternatively:
- do it by the discrete Hopf fibration, one fibre at a time
/// old shit below that didn't work VVVV
Chords: 1.74806 - the 120-cell has 7200 chords of this length
Looking for a way to partition the 600 vertices of the 120 cell into five
disjoint 600-cells, each of which has 120 vertices.
(there are 10 such 600-cells so two ways to do the partition I guess)
a 600-cell has 720 edges! optimistically this means that each chord in the
collection of 7200 belongs to one and only one of the 600-cells.
the way forward:
I need to take the 7200 chords (pairs of nodes) and divide them into sets
which are connected to one another - with any luck, each of these will be
one of the 10 600-cells
Then need to sort these 10 sets of 120 vertices into the two sets of 5
collate chords by node
Each 120-cell vertex has 24 of the chord3s from it - as a 600-cell has 12
edges to each vertex, this suggests that each 120-vertex belongs to two
600-cells with a disjoint set of vertices
Next algorithm - gather each 600-cell
use the chords as the basis for this.
n1 -> 24 chords -> add these 24 neighbours
bad luck - traversing chord3s from the first vertex reaches all 600 vertices-
which isn't suprising as the two 5 disjoint sets overlap. Sigh.
Use the angles between the chords? seems a bit complex
Get the angles from the 600-cell model. Use these to separate out the sets of
24 chords from a point on the 120-cell.
Notes from dinner:
- all of the 60-degree angles are chords joining the vertices of the tetrahedra
- there should be two sets of these
for eg - this works for the chords from 1!
[ 25, 41 ],
[ 25, 97 ],
[ 25, 109 ],
[ 25, 157 ],
[ 25, 161 ],
[ 41, 97 ],
[ 41, 109 ],
[ 41, 173 ],
[ 41, 177 ],
[ 97, 113 ],
[ 97, 161 ],
[ 97, 177 ],
[ 37, 53 ],
[ 37, 93 ],
[ 37, 113 ],
[ 37, 157 ],
[ 37, 161 ],
[ 53, 93 ],
[ 53, 113 ],
[ 53, 173 ],
[ 53, 177 ],
[ 173, 177 ]
[ 93, 109 ],
[ 93, 157 ],
[ 93, 173 ],
[ 109, 157 ],
[ 109, 173 ],
[ 113, 161 ],
[ 113, 177 ],
[ 157, 161 ],
[ 29, 45 ], 5
[ 29, 101 ], 101
[ 29, 105 ], 105
[ 29, 153 ], 153
[ 29, 165 ], 165
[ 45, 101 ],
[ 45, 105 ],
[ 45, 169 ], 169
[ 45, 181 ], 181
[ 101, 117 ], 117
[ 101, 165 ],
[ 101, 181 ],
[ 105, 153 ],
[ 105, 169 ],
[ 33, 49 ], 33 49
[ 33, 89 ], 89
[ 33, 117 ],
[ 33, 153 ],
[ 33, 165 ],
[ 49, 89 ],
[ 49, 117 ],
[ 49, 169 ],
[ 49, 181 ],
[ 169, 181 ],
[ 89, 105 ],
[ 89, 153 ],
[ 89, 169 ],
[ 117, 165 ],
[ 117, 181 ],
[ 153, 165 ],
So each of these is one of the two icosahedral pyramids from node 1.
Doing this manually for the rest of the partition is possible, but could it
be automated based on angles?
Plan for Sunday:
* use the existing label_subgraph to make a function which partitions the
60-angle chords into two groups (like I did manually above)
// this is done and seems to work
* test this labelling manually (ie colour one set of 60-angle vertices)
// done this with the manual labels and it looks good
* make another labeling routine which can fill out the rest of the 600-cell
from the starting dodecahedron, by only following chords which are at 60
to the entering chord
Then the big algorithm does the following:
- start from node 1, find 60-angles, pick one partition at random, label that 600-cell
- find the next unlabelled node
- find 60-angles, partition them, pick a partition with no unlabelled cells and label that 600-cell
- repeat the previous step for the remaining three 600-cells
Alternative, more manual option: just write the second labelling routine and
do the rest by hand
[ 25, 41 ],
[ 25, 97 ],
[ 25, 109 ],
[ 25, 157 ],
[ 25, 161 ],
[ 41, 97 ],
[ 41, 109 ],
[ 41, 173 ],
[ 41, 177 ],
[ 97, 113 ],
[ 97, 161 ],
[ 97, 177 ],
[ 37, 53 ],
[ 37, 93 ],
[ 37, 113 ],
[ 37, 157 ],
[ 37, 161 ],
[ 53, 93 ],
[ 53, 113 ],
[ 53, 173 ],
[ 53, 177 ],
[ 173, 177 ]
[ 93, 109 ],
[ 93, 157 ],
[ 93, 173 ],
[ 109, 157 ],
[ 109, 173 ],
[ 113, 161 ],
[ 113, 177 ],
[ 157, 161 ],
25 41 97 109
157 161 173 177
113 37 53 93
Another idea - look at colouring the vertices of a dodecahedron according to
the four compound tetrahedra and see if this can be repeated automatically to
neighbouring cells

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;
const HYPERPLANE = 2;
class FourDShape extends THREE.Group {
@ -18,10 +15,10 @@ class FourDShape extends THREE.Group {
this.nodes3 = {};
this.links = structure.links;
this.faces = ( "faces" in structure ) ? structure.faces : [];
this.node_scale = 1;
this.link_scale = 1;
this.node_size = structure.geometry.node_size;
this.link_size = structure.geometry.link_size;
this.geom_scale = 1;
this.hyperplane = HYPERPLANE;
this.foreshortening = W_FORESHORTENING;
this.initShapes();
}
@ -29,15 +26,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 +42,34 @@ 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));
updateLink(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);
link.object.scale.copy(new THREE.Vector3(this.geom_scale, this.geom_scale, length));
link.object.position.copy(centre);
link.object.lookAt(n2);
link.object.children[0].rotation.x = Math.PI / 2.0;
}
@ -91,62 +98,47 @@ 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);
}
}
render3(rotations, nodes_show, links_show) {
this.scalev3 = new THREE.Vector3(this.node_scale, this.node_scale, this.node_scale);
render3(rotations) {
this.scalev3 = new THREE.Vector3(this.geom_scale, this.geom_scale, this.geom_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);
}
for( const l of this.links ) {
this.updateLink(l, links_show);
this.updateLink(l);
}
for( const f of this.faces ) {
@ -154,6 +146,7 @@ class FourDShape extends THREE.Group {
}
}
}
export { FourDShape };

161
gui.js
View File

@ -1,113 +1,49 @@
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',
visibility: 5,
inscribed: false,
inscribe_all: false,
colour: 0x3293a9,
background: 0xd4d4d4,
hyperplane: 0.93,
zoom: 1,
xRotate: 'YZ',
yRotate: 'XZ',
dtheta: 0,
damping: false,
captions: true,
dpsi: 0,
}
const DEFAULT_SHAPE = '120-cell';
const DEFAULT_COLOR = 0x3293a9;
const DEFAULT_BG = 0x808080;
class FourDGUI {
constructor(funcs) {
this.shapes = funcs.shapes;
constructor(createShape, setColor, setBackground) {
this.gui = new GUI();
const SHAPE_NAMES = this.shapes.map((s) => s.name);
this.parseLinkParams();
const guiObj = this;
this.params = {
shape: this.link['shape'],
option: this.link['option'],
inscribed: this.link['inscribed'],
inscribe_all: this.link['inscribe_all'],
linksize: this.link['linksize'],
linkopacity: this.link['linkopacity'],
link2opacity: this.link['link2opacity'],
nodesize: this.link['nodesize'],
nodeopacity: this.link['nodeopacity'],
depth: this.link['depth'],
colour: this.link['colour'],
background: this.link['background'],
hyperplane: this.link['hyperplane'],
zoom: this.link['zoom'],
xRotate: this.link['xRotate'],
yRotate: this.link['yRotate'],
shape: this.link['shape'] || DEFAULT_SHAPE,
thickness: this.link['thickness'] || 1,
color: this.link['color'] || DEFAULT_COLOR,
background: this.link['background'] || DEFAULT_BG,
hyperplane: this.link['hyperplane'] || 2,
xRotate: this.link['xRotate'] || 'YW',
yRotate: this.link['yRotate'] || 'XZ',
damping: false,
captions: true,
dtheta: this.link['dtheta'],
dpsi: this.link['dpsi'],
"copy link": function () { guiObj.copyUrl() },
dtheta: this.link['dtheta'] || 0,
dpsi: this.link['dpsi'] || 0,
"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);
options_ctrl = options_ctrl.options(options).onChange((option) => {
funcs.setVisibility(option)
});
options_ctrl.setValue(options[0])
funcs.changeShape(shape)
});
const options = this.getShapeOptions(this.params['shape']);
options_ctrl = this.gui.add(this.params, 'option').options(options).onChange((option) => {
funcs.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, 'shape',
[ '5-cell', '16-cell', 'tesseract', '24-cell', '120-cell', '600-cell' ]
).onChange(createShape)
this.gui.add(this.params, 'hyperplane', 1.5, 4);
this.gui.add(this.params, 'thickness', 0.1, 4);
this.gui.addColor(this.params, 'color').onChange(setColor);
this.gui.addColor(this.params, 'background').onChange(setBackground);
this.gui.add(this.params, 'xRotate', [ 'YW', 'YZ', 'ZW' ]);
this.gui.add(this.params, 'yRotate', [ 'XZ', 'XY', 'XW' ]);
this.gui.add(this.params, 'captions').onChange(this.showDocs);
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);
if( spec && spec[0].options ) {
return spec[0].options.map((o) => o.name);
} else {
return [];
}
}
}
numParam(param, parser) {
numParam(param, parser, dft) {
const value = this.urlParams.get(param);
if( value ) {
const n = parser(value);
@ -115,7 +51,7 @@ class FourDGUI {
return n;
}
}
return DEFAULTS[param];
return dft;
}
stringToHex(cstr) {
@ -131,48 +67,36 @@ class FourDGUI {
parseLinkParams() {
this.linkUrl = new URL(window.location.toLocaleString());
this.link = {};
const guiObj = this;
this.urlParams = this.linkUrl.searchParams;
for( const param of [ "shape", "xRotate", "yRotate", "option" ]) {
for( const param of [ "shape", "xRotate", "yRotate" ]) {
const value = this.urlParams.get(param);
if( value ) {
this.link[param] = value;
} else {
this.link[param] = DEFAULTS[param];
}
}
for( const param of [ "inscribed", "inscribe_all"] ) {
this.link[param] = ( this.urlParams.get(param) === 'y' );
}
this.link['hyperplane'] = this.numParam('hyperplane', parseFloat);
this.link['zoom'] = this.numParam('zoom', parseFloat);
this.link['linksize'] = this.numParam('linksize', parseFloat);
this.link['linkopacity'] = this.numParam('linkopacity', parseFloat);
this.link['link2opacity'] = this.numParam('link2opacity', parseFloat);
this.link['nodesize'] = this.numParam('nodesize', parseFloat);
this.link['nodeopacity'] = this.numParam('nodeopacity', parseFloat);
this.link['colour'] = this.numParam('colour', (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);
const guiObj = this;
this.link['hyperplane'] = this.numParam('hyperplane', parseFloat, 2);
this.link['thickness'] = this.numParam('thickness', parseFloat, 1);
this.link['color'] = this.numParam(
'color', (s) => guiObj.stringToHex(s), DEFAULT_COLOR
);
this.link['background'] = this.numParam(
'background', (s) => guiObj.stringToHex(s), DEFAULT_BG
);
this.link['dpsi'] = this.numParam('dpsi', parseFloat, 0);
this.link['dtheta'] = this.numParam('dtheta', parseFloat, 0);
}
copyUrl() {
const url = new URL(this.linkUrl.origin + this.linkUrl.pathname);
url.searchParams.append("shape", this.params.shape);
url.searchParams.append("option", this.params.option);
url.searchParams.append("inscribed", this.params.inscribed ? 'y': 'n');
url.searchParams.append("inscribe_all", this.params.inscribe_all ? 'y': 'n');
url.searchParams.append("linksize", this.params.linksize.toString());
url.searchParams.append("nodesize", this.params.nodesize.toString());
url.searchParams.append("nodeopacity", this.params.nodesize.toString());
url.searchParams.append("linkopacity", this.params.nodeopacity.toString());
url.searchParams.append("colour", this.hexToString(this.params.colour));
url.searchParams.append("thickness", this.params.thickness.toString());
url.searchParams.append("color", this.hexToString(this.params.color));
url.searchParams.append("background", this.hexToString(this.params.background));
url.searchParams.append("hyperplane", this.params.hyperplane.toString());
url.searchParams.append("zoom", this.params.zoom.toString());
url.searchParams.append("xRotate", this.params.xRotate);
url.searchParams.append("yRotate", this.params.yRotate);
url.searchParams.append("dtheta", this.params.dtheta.toString());
@ -187,6 +111,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 +146,4 @@ class FourDGUI {
}
export { FourDGUI, DEFAULTS };
export { FourDGUI };

View File

@ -5,40 +5,9 @@
<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;
right: 0;
z-index: 2;
border:0.5em;
font-family: sans-serif }
</style>
</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> |
<a target="_blank" href="https://git.tilde.town/bombinans/fourdjs">source</a></div>
</body>
</html>
</html>

View File

@ -1,294 +0,0 @@
// bad stuff
function find_chords(chords, n) {
return chords.filter((c) => c[0].id === n.id || c[1].id === n.id);
}
function find_neighbours(chords, n) {
const c = find_chords(chords, n);
return c.map((c) => c[0].id === n.id ? c[1] : c[0])
}
// for a list of pairs [n1, n2] (these are nodes which share a common angle
// from a center), find all the groups of nodes which don't appear in a pair
// together
function partition_nodes(pairs) {
let groups = [];
const seen = new Set();
for( const pair of pairs ) {
// both nodes are in a group already
if( seen.has(pair[0]) && seen.has(pair[1]) ) {
continue;
}
let already = false;
// check if either node is already in a group
for( const group of groups ) {
if( group.has(pair[0]) ) {
group.add(pair[1]);
seen.add(pair[1]);
already = true;
continue;
} else if( group.has(pair[1]) ) {
group.has(pair[0]);
seen.has(pair[0]);
already = true;
continue;
}
}
// if neither of the pair was in a former group, start a new group
if( !already ) {
groups.push(new Set(pair));
}
// collapse any groups which now have common elements
groups = collapse_groups(groups);
}
return groups;
}
// given a list of groups, if any have common elements, collapse them
function collapse_groups(groups) {
const new_groups = [ ];
for( group of groups ) {
let collapsed = false;
for( new_group of new_groups ) {
const i = intersection(group, new_group);
if( i.size > 0 ) {
for( const e of group ) {
new_group.add(e);
}
collapsed = true;
break;
}
}
if( !collapsed ) {
new_groups.push(new Set(group));
}
}
return new_groups;
}
function intersection(s1, s2) {
const i = new Set();
for( const e of s1 ) {
if( s2.has(e) ) {
i.add(e)
}
}
return i;
}
function union(s1, s2) {
const u = new Set(s1);
for( const e of s2 ) {
u.add(e);
}
return u;
}
function vector_angle(n1, n2, n3) {
const v1 = new THREE.Vector4(n1.x, n1.y, n1.z, n1.w);
const v2 = new THREE.Vector4(n2.x, n2.y, n2.z, n2.w);
const v3 = new THREE.Vector4(n3.x, n3.y, n3.z, n3.w);
v2.sub(v1);
v3.sub(v1);
const dp = v2.dot(v3);
return Math.acos(dp / ( v2.length() * v3.length()));
}
function neighbour_angles_orig(chords, n) {
const ns = find_neighbours(chords, n);
const angles = {};
for( let i = 0; i < ns.length - 1; i++ ) {
for( let j = i + 1; j < ns.length; j++ ) {
const n2 = ns[i];
const n3 = ns[j];
const a = THREE.MathUtils.radToDeg(vector_angle(n, n2, n3));
const af = (a).toFixed(3);
if( ! (af in angles) ) {
angles[af] = [];
}
angles[af].push([n2.id, n3.id]);
}
}
return angles;
}
function neighbour_angles(chords, n, angle) {
const ns = find_neighbours(chords, n);
const pairs = [];
for( let i = 0; i < ns.length - 1; i++ ) {
for( let j = i + 1; j < ns.length; j++ ) {
const n2 = ns[i];
const n3 = ns[j];
const a = THREE.MathUtils.radToDeg(vector_angle(n, n2, n3));
const af = (a).toFixed(3);
if( af === angle ) {
pairs.push([n2.id, n3.id]);
}
}
}
return pairs;
}
function make_120_partition(nodes, n) {
const chords = find_all_chords(nodes);
const chord3 = chords["1.74806"]; // these are edges of the 600-cells;
const pairs60 = neighbour_angles(chord3, n, "60.000");
const icosas = partition_nodes(pairs60);
n.label = 1;
const angles = icosa_nodes(nodes, icosas[0]);
label_120_partition_r(nodes, chord3, 1, n, angles);
}
// recursive function to label a single 600-cell vertex partition of the
// 120-cell by following icosahedral nets
// this doesn't work! completely - labels only 108-112
function label_120_partition_r(nodes, chords, label, origin, neighbours) {
console.log(`label_120_partition_r ${origin.id}`);
console.log(neighbours.map((n) => n.id).join(', '));
// first try to label everything
const unlabelled = [];
for( const n of neighbours ) {
if( n.label === 0 ) {
console.log(`Labelled ${n.id} ${label}`);
n.label = label;
unlabelled.push(n);
} else if( n.label !== label ) {
console.log(`node ${n.id} is already in group ${n.label}`);
//return false;
}
}
for( const n of unlabelled ) {
// the angles represent two icosahedral pyramids - partition them and
// pick the one which is at 60 to the edge we arrived on
//console.log(`looking for more neighbors for ${n}`);
const pairs60 = neighbour_angles(chords, n, "60.000");
const icosas = partition_nodes(pairs60);
const icosa = choose_icosa(nodes, origin, n, icosas);
const icosa_n = icosa_nodes(nodes, icosa);
console.log(`recursing to ${nice_icosa(nodes,icosa)}`);
return label_120_partition_r(nodes, chords, label, n, icosa_n);
}
}
// given a pair of icosa-sets, pick the one which is at the right angle to
// the incoming vector
function choose_icosa(nodes, origin, n1, icosas) {
for( const icosa of icosas ) {
const inodes = icosa_nodes(nodes, icosa);
const a60 = inodes.map((ni) => {
const a = THREE.MathUtils.radToDeg(vector_angle(n1, origin, ni));
return a.toFixed(3);
});
if( a60.filter((a) => a === "60.000").length > 0 ) {
return icosa;
}
}
console.log("No icosa found!");
return undefined;
}
function icosa_nodes(nodes, icosa) {
return Array.from(icosa).map((nid) => node_by_id(nodes, nid)).sort((a, b) => a.id - b.id);
}
function node_by_id(nodes, nid) {
const ns = nodes.filter((n) => n.id === nid);
return ns[0];
}
function enumerate_icosas(nodes) {
const chords = find_all_chords(nodes);
const chord3 = chords["1.74806"]; // these are edges of the 600-cells;
for( const n of nodes ) {
const pairs60 = neighbour_angles(chord3, n, "60.000");
const icosas = partition_nodes(pairs60);
for( const icosa of icosas ) {
const inodes = icosa_nodes(nodes, icosa);
console.log(icosa_to_csv(n.id, inodes).join(','));
}
}
}
function icosa_to_csv(nid, icosa) {
const cols = [ nid ];
const ia = icosa.map((n) => n.id);
for( let i = 1; i < 601; i++ ) {
if( ia.includes(i) ) {
cols.push(i);
} else {
cols.push('')
}
}
return cols;
}
function start_icosas(nodes, chords, origin) {
const pairs60 = neighbour_angles(chords, origin, "60.000");
return partition_nodes(pairs60).map((i) => nice_icosa(nodes, i));
}
function next_icosa(nodes, chords, origin, nid) {
const n = node_by_id(nodes, nid);
const pairs60 = neighbour_angles(chords, n, "60.000");
const icosas = partition_nodes(pairs60);
const icosa = choose_icosa(nodes, origin, n, icosas);
return nice_icosa(nodes, icosa);
}
function nice_icosa(nodes, icosa) {
return icosa_nodes(nodes, icosa).map((n) => n.id).join(', ');
}
function find_by_chord(nodesid, n, d) {
const EPSILON = 0.02;
return Object.keys(nodesid).filter((n1) => {
const d2 = dist2(nodesid[n1], nodesid[n]);
return Math.abs(d2 - d ** 2) < EPSILON;
});
}
function has_chord(n1, n2, d) {
const d2 = dist2(n1, n2);
const EPSILON = 0.01;
return Math.abs(d2 - d ** 2) < EPSILON;
}
function find_all_chords(nodes) {
const chords = {};
for( let i = 0; i < nodes.length - 1; i++ ) {
for( let j = i + 1; j < nodes.length; j++ ) {
const n1 = nodes[i];
const n2 = nodes[j];
const chord = Math.sqrt(dist2(n1, n2)).toFixed(5);
if( !(chord in chords) ) {
chords[chord] = [];
}
chords[chord].push([n1, n2]);
}
}
return chords;
}

View File

@ -1,78 +0,0 @@
// New approach with tetrahedral coloring
function find_edges(links, nid) {
return links.filter((l) => l.source === nid || l.target === nid );
}
function find_adjacent(links, nid) {
return find_edges(links, nid).map((l) => {
if( l.source === nid ) {
return l.target;
} else {
return l.source;
}
});
}
function iterate_graph(nodes, links, n, fn) {
const queue = [];
const seen = {};
const nodes_id = {};
nodes.map((n) => nodes_id[n.id] = n);
queue.push(n.id);
seen[n.id] = true;
fn(n);
while( queue.length > 0 ) {
const v = queue.shift();
find_adjacent(links, v).map((aid) => {
if( !(aid in seen) ) {
seen[aid] = true;
queue.push(aid);
fn(nodes_id[aid]);
}
})
}
}
// stupid tetrahedral labelling
// keeps getting stuck
function naive_label_120cell(nodes, links, n) {
const nodes_id = {};
nodes.map((n) => nodes_id[n.id] = n);
iterate_graph(nodes, links, nodes[0], (n) => {
const cols = new Set();
const nbors = find_adjacent(links, n.id);
for( const nb of nbors ) {
if( nodes_id[nb].label > 0 ) {
cols.add(nodes_id[nb].label);
}
for( const nb2 of find_adjacent(links, nb) ) {
if( nb2 !== n.id && nodes_id[nb].label > 0 ) {
cols.add(nodes_id[nb2].label);
}
}
}
const pcols = [ 1, 2, 3, 4, 5 ].filter((c) => !cols.has(c));
if( pcols.length < 1 ) {
console.log(`Got stuck, no options at ${n.id}`);
return false;
} else {
n.label = pcols[0];
console.log(`found ${pcols.length} colors for node ${n.id}`);
console.log(`applied ${pcols[0]} to node ${n.id}`);
return true;
}
});
}

File diff suppressed because it is too large Load Diff

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

213
main.js
View File

@ -1,41 +1,13 @@
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 { FourDGUI, DEFAULTS } from './gui.js';
import { FourDGUI } 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 +22,24 @@ 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 = 4;
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);
scene.background = new THREE.Color(0x808080);
const material = new THREE.MeshStandardMaterial({ color: 0x3293a9 });
const node_ms = [ material ];
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 link_ms = [ material ];
const face_ms = [
new THREE.MeshStandardMaterial( { color: 0x44ff44 } )
new THREE.MeshLambertMaterial( { color: 0x44ff44 } )
];
for( const face_m of face_ms ) {
@ -96,142 +48,39 @@ for( const face_m of face_ms ) {
}
const STRUCTURES = POLYTOPES.build_all();
const STRUCTURES_BY_NAME = {};
STRUCTURES.map((s) => STRUCTURES_BY_NAME[s.name] = s);
const STRUCTURES = {
'5-cell': POLYTOPES.cell5(),
'16-cell': POLYTOPES.cell16(),
'tesseract': POLYTOPES.tesseract(),
'24-cell': POLYTOPES.cell24(),
'120-cell': POLYTOPES.cell120(),
'600-cell': POLYTOPES.cell600()
};
let shape = false;
let structure = false;
let node_show = [];
let link_show = [];
function createShape(name, option) {
function createShape(name) {
if( shape ) {
scene.remove(shape);
}
structure = STRUCTURES_BY_NAME[name];
shape = new FourDShape(node_ms, link_ms, face_ms, structure);
console.log(STRUCTURES[name]);
shape = new FourDShape(node_ms, link_ms, face_ms, STRUCTURES[name]);
scene.add(shape);
setVisibility(option ? option : structure.options[0].name);
}
function displayDocs(name) {
const docdiv = document.getElementById("description");
const description = STRUCTURES_BY_NAME[name].description;
if( description ) {
docdiv.innerHTML =`<p>${name}</p><p>${description}</p>`;
} else {
docdiv.innerHTML =`<p>${name}</p>`;
}
}
function showDocs(visible) {
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 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
}
});
}
}
function setNodeOpacity(o) {
node_ms.map((nm) => nm.opacity = o);
}
let gui;
function changeShape() {
createShape(gui.params.shape);
displayDocs(gui.params.shape);
}
function setVisibility(option_name) {
const option = structure.options.filter((o) => o.name === option_name);
if( option.length ) {
node_show = option[0].nodes;
link_show = option[0].links;
} else {
console.log(`Error: option '${option_name}' not found`);
}
}
gui = new FourDGUI(
{
shapes: STRUCTURES,
changeShape: changeShape,
setColours: setColours,
setBackground: setBackground,
setNodeOpacity: setNodeOpacity,
setLinkOpacity: setLinkOpacity,
setVisibility: setVisibility,
showDocs: showDocs,
}
const gui = new FourDGUI(
createShape,
(c) => { material.color = new THREE.Color(c) },
(c) => { scene.background = new THREE.Color(c) },
);
// these are here to pick up colour settings from the URL params
setColours(gui.params.colour);
setBackground(gui.params.background);
material.color = new THREE.Color(gui.params.color);
scene.background = new THREE.Color(gui.params.background);
const dragK = 0.005;
const damping = 0.99;
@ -270,8 +119,7 @@ renderer.domElement.addEventListener("pointerup", (event) => {
dragging = false;
})
createShape(gui.params.shape, gui.params.option);
displayDocs(gui.params.shape);
createShape(gui.params.shape);
function animate() {
requestAnimationFrame( animate );
@ -289,14 +137,9 @@ function animate() {
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);
shape.node_scale = gui.params.nodesize;
shape.link_scale = gui.params.linksize * gui.params.nodesize * 0.5;
shape.render3(rotations, node_show, link_show);
shape.hyperplane = gui.params.hyperplane;
shape.geom_scale = gui.params.thickness;
shape.render3(rotations);
renderer.render( scene, camera );
}

276
package-lock.json generated
View File

@ -4,11 +4,8 @@
"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 +13,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 +29,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 +45,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 +61,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 +77,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 +93,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 +109,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 +125,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 +141,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 +157,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 +173,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 +189,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 +205,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 +221,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 +237,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 +253,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 +269,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 +285,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 +301,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 +317,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 +333,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 +349,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 +364,10 @@
"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 +377,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 +415,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 +445,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 +473,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 +488,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 +503,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,12 +1,9 @@
{
"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": {
"vite": "^4.4.6"
},
"type": "module"
}
}

File diff suppressed because it is too large Load Diff

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/'
})