/*
* 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';
import { equationValueForListDisplay, latexToMathJS } from '../../propertyUtils/equationConversion.js';
import escapeHtml from 'escape-html';
import { Bezier } from 'bezier-js';
import * as math from 'mathjs';
import { curveTypePropertyInfoHtml } from '../ParamCurveObjMixin.js';
function compileEquationDerivative(eqnLatex) {
const p = latexToMathJS(eqnLatex);
const p_der = math.derivative(p, 'x').toString();
const p_der_tex = math.parse(p_der).toTex()
.replaceAll('{+', '{')
.replaceAll('\\mathrm{x}', 'x');
return evaluateLatex(p_der_tex);
}
/**
* 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 mergesWithGlass = true;
static serializableDefaults = {
p1: null,
p2: null,
eqn1: "0",
eqn2: "0.5\\cdot\\sqrt{1-x^2}",
curveStepSize: 0.1000001,
curveType: 'smoothNormal',
refIndex: 1.5,
cauchyB: 0.004,
partialReflect: true
};
static getDescription(objData, scene, detailed = false) {
const base = i18next.t('main:tools.categories.glass');
if (!detailed) {
return i18next.t('main:meta.parentheses', { main: base, sub: i18next.t('main:tools.CustomGlass.title') });
}
const eqn1 = objData?.eqn1 ?? '';
const eqn2 = objData?.eqn2 ?? '';
const parts = [eqn1, eqn2].filter(Boolean).map((v) => escapeHtml(equationValueForListDisplay(v)));
return parts.length ? i18next.t('main:meta.colon', { name: base, value: '<span style="font-family: monospace">' + parts.join(', ') + '</span>' }) : base;
}
static getPropertySchema(objData, scene) {
const info = '<ul><li>' + i18next.t('simulator:sceneObjs.common.eqnInfo.mathjs') + '<br><code>+ - * / ^ sqrt sin cos tan sec csc cot sinh cosh tanh log exp asin acos atan asinh acosh atanh floor round ceil fix max min abs sign</code></li><li>' + i18next.t('simulator:sceneObjs.common.eqnInfo.customFunctions') + '</li></ul>';
const curveTypeOptions = {
polygonal: i18next.t('simulator:sceneObjs.ParamCurveObjMixin.curveTypes.polygonal'),
smoothNormal: i18next.t('simulator:sceneObjs.ParamCurveObjMixin.curveTypes.smoothNormal'),
cubicBezier: i18next.t('simulator:sceneObjs.ParamCurveObjMixin.curveTypes.cubicBezier'),
};
return [
...super.getPropertySchema(objData, scene),
{ key: 'eqn1', type: 'equation', label: 'f(x)', variables: ['x'], info: info },
{ key: 'eqn2', type: 'equation', label: 'g(x)', variables: ['x'], info: info },
{
key: 'curveType',
type: 'dropdown',
label: i18next.t('simulator:sceneObjs.ParamCurveObjMixin.curveType'),
options: curveTypeOptions,
info: curveTypePropertyInfoHtml(),
},
{
key: 'curveStepSize',
type: 'number',
label: i18next.t('simulator:sceneObjs.common.curveStepSize') + ' (px)' + ' <sup class="beta-label-sup">Beta</sup>',
info: '<p>' + i18next.t('simulator:sceneObjs.common.eqnInfo.curveStepSize') + '</p>',
},
];
}
populateObjBar(objBar) {
objBar.setTitle(i18next.t('main:tools.categories.glass'));
objBar.createEquation('', this.eqn1, function (obj, value) {
obj.eqn1 = value;
// Invalidate path when equation changes
delete obj.path;
}, '<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</code> (' + i18next.t('simulator:sceneObjs.common.eqnInfo.naturalLog') + ') <code>exp arcsin arccos arctan arcsinh arccosh arctanh floor round ceil trunc sgn max min abs</code></li><li>' + i18next.t('simulator:sceneObjs.common.eqnInfo.module') + '</li></ul>');
objBar.createEquation(' < y < ', this.eqn2, function (obj, value) {
obj.eqn2 = value;
// Invalidate path when equation changes
delete obj.path;
});
if (objBar.showAdvanced(!this.arePropertiesDefault(['curveType']))) {
const curveTypeOptions = {
polygonal: i18next.t('simulator:sceneObjs.ParamCurveObjMixin.curveTypes.polygonal'),
smoothNormal: i18next.t('simulator:sceneObjs.ParamCurveObjMixin.curveTypes.smoothNormal'),
cubicBezier: i18next.t('simulator:sceneObjs.ParamCurveObjMixin.curveTypes.cubicBezier'),
};
objBar.createDropdown(i18next.t('simulator:sceneObjs.ParamCurveObjMixin.curveType'), this.curveType, curveTypeOptions, function (obj, value) {
obj.curveType = value;
delete obj.path;
delete obj.bezierSegments;
delete obj.bezierSegmentLinearFlags;
}, curveTypePropertyInfoHtml(), true);
}
if (objBar.showAdvanced(!this.arePropertiesDefault(['curveStepSize']))) {
objBar.createNumber(i18next.t('simulator:sceneObjs.common.curveStepSize') + ' (px)' + ' <sup class="beta-label-sup">Beta</sup>', 0.001, 1, 0.001, this.curveStepSize, function (obj, value) {
obj.curveStepSize = parseFloat(value);
delete obj.path;
delete obj.bezierSegments;
delete obj.bezierSegmentLinearFlags;
}, '<p>' + i18next.t('simulator:sceneObjs.common.eqnInfo.curveStepSize') + '</p>', true);
}
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;
}
this._invalidateCurveIfLengthScaleChanged();
if (isAboveLight) {
if (this.path) {
this.drawGlass(canvasRenderer, isAboveLight, isHovered);
}
return;
}
// Initialize path if needed
if (!this.path) {
if (!this.initPath()) {
// If initialization failed, draw error indicators
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);
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;
}
move(diffX, diffY) {
super.move(diffX, diffY);
// Invalidate path after moving
delete this.path;
delete this.bezierSegments;
delete this.bezierSegmentLinearFlags;
return true;
}
rotate(angle, center = null) {
super.rotate(angle, center);
// Invalidate path after rotating
delete this.path;
delete this.bezierSegments;
delete this.bezierSegmentLinearFlags;
return true;
}
scale(scale, center = null) {
super.scale(scale, center);
// Invalidate path after scaling
delete this.path;
delete this.bezierSegments;
delete this.bezierSegmentLinearFlags;
return true;
}
onConstructMouseDown(mouse, ctrl, shift) {
super.onConstructMouseDown(mouse, ctrl, shift);
// Invalidate path during construction
delete this.path;
delete this.bezierSegments;
delete this.bezierSegmentLinearFlags;
}
onConstructMouseMove(mouse, ctrl, shift) {
super.onConstructMouseMove(mouse, ctrl, shift);
// Invalidate path during construction
delete this.path;
delete this.bezierSegments;
delete this.bezierSegmentLinearFlags;
}
onConstructMouseUp(mouse, ctrl, shift) {
const result = super.onConstructMouseUp(mouse, ctrl, shift);
// Invalidate path after construction
delete this.path;
delete this.bezierSegments;
delete this.bezierSegmentLinearFlags;
return result;
}
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;
}
this._invalidateCurveIfLengthScaleChanged();
// Initialize path if needed
if (!this.path) {
if (!this.initPath()) {
return null;
}
}
if (this.curveType === 'cubicBezier' && this._ensureBezierPathReady()) {
for (let i = 0; i < this.bezierSegments.length; i++) {
const isLinearBoundary = this.bezierSegmentLinearFlags?.[i];
const isHit = isLinearBoundary
? mouse.isOnSegment(geometry.line(this.path[i], this.path[i + 1]))
: mouse.isOnCurve(this.bezierSegments[i]);
if (isHit) {
const mousePos = mouse.getPosSnappedToGrid();
dragContext.part = 0;
dragContext.mousePos0 = mousePos;
dragContext.mousePos1 = mousePos;
dragContext.snapContext = {};
return dragContext;
}
}
} else {
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;
}
}
}
return null;
}
onDrag(mouse, dragContext, ctrl, shift) {
super.onDrag(mouse, dragContext, ctrl, shift);
// Invalidate path after any dragging operation
delete this.path;
delete this.bezierSegments;
delete this.bezierSegmentLinearFlags;
}
checkRayIntersects(ray) {
this._invalidateCurveIfLengthScaleChanged();
// Initialize path if needed
if (!this.path) {
if (!this.initPath()) {
return null;
}
}
if (this.curveType === 'cubicBezier' && this._ensureBezierPathReady()) {
return this.checkRayIntersectsBezier(ray);
}
return this.checkRayIntersectsLinear(ray);
}
checkRayIntersectsLinear(ray) {
var s_lensq = Infinity;
var s_lensq_temp;
var s_point = null;
var s_point_temp = null;
var s_point_index = -1;
for (var i = 0; i < this.path.length; i++) {
s_point_temp = null;
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) {
return null;
} 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;
}
return null;
}
checkRayIntersectsBezier(ray) {
let closest = null;
let closestDistSq = Infinity;
const big = this.scene.lengthScale * 1e9;
const raySeg = geometry.line(ray.p1, geometry.point(
ray.p1.x + (ray.p2.x - ray.p1.x) * big,
ray.p1.y + (ray.p2.y - ray.p1.y) * big
));
for (let i = 0; i < this.bezierSegments.length; i++) {
if (this.bezierSegmentLinearFlags?.[i]) {
const rp = geometry.linesIntersection(geometry.line(ray.p1, ray.p2), geometry.line(this.path[i], this.path[i + 1]));
if (!geometry.intersectionIsOnSegment(rp, geometry.line(this.path[i], this.path[i + 1]))) continue;
if (!geometry.intersectionIsOnRay(rp, ray)) continue;
const distSq = geometry.distanceSquared(ray.p1, rp);
if (distSq <= Simulator.MIN_RAY_SEGMENT_LENGTH_SQUARED * this.scene.lengthScale * this.scene.lengthScale) continue;
if (distSq < closestDistSq) {
closestDistSq = distSq;
closest = rp;
this.tmp_i = i;
this.tmp_bezierU = null;
}
continue;
}
const curve = this.bezierSegments[i];
const us = curve.lineIntersects(raySeg);
for (let k = 0; k < us.length; k++) {
const rp = curve.get(us[k]);
if (!geometry.intersectionIsOnRay(rp, ray)) continue;
const distSq = geometry.distanceSquared(ray.p1, rp);
if (distSq <= Simulator.MIN_RAY_SEGMENT_LENGTH_SQUARED * this.scene.lengthScale * this.scene.lengthScale) continue;
if (distSq < closestDistSq) {
closestDistSq = distSq;
closest = rp;
this.tmp_i = i;
this.tmp_bezierU = us[k];
}
}
}
return closest;
}
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,
isUndefinedBehavior: true
};
}
return this.refract(ray, rayIndex, incidentPoint, incidentData.normal, n1, surfaceMergingObjs, ray.bodyMergingObj);
}
getIncidentData(ray) {
if (this.curveType === 'cubicBezier' && this._ensureBezierPathReady()) {
return this.getIncidentDataBezier(ray);
}
return this.getIncidentDataLinear(ray, this.curveType === 'smoothNormal');
}
getIncidentDataLinear(ray, smoothNormals) {
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 normal_xFinal = normal_x;
var normal_yFinal = normal_y;
if (smoothNormals) {
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);
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 };
}
getIncidentDataBezier(ray) {
const i = this.tmp_i;
if (this.bezierSegmentLinearFlags?.[i]) {
const segP1 = this.path[i];
const segP2 = this.path[i + 1];
const incidentPoint = geometry.linesIntersection(geometry.line(ray.p1, ray.p2), geometry.line(segP1, segP2));
const rayDx = ray.p2.x - ray.p1.x;
const rayDy = ray.p2.y - ray.p1.y;
const segDx = segP2.x - segP1.x;
const segDy = segP2.y - segP1.y;
const rcrosss = rayDx * segDy - rayDy * segDx;
const incidentType = rcrosss < 0 ? 1 : -1;
const rdots = rayDx * segDx + rayDy * segDy;
const ssq = segDx * segDx + segDy * segDy;
const normal_x = rdots * segDx - ssq * rayDx;
const normal_y = rdots * segDy - ssq * rayDy;
return { s_point: incidentPoint, normal: { x: normal_x, y: normal_y }, incidentType: incidentType };
}
const u = this.tmp_bezierU;
const curve = this.bezierSegments?.[i];
if (!curve) {
return { s_point: null, normal: { x: NaN, y: NaN }, incidentType: 0 };
}
const incidentPoint = curve.get(u);
const tangent = curve.derivative(u);
const tx = tangent.x;
const ty = tangent.y;
const rayDx = ray.p2.x - ray.p1.x;
const rayDy = ray.p2.y - ray.p1.y;
const rcrosss = rayDx * ty - rayDy * tx;
const incidentType = rcrosss < 0 ? 1 : -1;
const rdots = rayDx * tx + rayDy * ty;
const ssq = tx * tx + ty * ty;
const normal_x = rdots * tx - ssq * rayDx;
const normal_y = rdots * ty - ssq * rayDy;
return { s_point: incidentPoint, normal: { x: normal_x, y: normal_y }, incidentType: incidentType };
}
getIncidentType(ray) {
return this.getIncidentData(ray).incidentType;
}
/* Utility methods */
_invalidateCurveIfLengthScaleChanged() {
if (this.path && this._curveCacheLengthScale !== this.scene.lengthScale) {
delete this.path;
delete this.bezierSegments;
delete this.bezierSegmentLinearFlags;
delete this._curveCacheLengthScale;
}
}
drawGlass(canvasRenderer, isAboveLight, isHovered) {
const ctx = canvasRenderer.ctx;
ctx.beginPath();
if (this.curveType === 'cubicBezier' && this._ensureBezierPathReady()) {
const p0 = this.bezierSegments[0].get(0);
ctx.moveTo(p0.x, p0.y);
for (let i = 0; i < this.bezierSegments.length; i++) {
if (this.bezierSegmentLinearFlags?.[i]) {
const p1 = this.path[i + 1];
ctx.lineTo(p1.x, p1.y);
continue;
}
const curve = this.bezierSegments[i];
const pts = curve.points;
ctx.bezierCurveTo(pts[1].x, pts[1].y, pts[2].x, pts[2].y, pts[3].x, pts[3].y);
}
} else {
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);
}
_pathPairToHermiteBezier(p1, p2) {
const dt = p2.t - p1.t;
if (Math.abs(dt) < 1e-20) {
return new Bezier([
{ x: p1.x, y: p1.y },
{ x: p1.x, y: p1.y },
{ x: p2.x, y: p2.y },
{ x: p2.x, y: p2.y },
]);
}
return new Bezier([
{ x: p1.x, y: p1.y },
{ x: p1.x + (p1.dxdt * dt) / 3, y: p1.y + (p1.dydt * dt) / 3 },
{ x: p2.x - (p2.dxdt * dt) / 3, y: p2.y - (p2.dydt * dt) / 3 },
{ x: p2.x, y: p2.y },
]);
}
_ensureBezierPathReady() {
if (this.curveType !== 'cubicBezier') return true;
if (!this.path || this.path.length < 2) return false;
if (this.bezierSegments &&
this.bezierSegments.length === this.path.length - 1 &&
this.bezierSegmentLinearFlags &&
this.bezierSegmentLinearFlags.length === this.bezierSegments.length) return true;
this.bezierSegments = [];
this.bezierSegmentLinearFlags = [];
for (let i = 0; i < this.path.length - 1; i++) {
this.bezierSegments.push(this._pathPairToHermiteBezier(this.path[i], this.path[i + 1]));
this.bezierSegmentLinearFlags.push(this.path[i].side !== this.path[i + 1].side);
}
return true;
}
/**
* Initialize the path points based on the equations.
* This method is called by draw() and checkRayIntersects() when needed.
* @returns {boolean} Whether the initialization was successful.
*/
initPath() {
if (!(this.curveStepSize > 1e-6)) {
delete this.path;
this.error = i18next.t('simulator:sceneObjs.ParamCurveObjMixin.error.invalidStepSize', { step: this.curveStepSize });
return false;
}
if (this.p1.x == this.p2.x && this.p1.y == this.p2.y) {
delete this.path;
// this.error = "Invalid glass: endpoints are the same";
return false;
}
var fns;
const wantBezier = this.curveType === 'cubicBezier';
var derFns;
try {
fns = [evaluateLatex(this.eqn1), evaluateLatex(this.eqn2)];
if (wantBezier) {
derFns = [compileEquationDerivative(this.eqn1), compileEquationDerivative(this.eqn2)];
}
} catch (e) {
delete this.path;
delete this.bezierSegments;
delete this.bezierSegmentLinearFlags;
this.error = e.toString();
return false;
}
this.path = [{ x: this.p1.x, y: this.p1.y, side: 0 }];
if (wantBezier) {
const p12d0 = geometry.distance(this.p1, this.p2);
const dir10 = [(this.p2.x - this.p1.x) / p12d0, (this.p2.y - this.p1.y) / p12d0];
const dir20 = [dir10[1], -dir10[0]];
const d0 = derFns[0]({ x: -1 });
this.path[0].t = -1;
this.path[0].dxdt = dir10[0] * (p12d0 * 0.5) + dir20[0] * (p12d0 * 0.5) * d0;
this.path[0].dydt = dir10[1] * (p12d0 * 0.5) + dir20[1] * (p12d0 * 0.5) * d0;
}
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;
var hasCurveGenerationError = false;
const step = this.curveStepSize * this.scene.lengthScale;
const sampleOffset = 0.5 * step;
const sampleEndExtension = 0.9 * step;
for (i = -step; i < p12d + sampleEndExtension; i += step) {
var ix = i + sampleOffset;
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 = i18next.t('simulator:sceneObjs.CustomGlass.curveGenerationError', { x: (-scaled_x) });
hasCurveGenerationError = true;
}
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);
if (wantBezier) {
const dSide = side == 0 ? derFns[0]({ x: scaled_x }) : derFns[1]({ x: -scaled_x });
pt.t = scaled_x;
pt.dxdt = dir1[0] * (p12d * 0.5) + dir2[0] * (p12d * 0.5) * dSide;
pt.dydt = dir1[1] * (p12d * 0.5) + dir2[1] * (p12d * 0.5) * dSide;
}
pt.side = side;
this.path.push(pt);
hasPoints = true;
} catch (e) {
lastError = e;
}
}
if (!hasPoints || hasCurveGenerationError) {
delete this.path;
delete this.bezierSegments;
delete this.bezierSegmentLinearFlags;
this.error = lastError.toString();
return false;
}
}
this.error = null;
delete this.bezierSegments;
delete this.bezierSegmentLinearFlags;
this._curveCacheLengthScale = this.scene.lengthScale;
return true;
}
};
export default CustomGlass;