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"}}
+