diff options
Diffstat (limited to '')
| -rw-r--r-- | src/scene/primitives.ts | 90 |
1 files changed, 90 insertions, 0 deletions
diff --git a/src/scene/primitives.ts b/src/scene/primitives.ts new file mode 100644 index 0000000..bce9113 --- /dev/null +++ b/src/scene/primitives.ts @@ -0,0 +1,90 @@ +import * as THREE from 'three'; +import type { Point } from '../physics/electricField'; + +export const colours = { positive: '#ff967b', negative: '#73d7ff', field: '#91e2cb', gold: '#f5d58b', muted: '#8294a8' }; +export const vector = (p: Point) => new THREE.Vector3(p.x, p.y, p.z); +export function sphere(group: THREE.Group, position: Point, radius: number, colour: string): THREE.Mesh { + const mesh = new THREE.Mesh(new THREE.SphereGeometry(radius, 18, 12), new THREE.MeshStandardMaterial({ color: colour, roughness: 0.3, emissive: colour, emissiveIntensity: 0.12 })); + mesh.position.copy(vector(position)); + group.add(mesh); + return mesh; +} +export function box(group: THREE.Group, position: Point, dimensions: Point, colour: string, opacity = 1): THREE.Mesh { + const mesh = new THREE.Mesh(new THREE.BoxGeometry(dimensions.x, dimensions.y, dimensions.z), new THREE.MeshStandardMaterial({ color: colour, transparent: opacity < 1, opacity, depthWrite: opacity === 1, side: THREE.DoubleSide, roughness: 0.45 })); + mesh.position.copy(vector(position)); + group.add(mesh); + return mesh; +} +export function line(group: THREE.Group, points: Point[], colour: string, opacity = 1): THREE.Line { + const mesh = new THREE.Line(new THREE.BufferGeometry().setFromPoints(points.map(vector)), new THREE.LineBasicMaterial({ color: colour, transparent: opacity < 1, opacity })); + group.add(mesh); + return mesh; +} +export function arrow(group: THREE.Group, origin: Point, direction: Point, length: number, colour = colours.field): void { + const dir = vector(direction); + if (dir.lengthSq() < 1e-24) return; + group.add(new THREE.ArrowHelper(dir.normalize(), vector(origin), length, colour, Math.min(0.16, length * 0.3), Math.min(0.1, length * 0.2))); +} +export function label(group: THREE.Group, text: string, position: Point, colour: string = '#dbe7f0', width = 2): THREE.Sprite { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d')!; + context.font = '500 44px system-ui, sans-serif'; + canvas.width = Math.ceil(context.measureText(text).width) + 36; + canvas.height = 80; + context.fillStyle = 'rgba(16, 24, 34, 0.85)'; + context.fillRect(0, 0, canvas.width, canvas.height); + context.font = '500 44px system-ui, sans-serif'; + context.fillStyle = colour; + context.textAlign = 'center'; + context.textBaseline = 'middle'; + context.fillText(text, canvas.width / 2, 40); + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: texture, depthTest: false })); + sprite.position.copy(vector(position)); + const aspect = canvas.width / canvas.height; + const screenHeight = 23 + Math.min(2, Math.max(-1, width - 2)) * 2; + const viewportSize = new THREE.Vector2(); + const cameraPosition = new THREE.Vector3(); + // Keep labels legible when the camera or viewport changes. Width hints + // affect text size slightly; the actual texture fits the text content. + sprite.onBeforeRender = (renderer, _scene, camera) => { + if (!(camera instanceof THREE.PerspectiveCamera)) return; + sprite.getWorldPosition(cameraPosition).applyMatrix4(camera.matrixWorldInverse); + renderer.getSize(viewportSize); + const worldHeight = 2 * Math.abs(cameraPosition.z) * Math.tan(THREE.MathUtils.degToRad(camera.getEffectiveFOV()) / 2) * screenHeight / Math.max(1, viewportSize.y); + sprite.scale.set(worldHeight * aspect, worldHeight, 1); + sprite.updateMatrixWorld(); + }; + group.add(sprite); + return sprite; +} +export function grid(group: THREE.Group, y = -0.25) { + const helper = new THREE.GridHelper(12, 12, 0x375066, 0x22313f); + helper.position.y = y; + group.add(helper); +} + +/** Dispose shared geometries/materials once per scene replacement, including + * textures owned by generated labels. No GPU resources survive a lesson. */ +export function disposeGroup(group: THREE.Group) { + const geometries = new Set<THREE.BufferGeometry>(); + const materials = new Set<THREE.Material>(); + const textures = new Set<THREE.Texture>(); + group.traverse(object => { + const mesh = object as THREE.Mesh; + if (mesh.geometry) geometries.add(mesh.geometry); + if (mesh.material) { + const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material]; + for (const material of list) { + materials.add(material); + const map = (material as THREE.MeshBasicMaterial).map; + if (map) textures.add(map); + } + } + }); + geometries.forEach(geometry => geometry.dispose()); + materials.forEach(material => material.dispose()); + textures.forEach(texture => texture.dispose()); + group.clear(); +} |