/*
* 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 BaseGlass from '../BaseGlass.js';
import LineObjMixin from '../LineObjMixin.js';
import i18next from 'i18next';
import Simulator from '../../Simulator.js';
import geometry from '../../geometry.js';
import { evaluateLatex } from '../../equation.js';
/**
* Glass defined by a custom inequality.
*
* Tools -> Glass -> Custom equation
* @class
* @extends BaseGlass
* @memberof sceneObjs
* @property {Point} p1 - The point corresponding to (-1,0) in the coordinate system of the equation.
* @property {Point} p2 - The point corresponding to (1,0) in the coordinate system of the equation.
* @property {string} eqn1 - The equation of the surface with smaller y. The variable is x.
* @property {string} eqn2 - The equation of the surface with larger y. The variable is x.
* @property {number} refIndex - The refractive index of the glass, or the Cauchy coefficient A of the glass if "Simulate Colors" is on.
* @property {number} cauchyB - The Cauchy coefficient B of the glass if "Simulate Colors" is on, in micrometer squared.
* @property {Array<Point>} path - The points on the calculated curve.
* @property {number} tmp_i - The index of the point on the curve where the ray is incident.
*/
class CustomGlass extends LineObjMixin(BaseGlass) {
static type = 'CustomGlass';
static isOptical = true;
static supportsSurfaceMerging = true;
static serializableDefaults = {
p1: null,
p2: null,
eqn1: "0",
eqn2: "0.5\\cdot\\sqrt{1-x^2}",
refIndex: 1.5,
cauchyB: 0.004
};
populateObjBar(objBar) {
objBar.setTitle(i18next.t('main:tools.categories.glass'));
objBar.createEquation('', this.eqn1, function (obj, value) {
obj.eqn1 = value;
}, '<ul><li>' + i18next.t('simulator:sceneObjs.common.eqnInfo.constants') + '<br><code>pi e</code></li><li>' + i18next.t('simulator:sceneObjs.common.eqnInfo.operators') + '<br><code>+ - * / ^</code></li><li>' + i18next.t('simulator:sceneObjs.common.eqnInfo.functions') + '<br><code>sqrt sin cos tan sec csc cot sinh cosh tanh log exp arcsin arccos arctan arcsinh arccosh arctanh floor round ceil trunc sgn max min abs</code></li></ul>');
objBar.createEquation(' < y < ', this.eqn2, function (obj, value) {
obj.eqn2 = value;
});
super.populateObjBar(objBar);
}
draw(canvasRenderer, isAboveLight, isHovered) {
const ctx = canvasRenderer.ctx;
const ls = canvasRenderer.lengthScale;
if (this.p1.x == this.p2.x && this.p1.y == this.p2.y) {
ctx.fillStyle = 'rgb(128,128,128)';
ctx.fillRect(this.p1.x - 1.5 * ls, this.p1.y - 1.5 * ls, 3 * ls, 3 * ls);
return;
}
if (isAboveLight) {
if (this.path) {
this.drawGlass(canvasRenderer, isAboveLight, isHovered);
}
return;
}
var fns;
try {
fns = [evaluateLatex(this.eqn1), evaluateLatex(this.eqn2)];
} catch (e) {
delete this.path;
ctx.fillStyle = "red"
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);
this.error = e.toString();
return;
}
this.path = [{ x: this.p1.x, y: this.p1.y }];
for (var side = 0; side <= 1; side++) {
var p1 = (side == 0) ? this.p1 : this.p2;
var p2 = (side == 0) ? this.p2 : this.p1;
var p12d = geometry.distance(p1, p2);
var dir1 = [(p2.x - p1.x) / p12d, (p2.y - p1.y) / p12d];
var dir2 = [dir1[1], -dir1[0]];
var x0 = p12d / 2;
var i;
var lastError = "";
var hasPoints = false;
for (i = -0.1 * this.scene.lengthScale; i < p12d + 0.09 * this.scene.lengthScale; i += 0.1 * this.scene.lengthScale) {
var ix = i + 0.05 * this.scene.lengthScale;
if (ix < 0) ix = 0;
if (ix > p12d) ix = p12d;
var x = ix - x0;
var scaled_x = 2 * x / p12d;
var scaled_y;
try {
scaled_y = ((side == 0) ? 1 : (-1)) * fns[side]({ x: ((side == 0) ? scaled_x : (-scaled_x)) });
if (side == 1 && -scaled_y < fns[0]({ x: -scaled_x })) {
lastError = "Curve generation error: f(x) > g(x) at x = " + (-scaled_x);
}
var y = scaled_y * p12d * 0.5;
var pt = geometry.point(p1.x + dir1[0] * ix + dir2[0] * y, p1.y + dir1[1] * ix + dir2[1] * y);
this.path.push(pt);
hasPoints = true;
} catch (e) {
lastError = e;
}
}
if (!hasPoints || lastError.startsWith("Curve generation error:")) {
delete this.path;
ctx.fillStyle = "red"
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);
this.error = lastError.toString();
return;
}
}
if (isHovered) {
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);
}
this.drawGlass(canvasRenderer, isAboveLight, isHovered);
this.error = null;
}
checkMouseOver(mouse) {
let dragContext = {};
if (mouse.isOnPoint(this.p1) && geometry.distanceSquared(mouse.pos, this.p1) <= geometry.distanceSquared(mouse.pos, this.p2)) {
dragContext.part = 1;
dragContext.targetPoint = geometry.point(this.p1.x, this.p1.y);
return dragContext;
}
if (mouse.isOnPoint(this.p2)) {
dragContext.part = 2;
dragContext.targetPoint = geometry.point(this.p2.x, this.p2.y);
return dragContext;
}
if (!this.path) return false;
for (let i = 0; i < this.path.length - 1; i++) {
if (mouse.isOnSegment(geometry.line(this.path[i], this.path[(i+1)%this.path.length]))) {
const mousePos = mouse.getPosSnappedToGrid();
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;
}
}
}
checkRayIntersects(ray) {
if (!this.path) return;
if (this.refIndex <= 0) return;
var s_lensq = Infinity;
var s_lensq_temp;
var s_point = null;
var s_point_temp = null;
var s_point_index = -1;
var rp_exist = [];
var rp_lensq = [];
var rp_temp;
var p1;
var p2;
var p3;
var center;
var r;
for (var i = 0; i < this.path.length; i++) {
s_point_temp = null;
//Line segment i->i+1
var rp_temp = geometry.linesIntersection(geometry.line(ray.p1, ray.p2), geometry.line(this.path[i % this.path.length], this.path[(i + 1) % this.path.length]));
if (geometry.intersectionIsOnSegment(rp_temp, geometry.line(this.path[i % this.path.length], this.path[(i + 1) % this.path.length])) && geometry.intersectionIsOnRay(rp_temp, ray) && geometry.distanceSquared(ray.p1, rp_temp) > Simulator.MIN_RAY_SEGMENT_LENGTH_SQUARED * this.scene.lengthScale * this.scene.lengthScale) {
s_lensq_temp = geometry.distanceSquared(ray.p1, rp_temp);
s_point_temp = rp_temp;
}
if (s_point_temp) {
if (s_point && geometry.distanceSquared(s_point_temp, s_point) < Simulator.MIN_RAY_SEGMENT_LENGTH_SQUARED * this.scene.lengthScale * this.scene.lengthScale && s_point_index != i - 1) {
// The ray shots on a point where the upper and the lower surfaces overlap.
return;
} else if (s_lensq_temp < s_lensq) {
s_lensq = s_lensq_temp;
s_point = s_point_temp;
s_point_index = i;
}
}
}
if (s_point) {
this.tmp_i = s_point_index;
return s_point;
}
}
onRayIncident(ray, rayIndex, incidentPoint, surfaceMergingObjs) {
var incidentData = this.getIncidentData(ray);
var incidentType = incidentData.incidentType;
if (incidentType == 1) {
// From inside to outside
var n1 = this.getRefIndexAt(incidentPoint, ray);
} else if (incidentType == -1) {
// From outside to inside
var n1 = 1 / this.getRefIndexAt(incidentPoint, ray);
} else if (incidentType == 0) {
// Equivalent to not intersecting with the object (e.g. two interfaces overlap)
var n1 = 1;
} else {
// The situation that may cause bugs (e.g. incident on an edge point)
// To prevent shooting the ray to a wrong direction, absorb the ray
return {
isAbsorbed: true
};
}
return this.refract(ray, rayIndex, incidentPoint, incidentData.normal, n1, surfaceMergingObjs, ray.bodyMergingthis);
}
getIncidentData(ray) {
var i = this.tmp_i;
var pts = this.path;
var s_point = geometry.linesIntersection(geometry.line(ray.p1, ray.p2), geometry.line(this.path[i % this.path.length], this.path[(i + 1) % this.path.length]));
var incidentPoint = s_point;
var s_lensq = geometry.distanceSquared(ray.p1, s_point);
var rdots = (ray.p2.x - ray.p1.x) * (this.path[(i + 1) % this.path.length].x - this.path[i % this.path.length].x) + (ray.p2.y - ray.p1.y) * (this.path[(i + 1) % this.path.length].y - this.path[i % this.path.length].y);
var rcrosss = (ray.p2.x - ray.p1.x) * (this.path[(i + 1) % this.path.length].y - this.path[i % this.path.length].y) - (ray.p2.y - ray.p1.y) * (this.path[(i + 1) % this.path.length].x - this.path[i % this.path.length].x);
var ssq = (this.path[(i + 1) % this.path.length].x - this.path[i % this.path.length].x) * (this.path[(i + 1) % this.path.length].x - this.path[i % this.path.length].x) + (this.path[(i + 1) % this.path.length].y - this.path[i % this.path.length].y) * (this.path[(i + 1) % this.path.length].y - this.path[i % this.path.length].y);
var normal_x = rdots * (this.path[(i + 1) % this.path.length].x - this.path[i % this.path.length].x) - ssq * (ray.p2.x - ray.p1.x);
var normal_y = rdots * (this.path[(i + 1) % this.path.length].y - this.path[i % this.path.length].y) - ssq * (ray.p2.y - ray.p1.y);
if (rcrosss < 0) {
var incidentType = 1; // From inside to outside
} else {
var incidentType = -1; // From outside to inside
}
// Use a simple trick to smooth out the normal vector so that image detection works.
// However, a more proper numerical algorithm from the beginning (especially to handle singularities) is still desired.
var seg = geometry.line(pts[i % pts.length], pts[(i + 1) % pts.length]);
var rx = ray.p1.x - incidentPoint.x;
var ry = ray.p1.y - incidentPoint.y;
var mx = seg.p2.x - seg.p1.x;
var my = seg.p2.y - seg.p1.y;
var frac;
if (Math.abs(mx) > Math.abs(my)) {
frac = (incidentPoint.x - seg.p1.x) / mx;
} else {
frac = (incidentPoint.y - seg.p1.y) / my;
}
var segA;
if (frac < 0.5) {
segA = geometry.line(pts[(i - 1 + pts.length) % pts.length], pts[i % pts.length]);
} else {
segA = geometry.line(pts[(i + 1) % pts.length], pts[(i + 2) % pts.length]);
}
var rdotsA = (ray.p2.x - ray.p1.x) * (segA.p2.x - segA.p1.x) + (ray.p2.y - ray.p1.y) * (segA.p2.y - segA.p1.y);
var ssqA = (segA.p2.x - segA.p1.x) * (segA.p2.x - segA.p1.x) + (segA.p2.y - segA.p1.y) * (segA.p2.y - segA.p1.y);
var normal_xA = rdotsA * (segA.p2.x - segA.p1.x) - ssqA * (ray.p2.x - ray.p1.x);
var normal_yA = rdotsA * (segA.p2.y - segA.p1.y) - ssqA * (ray.p2.y - ray.p1.y);
var normal_xFinal;
var normal_yFinal;
if (frac < 0.5) {
normal_xFinal = normal_x * (0.5 + frac) + normal_xA * (0.5 - frac);
normal_yFinal = normal_y * (0.5 + frac) + normal_yA * (0.5 - frac);
} else {
normal_xFinal = normal_xA * (frac - 0.5) + normal_x * (1.5 - frac);
normal_yFinal = normal_yA * (frac - 0.5) + normal_y * (1.5 - frac);
}
return { s_point: s_point, normal: { x: normal_xFinal, y: normal_yFinal }, incidentType: incidentType };
}
/* Utility methods */
drawGlass(canvasRenderer, isAboveLight, isHovered) {
const ctx = canvasRenderer.ctx;
ctx.beginPath();
ctx.moveTo(this.path[0].x, this.path[0].y);
for (var i = 1; i < this.path.length; i++) {
ctx.lineTo(this.path[i].x, this.path[i].y);
}
this.fillGlass(canvasRenderer, isAboveLight, isHovered);
}
};
export default CustomGlass;