/*
* 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 BaseGrinGlass from '../BaseGrinGlass.js';
import ParamCurveObjMixin from '../ParamCurveObjMixin.js';
import i18next from 'i18next';
import geometry from '../../geometry.js';
/**
* Gradient-index glass with shape defined by parametric curve pieces.
*
* Tools -> Glass -> Parametric GRIN
* @class
* @extends BaseGrinGlass
* @memberof sceneObjs
* @property {Point} origin - The origin point of the parametric coordinate system.
* @property {Array} pieces - Array of parametric curve pieces, each with eqnX, eqnY, tMin, tMax, tStep.
* @property {string} refIndexFn - The refractive index function in x and y in LaTeX format.
* @property {number} stepSize - The step size for the ray trajectory equation.
* @property {number} intersectTol - The epsilon for the intersection calculations.
* @property {Array<Point>} path - The points on the calculated curve.
* @property {string} warning - Warning message if the curve is not closed or not positively oriented.
*/
class ParamGrinGlass extends ParamCurveObjMixin(BaseGrinGlass) {
static type = 'ParamGrinGlass';
static isOptical = true;
static mergesWithGlass = true;
static serializableDefaults = {
origin: { x: 0, y: 0 },
pieces: [
{
eqnX: "50\\cdot\\cos\\left(t\\right)",
eqnY: "50\\cdot\\sin\\left(t\\right)",
tMin: 0,
tMax: 2 * Math.PI,
tStep: 0.01
}
],
refIndexFn: '1+e^{-\\frac{x^2+y^2}{50^2}}',
stepSize: 1,
intersectTol: 1e-3
};
populateObjBar(objBar) {
objBar.setTitle(i18next.t('main:tools.ParamGrinGlass.title'));
objBar.createInfoBox('<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><li>' + i18next.t('simulator:sceneObjs.ParamGlass.eqnInfo.closedAndPositivelyOriented') + '</li><li>' + i18next.t('simulator:sceneObjs.common.eqnInfo.module') + '</li></ul>');
// Add parametric curve controls
this.populateObjBarShape(objBar);
// Add GRIN glass controls from BaseGrinGlass
super.populateObjBar(objBar);
}
draw(canvasRenderer, isAboveLight, isHovered) {
const ctx = canvasRenderer.ctx;
const ls = canvasRenderer.lengthScale;
// Initialize path if needed and validate curve
if (!this.path) {
if (!this.initPath()) {
// If initialization failed, draw error indicator at origin
ctx.fillStyle = "red";
ctx.fillRect(this.origin.x - 1.5 * ls, this.origin.y - 1.5 * ls, 3 * ls, 3 * ls);
return;
}
this.validateCurve();
}
// Check if curve is empty or fully degenerate
let isEmptyOrDegenerate = false;
if (this.path.length < 2) {
isEmptyOrDegenerate = true;
} else {
// Check if all points are identical (fully degenerate curve)
const firstPoint = this.path[0];
let allPointsIdentical = true;
for (let i = 1; i < this.path.length; i++) {
if (Math.abs(this.path[i].x - firstPoint.x) > 1e-10 ||
Math.abs(this.path[i].y - firstPoint.y) > 1e-10) {
allPointsIdentical = false;
break;
}
}
isEmptyOrDegenerate = allPointsIdentical;
}
if (isEmptyOrDegenerate) {
// Empty or fully degenerate curve - always draw origin square
ctx.fillStyle = isHovered ? 'rgb(255,0,0)' : 'rgb(128,128,128)';
ctx.fillRect(this.origin.x - 1.5 * ls, this.origin.y - 1.5 * ls, 3 * ls, 3 * ls);
return;
}
if (isAboveLight) {
if (this.path && this.path.length > 2) {
this.drawPath(canvasRenderer);
this.fillGlass(canvasRenderer, isAboveLight, isHovered);
}
return;
}
// Draw the parametric curve GRIN glass
if (this.path && this.path.length > 2) {
this.drawPath(canvasRenderer);
this.fillGlass(canvasRenderer, isAboveLight, isHovered);
}
}
move(diffX, diffY) {
super.move(diffX, diffY);
this.initFns();
return true;
}
rotate(angle, center) {
super.rotate(angle, center);
this.initFns();
return false;
}
scale(scale, center) {
super.scale(scale, center);
this.initFns();
return false;
}
onConstructMouseDown(mouse, ctrl, shift) {
super.onConstructMouseDown(mouse, ctrl, shift);
this.initFns();
}
onDrag(mouse, dragContext, ctrl, shift) {
super.onDrag(mouse, dragContext, ctrl, shift);
this.initFns();
}
checkRayIntersects(ray) {
console.log('checkRayIntersects', this.fn_p);
if (!this.fn_p) {
console.log('initFns');
this.initFns();
}
// If ray starts inside the glass or on boundary, step forward
if (this.isInsideGlass(ray.p1) || this.isOnBoundary(ray.p1)) {
let len = geometry.distance(ray.p1, ray.p2);
let x = ray.p1.x + (this.stepSize / len) * (ray.p2.x - ray.p1.x);
let y = ray.p1.y + (this.stepSize / len) * (ray.p2.y - ray.p1.y);
const intersection_point = geometry.point(x, y);
if (this.isInsideGlass(intersection_point)) {
return intersection_point;
}
}
var incidentData = this.getIncidentData(ray);
if (isNaN(incidentData.incidentType) || !incidentData.s_point) {
if (isNaN(incidentData.incidentType)) {
return {
isAbsorbed: true,
isUndefinedBehavior: true
};
}
return null;
}
return incidentData.s_point;
}
onRayIncident(ray, rayIndex, incidentPoint, surfaceMergingObjs) {
if (!this.fn_p) {
// This means that some error has occurred earlier in parsing the equation.
return {
isAbsorbed: true
};
}
try {
this.error = null;
// Check if ray is hitting the boundary from outside or inside
if ((this.isInsideGlass(ray.p1) || this.isOutsideGlass(ray.p1)) && this.isOnBoundary(incidentPoint)) {
let r_bodyMerging_obj = ray.bodyMergingObj; // Save the current bodyMergingObj
var incidentData = this.getIncidentData(ray);
var incidentType = incidentData.incidentType;
if (incidentType === 1) {
// From inside to outside
var n1 = this.getRefIndexAt(incidentPoint, ray);
var normal = incidentData.normal;
this.onRayExit(ray);
} else if (incidentType === -1) {
// From outside to inside
var n1 = 1 / this.getRefIndexAt(incidentPoint, ray);
var normal = incidentData.normal;
this.onRayEnter(ray);
} else {
// The situation that may cause bugs (e.g. incident on an edge point)
return {
isAbsorbed: true,
isUndefinedBehavior: true
};
}
return this.refract(ray, rayIndex, incidentPoint, normal, n1, surfaceMergingObjs, r_bodyMerging_obj);
} else {
// Ray is propagating inside the GRIN glass
if (ray.bodyMergingObj === undefined) {
ray.bodyMergingObj = this.initRefIndex(ray); // Initialize the bodyMerging object of the ray
}
const next_point = this.step(ray.p1, incidentPoint, ray);
ray.p1 = incidentPoint;
ray.p2 = next_point;
}
} catch (e) {
this.error = e.toString();
return {
isAbsorbed: true
};
}
}
getIncidentData(ray) {
// Get all intersections from the mixin
const intersections = this.getRayIntersections(ray);
if (intersections.length === 0) {
return {
s_point: null,
normal: { x: NaN, y: NaN },
incidentType: 0
};
}
// Find the nearest intersection (simple approach for GRIN glass)
let nearestIntersection = null;
let nearestDistance = Infinity;
for (const intersection of intersections) {
// Check for undefined behavior (NaN incidentType)
if (isNaN(intersection.incidentType)) {
return {
s_point: intersection.s_point,
normal: intersection.normal,
incidentType: NaN
};
}
const distance = geometry.distanceSquared(ray.p1, intersection.s_point);
if (distance < nearestDistance) {
nearestDistance = distance;
nearestIntersection = intersection;
}
}
if (nearestIntersection) {
return {
s_point: nearestIntersection.s_point,
normal: nearestIntersection.normal,
incidentType: nearestIntersection.incidentType
};
}
// No valid intersection found
return {
s_point: null,
normal: { x: NaN, y: NaN },
incidentType: 0
};
}
getIncidentType(ray) {
return this.getIncidentData(ray).incidentType;
}
isOutsideGlass(point) {
return super.isOutside(point);
}
isInsideGlass(point) {
return super.isInside(point);
}
isOnBoundary(point) {
return super.isOnBoundary(point);
}
/**
* Validate that the curve is closed and positively oriented.
* Sets this.warning if validation fails.
*/
validateCurve() {
if (!this.isClosed()) {
this.warning = i18next.t('simulator:sceneObjs.ParamGlass.warning.notClosed');
return;
}
if (!this.isPositivelyOriented()) {
this.warning = i18next.t('simulator:sceneObjs.ParamGlass.warning.notPositivelyOriented');
return;
}
this.warning = null;
}
/**
* Override initPath to validate curve after generation.
*/
initPath() {
const result = super.initPath();
if (result) {
this.validateCurve();
}
return result;
}
}
export default ParamGrinGlass;