From beb51dbb5cf64a6315bd95b663ef36758f834ec2 Mon Sep 17 00:00:00 2001 From: Christian Lawson-Perfect Date: Sun, 6 Apr 2025 15:54:58 +0000 Subject: [PATCH] first commit --- .gitignore | 1 + geometries.js | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 47 ++++++++++++ script.js | 145 ++++++++++++++++++++++++++++++++++++ style.css | 74 +++++++++++++++++++ 5 files changed, 468 insertions(+) create mode 100644 .gitignore create mode 100644 geometries.js create mode 100644 index.html create mode 100644 script.js create mode 100644 style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8bc0f76 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.make.* \ No newline at end of file diff --git a/geometries.js b/geometries.js new file mode 100644 index 0000000..cf2edf4 --- /dev/null +++ b/geometries.js @@ -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; + + } + +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..8e7f6d9 --- /dev/null +++ b/index.html @@ -0,0 +1,47 @@ + + + + + + Thomson problem + + + + + +
+ + + + +
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + + \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 0000000..a8b1ad4 --- /dev/null +++ b/script.js @@ -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 { + 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); +} \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..767aea6 --- /dev/null +++ b/style.css @@ -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; +} \ No newline at end of file