From fb6b8aa33cbbe0d46d858c192f43b036f8b291ab Mon Sep 17 00:00:00 2001
From: root <>
Date: Thu, 24 Apr 2025 12:39:51 +0000
Subject: [PATCH 01/10] adjust constant

---
 sketch.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sketch.js b/sketch.js
index 362e59f..e89068d 100644
--- a/sketch.js
+++ b/sketch.js
@@ -35,7 +35,7 @@ function move_charges() {
         charges[i].position,
         charges[j].position,
       );
-      const acceleration_mag = 1 / displacement.mag() * 0.005;
+      const acceleration_mag = 1 / displacement.mag() * 0.001;
       let ai = displacement.copy().normalize().mult(acceleration_mag);
       let aj = p5.Vector.mult(ai, -1);
       project_onto_plane(ai, charges[i].position);

From 6cbdb04786b381c948abd4f6b82d802f9de373b7 Mon Sep 17 00:00:00 2001
From: root <>
Date: Fri, 25 Apr 2025 08:56:49 +0000
Subject: [PATCH 02/10] move charge repelling stuff to its own file

---
 index.html         |  1 +
 sketch.js          | 42 +-----------------------------------------
 thomson-problem.js | 44 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 46 insertions(+), 41 deletions(-)
 create mode 100644 thomson-problem.js

diff --git a/index.html b/index.html
index cf15339..6129f5f 100644
--- a/index.html
+++ b/index.html
@@ -16,6 +16,7 @@ body {
 <body>
 <main>
 </main>
+<script src="thomson-problem.js"></script>
 <script src="sketch.js"></script>
 </body>
 </html>
diff --git a/sketch.js b/sketch.js
index e89068d..cf036fc 100644
--- a/sketch.js
+++ b/sketch.js
@@ -19,52 +19,12 @@ function draw() {
 
   make_lights();
   if (physics) {
-    move_charges();
+    move_charges(charges);
   }
   draw_charges(sphere_radius);
   draw_sphere(sphere_radius, 25);
 }
 
-function move_charges() {
-  for (charge of charges) {
-    charge.acceleration.setMag(0);
-  }
-  for (let i = 0; i < charges.length; i += 1) {
-    for (let j = 0; j < i; j += 1) {
-      const displacement = p5.Vector.sub(
-        charges[i].position,
-        charges[j].position,
-      );
-      const acceleration_mag = 1 / displacement.mag() * 0.001;
-      let ai = displacement.copy().normalize().mult(acceleration_mag);
-      let aj = p5.Vector.mult(ai, -1);
-      project_onto_plane(ai, charges[i].position);
-      project_onto_plane(aj, charges[j].position);
-      charges[i].acceleration.add(ai);
-      charges[j].acceleration.add(aj);
-    }
-  }
-  for (let i = 0; i < charges.length; i += 1) {
-    let charge = charges[i];
-    charge.velocity = charge.velocity.add(charge.acceleration);
-    //project_onto_plane(charge.velocity, charge.position);
-    charge.position = charge.position.add(charge.velocity);
-    charge.position.normalize();
-  }
-}
-
-function project_onto_unit_vector(v, unit_vector) {
-  let size = p5.Vector.dot(v, unit_vector);
-  return p5.Vector.mult(unit_vector, size);
-}
-
-/// Project `v` onto the plane normal to `unit_vector`
-/// Mutates `v`
-function project_onto_plane(v, unit_vector) {
-  let v_proj = project_onto_unit_vector(v, unit_vector);
-  v.sub(v_proj)
-}
-
 function make_charges(n) {
   charges = [];
   for (let i = 0; i < n; i += 1) {
diff --git a/thomson-problem.js b/thomson-problem.js
new file mode 100644
index 0000000..0cbbdaa
--- /dev/null
+++ b/thomson-problem.js
@@ -0,0 +1,44 @@
+function move_charges(charges) {
+  for (charge of charges) {
+    charge.acceleration.setMag(0);
+  }
+  for (let i = 0; i < charges.length; i += 1) {
+    for (let j = 0; j < i; j += 1) {
+      const displacement = p5.Vector.sub(
+        charges[i].position,
+        charges[j].position,
+      );
+      let acceleration_mag = 1 / displacement.mag() * 0.001;
+      let ai;
+      if (acceleration_mag === Infinity) {
+        ai = createVector(random(-1, 1), random(-1, 1), random(-1, 1));
+      } else {
+        ai = displacement.copy();
+      }
+      ai = ai.normalize().mult(acceleration_mag);
+      let aj = p5.Vector.mult(ai, -1);
+      project_onto_plane(ai, charges[i].position);
+      project_onto_plane(aj, charges[j].position);
+      charges[i].acceleration.add(ai);
+      charges[j].acceleration.add(aj);
+    }
+  }
+  for (let i = 0; i < charges.length; i += 1) {
+    let charge = charges[i];
+    charge.velocity = charge.velocity.add(charge.acceleration);
+    charge.position = charge.position.add(charge.velocity);
+    charge.position.normalize();
+  }
+}
+
+function project_onto_unit_vector(v, unit_vector) {
+  let size = p5.Vector.dot(v, unit_vector);
+  return p5.Vector.mult(unit_vector, size);
+}
+
+/// Project `v` onto the plane normal to `unit_vector`
+/// Mutates `v`
+function project_onto_plane(v, unit_vector) {
+  let v_proj = project_onto_unit_vector(v, unit_vector);
+  v.sub(v_proj)
+}

From c20044c4598e99f095fc56cfb7711d24794f54a9 Mon Sep 17 00:00:00 2001
From: root <>
Date: Fri, 25 Apr 2025 08:58:35 +0000
Subject: [PATCH 03/10] skeleton

---
 sketch.js | 43 ++++++++++++++++++++++++++++++++++---------
 1 file changed, 34 insertions(+), 9 deletions(-)

diff --git a/sketch.js b/sketch.js
index cf036fc..65842a6 100644
--- a/sketch.js
+++ b/sketch.js
@@ -1,12 +1,13 @@
 let charges = [];
 
-let sphere_mode = 'circles';
 let sphere_radius = 200;
 
 let physics = false;
+let earth = false;
+let skeleton = false;
 
 function preload() {
-  earth = loadImage("atlas1.jpg");
+  earth_image = loadImage("atlas1.jpg");
 }
 
 function setup() {
@@ -21,10 +22,32 @@ function draw() {
   if (physics) {
     move_charges(charges);
   }
+  if (skeleton) {
+    draw_skeleton(sphere_radius);
+  }
   draw_charges(sphere_radius);
   draw_sphere(sphere_radius, 25);
 }
 
+function draw_skeleton(radius) {
+  push();
+  noStroke();
+  fill(0xff);
+  sphere(4);
+  stroke(0xbf);
+  for (let charge of charges) {
+    line(
+      0,
+      0,
+      0,
+      charge.position.x * radius,
+      charge.position.y * radius,
+      charge.position.z * radius,
+    );
+  }
+  pop();
+}
+
 function make_charges(n) {
   charges = [];
   for (let i = 0; i < n; i += 1) {
@@ -71,7 +94,7 @@ function draw_sphere(radius, n_axis_circles) {
   rotateX(TAU / 4);
   draw_circles(
     radius,
-    sphere_mode === 'earth' ? 2 : n_axis_circles,
+    earth ? 2 : n_axis_circles,
     color(0x00, 0x9f, 0xff),
     color(0xff, 0x9f, 0x00),
   );
@@ -80,17 +103,17 @@ function draw_sphere(radius, n_axis_circles) {
   rotateY(TAU / 4);
   draw_circles(
     radius,
-    sphere_mode === 'earth' ? 2 : n_axis_circles,
+    earth ? 2 : n_axis_circles,
     color(0xff, 0x00, 0xff),
     color(0x00, 0xff, 0x00),
   );
   pop();
 
-  if (sphere_mode === 'earth') {
+  if (earth) {
     noStroke();
     noFill();
     tint(0xff, 0x9f);
-    texture(earth);
+    texture(earth_image);
     push();
     rotateY(TAU / 4);
     sphere(radius);
@@ -127,10 +150,12 @@ function make_lights() {
 }
 
 function keyPressed() {
-  if (key == 'd') {
-    sphere_mode = sphere_mode === 'earth' ? 'circles' : 'earth';
-  } else if (key == ' ') {
+  if (key == ' ') {
     physics = !physics;
+  } else if (key == 'd') {
+    earth = !earth;
+  } else if (key == 'f') {
+    skeleton = !skeleton;
   } else if (key >= '0' && key <= '9') {
     make_charges(int(key));
   }

From 620896fb49506ae8357c9f9048d73808eebed4bc Mon Sep 17 00:00:00 2001
From: root <>
Date: Fri, 25 Apr 2025 14:47:03 +0000
Subject: [PATCH 04/10] begin drawing faces

---
 sketch.js | 50 +++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 49 insertions(+), 1 deletion(-)

diff --git a/sketch.js b/sketch.js
index 65842a6..479ebf8 100644
--- a/sketch.js
+++ b/sketch.js
@@ -5,6 +5,10 @@ let sphere_radius = 200;
 let physics = false;
 let earth = false;
 let skeleton = false;
+let planes = false;
+
+let red;
+let yellow;
 
 function preload() {
   earth_image = loadImage("atlas1.jpg");
@@ -12,6 +16,8 @@ function preload() {
 
 function setup() {
   createCanvas(600, 600, WEBGL);
+  red = color(0xbf, 0x00, 0x00);
+  yellow = color(0xff, 0xff, 0x00);
 }
 
 function draw() {
@@ -25,6 +31,41 @@ function draw() {
   if (skeleton) {
     draw_skeleton(sphere_radius);
   }
+
+  if (planes) {
+    let p = charges[0];
+    let a = charges[1];
+    let b = charges[2];
+    p.color = yellow;
+    a.color = yellow;
+    b.color = yellow;
+    let n = p5.Vector.sub(p.position, a.position).cross(p5.Vector.sub(p.position, b.position));
+    n.normalize(); // unnecessary
+    push();
+    strokeWeight(5);
+    stroke(0x7f);
+    line(
+      0,0,0,
+      n.x*sphere_radius,n.y*sphere_radius,n.z*sphere_radius,
+    )
+    fill(0xff);
+    strokeWeight(3);
+    stroke(0x00);
+    beginShape(TRIANGLES);
+    const v1 = p5.Vector.mult(p.position, sphere_radius);
+    const v2 = p5.Vector.mult(a.position, sphere_radius);
+    const v3 = p5.Vector.mult(b.position, sphere_radius);
+    vertex(v1.x, v1.y, v1.z);
+    vertex(v2.x, v2.y, v2.z);
+    vertex(v3.x, v3.y, v3.z);
+    endShape();
+    pop();
+  } else {
+    for (let charge of charges) {
+      charge.color = red;
+    }
+  }
+
   draw_charges(sphere_radius);
   draw_sphere(sphere_radius, 25);
 }
@@ -67,6 +108,7 @@ function make_charges(n) {
       position: position,
       velocity: createVector(),
       acceleration: createVector(),
+      color: red,
     });
   }
 }
@@ -74,8 +116,8 @@ function make_charges(n) {
 function draw_charges(radius) {
   push();
   noStroke();
-  ambientMaterial(0xbf, 0x00, 0x00);
   for (let charge of charges.values()) {
+    ambientMaterial(charge.color);
     let position = charge.position.copy();
     position.mult(radius);
     push();
@@ -156,7 +198,13 @@ function keyPressed() {
     earth = !earth;
   } else if (key == 'f') {
     skeleton = !skeleton;
+  } else if (key == 'g') {
+    planes = !planes;
   } else if (key >= '0' && key <= '9') {
     make_charges(int(key));
   }
 }
+
+// TODO draw faces
+// algorithm: choose 3 vertices until 2-partition of other vertices has one empty set
+// done when V - E + F = 2. V is known. count E and F while creating faces

From 838eae2cbf198548d4e6c37865badac8dd25c440 Mon Sep 17 00:00:00 2001
From: root <>
Date: Fri, 25 Apr 2025 14:59:51 +0000
Subject: [PATCH 05/10] polyhedron alg progress: face separates vertices

the algorithm so far:
- select 3 vertices
- draw a triangle between them
- for all vertex pairs (non-triangle vertices):
-- draw a line between them
-- if line intersects with triangle plane, red
-- otherwise, green

if the line is red the two vertices are on other sides of the
triangle plane, and this triangle can't be a face, because we
want the polyhedron to be convex

to demo: press 9, press g, press space; press 9 over and over
---
 sketch.js | 51 +++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 39 insertions(+), 12 deletions(-)

diff --git a/sketch.js b/sketch.js
index 479ebf8..f97897e 100644
--- a/sketch.js
+++ b/sketch.js
@@ -33,33 +33,60 @@ function draw() {
   }
 
   if (planes) {
-    let p = charges[0];
-    let a = charges[1];
-    let b = charges[2];
-    p.color = yellow;
-    a.color = yellow;
-    b.color = yellow;
-    let n = p5.Vector.sub(p.position, a.position).cross(p5.Vector.sub(p.position, b.position));
+    let p_charge = charges[0];
+    let a_charge = charges[1];
+    let b_charge = charges[2];
+    p_charge.color = yellow;
+    a_charge.color = yellow;
+    b_charge.color = yellow;
+    let p = p_charge.position;
+    let a = a_charge.position;
+    let b = b_charge.position;
+    let n = p5.Vector.sub(p, a).cross(p5.Vector.sub(p, b));
     n.normalize(); // unnecessary
     push();
     strokeWeight(5);
     stroke(0x7f);
     line(
-      0,0,0,
-      n.x*sphere_radius,n.y*sphere_radius,n.z*sphere_radius,
+      0, 0, 0,
+      n.x * sphere_radius, n.y * sphere_radius, n.z * sphere_radius,
     )
     fill(0xff);
     strokeWeight(3);
     stroke(0x00);
     beginShape(TRIANGLES);
-    const v1 = p5.Vector.mult(p.position, sphere_radius);
-    const v2 = p5.Vector.mult(a.position, sphere_radius);
-    const v3 = p5.Vector.mult(b.position, sphere_radius);
+    const v1 = p5.Vector.mult(p, sphere_radius);
+    const v2 = p5.Vector.mult(a, sphere_radius);
+    const v3 = p5.Vector.mult(b, sphere_radius);
     vertex(v1.x, v1.y, v1.z);
     vertex(v2.x, v2.y, v2.z);
     vertex(v3.x, v3.y, v3.z);
     endShape();
     pop();
+    for (let i = 4; i < charges.length; i += 1) {
+      for (let j = 3; j < i; j += 1) {
+        push();
+        const u = charges[i].position;
+        const v = charges[j].position;
+        const t = p5.Vector.dot(p5.Vector.sub(p, u), n) / p5.Vector.dot(p5.Vector.sub(v, u), n);
+        const intersects_plane = t >= 0 && t <= 1;
+        if (intersects_plane) {
+          stroke(0xff, 0x1f, 0x00);
+        } else {
+          stroke(0x00, 0xff, 0x00);
+        }
+        strokeWeight(3);
+        line(
+          charges[i].position.x * sphere_radius,
+          charges[i].position.y * sphere_radius,
+          charges[i].position.z * sphere_radius,
+          charges[j].position.x * sphere_radius,
+          charges[j].position.y * sphere_radius,
+          charges[j].position.z * sphere_radius,
+        );
+        pop();
+      }
+    }
   } else {
     for (let charge of charges) {
       charge.color = red;

From cec27160f4e75883f5353ebf273f3b092b6609a4 Mon Sep 17 00:00:00 2001
From: root <>
Date: Fri, 25 Apr 2025 17:14:31 +0000
Subject: [PATCH 06/10] convex polyhedron

---
 sketch.js | 149 ++++++++++++++++++++++++++++++------------------------
 1 file changed, 83 insertions(+), 66 deletions(-)

diff --git a/sketch.js b/sketch.js
index f97897e..db98094 100644
--- a/sketch.js
+++ b/sketch.js
@@ -1,3 +1,6 @@
+let camera;
+let red;
+
 let charges = [];
 
 let sphere_radius = 200;
@@ -7,8 +10,6 @@ let earth = false;
 let skeleton = false;
 let planes = false;
 
-let red;
-let yellow;
 
 function preload() {
   earth_image = loadImage("atlas1.jpg");
@@ -16,8 +17,8 @@ function preload() {
 
 function setup() {
   createCanvas(600, 600, WEBGL);
+  camera = createCamera();
   red = color(0xbf, 0x00, 0x00);
-  yellow = color(0xff, 0xff, 0x00);
 }
 
 function draw() {
@@ -25,76 +26,92 @@ function draw() {
   background(50);
 
   make_lights();
-  if (physics) {
-    move_charges(charges);
-  }
-  if (skeleton) {
-    draw_skeleton(sphere_radius);
-  }
+  if (physics) move_charges(charges);
 
-  if (planes) {
-    let p_charge = charges[0];
-    let a_charge = charges[1];
-    let b_charge = charges[2];
-    p_charge.color = yellow;
-    a_charge.color = yellow;
-    b_charge.color = yellow;
-    let p = p_charge.position;
-    let a = a_charge.position;
-    let b = b_charge.position;
-    let n = p5.Vector.sub(p, a).cross(p5.Vector.sub(p, b));
-    n.normalize(); // unnecessary
-    push();
-    strokeWeight(5);
-    stroke(0x7f);
-    line(
-      0, 0, 0,
-      n.x * sphere_radius, n.y * sphere_radius, n.z * sphere_radius,
-    )
-    fill(0xff);
-    strokeWeight(3);
-    stroke(0x00);
+  draw_charges(sphere_radius);
+  draw_sphere(sphere_radius, 25);
+  if (skeleton) draw_skeleton(sphere_radius);
+  if (planes) draw_planes(sphere_radius);
+}
+
+function face_dist_sq([v1, v2, v3]) {
+  const center = p5.Vector.add(v1, v2).add(v3).mult(1 / 3);
+  return createVector(camera.eyeX, camera.eyeY, camera.eyeZ).sub(center).magSq();
+};
+
+function draw_planes(radius) {
+  let faces = [];
+  for (let i = 2; i < charges.length; i += 1) {
+    for (let j = 1; j < i; j += 1) {
+      for (let k = 0; k < j; k += 1) {
+        // Check if p1 p2 p3 form a face of the convex polyhedron
+        // enclosing all vertices ...
+        const p1 = charges[i].position;
+        const p2 = charges[j].position;
+        const p3 = charges[k].position;
+        const normal = p5.Vector.sub(p2, p1).cross(p5.Vector.sub(p3, p1));
+        // ... by checking if the other vertices are on the same
+        // side of the plane generated by p1 p2 p3
+        let plane_separates_vertices = false;
+        for (let r = 1; r < charges.length; r += 1) {
+          for (let s = 0; s < r; s += 1) {
+            if (
+              r === i || r === j || r === k ||
+              s === i || s === j || s === k
+            ) continue;
+            const q1 = charges[r].position;
+            const q2 = charges[s].position;
+            // Let l(t) := q1 + (q2 - q1) * t.
+            // L := { l(t) : 0 <= t <= 1 } is the line segment
+            // between q1 and q2.  L intersects the plane
+            // generated by p1 p2 p3 iff l(t) intersects with
+            // the plane for some 0 <= t <= 1.
+            //
+            // If L intersects the plane, q1 and q2 are on
+            // opposite sides of the plane generated by p1 p2 p3,
+            // so p1 p2 p3 can't be a face.  If we want the
+            // polyhedron to be convex.  Which we do.
+            //
+            // A point k is on the plane generated by p1 p2 p3 iff
+            // dot(k - p1, normal) = 0.  Let n := normal.
+            //
+            // dot(l(t) - p1, n) = 0
+            // iff dot(q1 + (q2 - q1) * t - p1, n) = 0
+            // iff dot(q1 - p1, n) + dot(q2 - q2, n) * t = 0
+            // iff t = dot(p1 - q1, n) / dot(q2 - q2, n)
+            const t = (
+              p5.Vector.dot(p5.Vector.sub(p1, q1), normal) /
+              p5.Vector.dot(p5.Vector.sub(q2, q1), normal)
+            );
+            plane_separates_vertices ||= t >= 0 && t <= 1;
+            if (plane_separates_vertices) break;
+          }
+          if (plane_separates_vertices) break;
+        }
+        if (!plane_separates_vertices) {
+          faces.push([
+            p5.Vector.mult(p1, radius),
+            p5.Vector.mult(p2, radius),
+            p5.Vector.mult(p3, radius),
+          ]);
+        }
+      }
+    }
+  }
+  // fix OpenGL stacking alpha behaviour
+  faces.sort((a, b) => face_dist_sq(b) - face_dist_sq(a));
+  push();
+  strokeWeight(2);
+  stroke(0x00);
+  for ([v1, v2, v3] of faces) {
+    fill(0xbf, 0x7f);
     beginShape(TRIANGLES);
-    const v1 = p5.Vector.mult(p, sphere_radius);
-    const v2 = p5.Vector.mult(a, sphere_radius);
-    const v3 = p5.Vector.mult(b, sphere_radius);
     vertex(v1.x, v1.y, v1.z);
     vertex(v2.x, v2.y, v2.z);
     vertex(v3.x, v3.y, v3.z);
     endShape();
-    pop();
-    for (let i = 4; i < charges.length; i += 1) {
-      for (let j = 3; j < i; j += 1) {
-        push();
-        const u = charges[i].position;
-        const v = charges[j].position;
-        const t = p5.Vector.dot(p5.Vector.sub(p, u), n) / p5.Vector.dot(p5.Vector.sub(v, u), n);
-        const intersects_plane = t >= 0 && t <= 1;
-        if (intersects_plane) {
-          stroke(0xff, 0x1f, 0x00);
-        } else {
-          stroke(0x00, 0xff, 0x00);
-        }
-        strokeWeight(3);
-        line(
-          charges[i].position.x * sphere_radius,
-          charges[i].position.y * sphere_radius,
-          charges[i].position.z * sphere_radius,
-          charges[j].position.x * sphere_radius,
-          charges[j].position.y * sphere_radius,
-          charges[j].position.z * sphere_radius,
-        );
-        pop();
-      }
-    }
-  } else {
-    for (let charge of charges) {
-      charge.color = red;
-    }
   }
-
-  draw_charges(sphere_radius);
-  draw_sphere(sphere_radius, 25);
+  pop();
 }
 
 function draw_skeleton(radius) {

From 34e1de68f1913d0c737aa2589b4981c78c556c3a Mon Sep 17 00:00:00 2001
From: root <>
Date: Sat, 26 Apr 2025 04:08:10 +0000
Subject: [PATCH 07/10] hide sphere; euler's forumla trick; cache faces

---
 sketch.js | 68 ++++++++++++++++++++++++++++++++++++-------------------
 1 file changed, 45 insertions(+), 23 deletions(-)

diff --git a/sketch.js b/sketch.js
index db98094..1d684e6 100644
--- a/sketch.js
+++ b/sketch.js
@@ -2,13 +2,18 @@ let camera;
 let red;
 
 let charges = [];
+let faces = [];
 
 let sphere_radius = 200;
 
+const SURFACE_NONE = 0;
+const SURFACE_CIRCLES = 1;
+const SURFACE_EARTH = 2;
+
+let surface = SURFACE_CIRCLES;
 let physics = false;
-let earth = false;
 let skeleton = false;
-let planes = false;
+let polyhedron = false;
 
 
 function preload() {
@@ -16,11 +21,15 @@ function preload() {
 }
 
 function setup() {
-  createCanvas(600, 600, WEBGL);
+  createCanvas(windowWidth, windowHeight, WEBGL);
   camera = createCamera();
   red = color(0xbf, 0x00, 0x00);
 }
 
+function windowResized() {
+  resizeCanvas(windowWidth, windowHeight);
+}
+
 function draw() {
   orbitControl();
   background(50);
@@ -29,9 +38,12 @@ function draw() {
   if (physics) move_charges(charges);
 
   draw_charges(sphere_radius);
-  draw_sphere(sphere_radius, 25);
   if (skeleton) draw_skeleton(sphere_radius);
-  if (planes) draw_planes(sphere_radius);
+  if (polyhedron) {
+    if (physics || faces.length === 0) find_faces();
+    draw_faces(sphere_radius);
+  }
+  draw_sphere(sphere_radius, 25);
 }
 
 function face_dist_sq([v1, v2, v3]) {
@@ -39,8 +51,8 @@ function face_dist_sq([v1, v2, v3]) {
   return createVector(camera.eyeX, camera.eyeY, camera.eyeZ).sub(center).magSq();
 };
 
-function draw_planes(radius) {
-  let faces = [];
+function find_faces() {
+  faces = [];
   for (let i = 2; i < charges.length; i += 1) {
     for (let j = 1; j < i; j += 1) {
       for (let k = 0; k < j; k += 1) {
@@ -53,6 +65,7 @@ function draw_planes(radius) {
         // ... by checking if the other vertices are on the same
         // side of the plane generated by p1 p2 p3
         let plane_separates_vertices = false;
+        let euler_formula = false;
         for (let r = 1; r < charges.length; r += 1) {
           for (let s = 0; s < r; s += 1) {
             if (
@@ -85,30 +98,32 @@ function draw_planes(radius) {
             );
             plane_separates_vertices ||= t >= 0 && t <= 1;
             if (plane_separates_vertices) break;
+            euler_formula ||= charges.length * 2 - faces.length == 4;
+            if (euler_formula) break;
           }
-          if (plane_separates_vertices) break;
+          if (plane_separates_vertices || euler_formula) break;
         }
+        if (euler_formula) return;
         if (!plane_separates_vertices) {
-          faces.push([
-            p5.Vector.mult(p1, radius),
-            p5.Vector.mult(p2, radius),
-            p5.Vector.mult(p3, radius),
-          ]);
+          faces.push([p1, p2, p3]);
         }
       }
     }
   }
+}
+
+function draw_faces(radius) {
   // fix OpenGL stacking alpha behaviour
   faces.sort((a, b) => face_dist_sq(b) - face_dist_sq(a));
   push();
   strokeWeight(2);
   stroke(0x00);
-  for ([v1, v2, v3] of faces) {
+  for ([p1, p2, p3] of faces) {
     fill(0xbf, 0x7f);
     beginShape(TRIANGLES);
-    vertex(v1.x, v1.y, v1.z);
-    vertex(v2.x, v2.y, v2.z);
-    vertex(v3.x, v3.y, v3.z);
+    vertex(p1.x * radius, p1.y * radius, p1.z * radius);
+    vertex(p2.x * radius, p2.y * radius, p2.z * radius);
+    vertex(p3.x * radius, p3.y * radius, p3.z * radius);
     endShape();
   }
   pop();
@@ -176,11 +191,14 @@ function draw_sphere(radius, n_axis_circles) {
   stroke(0x3f);
   noFill();
 
+  if (surface === SURFACE_NONE) n_axis_circles = 0;
+  else if (surface === SURFACE_EARTH) n_axis_circles = 2;
+
   push();
   rotateX(TAU / 4);
   draw_circles(
     radius,
-    earth ? 2 : n_axis_circles,
+    n_axis_circles,
     color(0x00, 0x9f, 0xff),
     color(0xff, 0x9f, 0x00),
   );
@@ -189,13 +207,13 @@ function draw_sphere(radius, n_axis_circles) {
   rotateY(TAU / 4);
   draw_circles(
     radius,
-    earth ? 2 : n_axis_circles,
+    n_axis_circles,
     color(0xff, 0x00, 0xff),
     color(0x00, 0xff, 0x00),
   );
   pop();
 
-  if (earth) {
+  if (surface === SURFACE_EARTH) {
     noStroke();
     noFill();
     tint(0xff, 0x9f);
@@ -239,16 +257,20 @@ function keyPressed() {
   if (key == ' ') {
     physics = !physics;
   } else if (key == 'd') {
-    earth = !earth;
+    surface = (surface + 1) % 3;
   } else if (key == 'f') {
     skeleton = !skeleton;
   } else if (key == 'g') {
-    planes = !planes;
+    polyhedron = !polyhedron;
   } else if (key >= '0' && key <= '9') {
-    make_charges(int(key));
+    make_charges(Math.pow(int(key), 2));
+    faces = [];
   }
 }
 
 // TODO draw faces
 // algorithm: choose 3 vertices until 2-partition of other vertices has one empty set
 // done when V - E + F = 2. V is known. count E and F while creating faces
+// when the graph is finished: E = F * 3 / 2
+// V - E + F = V - F * 3 / 2 + F = V - F / 2
+// V * 2 - F = 4

From d0be92d50ecd336643879a793964a178edb8f01a Mon Sep 17 00:00:00 2001
From: root <>
Date: Sat, 26 Apr 2025 04:12:55 +0000
Subject: [PATCH 08/10] charges should not have earth texture, lol

---
 sketch.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sketch.js b/sketch.js
index 1d684e6..cbc9252 100644
--- a/sketch.js
+++ b/sketch.js
@@ -214,11 +214,11 @@ function draw_sphere(radius, n_axis_circles) {
   pop();
 
   if (surface === SURFACE_EARTH) {
+    push();
     noStroke();
     noFill();
     tint(0xff, 0x9f);
     texture(earth_image);
-    push();
     rotateY(TAU / 4);
     sphere(radius);
     pop();

From e51d5df1dc684062d4b23ff59fdfce1e11d155f3 Mon Sep 17 00:00:00 2001
From: root <>
Date: Sat, 26 Apr 2025 04:15:57 +0000
Subject: [PATCH 09/10] cleanup

---
 sketch.js | 9 +--------
 1 file changed, 1 insertion(+), 8 deletions(-)

diff --git a/sketch.js b/sketch.js
index cbc9252..1d3cb0a 100644
--- a/sketch.js
+++ b/sketch.js
@@ -263,14 +263,7 @@ function keyPressed() {
   } else if (key == 'g') {
     polyhedron = !polyhedron;
   } else if (key >= '0' && key <= '9') {
-    make_charges(Math.pow(int(key), 2));
+    make_charges(int(key));
     faces = [];
   }
 }
-
-// TODO draw faces
-// algorithm: choose 3 vertices until 2-partition of other vertices has one empty set
-// done when V - E + F = 2. V is known. count E and F while creating faces
-// when the graph is finished: E = F * 3 / 2
-// V - E + F = V - F * 3 / 2 + F = V - F / 2
-// V * 2 - F = 4

From eeb1beaea3d8ade3e5c835df68973bcaac7cf34a Mon Sep 17 00:00:00 2001
From: root <>
Date: Sat, 26 Apr 2025 04:20:00 +0000
Subject: [PATCH 10/10] fix: firefox scrollbar

---
 index.html | 1 +
 1 file changed, 1 insertion(+)

diff --git a/index.html b/index.html
index 6129f5f..ff66bc9 100644
--- a/index.html
+++ b/index.html
@@ -10,6 +10,7 @@ body {
   padding: 0;
   margin: 0;
   background-color: #1b1b1b;
+  overflow-y: hidden;
 }
 </style>
 </head>