Compare commits

..

16 Commits

Author SHA1 Message Date
Mike Lynch 0ada3fce6f A few more ux refinements, and added the ability to hide captions 2024-04-26 18:31:27 +10:00
Mike Lynch 3f83bde533 Very cursed but entertaining bug as I try to get links scaling well 2024-04-26 16:57:21 +10:00
Mike Lynch 6f4d4cc633 Link foreshortening works, but updating the geometry of every edge
is making large objects like the 120-cell noticeably stuttery
2024-04-26 11:42:37 +10:00
Mike Lynch fb9c78d82f Fixed some scale problems 2024-04-26 08:59:26 +10:00
Mike Lynch e478abe7c6 Scaled the ends of the links so that they have w-perspective 2024-04-26 07:34:27 +10:00
Mike Lynch f79a90e0d9 Added the rest of the regular 3-d polyhedra 2024-04-25 12:38:40 +10:00
Mike Lynch 78ebb381ee Played around with the hyperplane and zoom so that it all looks
better with unit radius normalisation
2024-04-25 11:25:01 +10:00
Mike Lynch f99901f1b0 Normalised dodecahedron to unit radius 2024-04-25 11:07:21 +10:00
Mike Lynch 39fe6e5e40 Normalised 120-cell to unit radius 2024-04-25 11:05:23 +10:00
Mike Lynch 836e0d5ab6 Normalised 600-cell and snub 24-cell to unit radius 2024-04-25 11:03:11 +10:00
Mike Lynch 0be8c47608 Normalised 24-cell to unit radius 2024-04-25 11:00:14 +10:00
Mike Lynch 5e31403420 Normalised tesseract to unit radius 2024-04-25 10:58:41 +10:00
Mike Lynch aba20124db Normalised 5-cell and 16-cell to unit radius 2024-04-25 10:57:03 +10:00
Mike Lynch 1ec7955861 Merge branch 'feature-node-foreshortening' 2024-04-14 16:10:23 +10:00
Mike Lynch 1e5db22c25 scale factor for node foreshortening 2024-04-14 16:08:46 +10:00
Mike Lynch 680f9997f9 Added descriptions for each of the shapes 2024-04-14 16:05:17 +10:00
5 changed files with 306 additions and 100 deletions

View File

@ -2,7 +2,7 @@ import * as THREE from 'three';
const HYPERPLANE = 2.0; const HYPERPLANE = 2.0;
const W_FORESHORTENING = 0.04;
class FourDShape extends THREE.Group { class FourDShape extends THREE.Group {
@ -15,11 +15,10 @@ class FourDShape extends THREE.Group {
this.nodes3 = {}; this.nodes3 = {};
this.links = structure.links; this.links = structure.links;
this.faces = ( "faces" in structure ) ? structure.faces : []; this.faces = ( "faces" in structure ) ? structure.faces : [];
this.node_size = structure.geometry.node_size;
this.link_size = structure.geometry.link_size;
this.node_scale = 1; this.node_scale = 1;
this.link_scale = 1; this.link_scale = 1;
this.hyperplane = HYPERPLANE; this.hyperplane = HYPERPLANE;
this.foreshortening = W_FORESHORTENING;
this.initShapes(); this.initShapes();
} }
@ -44,32 +43,43 @@ class FourDShape extends THREE.Group {
} }
makeLink(material, link) { makeLink(material, link) {
const n1 = this.nodes3[link.source].v3; const n1 = this.nodes3[link.source];
const n2 = this.nodes3[link.target].v3; const n2 = this.nodes3[link.target];
const length = n1.distanceTo(n2); const s1 = n1.scale;
const s2 = n2.scale;
const length = n1.v3.distanceTo(n2.v3);
const centre = new THREE.Vector3(); const centre = new THREE.Vector3();
centre.lerpVectors(n1, n2, 0.5); centre.lerpVectors(n1.v3, n2.v3, 0.5);
const geometry = new THREE.CylinderGeometry(this.link_size, this.link_size, 1); const geometry = new THREE.CylinderGeometry(
this.link_scale * s2, this.link_scale * s1, 1,
16, 1, true
);
const cyl = new THREE.Mesh(geometry, material); const cyl = new THREE.Mesh(geometry, material);
const edge = new THREE.Group(); const edge = new THREE.Group();
edge.add(cyl); edge.add(cyl);
edge.position.copy(centre); edge.position.copy(centre);
edge.scale.copy(new THREE.Vector3(1, 1, length)); edge.scale.copy(new THREE.Vector3(1, 1, length));
edge.lookAt(n2); edge.lookAt(n2.v3);
cyl.rotation.x = Math.PI / 2.0; cyl.rotation.x = Math.PI / 2.0;
this.add(edge); this.add(edge);
return edge; return edge;
} }
updateLink(link, links_show) { updateLink(link, links_show) {
const n1 = this.nodes3[link.source].v3; const n1 = this.nodes3[link.source];
const n2 = this.nodes3[link.target].v3; const n2 = this.nodes3[link.target];
const length = n1.distanceTo(n2); const s1 = n1.scale;
const s2 = n2.scale;
const length = n1.v3.distanceTo(n2.v3);
const centre = new THREE.Vector3(); const centre = new THREE.Vector3();
centre.lerpVectors(n1, n2, 0.5); centre.lerpVectors(n1.v3, n2.v3, 0.5);
link.object.scale.copy(new THREE.Vector3(this.link_scale, this.link_scale, length)); // take the average of the ends as the thickness - as a workaround,
// because I haven't worked out how to reshape tapered links without
// having to reassign a new geometry to every link
const link_mean = this.link_scale * (s1 + s2) * 0.5;
link.object.scale.copy(new THREE.Vector3(link_mean, link_mean, length));
link.object.position.copy(centre); link.object.position.copy(centre);
link.object.lookAt(n2); link.object.lookAt(n2.v3);
link.object.children[0].rotation.x = Math.PI / 2.0; link.object.children[0].rotation.x = Math.PI / 2.0;
link.object.visible = (!links_show || link.label in links_show); link.object.visible = (!links_show || link.label in links_show);
} }
@ -133,6 +143,7 @@ class FourDShape extends THREE.Group {
const material = this.getMaterial(n, this.node_ms); const material = this.getMaterial(n, this.node_ms);
this.nodes3[n.id] = { this.nodes3[n.id] = {
v3: v3, v3: v3,
scale: k,
label: n.label, label: n.label,
object: this.makeNode(material, v3, k) object: this.makeNode(material, v3, k)
}; };
@ -154,9 +165,10 @@ class FourDShape extends THREE.Group {
const v4 = this.fourDrotate(n.x, n.y, n.z, n.w, rotations); const v4 = this.fourDrotate(n.x, n.y, n.z, n.w, rotations);
const k = this.fourDscale(v4.w); const k = this.fourDscale(v4.w);
const v3 = new THREE.Vector3(v4.x * k, v4.y * k, v4.z * k); const v3 = new THREE.Vector3(v4.x * k, v4.y * k, v4.z * k);
const s4 = k * this.node_scale; const s4 = k * this.node_scale * this.foreshortening;
const s3 = new THREE.Vector3(s4, s4, s4); const s3 = new THREE.Vector3(s4, s4, s4);
this.nodes3[n.id].v3 = v3; this.nodes3[n.id].v3 = v3;
this.nodes3[n.id].scale = k * this.foreshortening;
this.nodes3[n.id].object.position.copy(v3); this.nodes3[n.id].object.position.copy(v3);
this.nodes3[n.id].object.scale.copy(s3); this.nodes3[n.id].object.scale.copy(s3);
this.nodes3[n.id].object.visible = ( !nodes_show || n.label in nodes_show ); this.nodes3[n.id].object.visible = ( !nodes_show || n.label in nodes_show );

39
gui.js
View File

@ -2,10 +2,11 @@ import { GUI } from 'lil-gui';
const DEFAULTS = { const DEFAULTS = {
thickness: 1.0, nodesize: 0.25,
nodesize: 2.0, nodeopacity: 1,
linkopacity: 0.5, linksize: 0.2,
link2opacity: 0.5, linkopacity: 0.75,
link2opacity: 0.75,
shape: '120-cell', shape: '120-cell',
option: 'none', option: 'none',
visibility: 5, visibility: 5,
@ -13,11 +14,13 @@ const DEFAULTS = {
inscribe_all: false, inscribe_all: false,
color: 0x3293a9, color: 0x3293a9,
background: 0xd4d4d4, background: 0xd4d4d4,
hyperplane: 1.5, hyperplane: 0.93,
zoom: 1, zoom: 1,
xRotate: 'YW', xRotate: 'YW',
yRotate: 'XZ', yRotate: 'XW',
dtheta: 0, dtheta: 0,
damping: false,
captions: true,
dpsi: 0, dpsi: 0,
} }
@ -25,7 +28,7 @@ const DEFAULTS = {
class FourDGUI { class FourDGUI {
constructor(shapes, changeShape, setColor, setBackground, setLinkOpacity, setVisibility) { constructor(shapes, changeShape, setColor, setBackground, setNodeOpacity,setLinkOpacity, setVisibility, showDocs) {
this.gui = new GUI(); this.gui = new GUI();
const SHAPE_NAMES = shapes.map((s) => s.name); const SHAPE_NAMES = shapes.map((s) => s.name);
@ -36,10 +39,11 @@ class FourDGUI {
option: this.link['option'], option: this.link['option'],
inscribed: this.link['inscribed'], inscribed: this.link['inscribed'],
inscribe_all: this.link['inscribe_all'], inscribe_all: this.link['inscribe_all'],
thickness: this.link['thickness'], linksize: this.link['linksize'],
linkopacity: this.link['linkopacity'], linkopacity: this.link['linkopacity'],
link2opacity: this.link['linkopacity'], link2opacity: this.link['linkopacity'],
nodesize: this.link['nodesize'], nodesize: this.link['nodesize'],
nodeopacity: this.link['nodeopacity'],
depth: this.link['depth'], depth: this.link['depth'],
color: this.link['color'], color: this.link['color'],
background: this.link['background'], background: this.link['background'],
@ -48,6 +52,7 @@ class FourDGUI {
xRotate: this.link['xRotate'], xRotate: this.link['xRotate'],
yRotate: this.link['yRotate'], yRotate: this.link['yRotate'],
damping: false, damping: false,
captions: true,
dtheta: this.link['dtheta'], dtheta: this.link['dtheta'],
dpsi: this.link['dpsi'], dpsi: this.link['dpsi'],
"copy link": function () { guiObj.copyUrl() } "copy link": function () { guiObj.copyUrl() }
@ -65,20 +70,22 @@ class FourDGUI {
options_ctrl = this.gui.add(this.params, 'option').options(options).onChange((option) => { options_ctrl = this.gui.add(this.params, 'option').options(options).onChange((option) => {
setVisibility(option) setVisibility(option)
}); });
this.gui.add(this.params, 'hyperplane', 1.4, 2.0); 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, 'zoom', 0.1, 2.0);
this.gui.add(this.params, 'thickness', 0, 2); this.gui.add(this.params, 'nodesize', 0, 1);
this.gui.add(this.params, 'nodeopacity', 0, 1).onChange(setNodeOpacity);
this.gui.add(this.params, 'linksize', 0, 1);
this.gui.add(this.params, 'linkopacity', 0, 1).onChange( this.gui.add(this.params, 'linkopacity', 0, 1).onChange(
(v) => setLinkOpacity(v, true) (v) => setLinkOpacity(v, true)
); );
this.gui.add(this.params, 'link2opacity', 0, 1).onChange( this.gui.add(this.params, 'link2opacity', 0, 1).onChange(
(v) => setLinkOpacity(v, false) (v) => setLinkOpacity(v, false)
); );
this.gui.add(this.params, 'nodesize', 0.1, 4);
this.gui.addColor(this.params, 'color').onChange(setColor); this.gui.addColor(this.params, 'color').onChange(setColor);
this.gui.addColor(this.params, 'background').onChange(setBackground); this.gui.addColor(this.params, 'background').onChange(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' ]);
this.gui.add(this.params, 'captions').onChange(showDocs);
this.gui.add(this.params, 'damping'); this.gui.add(this.params, 'damping');
this.gui.add(this.params, 'copy link'); this.gui.add(this.params, 'copy link');
@ -134,10 +141,11 @@ class FourDGUI {
} }
this.link['hyperplane'] = this.numParam('hyperplane', parseFloat); this.link['hyperplane'] = this.numParam('hyperplane', parseFloat);
this.link['zoom'] = this.numParam('zoom', parseFloat); this.link['zoom'] = this.numParam('zoom', parseFloat);
this.link['thickness'] = this.numParam('thickness', parseFloat); this.link['linksize'] = this.numParam('linksize', parseFloat);
this.link['linkopacity'] = this.numParam('linkopacity', parseFloat); this.link['linkopacity'] = this.numParam('linkopacity', parseFloat);
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['color'] = this.numParam('color', (s) => guiObj.stringToHex(s)); this.link['color'] = this.numParam('color', (s) => guiObj.stringToHex(s));
this.link['background'] = this.numParam('background', (s) => guiObj.stringToHex(s)); this.link['background'] = this.numParam('background', (s) => guiObj.stringToHex(s));
this.link['dpsi'] = this.numParam('dpsi', parseFloat); this.link['dpsi'] = this.numParam('dpsi', parseFloat);
@ -151,10 +159,11 @@ class FourDGUI {
url.searchParams.append("option", this.params.option); url.searchParams.append("option", this.params.option);
url.searchParams.append("inscribed", this.params.inscribed ? 'y': 'n'); url.searchParams.append("inscribed", this.params.inscribed ? 'y': 'n');
url.searchParams.append("inscribe_all", this.params.inscribe_all ? 'y': 'n'); url.searchParams.append("inscribe_all", this.params.inscribe_all ? 'y': 'n');
url.searchParams.append("thickness", this.params.thickness.toString()); url.searchParams.append("linksize", this.params.linksize.toString());
url.searchParams.append("nodesize", this.params.nodesize.toString()); url.searchParams.append("nodesize", this.params.nodesize.toString());
url.searchParams.append("linkopacity", this.params.thickness.toString()); url.searchParams.append("nodeopacity", this.params.nodesize.toString());
url.searchParams.append("link2opacity", this.params.nodesize.toString()); url.searchParams.append("linkopacity", this.params.nodeopacity.toString());
url.searchParams.append("link2opacity", this.params.link2opacity.toString());
url.searchParams.append("color", this.hexToString(this.params.color)); url.searchParams.append("color", this.hexToString(this.params.color));
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());

View File

@ -5,6 +5,15 @@
<title>FourD</title> <title>FourD</title>
<style> <style>
body { margin: 0; } body { margin: 0; }
div#description {
position: fixed;
top: 0;
left: 0;
width: 20%;
z-index: 2;
font-family: sans-serif;
padding: 1em;
}
div#info { div#info {
position: fixed; position: fixed;
bottom:0; bottom:0;
@ -16,6 +25,7 @@
</head> </head>
<body> <body>
<script type="module" src="/main.js"></script> <script type="module" src="/main.js"></script>
<div id="description"></div>
<div id="info">by <a target="_blank" href="https://mikelynch.org/">Mike Lynch</a> - <div id="info">by <a target="_blank" href="https://mikelynch.org/">Mike Lynch</a> -
<a target="_blank" href="https://git.tilde.town/bombinans/fourdjs">source</a></div> <a target="_blank" href="https://git.tilde.town/bombinans/fourdjs">source</a></div>
</body> </body>

49
main.js
View File

@ -9,7 +9,7 @@ import { FourDShape } from './fourDShape.js';
import { get_colours } from './colours.js'; import { get_colours } from './colours.js';
const FACE_OPACITY = 0.3; const FACE_OPACITY = 0.3;
const CAMERA_K = 10; const CAMERA_K = 5;
// scene, lights and camera // scene, lights and camera
@ -47,11 +47,19 @@ material.opacity = 0.5;
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}));
node_ms.map((m) => {
m.transparent = true;
m.opacity = 1.0;
}
);
link_ms.map((m) => { link_ms.map((m) => {
m.transparent = true; m.transparent = true;
m.opacity = 0.5; m.opacity = 0.5;
} }
) );
const face_ms = [ const face_ms = [
new THREE.MeshLambertMaterial( { color: 0x44ff44 } ) new THREE.MeshLambertMaterial( { color: 0x44ff44 } )
@ -85,6 +93,26 @@ function createShape(name, option) {
setVisibility(option ? option : structure.options[0].name); setVisibility(option ? option : structure.options[0].name);
} }
function displayDocs(name) {
const docdiv = document.getElementById("description");
const description = STRUCTURES_BY_NAME[name].description;
if( description ) {
docdiv.innerHTML =`<p>${name}</p><p>${description}</p>`;
} else {
docdiv.innerHTML =`<p>${name}</p>`;
}
}
function showDocs(visible) {
console.log(`showDocs ${visible}`);
const docdiv = document.getElementById("description");
if( visible ) {
docdiv.style.display = '';
} else {
docdiv.style.display = 'none';
}
}
// initialise gui and read params from URL // initialise gui and read params from URL
// callbacks to do things which are triggered by controls: reset the shape, // callbacks to do things which are triggered by controls: reset the shape,
@ -115,11 +143,17 @@ function setLinkOpacity(o, primary) {
} }
} }
function setNodeOpacity(o) {
node_ms.map((nm) => nm.opacity = o);
}
let gui; let gui;
function changeShape() { function changeShape() {
createShape(gui.params.shape); createShape(gui.params.shape);
displayDocs(gui.params.shape);
} }
function setVisibility(option_name) { function setVisibility(option_name) {
@ -138,8 +172,10 @@ gui = new FourDGUI(
changeShape, changeShape,
setColors, setColors,
setBackground, setBackground,
setNodeOpacity,
setLinkOpacity, setLinkOpacity,
setVisibility setVisibility,
showDocs
); );
// these are here to pick up colour settings from the URL params // these are here to pick up colour settings from the URL params
@ -184,6 +220,7 @@ renderer.domElement.addEventListener("pointerup", (event) => {
}) })
createShape(gui.params.shape, gui.params.option); createShape(gui.params.shape, gui.params.option);
displayDocs(gui.params.shape);
function animate() { function animate() {
requestAnimationFrame( animate ); requestAnimationFrame( animate );
@ -201,11 +238,11 @@ function animate() {
rotfn[gui.params.xRotate](theta), rotfn[gui.params.xRotate](theta),
rotfn[gui.params.yRotate](psi) rotfn[gui.params.yRotate](psi)
]; ];
shape.hyperplane = gui.params.hyperplane; shape.hyperplane = 1 / gui.params.hyperplane;
camera.position.set(0, 0, gui.params.zoom * CAMERA_K / gui.params.hyperplane); camera.position.set(0, 0, gui.params.zoom * CAMERA_K * gui.params.hyperplane);
shape.link_scale = gui.params.thickness;
shape.node_scale = gui.params.nodesize; shape.node_scale = gui.params.nodesize;
shape.link_scale = gui.params.linksize * gui.params.nodesize * 0.5;
shape.render3(rotations, node_show, link_show); shape.render3(rotations, node_show, link_show);

View File

@ -58,16 +58,15 @@ export function auto_detect_edges(nodes, neighbours, debug=false) {
// too small and simple to calculate // too small and simple to calculate
export const cell5 = () => { export const cell5 = () => {
const r5 = Math.sqrt(5); const c1 = Math.sqrt(5) / 4;
const r2 = Math.sqrt(2) / 2;
return { return {
name: '5-cell', name: '5-cell',
nodes: [ nodes: [
{id:1, label: 1, x: r2, y: r2, z: r2, w: -r2 / r5 }, {id:1, label: 1, x: c1, y: c1, z: c1, w: -0.25 },
{id:2, label: 2, x: r2, y: -r2, z: -r2, w: -r2 / r5 }, {id:2, label: 2, x: c1, y: -c1, z: -c1, w: -0.25 },
{id:3, label: 3, x: -r2, y: r2, z: -r2, w: -r2 / r5 }, {id:3, label: 3, x: -c1, y: c1, z: -c1, w: -0.25 },
{id:4, label: 4, x: -r2, y: -r2, z: r2, w: -r2 / r5 }, {id:4, label: 4, x: -c1, y: -c1, z: c1, w: -0.25 },
{id:5, label: 5, x: 0, y: 0, z: 0, w: 4 * r2 / r5 }, {id:5, label: 5, x: 0, y: 0, z: 0, w: 1 },
], ],
links: [ links: [
{ id:1, source:1, target: 2}, { id:1, source:1, target: 2},
@ -81,11 +80,12 @@ export const cell5 = () => {
{ id:9, source:3, target: 5}, { id:9, source:3, target: 5},
{ id:10, source:4, target: 5}, { id:10, source:4, target: 5},
], ],
geometry: { options: [ { name: '--' }],
node_size: 0.02, description: `Five tetrahedra joined at ten faces with three
link_size: 0.02 tetrahedra around each edge. The 5-cell is the simplest regular
}, four-D polytope and the four-dimensional analogue of the tetrahedron.
options: [ { name: '--' }] A corresponding polytope, or simplex, exists for every n-dimensional
space.`,
}; };
}; };
@ -104,18 +104,18 @@ export const cell16 = () => {
nodes[1].label = 4; nodes[1].label = 4;
index_nodes(nodes); index_nodes(nodes);
scale_nodes(nodes, 0.75); scale_nodes(nodes, 0.5);
const links = auto_detect_edges(nodes, 6); const links = auto_detect_edges(nodes, 6);
return { return {
name: '16-cell', name: '16-cell',
nodes: nodes, nodes: nodes,
links: links, links: links,
geometry: { options: [ { name: '--' }],
node_size: 0.02, description: `Sixteen tetrahedra joined at 32 faces with four
link_size: 0.02 tetrahedra around each edge. The 16-cell is the four-dimensional
}, analogue of the octahedron and is dual to the tesseract. Every
options: [ { name: '--' }] n-dimensional space has a corresponding polytope in this family.`,
}; };
}; };
@ -133,7 +133,7 @@ export const tesseract = () => {
} }
} }
scale_nodes(nodes, Math.sqrt(2) / 2); scale_nodes(nodes, 0.5);
const links = auto_detect_edges(nodes, 4); const links = auto_detect_edges(nodes, 4);
links.map((l) => { l.label = 0 }); links.map((l) => { l.label = 0 });
@ -146,18 +146,20 @@ export const tesseract = () => {
return { return {
name: 'tesseract', name: 'Tesseract',
nodes: nodes, nodes: nodes,
links: links, links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
options: [ options: [
{ name: 'none', links: [ 0 ] }, { name: 'none', links: [ 0 ] },
{ name: 'one 16-cell', links: [ 0, 1 ] }, { name: 'one 16-cell', links: [ 0, 1 ] },
{ name: 'both 16-cells', links: [ 0, 1, 2 ] }, { name: 'both 16-cells', links: [ 0, 1, 2 ] },
], ],
description: `The most well-known four-dimensional shape, the
tesseract is analogous to the cube, and is constructed by placing two
cubes in parallel hyperplanes and joining their corresponding
vertices. It consists of eight cubes joined at 32 face with three
cubes around each edge, and is dual to the 16-cell. Every
n-dimensional space has a cube analogue or measure polytope.`,
}; };
} }
@ -183,6 +185,7 @@ export const cell24 = () => {
n.label = CELL24_INDEXING[axes[0]][axes[1]]; n.label = CELL24_INDEXING[axes[0]][axes[1]];
} }
scale_nodes(nodes, Math.sqrt(2) / 2);
index_nodes(nodes); index_nodes(nodes);
const links = auto_detect_edges(nodes, 8); const links = auto_detect_edges(nodes, 8);
links.map((l) => l.label = 0); links.map((l) => l.label = 0);
@ -206,16 +209,16 @@ export const cell24 = () => {
name: '24-cell', name: '24-cell',
nodes: nodes, nodes: nodes,
links: links, links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
base: {}, base: {},
options: [ options: [
{ name: 'none', links: [ 0 ] }, { name: 'none', links: [ 0 ] },
{ name: 'one 16-cell', links: [ 0, 1 ] }, { name: 'one 16-cell', links: [ 0, 1 ] },
{ name: 'three 16-cells', links: [ 0, 1, 2, 3 ] } { name: 'three 16-cells', links: [ 0, 1, 2, 3 ] }
] ],
description: `A unique object without an exact analogue in higher
or lower dimensions, the 24-cell is made of twenty-four octahedra
joined at 96 faces, with three around each edge. The 24-cell is
self-dual.`,
}; };
} }
@ -319,7 +322,7 @@ export function make_120cell_vertices() {
PERMUTE.coordinates([2, 1, phi, phiinv], 0, true), PERMUTE.coordinates([2, 1, phi, phiinv], 0, true),
].flat(); ].flat();
index_nodes(nodes); index_nodes(nodes);
scale_nodes(nodes, 0.5); scale_nodes(nodes, 0.25 * Math.sqrt(2));
return nodes; return nodes;
} }
@ -393,12 +396,10 @@ export const cell120_layered = (max) => {
name: '120-cell layered', name: '120-cell layered',
nodes: nodes, nodes: nodes,
links: links, links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
nolink2opacity: true, nolink2opacity: true,
options: options options: options,
description: `This version of the 120-cell lets you explore its
structure by building each layer from the 'north pole' onwards.`,
} }
} }
@ -426,15 +427,15 @@ export const cell120_inscribed = () => {
name: '120-cell', name: '120-cell',
nodes: nodes, nodes: nodes,
links: links, links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
options: [ options: [
{ 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 ] }
] ],
description: `The 120-cell is the four-dimensional analogue of the
dodecahedron, and consists of 120 dodecahedra joined at 720 faces,
with three dodecahedra around each edge. It is dual to the 600-cell,
and five 600-cells can be inscribed in its vertices.`,
} }
} }
@ -515,7 +516,7 @@ export function make_600cell_vertices() {
index_nodes(nodes); index_nodes(nodes);
scale_nodes(nodes, 0.75); scale_nodes(nodes, 0.5);
return nodes; return nodes;
} }
@ -557,15 +558,16 @@ export const cell600 = () => {
name: '600-cell', name: '600-cell',
nodes: nodes, nodes: nodes,
links: links, links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
options: [ options: [
{ name: "none", links: [ 0 ]}, { name: "none", links: [ 0 ]},
{ name: "one 24-cell", links: [ 0, 1 ] }, { name: "one 24-cell", links: [ 0, 1 ] },
{ name: "five 24-cells", links: [ 0, 1, 2, 3, 4, 5 ] } { name: "five 24-cells", links: [ 0, 1, 2, 3, 4, 5 ] }
] ],
description: `The 600-cell is the four-dimensional analogue of the
icosahedron, and consists of 600 tetrahedra joined at 1200 faces
with five tetrahedra around each edge. It is dual to the 120-cell.
Its 120 vertices can be partitioned into five sets which form the
vertices of five inscribed 24-cells.`,
} }
} }
@ -605,12 +607,10 @@ export const cell600_layered = () => {
name: '600-cell layered', name: '600-cell layered',
nodes: nodes, nodes: nodes,
links: links, links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
nolink2opacity: true, nolink2opacity: true,
options: options options: options,
description: `This version of the 600-cell lets you explore its
structure by building each layer from the 'north pole' onwards.`,
} }
@ -631,14 +631,15 @@ export const snub24cell = () => {
links.map((l) => l.label = 0); links.map((l) => l.label = 0);
return { return {
name: 'snub 24-cell', name: 'Snub 24-cell',
nodes: nodes, nodes: nodes,
links: links, links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
options: [ { name: "--" } ], options: [ { name: "--" } ],
description: `The snub 24-cell is a semiregular polytope which
connects the 24-cell with the 600-cell. It consists of 24 icosahedra
and 120 tetrahedra, and is constructed by removing one of the
five inscribed 24-cells from a 600-cell.`
} }
@ -678,6 +679,7 @@ function make_dodecahedron_vertices() {
{ x: -phi, y: phiinv, z:0, w: 0 , label: 4}, { x: -phi, y: phiinv, z:0, w: 0 , label: 4},
{ x: -phi, y: -phiinv, z:0, w: 0 , label: 2}, { x: -phi, y: -phiinv, z:0, w: 0 , label: 2},
]; ];
scale_nodes(nodes, 1 / Math.sqrt(3));
index_nodes(nodes); index_nodes(nodes);
return nodes; return nodes;
} }
@ -695,18 +697,146 @@ export const dodecahedron = () => {
} }
return { return {
name: 'dodecahedron', name: 'Dodecahedron',
nodes: nodes, nodes: nodes,
links: links, links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
options: [ options: [
{ name: "none", links: [ 0 ]}, { name: "none", links: [ 0 ]},
{ name: "one tetrahedron", links: [ 0, 1 ] }, { name: "one tetrahedron", links: [ 0, 1 ] },
{ name: "five tetrahedra", links: [ 0, 1, 2, 3, 4, 5 ] } { name: "five tetrahedra", links: [ 0, 1, 2, 3, 4, 5 ] }
] ],
description: `The dodecahedron is a three-dimensional polyhedron
which is included here so that you can see the partition of its
vertices into five interlocked tetrahedra. This structure is the
basis for the partition of the 120-cell's vertices into five
600-cells.`
}
}
export const tetrahedron = () => {
const r2 = Math.sqrt(2);
const r3 = Math.sqrt(3);
return {
name: 'Tetrahedron',
nodes: [
{id:1, label: 1, x: 2 * r2 / 3, y: 0, z: -1/3, w: 0 },
{id:2, label: 2, x: -r2 / 3, y: r2 / r3, z: -1/3, w: 0 },
{id:3, label: 3, x: -r2 / 3, y: -r2 / r3, z: -1/3, w: 0 },
{id:4, label: 4, x: 0, y: 0, z: 1, w: 0 },
],
links: [
{ id:1, source:1, target: 2},
{ id:2, source:1, target: 3},
{ id:3, source:1, target: 4},
{ id:4, source:2, target: 3},
{ id:5, source:2, target: 4},
{ id:6, source:3, target: 4},
],
options: [ { name: '--' }],
description: `The simplest three-dimensional polytope, consisting of four triangles joined at six edges. The 5-cell is its four-dimensional analogue.`,
};
};
export const octahedron = () => {
const nodes = [
{id: 1, label: 1, x: 1, y: 0, z: 0, w: 0},
{id: 2, label: 1, x: -1, y: 0, z: 0, w: 0},
{id: 3, label: 2, x: 0, y: 1, z: 0, w: 0},
{id: 4, label: 2, x: 0, y: -1, z: 0, w: 0},
{id: 5, label: 3, x: 0, y: 0, z: 1, w: 0},
{id: 6, label: 3, x: 0, y: 0, z: -1, w: 0},
];
const links = [
{id:1, source: 1, target: 3},
{id:2, source: 1, target: 4},
{id:3, source: 1, target: 5},
{id:4, source: 1, target: 6},
{id:5, source: 2, target: 3},
{id:6, source: 2, target: 4},
{id:7, source: 2, target: 5},
{id:8, source: 2, target: 6},
{id:9, source: 3, target: 5},
{id:10, source: 3, target: 6},
{id:11, source: 4, target: 5},
{id:12, source: 4, target: 6},
]
links.map((l) => { l.label = 0 });
return {
name: 'Octahedron',
nodes: nodes,
links: links,
options: [ { name: '--' }],
description: `The three-dimensional cross-polytope, the 16-cell is its four-dimensional analogue.`,
};
}
export const cube = () => {
const nodes = [
{id: 1, label: 1, x: 1, y: 1, z: 1, w: 0},
{id: 2, label: 2, x: -1, y: 1, z: 1, w: 0},
{id: 3, label: 2, x: 1, y: -1, z: 1, w: 0},
{id: 4, label: 1, x: -1, y: -1, z: 1, w: 0},
{id: 5, label: 2, x: 1, y: 1, z: -1, w: 0},
{id: 6, label: 1, x: -1, y: 1, z: -1, w: 0},
{id: 7, label: 1, x: 1, y: -1, z: -1, w: 0},
{id: 8, label: 2, x: -1, y: -1, z: -1, w: 0},
];
scale_nodes(nodes, 1/Math.sqrt(3));
const links = auto_detect_edges(nodes, 3);
links.map((l) => { l.label = 0 });
return {
name: 'Cube',
nodes: nodes,
links: links,
options: [ { name: '--' }],
description: `The three-dimensional measure polytope, the tesseract is its four-dimensional analogue.`,
};
}
function make_icosahedron_vertices() {
const phi = 0.5 * (1 + Math.sqrt(5));
const nodes = [
{ x: 0, y: 1, z: phi, w: 0, label: 1 },
{ x: 0, y: -1, z: phi, w: 0, label: 1 },
{ x: 0, y: 1, z: -phi, w: 0, label: 1 },
{ x: 0, y: -1, z: -phi, w: 0, label: 1 },
{ x: 1, y: phi, z: 0, w: 0, label: 2 },
{ x: -1, y: phi, z: 0, w: 0, label: 2 },
{ x: 1, y: -phi, z: 0, w: 0, label: 2 },
{ x: -1, y: -phi, z: 0, w: 0, label: 2 },
{ x: phi, y: 0, z: 1, w: 0, label: 3},
{ x: phi, y: 0, z: -1, w: 0, label: 3},
{ x: -phi, y: 0, z: 1, w: 0, label: 3},
{ x: -phi, y: 0, z: -1, w: 0, label: 3},
];
scale_nodes(nodes, 1/Math.sqrt((5 + Math.sqrt(5)) / 2));
index_nodes(nodes);
return nodes;
}
export const icosahedron = () => {
const nodes = make_icosahedron_vertices();
const links = auto_detect_edges(nodes, 5);
links.map((l) => l.label = 0);
return {
name: 'Icosahedron',
nodes: nodes,
links: links,
options: [
{ name: "--"},
],
description: `The icosahedron is a twenty-sided polyhedron and is dual to the dodecahedron. Its four-dimensional analogue is the 600-cell.`
} }
} }
@ -714,6 +844,10 @@ export const dodecahedron = () => {
export const build_all = () => { export const build_all = () => {
return [ return [
tetrahedron(),
octahedron(),
cube(),
icosahedron(),
dodecahedron(), dodecahedron(),
cell5(), cell5(),
cell16(), cell16(),
@ -726,3 +860,7 @@ export const build_all = () => {
cell120_layered() cell120_layered()
]; ];
} }
export const radii = (shape) => {
return shape.nodes.map(n => Math.sqrt(n.x * n.x + n.y * n.y + n.z * n.z + n.w * n.w))
}