aboutsummaryrefslogtreecommitdiff
path: root/src/scene
diff options
context:
space:
mode:
authorLalleSX2026-10-24 14:58:21 +0200
committerLalleSX2026-10-24 14:58:21 +0200
commit73b09270c54acab810c10a80bed1d77c1a7bb2ab (patch)
treee5bae1180c9a76345cc7779a24988875542d2dd9 /src/scene
parentAdd tests for math rendering and interactive scenes; update navigation and viewport handling (diff)
download3D-Electrophysics-73b09270c54acab810c10a80bed1d77c1a7bb2ab.tar.gz
3D-Electrophysics-73b09270c54acab810c10a80bed1d77c1a7bb2ab.zip
Tilføj danskHEADmain
Diffstat (limited to '')
-rw-r--r--src/scene/experiments.ts23
-rw-r--r--src/scene/lab.ts17
-rw-r--r--src/scene/test4.ts3
3 files changed, 27 insertions, 16 deletions
diff --git a/src/scene/experiments.ts b/src/scene/experiments.ts
index f48c984..f42017d 100644
--- a/src/scene/experiments.ts
+++ b/src/scene/experiments.ts
@@ -1,5 +1,6 @@
import * as THREE from 'three';
import { chargePair, carrierModel, number, plateModel, wireModel, type ExperimentId, type Parameters } from '../experiments';
+import { scene } from '../i18n';
import { electricField, type Charge, type Point } from '../physics/electricField';
import { forceOn, pairEnergy, potential } from '../physics/models';
import { arrow, box, colours, grid, label, line, sphere } from './primitives';
@@ -29,9 +30,9 @@ function fieldScene(group: THREE.Group, p: Parameters) {
}
const force = forceOn(charges[1], [charges[0]]);
arrow(group, { ...charges[1], y: 0.8 }, force, 1.3, colours.gold);
- label(group, String.raw`$\vec F$ on right charge`, { x: charges[1].x, y: 1.5, z: 0 }, colours.gold, 2);
+ label(group, String.raw`$\vec F$ ${scene('onRightCharge')}`, { x: charges[1].x, y: 1.5, z: 0 }, colours.gold, 2);
sphere(group, { x: 0, y: 0, z: 0 }, 0.06, colours.gold);
- label(group, 'Origin', { x: 0, y: 0, z: 0.45 }, colours.muted, 0.9);
+ label(group, scene('origin'), { x: 0, y: 0, z: 0.45 }, colours.muted, 0.9);
}
function fluxScene(group: THREE.Group, p: Parameters) {
@@ -52,7 +53,7 @@ function fluxScene(group: THREE.Group, p: Parameters) {
const dot = e.x * normal.x + e.y * normal.y + e.z * normal.z;
compressedFieldArrow(group, point, e, dot >= 0 ? colours.field : colours.positive);
}
- label(group, 'Gaussian surface', { x: 0, y: radius + 0.6, z: 0 }, colours.negative, 2.5);
+ label(group, scene('gaussian'), { x: 0, y: radius + 0.6, z: 0 }, colours.negative, 2.5);
line(group, [{ x: -4.7, y: 0, z: 0 }, { x: 4.7, y: 0, z: 0 }], colours.muted, 0.4);
}
@@ -116,7 +117,7 @@ function potentialScene(group: THREE.Group, p: Parameters) {
const probe = { x: number(p, 'probe'), y: 0, z: 1 };
const v = potential(probe, charges);
sphere(group, { ...probe, y: 2 * Math.tanh(v / 15) + 0.2 }, 0.13, colours.gold);
- label(group, 'Probe', { ...probe, y: 2 * Math.tanh(v / 15) + 0.8 }, colours.gold, 1.3);
+ label(group, scene('probe'), { ...probe, y: 2 * Math.tanh(v / 15) + 0.8 }, colours.gold, 1.3);
charges.forEach(charge => signedCharge(group, { ...charge, y: Math.sign(charge.coulombs) * 2.2 }));
grid(group, -2.4);
}
@@ -155,8 +156,8 @@ function plateScene(group: THREE.Group, p: Parameters, dielectric: boolean) {
}
}
line(group, [{ x: -halfGap, y: -side, z: 0 }, { x: halfGap, y: -side, z: 0 }], colours.muted);
- label(group, String.raw`$d = ${number(p, 'gap').toFixed(1)}\,\mathrm{mm}$ (gap $\times50$)`, { x: 0, y: -side - 0.45, z: 0 }, colours.muted, 2.5);
- label(group, String.raw`$${c.voltage.toFixed(2)}\,\mathrm V$ · ${p.connection === 'isolated' ? 'isolated' : 'supply connected'}`, { x: 0, y: side + 0.8, z: 0 }, colours.field, 3);
+ label(group, String.raw`$d = ${number(p, 'gap').toFixed(1)}\,\mathrm{mm}$ (${scene('gap')} $\times50$)`, { x: 0, y: -side - 0.45, z: 0 }, colours.muted, 2.5);
+ label(group, String.raw`$${c.voltage.toFixed(2)}\,\mathrm V$ · ${scene(p.connection === 'isolated' ? 'isolated' : 'connected')}`, { x: 0, y: side + 0.8, z: 0 }, colours.field, 3);
grid(group, -3.2);
}
@@ -182,9 +183,9 @@ function currentScene(group: THREE.Group, p: Parameters): Animate {
carriers.push(sphere(group, { x: length * (i / 32 - 0.5), y: r * Math.cos(angle), z: r * Math.sin(angle) }, 0.065, colours.negative));
}
arrow(group, { x: -Math.sign(model.current) * 1.1, y: radius + 0.8, z: 0 }, { x: model.current, y: 0, z: 0 }, 2.2, colours.field);
- label(group, model.current === 0 ? 'No net current' : 'Conventional current', { x: 0, y: radius + 1.5, z: 0 }, colours.field, 2.8);
+ label(group, scene(model.current === 0 ? 'noCurrent' : 'conventional'), { x: 0, y: radius + 1.5, z: 0 }, colours.field, 2.8);
arrow(group, { x: -Math.sign(model.electronDrift) * 1.1, y: -radius - 0.65, z: 0 }, { x: model.electronDrift, y: 0, z: 0 }, 2.2, colours.negative);
- label(group, String.raw`Electron drift · displayed $\times600$`, { x: 0, y: -radius - 1.1, z: 0 }, colours.negative, 3.3);
+ label(group, scene('drift'), { x: 0, y: -radius - 1.1, z: 0 }, colours.negative, 3.3);
const velocity = model.electronDrift * 600 * 3;
return delta => {
for (const electron of carriers) {
@@ -203,9 +204,9 @@ function semiconductorScene(group: THREE.Group, p: Parameters): Animate {
line(group, [{ x: -3.5, y: 0, z: 1.6 }, { x: 3.5, y: 0, z: 1.6 }], colours.muted, 0.7);
line(group, [{ x: -3.6, y: s.fermiShift * 3, z: 1.65 }, { x: 3.6, y: s.fermiShift * 3, z: 1.65 }], colours.gold);
label(group, String.raw`$E_F$`, { x: 4, y: s.fermiShift * 3, z: 1.65 }, colours.gold, 0.7);
- label(group, String.raw`$E_i\approx\text{midgap}$`, { x: -4.4, y: 0, z: 1.65 }, colours.muted, 1.7);
- label(group, 'Conduction band', { x: 0, y: bandEdge + 1.1, z: 0 }, colours.negative, 2.8);
- label(group, 'Valence band', { x: 0, y: -bandEdge - 1.1, z: 0 }, colours.positive, 2.8);
+ label(group, String.raw`$E_i\approx\text{${scene('midgap')}}$`, { x: -4.4, y: 0, z: 1.65 }, colours.muted, 1.7);
+ label(group, scene('conduction'), { x: 0, y: bandEdge + 1.1, z: 0 }, colours.negative, 2.8);
+ label(group, scene('valence'), { x: 0, y: -bandEdge - 1.1, z: 0 }, colours.positive, 2.8);
label(group, String.raw`$E_g=1.12\,\mathrm{eV}$`, { x: 4.1, y: 0, z: -1.3 }, colours.muted, 1.8);
line(group, [{ x: 3.8, y: -bandEdge, z: -1.3 }, { x: 3.8, y: bandEdge, z: -1.3 }], colours.muted);
const particles: { mesh: THREE.Mesh; base: THREE.Vector3; phase: number }[] = [];
diff --git a/src/scene/lab.ts b/src/scene/lab.ts
index 782f300..e00c234 100644
--- a/src/scene/lab.ts
+++ b/src/scene/lab.ts
@@ -2,6 +2,7 @@ import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { CSS2DRenderer } from 'three/addons/renderers/CSS2DRenderer.js';
import type { ExperimentId, Parameters } from '../experiments';
+import { onLanguageChange, t } from '../i18n';
import { buildExperiment, type Animate } from './experiments';
import { disposeGroup } from './primitives';
@@ -13,12 +14,13 @@ export function createLab(viewport: HTMLElement) {
const error = document.createElement('div');
error.className = 'webgl-error';
error.setAttribute('role', 'alert');
- error.textContent = 'WebGL could not start. Enable browser hardware acceleration and reload to see the 3D scenes. Lessons, controls and measurements still work.';
+ error.textContent = t('webglStart');
viewport.append(error);
- return { update: () => {}, resetCamera: (_id?: ExperimentId) => {}, setPaused: () => {}, dispose: () => {} };
+ const stop = onLanguageChange(() => { error.textContent = t('webglStart'); });
+ return { update: () => {}, resetCamera: (_id?: ExperimentId) => {}, setPaused: () => {}, dispose: () => stop() };
}
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
- renderer.domElement.setAttribute('aria-label', '3D experiment: drag to orbit, scroll to zoom, right-drag to pan');
+ renderer.domElement.setAttribute('aria-label', t('canvas'));
renderer.domElement.setAttribute('tabindex', '0');
viewport.append(renderer.domElement);
const labels = new CSS2DRenderer();
@@ -55,6 +57,11 @@ export function createLab(viewport: HTMLElement) {
let paused = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let lastTime = 0;
let pending: { id: ExperimentId; parameters: Parameters } | undefined;
+ let lostWarning: HTMLElement | undefined;
+ const stopLanguage = onLanguageChange(() => {
+ renderer.domElement.setAttribute('aria-label', t('canvas'));
+ if (lostWarning) lostWarning.textContent = t('webglLost');
+ });
function update(id: ExperimentId, parameters: Parameters) {
// Coalesce range events to one rebuild per animation frame.
pending = { id, parameters: { ...parameters } };
@@ -78,7 +85,8 @@ export function createLab(viewport: HTMLElement) {
const warning = document.createElement('p');
warning.setAttribute('role', 'alert');
warning.className = 'webgl-error';
- warning.textContent = 'WebGL context lost. Reload to restore the 3D view. Numeric experiments still work.';
+ warning.textContent = t('webglLost');
+ lostWarning = warning;
viewport.append(warning);
}
});
@@ -103,6 +111,7 @@ export function createLab(viewport: HTMLElement) {
resetCamera,
setPaused: (value: boolean) => { paused = value; },
dispose: () => {
+ stopLanguage();
renderer.setAnimationLoop(null);
observer.disconnect();
controls.dispose();
diff --git a/src/scene/test4.ts b/src/scene/test4.ts
index 97e53ee..a3f6ccd 100644
--- a/src/scene/test4.ts
+++ b/src/scene/test4.ts
@@ -1,5 +1,6 @@
import * as THREE from 'three';
import { number, format as formatNumber, type Parameters, type TestId } from '../experiments';
+import { scene } from '../i18n';
import { type Charge, type Point } from '../physics/electricField';
import { pairEnergy } from '../physics/models';
import { assemblyState, centralState, conservationState, motionState, movingCharge, progress, rotationState } from '../test4';
@@ -82,7 +83,7 @@ function conservation(group: THREE.Group, p: Parameters): Animate {
label(group, String.raw`$${name} = ${format(joules * 1e9)}\,\mathrm{nJ}$`, { x, y: baseline - 0.45, z: 1 }, colour, 1.3);
}
line(group, [{ x: -2.8, y: -0.1, z: 1 }, { x: 2.8, y: -0.1, z: 1 }], colours.field, 0.4);
- label(group, 'Energy changes form · total stays constant', { x: 0, y: -4.2, z: 1 }, colours.field, 3.5);
+ label(group, scene('energyConstant'), { x: 0, y: -4.2, z: 1 }, colours.field, 3.5);
return pulse(probe);
}