3D_slit_simulation / scene-manager.js
AK51's picture
Upload 8 files
ef3ec9f verified
/**
* SceneManager — Three.js scene setup, lighting, camera, OrbitControls.
* Loaded as a plain script; class is defined on the global scope.
*/
class SceneManager {
constructor() {
this.scene = null;
this.camera = null;
this.renderer = null;
this.controls = null;
this.detectorMarker = null;
// Default camera position and target for reset — top-down view
this.defaultCameraPosition = { x: 0, y: 700, z: 0 };
this.defaultCameraTarget = { x: 0, y: 0, z: 0 };
}
/**
* Initialise the Three.js scene inside the given container element.
* @param {HTMLElement} container
* @param {object} [options]
* @param {number} [options.detectorCol] - detector column x-index
* @param {number} [options.nx] - grid width in cells
* @param {number} [options.ny] - grid height in cells
*/
init(container, options) {
var opts = options || {};
var nx = opts.nx || 300;
var ny = opts.ny || 200;
var detectorCol = opts.detectorCol != null ? opts.detectorCol : Math.floor(nx * 0.85);
// Scene
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x2a2a3e);
// Camera — adjust distance based on aspect ratio so the full plane is visible
var w = Math.max(container.clientWidth, 1);
var h = Math.max(container.clientHeight, 1);
var aspect = w / h;
// On narrow/portrait screens, zoom out more to see the full plane
var zoomFactor = aspect < 0.8 ? 1.8 : (aspect < 1.2 ? 1.5 : (aspect < 1.6 ? 1.15 : 1.0));
this._zoomFactor = zoomFactor;
this.defaultTopPosition = { x: 0, y: 700 * zoomFactor, z: 0 };
this.camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 3000);
this.camera.position.set(
this.defaultCameraPosition.x,
this.defaultCameraPosition.y * zoomFactor,
this.defaultCameraPosition.z
);
this.camera.lookAt(
this.defaultCameraTarget.x,
this.defaultCameraTarget.y,
this.defaultCameraTarget.z
);
// Renderer — wrapped in try/catch for WebGL unavailability
try {
this.renderer = new THREE.WebGLRenderer({ antialias: true });
} catch (e) {
var msg = document.createElement('p');
msg.textContent =
'WebGL is not supported in your browser. Please use a modern browser with WebGL support.';
msg.style.cssText = 'color:#e0e0e0;padding:2rem;text-align:center;';
container.appendChild(msg);
return;
}
this.renderer.setSize(container.clientWidth, Math.max(container.clientHeight, 1));
this.renderer.setPixelRatio(window.devicePixelRatio || 1);
container.appendChild(this.renderer.domElement);
// OrbitControls — rotate (left-drag), zoom (scroll), pan (right-drag)
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
this.controls.target.set(
this.defaultCameraTarget.x,
this.defaultCameraTarget.y,
this.defaultCameraTarget.z
);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.1;
this.controls.update();
// Lighting — multiple sources for vivid colors
var dirLight = new THREE.DirectionalLight(0xffffff, 1.0);
dirLight.position.set(100, 400, 200);
this.scene.add(dirLight);
var dirLight2 = new THREE.DirectionalLight(0xccddff, 0.5);
dirLight2.position.set(-200, 300, -100);
this.scene.add(dirLight2);
var ambientLight = new THREE.AmbientLight(0x8888aa, 0.8);
this.scene.add(ambientLight);
// Hemisphere light for natural diffused illumination
var hemiLight = new THREE.HemisphereLight(0xccccff, 0x444466, 0.6);
this.scene.add(hemiLight);
// Detector marker — vertical line at the detector column position
this._addDetectorMarker(detectorCol, ny, nx);
}
/**
* Add a semi-transparent screen at the detector column.
* The wave surface is centered at origin, so we convert grid coords
* to world coords: worldX = -surfaceWidth/2 + detectorCol * dx.
* @param {number} detectorCol - x position in grid coordinates
* @param {number} ny - grid height
* @private
*/
_addDetectorMarker(detectorCol, ny, nx) {
nx = nx || 800;
var dx = 1.0;
this._nx = nx;
this._ny = ny;
var surfaceWidth = (nx - 1) * dx;
var surfaceDepth = (ny - 1) * dx;
// Convert grid column to world X (surface is centered at origin)
var worldX = -surfaceWidth / 2 + detectorCol * dx;
// Semi-transparent plane spanning the full depth and a reasonable height
var screenHeight = this._elementHeight || 30;
var geometry = new THREE.PlaneGeometry(surfaceDepth, screenHeight);
var material = new THREE.MeshBasicMaterial({
color: 0x00ff88,
transparent: true,
opacity: 0.15,
side: THREE.DoubleSide,
depthWrite: false
});
this.detectorMarker = new THREE.Mesh(geometry, material);
// Rotate so the plane faces along the X axis (stands upright in XZ plane)
this.detectorMarker.rotation.y = Math.PI / 2;
this.detectorMarker.position.set(worldX, screenHeight / 2 - 5, 0);
this.scene.add(this.detectorMarker);
}
/**
* Update the height of the detector marker.
* @param {number} height
*/
updateDetectorHeight(height) {
if (!this.detectorMarker || !this.scene) return;
var oldPos = this.detectorMarker.position;
this.scene.remove(this.detectorMarker);
if (this.detectorMarker.geometry) this.detectorMarker.geometry.dispose();
var nx = this._nx || 800;
var surfaceDepth = ((this._ny || 600) - 1) * 1.0;
var geometry = new THREE.PlaneGeometry(surfaceDepth, height);
this.detectorMarker.geometry = geometry;
this.detectorMarker.position.set(oldPos.x, height / 2 - 5, 0);
this.scene.add(this.detectorMarker);
}
/** Restore the camera to its default position and lookAt target. */
resetCamera() {
if (!this.camera || !this.controls) return;
this.camera.position.set(
this.defaultCameraPosition.x,
this.defaultCameraPosition.y,
this.defaultCameraPosition.z
);
this.controls.target.set(
this.defaultCameraTarget.x,
this.defaultCameraTarget.y,
this.defaultCameraTarget.z
);
this.camera.lookAt(
this.defaultCameraTarget.x,
this.defaultCameraTarget.y,
this.defaultCameraTarget.z
);
this.controls.update();
}
/**
* Switch the bottom interactive camera to a specific view.
* @param {'top'|'angle'} mode
*/
setBottomView(mode) {
if (!this.camera || !this.controls) return;
var wasDamping = this.controls.enableDamping;
this.controls.enableDamping = false;
var zf = this._zoomFactor || 1.0;
if (mode === 'top') {
this.camera.position.set(0, 700 * zf, 1);
this.controls.target.set(0, 0, 0);
} else if (mode === '135') {
this.camera.position.set(300 * zf, 400 * zf, -350 * zf);
this.controls.target.set(0, 0, 0);
} else {
this.camera.position.set(-300 * zf, 400 * zf, 400 * zf);
this.controls.target.set(0, 0, 0);
}
this.camera.lookAt(0, 0, 0);
this.controls.update();
this.controls.enableDamping = wasDamping;
}
/**
* Update renderer size and camera aspect ratio.
* @param {number} width
* @param {number} height
*/
resize(width, height) {
var w = Math.max(width, 1);
var h = Math.max(height, 1);
if (this.camera) {
this.camera.aspect = w / h;
this.camera.updateProjectionMatrix();
}
if (this.renderer) {
this.renderer.setSize(w, h);
}
this.containerWidth = w;
this.containerHeight = h;
}
render() {
if (!this.renderer || !this.scene || !this.camera) return;
if (this.controls) this.controls.update();
this.renderer.render(this.scene, this.camera);
}
}
window.SceneManager = SceneManager;