Skip to content

Commit c9ceb62

Browse files
committed
Merge branch 'master' of https://github.com/briidee3/ray-optics-metamaterials into weblate-weblate-ray-optics-simulation-main
2 parents 12640c8 + 0a3ea99 commit c9ceb62

13 files changed

Lines changed: 1958 additions & 4 deletions

File tree

ai-tools/knowledge/objects.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,10 @@ A glass with the shape of piecewise defined parametric curve. The `pieces` prope
299299

300300
In user provided scene created using the visual editor, this object may appear outside a module, and a similar object with `CustomGlass` type may also be used instead whose equation is defined in a relative coordinate system where `p1` and `p2` are $(-1,0)$ and $(1,0)$, respectively. However, when you write JSON code by yourself, please always use the `ParamGlass` type and put it inside a module.
301301

302+
### Cubic Bezier Glass
303+
304+
A glass with a shape defined by boundaries composed of cubic Bezier curves. The `points` property is an array of arrays of 2D points with length 3. Point 1 is the first anchor point, points 2 and 3 are the control points, and point 1 of the next curve in the series composing a loop of curves which represents a lens is the second control point of the curve.
305+
302306
## Ideal Objects
303307

304308
### Ideal Lens

locales/en/main.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@
119119
"title": "Parametric Curve",
120120
"description": "Glass with the shape defined by custom parametric equations."
121121
},
122+
"CurveGlass": {
123+
"title": "Cubic Bezier curves",
124+
"description": "Glass bounded by cubic Bezier curves.",
125+
"instruction": "Click to create a segment, click on the starting point when finish drawing. Click and drag the boxes to manipulate the control points, changing the curve."
126+
},
122127
"CircleGrinGlass": {
123128
"title": "GRIN Circle",
124129
"description": "Glass with a custom refractive index function n(x,y) and a circular boundary."
@@ -133,6 +138,11 @@
133138
"title": "GRIN Parametric Curve",
134139
"description": "Gradient-index glass with boundary defined by custom parametric equations."
135140
},
141+
"CurveGrinGlass": {
142+
"title": "GRIN Cubic Bezier curves",
143+
"description": "Gradient-index glass bounded by cubic Bezier curves.",
144+
"instruction": "Click to create a segment, click on the starting point when finish drawing. Click and drag the boxes to manipulate the control points, changing the curve."
145+
},
136146
"otherGrinGlass": {
137147
"title": "Gradient-index glass",
138148
"description": "Material with a custom refractive index function n(x,y)."

locales/en/simulator.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,14 @@
210210
"notPositivelyOriented": "Curve is not positively oriented."
211211
}
212212
},
213+
"CurveGlass": {
214+
"refIndex": "Refractive index",
215+
"refIndexInfo": {
216+
"relative": "Relative if inside another glass",
217+
"effective": "If this object is to be simulated with (absolute) refractive index n₁, but is placed inside another glass with refractive index n₀, then you should set the refractive index to n₁/n₀ here. In more complicated cases that several glasses overlap together, the effective refractive index of the overlapping region equals the product of the refractive indices of individual ones."
218+
},
219+
"cauchyCoeff": "Cauchy coefficient"
220+
},
213221
"BaseGrinGlass": {
214222
"refIndexFnInfo": {
215223
"lambda": "To simulate chromatic dispersion, use parameter {{lambda}} for the vacuum wavelength in nanometers.",
@@ -248,6 +256,12 @@
248256
"functions": "This numerical tolerance is used in the function 'isOnBoundary' inside the 'ParamCurveObjMixin.js' file."
249257
}
250258
},
259+
"CurveGrinGlass": {
260+
"epsInfo": {
261+
"units": "Units - [length].",
262+
"functions": "This numerical tolerance is used in the function 'isOnBoundary' inside the 'ParamCurveObjMixin.js' file."
263+
}
264+
},
251265
"Aperture": {
252266
"diameter": "Diameter"
253267
},

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"@popperjs/core": "2.11.8",
1111
"@vue/runtime-dom": "^3.5.13",
1212
"ace-builds": "^1.36.0",
13+
"bezier-js": "^6.1.4",
1314
"bootstrap": "5.1.3",
1415
"canvas2svg": "github:gliffy/canvas2svg",
1516
"copy-webpack-plugin": "^12.0.2",

src/app/components/toolbar/ToolsList.vue

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,13 @@
178178
:verticalOffset="0"
179179
:layout="layout"
180180
/>
181+
<ToolItem
182+
id="CurveGlass"
183+
:title="$t('main:tools.CurveGlass.title') + '<sup>Beta</sup>'"
184+
:popoverContent="$t('main:meta.parentheses', {main: $t('main:tools.CurveGlass.description'), sub: $t('main:tools.CurveGlass.instruction')})"
185+
:verticalOffset="0"
186+
:layout="layout"
187+
/>
181188
<ToolItem
182189
id="CircleGrinGlass"
183190
:title="$t('main:tools.CircleGrinGlass.title')"
@@ -199,6 +206,13 @@
199206
:verticalOffset="0"
200207
:layout="layout"
201208
/>
209+
<ToolItem
210+
id="CurveGrinGlass"
211+
:title="$t('main:tools.CurveGrinGlass.title') + '<sup>Beta</sup>'"
212+
:popoverContent="$t('main:meta.parentheses', {main: $t('main:tools.CurveGrinGlass.description'), sub: $t('main:tools.CurveGrinGlass.instruction')})"
213+
:verticalOffset="0"
214+
:layout="layout"
215+
/>
202216
</MoreToolsCollapsible>
203217
</ToolsCategory>
204218

src/core/Mouse.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import geometry from './geometry.js';
18+
import Bezier from 'bezier-js';
1819

1920
/**
2021
* The Mouse class encapsulates the state of the mouse within the scene.
@@ -88,6 +89,15 @@ class Mouse {
8889
return d_per < this.getClickExtent() * this.getClickExtent() && d_par >= 0 && d_par <= geometry.segmentLengthSquared(segment);
8990
}
9091

92+
/**
93+
* Determines if the mouse is currently over a given curve.
94+
* @param {Bezier} curve - The curve to check against the mouse position.
95+
* @returns {boolean} True if the mouse is on the given curve, false otherwise.
96+
*/
97+
isOnCurve(curve) {
98+
return curve.project(this.pos).d ** 2 < this.getClickExtent(true) * this.getClickExtent(true);
99+
}
100+
91101
/**
92102
* Determines if the mouse is currently over a given line.
93103
* @param {geometry.line} line - The line to check against the mouse position.

src/core/geometry.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,16 @@ const geometry = {
137137
return ret;
138138
},
139139

140+
/**
141+
* Calculate the intersections of a line and a curve.
142+
* @param {Line} l1
143+
* @param {Bezier} c1
144+
* @return {Point[]}
145+
*/
146+
lineCurveIntersections: function (l1, c1) {
147+
return c1.intersects(l1);
148+
},
149+
140150

141151
/**
142152
* Test if a point on the extension of a ray is actually on the ray.
@@ -159,6 +169,60 @@ const geometry = {
159169
return (p1.x - s1.p1.x) * (s1.p2.x - s1.p1.x) + (p1.y - s1.p1.y) * (s1.p2.y - s1.p1.y) >= 0 && (p1.x - s1.p2.x) * (s1.p1.x - s1.p2.x) + (p1.y - s1.p2.y) * (s1.p1.y - s1.p2.y) >= 0;
160170
},
161171

172+
/**
173+
* Test if a point on the extension of a curve is actually on the curve.
174+
* @param {Point} p1
175+
* @param {Bezier} curve
176+
* @return {Boolean}
177+
*/
178+
intersectionIsOnCurve: function (p1, curve, threshold) {
179+
var d_proj = curve.project(geometry.point(p1.x, p1.y)).d;
180+
181+
return Math.pow(d_proj, 2) < threshold;
182+
},
183+
184+
/**
185+
* Scale the ray based on the bounding box of the curve.
186+
* @param {Line} r1
187+
* @param {Bezier} curve
188+
* @return {Line} - Returns the vector pointing from r1.p1 to the farthest point on the curve's bounding box.
189+
*/
190+
scaleRayForCurve: function (r1, curve) {
191+
var bbox = curve.bbox();
192+
193+
// Offset each line from 0,0 by r1.p1
194+
bbox.x.min -= r1.p1.x;
195+
bbox.x.max -= r1.p1.x;
196+
bbox.y.min -= r1.p1.y;
197+
bbox.y.max -= r1.p1.y;
198+
199+
// Get vector (as a point) pointing from r1.p1 to r1.p2
200+
var v1 = geometry.point(r1.p2.x - r1.p1.x, r1.p2.y - r1.p1.y);
201+
202+
// Figure out which bounding box corner is farthest from r1.p1 based on what quadrant v1 is in after offsetting by p1
203+
var farthest = { x: Infinity, y: Infinity };
204+
if (Math.abs(bbox.x.min) > Math.abs(bbox.x.max)) {
205+
farthest.x = bbox.x.min;
206+
} else {
207+
farthest.x = bbox.x.max;
208+
}
209+
if (Math.abs(bbox.y.min) > Math.abs(bbox.y.max)) {
210+
farthest.y = bbox.y.min;
211+
} else {
212+
farthest.y = bbox.y.max;
213+
}
214+
215+
// Get distance between p1 and farthest point
216+
var dist = Math.sqrt(farthest.x ** 2 + farthest.y ** 2);
217+
218+
// Normalize v1 then scale it by dist
219+
var len_v1 = Math.sqrt(v1.x ** 2 + v1.y ** 2);
220+
v1.x = (v1.x / len_v1) * dist * 1.001;
221+
v1.y = (v1.y / len_v1) * dist * 1.001;
222+
223+
return geometry.line(r1.p1, geometry.point(v1.x + r1.p1.x, v1.y + r1.p1.y));
224+
},
225+
162226
/**
163227
* Calculate the length of a line segment.
164228
* @param {Line} seg
@@ -252,6 +316,30 @@ const geometry = {
252316
var dx = l1.p2.x - l1.p1.x;
253317
var dy = l1.p2.y - l1.p1.y;
254318
return geometry.line(p1, geometry.point(p1.x + dx, p1.y + dy));
319+
},
320+
321+
/**
322+
* Normalize the given point as if it were a vector.
323+
* @param {Point} p1
324+
* @return {Point}
325+
*/
326+
normalizeVec: function(p1) {
327+
var len = geometry.distance(geometry.point(0, 0), p1);
328+
329+
return geometry.point(p1.x / len, p1.y / len);
330+
},
331+
332+
/**
333+
* Rotate the given point as if it were a vector by the given angle in radians.
334+
* @param {Point} p1
335+
* @return {Point}
336+
*/
337+
rotateVec: function(p1, angle) {
338+
// Rotate by the rotation matrix
339+
return {
340+
x: p1.x * Math.cos(angle) - p1.y * Math.sin(angle),
341+
y: p1.x * Math.sin(angle) + p1.y * Math.cos(angle)
342+
}
255343
}
256344
};
257345

src/core/sceneObjs.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,13 @@ export const CircleGlass = require('./sceneObjs/glass/CircleGlass.js').default;
4343
export const Glass = require('./sceneObjs/glass/Glass.js').default;
4444
export const CustomGlass = require('./sceneObjs/glass/CustomGlass.js').default;
4545
export const ParamGlass = require('./sceneObjs/glass/ParamGlass.js').default;
46+
export const CurveGlass = require('./sceneObjs/glass/CurveGlass.js').default;
4647
export const IdealLens = require('./sceneObjs/glass/IdealLens.js').default;
4748
export const SphericalLens = require('./sceneObjs/glass/SphericalLens.js').default;
4849
export const CircleGrinGlass = require('./sceneObjs/glass/CircleGrinGlass.js').default;
4950
export const GrinGlass = require('./sceneObjs/glass/GrinGlass.js').default;
5051
export const ParamGrinGlass = require('./sceneObjs/glass/ParamGrinGlass.js').default;
52+
export const CurveGrinGlass = require('./sceneObjs/glass/CurveGrinGlass.js').default;
5153
export const Blocker = require('./sceneObjs/blocker/Blocker.js').default;
5254
export const CircleBlocker = require('./sceneObjs/blocker/CircleBlocker.js').default;
5355
export const Aperture = require('./sceneObjs/blocker/Aperture.js').default;

src/core/sceneObjs/BaseGlass.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,12 @@ class BaseGlass extends BaseSceneObj {
7171
ctx.globalAlpha = 1;
7272
return;
7373
}
74-
if (n >= 1) {
75-
var alpha = Math.log(n) / Math.log(1.5) * 0.2;
74+
const mod_neg = n <= -1 ? -1 : 1;
75+
if (n >= 1 || mod_neg === -1) {
76+
var alpha = Math.log(n * mod_neg) / Math.log(1.5) * 0.2;
7677
if (allowColorSubtraction) {
7778
ctx.globalCompositeOperation = 'lighter';
78-
ctx.fillStyle = "rgb(" + Math.round(alpha * this.scene.theme.glass.color.r * 255) + "," + Math.round(alpha * this.scene.theme.glass.color.g * 255) + "," + Math.round(alpha * this.scene.theme.glass.color.b * 255) + ")";
79+
ctx.fillStyle = mod_neg < 0 ? "rgb(" + Math.round(alpha * this.scene.theme.glass.color.r * 0.75 * 255) + "," + Math.round(alpha * this.scene.theme.glass.color.g * 255) + "," + Math.round(alpha * this.scene.theme.glass.color.b * 0.75 * 255) + ")" : "rgb(" + Math.round(alpha * this.scene.theme.glass.color.r * 255) + "," + Math.round(alpha * this.scene.theme.glass.color.g * 255) + "," + Math.round(alpha * this.scene.theme.glass.color.b * 255) + ")";
7980
} else {
8081
ctx.globalAlpha = alpha;
8182
ctx.fillStyle = "rgb(" + Math.round(this.scene.theme.glass.color.r * 255) + "," + Math.round(this.scene.theme.glass.color.g * 255) + "," + Math.round(this.scene.theme.glass.color.b * 255) + ")";
@@ -85,7 +86,7 @@ class BaseGlass extends BaseSceneObj {
8586
ctx.globalCompositeOperation = 'source-over';
8687

8788
} else {
88-
var alpha = Math.log(1 / n) / Math.log(1.5) * 0.2;
89+
var alpha = Math.log(1 / (n * mod_neg)) / Math.log(1.5) * 0.2;
8990
if (allowColorSubtraction) {
9091
// Subtract the gray color.
9192
// Use a trick to make the color become red (indicating nonphysical) if the total refractive index is lower than one.
@@ -167,6 +168,13 @@ class BaseGlass extends BaseSceneObj {
167168
}
168169
}
169170

171+
// Negative modifier for working with negative indices of refraction
172+
var mod_neg = false;
173+
if (n1 < 0) {
174+
n1 = -n1; // Flip n1 for compatibility with the following equations
175+
mod_neg = true;
176+
}
177+
170178
var normal_len = Math.sqrt(normal.x * normal.x + normal.y * normal.y);
171179
var normal_x = normal.x / normal_len;
172180
var normal_y = normal.y / normal_len;
@@ -225,6 +233,16 @@ class BaseGlass extends BaseSceneObj {
225233
}
226234

227235
// Handle the refracted ray
236+
237+
// Handle negative refractive index
238+
if (mod_neg) {
239+
// Restore n1
240+
n1 = -n1;
241+
242+
// Flip sign of cos2
243+
cos2 = Math.cos(2 * Math.PI - Math.acos(cos2));
244+
}
245+
228246
ray.p1 = incidentPoint;
229247
ray.p2 = geometry.point(incidentPoint.x + n1 * ray_x + (n1 * cos1 - cos2) * normal_x, incidentPoint.y + n1 * ray_y + (n1 * cos1 - cos2) * normal_y);
230248
ray.brightness_s = ray.brightness_s * (1 - R_s);

0 commit comments

Comments
 (0)