From deadd4d8342ed6e88dee29a1d45192942b755e57 Mon Sep 17 00:00:00 2001 From: Sepp Vogel <131676727+birdylife@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:06:28 +0100 Subject: [PATCH] Add ConcaveDiffractionGrating object and rotation with +/- keys --- locales/de/main.json | 5 + src/simulator/index.html | 8 + src/simulator/js/app.js | 12 + src/simulator/js/sceneObjs.js | 1 + .../mirror/ConcaveDiffractionGrating.js | 424 ++++++++++++++++++ src/simulator/js/versionUpdate.js | 5 + src/webpages/home.hbs | 7 + 7 files changed, 462 insertions(+) create mode 100644 src/simulator/js/sceneObjs/mirror/ConcaveDiffractionGrating.js diff --git a/locales/de/main.json b/locales/de/main.json index a72e63c4..b560df18 100644 --- a/locales/de/main.json +++ b/locales/de/main.json @@ -64,6 +64,11 @@ "description": "Ein Spiegel bestehend aus einem Kreisbogen, definiert durch drei Punkte.", "instruction": "Ziehen oder Klicken zum Erzeugen. Die ersten 2 Punkte definieren die Länge des Bogens, der letzte die Bogenkrümmung." }, + "ConcaveDiffractionGrating": { + "title": "Konkaves Reflexionsgitter", + "description": "Ein konkaver Gitterspiegel, welcher auf ArcMirror sowie DiffractionGrating basiert", + "instruction": "Ziehen oder Klicken zum Erzeugen. Die ersten 2 Punkte definieren die Länge des Bogens, der letzte die Bogenkrümmung." + }, "ParabolicMirror": { "title": "Parabolspiegel", "description": "Ein parabolischer Spiegel, definiert durch drei Punkte.", diff --git a/src/simulator/index.html b/src/simulator/index.html index b732be5f..4a23dd39 100644 --- a/src/simulator/index.html +++ b/src/simulator/index.html @@ -220,6 +220,10 @@ +
  • + + +
  • @@ -758,6 +762,10 @@
  • +
  • + + +
  • diff --git a/src/simulator/js/app.js b/src/simulator/js/app.js index c6086db2..053aa1db 100644 --- a/src/simulator/js/app.js +++ b/src/simulator/js/app.js @@ -482,6 +482,16 @@ async function startApp() { return false; } + //Plus and Minus Keys for rotation + if (e.keyCode == 107 || e.keyCode == 187) { // + key for rotate clockwise + scene.objs[editor.selectedObjIndex].rotate(1); + simulator.updateSimulation(!scene.objs[editor.selectedObjIndex].constructor.isOptical, true); + } + if (e.keyCode == 109 || e.keyCode == 189) { // - key for rotate c-clockwise + scene.objs[editor.selectedObjIndex].rotate(-1); + simulator.updateSimulation(!scene.objs[editor.selectedObjIndex].constructor.isOptical, true); + } + //Arrow Keys if (e.keyCode >= 37 && e.keyCode <= 40) { var step = scene.snapToGrid ? scene.gridSize : 1; @@ -1193,6 +1203,7 @@ function initUIText() { setText('mirrorToolsDropdown', i18next.t('main:tools.categories.mirror')); setText('tool_Mirror_label', i18next.t('main:tools.Mirror.title'), null, i18next.t('main:meta.parentheses', {main: i18next.t('main:tools.Mirror.description'), sub: i18next.t('main:tools.common.lineInstruction')}), 'Mirror.svg'); setText('tool_ArcMirror_label', i18next.t('main:tools.ArcMirror.title'), null, i18next.t('main:meta.parentheses', {main: i18next.t('main:tools.ArcMirror.description'), sub: i18next.t('main:tools.ArcMirror.instruction')}), 'ArcMirror.svg'); + setText('tool_ConcaveDiffractionGrating_label', i18next.t('main:tools.ConcaveDiffractionGrating.title'), null, i18next.t('main:meta.parentheses', {main: i18next.t('main:tools.ConcaveDiffractionGrating.description'), sub: i18next.t('main:tools.ConcaveDiffractionGrating.instruction')}), 'ConcaveDiffractionGrating.svg'); setText('tool_ParabolicMirror_label', i18next.t('main:tools.ParabolicMirror.title'), null, i18next.t('main:meta.parentheses', {main: i18next.t('main:tools.ParabolicMirror.description'), sub: i18next.t('main:tools.ParabolicMirror.instruction')}), 'ParabolicMirror.svg'); setText('tool_CustomMirror_label', i18next.t('main:tools.CustomMirror.title'), null, i18next.t('main:meta.parentheses', {main: i18next.t('main:tools.CustomMirror.description'), sub: i18next.t('main:tools.common.lineInstruction')}), 'CustomMirror.svg'); setText('tool_IdealMirror_label', i18next.t('main:tools.IdealMirror.title'), null, i18next.t('main:meta.parentheses', {main: i18next.t('main:tools.IdealMirror.description'), sub: i18next.t('main:tools.common.lineInstruction')}), 'IdealMirror.svg'); @@ -1283,6 +1294,7 @@ function initUIText() { setText('tool_AngleSource_mobile_label', i18next.t('main:tools.PointSource.title') + ' (<360\u00B0)'); setText('tool_Mirror_mobile_label', i18next.t('main:tools.Mirror.title')); setText('tool_ArcMirror_mobile_label', i18next.t('main:tools.ArcMirror.title')); + setText('tool_ConcaveDiffractionGrating_mobile_label', i18next.t('main:tools.ConcaveDiffractionGrating.title')); setText('tool_ParabolicMirror_mobile_label', i18next.t('main:tools.ParabolicMirror.title')); setText('tool_CustomMirror_mobile_label', i18next.t('main:tools.CustomMirror.title')); setText('tool_IdealMirror_mobile_label', i18next.t('main:tools.IdealMirror.title')); diff --git a/src/simulator/js/sceneObjs.js b/src/simulator/js/sceneObjs.js index 5c94a91e..ec9467a9 100644 --- a/src/simulator/js/sceneObjs.js +++ b/src/simulator/js/sceneObjs.js @@ -36,6 +36,7 @@ export const PointSource = require('./sceneObjs/lightSource/PointSource.js').def export const AngleSource = require('./sceneObjs/lightSource/AngleSource.js').default; export const Mirror = require('./sceneObjs/mirror/Mirror.js').default; export const ArcMirror = require('./sceneObjs/mirror/ArcMirror.js').default; +export const ConcaveDiffractionGrating = require('./sceneObjs/mirror/ConcaveDiffractionGrating.js').default; export const ParabolicMirror = require('./sceneObjs/mirror/ParabolicMirror.js').default; export const CustomMirror = require('./sceneObjs/mirror/CustomMirror.js').default; export const IdealMirror = require('./sceneObjs/mirror/IdealMirror.js').default; diff --git a/src/simulator/js/sceneObjs/mirror/ConcaveDiffractionGrating.js b/src/simulator/js/sceneObjs/mirror/ConcaveDiffractionGrating.js new file mode 100644 index 00000000..3a5e1fc1 --- /dev/null +++ b/src/simulator/js/sceneObjs/mirror/ConcaveDiffractionGrating.js @@ -0,0 +1,424 @@ +/* + * Copyright 2024 The Ray Optics Simulation authors and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import BaseFilter from '../BaseFilter.js'; +import i18next from 'i18next'; +import Simulator from '../../Simulator.js'; +import geometry from '../../geometry.js'; + +/** + * Mirror with shape of a circular arc. Diffracts light. + * + * Tools -> Mirror -> Concave Diffraction Grating + * @class + * @extends BaseFilter + * @memberof sceneObjs + * @property {Point} p1 - The first endpoint. + * @property {Point} p2 - The second endpoint. + * @property {Point} p3 - The control point on the arc. + * @property {number} lineDensity - The number of lines per millimeter. + * @property {boolean} customBrightness - Whether the output brightness are customized. + * @property {number[]} brightnesses - The brightnesses of the diffracted rays for m = 0, 1, -1, 2, -2, ... when `customBrightness` is true. The number is to be normalized to the brightness of the incident ray. The values not in the array are set to 0. + * @property {number} slitRatio - The ratio of the slit width to the line interval. + */ +class ConcaveDiffractionGrating extends BaseFilter { + static type = 'ConcaveDiffractionGrating'; + static isOptical = true; + static serializableDefaults = { + p1: null, + p2: null, + p3: null, + lineDensity: 1000, + customBrightness: false, + brightnesses: [1, 0.5, 0.5], + slitRatio: 0.5, + }; + + populateObjBar(objBar) { + objBar.setTitle(i18next.t('main:tools.ConcaveDiffractionGrating.title')); + objBar.createNumber(i18next.t('simulator:sceneObjs.DiffractionGrating.lineDensity', {lengthUnit: 'mm'}), 1, 2500, 5, this.lineDensity, function (obj, value) { + obj.lineDensity = value; + }); + + objBar.createBoolean(i18next.t('simulator:sceneObjs.DiffractionGrating.customBrightness'), this.customBrightness, function (obj, value) { + obj.customBrightness = value; + }, i18next.t('simulator:sceneObjs.DiffractionGrating.customBrightnessInfo'), true); + + if (this.customBrightness) { + objBar.createTuple('', this.brightnesses.join(', '), function (obj, value) { + obj.brightnesses = value.split(',').map(parseFloat); + }); + } else if (objBar.showAdvanced(!this.arePropertiesDefault(['slitRatio']))) { + objBar.createNumber(i18next.t('simulator:sceneObjs.DiffractionGrating.slitRatio'), 0, 1, 0.001, this.slitRatio, function (obj, value) { + obj.slitRatio = value; + }); + } + } + + draw(canvasRenderer, isAboveLight, isHovered) { + const ctx = canvasRenderer.ctx; + const ls = canvasRenderer.lengthScale; + + ctx.fillStyle = 'rgb(255,0,255)'; + if (this.p3 && this.p2) { + var center = geometry.linesIntersection(geometry.perpendicularBisector(geometry.line(this.p1, this.p3)), geometry.perpendicularBisector(geometry.line(this.p2, this.p3))); + if (isFinite(center.x) && isFinite(center.y)) { + var r = geometry.distance(center, this.p3); + var a1 = Math.atan2(this.p1.y - center.y, this.p1.x - center.x); + var a2 = Math.atan2(this.p2.y - center.y, this.p2.x - center.x); + var a3 = Math.atan2(this.p3.y - center.y, this.p3.x - center.x); + const colorArray = Simulator.wavelengthToColor(this.wavelength || Simulator.GREEN_WAVELENGTH, 1); + ctx.strokeStyle = isHovered ? 'cyan' : (this.scene.simulateColors && this.wavelength && this.filter ? canvasRenderer.rgbaToCssColor(colorArray) : 'rgb(168,168,168)'); + ctx.lineWidth = 1 * ls; + ctx.beginPath(); + ctx.arc(center.x, center.y, r, a1, a2, (a2 < a3 && a3 < a1) || (a1 < a2 && a2 < a3) || (a3 < a1 && a1 < a2)); + ctx.stroke(); + if (isHovered) { + ctx.fillRect(this.p3.x - 1.5 * ls, this.p3.y - 1.5 * ls, 3 * ls, 3 * ls); + ctx.fillStyle = 'rgb(255,0,0)'; + ctx.fillRect(this.p1.x - 1.5 * ls, this.p1.y - 1.5 * ls, 3 * ls, 3 * ls); + ctx.fillRect(this.p2.x - 1.5 * ls, this.p2.y - 1.5 * ls, 3 * ls, 3 * ls); + } + } else { + // The three points on the arc is colinear. Treat as a line segment. + const colorArray = Simulator.wavelengthToColor(this.wavelength || Simulator.GREEN_WAVELENGTH, 1); + ctx.strokeStyle = isHovered ? 'cyan' : (this.scene.simulateColors && this.wavelength && this.filter ? canvasRenderer.rgbaToCssColor(colorArray) : 'rgb(168,168,168)'); + ctx.lineWidth = 1 * ls; + ctx.beginPath(); + ctx.moveTo(this.p1.x, this.p1.y); + ctx.lineTo(this.p2.x, this.p2.y); + ctx.stroke(); + + ctx.fillRect(this.p3.x - 1.5 * ls, this.p3.y - 1.5 * ls, 3 * ls, 3 * ls); + ctx.fillStyle = 'rgb(255,0,0)'; + ctx.fillRect(this.p1.x - 1.5 * ls, this.p1.y - 1.5 * ls, 3 * ls, 3 * ls); + ctx.fillRect(this.p2.x - 1.5 * ls, this.p2.y - 1.5 * ls, 3 * ls, 3 * ls); + } + } else if (this.p2) { + ctx.fillStyle = 'rgb(255,0,0)'; + ctx.fillRect(this.p1.x - 1.5 * ls, this.p1.y - 1.5 * ls, 3 * ls, 3 * ls); + ctx.fillRect(this.p2.x - 1.5 * ls, this.p2.y - 1.5 * ls, 3 * ls, 3 * ls); + } else { + ctx.fillStyle = 'rgb(255,0,0)'; + ctx.fillRect(this.p1.x - 1.5 * ls, this.p1.y - 1.5 * ls, 3 * ls, 3 * ls); + } + } + + move(diffX, diffY) { + this.p1.x = this.p1.x + diffX; + this.p1.y = this.p1.y + diffY; + this.p2.x = this.p2.x + diffX; + this.p2.y = this.p2.y + diffY; + this.p3.x = this.p3.x + diffX; + this.p3.y = this.p3.y + diffY; + } + + //rotate around P3 + rotate(cw) { // cw = 1 for clockwise, cw = -1 for counterclockwise + const angle = 0.5; // Degree increment + const angleInRadians = angle * Math.PI / 180 * cw; // Convert to radians, apply direction + + // Difference from p3 + const diff_p1_x = this.p1.x - this.p3.x; + const diff_p1_y = this.p1.y - this.p3.y; + const diff_p2_x = this.p2.x - this.p3.x; + const diff_p2_y = this.p2.y - this.p3.y; + + // Apply rotation matrix to p1 + this.p1.x = this.p3.x + diff_p1_x * Math.cos(angleInRadians) - diff_p1_y * Math.sin(angleInRadians); + this.p1.y = this.p3.y + diff_p1_x * Math.sin(angleInRadians) + diff_p1_y * Math.cos(angleInRadians); + + // Apply rotation matrix to p2 + this.p2.x = this.p3.x + diff_p2_x * Math.cos(angleInRadians) - diff_p2_y * Math.sin(angleInRadians); + this.p2.y = this.p3.y + diff_p2_x * Math.sin(angleInRadians) + diff_p2_y * Math.cos(angleInRadians); + } + + onConstructMouseDown(mouse, ctrl, shift) { + if (!this.constructionPoint) { + // Initialize the construction stage. + this.constructionPoint = mouse.getPosSnappedToGrid(); + this.p1 = this.constructionPoint; + this.p2 = null; + this.p3 = null; + } + + if (!this.p2 && !this.p3) { + this.p2 = mouse.getPosSnappedToGrid(); + return; + } + + if (this.p2 && !this.p3 && !mouse.snapsOnPoint(this.p1)) { + if (shift) { + this.p2 = mouse.getPosSnappedToDirection(this.p1, [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 1, y: -1 }]); + } else { + this.p2 = mouse.getPosSnappedToGrid(); + } + this.p3 = mouse.getPosSnappedToGrid(); + return; + } + } + + onConstructMouseMove(mouse, ctrl, shift) { + if (!this.p3 && !mouse.isOnPoint(this.p1)) { + if (shift) { + this.p2 = mouse.getPosSnappedToDirection(this.constructionPoint, [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 1, y: -1 }]); + } else { + this.p2 = mouse.getPosSnappedToGrid(); + } + + this.p1 = ctrl ? geometry.point(2 * this.constructionPoint.x - this.p2.x, 2 * this.constructionPoint.y - this.p2.y) : this.constructionPoint; + + return; + } + if (this.p3) { + this.p3 = mouse.getPosSnappedToGrid(); + return; + } + } + + onConstructMouseUp(mouse, ctrl, shift) { + if (this.p2 && !this.p3 && !mouse.isOnPoint(this.p1)) { + this.p3 = mouse.getPosSnappedToGrid(); + return; + } + if (this.p3 && !mouse.isOnPoint(this.p2)) { + this.p3 = mouse.getPosSnappedToGrid(); + delete this.constructionPoint; + return { + isDone: true + }; + } + } + + checkMouseOver(mouse) { + let dragContext = {}; + if (mouse.isOnPoint(this.p1) && geometry.distanceSquared(mouse.pos, this.p1) <= geometry.distanceSquared(mouse.pos, this.p2) && geometry.distanceSquared(mouse.pos, this.p1) <= geometry.distanceSquared(mouse.pos, this.p3)) { + dragContext.part = 1; + dragContext.targetPoint = geometry.point(this.p1.x, this.p1.y); + return dragContext; + } + if (mouse.isOnPoint(this.p2) && geometry.distanceSquared(mouse.pos, this.p2) <= geometry.distanceSquared(mouse.pos, this.p3)) { + dragContext.part = 2; + dragContext.targetPoint = geometry.point(this.p2.x, this.p2.y); + return dragContext; + } + if (mouse.isOnPoint(this.p3)) { + dragContext.part = 3; + dragContext.targetPoint = geometry.point(this.p3.x, this.p3.y); + return dragContext; + } + + var center = geometry.linesIntersection(geometry.perpendicularBisector(geometry.line(this.p1, this.p3)), geometry.perpendicularBisector(geometry.line(this.p2, this.p3))); + const mousePos = mouse.getPosSnappedToGrid(); + if (isFinite(center.x) && isFinite(center.y)) { + var r = geometry.distance(center, this.p3); + var a1 = Math.atan2(this.p1.y - center.y, this.p1.x - center.x); + var a2 = Math.atan2(this.p2.y - center.y, this.p2.x - center.x); + var a3 = Math.atan2(this.p3.y - center.y, this.p3.x - center.x); + var a_m = Math.atan2(mouse.pos.y - center.y, mouse.pos.x - center.x); + if (Math.abs(geometry.distance(center, mouse.pos) - r) < mouse.getClickExtent() && (((a2 < a3 && a3 < a1) || (a1 < a2 && a2 < a3) || (a3 < a1 && a1 < a2)) == ((a2 < a_m && a_m < a1) || (a1 < a2 && a2 < a_m) || (a_m < a1 && a1 < a2)))) { + // Dragging the entire obj + dragContext.part = 0; + dragContext.mousePos0 = mousePos; // Mouse position when the user starts dragging + dragContext.mousePos1 = mousePos; // Mouse position at the last moment during dragging + dragContext.snapContext = {}; + return dragContext; + } + } else { + // The three points on the arc is colinear. Treat as a line segment. + if (mouse.isOnSegment(this)) { + dragContext.part = 0; + dragContext.mousePos0 = mousePos; // Mouse position when the user starts dragging + dragContext.mousePos1 = mousePos; // Mouse position at the last moment during dragging + dragContext.snapContext = {}; + return dragContext; + } + } + } + + onDrag(mouse, dragContext, ctrl, shift) { + var basePoint; + if (dragContext.part == 1) { + // Dragging the first endpoint + basePoint = ctrl ? geometry.segmentMidpoint(dragContext.originalObj) : dragContext.originalObj.p2; + + this.p1 = shift ? mouse.getPosSnappedToDirection(basePoint, [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 1, y: -1 }, { x: (dragContext.originalObj.p2.x - dragContext.originalObj.p1.x), y: (dragContext.originalObj.p2.y - dragContext.originalObj.p1.y) }]) : mouse.getPosSnappedToGrid(); + this.p2 = ctrl ? geometry.point(2 * basePoint.x - this.p1.x, 2 * basePoint.y - this.p1.y) : basePoint; + } + if (dragContext.part == 2) { + // Dragging the second endpoint + basePoint = ctrl ? geometry.segmentMidpoint(dragContext.originalObj) : dragContext.originalObj.p1; + + this.p2 = shift ? mouse.getPosSnappedToDirection(basePoint, [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 1, y: -1 }, { x: (dragContext.originalObj.p2.x - dragContext.originalObj.p1.x), y: (dragContext.originalObj.p2.y - dragContext.originalObj.p1.y) }]) : mouse.getPosSnappedToGrid(); + this.p1 = ctrl ? geometry.point(2 * basePoint.x - this.p2.x, 2 * basePoint.y - this.p2.y) : basePoint; + } + if (dragContext.part == 3) { + // Dragging the third endpoint + this.p3 = mouse.getPosSnappedToGrid(); + } + if (dragContext.part == 0) { + // Dragging the entire obj + if (shift) { + var mousePos = mouse.getPosSnappedToDirection(dragContext.mousePos0, [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: (dragContext.originalObj.p2.x - dragContext.originalObj.p1.x), y: (dragContext.originalObj.p2.y - dragContext.originalObj.p1.y) }, { x: (dragContext.originalObj.p2.y - dragContext.originalObj.p1.y), y: -(dragContext.originalObj.p2.x - dragContext.originalObj.p1.x) }], dragContext.snapContext); + } else { + var mousePos = mouse.getPosSnappedToGrid();; + dragContext.snapContext = {}; // Unlock the dragging direction when the user release the shift key + } + + var mouseDiffX = dragContext.mousePos1.x - mousePos.x; // The X difference between the mouse position now and at the previous moment + var mouseDiffY = dragContext.mousePos1.y - mousePos.y; // The Y difference between the mouse position now and at the previous moment + // Move the first point + this.p1.x = this.p1.x - mouseDiffX; + this.p1.y = this.p1.y - mouseDiffY; + // Move the second point + this.p2.x = this.p2.x - mouseDiffX; + this.p2.y = this.p2.y - mouseDiffY; + + this.p3.x = this.p3.x - mouseDiffX; + this.p3.y = this.p3.y - mouseDiffY; + + // Update the mouse position + dragContext.mousePos1 = mousePos; + } + } + + checkRayIntersects(ray) { + if (this.checkRayIntersectFilter(ray)) { + if (!this.p3) { return null; } + var center = geometry.linesIntersection(geometry.perpendicularBisector(geometry.line(this.p1, this.p3)), geometry.perpendicularBisector(geometry.line(this.p2, this.p3))); + if (isFinite(center.x) && isFinite(center.y)) { + var rp_temp = geometry.lineCircleIntersections(geometry.line(ray.p1, ray.p2), geometry.circle(center, this.p2)); + var rp_exist = []; + var rp_lensq = []; + for (var i = 1; i <= 2; i++) { + rp_exist[i] = !geometry.intersectionIsOnSegment(geometry.linesIntersection(geometry.line(this.p1, this.p2), geometry.line(this.p3, rp_temp[i])), geometry.line(this.p3, rp_temp[i])) && geometry.intersectionIsOnRay(rp_temp[i], ray) && geometry.distanceSquared(rp_temp[i], ray.p1) > Simulator.MIN_RAY_SEGMENT_LENGTH_SQUARED * this.scene.lengthScale * this.scene.lengthScale; + rp_lensq[i] = geometry.distanceSquared(ray.p1, rp_temp[i]); + } + if (rp_exist[1] && ((!rp_exist[2]) || rp_lensq[1] < rp_lensq[2])) { return rp_temp[1]; } + if (rp_exist[2] && ((!rp_exist[1]) || rp_lensq[2] < rp_lensq[1])) { return rp_temp[2]; } + } else { + // The three points on the arc is colinear. Treat as a line segment. + var rp_temp = geometry.linesIntersection(geometry.line(ray.p1, ray.p2), geometry.line(this.p1, this.p2)); + + if (geometry.intersectionIsOnSegment(rp_temp, this) && geometry.intersectionIsOnRay(rp_temp, ray)) { + return rp_temp; + } else { + return null; + } + } + } + } + + onRayIncident(ray, rayIndex, incidentPoint) { + const mm_in_nm = 1 / 1e6; + let truncation = 0; + + // Ray and Grating Geometry + const rx = ray.p1.x - incidentPoint.x; + const ry = ray.p1.y - incidentPoint.y; + + // Center of curvature (for reflection/diffraction) + const center = geometry.linesIntersection( + geometry.perpendicularBisector(geometry.line(this.p1, this.p3)), + geometry.perpendicularBisector(geometry.line(this.p2, this.p3)) + ); + + const wavelength = (ray.wavelength || Simulator.GREEN_WAVELENGTH) * mm_in_nm; + const interval = 1 / this.lineDensity; + const slit_width = interval * this.slitRatio; + + let newRays = []; + + // Incident and Normal Angles + let ray_angle = Math.atan2(ry, rx); + let normal_angle; + let dotProduct; + if (isFinite(center.x) && isFinite(center.y)) { + normal_angle = Math.atan2(incidentPoint.y - center.y, incidentPoint.x - center.x); // Initial normal pointing from center to the incident point + dotProduct = rx * (incidentPoint.x - center.x) + ry * (incidentPoint.y - center.y); // Check if the ray is hitting the inner surface + } else { // the mirror is plane - the 3 points are collinear + const dx = this.p2.x - this.p1.x; + const dy = this.p2.y - this.p1.y; + dotProduct = rx * dx + ry * dy; + normal_angle = Math.atan2(-dx, dy); // The normal is perpendicular to the mirror's direction vector + } + if (dotProduct < 0) { + normal_angle += Math.PI; // Reverse the normal direction for inner/outer surface reflection + } + + let incidence_angle = ray_angle - normal_angle; + + // Diffraction Orders + const m_min = Math.ceil((Math.sin(incidence_angle) - 1) * interval / wavelength); + const m_max = Math.floor((Math.sin(incidence_angle) + 1) * interval / wavelength); + + for (let m = m_min; m <= m_max; m++) { + const argument = (m * wavelength / interval) - Math.sin(incidence_angle); + if (argument < -1 || argument > 1) continue; // Skip invalid orders + + let diffracted_angle = Math.asin(argument); // computes: arcsin ((m*wavelength / interval) - sin(incidence_angle)) + // that's different to arcsin (sin(incidence_angle) - (m*wavelength / interval)) , which is the value necessary in the below calculation for the intensities with sinc + + // Rotate relative to the normal + const rot_angle = normal_angle + diffracted_angle; + const dx = Math.cos(rot_angle); + const dy = Math.sin(rot_angle); + + const diffracted_ray = geometry.line( + incidentPoint, + geometry.point(incidentPoint.x + dx, incidentPoint.y + dy) + ); + + // Calculate intensity + if (this.customBrightness) { + var intensity = this.brightnesses[m<=0 ? -2*m : 2*m-1] || 0; + } else { + // Treat the gratings as a blocker with slits + diffracted_angle = Math.asin(Math.sin(incidence_angle) - m * wavelength / interval); + var phase_diff = 2 * Math.PI * slit_width / wavelength * (Math.sin(incidence_angle) - Math.sin(diffracted_angle)) + var sinc_arg = (phase_diff == 0) ? 1 : Math.sin(phase_diff / 2) / (phase_diff / 2); + + // This formula may not be accurate when `diffracted_angle` is large. This is warned in the popover of the tool. + var intensity = slit_width * slit_width / (interval * interval) * Math.pow(sinc_arg, 2); + } + + if (intensity == 0) { + continue; + } + + diffracted_ray.wavelength = ray.wavelength; + diffracted_ray.brightness_s = ray.brightness_s * intensity; + diffracted_ray.brightness_p = ray.brightness_p * intensity; + + // There is currently no good way to make image detection work here. So just set gap to true to disable image detection for the diffracted rays. + diffracted_ray.gap = true; + + if (diffracted_ray.brightness_s + diffracted_ray.brightness_p > (this.scene.colorMode != 'default' ? 1e-6 : 0.01)) { + newRays.push(diffracted_ray); + } else { + truncation += diffracted_ray.brightness_s + diffracted_ray.brightness_p; + } + } + + return { + isAbsorbed: true, + newRays: newRays, + truncation: truncation + }; + } +}; + +export default ConcaveDiffractionGrating; \ No newline at end of file diff --git a/src/simulator/js/versionUpdate.js b/src/simulator/js/versionUpdate.js index 269464ad..ee9fc248 100644 --- a/src/simulator/js/versionUpdate.js +++ b/src/simulator/js/versionUpdate.js @@ -56,6 +56,11 @@ const obj_update = { "isDichroic": "filter", "isDichroicFilter": "invert" }, + "concavediffractiongrating": { + "type": "ConcaveDiffractionGrating", + "line_density": "lineDensity", + "slit_ratio": "slitRatio" + }, "parabolicmirror": { "type": "ParabolicMirror", "isDichroic": "filter", diff --git a/src/webpages/home.hbs b/src/webpages/home.hbs index f7e01469..f91e937e 100644 --- a/src/webpages/home.hbs +++ b/src/webpages/home.hbs @@ -153,6 +153,13 @@ {{t "main:tools.otherMirror.description"}} +
    + {{t +
    +

    {{t "main:meta.parentheses" main=(t "main:tools.categories.mirror") sub=(t "main:tools.otherMirror.title")}}

    + {{t "main:tools.otherMirror.description"}} +
    +
    {{t