first commit

This commit is contained in:
Christian Lawson-Perfect 2025-04-06 15:54:58 +00:00
commit beb51dbb5c
5 changed files with 468 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.make.*

201
geometries.js Normal file
View file

@ -0,0 +1,201 @@
import * as THREE from 'three';
import { ConvexHull } from 'three/addons/math/ConvexHull.js';
const {BufferGeometry, Float32BufferAttribute, Vector3} = THREE;
export class ConvexGeometry extends BufferGeometry {
constructor( points = [] ) {
super();
this.convexHull = new ConvexHull();
this.setFromPoints(points);
}
setFromPoints(points) {
const {convexHull} = this;
convexHull.setFromPoints(points);
// buffers
const vertices = [];
const normals = [];
// generate vertices and normals
const faces = convexHull.faces;
for ( let i = 0; i < faces.length; i ++ ) {
const face = faces[ i ];
let edge = face.edge;
// we move along a doubly-connected edge list to access all face points (see HalfEdge docs)
do {
const point = edge.head().point;
vertices.push( point.x, point.y, point.z );
normals.push( face.normal.x, face.normal.y, face.normal.z );
edge = edge.next;
} while ( edge !== face.edge );
}
// build geometry
this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
}
}
export class WireframeGeometry extends BufferGeometry {
constructor( geometry = null ) {
super();
this.type = 'WireframeGeometry';
this.parameters = {
geometry: geometry
};
this.update();
}
update() {
const {geometry} = this.parameters;
if ( geometry !== null ) {
// buffer
const vertices = [];
const edges = new Set();
// helper variables
const start = new Vector3();
const end = new Vector3();
if ( geometry.index !== null ) {
// indexed BufferGeometry
const position = geometry.attributes.position;
const indices = geometry.index;
let groups = geometry.groups;
if ( groups.length === 0 ) {
groups = [ { start: 0, count: indices.count, materialIndex: 0 } ];
}
// create a data structure that contains all edges without duplicates
for ( let o = 0, ol = groups.length; o < ol; ++ o ) {
const group = groups[ o ];
const groupStart = group.start;
const groupCount = group.count;
for ( let i = groupStart, l = ( groupStart + groupCount ); i < l; i += 3 ) {
for ( let j = 0; j < 3; j ++ ) {
const index1 = indices.getX( i + j );
const index2 = indices.getX( i + ( j + 1 ) % 3 );
start.fromBufferAttribute( position, index1 );
end.fromBufferAttribute( position, index2 );
if ( isUniqueEdge( start, end, edges ) === true ) {
vertices.push( start.x, start.y, start.z );
vertices.push( end.x, end.y, end.z );
}
}
}
}
} else {
// non-indexed BufferGeometry
const position = geometry.attributes.position;
for ( let i = 0, l = ( position.count / 3 ); i < l; i ++ ) {
for ( let j = 0; j < 3; j ++ ) {
// three edges per triangle, an edge is represented as (index1, index2)
// e.g. the first triangle has the following edges: (0,1),(1,2),(2,0)
const index1 = 3 * i + j;
const index2 = 3 * i + ( ( j + 1 ) % 3 );
start.fromBufferAttribute( position, index1 );
end.fromBufferAttribute( position, index2 );
if ( isUniqueEdge( start, end, edges ) === true ) {
vertices.push( start.x, start.y, start.z );
vertices.push( end.x, end.y, end.z );
}
}
}
}
// build geometry
this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
}
}
copy( source ) {
super.copy( source );
this.parameters = Object.assign( {}, source.parameters );
return this;
}
}
export function isUniqueEdge( start, end, edges ) {
const hash1 = `${start.x},${start.y},${start.z}-${end.x},${end.y},${end.z}`;
const hash2 = `${end.x},${end.y},${end.z}-${start.x},${start.y},${start.z}`; // coincident edge
if ( edges.has( hash1 ) === true || edges.has( hash2 ) === true ) {
return false;
} else {
edges.add( hash1 );
edges.add( hash2 );
return true;
}
}

47
index.html Normal file
View file

@ -0,0 +1,47 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Thomson problem</title>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.173.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.173.0/examples/jsm/"
}
}
</script>
<script type="module" src="script.js"></script>
<link rel="stylesheet" href="style.css"/>
</head>
<body>
<section id="controls">
<datalist id="heights">
<option value="1"/>
<option value="1.3">
</datalist>
<datalist id="scales">
<option value="0.86"/>
</datalist>
<div>
<label for="h1"><math><msub><mi>h</mi><mn>1</mn></msup></math>:</label>
<input type="range" min="0" max="2" value="1" step="0.01" id="h1" list="heights">
</div>
<div>
<label for="h2"><math><msub><mi>h</mi><mn>2</mn></msup></math>:</label>
<input type="range" min="0" max="2" value="1.33" step="0.02" id="h2" list="heights">
</div>
<div>
<label for="scale">Scale:</label>
<input type="range" min="0" max="1" value="0.86" step="0.02" id="scale" list="scales">
</div>
</section>
<section id="three-container"></section>
<footer>
<a href="https://somethingorotherwhatever.com">made by clp</a>
</footer>
</body>
</html>

145
script.js Normal file
View file

@ -0,0 +1,145 @@
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { STLExporter } from 'three/addons/exporters/STLExporter.js';
import { ConvexHull } from 'three/addons/math/ConvexHull.js';
const {BufferGeometry, Float32BufferAttribute, Vector3} = THREE;
import {ConvexGeometry, WireframeGeometry} from './geometries.js';
const scene = new THREE.Scene();
const width = window.innerWidth;
const height = window.innerHeight;
const w = width<height ? 1.3 : (width/height * 1.3);
const h = height/width * w;
const camera = new THREE.OrthographicCamera( -w, w, h, -h, 0, 100);
scene.background = new THREE.Color(0xc49879);
// Create a directional light
const light = new THREE.DirectionalLight(0xffffff, 1.0);
// move the light back and up a bit
light.position.set(0, 3, 3);
scene.add(light);
scene.add(new THREE.AmbientLight(0xffffff))
window.v3 = Vector3;
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize( width, height );
document.getElementById('three-container').appendChild( renderer.domElement );
const scale = 1.5;
renderer.domElement.width = scale*width;
renderer.domElement.height = scale*height;
renderer.setViewport(0,0,scale*width, scale*height);
function make_points(h1,h2) {
h1 = new Vector3(0,0,h1);
h2 = new Vector3(0,0,h2);
const [v1,v3,v10] = [0,1,2].map(i => {
const an = Math.PI*2 * i/3;
return new Vector3(Math.cos(an),Math.sin(an),0);
});
const v0 = v1.clone().add(v3).divideScalar(2).sub(h1);
const v2 = v1.clone().add(v3).divideScalar(2).add(h1);
const v4 = v1.clone().add(v3).add(v10).divideScalar(3).sub(h2);
const v7 = v1.clone().add(v3).add(v10).divideScalar(3).add(h2);
const v5 = v1.clone().add(v10).divideScalar(2).sub(h1);
const v6 = v1.clone().add(v10).divideScalar(2).add(h1);
const v8 = v3.clone().add(v10).divideScalar(2).add(h1);
const v9 = v3.clone().add(v10).divideScalar(2).sub(h1);
const points = [v0,v1,v2,v3,v4,v5,v6,v7,v8,v9,v10];
/*
const x1 = 0.2;
const x2 = 0.4;
const y = 0.5;
const points = [
[1,0,0], // 0
[0,1,0], // 1
[-1,0,0], // 2
[0,-1,0], // 3
[x2,0,h1], // 4
[x1,y,h2], // 5
[-x1,y,h2], // 6
[-x2,0,h1], // 7
[-x1,-y,h2], // 8
[x1,-y,h2], // 9
[0,0,h1+h2] // 10
].map(p => new Vector3(...p));
const [v0,v1,v2,v3,v4,v5,v6,v7,v8,v9,v10] = points;
*/
const edges = [
v0,v1,
v0,v4,
v0,v3,
v1,v2,
v1,v6,
v1,v5,
v2,v3,
v2,v7,
v3,v8,
v3,v9,
v4,v5,
v4,v9,
v5,v10,
v6,v7,
v6,v10,
v7,v8,
v8,v10,
v9,v10
].flatMap(p => [p.x,p.y,p.z]);
return {points,edges};
}
const hull_geometry = new ConvexGeometry( [] );
const hull_material = new THREE.MeshStandardMaterial( { color: 0x93c8ee, transparent: true} );
const hull = new THREE.Mesh( hull_geometry, hull_material );
hull.renderOrder = -1;
scene.add( hull );
const wireframe = new BufferGeometry( [] );
const line = new THREE.LineSegments( wireframe, new THREE.LineBasicMaterial({color: 0x0, transparent: true}) );
line.renderOrder = -1;
scene.add( line );
camera.position.z = 1.8;
const controls = new OrbitControls( camera, renderer.domElement );
hull.rotation.x = line.rotation.x = -Math.PI/2;
//hull.position.y = line.position.y = -0.5;
let i = 0;
function animate() {
i += 1;
const an = i/100;
controls.update();
renderer.render( scene, camera );
const h1 = document.getElementById('h1').valueAsNumber;
const h2 = document.getElementById('h2').valueAsNumber;
const scale = document.getElementById('scale').valueAsNumber;
const {points, edges} = make_points(h1 * scale, h2 * scale);
hull_geometry.setFromPoints(points);
//hull_geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(faces), 3))
wireframe.setAttribute('position', new Float32BufferAttribute(edges,3));
}
renderer.setAnimationLoop( animate );
function vlen([x,y,z]) {
return Math.sqrt(x**2+y**2+z**2);
}

74
style.css Normal file
View file

@ -0,0 +1,74 @@
/* Instrument Sans variable italic latin */
@font-face {
font-family: 'Instrument Sans';
font-style: italic;
font-display: swap;
font-weight: 400 700;
font-stretch: 75 100;
src: url(https://cdn.jsdelivr.net/fontsource/fonts/instrument-sans:vf@latest/latin-wdth-italic.woff2) format('woff2-variations');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
/* Instrument Sans variable normal latin */
@font-face {
font-family: 'Instrument Sans';
font-style: normal;
font-display: swap;
font-weight: 400 700;
font-stretch: 75 100;
src: url(https://cdn.jsdelivr.net/fontsource/fonts/instrument-sans:vf@latest/latin-wdth-normal.woff2) format('woff2-variations');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
body {
margin: 0;
width: 100svw;
height: 100svh;
}
body, input {
font-family: 'Instrument Sans', sans-serif;
}
a {
color: white;
}
#controls {
position: absolute;
top: 0;
padding: 1em;
box-sizing: border-box;
width: 100svw;
display: flex;
flex-wrap: wrap;
gap: 1em;
}
#controls > div {
display: flex;
align-items: center;
gap: 0.25em;
}
footer {
position: absolute;
bottom: 1em;
right: 1em;
}
#controls > #energy {
display: grid;
grid-template:
"label meter"
". output"
;
}
output[for="energy-meter"] {
font-family: monospace;
grid-area: output;
text-align: right;
}
#num-points {
width: 5em;
text-align: center;
}