first commit
This commit is contained in:
commit
beb51dbb5c
5 changed files with 468 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.make.*
|
201
geometries.js
Normal file
201
geometries.js
Normal 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
47
index.html
Normal 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
145
script.js
Normal 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
74
style.css
Normal 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;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue